Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
KennethGrace committed Jun 17, 2022
0 parents commit 9d6bec2
Show file tree
Hide file tree
Showing 12 changed files with 371 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tests/secrets
20 changes: 20 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2022 Dyntek Services Inc.

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# <img src="favicon.png" height="64"/> GoConfigure

> A simple SSH configuration deployment tool for the command-line.
# Operation

GoConfigure can be run at the command-line with `goconfigure`. Running `goconfigure` without
any arguments will launch an interactive session where target devices and commands can be
entered manually. Interactive mode does **not** support configuration templating.

Optional arguments include `-i inventory_filename` and `-t template_filename`. The template
should be defined in a plain-text document and the inventory filename should be defined in a
YAML formatted document according to the following schema;
```yaml
---
server-1:
hostname: s1.yourdomain.com
username: username
password: password
data:
ip_address: 192.168.0.1
server-2:
hostname: s2.yourdomain.com
username: username
password: password
```
*Note that `data` is an optional field*. The fields defined in `data` will be available to the
template during rendering. Fields required by the template must be defined in `data`. Field names
should be uppercase to be accessible from within the template.
Binary file added favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/dyntek-services-inc/goconfigure

go 1.18

require (
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
32 changes: 32 additions & 0 deletions inventory/inventory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package inventory

import (
"gopkg.in/yaml.v3"
"io"
"os"
)

type Inventory map[string]Host

type Host struct {
Hostname string `yaml:"hostname"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Data map[string]interface{} `yaml:"data"`
}

func LoadInventory(filename string) (Inventory, error) {
inv := Inventory{}
yFile, err := os.Open(filename)
if err != nil {
return nil, err
}
yContent, err := io.ReadAll(yFile)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(yContent, &inv); err != nil {
return nil, err
}
return inv, nil
}
92 changes: 92 additions & 0 deletions main/goconfigure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// GoConfigure is an application for templating configurations meant to be pushed via SSH.
// It can be consumed as a command line tool. Passing arguments of the form `goconfigure
// -t TEMPLATE_FILE_NAME -i DATA_FILE_NAME` will render and push the render to the devices
// defined in the inventory file.
package main

import (
"flag"
"github.com/dyntek-services-inc/goconfigure/inventory"
"github.com/dyntek-services-inc/goconfigure/render"
"github.com/dyntek-services-inc/goconfigure/ssh"
"log"
"os"
"path/filepath"
"strings"
)

var pwd, pwdErr = os.Getwd()

func deploy(inventory inventory.Inventory, tplString string) error {
rc := make(map[string]chan []string, len(inventory)) // The response channels.
for name, host := range inventory {
log.Printf("starting deployment for %s", host.Hostname)
rc[name] = make(chan []string)
handler, err := ssh.Connect(host)
log.Printf("finished connecting too %s", host.Hostname)
if err != nil {
return err
}
go func(ro chan []string, hdlr *ssh.Handler) {
rtplc := render.RenderCommands(hdlr.GetHost().Data, tplString)
cc := make([]chan string, len(rtplc))
for i, c := range rtplc {
cc[i] = make(chan string)
go func(co chan string, ci string) {
r, err := hdlr.Send(ci)
if err != nil {
log.Fatal(err)
}
co <- r
}(cc[i], c)
}
cco := make([]string, len(rtplc))
for i, co := range cc {
cco[i] = <-co
}
log.Printf("finished deployment of %s", hdlr.GetHost().Hostname)
ro <- cco
}(rc[name], handler)
}
for name, ro := range rc {
if pwdErr != nil {
return pwdErr
}
rro := <-ro
tr := strings.Join(rro, "\n")
of := filepath.Join(pwd, name)
log.Printf("writing output to %s.txt", of)
if err := os.WriteFile(of+".txt", []byte(tr), 0666); err != nil {
return err
}
}
return nil
}

func main() {
invFilename := flag.String("i", "", "inventory filename")
tplFilename := flag.String("t", "", "template filename")
flag.Parse()
if len(*invFilename) == 0 && len(*tplFilename) == 0 {
// No inventory or template flags were passed, start manual mode
// TODO: implement manual mode
} else {
if len(*invFilename) == 0 || len(*tplFilename) == 0 {
// One of the flags was passed but not the other
log.Fatal("one flag was passed, but not both")
} else {
// Both flags were passed, begin deployment
inv, err := inventory.LoadInventory(*invFilename)
if err != nil {
log.Fatal(err)
}
tplString, err := render.FileToString(*tplFilename)
if err != nil {
log.Fatal(err)
}
if err := deploy(inv, tplString); err != nil {
log.Fatal(err)
}
}
}
}
24 changes: 24 additions & 0 deletions render/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package render

import (
"bytes"
"os"
"strings"
"text/template"
)

// FileToString will accept a render file and return the render as a string.
func FileToString(tplFilename string) (string, error) {
readBytes, err := os.ReadFile(tplFilename)
if err != nil {
return "", err
}
return string(readBytes), nil
}

func RenderCommands(data map[string]interface{}, tplString string) []string {
tpl := template.Must(template.New("").Parse(tplString))
var tplBuffer bytes.Buffer
tpl.Execute(&tplBuffer, data)
return strings.Split(tplBuffer.String(), "\n")
}
49 changes: 49 additions & 0 deletions ssh/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package ssh

import (
"bytes"
"github.com/dyntek-services-inc/goconfigure/inventory"
"golang.org/x/crypto/ssh"
)

type Handler struct {
host inventory.Host
client *ssh.Client
}

func Connect(host inventory.Host) (*Handler, error) {
config := &ssh.ClientConfig{
User: host.Username,
Auth: []ssh.AuthMethod{
ssh.Password(host.Password),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", host.Hostname+":22", config)
if err != nil {
return nil, err
}
return &Handler{client: client, host: host}, nil
}

func (h Handler) GetHost() inventory.Host {
return h.host
}

// Send opens a new session to the SSH server and sends the passed string.
// The standard output from the server is returned.
func (h Handler) Send(command string) (string, error) {
session, err := h.client.NewSession()
if err != nil {
return "", err
}
defer session.Close()
var outBuffer bytes.Buffer
session.Stdout = &outBuffer
session.Run(command)
return outBuffer.String(), nil
}

func (h Handler) Close() error {
return h.client.Close()
}
106 changes: 106 additions & 0 deletions tests/Handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package tests

import (
"errors"
"fmt"
"github.com/dyntek-services-inc/goconfigure/inventory"
"github.com/dyntek-services-inc/goconfigure/render"
"github.com/dyntek-services-inc/goconfigure/ssh"
"strings"
"testing"
)

func TestConnectHandler(t *testing.T) {
inv, err := inventory.LoadInventory("secrets/hosts.yml")
if err != nil {
panic(err)
}
// Connect to Hosts
var handlers []*ssh.Handler
for name, host := range inv {
t.Logf("Connectiong to %s", name)
h, err := ssh.Connect(host)
handlers = append(handlers, h)
if err != nil {
panic(err)
}
}
// Cleanup
for _, h := range handlers {
h.Close()
}
}

func TestSendHandler(t *testing.T) {
inv, err := inventory.LoadInventory("secrets/hosts.yml")
if err != nil {
panic(err)
}
// Connect to Hosts
var handlers []*ssh.Handler
for name, host := range inv {
t.Logf("Connectiong to %s", name)
h, err := ssh.Connect(host)
handlers = append(handlers, h)
if err != nil {
panic(err)
}
}
// Send Command to Host
for _, h := range handlers {
response, err := h.Send("echo \"hello world!\"")
if err != nil {
panic(err)
}
response = strings.TrimSpace(response)
if response != "hello world!" {
panic(errors.New(fmt.Sprintf("response %s not equal to %s", response, "hello world!")))
} else {
t.Logf("response %s succesfully matches %s", response, "hello world!")
}
}
// Cleanup
for _, h := range handlers {
h.Close()
}
}

func TestMultiSendHandler(t *testing.T) {
inv, err := inventory.LoadInventory("secrets/hosts.yml")
tplString, err := render.FileToString("secrets/example.txt")
if err != nil {
panic(err)
}
// Connect to Hosts and Render Template
var handlers []*ssh.Handler
var tpls [][]string
for name, host := range inv {
t.Logf("Connectiong to %s", name)
h, err := ssh.Connect(host)
if err != nil {
panic(err)
}
handlers = append(handlers, h)
tpls = append(tpls, render.RenderCommands(host.Data, tplString))
}
// Send Commands to Host
for i, h := range handlers {
for _, command := range tpls[i] {
response, err := h.Send(command)
if err != nil {
panic(err)
}
response = strings.TrimSpace(response)
fmt.Println(response)
}
//if response != "hello world!" {
// panic(errors.New(fmt.Sprintf("response %s not equal to %s", response, "hello world!")))
//} else {
// t.Logf("response %s succesfully matches %s", response, "hello world!")
//}
}
// Cleanup
for _, h := range handlers {
h.Close()
}
}
1 change: 1 addition & 0 deletions tests/deploy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package tests

0 comments on commit 9d6bec2

Please # to comment.