diff --git a/core/register.go b/core/register.go index 22d3d0b04..9b81eb153 100644 --- a/core/register.go +++ b/core/register.go @@ -38,6 +38,8 @@ import ( clabnodesvr_cat9kv "github.com/srl-labs/containerlab/nodes/vr_cat9kv" clabnodesvr_csr "github.com/srl-labs/containerlab/nodes/vr_csr" clabnodesvr_freebsd "github.com/srl-labs/containerlab/nodes/vr_freebsd" + clabnodescisco_vios "github.com/srl-labs/containerlab/nodes/cisco_vios" + clabnodescisco_viosl2 "github.com/srl-labs/containerlab/nodes/cisco_viosl2" clabnodesvr_ftdv "github.com/srl-labs/containerlab/nodes/vr_ftdv" clabnodesvr_ftosv "github.com/srl-labs/containerlab/nodes/vr_ftosv" clabnodesvr_n9kv "github.com/srl-labs/containerlab/nodes/vr_n9kv" @@ -80,6 +82,8 @@ func (c *CLab) RegisterNodes() { //nolint:funlen clabnodesvr_csr.Register(c.Reg) clabnodesvr_c8000v.Register(c.Reg) clabnodesvr_freebsd.Register(c.Reg) + clabnodescisco_vios.Register(c.Reg) + clabnodescisco_viosl2.Register(c.Reg) clabnodesgeneric_vm.Register(c.Reg) clabnodesdell_sonic.Register(c.Reg) clabnodesvr_ftosv.Register(c.Reg) diff --git a/docs/manual/kinds/vr-vios.md b/docs/manual/kinds/vr-vios.md new file mode 100644 index 000000000..9aa8a1207 --- /dev/null +++ b/docs/manual/kinds/vr-vios.md @@ -0,0 +1,70 @@ +--- +search: + boost: 4 +kind_code_name: cisco_vios +kind_display_name: Cisco vIOS +--- +# Cisco vIOS + +Cisco vIOS virtualized router is identified with `-{{ kind_code_name }}-` kind in the [topology file](../topo-def-file.md). It is built using [vrnetlab](../vrnetlab.md) project and essentially is a Qemu VM packaged in a docker container format. + +## Managing Cisco vIOS nodes + +Cisco vIOS node launched with containerlab can be managed via the following interfaces: + +=== "bash" + to connect to a `bash` shell of a running Cisco vIOS container: + ```bash + docker exec -it bash + ``` +=== "CLI" + to connect to the vIOS CLI + ```bash + ssh cisco@ + ``` + +!!!info + Default user credentials: `cisco:cisco` + +## Interface naming + +You can use [interfaces names](../topo-def-file.md#interface-naming) in the topology file like they appear in -{{ kind_display_name }}-. + +The interface naming convention is: `GigabitEthernetX` (or `GiX`), where `X` is the port number. + +With that naming convention in mind: + +* `Gi0` - first data port available +* `Gi1` - second data port, and so on... + +The example ports above would be mapped to the following Linux interfaces inside the container running the -{{ kind_display_name }}- VM: + +* `eth0` - management interface connected to the containerlab management network (rendered as `GigabitEthernet0/0` in the CLI) +* `eth1` - first data interface, mapped to the first data port of the VM (rendered as `GigabitEthernet0`) +* `eth2+` - second and subsequent data interfaces, mapped to the second and subsequent data ports of the VM (rendered as `GigabitEthernet1` and so on) + +When containerlab launches -{{ kind_display_name }}- node the management interface gets assigned an address from the containerlab management network. + +Data interfaces `GigabitEthernet0+` need to be configured with IP addressing manually using CLI or other available management interfaces. + +## Features and options + +### Node configuration + +Cisco vIOS nodes come up with a basic configuration where only `cisco` user and management interface are provisioned. + +#### Startup configuration + +It is possible to make vIOS nodes boot up with a user-defined startup-config instead of a built-in one. With a [`startup-config`](../nodes.md#startup-config) property of the node/kind user sets the path to the config file that will be mounted to a container and used as a startup-config: + +```yaml +topology: + nodes: + node: + kind: cisco_vios + startup-config: myconfig.txt +``` + +With this knob containerlab is instructed to take a file `myconfig.txt` from the directory that hosts the topology file, and copy it to the lab directory for that specific node under the `/config/startup-config.cfg` name. Then the directory that hosts the startup-config dir is mounted to the container. This will result in this config being applied at startup by the node. + +Configuration is applied after the node is started, thus it can contain partial configuration snippets that you desire to add on top of the default config that a node boots up with. diff --git a/docs/manual/kinds/vr-viosl2.md b/docs/manual/kinds/vr-viosl2.md new file mode 100644 index 000000000..95f698d94 --- /dev/null +++ b/docs/manual/kinds/vr-viosl2.md @@ -0,0 +1,70 @@ +--- +search: + boost: 4 +kind_code_name: cisco_viosl2 +kind_display_name: Cisco vIOSL2 +--- +# Cisco vIOSL2 + +Cisco vIOSL2 virtualized layer-2 switch is identified with `-{{ kind_code_name }}-` kind in the [topology file](../topo-def-file.md). It is built using [vrnetlab](../vrnetlab.md) project and essentially is a Qemu VM packaged in a docker container format. + +## Managing Cisco vIOSL2 nodes + +Cisco vIOSL2 node launched with containerlab can be managed via the following interfaces: + +=== "bash" + to connect to a `bash` shell of a running Cisco vIOSL2 container: + ```bash + docker exec -it bash + ``` +=== "CLI" + to connect to the vIOSL2 CLI + ```bash + ssh cisco@ + ``` + +!!!info + Default user credentials: `cisco:cisco` + +## Interface naming + +You can use [interfaces names](../topo-def-file.md#interface-naming) in the topology file like they appear in -{{ kind_display_name }}-. + +The interface naming convention is: `GigabitEthernetX` (or `GiX`), where `X` is the port number. + +With that naming convention in mind: + +* `Gi0` - first data port available +* `Gi1` - second data port, and so on... + +The example ports above would be mapped to the following Linux interfaces inside the container running the -{{ kind_display_name }}- VM: + +* `eth0` - management interface connected to the containerlab management network (rendered as `GigabitEthernet0/0` in the CLI) +* `eth1` - first data interface, mapped to the first data port of the VM (rendered as `GigabitEthernet0`) +* `eth2+` - second and subsequent data interfaces, mapped to the second and subsequent data ports of the VM (rendered as `GigabitEthernet1` and so on) + +When containerlab launches -{{ kind_display_name }}- node the management interface gets assigned an address from the containerlab management network. + +Data interfaces `GigabitEthernet0+` need to be configured with IP addressing manually using CLI or other available management interfaces. + +## Features and options + +### Node configuration + +Cisco vIOSL2 nodes come up with a basic configuration where only `cisco` user and management interface are provisioned. + +#### Startup configuration + +It is possible to make vIOSL2 nodes boot up with a user-defined startup-config instead of a built-in one. With a [`startup-config`](../nodes.md#startup-config) property of the node/kind user sets the path to the config file that will be mounted to a container and used as a startup-config: + +```yaml +topology: + nodes: + node: + kind: cisco_viosl2 + startup-config: myconfig.txt +``` + +With this knob containerlab is instructed to take a file `myconfig.txt` from the directory that hosts the topology file, and copy it to the lab directory for that specific node under the `/config/startup-config.cfg` name. Then the directory that hosts the startup-config dir is mounted to the container. This will result in this config being applied at startup by the node. + +Configuration is applied after the node is started, thus it can contain partial configuration snippets that you desire to add on top of the default config that a node boots up with. diff --git a/nodes/cisco_vios/cisco-vios.go b/nodes/cisco_vios/cisco-vios.go new file mode 100644 index 000000000..d138a6502 --- /dev/null +++ b/nodes/cisco_vios/cisco-vios.go @@ -0,0 +1,91 @@ +// Copyright 2020 Nokia +// Licensed under the BSD 3-Clause License. +// SPDX-License-Identifier: BSD-3-Clause + +package cisco_vios + +import ( + "fmt" + "path" + "regexp" + + clabnodes "github.com/srl-labs/containerlab/nodes" + clabtypes "github.com/srl-labs/containerlab/types" + clabutils "github.com/srl-labs/containerlab/utils" +) + +var ( + kindnames = []string{"cisco_vios", "vr-vios", "vr-cisco_vios"} + defaultCredentials = clabnodes.NewCredentials("cisco", "cisco") + + InterfaceRegexp = regexp.MustCompile(`(?:Gi|GigabitEthernet)\s?(?P\d+)$`) + InterfaceOffset = 0 + InterfaceHelp = "GiX or GigabitEthernetX (where X >= 0) or ethX (where X >= 1)" +) + +const ( + scrapliPlatformName = "cisco_ios" +) + +// Register registers the node in the NodeRegistry. +func Register(r *clabnodes.NodeRegistry) { + platformAttrs := &clabnodes.PlatformAttrs{ + ScrapliPlatformName: scrapliPlatformName, + } + + nrea := clabnodes.NewNodeRegistryEntryAttributes(defaultCredentials, nil, platformAttrs) + + r.Register(kindnames, func() clabnodes.Node { + return new(vrVios) + }, nrea) +} + +type vrVios struct { + clabnodes.VRNode +} + +func (n *vrVios) Init(cfg *clabtypes.NodeConfig, opts ...clabnodes.NodeOption) error { + // Init VRNode + n.VRNode = *clabnodes.NewVRNode(n, defaultCredentials, scrapliPlatformName) + // set virtualization requirement + n.HostRequirements.VirtRequired = true + + n.Cfg = cfg + for _, o := range opts { + o(n) + } + // env vars are used to set launch.py arguments in vrnetlab container + defEnv := map[string]string{ + "CONNECTION_MODE": clabnodes.VrDefConnMode, + "USERNAME": defaultCredentials.GetUsername(), + "PASSWORD": defaultCredentials.GetPassword(), + "DOCKER_NET_V4_ADDR": n.Mgmt.IPv4Subnet, + "DOCKER_NET_V6_ADDR": n.Mgmt.IPv6Subnet, + } + n.Cfg.Env = clabutils.MergeStringMaps(defEnv, n.Cfg.Env) + + // mount config dir to support startup-config functionality + n.Cfg.Binds = append( + n.Cfg.Binds, + fmt.Sprint(path.Join(n.Cfg.LabDir, n.ConfigDirName), ":/config"), + ) + + if n.Cfg.Env["CONNECTION_MODE"] == "macvtap" { + // mount dev dir to enable macvtap + n.Cfg.Binds = append(n.Cfg.Binds, "/dev:/dev") + } + + n.Cfg.Cmd = fmt.Sprintf( + "--username %s --password %s --hostname %s --connection-mode %s --trace", + n.Cfg.Env["USERNAME"], + n.Cfg.Env["PASSWORD"], + n.Cfg.ShortName, + n.Cfg.Env["CONNECTION_MODE"], + ) + + n.InterfaceRegexp = InterfaceRegexp + n.InterfaceOffset = InterfaceOffset + n.InterfaceHelp = InterfaceHelp + + return nil +} diff --git a/nodes/cisco_vios/cisco-vios_test.go b/nodes/cisco_vios/cisco-vios_test.go new file mode 100644 index 000000000..5bb30a740 --- /dev/null +++ b/nodes/cisco_vios/cisco-vios_test.go @@ -0,0 +1,117 @@ +package cisco_vios + +import ( + "testing" + + clablinks "github.com/srl-labs/containerlab/links" + clabnodes "github.com/srl-labs/containerlab/nodes" + clabtypes "github.com/srl-labs/containerlab/types" +) + +func TestVIOSInterfaceParsing(t *testing.T) { + tests := map[string]struct { + endpoints []*clablinks.EndpointVeth + node *vrVios + resultEps []string + }{ + "alias-parse": { + endpoints: []*clablinks.EndpointVeth{ + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "Gi0", + }, + }, + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "GigabitEthernet1", + }, + }, + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "GigabitEthernet 2", + }, + }, + }, + node: &vrVios{ + VRNode: clabnodes.VRNode{ + DefaultNode: clabnodes.DefaultNode{ + Cfg: &clabtypes.NodeConfig{ + ShortName: "vios", + }, + InterfaceRegexp: InterfaceRegexp, + InterfaceOffset: InterfaceOffset, + }, + }, + }, + resultEps: []string{ + "eth1", "eth2", "eth3", + }, + }, + "original-parse": { + endpoints: []*clablinks.EndpointVeth{ + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "eth1", + }, + }, + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "eth2", + }, + }, + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "eth3", + }, + }, + }, + node: &vrVios{ + VRNode: clabnodes.VRNode{ + DefaultNode: clabnodes.DefaultNode{ + Cfg: &clabtypes.NodeConfig{ + ShortName: "vios", + }, + InterfaceRegexp: InterfaceRegexp, + InterfaceOffset: InterfaceOffset, + }, + }, + }, + resultEps: []string{ + "eth1", "eth2", "eth3", + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(tt *testing.T) { + foundError := false + tc.node.OverwriteNode = tc.node + tc.node.InterfaceMappedPrefix = "eth" + tc.node.FirstDataIfIndex = 1 + for _, ep := range tc.endpoints { + gotEndpointErr := tc.node.AddEndpoint(ep) + if gotEndpointErr != nil { + foundError = true + t.Errorf("got error for endpoint %+v", gotEndpointErr) + } + } + + if !foundError { + gotCheckErr := tc.node.CheckInterfaceName() + if gotCheckErr != nil { + foundError = true + t.Errorf("got error for check %+v", gotCheckErr) + } + + if !foundError { + for idx, ep := range tc.node.Endpoints { + if ep.GetIfaceName() != tc.resultEps[idx] { + t.Errorf("got wrong mapped endpoint %q (%q), want %q", + ep.GetIfaceName(), ep.GetIfaceAlias(), tc.resultEps[idx]) + } + } + } + } + }) + } +} diff --git a/nodes/cisco_viosl2/cisco-viosl2.go b/nodes/cisco_viosl2/cisco-viosl2.go new file mode 100644 index 000000000..ec139cb5b --- /dev/null +++ b/nodes/cisco_viosl2/cisco-viosl2.go @@ -0,0 +1,91 @@ +// Copyright 2020 Nokia +// Licensed under the BSD 3-Clause License. +// SPDX-License-Identifier: BSD-3-Clause + +package cisco_viosl2 + +import ( + "fmt" + "path" + "regexp" + + clabnodes "github.com/srl-labs/containerlab/nodes" + clabtypes "github.com/srl-labs/containerlab/types" + clabutils "github.com/srl-labs/containerlab/utils" +) + +var ( + kindnames = []string{"cisco_viosl2", "vr-viosl2", "vr-cisco_viosl2"} + defaultCredentials = clabnodes.NewCredentials("cisco", "cisco") + + InterfaceRegexp = regexp.MustCompile(`(?:Gi|GigabitEthernet)\s?(?P\d+)$`) + InterfaceOffset = 0 + InterfaceHelp = "GiX or GigabitEthernetX (where X >= 0) or ethX (where X >= 1)" +) + +const ( + scrapliPlatformName = "cisco_ios" +) + +// Register registers the node in the NodeRegistry. +func Register(r *clabnodes.NodeRegistry) { + platformAttrs := &clabnodes.PlatformAttrs{ + ScrapliPlatformName: scrapliPlatformName, + } + + nrea := clabnodes.NewNodeRegistryEntryAttributes(defaultCredentials, nil, platformAttrs) + + r.Register(kindnames, func() clabnodes.Node { + return new(vrViosL2) + }, nrea) +} + +type vrViosL2 struct { + clabnodes.VRNode +} + +func (n *vrViosL2) Init(cfg *clabtypes.NodeConfig, opts ...clabnodes.NodeOption) error { + // Init VRNode + n.VRNode = *clabnodes.NewVRNode(n, defaultCredentials, scrapliPlatformName) + // set virtualization requirement + n.HostRequirements.VirtRequired = true + + n.Cfg = cfg + for _, o := range opts { + o(n) + } + // env vars are used to set launch.py arguments in vrnetlab container + defEnv := map[string]string{ + "CONNECTION_MODE": clabnodes.VrDefConnMode, + "USERNAME": defaultCredentials.GetUsername(), + "PASSWORD": defaultCredentials.GetPassword(), + "DOCKER_NET_V4_ADDR": n.Mgmt.IPv4Subnet, + "DOCKER_NET_V6_ADDR": n.Mgmt.IPv6Subnet, + } + n.Cfg.Env = clabutils.MergeStringMaps(defEnv, n.Cfg.Env) + + // mount config dir to support startup-config functionality + n.Cfg.Binds = append( + n.Cfg.Binds, + fmt.Sprint(path.Join(n.Cfg.LabDir, n.ConfigDirName), ":/config"), + ) + + if n.Cfg.Env["CONNECTION_MODE"] == "macvtap" { + // mount dev dir to enable macvtap + n.Cfg.Binds = append(n.Cfg.Binds, "/dev:/dev") + } + + n.Cfg.Cmd = fmt.Sprintf( + "--username %s --password %s --hostname %s --connection-mode %s --trace", + n.Cfg.Env["USERNAME"], + n.Cfg.Env["PASSWORD"], + n.Cfg.ShortName, + n.Cfg.Env["CONNECTION_MODE"], + ) + + n.InterfaceRegexp = InterfaceRegexp + n.InterfaceOffset = InterfaceOffset + n.InterfaceHelp = InterfaceHelp + + return nil +} diff --git a/nodes/cisco_viosl2/cisco-viosl2_test.go b/nodes/cisco_viosl2/cisco-viosl2_test.go new file mode 100644 index 000000000..b1a5ae378 --- /dev/null +++ b/nodes/cisco_viosl2/cisco-viosl2_test.go @@ -0,0 +1,117 @@ +package cisco_viosl2 + +import ( + "testing" + + clablinks "github.com/srl-labs/containerlab/links" + clabnodes "github.com/srl-labs/containerlab/nodes" + clabtypes "github.com/srl-labs/containerlab/types" +) + +func TestVIOSL2InterfaceParsing(t *testing.T) { + tests := map[string]struct { + endpoints []*clablinks.EndpointVeth + node *vrViosL2 + resultEps []string + }{ + "alias-parse": { + endpoints: []*clablinks.EndpointVeth{ + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "Gi0", + }, + }, + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "GigabitEthernet1", + }, + }, + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "GigabitEthernet 2", + }, + }, + }, + node: &vrViosL2{ + VRNode: clabnodes.VRNode{ + DefaultNode: clabnodes.DefaultNode{ + Cfg: &clabtypes.NodeConfig{ + ShortName: "viosl2", + }, + InterfaceRegexp: InterfaceRegexp, + InterfaceOffset: InterfaceOffset, + }, + }, + }, + resultEps: []string{ + "eth1", "eth2", "eth3", + }, + }, + "original-parse": { + endpoints: []*clablinks.EndpointVeth{ + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "eth1", + }, + }, + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "eth2", + }, + }, + { + EndpointGeneric: clablinks.EndpointGeneric{ + IfaceName: "eth3", + }, + }, + }, + node: &vrViosL2{ + VRNode: clabnodes.VRNode{ + DefaultNode: clabnodes.DefaultNode{ + Cfg: &clabtypes.NodeConfig{ + ShortName: "viosl2", + }, + InterfaceRegexp: InterfaceRegexp, + InterfaceOffset: InterfaceOffset, + }, + }, + }, + resultEps: []string{ + "eth1", "eth2", "eth3", + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(tt *testing.T) { + foundError := false + tc.node.OverwriteNode = tc.node + tc.node.InterfaceMappedPrefix = "eth" + tc.node.FirstDataIfIndex = 1 + for _, ep := range tc.endpoints { + gotEndpointErr := tc.node.AddEndpoint(ep) + if gotEndpointErr != nil { + foundError = true + t.Errorf("got error for endpoint %+v", gotEndpointErr) + } + } + + if !foundError { + gotCheckErr := tc.node.CheckInterfaceName() + if gotCheckErr != nil { + foundError = true + t.Errorf("got error for check %+v", gotCheckErr) + } + + if !foundError { + for idx, ep := range tc.node.Endpoints { + if ep.GetIfaceName() != tc.resultEps[idx] { + t.Errorf("got wrong mapped endpoint %q (%q), want %q", + ep.GetIfaceName(), ep.GetIfaceAlias(), tc.resultEps[idx]) + } + } + } + } + }) + } +}