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

Fix management interface and startup configs for IOL #2347

Merged
merged 15 commits into from
Dec 18, 2024
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
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
Loading