Skip to content

Commit

Permalink
added support for config snippets
Browse files Browse the repository at this point in the history
  • Loading branch information
hellt committed Jul 12, 2021
1 parent 122a905 commit 0cd3efc
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 40 deletions.
39 changes: 34 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
[![Github all releases](https://img.shields.io/github/downloads/hellt/cmdo/total.svg?style=flat-square&color=424f35&labelColor=bec8d2)](https://github.com/hellt/cmdo/releases/)
---

Commando is a tiny tool that enables users to collect command outputs from a single or a multiple networking devices defined in an inventory file.
Commando is a tiny tool that enables users
* to collect command outputs from a single or a multiple networking devices defined in an inventory file
* send file-based or string-based configs towards the devices defined in the inventory file

all that with zero dependencies and a 1-click installation.

[![asciicast](https://asciinema.org/a/417792.svg)](https://asciinema.org/a/417792)

Expand Down Expand Up @@ -157,13 +161,38 @@ devices:
address: string
credentials: string # optional reference to the defined credentials
transport: string # optional reference to the defined transport options
send-commands-from-file: /path/to/file/with/show-commands.txt
send-commands:
- cmd1
- cmd2
- cmdN
- cmd1
- cmd2
- cmdN
send-configs-from-file: /path/to/file/with/config-commands.txt
send-configs:
- cmd1
- cmdN
```

`send-commands` list holds a list of commands which will be send towards a device. Check out the attached [example inventory](inventory.yml) file to a reference.
`send-commands` list holds a list of non-configuration commands which will be send towards a device. A non configuration command is a command that doesn't require to have a configuration mode enabled on a device. A typical example is a `show <something>` command.
Outputs from each command of a `send-commands` list will be saved/printed.

If you want to keep the commands in a separate file, then you can use `send-commands-from-file` element which takes a path to a said file. You can combine `send-commands` and `send-commands-from-file` in a single device.

In contrast with `send-commands*` options, it is possible to tell `commando` to send configuration commands. For that we have the following configuration elements:

* `send-configs` - takes a list of configuration commands and executes then without printing/saving any output the commands may return
* `send-configs-from-file` - does the same, but the commands are kept in a file.

Entering in the config mode is handled by commando, so your config commands doesn't need to have any `conf t` or `configure private` commands. Just remember to add the `commit` command if your device needs it.

The order these options are processed in:

1. send-configs-from-file
2. send-configs
3. send-commands-from-file
4. send-commands


Check out the attached [example inventory](inventory.yml) file for reference.

## Configuration options

Expand Down
76 changes: 58 additions & 18 deletions commando/cmdo.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,14 @@ type inventory struct {
}

type device struct {
Platform string `yaml:"platform,omitempty"`
Address string `yaml:"address,omitempty"`
Credentials string `yaml:"credentials,omitempty"`
Transport string `yaml:"transport,omitempty"`
SendCommands []string `yaml:"send-commands,omitempty"`
Platform string `yaml:"platform,omitempty"`
Address string `yaml:"address,omitempty"`
Credentials string `yaml:"credentials,omitempty"`
Transport string `yaml:"transport,omitempty"`
SendCommands []string `yaml:"send-commands,omitempty"`
SendCommandsFromFile string `yaml:"send-commands-from-file,omitempty"`
SendConfigs []string `yaml:"send-configs,omitempty"`
SendConfigsFromFile string `yaml:"send-configs-from-file,omitempty"`
}

type credentials struct {
Expand Down Expand Up @@ -110,7 +113,7 @@ func (app *appCfg) run() error {
}

rw := app.newResponseWriter(app.output)
rCh := make(chan *base.MultiResponse)
rCh := make(chan []*base.MultiResponse)

if app.output == fileOutput {
log.SetOutput(os.Stderr)
Expand All @@ -123,8 +126,8 @@ func (app *appCfg) run() error {
for n, d := range i.Devices {
go app.runCommands(n, d, rCh)

resp := <-rCh
go app.outputResult(wg, rw, n, d, resp)
resps := <-rCh
go app.outputResult(wg, rw, n, resps)
}

wg.Wait()
Expand Down Expand Up @@ -243,7 +246,7 @@ func (app *appCfg) loadOptions(d *device) ([]base.Option, error) {
func (app *appCfg) runCommands(
name string,
d *device,
rCh chan<- *base.MultiResponse) {
rCh chan<- []*base.MultiResponse) {
var driver *network.Driver

var err error
Expand Down Expand Up @@ -283,26 +286,63 @@ func (app *appCfg) runCommands(
return
}

r, err := driver.SendCommands(d.SendCommands)
if err != nil {
log.Errorf("failed to send commands to device %s; error: %+v\n", err, name)
rCh <- nil
var responses []*base.MultiResponse

return
// when sending configs we do not print any responses, as typically configs do not produce any output
if d.SendConfigsFromFile != "" {
_, err := driver.SendConfigsFromFile(d.SendConfigsFromFile)
if err != nil {
log.Errorf("failed to send configs to device %s; error: %+v\n", err, name)
rCh <- nil

return
}
}

rCh <- r
if len(d.SendConfigs) != 0 {
_, err := driver.SendConfigs(d.SendConfigs)
if err != nil {
log.Errorf("failed to send configs to device %s; error: %+v\n", err, name)
rCh <- nil

return
}
}

if d.SendCommandsFromFile != "" {
r, err := driver.SendCommandsFromFile(d.SendCommandsFromFile)
if err != nil {
log.Errorf("failed to send commands to device %s; error: %+v\n", err, name)
rCh <- nil

return
}

responses = append(responses, r)
}

if len(d.SendCommands) != 0 {
r, err := driver.SendCommands(d.SendCommands)
if err != nil {
log.Errorf("failed to send commands to device %s; error: %+v\n", err, name)
rCh <- nil

return
}

responses = append(responses, r)
}
rCh <- responses
}

func (app *appCfg) outputResult(
wg *sync.WaitGroup,
rw responseWriter,
name string,
d *device,
r *base.MultiResponse) {
r []*base.MultiResponse) {
defer wg.Done()

if err := rw.WriteResponse(r, name, d); err != nil {
if err := rw.WriteResponse(r, name); err != nil {
log.Errorf("error while writing the response: %v", err)
}
}
Expand Down
38 changes: 21 additions & 17 deletions commando/respwriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const (
)

type responseWriter interface {
WriteResponse(r *base.MultiResponse, name string, d *device) error
WriteResponse(r []*base.MultiResponse, name string) error
}

func (app *appCfg) newResponseWriter(f string) responseWriter {
Expand Down Expand Up @@ -50,49 +50,53 @@ func (w *consoleWriter) writeFailure(name string) error {
return nil
}

func (w *consoleWriter) writeSuccess(r *base.MultiResponse, name string, d *device) error {
func (w *consoleWriter) writeSuccess(r []*base.MultiResponse, name string) error {
c := color.New(color.FgGreen)
c.Fprintf(os.Stderr, "\n**************************\n%s\n**************************\n", name)

for idx, cmd := range d.SendCommands {
c := color.New(color.Bold)
c.Fprintf(os.Stderr, "\n-- %s:\n", cmd)
for _, mr := range r {
for _, resp := range mr.Responses {
c := color.New(color.Bold)
c.Fprintf(os.Stderr, "\n-- %s:\n", resp.ChannelInput)

if r.Responses[idx].Failed {
color.Set(color.FgRed)
}
if resp.Failed {
color.Set(color.FgRed)
}

fmt.Println(r.Responses[idx].Result)
fmt.Println(resp.Result)
}
}

return nil
}

func (w *consoleWriter) WriteResponse(r *base.MultiResponse, name string, d *device) error {
func (w *consoleWriter) WriteResponse(r []*base.MultiResponse, name string) error {
if r == nil {
return w.writeFailure(name)
}

return w.writeSuccess(r, name, d)
return w.writeSuccess(r, name)
}

// fileWriter writes the scrapli responses to the files on disk.
type fileWriter struct {
dir string // output dir name
}

func (w *fileWriter) WriteResponse(r *base.MultiResponse, name string, d *device) error {
func (w *fileWriter) WriteResponse(r []*base.MultiResponse, name string) error {
outDir := path.Join(w.dir, name)
if err := os.MkdirAll(outDir, filePermissions); err != nil {
return err
}

for idx, cmd := range d.SendCommands {
c := sanitizeCmd(cmd)
for _, mr := range r {
for _, resp := range mr.Responses {
c := sanitizeCmd(resp.ChannelInput)

rb := []byte(r.Responses[idx].Result)
if err := ioutil.WriteFile(path.Join(outDir, c), rb, filePermissions); err != nil {
return err
rb := []byte(resp.Result)
if err := ioutil.WriteFile(path.Join(outDir, c), rb, filePermissions); err != nil {
return err
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions inventory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ devices:
address: clab-scrapli-ceos
credentials: eos
transport: eos
send-commands-from-file: somefile.txt
send-commands:
- show version
- show uptime
srlinux:
platform: nokia_srlinux
address: clab-scrapli-srlinux
send-commands:
- /system information location "commando"
- commit now
send-commands:
- show version
- show network-instance interfaces

0 comments on commit 0cd3efc

Please sign in to comment.