|
| 1 | +// Copyright 2020 Nokia |
| 2 | +// Licensed under the BSD 3-Clause License. |
| 3 | +// SPDX-License-Identifier: BSD-3-Clause |
| 4 | + |
| 5 | +package cisco_vios |
| 6 | + |
| 7 | +import ( |
| 8 | + "bytes" |
| 9 | + "context" |
| 10 | + _ "embed" |
| 11 | + "fmt" |
| 12 | + "os" |
| 13 | + "path" |
| 14 | + "path/filepath" |
| 15 | + "regexp" |
| 16 | + "strings" |
| 17 | + "text/template" |
| 18 | + |
| 19 | + "github.com/charmbracelet/log" |
| 20 | + clabconstants "github.com/srl-labs/containerlab/constants" |
| 21 | + clabnodes "github.com/srl-labs/containerlab/nodes" |
| 22 | + clabtypes "github.com/srl-labs/containerlab/types" |
| 23 | + clabutils "github.com/srl-labs/containerlab/utils" |
| 24 | +) |
| 25 | + |
| 26 | +const ( |
| 27 | + typeRouter = "router" |
| 28 | + typeSwitch = "switch" |
| 29 | + typeL2 = "l2" |
| 30 | + |
| 31 | + scrapliPlatformName = "cisco_ios" |
| 32 | +) |
| 33 | + |
| 34 | +var ( |
| 35 | + kindnames = []string{"cisco_vios"} |
| 36 | + defaultCredentials = clabnodes.NewCredentials("admin", "admin") |
| 37 | + |
| 38 | + //go:embed vios.cfg |
| 39 | + cfgTemplate string |
| 40 | + |
| 41 | + InterfaceRegexp = regexp.MustCompile(`(?:Gi|GigabitEthernet)\s?(?P<port>\d+)$`) |
| 42 | + InterfaceOffset = 0 |
| 43 | + InterfaceHelp = "GiX or GigabitEthernetX (where X >= 0) or ethX (where X >= 1)" |
| 44 | + |
| 45 | + validTypes = []string{typeRouter, typeSwitch, typeL2} |
| 46 | +) |
| 47 | + |
| 48 | +// Register registers the node in the NodeRegistry. |
| 49 | +func Register(r *clabnodes.NodeRegistry) { |
| 50 | + platformAttrs := &clabnodes.PlatformAttrs{ |
| 51 | + ScrapliPlatformName: scrapliPlatformName, |
| 52 | + } |
| 53 | + |
| 54 | + nrea := clabnodes.NewNodeRegistryEntryAttributes(defaultCredentials, nil, platformAttrs) |
| 55 | + |
| 56 | + r.Register(kindnames, func() clabnodes.Node { |
| 57 | + return new(vios) |
| 58 | + }, nrea) |
| 59 | +} |
| 60 | + |
| 61 | +type vios struct { |
| 62 | + clabnodes.VRNode |
| 63 | + isL2Node bool |
| 64 | + bootCfg string |
| 65 | + partialStartupCfg string |
| 66 | +} |
| 67 | + |
| 68 | +func (n *vios) Init(cfg *clabtypes.NodeConfig, opts ...clabnodes.NodeOption) error { |
| 69 | + // Init VRNode |
| 70 | + n.VRNode = *clabnodes.NewVRNode(n, defaultCredentials, scrapliPlatformName) |
| 71 | + // set virtualization requirement |
| 72 | + n.HostRequirements.VirtRequired = true |
| 73 | + |
| 74 | + n.Cfg = cfg |
| 75 | + for _, o := range opts { |
| 76 | + o(n) |
| 77 | + } |
| 78 | + |
| 79 | + // Validate node type |
| 80 | + nodeType := strings.ToLower(n.Cfg.NodeType) |
| 81 | + switch nodeType { |
| 82 | + case "", typeRouter: |
| 83 | + // Default to router type |
| 84 | + n.isL2Node = false |
| 85 | + case typeSwitch, typeL2: |
| 86 | + // L2 switch type |
| 87 | + n.isL2Node = true |
| 88 | + default: |
| 89 | + return fmt.Errorf("invalid node type '%s'. Valid types are: %s", |
| 90 | + n.Cfg.NodeType, strings.Join(validTypes, ", ")) |
| 91 | + } |
| 92 | + |
| 93 | + // env vars are used to set launch.py arguments in vrnetlab container |
| 94 | + defEnv := map[string]string{ |
| 95 | + "CONNECTION_MODE": clabnodes.VrDefConnMode, |
| 96 | + "USERNAME": defaultCredentials.GetUsername(), |
| 97 | + "PASSWORD": defaultCredentials.GetPassword(), |
| 98 | + "DOCKER_NET_V4_ADDR": n.Mgmt.IPv4Subnet, |
| 99 | + "DOCKER_NET_V6_ADDR": n.Mgmt.IPv6Subnet, |
| 100 | + "CLAB_MGMT_PASSTHROUGH": "true", // force enable mgmt passthru |
| 101 | + } |
| 102 | + n.Cfg.Env = clabutils.MergeStringMaps(defEnv, n.Cfg.Env) |
| 103 | + |
| 104 | + // mount config dir to support startup-config functionality |
| 105 | + n.Cfg.Binds = append( |
| 106 | + n.Cfg.Binds, |
| 107 | + fmt.Sprint(path.Join(n.Cfg.LabDir, n.ConfigDirName), ":/config"), |
| 108 | + ) |
| 109 | + |
| 110 | + if n.Cfg.Env["CONNECTION_MODE"] == "macvtap" { |
| 111 | + // mount dev dir to enable macvtap |
| 112 | + n.Cfg.Binds = append(n.Cfg.Binds, "/dev:/dev") |
| 113 | + } |
| 114 | + |
| 115 | + n.Cfg.Cmd = fmt.Sprintf( |
| 116 | + "--username %s --password %s --hostname %s --connection-mode %s --trace", |
| 117 | + n.Cfg.Env["USERNAME"], |
| 118 | + n.Cfg.Env["PASSWORD"], |
| 119 | + n.Cfg.ShortName, |
| 120 | + n.Cfg.Env["CONNECTION_MODE"], |
| 121 | + ) |
| 122 | + |
| 123 | + n.InterfaceRegexp = InterfaceRegexp |
| 124 | + n.InterfaceOffset = InterfaceOffset |
| 125 | + n.InterfaceHelp = InterfaceHelp |
| 126 | + |
| 127 | + return nil |
| 128 | +} |
| 129 | + |
| 130 | +func (n *vios) PreDeploy(ctx context.Context, params *clabnodes.PreDeployParams) error { |
| 131 | + clabutils.CreateDirectory(n.Cfg.LabDir, clabconstants.PermissionsOpen) |
| 132 | + _, err := n.LoadOrGenerateCertificate(params.Cert, params.TopologyName) |
| 133 | + if err != nil { |
| 134 | + return err |
| 135 | + } |
| 136 | + |
| 137 | + configDir := filepath.Join(n.Cfg.LabDir, n.ConfigDirName) |
| 138 | + clabutils.CreateDirectory(configDir, clabconstants.PermissionsOpen) |
| 139 | + |
| 140 | + n.bootCfg = cfgTemplate |
| 141 | + |
| 142 | + if n.Cfg.StartupConfig != "" { |
| 143 | + cfg, err := os.ReadFile(n.Cfg.StartupConfig) |
| 144 | + if err != nil { |
| 145 | + return err |
| 146 | + } |
| 147 | + |
| 148 | + if clabutils.IsPartialConfigFile(n.Cfg.StartupConfig) { |
| 149 | + n.partialStartupCfg = string(cfg) |
| 150 | + } else { |
| 151 | + n.bootCfg = string(cfg) |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + return nil |
| 156 | +} |
| 157 | + |
| 158 | +func (n *vios) PostDeploy(ctx context.Context, _ *clabnodes.PostDeployParams) error { |
| 159 | + log.Info("Running postdeploy actions", "kind", n.Cfg.Kind, "node", n.Cfg.ShortName) |
| 160 | + return n.genBootConfig() |
| 161 | +} |
| 162 | + |
| 163 | +func (n *vios) genBootConfig() error { |
| 164 | + tplData := ViosTemplateData{ |
| 165 | + Hostname: n.Cfg.ShortName, |
| 166 | + Username: defaultCredentials.GetUsername(), |
| 167 | + Password: defaultCredentials.GetPassword(), |
| 168 | + IsL2Node: n.isL2Node, |
| 169 | + MgmtIPv4Addr: n.Cfg.MgmtIPv4Address, |
| 170 | + MgmtIPv4SubnetMask: clabutils.CIDRToDDN(n.Cfg.MgmtIPv4PrefixLength), |
| 171 | + MgmtIPv4GW: n.Cfg.MgmtIPv4Gateway, |
| 172 | + MgmtIPv6Addr: n.Cfg.MgmtIPv6Address, |
| 173 | + MgmtIPv6PrefixLen: n.Cfg.MgmtIPv6PrefixLength, |
| 174 | + MgmtIPv6GW: n.Cfg.MgmtIPv6Gateway, |
| 175 | + PartialCfg: n.partialStartupCfg, |
| 176 | + } |
| 177 | + |
| 178 | + viosCfgTpl, err := template.New("vios-config").Funcs(clabutils.CreateFuncs()).Parse(n.bootCfg) |
| 179 | + if err != nil { |
| 180 | + return fmt.Errorf("failed to parse cfg template for node %q: %w", n.Cfg.ShortName, err) |
| 181 | + } |
| 182 | + |
| 183 | + buf := new(bytes.Buffer) |
| 184 | + err = viosCfgTpl.Execute(buf, tplData) |
| 185 | + if err != nil { |
| 186 | + return fmt.Errorf("failed to execute cfg template for node %q: %w", n.Cfg.ShortName, err) |
| 187 | + } |
| 188 | + |
| 189 | + configDir := filepath.Join(n.Cfg.LabDir, n.ConfigDirName) |
| 190 | + dstCfg := filepath.Join(configDir, n.StartupCfgFName) |
| 191 | + err = clabutils.CreateFile(dstCfg, buf.String()) |
| 192 | + if err != nil { |
| 193 | + return fmt.Errorf("failed to write cfg file for node %q: %w", n.Cfg.ShortName, err) |
| 194 | + } |
| 195 | + |
| 196 | + return nil |
| 197 | +} |
| 198 | + |
| 199 | +// Stores the vars exposed in the config template |
| 200 | +type ViosTemplateData struct { |
| 201 | + Hostname string |
| 202 | + Username string |
| 203 | + Password string |
| 204 | + IsL2Node bool |
| 205 | + MgmtIPv4Addr string |
| 206 | + MgmtIPv4SubnetMask string |
| 207 | + MgmtIPv4GW string |
| 208 | + MgmtIPv6Addr string |
| 209 | + MgmtIPv6PrefixLen int |
| 210 | + MgmtIPv6GW string |
| 211 | + PartialCfg string |
| 212 | +} |
0 commit comments