Skip to content

Commit

Permalink
Fix management interface and startup configs for IOL (#2347)
Browse files Browse the repository at this point in the history
* Enable OpenStdin on container cfg

* Add startup configuration support to IOL

- Allow for user startup configs to override default cfg

- Allow for user to supply partial startup config with '.partial' file ext

- Modify cfg template to allow for partial cfg

- Rename startup config ('startup.cfg') to 'boot_config.txt' to better reflect nature of the file.

* Decouple interface and startup cfg generation.

* Add func to write to container stdin

* Add UpdateMgmtIntf func

- Updates IOL mgmt interface IP addressing on boots that are NOT the first boot.

* Define `WriteToStdinNoWait()` for podman runtime.

* Also update static default mgmt route in `UpdateMgmtIntf()`

* regen mocks

* format with make format

* Add startup cfg tests

* Extend full cfg test for template placeholders

* Add docs re: Startup configs

* Update management IP change delay to 10 seconds

* Only allow single IPv6 address

* Add tests for management interface address change

---------

Co-authored-by: Roman Dodin <[email protected]>
  • Loading branch information
kaelemc and hellt authored Dec 18, 2024
1 parent ada07a1 commit 3581550
Show file tree
Hide file tree
Showing 15 changed files with 358 additions and 27 deletions.
56 changes: 54 additions & 2 deletions docs/manual/kinds/cisco_iol.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"]
Expand Down
1 change: 1 addition & 0 deletions mocks/dependency_manager.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions mocks/mocknodes/default_node.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions mocks/mocknodes/node.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions mocks/mockruntime/runtime.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions nodes/iol/iol.cfg.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ line vty 0 4
login local
transport input ssh
!
{{ .PartialCfg }}
end
94 changes: 72 additions & 22 deletions nodes/iol/iol.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<slot>\d+)/(?P<port>\d+)$`)
InterfaceOffset = 1
InterfaceHelp = "eX/Y or EthernetX/Y (where X >= 0 and Y >= 1)"
Expand All @@ -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 {
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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 {
Expand All @@ -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).
Expand All @@ -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]+")

Expand All @@ -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,
Expand All @@ -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{
Expand All @@ -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 {
Expand All @@ -232,6 +267,7 @@ type IOLTemplateData struct {
MgmtIPv6PrefixLen int
MgmtIPv6GW string
DataIFaces []IOLInterface
PartialCfg string
}

// IOLinterface struct stores mapping info between
Expand Down Expand Up @@ -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))
}
Loading

0 comments on commit 3581550

Please sign in to comment.