diff --git a/docs/manual/kinds/cisco_iol.md b/docs/manual/kinds/cisco_iol.md index 3a6acaf3a..050d3c600 100644 --- a/docs/manual/kinds/cisco_iol.md +++ b/docs/manual/kinds/cisco_iol.md @@ -122,9 +122,61 @@ Ethernet0/2 unassigned YES unset administratively down down Ethernet0/3 unassigned YES unset administratively down down ``` +## Startup configuration + +When -{{ kind_display_name }}- is booted, it will start with a basic configuration which configures the following: + +- IP addressing for the Ethernet0/0 (management) interface. +- Management VRF for the Ethernet0/0 interface. +- Default route(s) in the management VRF context for the [management network](../network.md#management-network). +- SSH server. +- Sets all user defined interfaces into 'up' state. + +On subsequent boots (deployments which are not the first boot of -{{ kind_short_display_name }}-), -{{ kind_short_display_name }}- will take a few extra seconds to come up, this is because Containerlab must update the management interface IP addressing and default routes for the management network. + +### User-defined config + +-{{ kind_display_name }}- supports user defined startup configurations in two forms: + +- Full startup configuration. +- Partial startup configuration. + +Both types of startup configurations are only be applied on the **first boot** of -{{ kind_short_display_name }}-. When you save configuration in IOL to the NVRAM (using `write memory` or `copy run start` commands), the NVRAM configuration will override the startup configuration. + +#### Full startup configuration + +The full startup configuration is used to fully replace/override the default startup configuration that is applied. This means you must define IP addressing and the SSH server in your configuration to access -{{ kind_short_display_name }}-. + +You can use the template variables that are defined in the [default startup confguration](https://github.com/srl-labs/containerlab/blob/main/nodes/iol/iol.cfg.tmpl). On lab deployment the template variables will be replaced/substituted. + +```yaml +name: iol_full_startup_cfg +topology: + nodes: + sros: + kind: cisco_iol + startup-config: configuration.txt +``` + +#### Partial startup configuration + +The partial startup configuration is appended to the default startup configuration. This is useful to preconfigure certain things like loopback interfaces or IGP, while also taking advantage of the startup configuration that containerlab applies by default for management interface IP addressing and SSH access. + +The partial startup configuration must contain `.partial` in the filename. For example: `config.partial.txt` or `config.partial` + +```yaml +name: iol_partial_startup_cfg +topology: + nodes: + sros: + kind: cisco_iol + startup-config: configuration.txt.partial +``` + + ## Usage and sample topology -IOL-L2 has a different startup configuration compared to the regular IOL. You can tell containerlab you are using the L2 image by supplying the `type` field in your topology. +IOL-L2 requires a different startup configuration compared to the regular IOL. You can tell containerlab you are using the L2 image by supplying the `type` field in your topology. See the sample topology below @@ -141,7 +193,7 @@ topology: switch: kind: cisco_iol image: vrnetlab/cisco_iol:L2-17.12.01 - type: l2 + type: L2 links: - endpoints: ["router1:Ethernet0/1","switch:Ethernet0/1"] - endpoints: ["router2:Ethernet0/1","switch:e0/2"] diff --git a/mocks/dependency_manager.go b/mocks/dependency_manager.go index c5e6bd13b..a814a792a 100644 --- a/mocks/dependency_manager.go +++ b/mocks/dependency_manager.go @@ -21,6 +21,7 @@ import ( type MockDependencyManager struct { ctrl *gomock.Controller recorder *MockDependencyManagerMockRecorder + isgomock struct{} } // MockDependencyManagerMockRecorder is the mock recorder for MockDependencyManager. diff --git a/mocks/mocknodes/default_node.go b/mocks/mocknodes/default_node.go index 94a698a3f..6acc4530d 100644 --- a/mocks/mocknodes/default_node.go +++ b/mocks/mocknodes/default_node.go @@ -22,6 +22,7 @@ import ( type MockNodeOverwrites struct { ctrl *gomock.Controller recorder *MockNodeOverwritesMockRecorder + isgomock struct{} } // MockNodeOverwritesMockRecorder is the mock recorder for MockNodeOverwrites. @@ -41,6 +42,21 @@ func (m *MockNodeOverwrites) EXPECT() *MockNodeOverwritesMockRecorder { return m.recorder } +// CalculateInterfaceIndex mocks base method. +func (m *MockNodeOverwrites) CalculateInterfaceIndex(ifName string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CalculateInterfaceIndex", ifName) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CalculateInterfaceIndex indicates an expected call of CalculateInterfaceIndex. +func (mr *MockNodeOverwritesMockRecorder) CalculateInterfaceIndex(ifName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CalculateInterfaceIndex", reflect.TypeOf((*MockNodeOverwrites)(nil).CalculateInterfaceIndex), ifName) +} + // CheckInterfaceName mocks base method. func (m *MockNodeOverwrites) CheckInterfaceName() error { m.ctrl.T.Helper() @@ -98,6 +114,21 @@ func (mr *MockNodeOverwritesMockRecorder) GetImages(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImages", reflect.TypeOf((*MockNodeOverwrites)(nil).GetImages), ctx) } +// GetMappedInterfaceName mocks base method. +func (m *MockNodeOverwrites) GetMappedInterfaceName(ifName string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMappedInterfaceName", ifName) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMappedInterfaceName indicates an expected call of GetMappedInterfaceName. +func (mr *MockNodeOverwritesMockRecorder) GetMappedInterfaceName(ifName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMappedInterfaceName", reflect.TypeOf((*MockNodeOverwrites)(nil).GetMappedInterfaceName), ifName) +} + // PullImage mocks base method. func (m *MockNodeOverwrites) PullImage(ctx context.Context) error { m.ctrl.T.Helper() diff --git a/mocks/mocknodes/node.go b/mocks/mocknodes/node.go index 1f76fdc2d..aa5c4289c 100644 --- a/mocks/mocknodes/node.go +++ b/mocks/mocknodes/node.go @@ -28,6 +28,7 @@ import ( type MockNode struct { ctrl *gomock.Controller recorder *MockNodeMockRecorder + isgomock struct{} } // MockNodeMockRecorder is the mock recorder for MockNode. @@ -78,16 +79,16 @@ func (mr *MockNodeMockRecorder) AddLinkToContainer(ctx, link, f any) *gomock.Cal // CalculateInterfaceIndex mocks base method. func (m *MockNode) CalculateInterfaceIndex(ifName string) (int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CalculateInterfaceIndex") + ret := m.ctrl.Call(m, "CalculateInterfaceIndex", ifName) ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } // CalculateInterfaceIndex indicates an expected call of CalculateInterfaceIndex. -func (mr *MockNodeMockRecorder) CalculateInterfaceIndex() *gomock.Call { +func (mr *MockNodeMockRecorder) CalculateInterfaceIndex(ifName any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CalculateInterfaceIndex", reflect.TypeOf((*MockNode)(nil).CalculateInterfaceIndex)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CalculateInterfaceIndex", reflect.TypeOf((*MockNode)(nil).CalculateInterfaceIndex), ifName) } // CheckDeploymentConditions mocks base method. diff --git a/mocks/mockruntime/runtime.go b/mocks/mockruntime/runtime.go index f45b84746..4564d0baf 100644 --- a/mocks/mockruntime/runtime.go +++ b/mocks/mockruntime/runtime.go @@ -24,6 +24,7 @@ import ( type MockContainerRuntime struct { ctrl *gomock.Controller recorder *MockContainerRuntimeMockRecorder + isgomock struct{} } // MockContainerRuntimeMockRecorder is the mock recorder for MockContainerRuntime. @@ -370,10 +371,25 @@ func (mr *MockContainerRuntimeMockRecorder) WithMgmtNet(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithMgmtNet", reflect.TypeOf((*MockContainerRuntime)(nil).WithMgmtNet), arg0) } +// WriteToStdinNoWait mocks base method. +func (m *MockContainerRuntime) WriteToStdinNoWait(ctx context.Context, cID string, data []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WriteToStdinNoWait", ctx, cID, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// WriteToStdinNoWait indicates an expected call of WriteToStdinNoWait. +func (mr *MockContainerRuntimeMockRecorder) WriteToStdinNoWait(ctx, cID, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteToStdinNoWait", reflect.TypeOf((*MockContainerRuntime)(nil).WriteToStdinNoWait), ctx, cID, data) +} + // MockNode is a mock of Node interface. type MockNode struct { ctrl *gomock.Controller recorder *MockNodeMockRecorder + isgomock struct{} } // MockNodeMockRecorder is the mock recorder for MockNode. diff --git a/nodes/iol/iol.cfg.tmpl b/nodes/iol/iol.cfg.tmpl index 46ac8b932..a6114ddbb 100644 --- a/nodes/iol/iol.cfg.tmpl +++ b/nodes/iol/iol.cfg.tmpl @@ -44,4 +44,5 @@ line vty 0 4 login local transport input ssh ! +{{ .PartialCfg }} end diff --git a/nodes/iol/iol.go b/nodes/iol/iol.go index 84ac0f888..f84697ae3 100644 --- a/nodes/iol/iol.go +++ b/nodes/iol/iol.go @@ -9,12 +9,14 @@ import ( "context" _ "embed" "fmt" + "os" "path" "path/filepath" "regexp" "strconv" "strings" "text/template" + "time" "github.com/hairyhenderson/gomplate/v3" "github.com/hairyhenderson/gomplate/v3/data" @@ -42,9 +44,6 @@ var ( //go:embed iol.cfg.tmpl cfgTemplate string - IOLCfgTpl, _ = template.New("clab-iol-default-config").Funcs( - gomplate.CreateFuncs(context.Background(), new(data.Data))).Parse(cfgTemplate) - InterfaceRegexp = regexp.MustCompile(`(?:e|Ethernet)\s?(?P\d+)/(?P\d+)$`) InterfaceOffset = 1 InterfaceHelp = "eX/Y or EthernetX/Y (where X >= 0 and Y >= 1)" @@ -65,14 +64,19 @@ func Register(r *nodes.NodeRegistry) { type iol struct { nodes.DefaultNode - isL2Node bool - Pid string - nvramFile string + isL2Node bool + Pid string + nvramFile string + partialStartupCfg string + bootCfg string + interfaces []IOLInterface + firstBoot bool } func (n *iol) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error { // Init DefaultNode n.DefaultNode = *nodes.NewDefaultNode(n) + n.firstBoot = false n.Cfg = cfg for _, o := range opts { @@ -107,7 +111,7 @@ func (n *iol) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error { fmt.Sprint(path.Join(n.Cfg.LabDir, n.nvramFile), ":", path.Join(iol_workdir, n.nvramFile)), // mount launch config - fmt.Sprint(filepath.Join(n.Cfg.LabDir, "startup.cfg"), ":/iol/config.txt"), + fmt.Sprint(filepath.Join(n.Cfg.LabDir, "boot_config.txt"), ":/iol/config.txt"), // mount IOYAP and NETMAP for interface mapping fmt.Sprint(filepath.Join(n.Cfg.LabDir, "iouyap.ini"), ":/iol/iouyap.ini"), @@ -135,7 +139,17 @@ func (n *iol) PreDeploy(ctx context.Context, params *nodes.PreDeployParams) erro func (n *iol) PostDeploy(ctx context.Context, params *nodes.PostDeployParams) error { log.Infof("Running postdeploy actions for Cisco IOL '%s' node", n.Cfg.ShortName) - return n.GenInterfaceConfig(ctx) + n.GenBootConfig(ctx) + + // Must update mgmt IP if not first boot + if !n.firstBoot { + // iol has a 5sec boot delay, wait a few extra secs for the console + time.Sleep(10 * time.Second) + + return n.UpdateMgmtIntf(ctx) + } + + return nil } func (n *iol) CreateIOLFiles(ctx context.Context) error { @@ -144,15 +158,14 @@ func (n *iol) CreateIOLFiles(ctx context.Context) error { if !utils.FileExists(path.Join(n.Cfg.LabDir, n.nvramFile)) { // create nvram file utils.CreateFile(path.Join(n.Cfg.LabDir, n.nvramFile), "") + n.firstBoot = true } // create these files so the bind monut doesn't automatically // make folders. - utils.CreateFile(path.Join(n.Cfg.LabDir, "startup.cfg"), "") - utils.CreateFile(path.Join(n.Cfg.LabDir, "iouyap.ini"), "") - utils.CreateFile(path.Join(n.Cfg.LabDir, "NETMAP"), "") + utils.CreateFile(path.Join(n.Cfg.LabDir, "boot_config.txt"), "") - return nil + return n.GenInterfaceConfig(ctx) } // Generate interfaces configuration for IOL (and iouyap/netmap). @@ -163,8 +176,6 @@ func (n *iol) GenInterfaceConfig(_ context.Context) error { slot, port := 0, 0 - IOLInterfaces := []IOLInterface{} - // Regexp to pull number out of linux'ethX' interface naming IntfRegExpr := regexp.MustCompile("[0-9]+") @@ -182,7 +193,7 @@ func (n *iol) GenInterfaceConfig(_ context.Context) error { netmapdata += fmt.Sprintf("%s:%d/%d 513:%d/%d\n", n.Pid, slot, port, slot, port) // populate template array for config - IOLInterfaces = append(IOLInterfaces, + n.interfaces = append(n.interfaces, IOLInterface{ intf.GetIfaceName(), x, @@ -193,9 +204,31 @@ func (n *iol) GenInterfaceConfig(_ context.Context) error { } - // create IOYAP and NETMAP file for interface mappings - utils.CreateFile(path.Join(n.Cfg.LabDir, "iouyap.ini"), iouyapData) - utils.CreateFile(path.Join(n.Cfg.LabDir, "NETMAP"), netmapdata) + // create IOUYAP and NETMAP file for interface mappings + err := utils.CreateFile(path.Join(n.Cfg.LabDir, "iouyap.ini"), iouyapData) + if err != nil { + return err + } + err = utils.CreateFile(path.Join(n.Cfg.LabDir, "NETMAP"), netmapdata) + + return err +} + +func (n *iol) GenBootConfig(_ context.Context) error { + n.bootCfg = cfgTemplate + + if n.Cfg.StartupConfig != "" { + cfg, err := os.ReadFile(n.Cfg.StartupConfig) + if err != nil { + return err + } + + if isPartialConfigFile(n.Cfg.StartupConfig) { + n.partialStartupCfg = string(cfg) + } else { + n.bootCfg = string(cfg) + } + } // create startup config template tpl := IOLTemplateData{ @@ -207,19 +240,21 @@ func (n *iol) GenInterfaceConfig(_ context.Context) error { MgmtIPv6Addr: n.Cfg.MgmtIPv6Address, MgmtIPv6PrefixLen: n.Cfg.MgmtIPv6PrefixLength, MgmtIPv6GW: n.Cfg.MgmtIPv6Gateway, - DataIFaces: IOLInterfaces, + DataIFaces: n.interfaces, + PartialCfg: n.partialStartupCfg, } + IOLCfgTpl, _ := template.New("clab-iol-default-config").Funcs( + gomplate.CreateFuncs(context.Background(), new(data.Data))).Parse(n.bootCfg) + // generate the config buf := new(bytes.Buffer) err := IOLCfgTpl.Execute(buf, tpl) if err != nil { return err } - // write it to disk - utils.CreateFile(path.Join(n.Cfg.LabDir, "startup.cfg"), buf.String()) - return err + return utils.CreateFile(path.Join(n.Cfg.LabDir, "boot_config.txt"), buf.String()) } type IOLTemplateData struct { @@ -232,6 +267,7 @@ type IOLTemplateData struct { MgmtIPv6PrefixLen int MgmtIPv6GW string DataIFaces []IOLInterface + PartialCfg string } // IOLinterface struct stores mapping info between @@ -315,3 +351,17 @@ func (n *iol) CheckInterfaceName() error { return nil } + +// from vr-sros.go +// isPartialConfigFile returns true if the config file name contains .partial substring. +func isPartialConfigFile(c string) bool { + return strings.Contains(strings.ToUpper(c), ".PARTIAL") +} + +func (n *iol) UpdateMgmtIntf(ctx context.Context) error { + mgmt_str := fmt.Sprintf("\renable\rconfig terminal\rinterface Ethernet0/0\rip address %s %s\rno ipv6 address\ripv6 address %s/%d\rexit\rip route vrf clab-mgmt 0.0.0.0 0.0.0.0 Ethernet0/0 %s\ripv6 route vrf clab-mgmt ::/0 Ethernet0/0 %s\rend\rwr\r", + n.Cfg.MgmtIPv4Address, utils.CIDRToDDN(n.Cfg.MgmtIPv4PrefixLength), n.Cfg.MgmtIPv6Address, + n.Cfg.MgmtIPv6PrefixLength, n.Cfg.MgmtIPv4Gateway, n.Cfg.MgmtIPv6Gateway) + + return n.Runtime.WriteToStdinNoWait(ctx, n.Cfg.ContainerID, []byte(mgmt_str)) +} diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index a0715f803..15ccaddf6 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -431,6 +431,7 @@ func (d *DockerRuntime) CreateContainer(ctx context.Context, node *types.NodeCon AttachStderr: true, Hostname: node.ShortName, Tty: true, + OpenStdin: true, User: node.User, Labels: node.Labels, ExposedPorts: node.PortSet, @@ -1043,3 +1044,24 @@ func (d *DockerRuntime) IsHealthy(ctx context.Context, cID string) (bool, error) } return inspect.State.Health.Status == "healthy", nil } + +func (d *DockerRuntime) WriteToStdinNoWait(ctx context.Context, cID string, data []byte) error { + stdin, err := d.Client.ContainerAttach(ctx, cID, container.AttachOptions{ + Stdin: true, + Stream: true, + Stdout: true, + Stderr: true, + }) + if err != nil { + return err + } + + log.Debugf("Writing to %s: %v", cID, data) + + _, err = stdin.Conn.Write(data) + if err != nil { + return err + } + + return stdin.Conn.Close() +} diff --git a/runtime/ignite/ignite.go b/runtime/ignite/ignite.go index ed8f41b59..54cddf066 100644 --- a/runtime/ignite/ignite.go +++ b/runtime/ignite/ignite.go @@ -471,3 +471,8 @@ func (*IgniteRuntime) IsHealthy(_ context.Context, _ string) (bool, error) { log.Errorf("function GetContainerHealth(...) not implemented in the Containerlab IgniteRuntime") return true, nil } + +func (*IgniteRuntime) WriteToStdinNoWait(ctx context.Context, cID string, data []byte) error { + log.Infof("WriteToStdinNoWait is not yet implemented for Ignite runtime") + return nil +} diff --git a/runtime/podman/podman.go b/runtime/podman/podman.go index 869da5a70..992cece3d 100644 --- a/runtime/podman/podman.go +++ b/runtime/podman/podman.go @@ -406,3 +406,8 @@ func (r *PodmanRuntime) IsHealthy(ctx context.Context, cID string) (bool, error) } return icd.State.Health.Status == "healthy", nil } + +func (*PodmanRuntime) WriteToStdinNoWait(ctx context.Context, cID string, data []byte) error { + log.Infof("WriteToStdinNoWait is not yet implemented for Podman runtime") + return nil +} diff --git a/runtime/runtime.go b/runtime/runtime.go index 6d8571600..3167ba618 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -62,6 +62,8 @@ type ContainerRuntime interface { GetContainerStatus(ctx context.Context, cID string) ContainerStatus // IsHealthy returns true is the container is reported as being healthy, false otherwise IsHealthy(ctx context.Context, cID string) (bool, error) + // Immediately write to the stdin of a container, returns error + WriteToStdinNoWait(ctx context.Context, cID string, data []byte) error } type ContainerStatus string diff --git a/tests/10-basic-cisco_iol/01-iol.robot b/tests/10-basic-cisco_iol/01-iol.robot index 87287dada..8ea96c480 100644 --- a/tests/10-basic-cisco_iol/01-iol.robot +++ b/tests/10-basic-cisco_iol/01-iol.robot @@ -39,6 +39,96 @@ Verify links in node switch Should Contain ${output} 172.20.20. Should Contain ${output} up +Verify parital startup configuration on router2 + ${rc} ${output} = Run And Return Rc And Output + ... sshpass -p "admin" ssh -o "IdentitiesOnly=yes" admin@clab-${lab-name}-router2 show running-config interface Loopback0 + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} PARTIAL_CFG + +Verify full startup configuration on router3 + ${rc} ${output} = Run And Return Rc And Output + ... sshpass -p "admin" ssh -o "IdentitiesOnly=yes" admin@clab-${lab-name}-router3 "sh run | inc hostname" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} FULL_STARTUP_CFG-router3 + +Write configuration to NVRAM on router1 + ${rc} ${output} = Run And Return Rc And Output + ... sshpass -p "admin" ssh -o "IdentitiesOnly=yes" admin@clab-iol-router1 "write memory" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} [OK] + +Log IP addresses for router1 + ${rc} ${ipv4_addr} = Run And Return Rc And Output + ... cat ${CURDIR}/clab-${lab-name}/topology-data.json | jq '.nodes.router1."mgmt-ipv4-address"' + Log ${ipv4_addr} + Should Be Equal As Integers ${rc} 0 + ${rc} ${ipv6_addr} = Run And Return Rc And Output + ... cat ${CURDIR}/clab-${lab-name}/topology-data.json | jq '.nodes.router1."mgmt-ipv6-address"' + Log ${ipv6_addr} + +Write configuration to NVRAM on switch + ${rc} ${output} = Run And Return Rc And Output + ... sshpass -p "admin" ssh -o "IdentitiesOnly=yes" admin@clab-iol-switch "write memory" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} [OK] + +Log IP addresses for switch + ${rc} ${ipv4_addr} = Run And Return Rc And Output + ... cat ${CURDIR}/clab-${lab-name}/topology-data.json | jq '.nodes.switch."mgmt-ipv4-address"' + Log ${ipv4_addr} + Should Be Equal As Integers ${rc} 0 + ${rc} ${ipv6_addr} = Run And Return Rc And Output + ... cat ${CURDIR}/clab-${lab-name}/topology-data.json | jq '.nodes.switch."mgmt-ipv6-address"' + Log ${ipv6_addr} + +Destroy ${lab-name} lab + Log ${CURDIR} + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E ${CLAB_BIN} --runtime ${runtime} destroy -t ${CURDIR}/${lab-file-name} + Log ${output} + Should Be Equal As Integers ${rc} 0 + +Re-deploy ${lab-name} lab + Log ${CURDIR} + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E ${CLAB_BIN} --runtime ${runtime} deploy -t ${CURDIR}/${lab-file-name} + Log ${output} + Should Be Equal As Integers ${rc} 0 + +Wait 60s for nodes to boot + Sleep 60s + +Verify connectivity via new management addresses on router1 + ${rc} ${ipv4_addr} = Run And Return Rc And Output + ... cat ${CURDIR}/clab-${lab-name}/topology-data.json | jq -r '.nodes.router1."mgmt-ipv4-address"' + Should Be Equal As Integers ${rc} 0 + ${rc} ${ipv6_addr} = Run And Return Rc And Output + ... cat ${CURDIR}/clab-${lab-name}/topology-data.json | jq -r '.nodes.router1."mgmt-ipv6-address"' + Should Be Equal As Integers ${rc} 0 + ${rc} ${output} = Run And Return Rc And Output + ... sshpass -p "admin" ssh -o "IdentitiesOnly=yes" admin@clab-${lab-name}-router1 "sh run interface Ethernet0/0" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} ${ipv4_addr.upper()} + Should Contain ${output} ${ipv6_addr.upper()} + +Verify connectivity via new management addresses on switch + ${rc} ${ipv4_addr} = Run And Return Rc And Output + ... cat ${CURDIR}/clab-${lab-name}/topology-data.json | jq -r '.nodes.switch."mgmt-ipv4-address"' + Should Be Equal As Integers ${rc} 0 + ${rc} ${ipv6_addr} = Run And Return Rc And Output + ... cat ${CURDIR}/clab-${lab-name}/topology-data.json | jq -r '.nodes.switch."mgmt-ipv6-address"' + Should Be Equal As Integers ${rc} 0 + ${rc} ${output} = Run And Return Rc And Output + ... sshpass -p "admin" ssh -o "IdentitiesOnly=yes" admin@clab-${lab-name}-switch "sh run interface Ethernet0/0" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} ${ipv4_addr.upper()} + Should Contain ${output} ${ipv6_addr.upper()} *** Keywords *** Cleanup diff --git a/tests/10-basic-cisco_iol/iol.clab.yml b/tests/10-basic-cisco_iol/iol.clab.yml index 584d9a4e0..66c4312b8 100644 --- a/tests/10-basic-cisco_iol/iol.clab.yml +++ b/tests/10-basic-cisco_iol/iol.clab.yml @@ -7,6 +7,11 @@ topology: router2: kind: cisco_iol image: ghcr.io/srl-labs/containerlab/cisco_iol:17.12.01 + startup-config: ./loopback_config.partial + router3: + kind: cisco_iol + image: ghcr.io/srl-labs/containerlab/cisco_iol:17.12.01 + startup-config: ./router3-full.cfg switch: kind: cisco_iol image: ghcr.io/srl-labs/containerlab/cisco_iol:L2-17.12.01 diff --git a/tests/10-basic-cisco_iol/loopback_config.partial b/tests/10-basic-cisco_iol/loopback_config.partial new file mode 100644 index 000000000..5b6313e16 --- /dev/null +++ b/tests/10-basic-cisco_iol/loopback_config.partial @@ -0,0 +1,3 @@ +interface Loopback0 + description PARTIAL_CFG +! \ No newline at end of file diff --git a/tests/10-basic-cisco_iol/router3-full.cfg b/tests/10-basic-cisco_iol/router3-full.cfg new file mode 100644 index 000000000..c224fc8fa --- /dev/null +++ b/tests/10-basic-cisco_iol/router3-full.cfg @@ -0,0 +1,47 @@ +hostname FULL_STARTUP_CFG-{{ .Hostname }} +! +no aaa new-model +! +ip domain name lab +! +ip cef +! +ipv6 unicast-routing +! +no ip domain lookup +! +username admin privilege 15 secret admin +! +vrf definition clab-mgmt + description clab-mgmt + address-family ipv4 + ! + address-family ipv6 + ! +! +interface Ethernet0/0 +{{ if .IsL2Node }} + no switchport +{{ end }} + vrf forwarding clab-mgmt + description clab-mgmt + ip address {{ .MgmtIPv4Addr }} {{ .MgmtIPv4SubnetMask }} + ipv6 address {{ .MgmtIPv6Addr }}/{{ .MgmtIPv6PrefixLen }} + no shutdown +!{{ range $index, $item := .DataIFaces }} +interface Ethernet{{ .Slot }}/{{ .Port }} + no shutdown +!{{ end }} +ip forward-protocol nd +! +ip route vrf clab-mgmt 0.0.0.0 0.0.0.0 Ethernet0/0 {{ .MgmtIPv4GW }} +ipv6 route vrf clab-mgmt ::/0 Ethernet0/0 {{ .MgmtIPv6GW }} +! +ip ssh version 2 +crypto key generate rsa modulus 2048 +! +line vty 0 4 + login local + transport input ssh +! +end