Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

json output for exec command #465

Merged
merged 7 commits into from
Jun 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 59 additions & 17 deletions cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,45 @@ package cmd

import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"

"github.com/google/shlex"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/srl-labs/containerlab/clab"
"github.com/srl-labs/containerlab/types"
)

var labels []string
var (
labels []string
execFormat string
execCommand string
)

// execCmd represents the exec command
var execCmd = &cobra.Command{
Use: "exec",
Short: "execute a command on one or multiple containers",
PreRunE: sudoCheck,
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if name == "" && topo == "" {
fmt.Println("provide either lab name (--name) or topology file path (--topo)")
return
return errors.New("provide either lab name (--name) or topology file path (--topo)")

}

if execCommand == "" {
return errors.New("provide command to execute")
}
log.Debugf("raw command: %v", args)
if len(args) == 0 {
fmt.Println("provide command to execute")
return

switch execFormat {
case "json",
"plain":
// expected values, go on
default:
log.Error("format is expected to be either json or plain")
}
opts := []clab.ClabOption{
clab.WithDebug(debug),
Expand All @@ -52,14 +66,18 @@ var execCmd = &cobra.Command{
log.Fatalf("could not list containers: %v", err)
}
if len(containers) == 0 {
log.Println("no containers found")
return
return errors.New("no containers found")
}
cmds := make([]string, 0, len(args))
for _, a := range args {
cmds = append(cmds, strings.Split(a, " ")...)

cmds, err := shlex.Split(execCommand)
if err != nil {
return err
}

jsonResult := make(map[string]map[string]interface{})

for _, cont := range containers {
var doc interface{}
if cont.State != "running" {
continue
}
Expand All @@ -68,17 +86,41 @@ var execCmd = &cobra.Command{
log.Errorf("%s: failed to execute cmd: %v", cont.Names, err)
continue
}
if len(stdout) > 0 {
log.Infof("%s: stdout:\n%s", cont.Names, string(stdout))
contName := strings.TrimLeft(cont.Names[0], "/")
switch execFormat {
case "json":
jsonResult[contName] = make(map[string]interface{})
err := json.Unmarshal([]byte(stdout), &doc)
if err == nil {
jsonResult[contName]["stdout"] = doc
} else {
jsonResult[contName]["stdout"] = string(stdout)
}
jsonResult[contName]["stderr"] = string(stderr)
case "plain":
if len(stdout) > 0 {
log.Infof("%s: stdout:\n%s", contName, string(stdout))
}
if len(stderr) > 0 {
log.Infof("%s: stderr:\n%s", contName, string(stderr))
}

}
if len(stderr) > 0 {
log.Infof("%s: stderr:\n%s", cont.Names, string(stderr))
}
if execFormat == "json" {
result, err := json.Marshal(jsonResult)
if err != nil {
log.Errorf("Issue converting to json %v", err)
}
fmt.Println(string(result))
}
return err
},
}

func init() {
rootCmd.AddCommand(execCmd)
execCmd.Flags().StringVarP(&execCommand, "cmd", "", "", "command to execute")
execCmd.Flags().StringSliceVarP(&labels, "label", "", []string{}, "labels to filter container subset")
execCmd.Flags().StringVarP(&execFormat, "format", "f", "plain", "output format. One of [json, plain]")
}
118 changes: 118 additions & 0 deletions docs/cmd/exec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# exec command

### Description

The `exec` command allows to run a command inside the nodes that part of a certain lab.

This command does exactly the same thing as `docker exec` does, but it allows to run the same command across all the nodes of a lab.

### Usage

`containerlab [global-flags] exec [local-flags]`

### Flags

#### topology

With the global `--topo | -t` flag a user specifies from which lab to take the containers and perform the exec command.

#### cmd
The command to be executed on the nodes is provided with `--cmd` flag. The command is provided as a string, thus it needs to be quoted to accommodate for spaces or special characters.

#### format
The `--format | -f` flag allows to select between plain text format output or a json variant. Consult with the examples below to see the differences between these two formatting options.

Defaults to `plain` output format.

#### label
By default `exec` command will attempt to execute the command across all the nodes of a lab. To limit the scope of the execution, the users can leverage the `--label` flag to filter out the nodes of interest.

### Examples

```bash
# show ipv4 information from all the nodes of the lab
# with a plain text output
❯ containerlab exec -t srl02.yml --cmd 'ip -4 a show dummy-mgmt0'
INFO[0000] clab-srl02-srl1: stdout:
6: dummy-mgmt0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 1000
inet 172.20.20.3/24 brd 172.20.20.255 scope global dummy-mgmt0
valid_lft forever preferred_lft forever
INFO[0000] clab-srl02-srl2: stdout:
6: dummy-mgmt0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 1000
inet 172.20.20.2/24 brd 172.20.20.255 scope global dummy-mgmt0
valid_lft forever preferred_lft forever


# execute a CLI command with a plain text output
❯ containerlab exec -t srl02.yml --cmd 'sr_cli "show version"'
INFO[0001] clab-srl02-srl1: stdout:
----------------------------------------------------
Hostname : srl1
Chassis Type : 7250 IXR-6
Part Number : Sim Part No.
Serial Number : Sim Serial No.
System MAC Address: 02:00:6B:FF:00:00
Software Version : v20.6.3
Build Number : 145-g93496a3f8c
Architecture : x86_64
Last Booted : 2021-06-24T10:25:26.722Z
Total Memory : 24052875 kB
Free Memory : 21911906 kB
----------------------------------------------------
INFO[0003] clab-srl02-srl2: stdout:
----------------------------------------------------
Hostname : srl2
Chassis Type : 7250 IXR-6
Part Number : Sim Part No.
Serial Number : Sim Serial No.
System MAC Address: 02:D8:A9:FF:00:00
Software Version : v20.6.3
Build Number : 145-g93496a3f8c
Architecture : x86_64
Last Booted : 2021-06-24T10:25:26.904Z
Total Memory : 24052875 kB
Free Memory : 21911914 kB
----------------------------------------------------


# execute a CLI command with a json output
❯ containerlab exec -t srl02.yml --cmd 'sr_cli "show version | as json"' -f json | jq
{
"clab-srl02-srl1": {
"stderr": "",
"stdout": {
"basic system info": {
"Architecture": "x86_64",
"Build Number": "145-g93496a3f8c",
"Chassis Type": "7250 IXR-6",
"Free Memory": "21911367 kB",
"Hostname": "srl1",
"Last Booted": "2021-06-24T10:25:26.722Z",
"Part Number": "Sim Part No.",
"Serial Number": "Sim Serial No.",
"Software Version": "v20.6.3",
"System MAC Address": "02:00:6B:FF:00:00",
"Total Memory": "24052875 kB"
}
}
},
"clab-srl02-srl2": {
"stderr": "",
"stdout": {
"basic system info": {
"Architecture": "x86_64",
"Build Number": "145-g93496a3f8c",
"Chassis Type": "7250 IXR-6",
"Free Memory": "21911367 kB",
"Hostname": "srl2",
"Last Booted": "2021-06-24T10:25:26.904Z",
"Part Number": "Sim Part No.",
"Serial Number": "Sim Serial No.",
"Software Version": "v20.6.3",
"System MAC Address": "02:D8:A9:FF:00:00",
"Total Memory": "24052875 kB"
}
}
}
}
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ nav:
- destroy: cmd/destroy.md
- inspect: cmd/inspect.md
- save: cmd/save.md
- exec: cmd/exec.md
- generate: cmd/generate.md
- graph: cmd/graph.md
- tools:
Expand Down
2 changes: 1 addition & 1 deletion tests/01-smoke/03-bridges-and-host.robot
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Verify management network is using user-specified bridge
# show management interface info and cut the information about the ifindex of the remote veth
# note that exec returns the info in the stderr stream, thus we use stderr to parse the ifindex
${rc} ${iface} = OperatingSystem.Run And Return Rc And Output
... sudo containerlab --runtime ${runtime} exec -t ${CURDIR}/${lab-file} --label clab-node-name\=l1 ip l show eth0 2>&1 | cut -d ' ' -f5 | cut -d '@' -f2 | cut -c3-
... sudo containerlab --runtime ${runtime} exec -t ${CURDIR}/${lab-file} --label clab-node-name\=l1 --cmd "ip l show eth0" 2>&1 | cut -d ' ' -f5 | cut -d '@' -f2 | cut -c3-
Log ${iface}
Should Be Equal As Integers ${rc} 0
${rc} ${res} = OperatingSystem.Run And Return Rc And Output
Expand Down