diff --git a/clab/clab.go b/clab/clab.go index ec9226144..2bdb7796c 100644 --- a/clab/clab.go +++ b/clab/clab.go @@ -5,6 +5,7 @@ import ( "sync" "time" + "github.com/docker/docker/api/types" docker "github.com/docker/docker/client" log "github.com/sirupsen/logrus" ) @@ -99,3 +100,25 @@ func (c *cLab) CreateNode(ctx context.Context, node *Node, certs *certificates) } return c.CreateContainer(ctx, node) } + +// ExecPostDeployTasks executes tasks that some nodes might require to boot properly after start +func (c *cLab) ExecPostDeployTasks(ctx context.Context, node *Node) error { + switch node.Kind { + case "ceos": + log.Infof("Running postdeploy actions for '%s' node", node.ShortName) + // regenerate ceos config since it is now known which IP address docker assigned to this container + err := node.generateConfig(node.ResConfig) + if err != nil { + return err + } + log.Infof("Restarting '%s' node", node.ShortName) + // force stopping and start is faster than ContainerRestart + var timeout time.Duration = 1 + err = c.DockerClient.ContainerStop(ctx, node.ContainerID, &timeout) + if err != nil { + return err + } + err = c.DockerClient.ContainerStart(ctx, node.ContainerID, types.ContainerStartOptions{}) + } + return nil +} diff --git a/clab/config.go b/clab/config.go index 35c554599..e04ff1aa6 100644 --- a/clab/config.go +++ b/clab/config.go @@ -19,13 +19,16 @@ const ( dockerNetName = "clab" dockerNetIPv4Addr = "172.20.20.0/24" dockerNetIPv6Addr = "2001:172:20:20::/80" - - defaultConfigTemplate = "/etc/containerlab/templates/srl/srlconfig.tpl" ) // supported kinds var kinds = []string{"srl", "ceos", "linux", "alpine", "bridge"} +var defaultConfigTemplates = map[string]string{ + "srl": "/etc/containerlab/templates/srl/srlconfig.tpl", + "ceos": "/etc/containerlab/templates/arista/ceos.cfg.tpl", +} + var srlTypes = map[string]string{ "ixr6": "topology-7250IXR6.yml", "ixr10": "topology-7250IXR10.yml", @@ -79,31 +82,37 @@ type Config struct { // Node is a struct that contains the information of a container element type Node struct { - ShortName string - LongName string - Fqdn string - LabDir string - Index int - Group string - Kind string - Config string - NodeType string - Position string - License string - Image string - Topology string - EnvConf string - Sysctls map[string]string - User string - Cmd string - Env []string - Binds []string // Bind mounts strings (src:dest:options) - PortBindings nat.PortMap // PortBindings define the bindings between the container ports and host ports - PortSet nat.PortSet // PortSet define the ports that should be exposed on a container - - TLSCert string - TLSKey string - TLSAnchor string + ShortName string + LongName string + Fqdn string + LabDir string + Index int + Group string + Kind string + Config string // path to config template file that is used for config generation + ResConfig string // path to config file that is actually mounted to the container and is a result of templation + NodeType string + Position string + License string + Image string + Topology string + EnvConf string + Sysctls map[string]string + User string + Cmd string + Env []string + Binds []string // Bind mounts strings (src:dest:options) + PortBindings nat.PortMap // PortBindings define the bindings between the container ports and host ports + PortSet nat.PortSet // PortSet define the ports that should be exposed on a container + MgmtNet string // name of the docker network this node is connected to with its first interface + MgmtIPv4Address string + MgmtIPv4PrefixLength int + MgmtIPv6Address string + MgmtIPv6PrefixLength int + ContainerID string + TLSCert string + TLSKey string + TLSAnchor string } // Link is a struct that contains the information of a link between 2 containers @@ -230,7 +239,7 @@ func (c *cLab) configInitialization(nodeCfg *NodeConfig, kind string) string { if c.Config.Topology.Defaults.Config != "" { return c.Config.Topology.Defaults.Config } - return defaultConfigTemplate + return defaultConfigTemplates[kind] } func (c *cLab) imageInitialization(nodeCfg *NodeConfig, kind string) string { @@ -314,25 +323,24 @@ func (c *cLab) NewNode(nodeName string, nodeCfg NodeConfig, idx int) error { case "ceos": // initialize the global parameters with defaults, can be overwritten later node.Config = c.configInitialization(&nodeCfg, node.Kind) - //node.License = t.SRLLicense node.Image = c.imageInitialization(&nodeCfg, node.Kind) - //node.NodeType = "ixr6" node.Position = c.positionInitialization(&nodeCfg, node.Kind) // initialize specifc container information - node.Cmd = "/sbin/init systemd.setenv=INTFTYPE=eth systemd.setenv=ETBA=1 systemd.setenv=SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1 systemd.setenv=CEOS=1 systemd.setenv=EOS_PLATFORM=ceoslab systemd.setenv=container=docker" - //node.Cmd = "/sbin/init" + node.Cmd = "/sbin/init systemd.setenv=INTFTYPE=eth systemd.setenv=ETBA=4 systemd.setenv=SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1 systemd.setenv=CEOS=1 systemd.setenv=EOS_PLATFORM=ceoslab systemd.setenv=container=docker systemd.setenv=MAPETH0=1 systemd.setenv=MGMT_INTF=eth0" + node.Env = []string{ "CEOS=1", "EOS_PLATFORM=ceoslab", "container=docker", "ETBA=1", "SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1", - "INTFTYPE=eth"} + "INTFTYPE=eth", + "MAPETH0=1", + "MGMT_INTF=eth0"} node.User = "root" node.Group = c.groupInitialization(&nodeCfg, node.Kind) node.NodeType = nodeCfg.Type - node.Config = nodeCfg.Config node.Sysctls = make(map[string]string) node.Sysctls["net.ipv4.ip_forward"] = "0" @@ -342,6 +350,10 @@ func (c *cLab) NewNode(nodeName string, nodeCfg NodeConfig, idx int) error { node.Sysctls["net.ipv6.conf.all.autoconf"] = "0" node.Sysctls["net.ipv6.conf.default.autoconf"] = "0" + // mount config dir + cfgPath := filepath.Join(node.LabDir, "flash") + node.Binds = append(node.Binds, fmt.Sprint(cfgPath, ":/mnt/flash/")) + case "srl": // initialize the global parameters with defaults, can be overwritten later node.Config = c.configInitialization(&nodeCfg, node.Kind) diff --git a/clab/docker.go b/clab/docker.go index 94ec05509..67917bfdb 100644 --- a/clab/docker.go +++ b/clab/docker.go @@ -196,13 +196,14 @@ func (c *cLab) CreateContainer(ctx context.Context, node *Node) (err error) { if err != nil { return err } + log.Debugf("Container started: %s", node.LongName) nctx, cancelFn := context.WithTimeout(ctx, c.timeout) defer cancelFn() - cJson, err := c.DockerClient.ContainerInspect(nctx, cont.ID) + cJSON, err := c.DockerClient.ContainerInspect(nctx, cont.ID) if err != nil { return err } - return linkContainerNS(cJson.State.Pid, node.LongName) + return linkContainerNS(cJSON.State.Pid, node.LongName) } func (c *cLab) PullImageIfRequired(ctx context.Context, imageName string) error { diff --git a/clab/file.go b/clab/file.go index 39c722dff..6876794b1 100644 --- a/clab/file.go +++ b/clab/file.go @@ -132,19 +132,22 @@ func CreateDirectory(path string, perm os.FileMode) { } } -// CreateNodeDirStructure create the directory structure and files for the clab +// CreateNodeDirStructure create the directory structure and files for the lab nodes func (c *cLab) CreateNodeDirStructure(node *Node) (err error) { c.m.RLock() defer c.m.RUnlock() + + // create node directory in the lab directory + if node.Kind != "linux" && node.Kind != "bridge" { + CreateDirectory(node.LabDir, 0777) + } + switch node.Kind { case "srl": log.Infof("Create directory structure for SRL container: %s", node.ShortName) var src string var dst string - // create node directory in lab - CreateDirectory(node.LabDir, 0777) - // copy license file to node specific directory in lab src = node.License dst = path.Join(node.LabDir, "license.key") @@ -180,9 +183,20 @@ func (c *cLab) CreateNodeDirStructure(node *Node) (err error) { } log.Debugf("CopyFile src %s -> dst %s succeeded\n", src, dst) - case "alpine": case "linux": case "ceos": + // generate config directory + CreateDirectory(path.Join(node.LabDir, "flash"), 0777) + cfg := path.Join(node.LabDir, "flash", "startup-config") + node.ResConfig = cfg + if !fileExists(cfg) { + err = node.generateConfig(cfg) + if err != nil { + log.Errorf("node=%s, failed to generate config: %v", node.ShortName, err) + } + } else { + log.Debugf("Config file exists for node %s", node.ShortName) + } case "bridge": default: } @@ -192,6 +206,7 @@ func (c *cLab) CreateNodeDirStructure(node *Node) (err error) { // GenerateConfig generates configuration for the nodes func (node *Node) generateConfig(dst string) error { + log.Debugf("generating config for node %s from file %s", node.ShortName, node.Config) tpl, err := template.New(filepath.Base(node.Config)).ParseFiles(node.Config) if err != nil { return err diff --git a/cmd/deploy.go b/cmd/deploy.go index d62bfac64..f5f575e55 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -212,6 +212,24 @@ var deployCmd = &cobra.Command{ if len(containers) == 0 { return fmt.Errorf("no containers found") } + + log.Debug("enriching nodes with IP information...") + enrichNodes(containers, c.Nodes, c.Config.Mgmt.Network) + + wg = new(sync.WaitGroup) + wg.Add(len(c.Nodes)) + for _, node := range c.Nodes { + go func(node *clab.Node) { + defer wg.Done() + err := c.ExecPostDeployTasks(ctx, node) + if err != nil { + log.Errorf("failed to run postdeploy task for node %s: %v", node.ShortName, err) + } + }(node) + + } + wg.Wait() + log.Info("Writing /etc/hosts file") err = createHostsFile(containers, c.Config.Mgmt.Network) if err != nil { @@ -298,3 +316,20 @@ func hostsEntries(containers []types.Container, bridgeName string) []byte { } return buff.Bytes() } + +func enrichNodes(containers []types.Container, nodes map[string]*clab.Node, mgmtNet string) { + for _, c := range containers { + name = strings.Split(c.Names[0], "-")[2] + if node, ok := nodes[name]; ok { + // add network information + node.MgmtNet = mgmtNet + node.MgmtIPv4Address = c.NetworkSettings.Networks[mgmtNet].IPAddress + node.MgmtIPv4PrefixLength = c.NetworkSettings.Networks[mgmtNet].IPPrefixLen + node.MgmtIPv6Address = c.NetworkSettings.Networks[mgmtNet].GlobalIPv6Address + node.MgmtIPv6PrefixLength = c.NetworkSettings.Networks[mgmtNet].GlobalIPv6PrefixLen + + node.ContainerID = c.ID + } + + } +} diff --git a/cmd/destroy.go b/cmd/destroy.go index 0b407054c..a09209d90 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -63,7 +63,7 @@ var destroyCmd = &cobra.Command{ name = strings.TrimLeft(cont.Names[0], "/") } log.Infof("Stopping container: %s", name) - err = c.DeleteContainer(ctx, name) + err := c.DeleteContainer(ctx, name) if err != nil { log.Errorf("could not remove container '%s': %v", name, err) } diff --git a/templates/arista/ceos.cfg.tpl b/templates/arista/ceos.cfg.tpl new file mode 100644 index 000000000..83167d0b1 --- /dev/null +++ b/templates/arista/ceos.cfg.tpl @@ -0,0 +1,17 @@ +hostname {{ .ShortName }} +username admin privilege 15 secret admin +! +interface Management0 +{{ if .MgmtIPv4Address }}ip address {{ .MgmtIPv4Address }}/{{.MgmtIPv4PrefixLength}}{{end}} +{{ if .MgmtIPv6Address }}ipv6 address {{ .MgmtIPv6Address }}/{{.MgmtIPv6PrefixLength}}{{end}} +! +management api gnmi + transport grpc default +! +management api netconf + transport ssh default +! +management api http-commands + no shutdown +! +end \ No newline at end of file