diff --git a/cmd/inspect.go b/cmd/inspect.go index ba86ffc37..e31454916 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -12,8 +12,10 @@ import ( "path/filepath" "slices" "sort" + "strings" - "github.com/olekukonko/tablewriter" + tableWriter "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/srl-labs/containerlab/clab" @@ -130,12 +132,12 @@ func inspectFn(_ *cobra.Command, _ []string) error { return err } -func toTableData(det []types.ContainerDetails) [][]string { - tabData := make([][]string, 0, len(det)) - for i := range det { - d := &det[i] +func toTableData(contDetails []types.ContainerDetails) []tableWriter.Row { + tabData := make([]tableWriter.Row, 0, len(contDetails)) + for i := range contDetails { + d := &contDetails[i] - tabRow := []string{fmt.Sprintf("%d", i+1)} + tabRow := tableWriter.Row{} if all { tabRow = append(tabRow, d.LabPath, d.LabName) @@ -147,22 +149,54 @@ func toTableData(det []types.ContainerDetails) [][]string { } // Common fields - tabRow = append(tabRow, d.Name, d.ContainerID, d.Image, d.Kind, d.State, d.IPv4Address, d.IPv6Address) + tabRow = append(tabRow, + d.Name, + fmt.Sprintf("%s\n%s", d.Kind, d.Image), + d.State, + fmt.Sprintf("%s\n%s", + ipWithoutPrefix(d.IPv4Address), + ipWithoutPrefix(d.IPv6Address))) tabData = append(tabData, tabRow) } return tabData } +// getTopologyPath returns the relative path to the topology file +// if the relative path is shorted than the absolute path. +func getTopologyPath(p string) (string, error) { + if p == "" { + return "", nil + } + + // get topo file path relative of the cwd + cwd, err := os.Getwd() + if err != nil { + return "", err + } + + relPath, err := filepath.Rel(cwd, p) + if err != nil { + return "", err + } + + if len(relPath) < len(p) { + return relPath, nil + } + + return p, nil +} + func printContainerInspect(containers []runtime.GenericContainer, format string) error { contDetails := make([]types.ContainerDetails, 0, len(containers)) // Gather details of each container for _, cont := range containers { - // get topo file path relative of the cwd - cwd, _ := os.Getwd() - path, _ := filepath.Rel(cwd, cont.Labels[labels.TopoFile]) + path, err := getTopologyPath(cont.Labels[labels.TopoFile]) + if err != nil { + return fmt.Errorf("failed to get topology path: %v", err) + } cdet := &types.ContainerDetails{ LabName: cont.Labels[labels.Containerlab], @@ -213,35 +247,46 @@ func printContainerInspect(containers []runtime.GenericContainer, format string) case "table": tabData := toTableData(contDetails) - table := tablewriter.NewWriter(os.Stdout) - header := []string{ + table := tableWriter.NewWriter() + table.SetOutputMirror(os.Stdout) + table.SetStyle(tableWriter.StyleRounded) + table.Style().Format.Header = text.FormatTitle + table.Style().Format.HeaderAlign = text.AlignCenter + table.Style().Options.SeparateRows = true + table.Style().Color = tableWriter.ColorOptions{ + Header: text.Colors{text.Bold}, + } + + header := tableWriter.Row{ "Lab Name", "Name", - "Container ID", - "Image", - "Kind", + "Kind/Image", "State", - "IPv4 Address", - "IPv6 Address", - } + "IPv4/6 Address"} if wide { header = slices.Insert(header, 1, "Owner") } if all { - table.SetHeader(append([]string{"#", "Topo Path"}, header...)) + // merge cells with topo file path and lab name when in all mode + table.SetColumnConfigs([]tableWriter.ColumnConfig{ + {Number: 1, AutoMerge: true}, + {Number: 2, AutoMerge: true}, + }) + table.AppendHeader(append(tableWriter.Row{"Topology"}, header...)) } else { - table.SetHeader(append([]string{"#"}, header[1:]...)) + table.AppendHeader(append(tableWriter.Row{}, header[1:]...)) } - table.SetAutoFormatHeaders(false) - table.SetAutoWrapText(false) - // merge cells with lab name and topo file path - table.SetAutoMergeCellsByColumnIndex([]int{1, 2}) + if wide { - table.SetAutoMergeCellsByColumnIndex([]int{1, 2, 3}) + table.SetColumnConfigs([]tableWriter.ColumnConfig{ + {Number: 2, AutoMerge: true}, + }) } - table.AppendBulk(tabData) + + table.AppendRows(tabData) + table.Render() return nil @@ -253,3 +298,16 @@ type TokenFileResults struct { File string Labname string } + +func ipWithoutPrefix(ip string) string { + if strings.Contains(ip, "N/A") { + return ip + } + + ipParts := strings.Split(ip, "/") + if len(ipParts) != 2 { + return ip + } + + return ipParts[0] +} diff --git a/cmd/tools_netem.go b/cmd/tools_netem.go index bc1bbe119..914b2a2b5 100644 --- a/cmd/tools_netem.go +++ b/cmd/tools_netem.go @@ -15,7 +15,8 @@ import ( "github.com/containernetworking/plugins/pkg/ns" gotc "github.com/florianl/go-tc" - "github.com/olekukonko/tablewriter" + tableWriter "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/srl-labs/containerlab/clab" @@ -164,8 +165,16 @@ func validateInput(_ *cobra.Command, _ []string) error { } func printImpairments(qdiscs []gotc.Object) { - table := tablewriter.NewWriter(os.Stdout) - header := []string{ + table := tableWriter.NewWriter() + table.SetOutputMirror(os.Stdout) + table.SetStyle(tableWriter.StyleRounded) + table.Style().Format.Header = text.FormatTitle + table.Style().Format.HeaderAlign = text.AlignCenter + table.Style().Color = tableWriter.ColorOptions{ + Header: text.Colors{text.Bold}, + } + + header := tableWriter.Row{ "Interface", "Delay", "Jitter", @@ -174,21 +183,19 @@ func printImpairments(qdiscs []gotc.Object) { "Corruption", } - table.SetHeader(header) - table.SetAutoFormatHeaders(false) - table.SetAutoWrapText(false) + table.AppendHeader(header) - var rows [][]string + var rows []tableWriter.Row for _, qdisc := range qdiscs { rows = append(rows, qdiscToTableData(qdisc)) } - table.AppendBulk(rows) + table.AppendRows(rows) table.Render() } -func qdiscToTableData(qdisc gotc.Object) []string { +func qdiscToTableData(qdisc gotc.Object) tableWriter.Row { link, err := netlink.LinkByIndex(int(qdisc.Ifindex)) if err != nil { log.Errorf("could not get netlink interface by index: %v", err) @@ -204,7 +211,7 @@ func qdiscToTableData(qdisc gotc.Object) []string { // return N/A values when netem is not set // which is the case when qdisc is not set for an interface if qdisc.Netem == nil { - return []string{ + return tableWriter.Row{ ifDisplayName, "N/A", // delay "N/A", // jitter @@ -226,7 +233,7 @@ func qdiscToTableData(qdisc gotc.Object) []string { rate = strconv.Itoa(int(qdisc.Netem.Rate.Rate * 8 / 1000)) corruption = strconv.FormatFloat(float64(qdisc.Netem.Corrupt.Probability)/float64(math.MaxUint32)*100, 'f', 2, 64) + "%" - return []string{ + return tableWriter.Row{ ifDisplayName, delay, jitter, diff --git a/go.mod b/go.mod index c1ae202ff..04236f3e2 100644 --- a/go.mod +++ b/go.mod @@ -24,13 +24,13 @@ require ( github.com/h2non/gock v1.2.0 github.com/hairyhenderson/gomplate/v3 v3.11.8 github.com/hashicorp/go-version v1.7.0 + github.com/jedib0t/go-pretty/v6 v6.6.2-0.20241117161227-ada750040aae github.com/joho/godotenv v1.5.1 github.com/jsimonetti/rtnetlink v1.4.2 github.com/kellerza/template v0.0.6 github.com/klauspost/cpuid/v2 v2.2.9 github.com/mackerelio/go-osstat v0.2.5 github.com/mitchellh/go-homedir v1.1.0 - github.com/olekukonko/tablewriter v0.0.5 github.com/opencontainers/runtime-spec v1.2.0 github.com/pkg/errors v0.9.1 github.com/pmorjan/kmod v1.1.1 diff --git a/go.sum b/go.sum index c50c91719..ccfc6da2d 100644 --- a/go.sum +++ b/go.sum @@ -751,6 +751,10 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jedib0t/go-pretty/v6 v6.6.1 h1:iJ65Xjb680rHcikRj6DSIbzCex2huitmc7bDtxYVWyc= +github.com/jedib0t/go-pretty/v6 v6.6.1/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/jedib0t/go-pretty/v6 v6.6.2-0.20241117161227-ada750040aae h1:OwQkOZM+eaPuzQAe7ELAhJYGGUxEi/XZYfcC+AjnhIQ= +github.com/jedib0t/go-pretty/v6 v6.6.2-0.20241117161227-ada750040aae/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= @@ -876,7 +880,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= @@ -973,8 +976,6 @@ github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JX github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/runtime/docker/firewall/definitions/definitions.go b/runtime/docker/firewall/definitions/definitions.go index c83d44235..0e99ba480 100644 --- a/runtime/docker/firewall/definitions/definitions.go +++ b/runtime/docker/firewall/definitions/definitions.go @@ -2,7 +2,7 @@ package definitions import "errors" -var ErrNotAvailabel = errors.New("not available") +var ErrNotAvailable = errors.New("not available") const ( DockerFWUserChain = "DOCKER-USER" diff --git a/runtime/docker/firewall/iptables/client.go b/runtime/docker/firewall/iptables/client.go index cf5d78685..8af5d29e0 100644 --- a/runtime/docker/firewall/iptables/client.go +++ b/runtime/docker/firewall/iptables/client.go @@ -34,7 +34,7 @@ func NewIpTablesClient(bridgeName string) (*IpTablesClient, error) { if !loaded { log.Debug("ip_tables kernel module not available") // module is not loaded - return nil, definitions.ErrNotAvailabel + return nil, definitions.ErrNotAvailable } return &IpTablesClient{ diff --git a/runtime/docker/firewall/nftables/client.go b/runtime/docker/firewall/nftables/client.go index 0d3e1c38f..eb61d54ff 100644 --- a/runtime/docker/firewall/nftables/client.go +++ b/runtime/docker/firewall/nftables/client.go @@ -23,17 +23,6 @@ type NftablesClient struct { // NewNftablesClient returns a new NftablesClient. func NewNftablesClient(bridgeName string) (*NftablesClient, error) { - loaded, err := utils.IsKernelModuleLoaded("nf_tables") - if err != nil { - return nil, err - } - - if !loaded { - log.Debug("nf_tables kernel module not available") - // module is not loaded - return nil, definitions.ErrNotAvailabel - } - // setup netlink connection with nftables nftConn, err := nftables.New(nftables.AsLasting()) if err != nil { @@ -51,7 +40,7 @@ func NewNftablesClient(bridgeName string) (*NftablesClient, error) { } if len(chains) == 0 { log.Debugf("nftables does not seem to be in use, no %s chain found.", definitions.DockerFWUserChain) - return nil, definitions.ErrNotAvailabel + return nil, definitions.ErrNotAvailable } return nftC, nil diff --git a/tests/01-smoke/01-basic-flow.robot b/tests/01-smoke/01-basic-flow.robot index bc610686a..eeccc2254 100644 --- a/tests/01-smoke/01-basic-flow.robot +++ b/tests/01-smoke/01-basic-flow.robot @@ -16,7 +16,7 @@ ${runtime} docker ${runtime-cli-exec-cmd} sudo docker exec ${n2-ipv4} 172.20.20.100/24 ${n2-ipv6} 3fff:172:20:20::100/64 - +${table-delimit} │ *** Test Cases *** Verify number of Hosts entries before deploy @@ -190,17 +190,30 @@ Ensure "inspect all" outputs IP addresses ... sudo -E ${CLAB_BIN} --runtime ${runtime} inspect --all Log ${output} Should Be Equal As Integers ${rc} 0 - # get a 3rd line from the bottom of the inspect cmd. - # this relates to the l2 node - ${line} = String.Get Line ${output} -3 + + # get a 4th line from the bottom of the inspect cmd. + # this relates to the l2 node ipv4 + ${line} = String.Get Line ${output} -6 Log ${line} - @{data} = Split String ${line} | + + @{data} = Split String ${line} ${table-delimit} Log ${data} + # verify ipv4 address - ${ipv4} = String.Strip String ${data}[9] - Should Match Regexp ${ipv4} ^[\\d\\.]+/\\d{1,2}$ + ${ipv4} = String.Strip String ${data}[6] + Should Match Regexp ${ipv4} ^[\\d\\.]+$ + + # get a 3rd line from the bottom of the inspect cmd. + # this relates to the l2 node ipv6 + ${line} = String.Get Line ${output} -5 + Log ${line} + + @{data} = Split String ${line} ${table-delimit} + Log ${data} + # verify ipv6 address - Run Keyword Match IPv6 Address ${data}[10] + ${ipv6} = String.Strip String ${data}[6] + Run Keyword Match IPv6 Address ${ipv6} Verify bind mount in l1 node ${rc} ${output} = Run And Return Rc And Output @@ -334,7 +347,7 @@ Verify Exec rc != 0 on no containers match Verify l1 node is healthy [Documentation] Checking if l1 node is healthy after the lab is deployed - Sleep 3s + Sleep 10s ${output} = Process.Run Process ... sudo ${runtime} inspect clab-${lab-name}-l1 -f ''{{.State.Health.Status}}'' @@ -380,4 +393,4 @@ Match IPv6 Address [Arguments] ... ${address}=${None} ${ipv6} = String.Strip String ${address} - Should Match Regexp ${ipv6} ^[\\d:abcdef]+/\\d{1,2}$ + Should Match Regexp ${ipv6} ^[\\d:abcdef]+$ diff --git a/tests/01-smoke/02-destroy-all.robot b/tests/01-smoke/02-destroy-all.robot index 0f639a643..3e64009cd 100644 --- a/tests/01-smoke/02-destroy-all.robot +++ b/tests/01-smoke/02-destroy-all.robot @@ -47,34 +47,35 @@ Deploy second lab Inspect ${lab2-name} lab using its name ${rc} ${output} = Run And Return Rc And Output ... sudo -E ${CLAB_BIN} --runtime ${runtime} inspect --name ${lab2-name} - Log ${output} + Log \n--> LOG: Inspect output\n${output} console=True Should Be Equal As Integers ${rc} 0 ${num_lines} = Run bash -c "echo '${output}' | wc -l" - # lab2 only has 1 nodes and therefore inspect output should contain only 1 node (+4 lines for the table header and footer) - Should Be Equal As Integers ${num_lines} 5 + # lab2 only has 1 nodes and therefore inspect output should contain only 1 node with two lines + # (+4 lines for the table header and footer) + Should Be Equal As Integers ${num_lines} 6 Inspect ${lab2-name} lab using topology file reference ${result} = Run Process ... sudo -E ${CLAB_BIN} --runtime ${runtime} inspect -t ${orig_dir}/${lab2-file} ... shell=True - Log ${result.stdout} + Log \n--> LOG: Inspect output\n${result.stdout} console=True Log ${result.stderr} Should Be Equal As Integers ${result.rc} 0 ${num_lines} = Run bash -c "echo '${result.stdout}' | wc -l" # lab2 only has 1 nodes and therefore inspect output should contain only 1 node - Should Be Equal As Integers ${num_lines} 5 + Should Be Equal As Integers ${num_lines} 6 Inspect all ${rc} ${output} = Run And Return Rc And Output ... sudo -E ${CLAB_BIN} --runtime ${runtime} inspect --all - Log ${output} + Log \n--> LOG: Inspect all output\n${output} console=True Should Be Equal As Integers ${rc} 0 ${num_lines} = Run bash -c "echo '${output}' | wc -l" - # 3 nodes in lab1 and 1 node in lab2 (+4 lines for the header and footer) - Should Be Equal As Integers ${num_lines} 8 + # 3 nodes in lab1 and 1 node in lab2 (+ for the header, footer and row delimiters) + Should Be Equal As Integers ${num_lines} 15 Verify host mode networking for node l3 # l3 node is launched with host mode networking @@ -91,7 +92,7 @@ Verify ipv4-range is set correctly ${rc} ${output} = Run And Return Rc And Output ... sudo -E ${CLAB_BIN} --runtime ${runtime} inspect -t ${CURDIR}/01-linux-single-node.clab.yml Log ${output} - Should Contain ${output} 172.20.30.9/24 + Should Contain ${output} 172.20.30.9 Destroy all labs ${rc} ${output} = Run And Return Rc And Output diff --git a/tests/01-smoke/04-generate.robot b/tests/01-smoke/04-generate.robot index 605485878..ad8056940 100644 --- a/tests/01-smoke/04-generate.robot +++ b/tests/01-smoke/04-generate.robot @@ -12,7 +12,7 @@ ${runtime} docker Deploy ${lab-name} lab with generate command Skip If '${runtime}' != 'docker' ${rc} ${output} = Run And Return Rc And Output - ... sudo -E ${CLAB_BIN} --runtime ${runtime} generate --name ${lab-name} --kind linux --image ghcr.io/hellt/network-multitool --nodes 2,1,1 --deploy + ... sudo -E ${CLAB_BIN} --runtime ${runtime} generate --name ${lab-name} --kind linux --image ghcr.io/srl-labs/network-multitool --nodes 2,1,1 --deploy Log ${output} Should Be Equal As Integers ${rc} 0 diff --git a/tests/01-smoke/05-docker-bridge.robot b/tests/01-smoke/05-docker-bridge.robot index 4c39f6d32..0304c33b3 100644 --- a/tests/01-smoke/05-docker-bridge.robot +++ b/tests/01-smoke/05-docker-bridge.robot @@ -8,30 +8,34 @@ Suite Teardown Cleanup *** Variables *** -${lab-name} 05-docker-bridge -${lab-file} 05-docker-bridge.clab.yml -${runtime} docker +${lab-name} 05-docker-bridge +${lab-file} 05-docker-bridge.clab.yml +${runtime} docker +${table-delimit} │ *** Test Cases *** Deploy ${lab-name} lab ${rc} ${output} = Run And Return Rc And Output ... sudo -E ${CLAB_BIN} --runtime ${runtime} deploy -t ${CURDIR}/${lab-file} - Log ${output} + Log \n--> LOG: Deploy output\n${output} console=True Should Be Equal As Integers ${rc} 0 Ensure inspect outputs IP addresses ${rc} ${output} = Run And Return Rc And Output ... sudo -E ${CLAB_BIN} --runtime ${runtime} inspect --name ${lab-name} - Log ${output} + Log \n--> LOG: Inspect output\n${output} console=True Should Be Equal As Integers ${rc} 0 - ${line} = String.Get Line ${output} -2 - Log ${line} - @{data} = Split String ${line} | - Log ${data} + + ${line} = String.Get Line ${output} -3 + Log \n--> LOG: Fetched line\n${line} console=True + + @{data} = Split String ${line} ${table-delimit} + Log \n--> LOG: Fetched data\n${data} console=True + # verify ipv4 address - ${ipv4} = String.Strip String ${data}[7] - Should Match Regexp ${ipv4} ^[\\d\\.]+/\\d{1,2}$ + ${ipv4} = String.Strip String ${data}[4] + Should Match Regexp ${ipv4} ^[\\d\\.]+$ *** Keywords *** diff --git a/tests/06-ext-container/01-ext-container.robot b/tests/06-ext-container/01-ext-container.robot index 28c438252..fce1d520b 100644 --- a/tests/06-ext-container/01-ext-container.robot +++ b/tests/06-ext-container/01-ext-container.robot @@ -45,13 +45,13 @@ Inspect the lab using topology file reference ${result} = Run Process ... sudo -E ${CLAB_BIN} --runtime ${runtime} inspect -t ${CURDIR}/${lab-file-name} ... shell=True - Log ${result.stdout} + Log \n--> LOG: Inspect output\n${result.stdout} console=True Log ${result.stderr} Should Be Equal As Integers ${result.rc} 0 ${num_nodes} = Run bash -c "echo '${result.stdout}' | wc -l" - # the inspect command should return only two external nodes (+4 lines for the header and footer) - Should Be Equal As Integers ${num_nodes} 6 + # the inspect command should return only two external nodes (+ lines for the header and footer) + Should Be Equal As Integers ${num_nodes} 9 Verify links in node ext2 ${rc} ${output} = Run And Return Rc And Output