From 10fa1a86ecf703f8f9c8aa80e3fa8d2123ac4d93 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 12 Apr 2021 22:51:39 +0200 Subject: [PATCH 01/33] initial_vrsros_config --- clab/config.go | 2 +- clab/config/ssh.go | 134 +++++++++++++++++++ clab/config/template.go | 213 +++++++++++++++++++++++++++++++ clab/config/utils.go | 88 +++++++++++++ clab/config/utils_test.go | 55 ++++++++ cmd/config.go | 116 +++++++++++++++++ go.mod | 4 +- lab-examples/vr05/conf1.clab.yml | 41 ++++++ templates/vr-sros/base-link.tmpl | 20 +++ templates/vr-sros/base-node.tmpl | 92 +++++++++++++ 10 files changed, 763 insertions(+), 2 deletions(-) create mode 100644 clab/config/ssh.go create mode 100644 clab/config/template.go create mode 100644 clab/config/utils.go create mode 100644 clab/config/utils_test.go create mode 100644 cmd/config.go create mode 100644 lab-examples/vr05/conf1.clab.yml create mode 100644 templates/vr-sros/base-link.tmpl create mode 100644 templates/vr-sros/base-node.tmpl diff --git a/clab/config.go b/clab/config.go index c632605e1..10a90456b 100644 --- a/clab/config.go +++ b/clab/config.go @@ -711,7 +711,7 @@ func (c *CLab) verifyRootNetnsInterfaceUniqueness() error { for _, e := range endpoints { if e.Node.Kind == "bridge" || e.Node.Kind == "ovs-bridge" || e.Node.Kind == "host" { if _, ok := rootNsIfaces[e.EndpointName]; ok { - return fmt.Errorf(`interface %s defined for node %s has already been used in other bridges, ovs-bridges or host interfaces. + return fmt.Errorf(`interface %s defined for node %s has already been used in other bridges, ovs-bridges or host interfaces. Make sure that nodes of these kinds use unique interface names`, e.EndpointName, e.Node.ShortName) } else { rootNsIfaces[e.EndpointName] = struct{}{} diff --git a/clab/config/ssh.go b/clab/config/ssh.go new file mode 100644 index 000000000..5eb58d964 --- /dev/null +++ b/clab/config/ssh.go @@ -0,0 +1,134 @@ +package config + +import ( + "fmt" + "io" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" +) + +type Session struct { + In io.Reader + Out io.WriteCloser + Session *ssh.Session +} + +func NewSession(username, password string, host string) (*Session, error) { + + sshConfig := &ssh.ClientConfig{ + User: username, + Auth: []ssh.AuthMethod{ + ssh.Password(password), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + connection, err := ssh.Dial("tcp", host, sshConfig) + if err != nil { + return nil, fmt.Errorf("failed to connect: %s", err) + } + session, err := connection.NewSession() + if err != nil { + return nil, err + } + sshIn, err := session.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("session stdout: %s", err) + } + sshOut, err := session.StdinPipe() + if err != nil { + return nil, fmt.Errorf("session stdin: %s", err) + } + + if err := session.Shell(); err != nil { + session.Close() + return nil, fmt.Errorf("session shell: %s", err) + } + + return &Session{ + Session: session, + In: sshIn, + Out: sshOut, + }, nil +} + +func (ses *Session) Close() { + log.Debugf("Closing sesison") + ses.Session.Close() +} + +func (ses *Session) Expect(send, expect string, timeout int) string { + rChan := make(chan string) + + go func() { + buf := make([]byte, 1024) + n, err := ses.In.Read(buf) //this reads the ssh terminal + tmpStr := "" + if err == nil { + tmpStr = string(buf[:n]) + } + for (err == nil) && (!strings.Contains(tmpStr, expect)) { + n, err = ses.In.Read(buf) + tmpStr += string(buf[:n]) + } + rChan <- tmpStr + }() + + time.Sleep(10 * time.Millisecond) + + if send != "" { + ses.Write(send) + } + + select { + case ret := <-rChan: + return ret + case <-time.After(time.Duration(timeout) * time.Second): + log.Warnf("timeout waiting for %s", expect) + } + return "" +} + +func (ses *Session) Write(command string) (int, error) { + returnCode, err := ses.Out.Write([]byte(command + "\r")) + return returnCode, err +} + +// send multiple config to a device +func SendConfig(cs []*ConfigSnippet) error { + host := fmt.Sprintf("%s:22", cs[0].TargetNode.LongName) + + ses, err := NewSession("admin", "admin", host) + if err != nil { + return fmt.Errorf("cannot connect to %s: %s", host, err) + } + defer ses.Close() + + log.Infof("Connected to %s\n", host) + //Read to first prompt + ses.Expect("", "#", 1) + // Enter config mode + ses.Expect("/configure global", "#", 10) + ses.Expect("discard", "#", 10) + + for _, snip := range cs { + for _, l := range snip.Config { + l = strings.TrimSpace(l) + if l == "" || strings.HasPrefix(l, "#") { + continue + } + ses.Expect(l, "#", 3) + // fmt.Write("((%s))", res) + } + + // Commit + commit := ses.Expect("commit", "commit", 10) + commit += ses.Expect("", "#", 10) + log.Infof("COMMIT %s\n%s", snip, commit) + } + + return nil +} diff --git a/clab/config/template.go b/clab/config/template.go new file mode 100644 index 000000000..41353f3ba --- /dev/null +++ b/clab/config/template.go @@ -0,0 +1,213 @@ +package config + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "path/filepath" + "strings" + "text/template" + + log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/clab" +) + +type labelMap map[string]string +type ConfigSnippet struct { + TargetNode *clab.Node + templateName, source string + // All the labels used to render the template + templateLabels *labelMap + // Lines of config + Config []string +} + +// internal template cache +var templates map[string]*template.Template + +func LoadTemplate(kind string, templatePath string) error { + if templates == nil { + templates = make(map[string]*template.Template) + } + if _, ok := templates[kind]; ok { + return nil + } + + tp := filepath.Join(templatePath, kind, "*.tmpl") + log.Debugf("Load templates from: %s", tp) + + ct := template.New(kind).Funcs(funcMap) + var err error + templates[kind], err = ct.ParseGlob(tp) + if err != nil { + log.Errorf("could not load template %s", err) + return err + } + return nil +} + +func RenderTemplate(kind, name string, labels labelMap) (*ConfigSnippet, error) { + t := templates[kind] + + buf := new(bytes.Buffer) + + err := t.ExecuteTemplate(buf, name, labels) + if err != nil { + log.Errorf("could not render template %s", err) + b, _ := json.MarshalIndent(labels, "", " ") + log.Debugf("%s\n", b) + return nil, err + } + + var res []string + s := bufio.NewScanner(buf) + for s.Scan() { + res = append(res, s.Text()) + } + + return &ConfigSnippet{ + templateLabels: &labels, + templateName: name, + Config: res, + }, nil +} + +func RenderNode(node *clab.Node) (*ConfigSnippet, error) { + kind := node.Labels["clab-node-kind"] + log.Debugf("render node %s [%s]\n", node.LongName, kind) + + res, err := RenderTemplate(kind, "base-node.tmpl", node.Labels) + if err != nil { + return nil, fmt.Errorf("render node %s [%s]: %s", node.LongName, kind, err) + } + res.source = "node" + res.TargetNode = node + return res, nil +} + +func RenderLink(link *clab.Link) (*ConfigSnippet, *ConfigSnippet, error) { + // Link labels/values are different on node A & B + l := make(map[string][]string) + + // Link IPs + ipA, ipB, err := linkIPfromSystemIP(link) + if err != nil { + return nil, nil, fmt.Errorf("%s: %s", link, err) + } + l["ip"] = []string{ipA.String(), ipB.String()} + l["systemip"] = []string{link.A.Node.Labels[systemIP], link.B.Node.Labels[systemIP]} + + // Split all fields with a comma... + for k, v := range link.Labels { + r := strings.Split(v, ",") + switch len(r) { + case 1: + case 2: + l[k] = r + default: + log.Warnf("%s: %s contains %d elements: %s", link, k, len(r), v) + } + } + + // Set default Link/Interface Names + if _, ok := l["name"]; !ok { + linkNr := link.Labels["linkNr"] + if len(linkNr) > 0 { + linkNr = "_" + linkNr + } + l["name"] = []string{fmt.Sprintf("to_%s%s", link.B.Node.ShortName, linkNr), + fmt.Sprintf("to_%s%s", link.A.Node.ShortName, linkNr)} + } + + log.Debugf("%s: %s\n", link, l) + + var res, resA *ConfigSnippet + + var curL labelMap + var curN *clab.Node + + for li := 0; li < 2; li++ { + if li == 0 { + // set current node as A + curN = link.A.Node + curL = make(labelMap) + for k, v := range l { + curL[k] = v[0] + if len(v) > 1 { + curL[k+"_far"] = v[1] + } + } + } else { + curN = link.B.Node + curL = make(labelMap) + for k, v := range l { + if len(v) == 1 { + curL[k] = v[0] + } else { + curL[k] = v[1] + curL[k+"_far"] = v[0] + } + } + } + // Render the links + kind := curN.Labels["clab-node-kind"] + log.Debugf("render %s on %s (%s) - %s", link, curN.LongName, kind, curL) + res, err = RenderTemplate(kind, "base-link.tmpl", curL) + if err != nil { + return nil, nil, fmt.Errorf("render %s on %s (%s): %s", link, curN.LongName, kind, err) + } + res.source = link.String() + res.TargetNode = curN + if li == 0 { + resA = res + } + } + return resA, res, nil +} + +// Implement stringer for conf snippet +func (c *ConfigSnippet) String() string { + return fmt.Sprintf("%s: %s %d lines of config", c.TargetNode.LongName, c.source, len(c.Config)) +} + +var funcMap = map[string]interface{}{ + "require": func(val interface{}) (interface{}, error) { + if val == nil { + return nil, errors.New("required value not set") + } + return val, nil + }, + "ip": func(val interface{}) (interface{}, error) { + s := fmt.Sprintf("%v", val) + a := strings.Split(s, "/") + return a[0], nil + }, + "ipmask": func(val interface{}) (interface{}, error) { + s := fmt.Sprintf("%v", val) + a := strings.Split(s, "/") + return a[1], nil + }, + "default": func(val interface{}, def interface{}) (interface{}, error) { + if val == nil { + return def, nil + } + return val, nil + }, + "contains": func(str interface{}, substr interface{}) (interface{}, error) { + return strings.Contains(fmt.Sprintf("%v", str), fmt.Sprintf("%v", substr)), nil + }, + "slice": func(val interface{}, start interface{}, end interface{}) (interface{}, error) { + v := fmt.Sprintf("%v", val) + s := int(start.(int)) + e := int(end.(int)) + if s < 0 { + s += len(v) + } + if e < 0 { + e += len(v) + } + return v[s:e], nil + }, +} diff --git a/clab/config/utils.go b/clab/config/utils.go new file mode 100644 index 000000000..6308e4d9b --- /dev/null +++ b/clab/config/utils.go @@ -0,0 +1,88 @@ +package config + +import ( + "fmt" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/clab" + "inet.af/netaddr" +) + +const ( + systemIP = "systemip" +) + +func linkIPfromSystemIP(link *clab.Link) (netaddr.IPPrefix, netaddr.IPPrefix, error) { + var ipA netaddr.IPPrefix + var err error + if linkIp, ok := link.Labels["ip"]; ok { + // calc far end IP + ipA, err = netaddr.ParseIPPrefix(linkIp) + if err != nil { + return ipA, ipA, fmt.Errorf("invalid ip %s", link.A.EndpointName) + } + } else { + // caluculate link IP from the system IPs - tbd + //var sysA, sysB netaddr.IPPrefix + + sysA, err := netaddr.ParseIPPrefix(link.A.Node.Labels[systemIP]) + if err != nil { + return ipA, ipA, fmt.Errorf("no 'ip' on link & the '%s' of %s: %s", systemIP, link.A.Node.ShortName, err) + } + sysB, err := netaddr.ParseIPPrefix(link.B.Node.Labels[systemIP]) + if err != nil { + return ipA, ipA, fmt.Errorf("no 'ip' on link & the '%s' of %s: %s", systemIP, link.B.Node.ShortName, err) + } + o2, o3, o4 := ipLastOctet(sysA.IP), ipLastOctet(sysB.IP), 0 + if o3 < o2 { + o2, o3, o4 = o3, o2, o4+1 + } + ipA, err = netaddr.ParseIPPrefix(fmt.Sprintf("1.%d.%d.%d/31", o2, o3, o4)) + if err != nil { + log.Errorf("could not create link IP from system-ip: %s", err) + } + } + return ipA, ipFarEnd(ipA), nil +} + +func ipLastOctet(in netaddr.IP) int { + s := in.String() + i := strings.LastIndexAny(s, ".") + if i < 0 { + i = strings.LastIndexAny(s, ":") + } + res, err := strconv.Atoi(s[i+1:]) + if err != nil { + log.Errorf("last octect %s from IP %s not a string", s[i+1:], s) + } + return res +} + +func ipFarEnd(in netaddr.IPPrefix) netaddr.IPPrefix { + if in.IP.Is4() && in.Bits == 32 { + return netaddr.IPPrefix{} + } + + n := in.IP.Next() + + if in.IP.Is4() && in.Bits <= 30 { + if !in.Contains(n) || !in.Contains(in.IP.Prior()) { + return netaddr.IPPrefix{} + } + if !in.Contains(n.Next()) { + n = in.IP.Prior() + } + } + if !in.Contains(n) { + n = in.IP.Prior() + } + if !in.Contains(n) { + return netaddr.IPPrefix{} + } + return netaddr.IPPrefix{ + IP: n, + Bits: in.Bits, + } +} diff --git a/clab/config/utils_test.go b/clab/config/utils_test.go new file mode 100644 index 000000000..fc50b943e --- /dev/null +++ b/clab/config/utils_test.go @@ -0,0 +1,55 @@ +package config + +import ( + "testing" + + "inet.af/netaddr" +) + +func TestFarEndIP(t *testing.T) { + + lst := map[string]string{ + "10.0.0.1/32": "", + + "10.0.0.0/31": "10.0.0.1/31", + "10.0.0.1/31": "10.0.0.0/31", + "10.0.0.2/31": "10.0.0.3/31", + "10.0.0.3/31": "10.0.0.2/31", + + "10.0.0.1/30": "10.0.0.2/30", + "10.0.0.2/30": "10.0.0.1/30", + "10.0.0.0/30": "", + "10.0.0.3/30": "", + "10.0.0.4/30": "", + "10.0.0.5/30": "10.0.0.6/30", + "10.0.0.6/30": "10.0.0.5/30", + } + + for k, v := range lst { + n := ipFarEnd(netaddr.MustParseIPPrefix(k)) + if n.IsZero() && v == "" { + continue + } + if v != n.String() { + t.Errorf("far end of %s, got %s, expected %s", k, n, v) + } + } + +} + +func TestIPLastOctect(t *testing.T) { + + lst := map[string]int{ + "10.0.0.1/32": 1, + "::1/32": 1, + } + + for k, v := range lst { + n := netaddr.MustParseIPPrefix(k) + lo := ipLastOctet(n.IP) + if v != lo { + t.Errorf("far end of %s, got %d, expected %d", k, lo, v) + } + } + +} diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 000000000..04a2d3fc7 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,116 @@ +package cmd + +import ( + "fmt" + "sync" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/srl-labs/containerlab/clab" + "github.com/srl-labs/containerlab/clab/config" +) + +// path to additional templates +var templatePath string + +// configCmd represents the config command +var configCmd = &cobra.Command{ + Use: "config", + Short: "configure a lab", + Long: "configure a lab based using templates and variables from the topology definition file\nreference: https://containerlab.srlinux.dev/cmd/config/", + Aliases: []string{"conf"}, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + var err error + if err = topoSet(); err != nil { + return err + } + + opts := []clab.ClabOption{ + clab.WithDebug(debug), + clab.WithTimeout(timeout), + clab.WithTopoFile(topo), + clab.WithEnvDockerClient(), + } + c := clab.NewContainerLab(opts...) + + //ctx, cancel := context.WithCancel(context.Background()) + //defer cancel() + + setFlags(c.Config) + log.Debugf("lab Conf: %+v", c.Config) + // Parse topology information + if err = c.ParseTopology(); err != nil { + return err + } + + // config map per node. each node gets a couple of config snippets []string + allConfig := make(map[string][]*config.ConfigSnippet) + + renderErr := 0 + + for _, node := range c.Nodes { + kind := node.Labels["clab-node-kind"] + err = config.LoadTemplate(kind, templatePath) + if err != nil { + return err + } + + res, err := config.RenderNode(node) + if err != nil { + log.Errorln(err) + renderErr += 1 + continue + } + allConfig[node.LongName] = append(allConfig[node.LongName], res) + } + + for lIdx, link := range c.Links { + + resA, resB, err := config.RenderLink(link) + if err != nil { + log.Errorf("%d. %s\n", lIdx, err) + renderErr += 1 + continue + } + allConfig[link.A.Node.LongName] = append(allConfig[link.A.Node.LongName], resA) + allConfig[link.B.Node.LongName] = append(allConfig[link.B.Node.LongName], resB) + + } + + if renderErr > 0 { + return fmt.Errorf("%d render warnings", renderErr) + } + + // Debug log all config to be deployed + for _, v := range allConfig { + for _, r := range v { + log.Infof("%s\n%s", r, r.Config) + + } + } + + var wg sync.WaitGroup + wg.Add(len(allConfig)) + for _, cs := range allConfig { + go func(configSnippets []*config.ConfigSnippet) { + defer wg.Done() + + err := config.SendConfig(configSnippets) + if err != nil { + log.Errorf("%s\n", err) + } + + }(cs) + } + wg.Wait() + + return nil + }, +} + +func init() { + rootCmd.AddCommand(configCmd) + configCmd.Flags().StringVarP(&templatePath, "templates", "", "", "specify template path") + configCmd.MarkFlagDirname("templates") +} diff --git a/go.mod b/go.mod index 5ebbae99f..545710f91 100644 --- a/go.mod +++ b/go.mod @@ -25,5 +25,7 @@ require ( github.com/ziutek/telnet v0.0.0-20180329124119-c3b780dc415b // indirect golang.org/x/term v0.0.0-20210503060354-a79de5458b56 gopkg.in/yaml.v2 v2.4.0 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + gopkg.in/yaml.v2 v2.3.0 + inet.af/netaddr v0.0.0-20210403172118-1e1430f727e0 // indirect ) - diff --git a/lab-examples/vr05/conf1.clab.yml b/lab-examples/vr05/conf1.clab.yml new file mode 100644 index 000000000..8fdc3f7b0 --- /dev/null +++ b/lab-examples/vr05/conf1.clab.yml @@ -0,0 +1,41 @@ +name: conf1 + +topology: + defaults: + kind: vr-sros + image: registry.srlinux.dev/pub/vr-sros:21.2.R1 + license: /home/kellerza/containerlabs/license-sros21.txt + labels: + isis_iid: 0 + nodes: + sr1: + labels: + systemip: 10.0.50.31/32 + sid_idx: 1 + sr2: + labels: + systemip: 10.0.50.32/32 + sid_idx: 2 + sr3: + labels: + systemip: 10.0.50.33/32 + sid_idx: 3 + sr4: + labels: + systemip: 10.0.50.34/32 + sid_idx: 4 + links: + - endpoints: [sr1:eth1, sr2:eth2] + labels: + port: 1/1/c1/1, 1/1/c2/1 + ip: 1.1.1.2/30 + vlan: 99 + - endpoints: [sr2:eth1, sr3:eth2] + labels: + port: 1/1/c1/1, 1/1/c2/1 + - endpoints: [sr3:eth1, sr4:eth2] + labels: + port: 1/1/c1/1, 1/1/c2/1 + - endpoints: [sr4:eth1, sr1:eth2] + labels: + port: 1/1/c1/1, 1/1/c2/1 diff --git a/templates/vr-sros/base-link.tmpl b/templates/vr-sros/base-link.tmpl new file mode 100644 index 000000000..57effc1ff --- /dev/null +++ b/templates/vr-sros/base-link.tmpl @@ -0,0 +1,20 @@ +{{ if contains .port "/c" }} +/configure port {{ slice .port 0 -2 }} admin-state enable +/configure port {{ slice .port 0 -2 }} connector breakout c1-10g +{{ end }} + +/configure port {{ .port }} admin-state enable + +/configure router interface {{ .name }} + ipv4 primary address {{ ip .ip }} + ipv4 primary prefix-length {{ ipmask .ip }} + port {{ .port }}:{{ default .vlan "1" }} + +/configure router isis {{ default .isis_iid "0" }} + interface {{ .name }} + +/configure router rsvp + interface {{ .name }} admin-state enable + +/configure router mpls + interface {{ .name }} admin-state enable diff --git a/templates/vr-sros/base-node.tmpl b/templates/vr-sros/base-node.tmpl new file mode 100644 index 000000000..ecd150c79 --- /dev/null +++ b/templates/vr-sros/base-node.tmpl @@ -0,0 +1,92 @@ +/configure system login-control idle-timeout 1440 + +/configure apply-groups ["baseport"] +/configure groups { + group "baseport" { + port "<.*\/[0-9]+>" { + # wanted to add this, but you really need the /1 context to exist + # admin-state enable + ethernet { + mode hybrid + encap-type dot1q + lldp { + dest-mac nearest-bridge { + notification true + receive true + transmit true + tx-tlvs { + #port-desc true + sys-name true + #sys-desc true + sys-cap true + } + tx-mgmt-address system { + admin-state enable + } + } + } + } + } +# port "<.*c[0-9]+>" { +# connector { +# breakout c1-10g +# } +# } + } +} +/configure groups { + group "basebgp" { + router "Base" { + bgp { + group "<.*>" { + admin-state enable + type internal + family { + vpn-ipv4 true + ipv4 true + vpn-ipv6 true + ipv6 true + } + } + neighbor "<.*>" { + admin-state enable + group "ibgp" + } + } + } + } +} + +/configure router bgp apply-groups ["basebgp"] + +/configure router interface "system" + ipv4 primary address {{ ip .systemip }} + ipv4 primary prefix-length {{ ipmask .systemip }} + admin-state enable + +/configure router + autonomous-system {{ default .as_number "64500" }} + mpls-labels sr-labels start {{ default .sid_start "19000" }} end {{ default .sid_end "30000" }} + +/configure router isis {{ default .isis_iid "0" }} + area-address 49.0000.000{{ default .isis_iid "0" }} + level-capability 2 + level 2 wide-metrics-only + interface "system" ipv4-node-sid index {{ .sid_idx }} + #database-export igp-identifier {{ default .isis_iid "0" }} bgp-ls-identifier value {{ default .isis_iid "0" }} + traffic-engineering + advertise-router-capability area + segment-routing prefix-sid-range global + segment-routing admin-state enable + admin-state enable + +/configure router rsvp + admin-state enable + interface system admin-state enable + +/configure router mpls + cspf-on-loose-hop + interface system admin-state enable + admin-state enable + pce-report rsvp-te true + pce-report sr-te true From 74c62f1fa373e55245daa7c1f8c7306c3e3da091 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 13 Apr 2021 13:02:56 +0200 Subject: [PATCH 02/33] template rendering, single values & tests --- clab/config/ssh.go | 2 +- clab/config/template.go | 58 +++++++++++++++++++++++++------- clab/config/template_test.go | 40 ++++++++++++++++++++++ lab-examples/vr05/conf1.clab.yml | 5 +-- 4 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 clab/config/template_test.go diff --git a/clab/config/ssh.go b/clab/config/ssh.go index 5eb58d964..12475427f 100644 --- a/clab/config/ssh.go +++ b/clab/config/ssh.go @@ -16,7 +16,7 @@ type Session struct { Session *ssh.Session } -func NewSession(username, password string, host string) (*Session, error) { +func NewSession(username, password, host string) (*Session, error) { sshConfig := &ssh.ClientConfig{ User: username, diff --git a/clab/config/template.go b/clab/config/template.go index 41353f3ba..ab2610f5a 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -103,8 +103,7 @@ func RenderLink(link *clab.Link) (*ConfigSnippet, *ConfigSnippet, error) { for k, v := range link.Labels { r := strings.Split(v, ",") switch len(r) { - case 1: - case 2: + case 1, 2: l[k] = r default: log.Warnf("%s: %s contains %d elements: %s", link, k, len(r), v) @@ -190,8 +189,18 @@ var funcMap = map[string]interface{}{ return a[1], nil }, "default": func(val interface{}, def interface{}) (interface{}, error) { - if val == nil { - return def, nil + if def == nil { + return nil, fmt.Errorf("default value expected") + } + switch val.(type) { + case string: + if val == "" { + return def, nil + } + default: + if val == nil { + return def, nil + } } return val, nil }, @@ -199,15 +208,40 @@ var funcMap = map[string]interface{}{ return strings.Contains(fmt.Sprintf("%v", str), fmt.Sprintf("%v", substr)), nil }, "slice": func(val interface{}, start interface{}, end interface{}) (interface{}, error) { - v := fmt.Sprintf("%v", val) - s := int(start.(int)) - e := int(end.(int)) - if s < 0 { - s += len(v) + // Start and end values + var s, e int + switch tmp := start.(type) { + case int: + s = tmp + default: + return nil, fmt.Errorf("int expeted for 2nd parameter %v", tmp) } - if e < 0 { - e += len(v) + switch tmp := end.(type) { + case int: + e = tmp + default: + return nil, fmt.Errorf("int expeted for 3rd parameter %v", tmp) + } + + // string or array + switch v := val.(type) { + case string: + if s < 0 { + s += len(v) + } + if e < 0 { + e += len(v) + } + return v[s:e], nil + case []interface{}: + if s < 0 { + s += len(v) + } + if e < 0 { + e += len(v) + } + return v[s:e], nil } - return v[s:e], nil + return nil, fmt.Errorf("not an array") }, } diff --git a/clab/config/template_test.go b/clab/config/template_test.go new file mode 100644 index 000000000..0de3cfc9b --- /dev/null +++ b/clab/config/template_test.go @@ -0,0 +1,40 @@ +package config + +import ( + "fmt" + "testing" +) + +func TestFuncMapDefault(t *testing.T) { + + // parameters & return + tSet := map[string][][]interface{}{ + "default": { + {nil, 1, 1}, + {5, 1, 5}, + {"", "1", "1"}, + {nil, nil, fmt.Errorf("")}, + }, + "contains": { + {"aa.", ".", true}, + {"ss", ".", false}, + }} + + for name, set := range tSet { + fn := funcMap[name].(func(interface{}, interface{}) (interface{}, error)) + + for _, p := range set { + exp := p[len(p)-1] + var expe error + switch v := exp.(type) { + case error: + expe = v + exp = nil + } + res, err := fn(p[0], p[1]) + if res != exp || (err != nil && expe == nil) { + t.Errorf("%v expected %v got %v error %v err: %v", p, exp, res, expe, err) + } + } + } +} diff --git a/lab-examples/vr05/conf1.clab.yml b/lab-examples/vr05/conf1.clab.yml index 8fdc3f7b0..526cc4fb0 100644 --- a/lab-examples/vr05/conf1.clab.yml +++ b/lab-examples/vr05/conf1.clab.yml @@ -29,10 +29,11 @@ topology: labels: port: 1/1/c1/1, 1/1/c2/1 ip: 1.1.1.2/30 - vlan: 99 + vlan: "99, 99" - endpoints: [sr2:eth1, sr3:eth2] labels: - port: 1/1/c1/1, 1/1/c2/1 + port: 1/1/c1/1, 1/1/c2/1ssh + vlan: 98 - endpoints: [sr3:eth1, sr4:eth2] labels: port: 1/1/c1/1, 1/1/c2/1 From 618d5476b55bdda062250e10144582c46235b707 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 13 Apr 2021 22:37:00 +0200 Subject: [PATCH 03/33] template rendering functions & tests --- clab/config/template.go | 81 ++++++++++++++++++++++++++++++-- clab/config/template_test.go | 47 +++++++++++++----- templates/vr-sros/base-node.tmpl | 11 +++-- 3 files changed, 120 insertions(+), 19 deletions(-) diff --git a/clab/config/template.go b/clab/config/template.go index ab2610f5a..1e63c97f4 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "path/filepath" + "strconv" "strings" "text/template" @@ -171,7 +172,20 @@ func (c *ConfigSnippet) String() string { return fmt.Sprintf("%s: %s %d lines of config", c.TargetNode.LongName, c.source, len(c.Config)) } +func typeof(val interface{}) string { + switch val.(type) { + case string: + return "string" + case int: + return "int" + } + return "" +} + var funcMap = map[string]interface{}{ + "expect": func(val interface{}, format interface{}) (interface{}, error) { + return nil, nil + }, "require": func(val interface{}) (interface{}, error) { if val == nil { return nil, errors.New("required value not set") @@ -192,21 +206,80 @@ var funcMap = map[string]interface{}{ if def == nil { return nil, fmt.Errorf("default value expected") } - switch val.(type) { + + switch v := val.(type) { case string: - if val == "" { + if v == "" { return def, nil } - default: - if val == nil { + case bool: + if !v { return def, nil } } + if val == nil { + return def, nil + } + + // If we have a input value, do some type checking + tval, tdef := typeof(val), typeof(def) + if tval == "string" && tdef == "int" { + if _, err := strconv.Atoi(val.(string)); err == nil { + tval = "int" + } + } + if tdef != tval { + return val, fmt.Errorf("expected type %v, got %v (value=%v)", tdef, tval, val) + } + + // Return the value return val, nil }, "contains": func(str interface{}, substr interface{}) (interface{}, error) { return strings.Contains(fmt.Sprintf("%v", str), fmt.Sprintf("%v", substr)), nil }, + "split": func(val interface{}, sep interface{}) (interface{}, error) { + // Start and end values + if val == nil { + return []interface{}{}, nil + } + s := fmt.Sprintf("%v", sep) + if sep == nil { + s = " " + } + + v := fmt.Sprintf("%v", val) + + res := strings.Split(v, s) + r := make([]interface{}, len(res)) + for i, p := range res { + r[i] = p + } + return r, nil + }, + "join": func(val interface{}, sep interface{}) (interface{}, error) { + s := fmt.Sprintf("%s", sep) + if sep == nil { + s = " " + } + // Start and end values + switch v := val.(type) { + case []interface{}: + if val == nil { + return "", nil + } + res := make([]string, len(v)) + for i, v := range v { + res[i] = fmt.Sprintf("%v", v) + } + return strings.Join(res, s), nil + case []string: + return strings.Join(v, s), nil + case []int, []int16, []int32: + return strings.Trim(strings.Replace(fmt.Sprint(v), " ", s, -1), "[]"), nil + } + return nil, fmt.Errorf("expected array [], got %v", val) + }, "slice": func(val interface{}, start interface{}, end interface{}) (interface{}, error) { // Start and end values var s, e int diff --git a/clab/config/template_test.go b/clab/config/template_test.go index 0de3cfc9b..b9a434182 100644 --- a/clab/config/template_test.go +++ b/clab/config/template_test.go @@ -10,30 +10,55 @@ func TestFuncMapDefault(t *testing.T) { // parameters & return tSet := map[string][][]interface{}{ "default": { - {nil, 1, 1}, + {0, nil, nil, true}, {5, 1, 5}, + {5, "1", 5, "invalid types"}, + {"a", 1, "a", "invalid types"}, {"", "1", "1"}, - {nil, nil, fmt.Errorf("")}, + {nil, nil, nil, true}, }, "contains": { {"aa.", ".", true}, {"ss", ".", false}, + }, + "split": { + {"a.a", ".", "[a a]"}, + {"a bb", nil, "[a bb]"}, + {nil, nil, "[]"}, + {nil, ".", "[]"}, + }, + "join": { + {[]interface{}{"a", "b"}, ".", "a.b"}, + {[]string{"a", "b"}, ".", "a.b"}, + {[]int{1, 2}, ".", "1.2"}, }} for name, set := range tSet { fn := funcMap[name].(func(interface{}, interface{}) (interface{}, error)) for _, p := range set { - exp := p[len(p)-1] - var expe error - switch v := exp.(type) { - case error: - expe = v - exp = nil - } + // Execute the funciton res, err := fn(p[0], p[1]) - if res != exp || (err != nil && expe == nil) { - t.Errorf("%v expected %v got %v error %v err: %v", p, exp, res, expe, err) + + // expect return value + exp := p[2] + exp_err := len(p) == 4 + + // Check errors + if err != nil && !exp_err { + t.Errorf("%v no err expected, err: %v", p, err) + } + if err == nil && exp_err { + t.Errorf("%v err expected, non found", p) + } + + // Check value + if res != exp { + // allow arrays (match on string only) + if fmt.Sprintf("%v", res) == fmt.Sprintf("%v", exp) { + continue + } + t.Errorf("%v expected %v got %v", p, exp, res) } } } diff --git a/templates/vr-sros/base-node.tmpl b/templates/vr-sros/base-node.tmpl index ecd150c79..8bf29749e 100644 --- a/templates/vr-sros/base-node.tmpl +++ b/templates/vr-sros/base-node.tmpl @@ -1,3 +1,6 @@ +{{ expect .systemip "ip" }} +{{ expect .sid_idx "0-1000" }} + /configure system login-control idle-timeout 1440 /configure apply-groups ["baseport"] @@ -66,13 +69,13 @@ /configure router autonomous-system {{ default .as_number "64500" }} - mpls-labels sr-labels start {{ default .sid_start "19000" }} end {{ default .sid_end "30000" }} + mpls-labels sr-labels start {{ default .sid_start 19000 }} end {{ default .sid_end 30000 }} -/configure router isis {{ default .isis_iid "0" }} - area-address 49.0000.000{{ default .isis_iid "0" }} +/configure router isis {{ default .isis_iid 0 }} + area-address 49.0000.000{{ default .isis_iid 0 }} level-capability 2 level 2 wide-metrics-only - interface "system" ipv4-node-sid index {{ .sid_idx }} + interface "system" ipv4-node-sid index {{ require .sid_idx }} #database-export igp-identifier {{ default .isis_iid "0" }} bgp-ls-identifier value {{ default .isis_iid "0" }} traffic-engineering advertise-router-capability area From 1072d2142c77302192241712fbd402836f460fb1 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Wed, 14 Apr 2021 22:27:59 +0200 Subject: [PATCH 04/33] transport&improved ssh/tests --- .gitignore | 3 +- clab/config/ssh.go | 284 +++++++++++++++++++++++++---------- clab/config/template.go | 57 +++---- clab/config/template_test.go | 138 ++++++++++------- clab/config/transport.go | 49 ++++++ cmd/config.go | 45 +++++- 6 files changed, 407 insertions(+), 169 deletions(-) create mode 100644 clab/config/transport.go diff --git a/.gitignore b/.gitignore index 5e62f984a..3502f5eca 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ containerlab .vscode/ .DS_Store __rd* -tests/out \ No newline at end of file +tests/out + diff --git a/clab/config/ssh.go b/clab/config/ssh.go index 12475427f..d98a7fedd 100644 --- a/clab/config/ssh.go +++ b/clab/config/ssh.go @@ -10,125 +10,245 @@ import ( "golang.org/x/crypto/ssh" ) -type Session struct { +type SshSession struct { In io.Reader Out io.WriteCloser Session *ssh.Session } -func NewSession(username, password, host string) (*Session, error) { - - sshConfig := &ssh.ClientConfig{ - User: username, - Auth: []ssh.AuthMethod{ - ssh.Password(password), - }, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - } +// The reply the execute command and the prompt. +type SshReply struct{ result, prompt string } + +// SshTransport setting needs to be set before calling Connect() +// SshTransport implement the Transport interface +type SshTransport struct { + // Channel used to read. Can use Expect to Write & read wit timeout + in chan SshReply + // SSH Session + ses *SshSession + // Contains the first read after connecting + BootMsg SshReply + + // SSH parameters used in connect + // defualt: 22 + Port int + // SSH Options + // required! + SshConfig *ssh.ClientConfig + // Character to split the incoming stream (#/$/>) + // default: # + PromptChar string + // Prompt parsing function. Default return the last line of the # + // default: DefaultPrompParse + PromptParse func(in *string) *SshReply +} - connection, err := ssh.Dial("tcp", host, sshConfig) - if err != nil { - return nil, fmt.Errorf("failed to connect: %s", err) - } - session, err := connection.NewSession() - if err != nil { - return nil, err - } - sshIn, err := session.StdoutPipe() - if err != nil { - return nil, fmt.Errorf("session stdout: %s", err) - } - sshOut, err := session.StdinPipe() - if err != nil { - return nil, fmt.Errorf("session stdin: %s", err) +// This is the default prompt parse function used by SSH transport +func DefaultPrompParse(in *string) *SshReply { + n := strings.LastIndex(*in, "\n") + res := (*in)[:n] + n = strings.LastIndex(res, "\n") + if n < 0 { + n = 0 } - if err := session.Shell(); err != nil { - session.Close() - return nil, fmt.Errorf("session shell: %s", err) + return &SshReply{ + result: (*in)[:n], + prompt: (*in)[n:] + "#", } - - return &Session{ - Session: session, - In: sshIn, - Out: sshOut, - }, nil } -func (ses *Session) Close() { - log.Debugf("Closing sesison") - ses.Session.Close() -} - -func (ses *Session) Expect(send, expect string, timeout int) string { - rChan := make(chan string) +// The channel does +func (t *SshTransport) InChannel() { + // Ensure we have one working channel + t.in = make(chan SshReply) + // setup a buffered string channel go func() { buf := make([]byte, 1024) - n, err := ses.In.Read(buf) //this reads the ssh terminal - tmpStr := "" + tmpS := "" + n, err := t.ses.In.Read(buf) //this reads the ssh terminal if err == nil { - tmpStr = string(buf[:n]) + tmpS = string(buf[:n]) } - for (err == nil) && (!strings.Contains(tmpStr, expect)) { - n, err = ses.In.Read(buf) - tmpStr += string(buf[:n]) + for err == nil { + + if strings.Contains(tmpS, "#") { + parts := strings.Split(tmpS, "#") + li := len(parts) - 1 + for i := 0; i < li; i++ { + t.in <- *t.PromptParse(&parts[i]) + } + tmpS = parts[li] + } + n, err = t.ses.In.Read(buf) + tmpS += string(buf[:n]) + } + log.Debugf("In Channel closing: %v", err) + t.in <- SshReply{ + result: tmpS, + prompt: "", } - rChan <- tmpStr }() - time.Sleep(10 * time.Millisecond) + t.BootMsg = t.Run("", 15) + log.Infof("%s\n", t.BootMsg.result) + log.Debugf("%s\n", t.BootMsg.prompt) +} - if send != "" { - ses.Write(send) +// Run a single command and wait for the reply +func (t *SshTransport) Run(command string, timeout int) SshReply { + if command != "" { + t.ses.Writeln(command) } + // Read from the channel with a timeout select { - case ret := <-rChan: + case ret := <-t.in: + if ret.result != "" { + rr := strings.Trim(ret.result, " \n") + + if strings.HasPrefix(rr, command) { + rr = rr[len(command):] + fmt.Println(rr) + } else { + log.Errorf("'%s' != '%s'\n--", rr, command) + if !strings.Contains(rr, command) { + log.Errorln("YY") + t.Run("", 10) + } + } + } return ret case <-time.After(time.Duration(timeout) * time.Second): - log.Warnf("timeout waiting for %s", expect) + log.Warnf("timeout waiting for prompt: %s", command) } - return "" + return SshReply{} } -func (ses *Session) Write(command string) (int, error) { - returnCode, err := ses.Out.Write([]byte(command + "\r")) - return returnCode, err +// Write a config snippet (a set of commands) +// Session NEEDS to be configurable for other kinds +// Part of the Transport interface +func (t *SshTransport) Write(snip *ConfigSnippet) error { + t.Run("/configure global", 2) + t.Run("discard", 2) + + c, b := 0, 0 + for _, l := range snip.Lines() { + l = strings.TrimSpace(l) + if l == "" || strings.HasPrefix(l, "#") { + continue + } + c += 1 + b += len(l) + t.Run(l, 3) + } + + // Commit + commit := t.Run("commit", 10) + //commit += t.Run("", 10) + log.Infof("COMMIT %s - %d lines %d bytes\n%s", snip, c, b, commit) + return nil } -// send multiple config to a device -func SendConfig(cs []*ConfigSnippet) error { - host := fmt.Sprintf("%s:22", cs[0].TargetNode.LongName) +// Connect to a host +// Part of the Transport interface +func (t *SshTransport) Connect(host string) error { + // Assign Default Values + if t.PromptParse == nil { + t.PromptParse = DefaultPrompParse + } + if t.PromptChar == "" { + t.PromptChar = "#" + } + if t.Port == 0 { + t.Port = 22 + } + if t.SshConfig == nil { + return fmt.Errorf("require auth credentials in SshConfig") + } - ses, err := NewSession("admin", "admin", host) - if err != nil { + // Start some client config + host = fmt.Sprintf("%s:%d", host, t.Port) + //sshConfig := &ssh.ClientConfig{} + //SshConfigWithUserNamePassword(sshConfig, "admin", "admin") + + ses_, err := NewSshSession(host, t.SshConfig) + if err != nil || ses_ == nil { return fmt.Errorf("cannot connect to %s: %s", host, err) } - defer ses.Close() + t.ses = ses_ log.Infof("Connected to %s\n", host) + t.InChannel() //Read to first prompt - ses.Expect("", "#", 1) - // Enter config mode - ses.Expect("/configure global", "#", 10) - ses.Expect("discard", "#", 10) - - for _, snip := range cs { - for _, l := range snip.Config { - l = strings.TrimSpace(l) - if l == "" || strings.HasPrefix(l, "#") { - continue - } - ses.Expect(l, "#", 3) - // fmt.Write("((%s))", res) - } + return nil +} - // Commit - commit := ses.Expect("commit", "commit", 10) - commit += ses.Expect("", "#", 10) - log.Infof("COMMIT %s\n%s", snip, commit) +// Close the Session and channels +// Part of the Transport interface +func (t *SshTransport) Close() { + // if t.in != nil { + // close(t.in) + // t.in = nil + // } + //t.ses.Close() +} + +// Add a basic username & password to a config. +// Will initilize the config if required +func SshConfigWithUserNamePassword(config *ssh.ClientConfig, username, password string) { + if config == nil { + config = &ssh.ClientConfig{} + } + config.User = username + if config.Auth == nil { + config.Auth = []ssh.AuthMethod{} } + config.Auth = append(config.Auth, ssh.Password(password)) + config.HostKeyCallback = ssh.InsecureIgnoreHostKey() +} - return nil +// Create a new SSH session (Dial, open in/out pipes and start the shell) +// pass the authntication details in sshConfig +func NewSshSession(host string, sshConfig *ssh.ClientConfig) (*SshSession, error) { + if !strings.Contains(host, ":") { + return nil, fmt.Errorf("include the port in the host: %s", host) + } + + connection, err := ssh.Dial("tcp", host, sshConfig) + if err != nil { + return nil, fmt.Errorf("failed to connect: %s", err) + } + session, err := connection.NewSession() + if err != nil { + return nil, err + } + sshIn, err := session.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("session stdout: %s", err) + } + sshOut, err := session.StdinPipe() + if err != nil { + return nil, fmt.Errorf("session stdin: %s", err) + } + if err := session.Shell(); err != nil { + session.Close() + return nil, fmt.Errorf("session shell: %s", err) + } + + return &SshSession{ + Session: session, + In: sshIn, + Out: sshOut, + }, nil +} + +func (ses *SshSession) Writeln(command string) (int, error) { + return ses.Out.Write([]byte(command + "\r")) +} + +func (ses *SshSession) Close() { + log.Debugf("Closing sesison") + ses.Session.Close() } diff --git a/clab/config/template.go b/clab/config/template.go index 1e63c97f4..2a5578b76 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -1,7 +1,6 @@ package config import ( - "bufio" "bytes" "encoding/json" "errors" @@ -15,16 +14,6 @@ import ( "github.com/srl-labs/containerlab/clab" ) -type labelMap map[string]string -type ConfigSnippet struct { - TargetNode *clab.Node - templateName, source string - // All the labels used to render the template - templateLabels *labelMap - // Lines of config - Config []string -} - // internal template cache var templates map[string]*template.Template @@ -49,7 +38,7 @@ func LoadTemplate(kind string, templatePath string) error { return nil } -func RenderTemplate(kind, name string, labels labelMap) (*ConfigSnippet, error) { +func RenderTemplate(kind, name string, labels stringMap) (*ConfigSnippet, error) { t := templates[kind] buf := new(bytes.Buffer) @@ -62,16 +51,10 @@ func RenderTemplate(kind, name string, labels labelMap) (*ConfigSnippet, error) return nil, err } - var res []string - s := bufio.NewScanner(buf) - for s.Scan() { - res = append(res, s.Text()) - } - return &ConfigSnippet{ templateLabels: &labels, templateName: name, - Config: res, + Data: buf.Bytes(), }, nil } @@ -125,14 +108,14 @@ func RenderLink(link *clab.Link) (*ConfigSnippet, *ConfigSnippet, error) { var res, resA *ConfigSnippet - var curL labelMap + var curL stringMap var curN *clab.Node for li := 0; li < 2; li++ { if li == 0 { // set current node as A curN = link.A.Node - curL = make(labelMap) + curL = make(stringMap) for k, v := range l { curL[k] = v[0] if len(v) > 1 { @@ -141,7 +124,7 @@ func RenderLink(link *clab.Link) (*ConfigSnippet, *ConfigSnippet, error) { } } else { curN = link.B.Node - curL = make(labelMap) + curL = make(stringMap) for k, v := range l { if len(v) == 1 { curL[k] = v[0] @@ -169,7 +152,12 @@ func RenderLink(link *clab.Link) (*ConfigSnippet, *ConfigSnippet, error) { // Implement stringer for conf snippet func (c *ConfigSnippet) String() string { - return fmt.Sprintf("%s: %s %d lines of config", c.TargetNode.LongName, c.source, len(c.Config)) + return fmt.Sprintf("%s: %s (%d bytes)", c.TargetNode.LongName, c.source, len(c.Data)) +} + +// Return the buffer as strings +func (c *ConfigSnippet) Lines() []string { + return strings.Split(string(c.Data), "\n") } func typeof(val interface{}) string { @@ -202,12 +190,20 @@ var funcMap = map[string]interface{}{ a := strings.Split(s, "/") return a[1], nil }, - "default": func(val interface{}, def interface{}) (interface{}, error) { - if def == nil { + "default": func(in ...interface{}) (interface{}, error) { + if len(in) < 2 { return nil, fmt.Errorf("default value expected") } + if len(in) > 2 { + return nil, fmt.Errorf("too many arguments") + } + + val := in[0] + def := in[1] switch v := val.(type) { + case nil: + return def, nil case string: if v == "" { return def, nil @@ -217,9 +213,9 @@ var funcMap = map[string]interface{}{ return def, nil } } - if val == nil { - return def, nil - } + // if val == nil { + // return def, nil + // } // If we have a input value, do some type checking tval, tdef := typeof(val), typeof(def) @@ -227,6 +223,11 @@ var funcMap = map[string]interface{}{ if _, err := strconv.Atoi(val.(string)); err == nil { tval = "int" } + if tdef == "str" { + if _, err := strconv.Atoi(def.(string)); err == nil { + tdef = "int" + } + } } if tdef != tval { return val, fmt.Errorf("expected type %v, got %v (value=%v)", tdef, tval, val) diff --git a/clab/config/template_test.go b/clab/config/template_test.go index b9a434182..251424cba 100644 --- a/clab/config/template_test.go +++ b/clab/config/template_test.go @@ -1,65 +1,97 @@ package config import ( + "bytes" "fmt" + "strings" "testing" + "text/template" ) -func TestFuncMapDefault(t *testing.T) { - - // parameters & return - tSet := map[string][][]interface{}{ - "default": { - {0, nil, nil, true}, - {5, 1, 5}, - {5, "1", 5, "invalid types"}, - {"a", 1, "a", "invalid types"}, - {"", "1", "1"}, - {nil, nil, nil, true}, - }, - "contains": { - {"aa.", ".", true}, - {"ss", ".", false}, - }, - "split": { - {"a.a", ".", "[a a]"}, - {"a bb", nil, "[a bb]"}, - {nil, nil, "[]"}, - {nil, ".", "[]"}, - }, - "join": { - {[]interface{}{"a", "b"}, ".", "a.b"}, - {[]string{"a", "b"}, ".", "a.b"}, - {[]int{1, 2}, ".", "1.2"}, - }} - - for name, set := range tSet { - fn := funcMap[name].(func(interface{}, interface{}) (interface{}, error)) - - for _, p := range set { - // Execute the funciton - res, err := fn(p[0], p[1]) - - // expect return value - exp := p[2] - exp_err := len(p) == 4 - - // Check errors - if err != nil && !exp_err { - t.Errorf("%v no err expected, err: %v", p, err) - } - if err == nil && exp_err { - t.Errorf("%v err expected, non found", p) - } +var test_set = map[string][]interface{}{ + // empty values + "default .x 0": {"0"}, + "default \"\" 0": {"0"}, + "default false 0": {"0"}, + // ints pass through ok + "default 0 1": {"0"}, + // errors + "default .x": {"", "default value expected"}, + "default .x 1 1": {"", "too many arguments"}, + // type check + "default .i5 0": {"5"}, + "default .sA 0": {"", "expected type int"}, + "default .sA \"5\"": {"A"}, + + "contains .sAAA \".\"": {"true"}, + "contains .sA \".\"": {"false"}, + + "split \"a.a\" \".\"": {"[a a]"}, + "split \"a bb\" \" \"": {"[a bb]"}, + "split \"a bb\" 0": {"[a bb]"}, + + "ip \"1.1.1.1/32\"": {"1.1.1.1"}, + "ipmask \"1.1.1.1/32\"": {"32"}, + + //"split \"a bb\" \" \" | join \"-\"": {"a-bb"}, +} + +// "split": { +// {nil, nil, "[]"}, +// {nil, ".", "[]"}, +// }, +// "join": { +// {[]interface{}{"a", "b"}, ".", "a.b"}, +// {[]string{"a", "b"}, ".", "a.b"}, +// {[]int{1, 2}, ".", "1.2"}, +// }} + +func render(templateS string, vars stringMap) (string, error) { + var err error + buf := new(bytes.Buffer) + ts := fmt.Sprintf("{{ %v }}", strings.Trim(templateS, "{} ")) + tem, err := template.New("").Funcs(funcMap).Parse(ts) + if err != nil { + return "invalide template", fmt.Errorf("invalid template") + } + err = tem.Execute(buf, vars) + return buf.String(), err +} - // Check value - if res != exp { - // allow arrays (match on string only) - if fmt.Sprintf("%v", res) == fmt.Sprintf("%v", exp) { - continue - } - t.Errorf("%v expected %v got %v", p, exp, res) +func TestRender1(t *testing.T) { + + l := stringMap{ + "i5": "5", + "sA": "A", + "sAAA": "aa.", + "v_str": "s", + } + + for tem, exp := range test_set { + res, err := render(tem, l) + + ss := fmt.Sprintf("{{ %v }} = %v", tem, res) + if err != nil { + ss += " error" + } + + exp_err := len(exp) > 1 + // Check errors + if exp_err { + if err == nil { + t.Errorf("%s: expected '%s' in error, non found", ss, exp[1]) + } else if !strings.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", exp[1])) { + t.Errorf("%s: expected '%s' in error, got %s", ss, exp[1], err) } + } else if err != nil { + t.Errorf("%s: no err expected, got %s", ss, err) + } + + // Check value + if res != exp[0] { + t.Errorf("%s: expected %v got %v", ss, exp[0], res) } + } + } diff --git a/clab/config/transport.go b/clab/config/transport.go new file mode 100644 index 000000000..0ed6c550b --- /dev/null +++ b/clab/config/transport.go @@ -0,0 +1,49 @@ +package config + +import ( + "fmt" + + "github.com/srl-labs/containerlab/clab" +) + +type stringMap map[string]string + +type ConfigSnippet struct { + TargetNode *clab.Node + Data []byte // the Rendered template + + // some info for tracing/debugging + templateName, source string + // All the variables used to render the template + templateLabels *stringMap +} + +type Transport interface { + // Connect to the target host + Connect(host string) error + // Execute some config + Write(snip *ConfigSnippet) error + Close() +} + +func WriteConfig(transport Transport, snips []*ConfigSnippet) error { + host := snips[0].TargetNode.LongName + + // the Kind should configure the transport parameters before + + err := transport.Connect(host) + if err != nil { + return fmt.Errorf("%s: %s", host, err) + } + + defer transport.Close() + + for _, snip := range snips { + err := transport.Write(snip) + if err != nil { + return fmt.Errorf("could not write config %s: %s", snip, err) + } + } + + return nil +} diff --git a/cmd/config.go b/cmd/config.go index 04a2d3fc7..1eeae8d57 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/srl-labs/containerlab/clab" "github.com/srl-labs/containerlab/clab/config" + "golang.org/x/crypto/ssh" ) // path to additional templates @@ -85,23 +86,43 @@ var configCmd = &cobra.Command{ // Debug log all config to be deployed for _, v := range allConfig { for _, r := range v { - log.Infof("%s\n%s", r, r.Config) + log.Infof("%s\n%v", r, r.Lines()) } } var wg sync.WaitGroup wg.Add(len(allConfig)) - for _, cs := range allConfig { - go func(configSnippets []*config.ConfigSnippet) { + for _, cs_ := range allConfig { + go func(cs []*config.ConfigSnippet) { defer wg.Done() - err := config.SendConfig(configSnippets) + var transport config.Transport + + ct, ok := cs[0].TargetNode.Labels["config.transport"] + if !ok { + ct = "ssh" + } + + if ct == "ssh" { + transport, _ = newSSHTransport(cs[0].TargetNode) + if err != nil { + log.Errorf("%s: %s", kind, err) + } + log.Info(transport.(*config.SshTransport).SshConfig) + } else if ct == "grpc" { + // newGRPCTransport + } else { + log.Errorf("Unknown transport: %s", ct) + return + } + + err := config.WriteConfig(transport, cs) if err != nil { log.Errorf("%s\n", err) } - }(cs) + }(cs_) } wg.Wait() @@ -109,6 +130,20 @@ var configCmd = &cobra.Command{ }, } +func newSSHTransport(node *clab.Node) (*config.SshTransport, error) { + switch node.Kind { + case "vr-sros", "srl": + c := &config.SshTransport{} + c.SshConfig = &ssh.ClientConfig{} + config.SshConfigWithUserNamePassword( + c.SshConfig, + clab.DefaultCredentials[node.Kind][0], + clab.DefaultCredentials[node.Kind][1]) + return c, nil + } + return nil, fmt.Errorf("no tranport implemented for kind: %s", kind) +} + func init() { rootCmd.AddCommand(configCmd) configCmd.Flags().StringVarP(&templatePath, "templates", "", "", "specify template path") From 2464a53f51f3fa18df462c1579358cd516c9b261 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 16 Apr 2021 18:11:37 +0200 Subject: [PATCH 05/33] almost there --- clab/config/functions.go | 230 +++++++++++++++++++++ clab/config/functions_test.go | 123 ++++++++++++ clab/config/ssh.go | 82 +++++--- clab/config/template.go | 333 ++++++++++--------------------- clab/config/template_test.go | 97 --------- clab/config/transport.go | 46 +++-- cmd/config.go | 32 +-- lab-examples/vr05/conf1.clab.yml | 4 +- templates/vr-sros/base-link.tmpl | 15 +- templates/vr-sros/base-node.tmpl | 72 +++---- 10 files changed, 610 insertions(+), 424 deletions(-) create mode 100644 clab/config/functions.go create mode 100644 clab/config/functions_test.go delete mode 100644 clab/config/template_test.go diff --git a/clab/config/functions.go b/clab/config/functions.go new file mode 100644 index 000000000..c57ae5f45 --- /dev/null +++ b/clab/config/functions.go @@ -0,0 +1,230 @@ +package config + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "inet.af/netaddr" +) + +func typeof(val interface{}) string { + switch val.(type) { + case string: + return "string" + case int, int16, int32: + return "int" + } + return "" +} + +func hasInt(val interface{}) (int, bool) { + if i, err := strconv.Atoi(fmt.Sprintf("%v", val)); err == nil { + return i, true + } + return 0, false +} + +func expectFunc(val interface{}, format string) (interface{}, error) { + t := typeof(val) + vals := fmt.Sprintf("%s", val) + + // known formats + switch format { + case "str", "string": + if t == "string" { + return "", nil + } + return "", fmt.Errorf("string expected, got %s (%v)", t, val) + case "int": + if _, ok := hasInt(val); ok { + return "", nil + } + return "", fmt.Errorf("int expected, got %s (%v)", t, val) + case "ip": + if _, err := netaddr.ParseIPPrefix(vals); err == nil { + return "", nil + } + return "", fmt.Errorf("IP/mask expected, got %v", val) + } + + // try range + if matched, _ := regexp.MatchString(`\d+-\d+`, format); matched { + iv, ok := hasInt(val) + if !ok { + return "", fmt.Errorf("int expected, got %s (%v)", t, val) + } + r := strings.Split(format, "-") + i0, _ := hasInt(r[0]) + i1, _ := hasInt(r[1]) + if i1 < i0 { + i0, i1 = i1, i0 + } + if i0 <= iv && iv <= i1 { + return "", nil + } + return "", fmt.Errorf("value (%d) expected to be in range %d-%d", iv, i0, i1) + } + + // Try regex + matched, err := regexp.MatchString(format, vals) + if err != nil || !matched { + return "", fmt.Errorf("value %s does not match regex %s %v", vals, format, err) + } + + return "", nil +} + +var funcMap = map[string]interface{}{ + "optional": func(val interface{}, format string) (interface{}, error) { + if val == nil { + return "", nil + } + return expectFunc(val, format) + }, + "expect": expectFunc, + // "require": func(val interface{}) (interface{}, error) { + // if val == nil { + // return nil, errors.New("required value not set") + // } + // return val, nil + // }, + "ip": func(val interface{}) (interface{}, error) { + s := fmt.Sprintf("%v", val) + a := strings.Split(s, "/") + return a[0], nil + }, + "ipmask": func(val interface{}) (interface{}, error) { + s := fmt.Sprintf("%v", val) + a := strings.Split(s, "/") + return a[1], nil + }, + "default": func(in ...interface{}) (interface{}, error) { + if len(in) < 2 { + return nil, fmt.Errorf("default value expected") + } + if len(in) > 2 { + return nil, fmt.Errorf("too many arguments") + } + + val := in[len(in)-1] + def := in[0] + + switch v := val.(type) { + case nil: + return def, nil + case string: + if v == "" { + return def, nil + } + case bool: + if !v { + return def, nil + } + } + // if val == nil { + // return def, nil + // } + + // If we have a input value, do some type checking + tval, tdef := typeof(val), typeof(def) + if tval == "string" && tdef == "int" { + if _, err := strconv.Atoi(val.(string)); err == nil { + tval = "int" + } + if tdef == "str" { + if _, err := strconv.Atoi(def.(string)); err == nil { + tdef = "int" + } + } + } + if tdef != tval { + return val, fmt.Errorf("expected type %v, got %v (value=%v)", tdef, tval, val) + } + + // Return the value + return val, nil + }, + "contains": func(substr string, str string) (interface{}, error) { + return strings.Contains(fmt.Sprintf("%v", str), fmt.Sprintf("%v", substr)), nil + }, + "split": func(sep string, val interface{}) (interface{}, error) { + // Start and end values + if val == nil { + return []interface{}{}, nil + } + if sep == "" { + sep = " " + } + + v := fmt.Sprintf("%v", val) + + res := strings.Split(v, sep) + r := make([]interface{}, len(res)) + for i, p := range res { + r[i] = p + } + return r, nil + }, + "join": func(sep string, val interface{}) (interface{}, error) { + if sep == "" { + sep = " " + } + // Start and end values + switch v := val.(type) { + case []interface{}: + if val == nil { + return "", nil + } + res := make([]string, len(v)) + for i, v := range v { + res[i] = fmt.Sprintf("%v", v) + } + return strings.Join(res, sep), nil + case []string: + return strings.Join(v, sep), nil + case []int, []int16, []int32: + return strings.Trim(strings.Replace(fmt.Sprint(v), " ", sep, -1), "[]"), nil + } + return nil, fmt.Errorf("expected array [], got %v", val) + }, + "slice": func(start, end int, val interface{}) (interface{}, error) { + // string or array + switch v := val.(type) { + case string: + if start < 0 { + start += len(v) + } + if end < 0 { + end += len(v) + } + return v[start:end], nil + case []interface{}: + if start < 0 { + start += len(v) + } + if end < 0 { + end += len(v) + } + return v[start:end], nil + } + return nil, fmt.Errorf("not an array") + }, + "index": func(idx int, val interface{}) (interface{}, error) { + // string or array + switch v := val.(type) { + case string: + if idx < 0 { + idx += len(v) + } + return v[idx], nil + case []interface{}: + if idx < 0 { + idx += len(v) + } + return v[idx], nil + } + return nil, fmt.Errorf("not an array") + }, +} diff --git a/clab/config/functions_test.go b/clab/config/functions_test.go new file mode 100644 index 000000000..fe9be3239 --- /dev/null +++ b/clab/config/functions_test.go @@ -0,0 +1,123 @@ +package config + +import ( + "bytes" + "fmt" + "strings" + "testing" + "text/template" +) + +var test_set = map[string][]string{ + // empty values + "default 0 .x": {"0"}, + ".x | default 0": {"0"}, + "default 0 \"\"": {"0"}, + "default 0 false": {"0"}, + "false | default 0": {"0"}, + // ints pass through ok + "default 1 0": {"0"}, + // errors + "default .x": {"", "default value expected"}, + "default .x 1 1": {"", "too many arguments"}, + // type check + "default 0 .i5": {"5"}, + "default 0 .sA": {"", "expected type int"}, + `default "5" .sA`: {"A"}, + + `contains "." .sAAA`: {"true"}, + `.sAAA | contains "."`: {"true"}, + `contains "." .sA`: {"false"}, + `.sA | contains "."`: {"false"}, + + `split "." "a.a"`: {"[a a]"}, + `split " " "a bb"`: {"[a bb]"}, + + `ip "1.1.1.1/32"`: {"1.1.1.1"}, + `"1.1.1.1" | ip`: {"1.1.1.1"}, + `ipmask "1.1.1.1/32"`: {"32"}, + `"1.1.1.1/32" | split "/" | slice 0 1 | join ""`: {"1.1.1.1"}, + `"1.1.1.1/32" | split "/" | slice 1 2 | join ""`: {"32"}, + + `split " " "a bb" | join "-"`: {"a-bb"}, + `split "" ""`: {"[]"}, + `split "abc" ""`: {"[]"}, + + `"1.1.1.1/32" | split "/" | index 1`: {"32"}, + `"1.1.1.1/32" | split "/" | index -1`: {"32"}, + `"1.1.1.1/32" | split "/" | index -2`: {"1.1.1.1"}, + `"1.1.1.1/32" | split "/" | index -3`: {"", "out of range"}, + `"1.1.1.1/32" | split "/" | index 2`: {"", "out of range"}, + + `expect "1.1.1.1/32" "ip"`: {""}, + `expect "1.1.1.1" "ip"`: {"", "IP/mask"}, + `expect "1" "0-10"`: {""}, + `expect "1" "10-10"`: {"", "range"}, + `expect "1.1" "\\d+\\.\\d+"`: {""}, + `expect 11 "\\d"`: {""}, + `expect 11 "\\d+"`: {""}, + `expect "abc" "^[a-z]+$"`: {""}, + + `expect 1 "int"`: {""}, + `expect 1 "str"`: {"", "string expected"}, + `expect 1 "string"`: {"", "string expected"}, + `expect .i5 "int"`: {""}, + `expect "5" "int"`: {""}, // hasInt + `expect "aa" "int"`: {"", "int expected"}, + + `optional 1 "int"`: {""}, + `optional .x "int"`: {""}, + `optional .x "str"`: {""}, + `optional .i5 "str"`: {""}, // corner case, although it hasInt everything is always a string +} + +func render(templateS string, vars map[string]string) (string, error) { + var err error + buf := new(bytes.Buffer) + ts := fmt.Sprintf("{{ %v }}", strings.Trim(templateS, "{} ")) + tem, err := template.New("").Funcs(funcMap).Parse(ts) + if err != nil { + return "", fmt.Errorf("invalid template") + } + err = tem.Execute(buf, vars) + return buf.String(), err +} + +func TestRender1(t *testing.T) { + + l := map[string]string{ + "i5": "5", + "sA": "A", + "sAAA": "aa.", + "dot": ".", + "space": " ", + } + + for tem, exp := range test_set { + res, err := render(tem, l) + + e := []string{fmt.Sprintf(`{{ %v }} = "%v", error=%v`, tem, res, err)} + + // Check value + if res != exp[0] { + e = append(e, fmt.Sprintf("- expected value = %v", exp[0])) + } + + // Check errors + if len(exp) > 1 { + ee := fmt.Sprintf("- expected error with %s", exp[1]) + if err == nil { + e = append(e, ee) + } else if !strings.Contains(err.Error(), exp[1]) { + e = append(e, ee) + } + } else if err != nil { + e = append(e, "- no error expected") + } + + if len(e) > 1 { + t.Error(strings.Join(e, "\n")) + } + } + +} diff --git a/clab/config/ssh.go b/clab/config/ssh.go index d98a7fedd..3847de56e 100644 --- a/clab/config/ssh.go +++ b/clab/config/ssh.go @@ -16,6 +16,9 @@ type SshSession struct { Session *ssh.Session } +// Display the SSH login message +var LoginMessages bool + // The reply the execute command and the prompt. type SshReply struct{ result, prompt string } @@ -27,7 +30,7 @@ type SshTransport struct { // SSH Session ses *SshSession // Contains the first read after connecting - BootMsg SshReply + LoginMessage SshReply // SSH parameters used in connect // defualt: 22 @@ -46,12 +49,17 @@ type SshTransport struct { // This is the default prompt parse function used by SSH transport func DefaultPrompParse(in *string) *SshReply { n := strings.LastIndex(*in, "\n") + if strings.Contains((*in)[n:], " ") { + return &SshReply{ + result: *in, + prompt: "", + } + } res := (*in)[:n] n = strings.LastIndex(res, "\n") if n < 0 { n = 0 } - return &SshReply{ result: (*in)[:n], prompt: (*in)[n:] + "#", @@ -91,9 +99,11 @@ func (t *SshTransport) InChannel() { } }() - t.BootMsg = t.Run("", 15) - log.Infof("%s\n", t.BootMsg.result) - log.Debugf("%s\n", t.BootMsg.prompt) + t.LoginMessage = t.Run("", 15) + if LoginMessages { + log.Infof("%s\n", t.LoginMessage.result) + } + //log.Debugf("%s\n", t.BootMsg.prompt) } // Run a single command and wait for the reply @@ -102,28 +112,44 @@ func (t *SshTransport) Run(command string, timeout int) SshReply { t.ses.Writeln(command) } - // Read from the channel with a timeout - select { - case ret := <-t.in: - if ret.result != "" { + sHistory := "" + + for { + // Read from the channel with a timeout + select { + case <-time.After(time.Duration(timeout) * time.Second): + log.Warnf("timeout waiting for prompt: %s", command) + return SshReply{} + case ret := <-t.in: + if ret.prompt == "" && ret.result != "" { + // we should continue reading... + sHistory += ret.result + timeout = 1 // reduce timeout, node is already sending data + continue + } + if ret.result == "" && ret.prompt == "" { + log.Errorf("received zero?") + continue + } rr := strings.Trim(ret.result, " \n") + if sHistory != "" { + rr = sHistory + rr + sHistory = "" + } if strings.HasPrefix(rr, command) { - rr = rr[len(command):] - fmt.Println(rr) - } else { - log.Errorf("'%s' != '%s'\n--", rr, command) - if !strings.Contains(rr, command) { - log.Errorln("YY") - t.Run("", 10) - } + rr = strings.Trim(rr[len(command):], " \n\r") + // fmt.Print(rr) + } else if !strings.Contains(rr, command) { + sHistory = rr + continue + } + return SshReply{ + result: rr, + prompt: ret.prompt, } } - return ret - case <-time.After(time.Duration(timeout) * time.Second): - log.Warnf("timeout waiting for prompt: %s", command) } - return SshReply{} } // Write a config snippet (a set of commands) @@ -147,7 +173,7 @@ func (t *SshTransport) Write(snip *ConfigSnippet) error { // Commit commit := t.Run("commit", 10) //commit += t.Run("", 10) - log.Infof("COMMIT %s - %d lines %d bytes\n%s", snip, c, b, commit) + log.Infof("COMMIT %s - %d lines %d bytes\n%s", snip, c, b, commit.result) return nil } @@ -188,11 +214,11 @@ func (t *SshTransport) Connect(host string) error { // Close the Session and channels // Part of the Transport interface func (t *SshTransport) Close() { - // if t.in != nil { - // close(t.in) - // t.in = nil - // } - //t.ses.Close() + if t.in != nil { + close(t.in) + t.in = nil + } + t.ses.Close() } // Add a basic username & password to a config. @@ -249,6 +275,6 @@ func (ses *SshSession) Writeln(command string) (int, error) { } func (ses *SshSession) Close() { - log.Debugf("Closing sesison") + log.Debugf("Closing session") ses.Session.Close() } diff --git a/clab/config/template.go b/clab/config/template.go index 2a5578b76..0fc8ec240 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -3,10 +3,8 @@ package config import ( "bytes" "encoding/json" - "errors" "fmt" "path/filepath" - "strconv" "strings" "text/template" @@ -14,6 +12,16 @@ import ( "github.com/srl-labs/containerlab/clab" ) +type ConfigSnippet struct { + TargetNode *clab.Node + // the Rendered template + Data []byte + // some info for tracing/debugging + templateName, source string + // All the variables used to render the template + vars *map[string]string +} + // internal template cache var templates map[string]*template.Template @@ -38,121 +46,133 @@ func LoadTemplate(kind string, templatePath string) error { return nil } -func RenderTemplate(kind, name string, labels stringMap) (*ConfigSnippet, error) { - t := templates[kind] +func (c *ConfigSnippet) Render() error { + t := templates[c.TargetNode.Kind] + buf := new(strings.Builder) + c.Data = nil - buf := new(bytes.Buffer) + varsP, _ := json.MarshalIndent(c.vars, "", " ") + log.Debugf("Render %s vars=%s\n", c.String(), varsP) - err := t.ExecuteTemplate(buf, name, labels) + err := t.ExecuteTemplate(buf, c.templateName, c.vars) if err != nil { - log.Errorf("could not render template %s", err) - b, _ := json.MarshalIndent(labels, "", " ") - log.Debugf("%s\n", b) - return nil, err + log.Errorf("could not render template: %s %s vars=%s\n", c.String(), err, varsP) + return fmt.Errorf("could not render template: %s %s", c.String(), err) } - return &ConfigSnippet{ - templateLabels: &labels, - templateName: name, - Data: buf.Bytes(), - }, nil + // Strip blank lines + res := strings.Trim(buf.String(), "\n") + res = strings.ReplaceAll(res, "\n\n\n", "\n\n") + c.Data = []byte(res) + + return nil } -func RenderNode(node *clab.Node) (*ConfigSnippet, error) { - kind := node.Labels["clab-node-kind"] - log.Debugf("render node %s [%s]\n", node.LongName, kind) +func RenderNode(node *clab.Node) ([]ConfigSnippet, error) { + snips := []ConfigSnippet{} + nc := GetNodeConfigFromLabels(node.Labels) - res, err := RenderTemplate(kind, "base-node.tmpl", node.Labels) - if err != nil { - return nil, fmt.Errorf("render node %s [%s]: %s", node.LongName, kind, err) + for _, tn := range nc.Templates { + tn = fmt.Sprintf("%s-node.tmpl", tn) + snip := ConfigSnippet{ + vars: &nc.Vars, + templateName: tn, + TargetNode: node, + source: "node", + } + + err := snip.Render() + if err != nil { + return nil, err + } + snips = append(snips, snip) } - res.source = "node" - res.TargetNode = node - return res, nil + return snips, nil } -func RenderLink(link *clab.Link) (*ConfigSnippet, *ConfigSnippet, error) { +func RenderLink(link *clab.Link) ([]ConfigSnippet, error) { // Link labels/values are different on node A & B - l := make(map[string][]string) + vars := make(map[string][]string) + + ncA := GetNodeConfigFromLabels(link.A.Node.Labels) + ncB := GetNodeConfigFromLabels(link.B.Node.Labels) + linkVars := link.Labels // Link IPs ipA, ipB, err := linkIPfromSystemIP(link) if err != nil { - return nil, nil, fmt.Errorf("%s: %s", link, err) + return nil, fmt.Errorf("%s: %s", link, err) } - l["ip"] = []string{ipA.String(), ipB.String()} - l["systemip"] = []string{link.A.Node.Labels[systemIP], link.B.Node.Labels[systemIP]} + vars["ip"] = []string{ipA.String(), ipB.String()} + vars[systemIP] = []string{ncA.Vars[systemIP], ncB.Vars[systemIP]} // Split all fields with a comma... - for k, v := range link.Labels { + for k, v := range linkVars { r := strings.Split(v, ",") switch len(r) { case 1, 2: - l[k] = r + vars[k] = r default: - log.Warnf("%s: %s contains %d elements: %s", link, k, len(r), v) + log.Warnf("%s: %s contains %d elements, should be 1 or 2: %s", link.String(), k, len(r), v) } } // Set default Link/Interface Names - if _, ok := l["name"]; !ok { - linkNr := link.Labels["linkNr"] + if _, ok := vars["name"]; !ok { + linkNr := linkVars["linkNr"] if len(linkNr) > 0 { linkNr = "_" + linkNr } - l["name"] = []string{fmt.Sprintf("to_%s%s", link.B.Node.ShortName, linkNr), + vars["name"] = []string{fmt.Sprintf("to_%s%s", link.B.Node.ShortName, linkNr), fmt.Sprintf("to_%s%s", link.A.Node.ShortName, linkNr)} } - log.Debugf("%s: %s\n", link, l) + snips := []ConfigSnippet{} - var res, resA *ConfigSnippet + for li := 0; li < 2; li++ { + // Current Node + curNode := link.A.Node + if li == 1 { + curNode = link.B.Node + } + // Current Vars + curVars := make(map[string]string) + for k, v := range vars { + if len(v) == 1 { + curVars[k] = strings.Trim(v[0], " \n\t") + } else { + curVars[k] = strings.Trim(v[li], " \n\t") + curVars[k+"_far"] = strings.Trim(v[(li+1)%2], " \n\t") + } + } - var curL stringMap - var curN *clab.Node + curNodeC := GetNodeConfigFromLabels(curNode.Labels) - for li := 0; li < 2; li++ { - if li == 0 { - // set current node as A - curN = link.A.Node - curL = make(stringMap) - for k, v := range l { - curL[k] = v[0] - if len(v) > 1 { - curL[k+"_far"] = v[1] - } + for _, tn := range curNodeC.Templates { + snip := ConfigSnippet{ + vars: &curVars, + templateName: fmt.Sprintf("%s-link.tmpl", tn), + TargetNode: curNode, + source: link.String(), } - } else { - curN = link.B.Node - curL = make(stringMap) - for k, v := range l { - if len(v) == 1 { - curL[k] = v[0] - } else { - curL[k] = v[1] - curL[k+"_far"] = v[0] - } + err := snip.Render() + //res, err := RenderTemplate(kind, tn, curVars, curNode, link.String()) + if err != nil { + return nil, fmt.Errorf("render %s on %s (%s): %s", link, curNode.LongName, curNode.Kind, err) } - } - // Render the links - kind := curN.Labels["clab-node-kind"] - log.Debugf("render %s on %s (%s) - %s", link, curN.LongName, kind, curL) - res, err = RenderTemplate(kind, "base-link.tmpl", curL) - if err != nil { - return nil, nil, fmt.Errorf("render %s on %s (%s): %s", link, curN.LongName, kind, err) - } - res.source = link.String() - res.TargetNode = curN - if li == 0 { - resA = res + snips = append(snips, snip) } } - return resA, res, nil + return snips, nil } // Implement stringer for conf snippet func (c *ConfigSnippet) String() string { - return fmt.Sprintf("%s: %s (%d bytes)", c.TargetNode.LongName, c.source, len(c.Data)) + s := fmt.Sprintf("%s %s using %s/%s", c.TargetNode.ShortName, c.source, c.TargetNode.Kind, c.templateName) + if c.Data != nil { + s += fmt.Sprintf(" (%d lines)", bytes.Count(c.Data, []byte("\n"))+1) + } + return s } // Return the buffer as strings @@ -160,162 +180,19 @@ func (c *ConfigSnippet) Lines() []string { return strings.Split(string(c.Data), "\n") } -func typeof(val interface{}) string { - switch val.(type) { - case string: - return "string" - case int: - return "int" - } - return "" -} - -var funcMap = map[string]interface{}{ - "expect": func(val interface{}, format interface{}) (interface{}, error) { - return nil, nil - }, - "require": func(val interface{}) (interface{}, error) { - if val == nil { - return nil, errors.New("required value not set") - } - return val, nil - }, - "ip": func(val interface{}) (interface{}, error) { - s := fmt.Sprintf("%v", val) - a := strings.Split(s, "/") - return a[0], nil - }, - "ipmask": func(val interface{}) (interface{}, error) { - s := fmt.Sprintf("%v", val) - a := strings.Split(s, "/") - return a[1], nil - }, - "default": func(in ...interface{}) (interface{}, error) { - if len(in) < 2 { - return nil, fmt.Errorf("default value expected") - } - if len(in) > 2 { - return nil, fmt.Errorf("too many arguments") - } - - val := in[0] - def := in[1] - - switch v := val.(type) { - case nil: - return def, nil - case string: - if v == "" { - return def, nil - } - case bool: - if !v { - return def, nil - } - } - // if val == nil { - // return def, nil - // } +// Print the configSnippet +func (c *ConfigSnippet) Print(printLines int) { + vars, _ := json.MarshalIndent(c.vars, "", " ") - // If we have a input value, do some type checking - tval, tdef := typeof(val), typeof(def) - if tval == "string" && tdef == "int" { - if _, err := strconv.Atoi(val.(string)); err == nil { - tval = "int" - } - if tdef == "str" { - if _, err := strconv.Atoi(def.(string)); err == nil { - tdef = "int" - } - } - } - if tdef != tval { - return val, fmt.Errorf("expected type %v, got %v (value=%v)", tdef, tval, val) - } - - // Return the value - return val, nil - }, - "contains": func(str interface{}, substr interface{}) (interface{}, error) { - return strings.Contains(fmt.Sprintf("%v", str), fmt.Sprintf("%v", substr)), nil - }, - "split": func(val interface{}, sep interface{}) (interface{}, error) { - // Start and end values - if val == nil { - return []interface{}{}, nil - } - s := fmt.Sprintf("%v", sep) - if sep == nil { - s = " " - } - - v := fmt.Sprintf("%v", val) - - res := strings.Split(v, s) - r := make([]interface{}, len(res)) - for i, p := range res { - r[i] = p - } - return r, nil - }, - "join": func(val interface{}, sep interface{}) (interface{}, error) { - s := fmt.Sprintf("%s", sep) - if sep == nil { - s = " " - } - // Start and end values - switch v := val.(type) { - case []interface{}: - if val == nil { - return "", nil - } - res := make([]string, len(v)) - for i, v := range v { - res[i] = fmt.Sprintf("%v", v) - } - return strings.Join(res, s), nil - case []string: - return strings.Join(v, s), nil - case []int, []int16, []int32: - return strings.Trim(strings.Replace(fmt.Sprint(v), " ", s, -1), "[]"), nil - } - return nil, fmt.Errorf("expected array [], got %v", val) - }, - "slice": func(val interface{}, start interface{}, end interface{}) (interface{}, error) { - // Start and end values - var s, e int - switch tmp := start.(type) { - case int: - s = tmp - default: - return nil, fmt.Errorf("int expeted for 2nd parameter %v", tmp) - } - switch tmp := end.(type) { - case int: - e = tmp - default: - return nil, fmt.Errorf("int expeted for 3rd parameter %v", tmp) + s := "" + if printLines > 0 { + cl := strings.SplitN(string(c.Data), "\n", printLines+1) + if len(cl) > printLines { + cl[printLines] = "..." } + s = "\n | " + s += strings.Join(cl, s) + } - // string or array - switch v := val.(type) { - case string: - if s < 0 { - s += len(v) - } - if e < 0 { - e += len(v) - } - return v[s:e], nil - case []interface{}: - if s < 0 { - s += len(v) - } - if e < 0 { - e += len(v) - } - return v[s:e], nil - } - return nil, fmt.Errorf("not an array") - }, + log.Infof("%s %s%s\n", c.String(), vars, s) } diff --git a/clab/config/template_test.go b/clab/config/template_test.go deleted file mode 100644 index 251424cba..000000000 --- a/clab/config/template_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package config - -import ( - "bytes" - "fmt" - "strings" - "testing" - "text/template" -) - -var test_set = map[string][]interface{}{ - // empty values - "default .x 0": {"0"}, - "default \"\" 0": {"0"}, - "default false 0": {"0"}, - // ints pass through ok - "default 0 1": {"0"}, - // errors - "default .x": {"", "default value expected"}, - "default .x 1 1": {"", "too many arguments"}, - // type check - "default .i5 0": {"5"}, - "default .sA 0": {"", "expected type int"}, - "default .sA \"5\"": {"A"}, - - "contains .sAAA \".\"": {"true"}, - "contains .sA \".\"": {"false"}, - - "split \"a.a\" \".\"": {"[a a]"}, - "split \"a bb\" \" \"": {"[a bb]"}, - "split \"a bb\" 0": {"[a bb]"}, - - "ip \"1.1.1.1/32\"": {"1.1.1.1"}, - "ipmask \"1.1.1.1/32\"": {"32"}, - - //"split \"a bb\" \" \" | join \"-\"": {"a-bb"}, -} - -// "split": { -// {nil, nil, "[]"}, -// {nil, ".", "[]"}, -// }, -// "join": { -// {[]interface{}{"a", "b"}, ".", "a.b"}, -// {[]string{"a", "b"}, ".", "a.b"}, -// {[]int{1, 2}, ".", "1.2"}, -// }} - -func render(templateS string, vars stringMap) (string, error) { - var err error - buf := new(bytes.Buffer) - ts := fmt.Sprintf("{{ %v }}", strings.Trim(templateS, "{} ")) - tem, err := template.New("").Funcs(funcMap).Parse(ts) - if err != nil { - return "invalide template", fmt.Errorf("invalid template") - } - err = tem.Execute(buf, vars) - return buf.String(), err -} - -func TestRender1(t *testing.T) { - - l := stringMap{ - "i5": "5", - "sA": "A", - "sAAA": "aa.", - "v_str": "s", - } - - for tem, exp := range test_set { - res, err := render(tem, l) - - ss := fmt.Sprintf("{{ %v }} = %v", tem, res) - if err != nil { - ss += " error" - } - - exp_err := len(exp) > 1 - // Check errors - if exp_err { - if err == nil { - t.Errorf("%s: expected '%s' in error, non found", ss, exp[1]) - } else if !strings.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", exp[1])) { - t.Errorf("%s: expected '%s' in error, got %s", ss, exp[1], err) - } - } else if err != nil { - t.Errorf("%s: no err expected, got %s", ss, err) - } - - // Check value - if res != exp[0] { - t.Errorf("%s: expected %v got %v", ss, exp[0], res) - } - - } - -} diff --git a/clab/config/transport.go b/clab/config/transport.go index 0ed6c550b..3bcedeec4 100644 --- a/clab/config/transport.go +++ b/clab/config/transport.go @@ -2,22 +2,9 @@ package config import ( "fmt" - - "github.com/srl-labs/containerlab/clab" + "strings" ) -type stringMap map[string]string - -type ConfigSnippet struct { - TargetNode *clab.Node - Data []byte // the Rendered template - - // some info for tracing/debugging - templateName, source string - // All the variables used to render the template - templateLabels *stringMap -} - type Transport interface { // Connect to the target host Connect(host string) error @@ -26,7 +13,7 @@ type Transport interface { Close() } -func WriteConfig(transport Transport, snips []*ConfigSnippet) error { +func WriteConfig(transport Transport, snips []ConfigSnippet) error { host := snips[0].TargetNode.LongName // the Kind should configure the transport parameters before @@ -39,11 +26,36 @@ func WriteConfig(transport Transport, snips []*ConfigSnippet) error { defer transport.Close() for _, snip := range snips { - err := transport.Write(snip) + err := transport.Write(&snip) if err != nil { - return fmt.Errorf("could not write config %s: %s", snip, err) + return fmt.Errorf("could not write config %s: %s", &snip, err) } } return nil } + +// the new agreed node config +type nodeConfig struct { + Vars map[string]string + Transport string + Templates []string +} + +func GetNodeConfigFromLabels(labels map[string]string) nodeConfig { + nc := nodeConfig{ + Vars: labels, + Templates: []string{"base"}, + Transport: "ssh", + } + if t, ok := labels["templates"]; ok { + nc.Templates = strings.Split(t, ",") + for i, v := range nc.Templates { + nc.Templates[i] = strings.Trim(v, " \n\t") + } + } + if t, ok := labels["transport"]; ok { + nc.Transport = t + } + return nc +} diff --git a/cmd/config.go b/cmd/config.go index 1eeae8d57..0a4fdf050 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -14,6 +14,9 @@ import ( // path to additional templates var templatePath string +// Only print config locally, dont send to the node +var printLines int + // configCmd represents the config command var configCmd = &cobra.Command{ Use: "config", @@ -46,7 +49,7 @@ var configCmd = &cobra.Command{ } // config map per node. each node gets a couple of config snippets []string - allConfig := make(map[string][]*config.ConfigSnippet) + allConfig := make(map[string][]config.ConfigSnippet) renderErr := 0 @@ -63,19 +66,21 @@ var configCmd = &cobra.Command{ renderErr += 1 continue } - allConfig[node.LongName] = append(allConfig[node.LongName], res) + allConfig[node.LongName] = append(allConfig[node.LongName], res...) + } for lIdx, link := range c.Links { - resA, resB, err := config.RenderLink(link) + res, err := config.RenderLink(link) if err != nil { log.Errorf("%d. %s\n", lIdx, err) renderErr += 1 continue } - allConfig[link.A.Node.LongName] = append(allConfig[link.A.Node.LongName], resA) - allConfig[link.B.Node.LongName] = append(allConfig[link.B.Node.LongName], resB) + for _, rr := range res { + allConfig[rr.TargetNode.LongName] = append(allConfig[rr.TargetNode.LongName], rr) + } } @@ -83,18 +88,20 @@ var configCmd = &cobra.Command{ return fmt.Errorf("%d render warnings", renderErr) } - // Debug log all config to be deployed - for _, v := range allConfig { - for _, r := range v { - log.Infof("%s\n%v", r, r.Lines()) - + if printLines > 0 { + // Debug log all config to be deployed + for _, v := range allConfig { + for _, r := range v { + r.Print(printLines) + } } + return nil } var wg sync.WaitGroup wg.Add(len(allConfig)) for _, cs_ := range allConfig { - go func(cs []*config.ConfigSnippet) { + go func(cs []config.ConfigSnippet) { defer wg.Done() var transport config.Transport @@ -109,7 +116,6 @@ var configCmd = &cobra.Command{ if err != nil { log.Errorf("%s: %s", kind, err) } - log.Info(transport.(*config.SshTransport).SshConfig) } else if ct == "grpc" { // newGRPCTransport } else { @@ -147,5 +153,7 @@ func newSSHTransport(node *clab.Node) (*config.SshTransport, error) { func init() { rootCmd.AddCommand(configCmd) configCmd.Flags().StringVarP(&templatePath, "templates", "", "", "specify template path") + configCmd.Flags().IntVarP(&printLines, "print-only", "p", 0, "print config, don't send it. Restricted to n lines") + configCmd.Flags().BoolVarP(&config.LoginMessages, "login-message", "", false, "show the SSH login message") configCmd.MarkFlagDirname("templates") } diff --git a/lab-examples/vr05/conf1.clab.yml b/lab-examples/vr05/conf1.clab.yml index 526cc4fb0..c2f8f4250 100644 --- a/lab-examples/vr05/conf1.clab.yml +++ b/lab-examples/vr05/conf1.clab.yml @@ -29,10 +29,10 @@ topology: labels: port: 1/1/c1/1, 1/1/c2/1 ip: 1.1.1.2/30 - vlan: "99, 99" + vlan: "99,99" - endpoints: [sr2:eth1, sr3:eth2] labels: - port: 1/1/c1/1, 1/1/c2/1ssh + port: 1/1/c1/1, 1/1/c2/1 vlan: 98 - endpoints: [sr3:eth1, sr4:eth2] labels: diff --git a/templates/vr-sros/base-link.tmpl b/templates/vr-sros/base-link.tmpl index 57effc1ff..d8533a325 100644 --- a/templates/vr-sros/base-link.tmpl +++ b/templates/vr-sros/base-link.tmpl @@ -1,6 +1,11 @@ -{{ if contains .port "/c" }} -/configure port {{ slice .port 0 -2 }} admin-state enable -/configure port {{ slice .port 0 -2 }} connector breakout c1-10g +{{ optional .vlan "1-4096" }} +{{ optional .isis_iid "0-32" }} +{{ expect .port "^\\d+/\\d/" }} +{{ expect .name "string" }} + +{{ if contains "/c" .port }} +/configure port {{ slice 0 -2 .port }} admin-state enable +/configure port {{ .port | slice 0 -2 }} connector breakout c1-10g {{ end }} /configure port {{ .port }} admin-state enable @@ -8,9 +13,9 @@ /configure router interface {{ .name }} ipv4 primary address {{ ip .ip }} ipv4 primary prefix-length {{ ipmask .ip }} - port {{ .port }}:{{ default .vlan "1" }} + port {{ .port }}:{{ default 1 .vlan }} -/configure router isis {{ default .isis_iid "0" }} +/configure router isis {{ default 0 .isis_iid }} interface {{ .name }} /configure router rsvp diff --git a/templates/vr-sros/base-node.tmpl b/templates/vr-sros/base-node.tmpl index 8bf29749e..5774d261b 100644 --- a/templates/vr-sros/base-node.tmpl +++ b/templates/vr-sros/base-node.tmpl @@ -1,9 +1,45 @@ {{ expect .systemip "ip" }} -{{ expect .sid_idx "0-1000" }} +{{ expect .sid_idx "1-10000" }} +{{ optional .sid_start "19000-30000" }} +{{ optional .sid_end "19000-30000" }} /configure system login-control idle-timeout 1440 +/configure router interface "system" + ipv4 primary address {{ ip .systemip }} + ipv4 primary prefix-length {{ ipmask .systemip }} + admin-state enable + +/configure router + autonomous-system {{ default 64500 .as_number }} + mpls-labels sr-labels start {{ default 19000 .sid_start }} end {{ default 30000 .sid_end }} + +/configure router isis {{ default 0 .isis_iid }} + area-address 49.0000.000{{ default 0 .isis_iid }} + level-capability 2 + level 2 wide-metrics-only + interface "system" ipv4-node-sid index {{ .sid_idx }} + #database-export igp-identifier {{ default 0 .isis_iid }} bgp-ls-identifier value {{ default 0 .isis_iid }} + traffic-engineering + advertise-router-capability area + segment-routing prefix-sid-range global + segment-routing admin-state enable + admin-state enable + +/configure router rsvp + admin-state enable + interface system admin-state enable + +/configure router mpls + cspf-on-loose-hop + interface system admin-state enable + admin-state enable + pce-report rsvp-te true + pce-report sr-te true + /configure apply-groups ["baseport"] +/configure router bgp apply-groups ["basebgp"] + /configure groups { group "baseport" { port "<.*\/[0-9]+>" { @@ -59,37 +95,3 @@ } } } - -/configure router bgp apply-groups ["basebgp"] - -/configure router interface "system" - ipv4 primary address {{ ip .systemip }} - ipv4 primary prefix-length {{ ipmask .systemip }} - admin-state enable - -/configure router - autonomous-system {{ default .as_number "64500" }} - mpls-labels sr-labels start {{ default .sid_start 19000 }} end {{ default .sid_end 30000 }} - -/configure router isis {{ default .isis_iid 0 }} - area-address 49.0000.000{{ default .isis_iid 0 }} - level-capability 2 - level 2 wide-metrics-only - interface "system" ipv4-node-sid index {{ require .sid_idx }} - #database-export igp-identifier {{ default .isis_iid "0" }} bgp-ls-identifier value {{ default .isis_iid "0" }} - traffic-engineering - advertise-router-capability area - segment-routing prefix-sid-range global - segment-routing admin-state enable - admin-state enable - -/configure router rsvp - admin-state enable - interface system admin-state enable - -/configure router mpls - cspf-on-loose-hop - interface system admin-state enable - admin-state enable - pce-report rsvp-te true - pce-report sr-te true From 8b6330e37a9204924fc40a9d57da9aa1677d16bd Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 16 Apr 2021 21:07:01 +0200 Subject: [PATCH 06/33] ignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3502f5eca..ac94a04bc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ dist # Output of the go coverage tool, specifically when used with LiteIDE *.out +# clab directories +lab-examples/**/clab-* + # ignore the following files/directories graph/* license.key From b4248c50e5cca1c52444fa031f86abe40ef5b9e2 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 16 Apr 2021 21:07:51 +0200 Subject: [PATCH 07/33] ds --- clab/config/functions.go | 2 +- clab/config/template.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/clab/config/functions.go b/clab/config/functions.go index c57ae5f45..bc66d5c62 100644 --- a/clab/config/functions.go +++ b/clab/config/functions.go @@ -185,7 +185,7 @@ var funcMap = map[string]interface{}{ case []string: return strings.Join(v, sep), nil case []int, []int16, []int32: - return strings.Trim(strings.Replace(fmt.Sprint(v), " ", sep, -1), "[]"), nil + return strings.Trim(strings.ReplaceAll(fmt.Sprint(v), " ", sep), "[]"), nil } return nil, fmt.Errorf("expected array [], got %v", val) }, diff --git a/clab/config/template.go b/clab/config/template.go index 0fc8ec240..7719bac02 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -25,7 +25,7 @@ type ConfigSnippet struct { // internal template cache var templates map[string]*template.Template -func LoadTemplate(kind string, templatePath string) error { +func LoadTemplate(kind, templatePath string) error { if templates == nil { templates = make(map[string]*template.Template) } From f51b5e0262fecc655fc022e2413b1221d2111528 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 16 Apr 2021 23:06:25 +0200 Subject: [PATCH 08/33] srlv1 --- clab/config.go | 1 + clab/config/ssh.go | 35 +++++++++++++++++++++++++++----- cmd/config.go | 2 ++ templates/srl/base-link.tmpl | 14 +++++++++++++ templates/srl/base-node.tmpl | 0 templates/vr-sros/base-link.tmpl | 5 +++-- 6 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 templates/srl/base-link.tmpl create mode 100644 templates/srl/base-node.tmpl diff --git a/clab/config.go b/clab/config.go index 10a90456b..c7e5639cb 100644 --- a/clab/config.go +++ b/clab/config.go @@ -66,6 +66,7 @@ var defaultConfigTemplates = map[string]string{ // DefaultCredentials holds default username and password per each kind var DefaultCredentials = map[string][]string{ + "srl": {"admin", "admin"}, "vr-sros": {"admin", "admin"}, "vr-vmx": {"admin", "admin@123"}, "vr-xrv9k": {"clab", "clab@123"}, diff --git a/clab/config/ssh.go b/clab/config/ssh.go index 3847de56e..9046c165c 100644 --- a/clab/config/ssh.go +++ b/clab/config/ssh.go @@ -32,6 +32,9 @@ type SshTransport struct { // Contains the first read after connecting LoginMessage SshReply + ConfigStart func(s *SshTransport) + ConfigCommit func(s *SshTransport) SshReply + // SSH parameters used in connect // defualt: 22 Port int @@ -156,8 +159,10 @@ func (t *SshTransport) Run(command string, timeout int) SshReply { // Session NEEDS to be configurable for other kinds // Part of the Transport interface func (t *SshTransport) Write(snip *ConfigSnippet) error { - t.Run("/configure global", 2) - t.Run("discard", 2) + if t.ConfigStart == nil { + return fmt.Errorf("SSH Transport not ready %s", snip.TargetNode.Kind) + } + t.ConfigStart(t) c, b := 0, 0 for _, l := range snip.Lines() { @@ -170,9 +175,8 @@ func (t *SshTransport) Write(snip *ConfigSnippet) error { t.Run(l, 3) } - // Commit - commit := t.Run("commit", 10) - //commit += t.Run("", 10) + commit := t.ConfigCommit(t) + log.Infof("COMMIT %s - %d lines %d bytes\n%s", snip, c, b, commit.result) return nil } @@ -278,3 +282,24 @@ func (ses *SshSession) Close() { log.Debugf("Closing session") ses.Session.Close() } + +func (s *SshTransport) SetupKind(kind string) { + switch kind { + case "srl": + s.ConfigStart = func(s *SshTransport) { + s.Run("enter candidate", 10) + } + s.ConfigCommit = func(s *SshTransport) SshReply { + return s.Run("commit now", 10) + } + case "vr-sros": + s.ConfigStart = func(s *SshTransport) { + s.Run("/configure global", 2) + s.Run("discard", 1) + } + s.ConfigCommit = func(s *SshTransport) SshReply { + return s.Run("commit", 10) + } + + } +} diff --git a/cmd/config.go b/cmd/config.go index 0a4fdf050..4f931e627 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -145,6 +145,8 @@ func newSSHTransport(node *clab.Node) (*config.SshTransport, error) { c.SshConfig, clab.DefaultCredentials[node.Kind][0], clab.DefaultCredentials[node.Kind][1]) + + c.SetupKind(node.Kind) return c, nil } return nil, fmt.Errorf("no tranport implemented for kind: %s", kind) diff --git a/templates/srl/base-link.tmpl b/templates/srl/base-link.tmpl new file mode 100644 index 000000000..bd4b1ed2c --- /dev/null +++ b/templates/srl/base-link.tmpl @@ -0,0 +1,14 @@ +{{ expect .port "^ethernet-\\d+/\\d+$" }} +{{ expect .name "string" }} +{{ expect .ip "ip" }} +{{ optional .vlan "0-4095" }} + +#enter candidate +set / interface {{ .port }} ethernet-1/1 +set / interface {{ .port }} admin-state enable +set / interface {{ .port }} subinterface 0 +set / interface {{ .port }} subinterface 0 ipv4 +set / interface {{ .port }} subinterface 0 ipv4 address {{ .ip }} +set / network-instance default +set / network-instance default interface {{ .port }}.{{ default 0 .vlan }} +#commit now \ No newline at end of file diff --git a/templates/srl/base-node.tmpl b/templates/srl/base-node.tmpl new file mode 100644 index 000000000..e69de29bb diff --git a/templates/vr-sros/base-link.tmpl b/templates/vr-sros/base-link.tmpl index d8533a325..304b6a4ff 100644 --- a/templates/vr-sros/base-link.tmpl +++ b/templates/vr-sros/base-link.tmpl @@ -1,7 +1,8 @@ -{{ optional .vlan "1-4096" }} -{{ optional .isis_iid "0-32" }} {{ expect .port "^\\d+/\\d/" }} {{ expect .name "string" }} +{{ expect .ip "ip" }} +{{ optional .vlan "0-4096" }} +{{ optional .isis_iid "0-32" }} {{ if contains "/c" .port }} /configure port {{ slice 0 -2 .port }} admin-state enable From 8441f0ae1066f993c7ba7664f82a1c4931d92af5 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 19 Apr 2021 20:49:07 +0200 Subject: [PATCH 09/33] test ok --- clab/config/ssh.go | 225 ++++++++++++++----- clab/config/template.go | 17 +- clab/config/transport.go | 24 +- cmd/config.go | 12 +- lab-examples/vr05/conf1.clab.yml | 2 +- templates/srl/base-link.tmpl | 39 +++- templates/srl/base-node.tmpl | 37 +++ templates/srl/show-route-table-link.tmpl | 0 templates/srl/show-route-table-node.tmpl | 1 + templates/vr-sros/base-link.tmpl | 13 +- templates/vr-sros/base-node.tmpl | 14 +- templates/vr-sros/show-route-table-link.tmpl | 0 templates/vr-sros/show-route-table-node.tmpl | 1 + 13 files changed, 292 insertions(+), 93 deletions(-) create mode 100644 templates/srl/show-route-table-link.tmpl create mode 100644 templates/srl/show-route-table-node.tmpl create mode 100644 templates/vr-sros/show-route-table-link.tmpl create mode 100644 templates/vr-sros/show-route-table-node.tmpl diff --git a/clab/config/ssh.go b/clab/config/ssh.go index 9046c165c..4a85f4457 100644 --- a/clab/config/ssh.go +++ b/clab/config/ssh.go @@ -3,6 +3,7 @@ package config import ( "fmt" "io" + "runtime" "strings" "time" @@ -31,47 +32,34 @@ type SshTransport struct { ses *SshSession // Contains the first read after connecting LoginMessage SshReply - - ConfigStart func(s *SshTransport) - ConfigCommit func(s *SshTransport) SshReply - // SSH parameters used in connect // defualt: 22 Port int + // extra debug print + debug bool + // SSH Options // required! SshConfig *ssh.ClientConfig + // Character to split the incoming stream (#/$/>) // default: # PromptChar string - // Prompt parsing function. Default return the last line of the # - // default: DefaultPrompParse - PromptParse func(in *string) *SshReply -} -// This is the default prompt parse function used by SSH transport -func DefaultPrompParse(in *string) *SshReply { - n := strings.LastIndex(*in, "\n") - if strings.Contains((*in)[n:], " ") { - return &SshReply{ - result: *in, - prompt: "", - } - } - res := (*in)[:n] - n = strings.LastIndex(res, "\n") - if n < 0 { - n = 0 - } - return &SshReply{ - result: (*in)[:n], - prompt: (*in)[n:] + "#", - } + // Kind specific transactions & prompt checking function + K SshKind } -// The channel does +// Creates the channel reading the SSH connection +// +// The first prompt is saved in LoginMessages +// +// - The channel read the SSH session, splits on PromptChar +// - Uses SshKind's PromptParse to split the received data in *result* and *prompt* parts +// (if no valid prompt was found, prompt will simply be empty and result contain all the data) +// - Emit data func (t *SshTransport) InChannel() { - // Ensure we have one working channel + // Ensure we have a working channel t.in = make(chan SshReply) // setup a buffered string channel @@ -88,7 +76,7 @@ func (t *SshTransport) InChannel() { parts := strings.Split(tmpS, "#") li := len(parts) - 1 for i := 0; i < li; i++ { - t.in <- *t.PromptParse(&parts[i]) + t.in <- *t.K.PromptParse(t, &parts[i]) } tmpS = parts[li] } @@ -102,11 +90,11 @@ func (t *SshTransport) InChannel() { } }() + // Save first prompt t.LoginMessage = t.Run("", 15) if LoginMessages { - log.Infof("%s\n", t.LoginMessage.result) + t.LoginMessage.Infof("") } - //log.Debugf("%s\n", t.BootMsg.prompt) } // Run a single command and wait for the reply @@ -119,11 +107,17 @@ func (t *SshTransport) Run(command string, timeout int) SshReply { for { // Read from the channel with a timeout + var rr string + select { case <-time.After(time.Duration(timeout) * time.Second): log.Warnf("timeout waiting for prompt: %s", command) return SshReply{} case ret := <-t.in: + if t.debug { + ret.Debug() + } + if ret.prompt == "" && ret.result != "" { // we should continue reading... sHistory += ret.result @@ -134,23 +128,26 @@ func (t *SshTransport) Run(command string, timeout int) SshReply { log.Errorf("received zero?") continue } - rr := strings.Trim(ret.result, " \n") - if sHistory != "" { - rr = sHistory + rr + + if sHistory == "" { + rr = strings.Trim(ret.result, " \n\r\t") + } else { + rr = strings.Trim(sHistory+ret.result, " \n\r\t") sHistory = "" } if strings.HasPrefix(rr, command) { - rr = strings.Trim(rr[len(command):], " \n\r") - // fmt.Print(rr) + rr = strings.Trim(rr[len(command):], " \n\r\t") } else if !strings.Contains(rr, command) { sHistory = rr continue } - return SshReply{ + res := SshReply{ result: rr, prompt: ret.prompt, } + res.Debug() + return res } } } @@ -159,12 +156,20 @@ func (t *SshTransport) Run(command string, timeout int) SshReply { // Session NEEDS to be configurable for other kinds // Part of the Transport interface func (t *SshTransport) Write(snip *ConfigSnippet) error { - if t.ConfigStart == nil { - return fmt.Errorf("SSH Transport not ready %s", snip.TargetNode.Kind) + if len(snip.Data) == 0 { + return nil + } + + transaction := !strings.HasPrefix(snip.templateName, "show-") + + err := t.K.ConfigStart(t, snip.TargetNode.ShortName, transaction) + if err != nil { + return err } - t.ConfigStart(t) c, b := 0, 0 + var r SshReply + for _, l := range snip.Lines() { l = strings.TrimSpace(l) if l == "" || strings.HasPrefix(l, "#") { @@ -172,12 +177,18 @@ func (t *SshTransport) Write(snip *ConfigSnippet) error { } c += 1 b += len(l) - t.Run(l, 3) + r = t.Run(l, 5) + if r.result != "" { + r.Infof(snip.TargetNode.ShortName) + } } - commit := t.ConfigCommit(t) + if transaction { + commit, _ := t.K.ConfigCommit(t) + + commit.Infof("COMMIT %s - %d lines %d bytes", snip, c, b) + } - log.Infof("COMMIT %s - %d lines %d bytes\n%s", snip, c, b, commit.result) return nil } @@ -185,9 +196,6 @@ func (t *SshTransport) Write(snip *ConfigSnippet) error { // Part of the Transport interface func (t *SshTransport) Connect(host string) error { // Assign Default Values - if t.PromptParse == nil { - t.PromptParse = DefaultPrompParse - } if t.PromptChar == "" { t.PromptChar = "#" } @@ -262,6 +270,20 @@ func NewSshSession(host string, sshConfig *ssh.ClientConfig) (*SshSession, error if err != nil { return nil, fmt.Errorf("session stdin: %s", err) } + // sshIn2, err := session.StderrPipe() + // if err != nil { + // return nil, fmt.Errorf("session stderr: %s", err) + // } + // Request PTY (required for srl) + modes := ssh.TerminalModes{ + ssh.ECHO: 1, // disable echo + } + err = session.RequestPty("dumb", 24, 100, modes) + if err != nil { + session.Close() + return nil, fmt.Errorf("pty request failed: %s", err) + } + if err := session.Shell(); err != nil { session.Close() return nil, fmt.Errorf("session shell: %s", err) @@ -283,23 +305,108 @@ func (ses *SshSession) Close() { ses.Session.Close() } -func (s *SshTransport) SetupKind(kind string) { - switch kind { - case "srl": - s.ConfigStart = func(s *SshTransport) { - s.Run("enter candidate", 10) +// This is a helper funciton to parse the prompt, and can be used by SshKind's ParsePrompt +// Used in SROS & SRL today +func promptParseNoSpaces(in *string, promptChar string, lines int) *SshReply { + n := strings.LastIndex(*in, "\n") + if n < 0 { + return &SshReply{ + result: *in, + prompt: "", } - s.ConfigCommit = func(s *SshTransport) SshReply { - return s.Run("commit now", 10) + + } + if strings.Contains((*in)[n:], " ") { + return &SshReply{ + result: *in, + prompt: "", } - case "vr-sros": - s.ConfigStart = func(s *SshTransport) { - s.Run("/configure global", 2) - s.Run("discard", 1) + } + if lines > 1 { + // Add another line to the prompt + res := (*in)[:n] + n = strings.LastIndex(res, "\n") + } + if n < 0 { + n = 0 + } + return &SshReply{ + result: (*in)[:n], + prompt: (*in)[n:] + promptChar, + } +} + +// an interface to implement kind specific methods for transactions and prompt checking +type SshKind interface { + // Start a config transaction + ConfigStart(s *SshTransport, node string, transaction bool) error + // Commit a config transaction + ConfigCommit(s *SshTransport) (SshReply, error) + // Prompt parsing function. + // This function receives string, split by the delimiter and should ensure this is a valid prompt + // Valid prompt, strip te prompt from the result and add it to the prompt in SshReply + // + // A defualt implementation is promptParseNoSpaces, which simply ensures there are + // no spaces between the start of the line and the # + PromptParse(s *SshTransport, in *string) *SshReply +} + +// implements SShKind +type VrSrosSshKind struct{} + +func (sk *VrSrosSshKind) ConfigStart(s *SshTransport, node string, transaction bool) error { + s.PromptChar = "#" // ensure it's '#' + //s.debug = true + if transaction { + cc := s.Run("/configure global", 5) + if cc.result != "" { + cc.Infof(node) } - s.ConfigCommit = func(s *SshTransport) SshReply { - return s.Run("commit", 10) + cc = s.Run("discard", 1) + if cc.result != "" { + cc.Infof("%s discard", node) } + } else { + s.Run("/environment more false", 5) + } + return nil +} +func (sk *VrSrosSshKind) ConfigCommit(s *SshTransport) (SshReply, error) { + return s.Run("commit", 10), nil +} +func (sk *VrSrosSshKind) PromptParse(s *SshTransport, in *string) *SshReply { + return promptParseNoSpaces(in, s.PromptChar, 2) +} + +// implements SShKind +type SrlSshKind struct{} + +func (sk *SrlSshKind) ConfigStart(s *SshTransport, node string, transaction bool) error { + s.PromptChar = "#" // ensure it's '#' + s.debug = true + if transaction { + s.Run("enter candidate", 5) + s.Run("discard stay", 2) + } + return nil +} +func (sk *SrlSshKind) ConfigCommit(s *SshTransport) (SshReply, error) { + return s.Run("commit now", 10), nil +} +func (sk *SrlSshKind) PromptParse(s *SshTransport, in *string) *SshReply { + return promptParseNoSpaces(in, s.PromptChar, 2) +} + +func (r *SshReply) Debug() { + _, fn, line, _ := runtime.Caller(1) + log.Debugf("(%s line %d) *RESULT: %s.\n | %v\n*PROMPT:%v.\n*PROMPT:%v.\n", fn, line, r.result, []byte(r.result), r.prompt, []byte(r.prompt)) +} +func (r *SshReply) Infof(msg string, args ...interface{}) { + var s string + if r.result != "" { + s = "\n | " + s += strings.Join(strings.Split(r.result, "\n"), s) } + log.Infof(msg+s, args...) } diff --git a/clab/config/template.go b/clab/config/template.go index 7719bac02..51cc52311 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -51,13 +51,15 @@ func (c *ConfigSnippet) Render() error { buf := new(strings.Builder) c.Data = nil - varsP, _ := json.MarshalIndent(c.vars, "", " ") - log.Debugf("Render %s vars=%s\n", c.String(), varsP) + varsP, err := json.MarshalIndent(c.vars, "", " ") + if err != nil { + varsP = []byte(fmt.Sprintf("%s", c.vars)) + } - err := t.ExecuteTemplate(buf, c.templateName, c.vars) + err = t.ExecuteTemplate(buf, c.templateName, c.vars) if err != nil { - log.Errorf("could not render template: %s %s vars=%s\n", c.String(), err, varsP) - return fmt.Errorf("could not render template: %s %s", c.String(), err) + log.Errorf("could not render template %s: %s vars=%s\n", c.String(), err, varsP) + return fmt.Errorf("could not render template %s: %s", c.String(), err) } // Strip blank lines @@ -182,7 +184,10 @@ func (c *ConfigSnippet) Lines() []string { // Print the configSnippet func (c *ConfigSnippet) Print(printLines int) { - vars, _ := json.MarshalIndent(c.vars, "", " ") + vars := []byte{} + if log.IsLevelEnabled(log.DebugLevel) { + vars, _ = json.MarshalIndent(c.vars, "", " ") + } s := "" if printLines > 0 { diff --git a/clab/config/transport.go b/clab/config/transport.go index 3bcedeec4..fe3674b16 100644 --- a/clab/config/transport.go +++ b/clab/config/transport.go @@ -35,6 +35,9 @@ func WriteConfig(transport Transport, snips []ConfigSnippet) error { return nil } +// templates to execute +var TemplateOverride string + // the new agreed node config type nodeConfig struct { Vars map[string]string @@ -42,17 +45,26 @@ type nodeConfig struct { Templates []string } +// Split a string on commans and trim each +func SplitTrim(s string) []string { + res := strings.Split(s, ",") + for i, v := range res { + res[i] = strings.Trim(v, " \n\t") + } + return res +} + func GetNodeConfigFromLabels(labels map[string]string) nodeConfig { nc := nodeConfig{ Vars: labels, - Templates: []string{"base"}, Transport: "ssh", } - if t, ok := labels["templates"]; ok { - nc.Templates = strings.Split(t, ",") - for i, v := range nc.Templates { - nc.Templates[i] = strings.Trim(v, " \n\t") - } + if TemplateOverride != "" { + nc.Templates = SplitTrim(TemplateOverride) + } else if t, ok := labels["templates"]; ok { + nc.Templates = SplitTrim(t) + } else { + nc.Templates = []string{"base"} } if t, ok := labels["transport"]; ok { nc.Transport = t diff --git a/cmd/config.go b/cmd/config.go index 4f931e627..b62f60729 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -146,7 +146,12 @@ func newSSHTransport(node *clab.Node) (*config.SshTransport, error) { clab.DefaultCredentials[node.Kind][0], clab.DefaultCredentials[node.Kind][1]) - c.SetupKind(node.Kind) + switch node.Kind { + case "vr-sros": + c.K = &config.VrSrosSshKind{} + case "srl": + c.K = &config.SrlSshKind{} + } return c, nil } return nil, fmt.Errorf("no tranport implemented for kind: %s", kind) @@ -154,8 +159,9 @@ func newSSHTransport(node *clab.Node) (*config.SshTransport, error) { func init() { rootCmd.AddCommand(configCmd) - configCmd.Flags().StringVarP(&templatePath, "templates", "", "", "specify template path") + configCmd.Flags().StringVarP(&templatePath, "path", "", "", "specify template path") + configCmd.MarkFlagDirname("path") + configCmd.Flags().StringVarP(&config.TemplateOverride, "templates", "", "", "specify a list of template to apply") configCmd.Flags().IntVarP(&printLines, "print-only", "p", 0, "print config, don't send it. Restricted to n lines") configCmd.Flags().BoolVarP(&config.LoginMessages, "login-message", "", false, "show the SSH login message") - configCmd.MarkFlagDirname("templates") } diff --git a/lab-examples/vr05/conf1.clab.yml b/lab-examples/vr05/conf1.clab.yml index c2f8f4250..d116e2602 100644 --- a/lab-examples/vr05/conf1.clab.yml +++ b/lab-examples/vr05/conf1.clab.yml @@ -4,7 +4,7 @@ topology: defaults: kind: vr-sros image: registry.srlinux.dev/pub/vr-sros:21.2.R1 - license: /home/kellerza/containerlabs/license-sros21.txt + license: /home/kellerza/license/license-sros21.txt labels: isis_iid: 0 nodes: diff --git a/templates/srl/base-link.tmpl b/templates/srl/base-link.tmpl index bd4b1ed2c..218248cd1 100644 --- a/templates/srl/base-link.tmpl +++ b/templates/srl/base-link.tmpl @@ -1,14 +1,33 @@ -{{ expect .port "^ethernet-\\d+/\\d+$" }} +{{ expect .port "^(ethernet-\\d+/|e\\d+-)\\d+$" }} {{ expect .name "string" }} {{ expect .ip "ip" }} {{ optional .vlan "0-4095" }} +{{ optional .metric "1-10000" }} -#enter candidate -set / interface {{ .port }} ethernet-1/1 -set / interface {{ .port }} admin-state enable -set / interface {{ .port }} subinterface 0 -set / interface {{ .port }} subinterface 0 ipv4 -set / interface {{ .port }} subinterface 0 ipv4 address {{ .ip }} -set / network-instance default -set / network-instance default interface {{ .port }}.{{ default 0 .vlan }} -#commit now \ No newline at end of file + +/interface {{ .port }} { + admin-state enable + vlan-tagging true + subinterface {{ default 10 .vlan }} { + set vlan encap single-tagged vlan-id {{ default 10 .vlan }} + set ipv4 address {{ .ip }} + set ipv6 address ::FFFF:{{ ip .ip }}/127 + } +} + +/network-instance default { + interface {{ .port }}.{{ default 10 .vlan }} { + } + protocols { + isis { + instance default { + interface {{ .port }}.{{ default 10 .vlan }} { + circuit-type point-to-point + level 2 { + metric {{ default 10 .metric }} + } + } + } + } + } +} diff --git a/templates/srl/base-node.tmpl b/templates/srl/base-node.tmpl index e69de29bb..656907f9c 100644 --- a/templates/srl/base-node.tmpl +++ b/templates/srl/base-node.tmpl @@ -0,0 +1,37 @@ +{{ expect .systemip "ip" }} +{{ optional .isis_iid "0-31" }} + +/interface lo0 { + admin-state enable + subinterface 0 { + ipv4 { + address {{ .systemip }} { + } + } + ipv6 { + address ::ffff:{{ ip .systemip }}/128 { + } + } + } +} + +/network-instance default { + router-id {{ ip .systemip }} + interface lo0.0 { + } + protocols { + isis { + instance default { + admin-state enable + level-capability L2 + set level 2 metric-style wide + # net should not be multiline (net [), becasue of the SRL ... prompt + net [ 49.0000.0000.0000.0{{ default 0 .isis_iid }} ] + interface lo0.0 { + } + } + } + } +} + + /system lldp admin-state enable diff --git a/templates/srl/show-route-table-link.tmpl b/templates/srl/show-route-table-link.tmpl new file mode 100644 index 000000000..e69de29bb diff --git a/templates/srl/show-route-table-node.tmpl b/templates/srl/show-route-table-node.tmpl new file mode 100644 index 000000000..d0d279bb4 --- /dev/null +++ b/templates/srl/show-route-table-node.tmpl @@ -0,0 +1 @@ +/show network-instance default route-table ipv4-unicast summary \ No newline at end of file diff --git a/templates/vr-sros/base-link.tmpl b/templates/vr-sros/base-link.tmpl index 304b6a4ff..8cd331b97 100644 --- a/templates/vr-sros/base-link.tmpl +++ b/templates/vr-sros/base-link.tmpl @@ -3,21 +3,26 @@ {{ expect .ip "ip" }} {{ optional .vlan "0-4096" }} {{ optional .isis_iid "0-32" }} +{{ optional .metric "1-10000" }} -{{ if contains "/c" .port }} +{{- if contains "/c" .port }} /configure port {{ slice 0 -2 .port }} admin-state enable /configure port {{ .port | slice 0 -2 }} connector breakout c1-10g -{{ end }} +{{- end }} /configure port {{ .port }} admin-state enable /configure router interface {{ .name }} ipv4 primary address {{ ip .ip }} ipv4 primary prefix-length {{ ipmask .ip }} - port {{ .port }}:{{ default 1 .vlan }} + port {{ .port }}:{{ default 10 .vlan }} /configure router isis {{ default 0 .isis_iid }} - interface {{ .name }} + interface {{ .name }} admin-state enable + interface {{ .name }} interface-type point-to-point + {{- if .metric }} + interface {{ .name }} level 2 metric {{ .metric }} + {{- end }} /configure router rsvp interface {{ .name }} admin-state enable diff --git a/templates/vr-sros/base-node.tmpl b/templates/vr-sros/base-node.tmpl index 5774d261b..df15dc1b5 100644 --- a/templates/vr-sros/base-node.tmpl +++ b/templates/vr-sros/base-node.tmpl @@ -1,5 +1,6 @@ {{ expect .systemip "ip" }} -{{ expect .sid_idx "1-10000" }} +{{ optional .isis_iid "0-31" }} +{{ optional .sid_idx "1-10000" }} {{ optional .sid_start "19000-30000" }} {{ optional .sid_end "19000-30000" }} @@ -12,19 +13,24 @@ /configure router autonomous-system {{ default 64500 .as_number }} + {{- if .sid_idx }} mpls-labels sr-labels start {{ default 19000 .sid_start }} end {{ default 30000 .sid_end }} + {{- end }} /configure router isis {{ default 0 .isis_iid }} - area-address 49.0000.000{{ default 0 .isis_iid }} + area-address 49.0000.0000.0000.0{{ default 0 .isis_iid }} level-capability 2 level 2 wide-metrics-only - interface "system" ipv4-node-sid index {{ .sid_idx }} #database-export igp-identifier {{ default 0 .isis_iid }} bgp-ls-identifier value {{ default 0 .isis_iid }} traffic-engineering advertise-router-capability area + admin-state enable + interface "system" admin-state enable + {{- if .sid_idx }} + interface "system" ipv4-node-sid index {{ .sid_idx }} segment-routing prefix-sid-range global segment-routing admin-state enable - admin-state enable + {{- end }} /configure router rsvp admin-state enable diff --git a/templates/vr-sros/show-route-table-link.tmpl b/templates/vr-sros/show-route-table-link.tmpl new file mode 100644 index 000000000..e69de29bb diff --git a/templates/vr-sros/show-route-table-node.tmpl b/templates/vr-sros/show-route-table-node.tmpl new file mode 100644 index 000000000..6b5657a04 --- /dev/null +++ b/templates/vr-sros/show-route-table-node.tmpl @@ -0,0 +1 @@ +/show router route-table \ No newline at end of file From a2d53752179763fc4112a1605952f837a45d54f0 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 19 Apr 2021 22:41:03 +0200 Subject: [PATCH 10/33] failfast --- clab/config/ssh.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clab/config/ssh.go b/clab/config/ssh.go index 4a85f4457..17e28b0cf 100644 --- a/clab/config/ssh.go +++ b/clab/config/ssh.go @@ -180,6 +180,7 @@ func (t *SshTransport) Write(snip *ConfigSnippet) error { r = t.Run(l, 5) if r.result != "" { r.Infof(snip.TargetNode.ShortName) + log.Errorf("%s: Aborting deployment...", snip.TargetNode.ShortName) } } @@ -360,7 +361,7 @@ func (sk *VrSrosSshKind) ConfigStart(s *SshTransport, node string, transaction b if transaction { cc := s.Run("/configure global", 5) if cc.result != "" { - cc.Infof(node) + cc.Infof("%s /config global", node) } cc = s.Run("discard", 1) if cc.result != "" { From 08652c19bd4717179711c90dc6057abc00a05948 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 19 Apr 2021 22:46:00 +0200 Subject: [PATCH 11/33] deepsource --- clab/config/transport.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clab/config/transport.go b/clab/config/transport.go index fe3674b16..10b083e43 100644 --- a/clab/config/transport.go +++ b/clab/config/transport.go @@ -39,7 +39,7 @@ func WriteConfig(transport Transport, snips []ConfigSnippet) error { var TemplateOverride string // the new agreed node config -type nodeConfig struct { +type NodeConfig struct { Vars map[string]string Transport string Templates []string @@ -54,8 +54,8 @@ func SplitTrim(s string) []string { return res } -func GetNodeConfigFromLabels(labels map[string]string) nodeConfig { - nc := nodeConfig{ +func GetNodeConfigFromLabels(labels map[string]string) NodeConfig { + nc := NodeConfig{ Vars: labels, Transport: "ssh", } From 23b095349fcb251dab18a5272280a1acacf2b151 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 23 Apr 2021 14:13:38 +0200 Subject: [PATCH 12/33] ok --- clab/config/ssh.go | 220 +++++++++++++++++------------------------ clab/config/sshkind.go | 113 +++++++++++++++++++++ cmd/config.go | 12 ++- cmd/root.go | 4 +- 4 files changed, 219 insertions(+), 130 deletions(-) create mode 100644 clab/config/sshkind.go diff --git a/clab/config/ssh.go b/clab/config/ssh.go index 17e28b0cf..49e86fb9b 100644 --- a/clab/config/ssh.go +++ b/clab/config/ssh.go @@ -20,8 +20,11 @@ type SshSession struct { // Display the SSH login message var LoginMessages bool +// Debug count +var DebugCount int + // The reply the execute command and the prompt. -type SshReply struct{ result, prompt string } +type SshReply struct{ result, prompt, command string } // SshTransport setting needs to be set before calling Connect() // SshTransport implement the Transport interface @@ -31,12 +34,13 @@ type SshTransport struct { // SSH Session ses *SshSession // Contains the first read after connecting - LoginMessage SshReply + LoginMessage *SshReply // SSH parameters used in connect // defualt: 22 Port int - // extra debug print - debug bool + + // Keep the target for logging + Target string // SSH Options // required! @@ -76,7 +80,13 @@ func (t *SshTransport) InChannel() { parts := strings.Split(tmpS, "#") li := len(parts) - 1 for i := 0; i < li; i++ { - t.in <- *t.K.PromptParse(t, &parts[i]) + r := t.K.PromptParse(t, &parts[i]) + if r == nil { + r = &SshReply{ + result: parts[i], + } + } + t.in <- *r } tmpS = parts[li] } @@ -93,14 +103,15 @@ func (t *SshTransport) InChannel() { // Save first prompt t.LoginMessage = t.Run("", 15) if LoginMessages { - t.LoginMessage.Infof("") + t.LoginMessage.Info(t.Target) } } // Run a single command and wait for the reply -func (t *SshTransport) Run(command string, timeout int) SshReply { +func (t *SshTransport) Run(command string, timeout int) *SshReply { if command != "" { t.ses.Writeln(command) + log.Debugf("--> %s\n", command) } sHistory := "" @@ -112,41 +123,51 @@ func (t *SshTransport) Run(command string, timeout int) SshReply { select { case <-time.After(time.Duration(timeout) * time.Second): log.Warnf("timeout waiting for prompt: %s", command) - return SshReply{} + return &SshReply{ + result: sHistory, + command: command, + } case ret := <-t.in: - if t.debug { - ret.Debug() + if DebugCount > 1 { + ret.Debug(t.Target, command+"<--InChannel--") + } + + if ret.result == "" && ret.prompt == "" { + log.Fatalf("received zero?") + continue } if ret.prompt == "" && ret.result != "" { // we should continue reading... sHistory += ret.result - timeout = 1 // reduce timeout, node is already sending data - continue - } - if ret.result == "" && ret.prompt == "" { - log.Errorf("received zero?") + if DebugCount > 1 { + log.Debugf("+") + } + timeout = 2 // reduce timeout, node is already sending data continue } if sHistory == "" { - rr = strings.Trim(ret.result, " \n\r\t") + rr = ret.result } else { - rr = strings.Trim(sHistory+ret.result, " \n\r\t") + rr = sHistory + "#" + ret.result sHistory = "" } + rr = strings.Trim(rr, " \n\r\t") if strings.HasPrefix(rr, command) { rr = strings.Trim(rr[len(command):], " \n\r\t") } else if !strings.Contains(rr, command) { + log.Debugf("read more %s:%s", command, rr) sHistory = rr continue } - res := SshReply{ - result: rr, - prompt: ret.prompt, + res := &SshReply{ + result: rr, + prompt: ret.prompt, + command: command, } - res.Debug() + res.Debug(t.Target, command+"<--RUN--") return res } } @@ -162,13 +183,12 @@ func (t *SshTransport) Write(snip *ConfigSnippet) error { transaction := !strings.HasPrefix(snip.templateName, "show-") - err := t.K.ConfigStart(t, snip.TargetNode.ShortName, transaction) + err := t.K.ConfigStart(t, transaction) if err != nil { return err } - c, b := 0, 0 - var r SshReply + c := 0 for _, l := range snip.Lines() { l = strings.TrimSpace(l) @@ -176,18 +196,22 @@ func (t *SshTransport) Write(snip *ConfigSnippet) error { continue } c += 1 - b += len(l) - r = t.Run(l, 5) - if r.result != "" { - r.Infof(snip.TargetNode.ShortName) - log.Errorf("%s: Aborting deployment...", snip.TargetNode.ShortName) - } + t.Run(l, 5).Info(t.Target) } if transaction { - commit, _ := t.K.ConfigCommit(t) - - commit.Infof("COMMIT %s - %d lines %d bytes", snip, c, b) + commit, err := t.K.ConfigCommit(t) + msg := snip.String() + i := strings.Index(msg, " ") + msg = fmt.Sprintf("%s COMMIT%s - %d lines", msg[:i], msg[i:], c) + if commit.result != "" { + msg += commit.LogString(snip.TargetNode.ShortName, true, false) + } + if err != nil { + log.Error(msg) + return err + } + log.Info(msg) } return nil @@ -212,6 +236,8 @@ func (t *SshTransport) Connect(host string) error { //sshConfig := &ssh.ClientConfig{} //SshConfigWithUserNamePassword(sshConfig, "admin", "admin") + t.Target = strings.Split(strings.Split(host, ":")[0], "-")[2] + ses_, err := NewSshSession(host, t.SshConfig) if err != nil || ses_ == nil { return fmt.Errorf("cannot connect to %s: %s", host, err) @@ -306,108 +332,48 @@ func (ses *SshSession) Close() { ses.Session.Close() } -// This is a helper funciton to parse the prompt, and can be used by SshKind's ParsePrompt -// Used in SROS & SRL today -func promptParseNoSpaces(in *string, promptChar string, lines int) *SshReply { - n := strings.LastIndex(*in, "\n") - if n < 0 { - return &SshReply{ - result: *in, - prompt: "", - } - +// The LogString will include the entire SshReply +// Each field will be prefixed by a character. +// # - command sent +// | - result recieved +// ? - prompt part of the result +func (r *SshReply) LogString(node string, linefeed, debug bool) string { + ind := 12 + len(node) + prefix := "\n" + strings.Repeat(" ", ind) + s := "" + if linefeed { + s = "\n" + strings.Repeat(" ", 11) } - if strings.Contains((*in)[n:], " ") { - return &SshReply{ - result: *in, - prompt: "", + s += node + " # " + r.command + s += prefix + "| " + s += strings.Join(strings.Split(r.result, "\n"), prefix+"| ") + if debug { // Add the prompt & more + s = "" + strings.Repeat(" ", ind) + s + s += prefix + "? " + s += strings.Join(strings.Split(r.prompt, "\n"), prefix+"? ") + if DebugCount > 3 { // add bytestring + s += fmt.Sprintf("%s| %v%s ? %v", prefix, []byte(r.result), prefix, []byte(r.prompt)) } - } - if lines > 1 { - // Add another line to the prompt - res := (*in)[:n] - n = strings.LastIndex(res, "\n") - } - if n < 0 { - n = 0 - } - return &SshReply{ - result: (*in)[:n], - prompt: (*in)[n:] + promptChar, - } -} - -// an interface to implement kind specific methods for transactions and prompt checking -type SshKind interface { - // Start a config transaction - ConfigStart(s *SshTransport, node string, transaction bool) error - // Commit a config transaction - ConfigCommit(s *SshTransport) (SshReply, error) - // Prompt parsing function. - // This function receives string, split by the delimiter and should ensure this is a valid prompt - // Valid prompt, strip te prompt from the result and add it to the prompt in SshReply - // - // A defualt implementation is promptParseNoSpaces, which simply ensures there are - // no spaces between the start of the line and the # - PromptParse(s *SshTransport, in *string) *SshReply -} - -// implements SShKind -type VrSrosSshKind struct{} -func (sk *VrSrosSshKind) ConfigStart(s *SshTransport, node string, transaction bool) error { - s.PromptChar = "#" // ensure it's '#' - //s.debug = true - if transaction { - cc := s.Run("/configure global", 5) - if cc.result != "" { - cc.Infof("%s /config global", node) - } - cc = s.Run("discard", 1) - if cc.result != "" { - cc.Infof("%s discard", node) - } - } else { - s.Run("/environment more false", 5) } - return nil -} -func (sk *VrSrosSshKind) ConfigCommit(s *SshTransport) (SshReply, error) { - return s.Run("commit", 10), nil + return s } -func (sk *VrSrosSshKind) PromptParse(s *SshTransport, in *string) *SshReply { - return promptParseNoSpaces(in, s.PromptChar, 2) -} - -// implements SShKind -type SrlSshKind struct{} -func (sk *SrlSshKind) ConfigStart(s *SshTransport, node string, transaction bool) error { - s.PromptChar = "#" // ensure it's '#' - s.debug = true - if transaction { - s.Run("enter candidate", 5) - s.Run("discard stay", 2) +func (r *SshReply) Info(node string) *SshReply { + if r.result == "" { + return r } - return nil -} -func (sk *SrlSshKind) ConfigCommit(s *SshTransport) (SshReply, error) { - return s.Run("commit now", 10), nil -} -func (sk *SrlSshKind) PromptParse(s *SshTransport, in *string) *SshReply { - return promptParseNoSpaces(in, s.PromptChar, 2) + log.Info(r.LogString(node, false, false)) + return r } -func (r *SshReply) Debug() { - _, fn, line, _ := runtime.Caller(1) - log.Debugf("(%s line %d) *RESULT: %s.\n | %v\n*PROMPT:%v.\n*PROMPT:%v.\n", fn, line, r.result, []byte(r.result), r.prompt, []byte(r.prompt)) -} - -func (r *SshReply) Infof(msg string, args ...interface{}) { - var s string - if r.result != "" { - s = "\n | " - s += strings.Join(strings.Split(r.result, "\n"), s) +func (r *SshReply) Debug(node, message string, t ...interface{}) { + msg := message + if len(t) > 0 { + msg = t[0].(string) } - log.Infof(msg+s, args...) + _, fn, line, _ := runtime.Caller(1) + msg += fmt.Sprintf("(%s line %d)", fn, line) + msg += r.LogString(node, true, true) + log.Debugf(msg) } diff --git a/clab/config/sshkind.go b/clab/config/sshkind.go new file mode 100644 index 000000000..f0a4b0910 --- /dev/null +++ b/clab/config/sshkind.go @@ -0,0 +1,113 @@ +package config + +import ( + "fmt" + "strings" + + log "github.com/sirupsen/logrus" +) + +// an interface to implement kind specific methods for transactions and prompt checking +type SshKind interface { + // Start a config transaction + ConfigStart(s *SshTransport, transaction bool) error + // Commit a config transaction + ConfigCommit(s *SshTransport) (*SshReply, error) + // Prompt parsing function. + // This function receives string, split by the delimiter and should ensure this is a valid prompt + // Valid prompt, strip te prompt from the result and add it to the prompt in SshReply + // + // A defualt implementation is promptParseNoSpaces, which simply ensures there are + // no spaces between the start of the line and the # + PromptParse(s *SshTransport, in *string) *SshReply +} + +// implements SShKind +type VrSrosSshKind struct{} + +func (sk *VrSrosSshKind) ConfigStart(s *SshTransport, transaction bool) error { + s.PromptChar = "#" // ensure it's '#' + //s.debug = true + r := s.Run("/environment more false", 5) + if r.result != "" { + log.Warn("%s Are you in MD-Mode?", s.Target, r.LogString(s.Target, true, false)) + } + + if transaction { + s.Run("/configure global", 5).Info(s.Target) + s.Run("discard", 1).Info(s.Target) + } + return nil +} +func (sk *VrSrosSshKind) ConfigCommit(s *SshTransport) (*SshReply, error) { + res := s.Run("commit", 10) + if res.result != "" { + return res, fmt.Errorf("could not commit %s", res.result) + } + return res, nil +} + +func (sk *VrSrosSshKind) PromptParse(s *SshTransport, in *string) *SshReply { + // SROS MD-CLI \r...prompt + r := strings.LastIndex(*in, "\r\n\r\n") + if r > 0 { + return &SshReply{ + result: (*in)[:r], + prompt: (*in)[r+4:] + s.PromptChar, + } + } + return nil +} + +// implements SShKind +type SrlSshKind struct{} + +func (sk *SrlSshKind) ConfigStart(s *SshTransport, transaction bool) error { + s.PromptChar = "#" // ensure it's '#' + if transaction { + r0 := s.Run("enter candidate private", 5) + r1 := s.Run("discard stay", 2) + if !strings.Contains(r1.result, "Nothing to discard") { + r0.result += r1.result + r0.command += "; " + r1.command + } + r0.Info(s.Target) + } + return nil +} +func (sk *SrlSshKind) ConfigCommit(s *SshTransport) (*SshReply, error) { + r := s.Run("commit now", 10) + if strings.Contains(r.result, "All changes have been committed") { + r.result = "" + } else { + return r, fmt.Errorf("could not commit %s", r.result) + } + return r, nil +} +func (sk *SrlSshKind) PromptParse(s *SshTransport, in *string) *SshReply { + return promptParseNoSpaces(in, s.PromptChar, 2) +} + +// This is a helper funciton to parse the prompt, and can be used by SshKind's ParsePrompt +// Used in SRL today +func promptParseNoSpaces(in *string, promptChar string, lines int) *SshReply { + n := strings.LastIndex(*in, "\n") + if n < 0 { + return nil + } + if strings.Contains((*in)[n:], " ") { + return nil + } + if lines > 1 { + // Add another line to the prompt + res := (*in)[:n] + n = strings.LastIndex(res, "\n") + } + if n < 0 { + n = 0 + } + return &SshReply{ + result: (*in)[:n], + prompt: (*in)[n:] + promptChar, + } +} diff --git a/cmd/config.go b/cmd/config.go index b62f60729..288c83b28 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -30,6 +30,8 @@ var configCmd = &cobra.Command{ return err } + config.DebugCount = debugCount + opts := []clab.ClabOption{ clab.WithDebug(debug), clab.WithTimeout(timeout), @@ -101,7 +103,7 @@ var configCmd = &cobra.Command{ var wg sync.WaitGroup wg.Add(len(allConfig)) for _, cs_ := range allConfig { - go func(cs []config.ConfigSnippet) { + deploy1 := func(cs []config.ConfigSnippet) { defer wg.Done() var transport config.Transport @@ -127,8 +129,14 @@ var configCmd = &cobra.Command{ if err != nil { log.Errorf("%s\n", err) } + } - }(cs_) + // On debug this will not be executed concurrently + if log.IsLevelEnabled(log.DebugLevel) { + deploy1(cs_) + } else { + go deploy1(cs_) + } } wg.Wait() diff --git a/cmd/root.go b/cmd/root.go index c711b8b69..885f6a103 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,6 +10,7 @@ import ( "github.com/srl-labs/containerlab/runtime" ) +var debugCount int var debug bool var timeout time.Duration @@ -26,6 +27,7 @@ var rootCmd = &cobra.Command{ Use: "containerlab", Short: "deploy container based lab environments with a user-defined interconnections", PersistentPreRun: func(cmd *cobra.Command, args []string) { + debug = debugCount > 0 if debug { log.SetLevel(log.DebugLevel) } @@ -43,7 +45,7 @@ func Execute() { func init() { rootCmd.SilenceUsage = true - rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "enable debug mode") + rootCmd.PersistentFlags().CountVarP(&debugCount, "debug", "d", "enable debug mode") rootCmd.PersistentFlags().StringVarP(&topo, "topo", "t", "", "path to the file with topology information") _ = rootCmd.MarkPersistentFlagFilename("topo", "*.yaml", "*.yml") rootCmd.PersistentFlags().StringVarP(&name, "name", "n", "", "lab name") From bd3a858a3a82497b31cf6713b73ff945cc6552c3 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 23 Apr 2021 14:19:01 +0200 Subject: [PATCH 13/33] ok2 --- clab/config/sshkind.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clab/config/sshkind.go b/clab/config/sshkind.go index f0a4b0910..c729126dc 100644 --- a/clab/config/sshkind.go +++ b/clab/config/sshkind.go @@ -30,7 +30,7 @@ func (sk *VrSrosSshKind) ConfigStart(s *SshTransport, transaction bool) error { //s.debug = true r := s.Run("/environment more false", 5) if r.result != "" { - log.Warn("%s Are you in MD-Mode?", s.Target, r.LogString(s.Target, true, false)) + log.Warnf("%s Are you in MD-Mode?%s", s.Target, r.LogString(s.Target, true, false)) } if transaction { @@ -68,7 +68,7 @@ func (sk *SrlSshKind) ConfigStart(s *SshTransport, transaction bool) error { r0 := s.Run("enter candidate private", 5) r1 := s.Run("discard stay", 2) if !strings.Contains(r1.result, "Nothing to discard") { - r0.result += r1.result + r0.result += "; " + r1.result r0.command += "; " + r1.command } r0.Info(s.Target) From 224a26dc044f620cbb4f26c8a76b8d96c2885162 Mon Sep 17 00:00:00 2001 From: "Johann Kellerman kellerza@gmail.com" Date: Fri, 23 Apr 2021 22:41:15 +0200 Subject: [PATCH 14/33] feedback --- clab/config/ssh.go | 5 +---- clab/config/template.go | 2 +- clab/config/transport.go | 6 +++--- cmd/config.go | 14 ++++++-------- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/clab/config/ssh.go b/clab/config/ssh.go index 49e86fb9b..d9908503e 100644 --- a/clab/config/ssh.go +++ b/clab/config/ssh.go @@ -17,9 +17,6 @@ type SshSession struct { Session *ssh.Session } -// Display the SSH login message -var LoginMessages bool - // Debug count var DebugCount int @@ -102,7 +99,7 @@ func (t *SshTransport) InChannel() { // Save first prompt t.LoginMessage = t.Run("", 15) - if LoginMessages { + if DebugCount > 1 { t.LoginMessage.Info(t.Target) } } diff --git a/clab/config/template.go b/clab/config/template.go index 51cc52311..b5d9f8c8e 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -40,7 +40,7 @@ func LoadTemplate(kind, templatePath string) error { var err error templates[kind], err = ct.ParseGlob(tp) if err != nil { - log.Errorf("could not load template %s", err) + log.Errorf("Could not load %s %s", tp, err) return err } return nil diff --git a/clab/config/transport.go b/clab/config/transport.go index 10b083e43..001eb0642 100644 --- a/clab/config/transport.go +++ b/clab/config/transport.go @@ -36,7 +36,7 @@ func WriteConfig(transport Transport, snips []ConfigSnippet) error { } // templates to execute -var TemplateOverride string +var TemplateOverride []string // the new agreed node config type NodeConfig struct { @@ -59,8 +59,8 @@ func GetNodeConfigFromLabels(labels map[string]string) NodeConfig { Vars: labels, Transport: "ssh", } - if TemplateOverride != "" { - nc.Templates = SplitTrim(TemplateOverride) + if len(TemplateOverride) > 0 { + nc.Templates = TemplateOverride } else if t, ok := labels["templates"]; ok { nc.Templates = SplitTrim(t) } else { diff --git a/cmd/config.go b/cmd/config.go index 288c83b28..bd2641f09 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -44,7 +44,7 @@ var configCmd = &cobra.Command{ //defer cancel() setFlags(c.Config) - log.Debugf("lab Conf: %+v", c.Config) + log.Debugf("Topology definition: %+v", c.Config) // Parse topology information if err = c.ParseTopology(); err != nil { return err @@ -56,8 +56,7 @@ var configCmd = &cobra.Command{ renderErr := 0 for _, node := range c.Nodes { - kind := node.Labels["clab-node-kind"] - err = config.LoadTemplate(kind, templatePath) + err = config.LoadTemplate(node.Kind, templatePath) if err != nil { return err } @@ -167,9 +166,8 @@ func newSSHTransport(node *clab.Node) (*config.SshTransport, error) { func init() { rootCmd.AddCommand(configCmd) - configCmd.Flags().StringVarP(&templatePath, "path", "", "", "specify template path") - configCmd.MarkFlagDirname("path") - configCmd.Flags().StringVarP(&config.TemplateOverride, "templates", "", "", "specify a list of template to apply") - configCmd.Flags().IntVarP(&printLines, "print-only", "p", 0, "print config, don't send it. Restricted to n lines") - configCmd.Flags().BoolVarP(&config.LoginMessages, "login-message", "", false, "show the SSH login message") + configCmd.Flags().StringVarP(&templatePath, "template-path", "p", "", "directory with templates used to render config") + configCmd.MarkFlagDirname("template-path") + configCmd.Flags().StringSliceVarP(&config.TemplateOverride, "template-list", "l", []string{}, "comma separated list of template names to render") + configCmd.Flags().IntVarP(&printLines, "check", "c", 0, "render dry-run & print n lines of config") } From 0a9fd67282160abf5306c247d92e04f3ffb82333 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 25 May 2021 22:09:22 +0200 Subject: [PATCH 15/33] packages --- clab/config/functions.go | 230 ------------------------- clab/config/functions_test.go | 123 ------------- clab/config/helpers.go | 39 +++++ clab/config/template.go | 221 ++++++------------------ clab/config/transport.go | 73 -------- clab/config/{ => transport}/ssh.go | 127 ++++++++------ clab/config/{ => transport}/sshkind.go | 36 ++-- clab/config/transport/transport.go | 36 ++++ clab/config/utils.go | 112 ++++++++++-- clab/config/utils_test.go | 2 +- cmd/config.go | 89 +++------- go.mod | 8 +- go.sum | 16 +- templates/vr-sros/base-link.tmpl | 18 ++ templates/vr-sros/base-node.tmpl | 15 -- types/types.go | 6 + 16 files changed, 386 insertions(+), 765 deletions(-) delete mode 100644 clab/config/functions.go delete mode 100644 clab/config/functions_test.go create mode 100644 clab/config/helpers.go delete mode 100644 clab/config/transport.go rename clab/config/{ => transport}/ssh.go (71%) rename clab/config/{ => transport}/sshkind.go (74%) create mode 100644 clab/config/transport/transport.go diff --git a/clab/config/functions.go b/clab/config/functions.go deleted file mode 100644 index bc66d5c62..000000000 --- a/clab/config/functions.go +++ /dev/null @@ -1,230 +0,0 @@ -package config - -import ( - "fmt" - "regexp" - "strconv" - "strings" - - "inet.af/netaddr" -) - -func typeof(val interface{}) string { - switch val.(type) { - case string: - return "string" - case int, int16, int32: - return "int" - } - return "" -} - -func hasInt(val interface{}) (int, bool) { - if i, err := strconv.Atoi(fmt.Sprintf("%v", val)); err == nil { - return i, true - } - return 0, false -} - -func expectFunc(val interface{}, format string) (interface{}, error) { - t := typeof(val) - vals := fmt.Sprintf("%s", val) - - // known formats - switch format { - case "str", "string": - if t == "string" { - return "", nil - } - return "", fmt.Errorf("string expected, got %s (%v)", t, val) - case "int": - if _, ok := hasInt(val); ok { - return "", nil - } - return "", fmt.Errorf("int expected, got %s (%v)", t, val) - case "ip": - if _, err := netaddr.ParseIPPrefix(vals); err == nil { - return "", nil - } - return "", fmt.Errorf("IP/mask expected, got %v", val) - } - - // try range - if matched, _ := regexp.MatchString(`\d+-\d+`, format); matched { - iv, ok := hasInt(val) - if !ok { - return "", fmt.Errorf("int expected, got %s (%v)", t, val) - } - r := strings.Split(format, "-") - i0, _ := hasInt(r[0]) - i1, _ := hasInt(r[1]) - if i1 < i0 { - i0, i1 = i1, i0 - } - if i0 <= iv && iv <= i1 { - return "", nil - } - return "", fmt.Errorf("value (%d) expected to be in range %d-%d", iv, i0, i1) - } - - // Try regex - matched, err := regexp.MatchString(format, vals) - if err != nil || !matched { - return "", fmt.Errorf("value %s does not match regex %s %v", vals, format, err) - } - - return "", nil -} - -var funcMap = map[string]interface{}{ - "optional": func(val interface{}, format string) (interface{}, error) { - if val == nil { - return "", nil - } - return expectFunc(val, format) - }, - "expect": expectFunc, - // "require": func(val interface{}) (interface{}, error) { - // if val == nil { - // return nil, errors.New("required value not set") - // } - // return val, nil - // }, - "ip": func(val interface{}) (interface{}, error) { - s := fmt.Sprintf("%v", val) - a := strings.Split(s, "/") - return a[0], nil - }, - "ipmask": func(val interface{}) (interface{}, error) { - s := fmt.Sprintf("%v", val) - a := strings.Split(s, "/") - return a[1], nil - }, - "default": func(in ...interface{}) (interface{}, error) { - if len(in) < 2 { - return nil, fmt.Errorf("default value expected") - } - if len(in) > 2 { - return nil, fmt.Errorf("too many arguments") - } - - val := in[len(in)-1] - def := in[0] - - switch v := val.(type) { - case nil: - return def, nil - case string: - if v == "" { - return def, nil - } - case bool: - if !v { - return def, nil - } - } - // if val == nil { - // return def, nil - // } - - // If we have a input value, do some type checking - tval, tdef := typeof(val), typeof(def) - if tval == "string" && tdef == "int" { - if _, err := strconv.Atoi(val.(string)); err == nil { - tval = "int" - } - if tdef == "str" { - if _, err := strconv.Atoi(def.(string)); err == nil { - tdef = "int" - } - } - } - if tdef != tval { - return val, fmt.Errorf("expected type %v, got %v (value=%v)", tdef, tval, val) - } - - // Return the value - return val, nil - }, - "contains": func(substr string, str string) (interface{}, error) { - return strings.Contains(fmt.Sprintf("%v", str), fmt.Sprintf("%v", substr)), nil - }, - "split": func(sep string, val interface{}) (interface{}, error) { - // Start and end values - if val == nil { - return []interface{}{}, nil - } - if sep == "" { - sep = " " - } - - v := fmt.Sprintf("%v", val) - - res := strings.Split(v, sep) - r := make([]interface{}, len(res)) - for i, p := range res { - r[i] = p - } - return r, nil - }, - "join": func(sep string, val interface{}) (interface{}, error) { - if sep == "" { - sep = " " - } - // Start and end values - switch v := val.(type) { - case []interface{}: - if val == nil { - return "", nil - } - res := make([]string, len(v)) - for i, v := range v { - res[i] = fmt.Sprintf("%v", v) - } - return strings.Join(res, sep), nil - case []string: - return strings.Join(v, sep), nil - case []int, []int16, []int32: - return strings.Trim(strings.ReplaceAll(fmt.Sprint(v), " ", sep), "[]"), nil - } - return nil, fmt.Errorf("expected array [], got %v", val) - }, - "slice": func(start, end int, val interface{}) (interface{}, error) { - // string or array - switch v := val.(type) { - case string: - if start < 0 { - start += len(v) - } - if end < 0 { - end += len(v) - } - return v[start:end], nil - case []interface{}: - if start < 0 { - start += len(v) - } - if end < 0 { - end += len(v) - } - return v[start:end], nil - } - return nil, fmt.Errorf("not an array") - }, - "index": func(idx int, val interface{}) (interface{}, error) { - // string or array - switch v := val.(type) { - case string: - if idx < 0 { - idx += len(v) - } - return v[idx], nil - case []interface{}: - if idx < 0 { - idx += len(v) - } - return v[idx], nil - } - return nil, fmt.Errorf("not an array") - }, -} diff --git a/clab/config/functions_test.go b/clab/config/functions_test.go deleted file mode 100644 index fe9be3239..000000000 --- a/clab/config/functions_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package config - -import ( - "bytes" - "fmt" - "strings" - "testing" - "text/template" -) - -var test_set = map[string][]string{ - // empty values - "default 0 .x": {"0"}, - ".x | default 0": {"0"}, - "default 0 \"\"": {"0"}, - "default 0 false": {"0"}, - "false | default 0": {"0"}, - // ints pass through ok - "default 1 0": {"0"}, - // errors - "default .x": {"", "default value expected"}, - "default .x 1 1": {"", "too many arguments"}, - // type check - "default 0 .i5": {"5"}, - "default 0 .sA": {"", "expected type int"}, - `default "5" .sA`: {"A"}, - - `contains "." .sAAA`: {"true"}, - `.sAAA | contains "."`: {"true"}, - `contains "." .sA`: {"false"}, - `.sA | contains "."`: {"false"}, - - `split "." "a.a"`: {"[a a]"}, - `split " " "a bb"`: {"[a bb]"}, - - `ip "1.1.1.1/32"`: {"1.1.1.1"}, - `"1.1.1.1" | ip`: {"1.1.1.1"}, - `ipmask "1.1.1.1/32"`: {"32"}, - `"1.1.1.1/32" | split "/" | slice 0 1 | join ""`: {"1.1.1.1"}, - `"1.1.1.1/32" | split "/" | slice 1 2 | join ""`: {"32"}, - - `split " " "a bb" | join "-"`: {"a-bb"}, - `split "" ""`: {"[]"}, - `split "abc" ""`: {"[]"}, - - `"1.1.1.1/32" | split "/" | index 1`: {"32"}, - `"1.1.1.1/32" | split "/" | index -1`: {"32"}, - `"1.1.1.1/32" | split "/" | index -2`: {"1.1.1.1"}, - `"1.1.1.1/32" | split "/" | index -3`: {"", "out of range"}, - `"1.1.1.1/32" | split "/" | index 2`: {"", "out of range"}, - - `expect "1.1.1.1/32" "ip"`: {""}, - `expect "1.1.1.1" "ip"`: {"", "IP/mask"}, - `expect "1" "0-10"`: {""}, - `expect "1" "10-10"`: {"", "range"}, - `expect "1.1" "\\d+\\.\\d+"`: {""}, - `expect 11 "\\d"`: {""}, - `expect 11 "\\d+"`: {""}, - `expect "abc" "^[a-z]+$"`: {""}, - - `expect 1 "int"`: {""}, - `expect 1 "str"`: {"", "string expected"}, - `expect 1 "string"`: {"", "string expected"}, - `expect .i5 "int"`: {""}, - `expect "5" "int"`: {""}, // hasInt - `expect "aa" "int"`: {"", "int expected"}, - - `optional 1 "int"`: {""}, - `optional .x "int"`: {""}, - `optional .x "str"`: {""}, - `optional .i5 "str"`: {""}, // corner case, although it hasInt everything is always a string -} - -func render(templateS string, vars map[string]string) (string, error) { - var err error - buf := new(bytes.Buffer) - ts := fmt.Sprintf("{{ %v }}", strings.Trim(templateS, "{} ")) - tem, err := template.New("").Funcs(funcMap).Parse(ts) - if err != nil { - return "", fmt.Errorf("invalid template") - } - err = tem.Execute(buf, vars) - return buf.String(), err -} - -func TestRender1(t *testing.T) { - - l := map[string]string{ - "i5": "5", - "sA": "A", - "sAAA": "aa.", - "dot": ".", - "space": " ", - } - - for tem, exp := range test_set { - res, err := render(tem, l) - - e := []string{fmt.Sprintf(`{{ %v }} = "%v", error=%v`, tem, res, err)} - - // Check value - if res != exp[0] { - e = append(e, fmt.Sprintf("- expected value = %v", exp[0])) - } - - // Check errors - if len(exp) > 1 { - ee := fmt.Sprintf("- expected error with %s", exp[1]) - if err == nil { - e = append(e, ee) - } else if !strings.Contains(err.Error(), exp[1]) { - e = append(e, ee) - } - } else if err != nil { - e = append(e, "- no error expected") - } - - if len(e) > 1 { - t.Error(strings.Join(e, "\n")) - } - } - -} diff --git a/clab/config/helpers.go b/clab/config/helpers.go new file mode 100644 index 000000000..c4c1d5bbc --- /dev/null +++ b/clab/config/helpers.go @@ -0,0 +1,39 @@ +package config + +import ( + "strings" +) + +// Split a string on commans and trim each +func SplitTrim(s string) []string { + res := strings.Split(s, ",") + for i, v := range res { + res[i] = strings.Trim(v, " \n\t") + } + return res +} + +// the new agreed node config +type NodeSettings struct { + Vars map[string]string + Transport string + Templates []string +} + +func GetNodeConfigFromLabels(labels map[string]string) NodeSettings { + nc := NodeSettings{ + Vars: labels, + Transport: "ssh", + } + if len(TemplateNames) > 0 { + nc.Templates = TemplateNames + } else if t, ok := labels["templates"]; ok { + nc.Templates = SplitTrim(t) + } else { + nc.Templates = []string{"base"} + } + if t, ok := labels["transport"]; ok { + nc.Transport = t + } + return nc +} diff --git a/clab/config/template.go b/clab/config/template.go index b5d9f8c8e..e0b2811d5 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -1,203 +1,90 @@ package config import ( - "bytes" "encoding/json" "fmt" - "path/filepath" "strings" - "text/template" + + "github.com/kellerza/template" log "github.com/sirupsen/logrus" - "github.com/srl-labs/containerlab/clab" + "github.com/srl-labs/containerlab/types" ) -type ConfigSnippet struct { - TargetNode *clab.Node - // the Rendered template - Data []byte - // some info for tracing/debugging - templateName, source string - // All the variables used to render the template - vars *map[string]string -} - -// internal template cache -var templates map[string]*template.Template - -func LoadTemplate(kind, templatePath string) error { - if templates == nil { - templates = make(map[string]*template.Template) - } - if _, ok := templates[kind]; ok { - return nil - } - - tp := filepath.Join(templatePath, kind, "*.tmpl") - log.Debugf("Load templates from: %s", tp) - - ct := template.New(kind).Funcs(funcMap) - var err error - templates[kind], err = ct.ParseGlob(tp) - if err != nil { - log.Errorf("Could not load %s %s", tp, err) - return err - } - return nil -} - -func (c *ConfigSnippet) Render() error { - t := templates[c.TargetNode.Kind] - buf := new(strings.Builder) - c.Data = nil - - varsP, err := json.MarshalIndent(c.vars, "", " ") - if err != nil { - varsP = []byte(fmt.Sprintf("%s", c.vars)) - } - - err = t.ExecuteTemplate(buf, c.templateName, c.vars) - if err != nil { - log.Errorf("could not render template %s: %s vars=%s\n", c.String(), err, varsP) - return fmt.Errorf("could not render template %s: %s", c.String(), err) - } - - // Strip blank lines - res := strings.Trim(buf.String(), "\n") - res = strings.ReplaceAll(res, "\n\n\n", "\n\n") - c.Data = []byte(res) +// templates to execute +var TemplateNames []string - return nil -} - -func RenderNode(node *clab.Node) ([]ConfigSnippet, error) { - snips := []ConfigSnippet{} - nc := GetNodeConfigFromLabels(node.Labels) - - for _, tn := range nc.Templates { - tn = fmt.Sprintf("%s-node.tmpl", tn) - snip := ConfigSnippet{ - vars: &nc.Vars, - templateName: tn, - TargetNode: node, - source: "node", - } - - err := snip.Render() - if err != nil { - return nil, err - } - snips = append(snips, snip) - } - return snips, nil +type NodeConfig struct { + TargetNode *types.Node + // All the variables used to render the template + Vars map[string]interface{} + // the Rendered templates + Data []string + Info []string } -func RenderLink(link *clab.Link) ([]ConfigSnippet, error) { - // Link labels/values are different on node A & B - vars := make(map[string][]string) +var Tmpl *template.Template = template.New("") - ncA := GetNodeConfigFromLabels(link.A.Node.Labels) - ncB := GetNodeConfigFromLabels(link.B.Node.Labels) - linkVars := link.Labels +func RenderAll(nodes map[string]*types.Node, links map[int]*types.Link) (map[string]*NodeConfig, error) { + res := make(map[string]*NodeConfig) - // Link IPs - ipA, ipB, err := linkIPfromSystemIP(link) - if err != nil { - return nil, fmt.Errorf("%s: %s", link, err) - } - vars["ip"] = []string{ipA.String(), ipB.String()} - vars[systemIP] = []string{ncA.Vars[systemIP], ncB.Vars[systemIP]} - - // Split all fields with a comma... - for k, v := range linkVars { - r := strings.Split(v, ",") - switch len(r) { - case 1, 2: - vars[k] = r - default: - log.Warnf("%s: %s contains %d elements, should be 1 or 2: %s", link.String(), k, len(r), v) + for nodeName, vars := range PrepareVars(nodes, links) { + res[nodeName] = &NodeConfig{ + TargetNode: nodes[nodeName], + Vars: vars, } - } - // Set default Link/Interface Names - if _, ok := vars["name"]; !ok { - linkNr := linkVars["linkNr"] - if len(linkNr) > 0 { - linkNr = "_" + linkNr - } - vars["name"] = []string{fmt.Sprintf("to_%s%s", link.B.Node.ShortName, linkNr), - fmt.Sprintf("to_%s%s", link.A.Node.ShortName, linkNr)} - } - - snips := []ConfigSnippet{} - - for li := 0; li < 2; li++ { - // Current Node - curNode := link.A.Node - if li == 1 { - curNode = link.B.Node - } - // Current Vars - curVars := make(map[string]string) - for k, v := range vars { - if len(v) == 1 { - curVars[k] = strings.Trim(v[0], " \n\t") - } else { - curVars[k] = strings.Trim(v[li], " \n\t") - curVars[k+"_far"] = strings.Trim(v[(li+1)%2], " \n\t") - } - } - - curNodeC := GetNodeConfigFromLabels(curNode.Labels) - - for _, tn := range curNodeC.Templates { - snip := ConfigSnippet{ - vars: &curVars, - templateName: fmt.Sprintf("%s-link.tmpl", tn), - TargetNode: curNode, - source: link.String(), - } - err := snip.Render() - //res, err := RenderTemplate(kind, tn, curVars, curNode, link.String()) + for _, baseN := range TemplateNames { + tmplN := fmt.Sprintf("%s-%s.tmpl", baseN, vars["roles"]) + data1, err := Tmpl.ExecuteTemplate(tmplN, vars) if err != nil { - return nil, fmt.Errorf("render %s on %s (%s): %s", link, curNode.LongName, curNode.Kind, err) + log.Errorf("could not render template %s: %s", tmplN, err) + //log.Errorf("could not render template %s: %s vars=%s\n", c.String(), err, varsP) + return nil, fmt.Errorf("could not render template %s: %s", tmplN, err) } - snips = append(snips, snip) + res[nodeName].Data = append(res[nodeName].Data, data1) + res[nodeName].Info = append(res[nodeName].Info, tmplN) } } - return snips, nil + return res, nil } // Implement stringer for conf snippet -func (c *ConfigSnippet) String() string { - s := fmt.Sprintf("%s %s using %s/%s", c.TargetNode.ShortName, c.source, c.TargetNode.Kind, c.templateName) - if c.Data != nil { - s += fmt.Sprintf(" (%d lines)", bytes.Count(c.Data, []byte("\n"))+1) - } +func (c *NodeConfig) String() string { + + s := fmt.Sprintf("%s: %v", c.TargetNode.ShortName, c.Info) + // s := fmt.Sprintf("%s %s using %s/%s", c.TargetNode.ShortName, c.source, c.TargetNode.Kind, c.templateName) + // if c.Data != nil { + // s += fmt.Sprintf(" (%d lines)", bytes.Count(c.Data, []byte("\n"))+1) + // } return s } -// Return the buffer as strings -func (c *ConfigSnippet) Lines() []string { - return strings.Split(string(c.Data), "\n") -} +// Print the config +func (c *NodeConfig) Print(printLines int) { + var s strings.Builder + + s.WriteString(c.TargetNode.ShortName) -// Print the configSnippet -func (c *ConfigSnippet) Print(printLines int) { - vars := []byte{} if log.IsLevelEnabled(log.DebugLevel) { - vars, _ = json.MarshalIndent(c.vars, "", " ") + vars, _ := json.MarshalIndent(c.Vars, "", " ") + s.Write(vars) } - s := "" if printLines > 0 { - cl := strings.SplitN(string(c.Data), "\n", printLines+1) - if len(cl) > printLines { - cl[printLines] = "..." + for idx, conf := range c.Data { + fmt.Fprintf(&s, "%s", c.Info[idx]) + + cl := strings.SplitN(conf, "\n", printLines+1) + if len(cl) > printLines { + cl[printLines] = "..." + } + for _, l := range cl { + s.WriteString("\n ") + s.WriteString(l) + } } - s = "\n | " - s += strings.Join(cl, s) } - log.Infof("%s %s%s\n", c.String(), vars, s) + log.Infoln(s.String()) } diff --git a/clab/config/transport.go b/clab/config/transport.go deleted file mode 100644 index 001eb0642..000000000 --- a/clab/config/transport.go +++ /dev/null @@ -1,73 +0,0 @@ -package config - -import ( - "fmt" - "strings" -) - -type Transport interface { - // Connect to the target host - Connect(host string) error - // Execute some config - Write(snip *ConfigSnippet) error - Close() -} - -func WriteConfig(transport Transport, snips []ConfigSnippet) error { - host := snips[0].TargetNode.LongName - - // the Kind should configure the transport parameters before - - err := transport.Connect(host) - if err != nil { - return fmt.Errorf("%s: %s", host, err) - } - - defer transport.Close() - - for _, snip := range snips { - err := transport.Write(&snip) - if err != nil { - return fmt.Errorf("could not write config %s: %s", &snip, err) - } - } - - return nil -} - -// templates to execute -var TemplateOverride []string - -// the new agreed node config -type NodeConfig struct { - Vars map[string]string - Transport string - Templates []string -} - -// Split a string on commans and trim each -func SplitTrim(s string) []string { - res := strings.Split(s, ",") - for i, v := range res { - res[i] = strings.Trim(v, " \n\t") - } - return res -} - -func GetNodeConfigFromLabels(labels map[string]string) NodeConfig { - nc := NodeConfig{ - Vars: labels, - Transport: "ssh", - } - if len(TemplateOverride) > 0 { - nc.Templates = TemplateOverride - } else if t, ok := labels["templates"]; ok { - nc.Templates = SplitTrim(t) - } else { - nc.Templates = []string{"base"} - } - if t, ok := labels["transport"]; ok { - nc.Transport = t - } - return nc -} diff --git a/clab/config/ssh.go b/clab/config/transport/ssh.go similarity index 71% rename from clab/config/ssh.go rename to clab/config/transport/ssh.go index d9908503e..33ef8fbd4 100644 --- a/clab/config/ssh.go +++ b/clab/config/transport/ssh.go @@ -1,4 +1,4 @@ -package config +package transport import ( "fmt" @@ -8,30 +8,30 @@ import ( "time" log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/types" "golang.org/x/crypto/ssh" ) -type SshSession struct { +type SSHSession struct { In io.Reader Out io.WriteCloser Session *ssh.Session } -// Debug count -var DebugCount int +type SSHOption func(*SSHTransport) error // The reply the execute command and the prompt. -type SshReply struct{ result, prompt, command string } +type SSHReply struct{ result, prompt, command string } -// SshTransport setting needs to be set before calling Connect() -// SshTransport implement the Transport interface -type SshTransport struct { +// SSHTransport setting needs to be set before calling Connect() +// SSHTransport implements the Transport interface +type SSHTransport struct { // Channel used to read. Can use Expect to Write & read wit timeout - in chan SshReply + in chan SSHReply // SSH Session - ses *SshSession + ses *SSHSession // Contains the first read after connecting - LoginMessage *SshReply + LoginMessage *SSHReply // SSH parameters used in connect // defualt: 22 Port int @@ -41,14 +41,36 @@ type SshTransport struct { // SSH Options // required! - SshConfig *ssh.ClientConfig + SSHConfig *ssh.ClientConfig // Character to split the incoming stream (#/$/>) // default: # PromptChar string // Kind specific transactions & prompt checking function - K SshKind + K SSHKind +} + +func NewSSHTransport(node *types.Node, options ...SSHOption) (*SSHTransport, error) { + switch node.Kind { + case "vr-sros", "srl": + c := &SSHTransport{} + c.SSHConfig = &ssh.ClientConfig{} + + // apply options + for _, opt := range options { + opt(c) + } + + switch node.Kind { + case "vr-sros": + c.K = &VrSrosSSHKind{} + case "srl": + c.K = &SrlSSHKind{} + } + return c, nil + } + return nil, fmt.Errorf("no tranport implemented for kind: %s", node.Kind) } // Creates the channel reading the SSH connection @@ -56,12 +78,12 @@ type SshTransport struct { // The first prompt is saved in LoginMessages // // - The channel read the SSH session, splits on PromptChar -// - Uses SshKind's PromptParse to split the received data in *result* and *prompt* parts +// - Uses SSHKind's PromptParse to split the received data in *result* and *prompt* parts // (if no valid prompt was found, prompt will simply be empty and result contain all the data) // - Emit data -func (t *SshTransport) InChannel() { +func (t *SSHTransport) InChannel() { // Ensure we have a working channel - t.in = make(chan SshReply) + t.in = make(chan SSHReply) // setup a buffered string channel go func() { @@ -79,7 +101,7 @@ func (t *SshTransport) InChannel() { for i := 0; i < li; i++ { r := t.K.PromptParse(t, &parts[i]) if r == nil { - r = &SshReply{ + r = &SSHReply{ result: parts[i], } } @@ -91,7 +113,7 @@ func (t *SshTransport) InChannel() { tmpS += string(buf[:n]) } log.Debugf("In Channel closing: %v", err) - t.in <- SshReply{ + t.in <- SSHReply{ result: tmpS, prompt: "", } @@ -105,7 +127,7 @@ func (t *SshTransport) InChannel() { } // Run a single command and wait for the reply -func (t *SshTransport) Run(command string, timeout int) *SshReply { +func (t *SSHTransport) Run(command string, timeout int) *SSHReply { if command != "" { t.ses.Writeln(command) log.Debugf("--> %s\n", command) @@ -120,7 +142,7 @@ func (t *SshTransport) Run(command string, timeout int) *SshReply { select { case <-time.After(time.Duration(timeout) * time.Second): log.Warnf("timeout waiting for prompt: %s", command) - return &SshReply{ + return &SSHReply{ result: sHistory, command: command, } @@ -159,7 +181,7 @@ func (t *SshTransport) Run(command string, timeout int) *SshReply { sHistory = rr continue } - res := &SshReply{ + res := &SSHReply{ result: rr, prompt: ret.prompt, command: command, @@ -173,12 +195,12 @@ func (t *SshTransport) Run(command string, timeout int) *SshReply { // Write a config snippet (a set of commands) // Session NEEDS to be configurable for other kinds // Part of the Transport interface -func (t *SshTransport) Write(snip *ConfigSnippet) error { - if len(snip.Data) == 0 { +func (t *SSHTransport) Write(data *string, info *string) error { + if len(*data) == 0 { return nil } - transaction := !strings.HasPrefix(snip.templateName, "show-") + transaction := !strings.HasPrefix(*info, "show-") err := t.K.ConfigStart(t, transaction) if err != nil { @@ -187,7 +209,7 @@ func (t *SshTransport) Write(snip *ConfigSnippet) error { c := 0 - for _, l := range snip.Lines() { + for _, l := range strings.Split(*data, "\n") { l = strings.TrimSpace(l) if l == "" || strings.HasPrefix(l, "#") { continue @@ -198,11 +220,9 @@ func (t *SshTransport) Write(snip *ConfigSnippet) error { if transaction { commit, err := t.K.ConfigCommit(t) - msg := snip.String() - i := strings.Index(msg, " ") - msg = fmt.Sprintf("%s COMMIT%s - %d lines", msg[:i], msg[i:], c) + msg := fmt.Sprintf("%s COMMIT - %d lines", *info, c) if commit.result != "" { - msg += commit.LogString(snip.TargetNode.ShortName, true, false) + msg += commit.LogString(t.Target, true, false) } if err != nil { log.Error(msg) @@ -216,7 +236,7 @@ func (t *SshTransport) Write(snip *ConfigSnippet) error { // Connect to a host // Part of the Transport interface -func (t *SshTransport) Connect(host string) error { +func (t *SSHTransport) Connect(host string, options ...func(*Transport)) error { // Assign Default Values if t.PromptChar == "" { t.PromptChar = "#" @@ -224,18 +244,18 @@ func (t *SshTransport) Connect(host string) error { if t.Port == 0 { t.Port = 22 } - if t.SshConfig == nil { - return fmt.Errorf("require auth credentials in SshConfig") + if t.SSHConfig == nil { + return fmt.Errorf("require auth credentials in SSHConfig") } // Start some client config host = fmt.Sprintf("%s:%d", host, t.Port) //sshConfig := &ssh.ClientConfig{} - //SshConfigWithUserNamePassword(sshConfig, "admin", "admin") + //SSHConfigWithUserNamePassword(sshConfig, "admin", "admin") t.Target = strings.Split(strings.Split(host, ":")[0], "-")[2] - ses_, err := NewSshSession(host, t.SshConfig) + ses_, err := NewSSHSession(host, t.SSHConfig) if err != nil || ses_ == nil { return fmt.Errorf("cannot connect to %s: %s", host, err) } @@ -249,7 +269,7 @@ func (t *SshTransport) Connect(host string) error { // Close the Session and channels // Part of the Transport interface -func (t *SshTransport) Close() { +func (t *SSHTransport) Close() { if t.in != nil { close(t.in) t.in = nil @@ -259,21 +279,24 @@ func (t *SshTransport) Close() { // Add a basic username & password to a config. // Will initilize the config if required -func SshConfigWithUserNamePassword(config *ssh.ClientConfig, username, password string) { - if config == nil { - config = &ssh.ClientConfig{} - } - config.User = username - if config.Auth == nil { - config.Auth = []ssh.AuthMethod{} +func WithUserNamePassword(username, password string) SSHOption { + return func(tx *SSHTransport) error { + if tx.SSHConfig == nil { + tx.SSHConfig = &ssh.ClientConfig{} + } + tx.SSHConfig.User = username + if tx.SSHConfig.Auth == nil { + tx.SSHConfig.Auth = []ssh.AuthMethod{} + } + tx.SSHConfig.Auth = append(tx.SSHConfig.Auth, ssh.Password(password)) + tx.SSHConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey() + return nil } - config.Auth = append(config.Auth, ssh.Password(password)) - config.HostKeyCallback = ssh.InsecureIgnoreHostKey() } // Create a new SSH session (Dial, open in/out pipes and start the shell) // pass the authntication details in sshConfig -func NewSshSession(host string, sshConfig *ssh.ClientConfig) (*SshSession, error) { +func NewSSHSession(host string, sshConfig *ssh.ClientConfig) (*SSHSession, error) { if !strings.Contains(host, ":") { return nil, fmt.Errorf("include the port in the host: %s", host) } @@ -313,28 +336,28 @@ func NewSshSession(host string, sshConfig *ssh.ClientConfig) (*SshSession, error return nil, fmt.Errorf("session shell: %s", err) } - return &SshSession{ + return &SSHSession{ Session: session, In: sshIn, Out: sshOut, }, nil } -func (ses *SshSession) Writeln(command string) (int, error) { +func (ses *SSHSession) Writeln(command string) (int, error) { return ses.Out.Write([]byte(command + "\r")) } -func (ses *SshSession) Close() { +func (ses *SSHSession) Close() { log.Debugf("Closing session") ses.Session.Close() } -// The LogString will include the entire SshReply +// The LogString will include the entire SSHReply // Each field will be prefixed by a character. // # - command sent // | - result recieved // ? - prompt part of the result -func (r *SshReply) LogString(node string, linefeed, debug bool) string { +func (r *SSHReply) LogString(node string, linefeed, debug bool) string { ind := 12 + len(node) prefix := "\n" + strings.Repeat(" ", ind) s := "" @@ -356,7 +379,7 @@ func (r *SshReply) LogString(node string, linefeed, debug bool) string { return s } -func (r *SshReply) Info(node string) *SshReply { +func (r *SSHReply) Info(node string) *SSHReply { if r.result == "" { return r } @@ -364,7 +387,7 @@ func (r *SshReply) Info(node string) *SshReply { return r } -func (r *SshReply) Debug(node, message string, t ...interface{}) { +func (r *SSHReply) Debug(node, message string, t ...interface{}) { msg := message if len(t) > 0 { msg = t[0].(string) diff --git a/clab/config/sshkind.go b/clab/config/transport/sshkind.go similarity index 74% rename from clab/config/sshkind.go rename to clab/config/transport/sshkind.go index c729126dc..8ed5e9544 100644 --- a/clab/config/sshkind.go +++ b/clab/config/transport/sshkind.go @@ -1,4 +1,4 @@ -package config +package transport import ( "fmt" @@ -8,24 +8,24 @@ import ( ) // an interface to implement kind specific methods for transactions and prompt checking -type SshKind interface { +type SSHKind interface { // Start a config transaction - ConfigStart(s *SshTransport, transaction bool) error + ConfigStart(s *SSHTransport, transaction bool) error // Commit a config transaction - ConfigCommit(s *SshTransport) (*SshReply, error) + ConfigCommit(s *SSHTransport) (*SSHReply, error) // Prompt parsing function. // This function receives string, split by the delimiter and should ensure this is a valid prompt - // Valid prompt, strip te prompt from the result and add it to the prompt in SshReply + // Valid prompt, strip te prompt from the result and add it to the prompt in SSHReply // // A defualt implementation is promptParseNoSpaces, which simply ensures there are // no spaces between the start of the line and the # - PromptParse(s *SshTransport, in *string) *SshReply + PromptParse(s *SSHTransport, in *string) *SSHReply } // implements SShKind -type VrSrosSshKind struct{} +type VrSrosSSHKind struct{} -func (sk *VrSrosSshKind) ConfigStart(s *SshTransport, transaction bool) error { +func (sk *VrSrosSSHKind) ConfigStart(s *SSHTransport, transaction bool) error { s.PromptChar = "#" // ensure it's '#' //s.debug = true r := s.Run("/environment more false", 5) @@ -39,7 +39,7 @@ func (sk *VrSrosSshKind) ConfigStart(s *SshTransport, transaction bool) error { } return nil } -func (sk *VrSrosSshKind) ConfigCommit(s *SshTransport) (*SshReply, error) { +func (sk *VrSrosSSHKind) ConfigCommit(s *SSHTransport) (*SSHReply, error) { res := s.Run("commit", 10) if res.result != "" { return res, fmt.Errorf("could not commit %s", res.result) @@ -47,11 +47,11 @@ func (sk *VrSrosSshKind) ConfigCommit(s *SshTransport) (*SshReply, error) { return res, nil } -func (sk *VrSrosSshKind) PromptParse(s *SshTransport, in *string) *SshReply { +func (sk *VrSrosSSHKind) PromptParse(s *SSHTransport, in *string) *SSHReply { // SROS MD-CLI \r...prompt r := strings.LastIndex(*in, "\r\n\r\n") if r > 0 { - return &SshReply{ + return &SSHReply{ result: (*in)[:r], prompt: (*in)[r+4:] + s.PromptChar, } @@ -60,9 +60,9 @@ func (sk *VrSrosSshKind) PromptParse(s *SshTransport, in *string) *SshReply { } // implements SShKind -type SrlSshKind struct{} +type SrlSSHKind struct{} -func (sk *SrlSshKind) ConfigStart(s *SshTransport, transaction bool) error { +func (sk *SrlSSHKind) ConfigStart(s *SSHTransport, transaction bool) error { s.PromptChar = "#" // ensure it's '#' if transaction { r0 := s.Run("enter candidate private", 5) @@ -75,7 +75,7 @@ func (sk *SrlSshKind) ConfigStart(s *SshTransport, transaction bool) error { } return nil } -func (sk *SrlSshKind) ConfigCommit(s *SshTransport) (*SshReply, error) { +func (sk *SrlSSHKind) ConfigCommit(s *SSHTransport) (*SSHReply, error) { r := s.Run("commit now", 10) if strings.Contains(r.result, "All changes have been committed") { r.result = "" @@ -84,13 +84,13 @@ func (sk *SrlSshKind) ConfigCommit(s *SshTransport) (*SshReply, error) { } return r, nil } -func (sk *SrlSshKind) PromptParse(s *SshTransport, in *string) *SshReply { +func (sk *SrlSSHKind) PromptParse(s *SSHTransport, in *string) *SSHReply { return promptParseNoSpaces(in, s.PromptChar, 2) } -// This is a helper funciton to parse the prompt, and can be used by SshKind's ParsePrompt +// This is a helper funciton to parse the prompt, and can be used by SSHKind's ParsePrompt // Used in SRL today -func promptParseNoSpaces(in *string, promptChar string, lines int) *SshReply { +func promptParseNoSpaces(in *string, promptChar string, lines int) *SSHReply { n := strings.LastIndex(*in, "\n") if n < 0 { return nil @@ -106,7 +106,7 @@ func promptParseNoSpaces(in *string, promptChar string, lines int) *SshReply { if n < 0 { n = 0 } - return &SshReply{ + return &SSHReply{ result: (*in)[:n], prompt: (*in)[n:] + promptChar, } diff --git a/clab/config/transport/transport.go b/clab/config/transport/transport.go new file mode 100644 index 000000000..c08db71a4 --- /dev/null +++ b/clab/config/transport/transport.go @@ -0,0 +1,36 @@ +package transport + +import ( + "fmt" +) + +// Debug count +var DebugCount int + +type Transport interface { + // Connect to the target host + Connect(host string, options ...func(*Transport)) error + // Execute some config + Write(data *string, info *string) error + Close() +} + +func Write(tx Transport, host string, data, info []string, options ...func(*Transport)) error { + // the Kind should configure the transport parameters before + + err := tx.Connect(host, options...) + if err != nil { + return fmt.Errorf("%s: %s", host, err) + } + + defer tx.Close() + + for i1, d1 := range data { + err := tx.Write(&d1, &info[i1]) + if err != nil { + return fmt.Errorf("could not write config %s: %s", d1, err) + } + } + + return nil +} diff --git a/clab/config/utils.go b/clab/config/utils.go index 6308e4d9b..2786518a2 100644 --- a/clab/config/utils.go +++ b/clab/config/utils.go @@ -6,7 +6,7 @@ import ( "strings" log "github.com/sirupsen/logrus" - "github.com/srl-labs/containerlab/clab" + "github.com/srl-labs/containerlab/types" "inet.af/netaddr" ) @@ -14,7 +14,96 @@ const ( systemIP = "systemip" ) -func linkIPfromSystemIP(link *clab.Link) (netaddr.IPPrefix, netaddr.IPPrefix, error) { +type Dict map[string]interface{} + +func PrepareVars(nodes map[string]*types.Node, links map[int]*types.Link) map[string]Dict { + + res := make(map[string]Dict) + + // preparing all nodes vars + for _, node := range nodes { + name := node.ShortName + // Init array for this node + res[name] = make(map[string]interface{}) + nc := GetNodeConfigFromLabels(node.Labels) + for _, key := range nc.Vars { + res[name][key] = nc.Vars[key] + } + // Create link array + res[name]["links"] = make([]interface{}, 2) + // Ensure role or Kind + if _, ok := res[name]["role"]; !ok { + res[name]["role"] = node.Kind + } + } + + // prepare all links + for lIdx, link := range links { + varsA := make(map[string]interface{}) + varsB := make(map[string]interface{}) + err := prepareLinkVars(lIdx, link, varsA, varsB) + if err != nil { + log.Errorf("cannot prepare link vars for %d. %s: %s", lIdx, link.String(), err) + } + res[link.A.Node.ShortName]["links"] = append(res[link.A.Node.ShortName]["links"].([]interface{}), varsA) + res[link.B.Node.ShortName]["links"] = append(res[link.B.Node.ShortName]["links"].([]interface{}), varsB) + } + return res +} + +func prepareLinkVars(lIdx int, link *types.Link, varsA, varsB map[string]interface{}) error { + ncA := GetNodeConfigFromLabels(link.A.Node.Labels) + ncB := GetNodeConfigFromLabels(link.B.Node.Labels) + linkVars := link.Labels + + addV := func(key string, v1 interface{}, v2 ...interface{}) { + varsA[key] = v1 + if len(v2) == 0 { + varsB[key] = v1 + } else { + varsA[key+"_far"] = v2[1] + varsB[key] = v2[1] + varsB[key+"_far"] = v1 + } + } + + // Link IPs + ipA, ipB, err := linkIPfromSystemIP(link) + if err != nil { + return fmt.Errorf("%s: %s", link, err) + } + addV("ip", ipA.String(), ipB.String()) + addV(systemIP, ncA.Vars[systemIP], ncB.Vars[systemIP]) + + // Split all fields with a comma... + for k, v := range linkVars { + r := SplitTrim(v) + switch len(r) { + case 1: + addV(k, r[0]) + case 2: + addV(k, r[0], r[1]) + default: + log.Warnf("%s: %s contains %d elements, should be 1 or 2: %s", link.String(), k, len(r), v) + } + } + + //Repeat the following for varsA and varsB + for _, vars := range []map[string]interface{}{varsA, varsB} { + // Set default Link/Interface Names + if _, ok := vars["name"]; !ok { + var linkNr string + if v, ok := vars["linkNr"]; ok { + linkNr = fmt.Sprintf("_%v", v) + } + vars["name"] = []string{fmt.Sprintf("to_%s%s", link.B.Node.ShortName, linkNr), + fmt.Sprintf("to_%s%s", link.A.Node.ShortName, linkNr)} + } + } + return nil +} + +func linkIPfromSystemIP(link *types.Link) (netaddr.IPPrefix, netaddr.IPPrefix, error) { var ipA netaddr.IPPrefix var err error if linkIp, ok := link.Labels["ip"]; ok { @@ -35,7 +124,7 @@ func linkIPfromSystemIP(link *clab.Link) (netaddr.IPPrefix, netaddr.IPPrefix, er if err != nil { return ipA, ipA, fmt.Errorf("no 'ip' on link & the '%s' of %s: %s", systemIP, link.B.Node.ShortName, err) } - o2, o3, o4 := ipLastOctet(sysA.IP), ipLastOctet(sysB.IP), 0 + o2, o3, o4 := ipLastOctet(sysA.IP()), ipLastOctet(sysB.IP()), 0 if o3 < o2 { o2, o3, o4 = o3, o2, o4+1 } @@ -61,28 +150,25 @@ func ipLastOctet(in netaddr.IP) int { } func ipFarEnd(in netaddr.IPPrefix) netaddr.IPPrefix { - if in.IP.Is4() && in.Bits == 32 { + if in.IP().Is4() && in.Bits() == 32 { return netaddr.IPPrefix{} } - n := in.IP.Next() + n := in.IP().Next() - if in.IP.Is4() && in.Bits <= 30 { - if !in.Contains(n) || !in.Contains(in.IP.Prior()) { + if in.IP().Is4() && in.Bits() <= 30 { + if !in.Contains(n) || !in.Contains(in.IP().Prior()) { return netaddr.IPPrefix{} } if !in.Contains(n.Next()) { - n = in.IP.Prior() + n = in.IP().Prior() } } if !in.Contains(n) { - n = in.IP.Prior() + n = in.IP().Prior() } if !in.Contains(n) { return netaddr.IPPrefix{} } - return netaddr.IPPrefix{ - IP: n, - Bits: in.Bits, - } + return netaddr.IPPrefixFrom(n, in.Bits()) } diff --git a/clab/config/utils_test.go b/clab/config/utils_test.go index fc50b943e..177a45642 100644 --- a/clab/config/utils_test.go +++ b/clab/config/utils_test.go @@ -46,7 +46,7 @@ func TestIPLastOctect(t *testing.T) { for k, v := range lst { n := netaddr.MustParseIPPrefix(k) - lo := ipLastOctet(n.IP) + lo := ipLastOctet(n.IP()) if v != lo { t.Errorf("far end of %s, got %d, expected %d", k, lo, v) } diff --git a/cmd/config.go b/cmd/config.go index bd2641f09..64f2fe75c 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/srl-labs/containerlab/clab" "github.com/srl-labs/containerlab/clab/config" - "golang.org/x/crypto/ssh" + "github.com/srl-labs/containerlab/clab/config/transport" ) // path to additional templates @@ -30,15 +30,13 @@ var configCmd = &cobra.Command{ return err } - config.DebugCount = debugCount + transport.DebugCount = debugCount - opts := []clab.ClabOption{ + c := clab.NewContainerLab( clab.WithDebug(debug), clab.WithTimeout(timeout), clab.WithTopoFile(topo), - clab.WithEnvDockerClient(), - } - c := clab.NewContainerLab(opts...) + ) //ctx, cancel := context.WithCancel(context.Background()) //defer cancel() @@ -51,39 +49,14 @@ var configCmd = &cobra.Command{ } // config map per node. each node gets a couple of config snippets []string - allConfig := make(map[string][]config.ConfigSnippet) - - renderErr := 0 - - for _, node := range c.Nodes { - err = config.LoadTemplate(node.Kind, templatePath) - if err != nil { - return err - } - - res, err := config.RenderNode(node) - if err != nil { - log.Errorln(err) - renderErr += 1 - continue - } - allConfig[node.LongName] = append(allConfig[node.LongName], res...) - + allConfig, err := config.RenderAll(c.Nodes, c.Links) + if err != nil { + return err } - for lIdx, link := range c.Links { + // render them all - res, err := config.RenderLink(link) - if err != nil { - log.Errorf("%d. %s\n", lIdx, err) - renderErr += 1 - continue - } - for _, rr := range res { - allConfig[rr.TargetNode.LongName] = append(allConfig[rr.TargetNode.LongName], rr) - } - - } + renderErr := 0 if renderErr > 0 { return fmt.Errorf("%d render warnings", renderErr) @@ -91,10 +64,8 @@ var configCmd = &cobra.Command{ if printLines > 0 { // Debug log all config to be deployed - for _, v := range allConfig { - for _, r := range v { - r.Print(printLines) - } + for _, c := range allConfig { + c.Print(printLines) } return nil } @@ -102,18 +73,23 @@ var configCmd = &cobra.Command{ var wg sync.WaitGroup wg.Add(len(allConfig)) for _, cs_ := range allConfig { - deploy1 := func(cs []config.ConfigSnippet) { + deploy1 := func(cs *config.NodeConfig) { defer wg.Done() - var transport config.Transport + var tx transport.Transport - ct, ok := cs[0].TargetNode.Labels["config.transport"] + ct, ok := cs.TargetNode.Labels["config.transport"] if !ok { ct = "ssh" } if ct == "ssh" { - transport, _ = newSSHTransport(cs[0].TargetNode) + tx, err = transport.NewSSHTransport( + cs.TargetNode, + transport.WithUserNamePassword( + clab.DefaultCredentials[cs.TargetNode.Kind][0], + clab.DefaultCredentials[cs.TargetNode.Kind][1]), + ) if err != nil { log.Errorf("%s: %s", kind, err) } @@ -124,7 +100,7 @@ var configCmd = &cobra.Command{ return } - err := config.WriteConfig(transport, cs) + err := transport.Write(tx, cs.TargetNode.LongName, cs.Data, cs.Info) if err != nil { log.Errorf("%s\n", err) } @@ -143,31 +119,10 @@ var configCmd = &cobra.Command{ }, } -func newSSHTransport(node *clab.Node) (*config.SshTransport, error) { - switch node.Kind { - case "vr-sros", "srl": - c := &config.SshTransport{} - c.SshConfig = &ssh.ClientConfig{} - config.SshConfigWithUserNamePassword( - c.SshConfig, - clab.DefaultCredentials[node.Kind][0], - clab.DefaultCredentials[node.Kind][1]) - - switch node.Kind { - case "vr-sros": - c.K = &config.VrSrosSshKind{} - case "srl": - c.K = &config.SrlSshKind{} - } - return c, nil - } - return nil, fmt.Errorf("no tranport implemented for kind: %s", kind) -} - func init() { rootCmd.AddCommand(configCmd) configCmd.Flags().StringVarP(&templatePath, "template-path", "p", "", "directory with templates used to render config") configCmd.MarkFlagDirname("template-path") - configCmd.Flags().StringSliceVarP(&config.TemplateOverride, "template-list", "l", []string{}, "comma separated list of template names to render") + configCmd.Flags().StringSliceVarP(&config.TemplateNames, "template-list", "l", []string{}, "comma separated list of template names to render") configCmd.Flags().IntVarP(&printLines, "check", "c", 0, "render dry-run & print n lines of config") } diff --git a/go.mod b/go.mod index 545710f91..0f0164d03 100644 --- a/go.mod +++ b/go.mod @@ -16,16 +16,16 @@ require ( github.com/google/uuid v1.2.0 github.com/hashicorp/go-version v1.2.1 github.com/jsimonetti/rtnetlink v0.0.0-20210226120601-1b79e63a70a0 + github.com/kellerza/template v0.0.1 github.com/mitchellh/go-homedir v1.1.0 github.com/morikuni/aec v1.0.0 // indirect github.com/olekukonko/tablewriter v0.0.5-0.20201029120751-42e21c7531a3 - github.com/sirupsen/logrus v1.7.0 + github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.0.0 github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852 github.com/ziutek/telnet v0.0.0-20180329124119-c3b780dc415b // indirect + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/term v0.0.0-20210503060354-a79de5458b56 gopkg.in/yaml.v2 v2.4.0 - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 - gopkg.in/yaml.v2 v2.3.0 - inet.af/netaddr v0.0.0-20210403172118-1e1430f727e0 // indirect + inet.af/netaddr v0.0.0-20210521171555-9ee55bc0c50b ) diff --git a/go.sum b/go.sum index 06377413e..4fba918f3 100644 --- a/go.sum +++ b/go.sum @@ -246,6 +246,7 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -412,6 +413,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kellerza/template v0.0.1 h1:4myoezGQD/BDIu+IOEGej60mcjz7LTjaJ2M6H7REIcg= +github.com/kellerza/template v0.0.1/go.mod h1:DRgSShodacWyLf9h+LWtlxxvTfWZW/AQlHKX2kb8g+4= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -588,8 +591,9 @@ github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -678,6 +682,11 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg= +go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -838,12 +847,12 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= @@ -897,6 +906,7 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1009,6 +1019,8 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +inet.af/netaddr v0.0.0-20210521171555-9ee55bc0c50b h1:GoOAHwQKK/dkkO/DgT/NQCSNZuXS6tlgLsHiXRakEiY= +inet.af/netaddr v0.0.0-20210521171555-9ee55bc0c50b/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls= k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= diff --git a/templates/vr-sros/base-link.tmpl b/templates/vr-sros/base-link.tmpl index 8cd331b97..4ab5d7d74 100644 --- a/templates/vr-sros/base-link.tmpl +++ b/templates/vr-sros/base-link.tmpl @@ -17,13 +17,31 @@ ipv4 primary prefix-length {{ ipmask .ip }} port {{ .port }}:{{ default 10 .vlan }} + /configure router isis {{ default 0 .isis_iid }} + area-address 49.0000.0000.0000.0{{ default 0 .isis_iid }} + level-capability 2 + level 2 wide-metrics-only + #database-export igp-identifier {{ default 0 .isis_iid }} bgp-ls-identifier value {{ default 0 .isis_iid }} + traffic-engineering + advertise-router-capability area + admin-state enable + interface "system" admin-state enable + {{- if .sid_idx }} + interface "system" ipv4-node-sid index {{ .sid_idx }} + segment-routing prefix-sid-range global + segment-routing admin-state enable + {{- end }} + interface {{ .name }} admin-state enable interface {{ .name }} interface-type point-to-point {{- if .metric }} interface {{ .name }} level 2 metric {{ .metric }} {{- end }} + +/configure router isis {{ default 0 .isis_iid }} + /configure router rsvp interface {{ .name }} admin-state enable diff --git a/templates/vr-sros/base-node.tmpl b/templates/vr-sros/base-node.tmpl index df15dc1b5..feb75118b 100644 --- a/templates/vr-sros/base-node.tmpl +++ b/templates/vr-sros/base-node.tmpl @@ -17,21 +17,6 @@ mpls-labels sr-labels start {{ default 19000 .sid_start }} end {{ default 30000 .sid_end }} {{- end }} -/configure router isis {{ default 0 .isis_iid }} - area-address 49.0000.0000.0000.0{{ default 0 .isis_iid }} - level-capability 2 - level 2 wide-metrics-only - #database-export igp-identifier {{ default 0 .isis_iid }} bgp-ls-identifier value {{ default 0 .isis_iid }} - traffic-engineering - advertise-router-capability area - admin-state enable - interface "system" admin-state enable - {{- if .sid_idx }} - interface "system" ipv4-node-sid index {{ .sid_idx }} - segment-routing prefix-sid-range global - segment-routing admin-state enable - {{- end }} - /configure router rsvp admin-state enable interface system admin-state enable diff --git a/types/types.go b/types/types.go index ae7913917..c4b214af6 100644 --- a/types/types.go +++ b/types/types.go @@ -2,6 +2,7 @@ package types import ( "bytes" + "fmt" "os" "path/filepath" "text/template" @@ -19,6 +20,11 @@ type Link struct { Labels map[string]string } +func (link *Link) String() string { + return fmt.Sprintf("link [%s:%s, %s:%s]", link.A.Node.ShortName, + link.A.EndpointName, link.B.Node.ShortName, link.B.EndpointName) +} + // Endpoint is a struct that contains information of a link endpoint type Endpoint struct { Node *Node From 6106301587e555b5cff96f4d9b4ce79cd41100aa Mon Sep 17 00:00:00 2001 From: "Johann Kellerman kellerza@gmail.com" Date: Mon, 26 Apr 2021 15:24:35 +0200 Subject: [PATCH 16/33] multiinstance --- templates/config/base-vr-sros.tmpl | 149 +++++++++++++++++++++++++++++ templates/vr-sros/base-link.tmpl | 8 +- templates/vr-sros/base-node.tmpl | 19 +++- 3 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 templates/config/base-vr-sros.tmpl diff --git a/templates/config/base-vr-sros.tmpl b/templates/config/base-vr-sros.tmpl new file mode 100644 index 000000000..0e536801b --- /dev/null +++ b/templates/config/base-vr-sros.tmpl @@ -0,0 +1,149 @@ +{{ expect .systemip "ip" }} +{{ optional .isis_iid "0-31" }} +{{ optional .sid_idx "0-999" }} +{{ optional .sid_start "19000-30000" }} +{{ optional .sid_end "19000-30000" }} +{{ range .links }} + {{ expect .port "^\\d+/\\d/" }} + {{ expect .name "string" }} + {{ expect .ip "ip" }} + {{ optional .vlan "0-4096" }} + {{ optional .isis_iid "0-31" }} + {{ optional .sid_idx "0-999" }} + {{ optional .metric "1-10000" }} +{{ end }} + +/configure system login-control idle-timeout 1440 + +/configure router interface "system" + ipv4 primary address {{ ip .systemip }} + ipv4 primary prefix-length {{ ipmask .systemip }} + admin-state enable + +/configure router + autonomous-system {{ default 64500 .as_number }} + mpls-labels sr-labels start {{ default 19000 .sid_start }} end {{ default 30000 .sid_end }} + +{{ if .isis_iid }} +/configure router isis {{ .isis_iid }} + area-address 49.0000.0000.0000.{{ .isis_iid | printf "%02s" }} + level-capability 2 + level 2 wide-metrics-only + #database-export igp-identifier {{ .isis_iid }} bgp-ls-identifier value {{ default 0 .isis_iid }} + traffic-engineering + advertise-router-capability area + admin-state enable + interface "system" admin-state enable + {{- if .sid_idx }} + interface "system" ipv4-node-sid index {{ .sid_idx }}{{ .isis_iid | printf "%02s" }} + segment-routing prefix-sid-range global + segment-routing admin-state enable + {{- end }} +{{ end }} + +/configure router rsvp + admin-state enable + interface system admin-state enable + +/configure router mpls + cspf-on-loose-hop + interface system admin-state enable + admin-state enable + pce-report rsvp-te true + pce-report sr-te true + +{{ range .links }} + +{{- if contains "/c" .port }} +/configure port {{ slice 0 -2 .port }} admin-state enable +/configure port {{ .port | slice 0 -2 }} connector breakout c1-10g +{{- end }} + +/configure port {{ .port }} admin-state enable + +/configure router interface {{ .name }} + ipv4 primary address {{ ip .ip }} + ipv4 primary prefix-length {{ ipmask .ip }} + port {{ .port }}:{{ default 10 .vlan }} + +{{- if .isis_iid }} +/configure router isis {{ default 0 .isis_iid }} + area-address 49.0000.0000.0000.{{ .isis_iid | printf "%02s" }} + level-capability 2 + level 2 wide-metrics-only + traffic-engineering + advertise-router-capability area + admin-state enable + interface "system" admin-state enable + {{- if .sid_idx }} + interface "system" ipv4-node-sid index {{ .sid_idx }}{{ default 0 .isis_iid | printf "%02s" }} + segment-routing prefix-sid-range global + segment-routing admin-state enable + {{- end }} + interface {{ .name }} admin-state enable + interface {{ .name }} interface-type point-to-point + interface {{ .name }} level 2 metric {{ default 10 .metric }} +{{- end }} + +/configure router rsvp + interface {{ .name }} admin-state enable + +/configure router mpls + interface {{ .name }} admin-state enable + +{{ end }} + + + +/configure apply-groups ["baseport"] +/configure router bgp apply-groups ["basebgp"] + +/configure groups { + group "baseport" { + port "<.*\/[0-9]+>" { + # wanted to add this, but you really need the /1 context to exist + # admin-state enable + ethernet { + mode hybrid + encap-type dot1q + lldp { + dest-mac nearest-bridge { + notification true + receive true + transmit true + tx-tlvs { + #port-desc true + sys-name true + #sys-desc true + sys-cap true + } + tx-mgmt-address system { + admin-state enable + } + } + } + } + } +# port "<.*c[0-9]+>" { +# connector { +# breakout c1-10g +# } +# } + } +} +/configure groups { + group "basebgp" { + router "Base" { + bgp { + group "<.*>" { + admin-state enable + min-route-advertisement 5 + type internal + } + neighbor "<.*>" { + admin-state enable + } + } + } + } +} diff --git a/templates/vr-sros/base-link.tmpl b/templates/vr-sros/base-link.tmpl index 4ab5d7d74..60a702db8 100644 --- a/templates/vr-sros/base-link.tmpl +++ b/templates/vr-sros/base-link.tmpl @@ -3,6 +3,7 @@ {{ expect .ip "ip" }} {{ optional .vlan "0-4096" }} {{ optional .isis_iid "0-32" }} +{{ optional .sid_idx "0-999" }} {{ optional .metric "1-10000" }} {{- if contains "/c" .port }} @@ -22,22 +23,19 @@ area-address 49.0000.0000.0000.0{{ default 0 .isis_iid }} level-capability 2 level 2 wide-metrics-only - #database-export igp-identifier {{ default 0 .isis_iid }} bgp-ls-identifier value {{ default 0 .isis_iid }} traffic-engineering advertise-router-capability area admin-state enable interface "system" admin-state enable {{- if .sid_idx }} - interface "system" ipv4-node-sid index {{ .sid_idx }} + interface "system" ipv4-node-sid index {{ default 0 .isis_iid }}0{{ .sid_idx }} segment-routing prefix-sid-range global segment-routing admin-state enable {{- end }} interface {{ .name }} admin-state enable interface {{ .name }} interface-type point-to-point - {{- if .metric }} - interface {{ .name }} level 2 metric {{ .metric }} - {{- end }} + interface {{ .name }} level 2 metric {{ default 10 .metric }} /configure router isis {{ default 0 .isis_iid }} diff --git a/templates/vr-sros/base-node.tmpl b/templates/vr-sros/base-node.tmpl index feb75118b..fc0698cdf 100644 --- a/templates/vr-sros/base-node.tmpl +++ b/templates/vr-sros/base-node.tmpl @@ -1,6 +1,6 @@ {{ expect .systemip "ip" }} {{ optional .isis_iid "0-31" }} -{{ optional .sid_idx "1-10000" }} +{{ optional .sid_idx "0-999" }} {{ optional .sid_start "19000-30000" }} {{ optional .sid_end "19000-30000" }} @@ -17,6 +17,23 @@ mpls-labels sr-labels start {{ default 19000 .sid_start }} end {{ default 30000 .sid_end }} {{- end }} +{{ if .isis_iid }} +/configure router isis {{ .isis_iid }} + area-address 49.0000.0000.0000.0{{ .isis_iid }} + level-capability 2 + level 2 wide-metrics-only + #database-export igp-identifier {{ .isis_iid }} bgp-ls-identifier value {{ default 0 .isis_iid }} + traffic-engineering + advertise-router-capability area + admin-state enable + interface "system" admin-state enable + {{- if .sid_idx }} + interface "system" ipv4-node-sid index {{ .isis_iid }}0{{ .sid_idx }} + segment-routing prefix-sid-range global + segment-routing admin-state enable + {{- end }} +{{ end }} + /configure router rsvp admin-state enable interface system admin-state enable From a63c976823828e686959db848d1a70aa8fdfe729 Mon Sep 17 00:00:00 2001 From: "Johann Kellerman kellerza@gmail.com" Date: Tue, 8 Jun 2021 20:38:18 +0200 Subject: [PATCH 17/33] templates --- go.mod | 2 +- go.sum | 7 +- templates/config/base-srl.tmpl | 75 +++++++++++++ .../show-route-table-srl.tmpl} | 0 .../show-route-table-vr-sros.tmpl} | 0 templates/srl/base-link.tmpl | 33 ------ templates/srl/base-node.tmpl | 37 ------ templates/srl/show-route-table-link.tmpl | 0 templates/vr-sros/base-link.tmpl | 47 -------- templates/vr-sros/base-node.tmpl | 105 ------------------ templates/vr-sros/show-route-table-link.tmpl | 0 11 files changed, 80 insertions(+), 226 deletions(-) create mode 100644 templates/config/base-srl.tmpl rename templates/{srl/show-route-table-node.tmpl => config/show-route-table-srl.tmpl} (100%) rename templates/{vr-sros/show-route-table-node.tmpl => config/show-route-table-vr-sros.tmpl} (100%) delete mode 100644 templates/srl/base-link.tmpl delete mode 100644 templates/srl/base-node.tmpl delete mode 100644 templates/srl/show-route-table-link.tmpl delete mode 100644 templates/vr-sros/base-link.tmpl delete mode 100644 templates/vr-sros/base-node.tmpl delete mode 100644 templates/vr-sros/show-route-table-link.tmpl diff --git a/go.mod b/go.mod index 0f0164d03..b2575e560 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/google/uuid v1.2.0 github.com/hashicorp/go-version v1.2.1 github.com/jsimonetti/rtnetlink v0.0.0-20210226120601-1b79e63a70a0 - github.com/kellerza/template v0.0.1 + github.com/kellerza/template v0.0.3 github.com/mitchellh/go-homedir v1.1.0 github.com/morikuni/aec v1.0.0 // indirect github.com/olekukonko/tablewriter v0.0.5-0.20201029120751-42e21c7531a3 diff --git a/go.sum b/go.sum index 4fba918f3..524b35ed7 100644 --- a/go.sum +++ b/go.sum @@ -413,8 +413,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kellerza/template v0.0.1 h1:4myoezGQD/BDIu+IOEGej60mcjz7LTjaJ2M6H7REIcg= -github.com/kellerza/template v0.0.1/go.mod h1:DRgSShodacWyLf9h+LWtlxxvTfWZW/AQlHKX2kb8g+4= +github.com/kellerza/template v0.0.3 h1:JWRnuMZnTHO1onmSj3HYQA/CJqn4x+CiKFD+bbD0SNE= +github.com/kellerza/template v0.0.3/go.mod h1:Og3Jdssypk1J/bNpWIrmymW5FOWI88vsY5Qx/e57V7M= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -851,8 +851,9 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= diff --git a/templates/config/base-srl.tmpl b/templates/config/base-srl.tmpl new file mode 100644 index 000000000..af71fdd5b --- /dev/null +++ b/templates/config/base-srl.tmpl @@ -0,0 +1,75 @@ +{{ expect .systemip "ip" }} +{{ optional .isis_iid "0-31" }} +{{ range .links }} + {{ expect .port "^(ethernet-\\d+/|e\\d+-)\\d+$" }} + {{ expect .name "string" }} + {{ expect .ip "ip" }} + {{ optional .vlan "0-4095" }} + {{ optional .metric "1-10000" }} +{{ end }} + +/interface lo0 { + admin-state enable + subinterface 0 { + ipv4 { + address {{ .systemip }} { + } + } + ipv6 { + address ::ffff:{{ ip .systemip }}/128 { + } + } + } +} + +/network-instance default { + router-id {{ ip .systemip }} + interface lo0.0 { + } + protocols { + isis { + instance default { + admin-state enable + level-capability L2 + set level 2 metric-style wide + # net should not be multiline (net [), becasue of the SRL ... prompt + net [ 49.0000.0000.0000.0{{ default 0 .isis_iid }} ] + interface lo0.0 { + } + } + } + } +} + + /system lldp admin-state enable + + +{{ .range links }} +/interface {{ .port }} { + admin-state enable + vlan-tagging true + subinterface {{ default 10 .vlan }} { + set vlan encap single-tagged vlan-id {{ default 10 .vlan }} + set ipv4 address {{ .ip }} + set ipv6 address ::FFFF:{{ ip .ip }}/127 + } +} + +/network-instance default { + interface {{ .port }}.{{ default 10 .vlan }} { + } + protocols { + isis { + instance default { + interface {{ .port }}.{{ default 10 .vlan }} { + circuit-type point-to-point + level 2 { + metric {{ default 10 .metric }} + } + } + } + } + } +} + +{{ end }} \ No newline at end of file diff --git a/templates/srl/show-route-table-node.tmpl b/templates/config/show-route-table-srl.tmpl similarity index 100% rename from templates/srl/show-route-table-node.tmpl rename to templates/config/show-route-table-srl.tmpl diff --git a/templates/vr-sros/show-route-table-node.tmpl b/templates/config/show-route-table-vr-sros.tmpl similarity index 100% rename from templates/vr-sros/show-route-table-node.tmpl rename to templates/config/show-route-table-vr-sros.tmpl diff --git a/templates/srl/base-link.tmpl b/templates/srl/base-link.tmpl deleted file mode 100644 index 218248cd1..000000000 --- a/templates/srl/base-link.tmpl +++ /dev/null @@ -1,33 +0,0 @@ -{{ expect .port "^(ethernet-\\d+/|e\\d+-)\\d+$" }} -{{ expect .name "string" }} -{{ expect .ip "ip" }} -{{ optional .vlan "0-4095" }} -{{ optional .metric "1-10000" }} - - -/interface {{ .port }} { - admin-state enable - vlan-tagging true - subinterface {{ default 10 .vlan }} { - set vlan encap single-tagged vlan-id {{ default 10 .vlan }} - set ipv4 address {{ .ip }} - set ipv6 address ::FFFF:{{ ip .ip }}/127 - } -} - -/network-instance default { - interface {{ .port }}.{{ default 10 .vlan }} { - } - protocols { - isis { - instance default { - interface {{ .port }}.{{ default 10 .vlan }} { - circuit-type point-to-point - level 2 { - metric {{ default 10 .metric }} - } - } - } - } - } -} diff --git a/templates/srl/base-node.tmpl b/templates/srl/base-node.tmpl deleted file mode 100644 index 656907f9c..000000000 --- a/templates/srl/base-node.tmpl +++ /dev/null @@ -1,37 +0,0 @@ -{{ expect .systemip "ip" }} -{{ optional .isis_iid "0-31" }} - -/interface lo0 { - admin-state enable - subinterface 0 { - ipv4 { - address {{ .systemip }} { - } - } - ipv6 { - address ::ffff:{{ ip .systemip }}/128 { - } - } - } -} - -/network-instance default { - router-id {{ ip .systemip }} - interface lo0.0 { - } - protocols { - isis { - instance default { - admin-state enable - level-capability L2 - set level 2 metric-style wide - # net should not be multiline (net [), becasue of the SRL ... prompt - net [ 49.0000.0000.0000.0{{ default 0 .isis_iid }} ] - interface lo0.0 { - } - } - } - } -} - - /system lldp admin-state enable diff --git a/templates/srl/show-route-table-link.tmpl b/templates/srl/show-route-table-link.tmpl deleted file mode 100644 index e69de29bb..000000000 diff --git a/templates/vr-sros/base-link.tmpl b/templates/vr-sros/base-link.tmpl deleted file mode 100644 index 60a702db8..000000000 --- a/templates/vr-sros/base-link.tmpl +++ /dev/null @@ -1,47 +0,0 @@ -{{ expect .port "^\\d+/\\d/" }} -{{ expect .name "string" }} -{{ expect .ip "ip" }} -{{ optional .vlan "0-4096" }} -{{ optional .isis_iid "0-32" }} -{{ optional .sid_idx "0-999" }} -{{ optional .metric "1-10000" }} - -{{- if contains "/c" .port }} -/configure port {{ slice 0 -2 .port }} admin-state enable -/configure port {{ .port | slice 0 -2 }} connector breakout c1-10g -{{- end }} - -/configure port {{ .port }} admin-state enable - -/configure router interface {{ .name }} - ipv4 primary address {{ ip .ip }} - ipv4 primary prefix-length {{ ipmask .ip }} - port {{ .port }}:{{ default 10 .vlan }} - - -/configure router isis {{ default 0 .isis_iid }} - area-address 49.0000.0000.0000.0{{ default 0 .isis_iid }} - level-capability 2 - level 2 wide-metrics-only - traffic-engineering - advertise-router-capability area - admin-state enable - interface "system" admin-state enable - {{- if .sid_idx }} - interface "system" ipv4-node-sid index {{ default 0 .isis_iid }}0{{ .sid_idx }} - segment-routing prefix-sid-range global - segment-routing admin-state enable - {{- end }} - - interface {{ .name }} admin-state enable - interface {{ .name }} interface-type point-to-point - interface {{ .name }} level 2 metric {{ default 10 .metric }} - - -/configure router isis {{ default 0 .isis_iid }} - -/configure router rsvp - interface {{ .name }} admin-state enable - -/configure router mpls - interface {{ .name }} admin-state enable diff --git a/templates/vr-sros/base-node.tmpl b/templates/vr-sros/base-node.tmpl deleted file mode 100644 index fc0698cdf..000000000 --- a/templates/vr-sros/base-node.tmpl +++ /dev/null @@ -1,105 +0,0 @@ -{{ expect .systemip "ip" }} -{{ optional .isis_iid "0-31" }} -{{ optional .sid_idx "0-999" }} -{{ optional .sid_start "19000-30000" }} -{{ optional .sid_end "19000-30000" }} - -/configure system login-control idle-timeout 1440 - -/configure router interface "system" - ipv4 primary address {{ ip .systemip }} - ipv4 primary prefix-length {{ ipmask .systemip }} - admin-state enable - -/configure router - autonomous-system {{ default 64500 .as_number }} - {{- if .sid_idx }} - mpls-labels sr-labels start {{ default 19000 .sid_start }} end {{ default 30000 .sid_end }} - {{- end }} - -{{ if .isis_iid }} -/configure router isis {{ .isis_iid }} - area-address 49.0000.0000.0000.0{{ .isis_iid }} - level-capability 2 - level 2 wide-metrics-only - #database-export igp-identifier {{ .isis_iid }} bgp-ls-identifier value {{ default 0 .isis_iid }} - traffic-engineering - advertise-router-capability area - admin-state enable - interface "system" admin-state enable - {{- if .sid_idx }} - interface "system" ipv4-node-sid index {{ .isis_iid }}0{{ .sid_idx }} - segment-routing prefix-sid-range global - segment-routing admin-state enable - {{- end }} -{{ end }} - -/configure router rsvp - admin-state enable - interface system admin-state enable - -/configure router mpls - cspf-on-loose-hop - interface system admin-state enable - admin-state enable - pce-report rsvp-te true - pce-report sr-te true - -/configure apply-groups ["baseport"] -/configure router bgp apply-groups ["basebgp"] - -/configure groups { - group "baseport" { - port "<.*\/[0-9]+>" { - # wanted to add this, but you really need the /1 context to exist - # admin-state enable - ethernet { - mode hybrid - encap-type dot1q - lldp { - dest-mac nearest-bridge { - notification true - receive true - transmit true - tx-tlvs { - #port-desc true - sys-name true - #sys-desc true - sys-cap true - } - tx-mgmt-address system { - admin-state enable - } - } - } - } - } -# port "<.*c[0-9]+>" { -# connector { -# breakout c1-10g -# } -# } - } -} -/configure groups { - group "basebgp" { - router "Base" { - bgp { - group "<.*>" { - admin-state enable - type internal - family { - vpn-ipv4 true - ipv4 true - vpn-ipv6 true - ipv6 true - } - } - neighbor "<.*>" { - admin-state enable - group "ibgp" - } - } - } - } -} diff --git a/templates/vr-sros/show-route-table-link.tmpl b/templates/vr-sros/show-route-table-link.tmpl deleted file mode 100644 index e69de29bb..000000000 From 50e974cbf6182a487b5083d37899e2dc69f3fbc5 Mon Sep 17 00:00:00 2001 From: "Johann Kellerman kellerza@gmail.com" Date: Tue, 8 Jun 2021 21:06:07 +0200 Subject: [PATCH 18/33] 0.0.3 options --- clab/config/template.go | 12 +++++++++--- clab/config/transport/ssh.go | 4 ++-- cmd/config.go | 5 +---- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/clab/config/template.go b/clab/config/template.go index e0b2811d5..b894cb811 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -14,6 +14,9 @@ import ( // templates to execute var TemplateNames []string +// path to additional templates +var TemplatePath string + type NodeConfig struct { TargetNode *types.Node // All the variables used to render the template @@ -23,11 +26,14 @@ type NodeConfig struct { Info []string } -var Tmpl *template.Template = template.New("") - func RenderAll(nodes map[string]*types.Node, links map[int]*types.Link) (map[string]*NodeConfig, error) { res := make(map[string]*NodeConfig) + tmpl, err := template.New("", template.SearchPath(TemplatePath)) + if err != nil { + return nil, err + } + for nodeName, vars := range PrepareVars(nodes, links) { res[nodeName] = &NodeConfig{ TargetNode: nodes[nodeName], @@ -36,7 +42,7 @@ func RenderAll(nodes map[string]*types.Node, links map[int]*types.Link) (map[str for _, baseN := range TemplateNames { tmplN := fmt.Sprintf("%s-%s.tmpl", baseN, vars["roles"]) - data1, err := Tmpl.ExecuteTemplate(tmplN, vars) + data1, err := tmpl.ExecuteTemplate(tmplN, vars) if err != nil { log.Errorf("could not render template %s: %s", tmplN, err) //log.Errorf("could not render template %s: %s vars=%s\n", c.String(), err, varsP) diff --git a/clab/config/transport/ssh.go b/clab/config/transport/ssh.go index 33ef8fbd4..4b5241ce8 100644 --- a/clab/config/transport/ssh.go +++ b/clab/config/transport/ssh.go @@ -195,8 +195,8 @@ func (t *SSHTransport) Run(command string, timeout int) *SSHReply { // Write a config snippet (a set of commands) // Session NEEDS to be configurable for other kinds // Part of the Transport interface -func (t *SSHTransport) Write(data *string, info *string) error { - if len(*data) == 0 { +func (t *SSHTransport) Write(data, info *string) error { + if *data == "" { return nil } diff --git a/cmd/config.go b/cmd/config.go index 64f2fe75c..4cbd8546f 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -11,9 +11,6 @@ import ( "github.com/srl-labs/containerlab/clab/config/transport" ) -// path to additional templates -var templatePath string - // Only print config locally, dont send to the node var printLines int @@ -121,7 +118,7 @@ var configCmd = &cobra.Command{ func init() { rootCmd.AddCommand(configCmd) - configCmd.Flags().StringVarP(&templatePath, "template-path", "p", "", "directory with templates used to render config") + configCmd.Flags().StringVarP(&config.TemplatePath, "template-path", "p", "", "directory with templates used to render config") configCmd.MarkFlagDirname("template-path") configCmd.Flags().StringSliceVarP(&config.TemplateNames, "template-list", "l", []string{}, "comma separated list of template names to render") configCmd.Flags().IntVarP(&printLines, "check", "c", 0, "render dry-run & print n lines of config") From c5f771dbf4ee5a5d710248578afc4fb2929ac074 Mon Sep 17 00:00:00 2001 From: "Johann Kellerman kellerza@gmail.com" Date: Wed, 9 Jun 2021 14:31:58 +0200 Subject: [PATCH 19/33] templateok --- clab/config/template.go | 18 ++++++++++-------- clab/config/transport/ssh.go | 8 ++++---- clab/config/transport/transport.go | 6 ++++-- clab/config/utils.go | 27 +++++++++++++-------------- cmd/config.go | 15 +-------------- go.mod | 2 +- go.sum | 4 ++-- templates/config/base-vr-sros.tmpl | 7 ------- 8 files changed, 35 insertions(+), 52 deletions(-) diff --git a/clab/config/template.go b/clab/config/template.go index b894cb811..17f153d25 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -41,13 +41,12 @@ func RenderAll(nodes map[string]*types.Node, links map[int]*types.Link) (map[str } for _, baseN := range TemplateNames { - tmplN := fmt.Sprintf("%s-%s.tmpl", baseN, vars["roles"]) + tmplN := fmt.Sprintf("%s-%s.tmpl", baseN, vars["role"]) data1, err := tmpl.ExecuteTemplate(tmplN, vars) if err != nil { - log.Errorf("could not render template %s: %s", tmplN, err) - //log.Errorf("could not render template %s: %s vars=%s\n", c.String(), err, varsP) - return nil, fmt.Errorf("could not render template %s: %s", tmplN, err) + return nil, err } + data1 = strings.ReplaceAll(strings.Trim(data1, "\n \t"), "\n\n\n", "\n\n") res[nodeName].Data = append(res[nodeName].Data, data1) res[nodeName].Info = append(res[nodeName].Info, tmplN) } @@ -73,22 +72,25 @@ func (c *NodeConfig) Print(printLines int) { s.WriteString(c.TargetNode.ShortName) if log.IsLevelEnabled(log.DebugLevel) { - vars, _ := json.MarshalIndent(c.Vars, "", " ") - s.Write(vars) + s.WriteString(" vars = ") + vars, _ := json.MarshalIndent(c.Vars, "", " ") + s.Write(vars[0 : len(vars)-1]) + s.WriteString(" }") } if printLines > 0 { for idx, conf := range c.Data { - fmt.Fprintf(&s, "%s", c.Info[idx]) + fmt.Fprintf(&s, "\n Template %s for %s = [[", c.Info[idx], c.TargetNode.ShortName) cl := strings.SplitN(conf, "\n", printLines+1) if len(cl) > printLines { cl[printLines] = "..." } for _, l := range cl { - s.WriteString("\n ") + s.WriteString("\n ") s.WriteString(l) } + s.WriteString("\n ]]") } } diff --git a/clab/config/transport/ssh.go b/clab/config/transport/ssh.go index 4b5241ce8..f46525fc1 100644 --- a/clab/config/transport/ssh.go +++ b/clab/config/transport/ssh.go @@ -18,7 +18,7 @@ type SSHSession struct { Session *ssh.Session } -type SSHOption func(*SSHTransport) error +type SSHTransportOption func(*SSHTransport) error // The reply the execute command and the prompt. type SSHReply struct{ result, prompt, command string } @@ -51,7 +51,7 @@ type SSHTransport struct { K SSHKind } -func NewSSHTransport(node *types.Node, options ...SSHOption) (*SSHTransport, error) { +func NewSSHTransport(node *types.Node, options ...SSHTransportOption) (*SSHTransport, error) { switch node.Kind { case "vr-sros", "srl": c := &SSHTransport{} @@ -236,7 +236,7 @@ func (t *SSHTransport) Write(data, info *string) error { // Connect to a host // Part of the Transport interface -func (t *SSHTransport) Connect(host string, options ...func(*Transport)) error { +func (t *SSHTransport) Connect(host string, options ...TransportOption) error { // Assign Default Values if t.PromptChar == "" { t.PromptChar = "#" @@ -279,7 +279,7 @@ func (t *SSHTransport) Close() { // Add a basic username & password to a config. // Will initilize the config if required -func WithUserNamePassword(username, password string) SSHOption { +func WithUserNamePassword(username, password string) SSHTransportOption { return func(tx *SSHTransport) error { if tx.SSHConfig == nil { tx.SSHConfig = &ssh.ClientConfig{} diff --git a/clab/config/transport/transport.go b/clab/config/transport/transport.go index c08db71a4..5863803b5 100644 --- a/clab/config/transport/transport.go +++ b/clab/config/transport/transport.go @@ -7,15 +7,17 @@ import ( // Debug count var DebugCount int +type TransportOption func(*Transport) + type Transport interface { // Connect to the target host - Connect(host string, options ...func(*Transport)) error + Connect(host string, options ...TransportOption) error // Execute some config Write(data *string, info *string) error Close() } -func Write(tx Transport, host string, data, info []string, options ...func(*Transport)) error { +func Write(tx Transport, host string, data, info []string, options ...TransportOption) error { // the Kind should configure the transport parameters before err := tx.Connect(host, options...) diff --git a/clab/config/utils.go b/clab/config/utils.go index 2786518a2..cb85f7c42 100644 --- a/clab/config/utils.go +++ b/clab/config/utils.go @@ -26,11 +26,11 @@ func PrepareVars(nodes map[string]*types.Node, links map[int]*types.Link) map[st // Init array for this node res[name] = make(map[string]interface{}) nc := GetNodeConfigFromLabels(node.Labels) - for _, key := range nc.Vars { + for key := range nc.Vars { res[name][key] = nc.Vars[key] } // Create link array - res[name]["links"] = make([]interface{}, 2) + res[name]["links"] = []interface{}{} // Ensure role or Kind if _, ok := res[name]["role"]; !ok { res[name]["role"] = node.Kind @@ -61,8 +61,8 @@ func prepareLinkVars(lIdx int, link *types.Link, varsA, varsB map[string]interfa if len(v2) == 0 { varsB[key] = v1 } else { - varsA[key+"_far"] = v2[1] - varsB[key] = v2[1] + varsA[key+"_far"] = v2[0] + varsB[key] = v2[0] varsB[key+"_far"] = v1 } } @@ -88,18 +88,15 @@ func prepareLinkVars(lIdx int, link *types.Link, varsA, varsB map[string]interfa } } - //Repeat the following for varsA and varsB - for _, vars := range []map[string]interface{}{varsA, varsB} { - // Set default Link/Interface Names - if _, ok := vars["name"]; !ok { - var linkNr string - if v, ok := vars["linkNr"]; ok { - linkNr = fmt.Sprintf("_%v", v) - } - vars["name"] = []string{fmt.Sprintf("to_%s%s", link.B.Node.ShortName, linkNr), - fmt.Sprintf("to_%s%s", link.A.Node.ShortName, linkNr)} + if _, ok := varsA["name"]; !ok { + var linkNr string + if v, ok := varsA["linkNr"]; ok { + linkNr = fmt.Sprintf("_%v", v) } + addV("name", fmt.Sprintf("to_%s%s", link.B.Node.ShortName, linkNr), + fmt.Sprintf("to_%s%s", link.A.Node.ShortName, linkNr)) } + return nil } @@ -172,3 +169,5 @@ func ipFarEnd(in netaddr.IPPrefix) netaddr.IPPrefix { } return netaddr.IPPrefixFrom(n, in.Bits()) } + +// DictString diff --git a/cmd/config.go b/cmd/config.go index 4cbd8546f..883230be1 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "sync" log "github.com/sirupsen/logrus" @@ -35,9 +34,6 @@ var configCmd = &cobra.Command{ clab.WithTopoFile(topo), ) - //ctx, cancel := context.WithCancel(context.Background()) - //defer cancel() - setFlags(c.Config) log.Debugf("Topology definition: %+v", c.Config) // Parse topology information @@ -45,22 +41,13 @@ var configCmd = &cobra.Command{ return err } - // config map per node. each node gets a couple of config snippets []string + // config map per node. each node gets a config.NodeConfig allConfig, err := config.RenderAll(c.Nodes, c.Links) if err != nil { return err } - // render them all - - renderErr := 0 - - if renderErr > 0 { - return fmt.Errorf("%d render warnings", renderErr) - } - if printLines > 0 { - // Debug log all config to be deployed for _, c := range allConfig { c.Print(printLines) } diff --git a/go.mod b/go.mod index b2575e560..b771b6e87 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/google/uuid v1.2.0 github.com/hashicorp/go-version v1.2.1 github.com/jsimonetti/rtnetlink v0.0.0-20210226120601-1b79e63a70a0 - github.com/kellerza/template v0.0.3 + github.com/kellerza/template v0.0.4 github.com/mitchellh/go-homedir v1.1.0 github.com/morikuni/aec v1.0.0 // indirect github.com/olekukonko/tablewriter v0.0.5-0.20201029120751-42e21c7531a3 diff --git a/go.sum b/go.sum index 524b35ed7..a180ece27 100644 --- a/go.sum +++ b/go.sum @@ -413,8 +413,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kellerza/template v0.0.3 h1:JWRnuMZnTHO1onmSj3HYQA/CJqn4x+CiKFD+bbD0SNE= -github.com/kellerza/template v0.0.3/go.mod h1:Og3Jdssypk1J/bNpWIrmymW5FOWI88vsY5Qx/e57V7M= +github.com/kellerza/template v0.0.4 h1:z/qhD9D50bEhbTu/1JN6ANiTECws8rKQBKmLDdwK6hs= +github.com/kellerza/template v0.0.4/go.mod h1:Og3Jdssypk1J/bNpWIrmymW5FOWI88vsY5Qx/e57V7M= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= diff --git a/templates/config/base-vr-sros.tmpl b/templates/config/base-vr-sros.tmpl index 0e536801b..759cae7d8 100644 --- a/templates/config/base-vr-sros.tmpl +++ b/templates/config/base-vr-sros.tmpl @@ -93,8 +93,6 @@ {{ end }} - - /configure apply-groups ["baseport"] /configure router bgp apply-groups ["basebgp"] @@ -124,11 +122,6 @@ } } } -# port "<.*c[0-9]+>" { -# connector { -# breakout c1-10g -# } -# } } } /configure groups { From 859313848d76f6b5e1f9a8a9ebe441c4f2d4af7c Mon Sep 17 00:00:00 2001 From: "Johann Kellerman kellerza@gmail.com" Date: Wed, 9 Jun 2021 17:17:40 +0200 Subject: [PATCH 20/33] ssh&examples --- .pre-commit-config.yaml | 5 ++ clab/config/transport/ssh.go | 55 +++++++++++++------ cmd/config.go | 1 + .../vr05/{conf1.clab.yml => sros4.clab.yml} | 4 ++ lab-examples/vr05/vr01.clab.yml | 28 ++++++++++ templates/config/base-srl.tmpl | 2 +- 6 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 .pre-commit-config.yaml rename lab-examples/vr05/{conf1.clab.yml => sros4.clab.yml} (91%) create mode 100644 lab-examples/vr05/vr01.clab.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..ee38b8751 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/codespell-project/codespell + rev: v2.0.0 + hooks: + - id: codespell diff --git a/clab/config/transport/ssh.go b/clab/config/transport/ssh.go index f46525fc1..a9d0f4595 100644 --- a/clab/config/transport/ssh.go +++ b/clab/config/transport/ssh.go @@ -3,6 +3,7 @@ package transport import ( "fmt" "io" + "net" "runtime" "strings" "time" @@ -51,6 +52,38 @@ type SSHTransport struct { K SSHKind } +// Add username & password authentication +func WithUserNamePassword(username, password string) SSHTransportOption { + return func(tx *SSHTransport) error { + tx.SSHConfig.User = username + if tx.SSHConfig.Auth == nil { + tx.SSHConfig.Auth = []ssh.AuthMethod{} + } + tx.SSHConfig.Auth = append(tx.SSHConfig.Auth, ssh.Password(password)) + return nil + } +} + +// Add a basic username & password to a config. +// Will initilize the config if required +func HostKeyCallback(callback ...ssh.HostKeyCallback) SSHTransportOption { + return func(tx *SSHTransport) error { + tx.SSHConfig.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error { + if len(callback) == 0 { + log.Warnf("Skipping host key verification for %s", hostname) + return nil + } + for _, hkc := range callback { + if hkc(hostname, remote, key) == nil { + return nil + } + } + return fmt.Errorf("invalid host key %s: %s", hostname, key) + } + return nil + } +} + func NewSSHTransport(node *types.Node, options ...SSHTransportOption) (*SSHTransport, error) { switch node.Kind { case "vr-sros", "srl": @@ -59,7 +92,10 @@ func NewSSHTransport(node *types.Node, options ...SSHTransportOption) (*SSHTrans // apply options for _, opt := range options { - opt(c) + err := opt(c) + if err != nil { + return nil, err + } } switch node.Kind { @@ -277,23 +313,6 @@ func (t *SSHTransport) Close() { t.ses.Close() } -// Add a basic username & password to a config. -// Will initilize the config if required -func WithUserNamePassword(username, password string) SSHTransportOption { - return func(tx *SSHTransport) error { - if tx.SSHConfig == nil { - tx.SSHConfig = &ssh.ClientConfig{} - } - tx.SSHConfig.User = username - if tx.SSHConfig.Auth == nil { - tx.SSHConfig.Auth = []ssh.AuthMethod{} - } - tx.SSHConfig.Auth = append(tx.SSHConfig.Auth, ssh.Password(password)) - tx.SSHConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey() - return nil - } -} - // Create a new SSH session (Dial, open in/out pipes and start the shell) // pass the authntication details in sshConfig func NewSSHSession(host string, sshConfig *ssh.ClientConfig) (*SSHSession, error) { diff --git a/cmd/config.go b/cmd/config.go index 883230be1..af6f01bdd 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -73,6 +73,7 @@ var configCmd = &cobra.Command{ transport.WithUserNamePassword( clab.DefaultCredentials[cs.TargetNode.Kind][0], clab.DefaultCredentials[cs.TargetNode.Kind][1]), + transport.HostKeyCallback(), ) if err != nil { log.Errorf("%s: %s", kind, err) diff --git a/lab-examples/vr05/conf1.clab.yml b/lab-examples/vr05/sros4.clab.yml similarity index 91% rename from lab-examples/vr05/conf1.clab.yml rename to lab-examples/vr05/sros4.clab.yml index d116e2602..91ef81431 100644 --- a/lab-examples/vr05/conf1.clab.yml +++ b/lab-examples/vr05/sros4.clab.yml @@ -30,13 +30,17 @@ topology: port: 1/1/c1/1, 1/1/c2/1 ip: 1.1.1.2/30 vlan: "99,99" + isis_iid: 0 - endpoints: [sr2:eth1, sr3:eth2] labels: port: 1/1/c1/1, 1/1/c2/1 vlan: 98 + isis_iid: 0 - endpoints: [sr3:eth1, sr4:eth2] labels: port: 1/1/c1/1, 1/1/c2/1 + isis_iid: 0 - endpoints: [sr4:eth1, sr1:eth2] labels: port: 1/1/c1/1, 1/1/c2/1 + isis_iid: 0 diff --git a/lab-examples/vr05/vr01.clab.yml b/lab-examples/vr05/vr01.clab.yml new file mode 100644 index 000000000..5b5f05182 --- /dev/null +++ b/lab-examples/vr05/vr01.clab.yml @@ -0,0 +1,28 @@ +name: vr01 + +topology: + nodes: + srl: + kind: srl + image: registry.srlinux.dev/pub/srlinux:21.3.1-410 + license: /home/kellerza/license/srl21.key + labels: + systemip: 10.0.50.50/32 + isis_iid: 0 + sid_idx: 11 + sros: + kind: vr-sros + image: registry.srlinux.dev/pub/vr-sros:21.2.R1 + type: sr-1 + license: /home/kellerza/license/license-sros21.txt + labels: + systemip: 10.0.50.51/32 + sid_idx: 10 + isis_iid: 0 + + links: + - endpoints: ["srl:e1-1", "sros:eth1"] + labels: + port: ethernet-1/1, 1/1/c1/1 + vlan: 10 + isis_iid: 0 diff --git a/templates/config/base-srl.tmpl b/templates/config/base-srl.tmpl index af71fdd5b..a54eeab5b 100644 --- a/templates/config/base-srl.tmpl +++ b/templates/config/base-srl.tmpl @@ -44,7 +44,7 @@ /system lldp admin-state enable -{{ .range links }} +{{ range .links }} /interface {{ .port }} { admin-state enable vlan-tagging true From 9110c1c4396a413bcbdc6540616c7ed21766b542 Mon Sep 17 00:00:00 2001 From: "Johann Kellerman kellerza@gmail.com" Date: Wed, 9 Jun 2021 19:45:35 +0200 Subject: [PATCH 21/33] comments --- .pre-commit-config.yaml | 8 ++++++++ clab/config/helpers.go | 6 ++++-- clab/config/template.go | 4 ---- clab/config/transport/ssh.go | 14 +++++++------- clab/config/transport/sshkind.go | 11 ++++++----- clab/config/transport/transport.go | 1 + clab/config/utils.go | 8 ++++---- clab/config/utils_test.go | 1 - cmd/config.go | 4 ++-- 9 files changed, 32 insertions(+), 25 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ee38b8751..dbb6547be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,3 +3,11 @@ repos: rev: v2.0.0 hooks: - id: codespell + - repo: https://github.com/syntaqx/git-hooks + rev: v0.0.17 + hooks: + - id: forbid-binary + - id: go-fmt + - id: go-test + - id: go-mod-tidy + - id: shfmt diff --git a/clab/config/helpers.go b/clab/config/helpers.go index c4c1d5bbc..318b346a8 100644 --- a/clab/config/helpers.go +++ b/clab/config/helpers.go @@ -4,7 +4,7 @@ import ( "strings" ) -// Split a string on commans and trim each +// Split a string on commas and trim each line func SplitTrim(s string) []string { res := strings.Split(s, ",") for i, v := range res { @@ -13,13 +13,15 @@ func SplitTrim(s string) []string { return res } -// the new agreed node config +// The new agreed node config type NodeSettings struct { Vars map[string]string Transport string Templates []string } +// Temporary function to extract NodeSettings from the Labels +// In the next phase node settings will be added to the clab file func GetNodeConfigFromLabels(labels map[string]string) NodeSettings { nc := NodeSettings{ Vars: labels, diff --git a/clab/config/template.go b/clab/config/template.go index 17f153d25..53fb95a3f 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -58,10 +58,6 @@ func RenderAll(nodes map[string]*types.Node, links map[int]*types.Link) (map[str func (c *NodeConfig) String() string { s := fmt.Sprintf("%s: %v", c.TargetNode.ShortName, c.Info) - // s := fmt.Sprintf("%s %s using %s/%s", c.TargetNode.ShortName, c.source, c.TargetNode.Kind, c.templateName) - // if c.Data != nil { - // s += fmt.Sprintf(" (%d lines)", bytes.Count(c.Data, []byte("\n"))+1) - // } return s } diff --git a/clab/config/transport/ssh.go b/clab/config/transport/ssh.go index a9d0f4595..4a46f683f 100644 --- a/clab/config/transport/ssh.go +++ b/clab/config/transport/ssh.go @@ -21,20 +21,20 @@ type SSHSession struct { type SSHTransportOption func(*SSHTransport) error -// The reply the execute command and the prompt. +// The SSH reply, executed command and the prompt type SSHReply struct{ result, prompt, command string } // SSHTransport setting needs to be set before calling Connect() // SSHTransport implements the Transport interface type SSHTransport struct { - // Channel used to read. Can use Expect to Write & read wit timeout + // Channel used to read. Can use Expect to Write & read with timeout in chan SSHReply // SSH Session ses *SSHSession // Contains the first read after connecting LoginMessage *SSHReply // SSH parameters used in connect - // defualt: 22 + // default: 22 Port int // Keep the target for logging @@ -64,8 +64,8 @@ func WithUserNamePassword(username, password string) SSHTransportOption { } } -// Add a basic username & password to a config. -// Will initilize the config if required +// Add a basic username & password to a config +// Will initialize the config if required func HostKeyCallback(callback ...ssh.HostKeyCallback) SSHTransportOption { return func(tx *SSHTransport) error { tx.SSHConfig.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error { @@ -106,7 +106,7 @@ func NewSSHTransport(node *types.Node, options ...SSHTransportOption) (*SSHTrans } return c, nil } - return nil, fmt.Errorf("no tranport implemented for kind: %s", node.Kind) + return nil, fmt.Errorf("no transport implemented for kind: %s", node.Kind) } // Creates the channel reading the SSH connection @@ -374,7 +374,7 @@ func (ses *SSHSession) Close() { // The LogString will include the entire SSHReply // Each field will be prefixed by a character. // # - command sent -// | - result recieved +// | - result received // ? - prompt part of the result func (r *SSHReply) LogString(node string, linefeed, debug bool) string { ind := 12 + len(node) diff --git a/clab/config/transport/sshkind.go b/clab/config/transport/sshkind.go index 8ed5e9544..2e65090b3 100644 --- a/clab/config/transport/sshkind.go +++ b/clab/config/transport/sshkind.go @@ -7,17 +7,18 @@ import ( log "github.com/sirupsen/logrus" ) -// an interface to implement kind specific methods for transactions and prompt checking +// An interface to implement kind specific methods for transactions and prompt checking type SSHKind interface { // Start a config transaction ConfigStart(s *SSHTransport, transaction bool) error // Commit a config transaction ConfigCommit(s *SSHTransport) (*SSHReply, error) - // Prompt parsing function. + // Prompt parsing function + // // This function receives string, split by the delimiter and should ensure this is a valid prompt - // Valid prompt, strip te prompt from the result and add it to the prompt in SSHReply + // Valid prompt, strip the prompt from the result and add it to the prompt in SSHReply // - // A defualt implementation is promptParseNoSpaces, which simply ensures there are + // A default implementation is promptParseNoSpaces, which simply ensures there are // no spaces between the start of the line and the # PromptParse(s *SSHTransport, in *string) *SSHReply } @@ -88,7 +89,7 @@ func (sk *SrlSSHKind) PromptParse(s *SSHTransport, in *string) *SSHReply { return promptParseNoSpaces(in, s.PromptChar, 2) } -// This is a helper funciton to parse the prompt, and can be used by SSHKind's ParsePrompt +// This is a helper function to parse the prompt, and can be used by SSHKind's ParsePrompt // Used in SRL today func promptParseNoSpaces(in *string, promptChar string, lines int) *SSHReply { n := strings.LastIndex(*in, "\n") diff --git a/clab/config/transport/transport.go b/clab/config/transport/transport.go index 5863803b5..ad3f83db9 100644 --- a/clab/config/transport/transport.go +++ b/clab/config/transport/transport.go @@ -17,6 +17,7 @@ type Transport interface { Close() } +// Write config to a node func Write(tx Transport, host string, data, info []string, options ...TransportOption) error { // the Kind should configure the transport parameters before diff --git a/clab/config/utils.go b/clab/config/utils.go index cb85f7c42..40cfa83d6 100644 --- a/clab/config/utils.go +++ b/clab/config/utils.go @@ -16,6 +16,7 @@ const ( type Dict map[string]interface{} +// Prepare variables for all nodes. This will also prepare all variables for the links func PrepareVars(nodes map[string]*types.Node, links map[int]*types.Link) map[string]Dict { res := make(map[string]Dict) @@ -51,6 +52,7 @@ func PrepareVars(nodes map[string]*types.Node, links map[int]*types.Link) map[st return res } +// Prepare variables for a specific link func prepareLinkVars(lIdx int, link *types.Link, varsA, varsB map[string]interface{}) error { ncA := GetNodeConfigFromLabels(link.A.Node.Labels) ncB := GetNodeConfigFromLabels(link.B.Node.Labels) @@ -110,9 +112,7 @@ func linkIPfromSystemIP(link *types.Link) (netaddr.IPPrefix, netaddr.IPPrefix, e return ipA, ipA, fmt.Errorf("invalid ip %s", link.A.EndpointName) } } else { - // caluculate link IP from the system IPs - tbd - //var sysA, sysB netaddr.IPPrefix - + // Calculate link IP from the system IPs sysA, err := netaddr.ParseIPPrefix(link.A.Node.Labels[systemIP]) if err != nil { return ipA, ipA, fmt.Errorf("no 'ip' on link & the '%s' of %s: %s", systemIP, link.A.Node.ShortName, err) @@ -141,7 +141,7 @@ func ipLastOctet(in netaddr.IP) int { } res, err := strconv.Atoi(s[i+1:]) if err != nil { - log.Errorf("last octect %s from IP %s not a string", s[i+1:], s) + log.Errorf("last octet %s from IP %s not a string", s[i+1:], s) } return res } diff --git a/clab/config/utils_test.go b/clab/config/utils_test.go index 177a45642..bfd275c31 100644 --- a/clab/config/utils_test.go +++ b/clab/config/utils_test.go @@ -43,7 +43,6 @@ func TestIPLastOctect(t *testing.T) { "10.0.0.1/32": 1, "::1/32": 1, } - for k, v := range lst { n := netaddr.MustParseIPPrefix(k) lo := ipLastOctet(n.IP()) diff --git a/cmd/config.go b/cmd/config.go index af6f01bdd..43c23ad3d 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -41,7 +41,7 @@ var configCmd = &cobra.Command{ return err } - // config map per node. each node gets a config.NodeConfig + // Config map per node. Each node gets a config.NodeConfig allConfig, err := config.RenderAll(c.Nodes, c.Links) if err != nil { return err @@ -79,7 +79,7 @@ var configCmd = &cobra.Command{ log.Errorf("%s: %s", kind, err) } } else if ct == "grpc" { - // newGRPCTransport + // NewGRPCTransport } else { log.Errorf("Unknown transport: %s", ct) return From 16053d365afacd5f4eb2ded152b13ae2f048703b Mon Sep 17 00:00:00 2001 From: "Johann Kellerman kellerza@gmail.com" Date: Wed, 9 Jun 2021 20:25:39 +0200 Subject: [PATCH 22/33] paths --- clab/config/template.go | 11 +++++++++-- cmd/config.go | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/clab/config/template.go b/clab/config/template.go index 53fb95a3f..b576fdeb4 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -15,7 +15,7 @@ import ( var TemplateNames []string // path to additional templates -var TemplatePath string +var TemplatePaths []string type NodeConfig struct { TargetNode *types.Node @@ -29,7 +29,14 @@ type NodeConfig struct { func RenderAll(nodes map[string]*types.Node, links map[int]*types.Link) (map[string]*NodeConfig, error) { res := make(map[string]*NodeConfig) - tmpl, err := template.New("", template.SearchPath(TemplatePath)) + if len(TemplateNames) == 0 { + return nil, fmt.Errorf("please specify one of more templates with --template-list") + } + if len(TemplatePaths) == 0 { + return nil, fmt.Errorf("please specify one of more paths with --template-path") + } + + tmpl, err := template.New("", template.SearchPath(TemplatePaths...)) if err != nil { return nil, err } diff --git a/cmd/config.go b/cmd/config.go index 43c23ad3d..327bff3d1 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -106,7 +106,7 @@ var configCmd = &cobra.Command{ func init() { rootCmd.AddCommand(configCmd) - configCmd.Flags().StringVarP(&config.TemplatePath, "template-path", "p", "", "directory with templates used to render config") + configCmd.Flags().StringSliceVarP(&config.TemplatePaths, "template-path", "p", []string{}, "comma separated list of paths to search for templates") configCmd.MarkFlagDirname("template-path") configCmd.Flags().StringSliceVarP(&config.TemplateNames, "template-list", "l", []string{}, "comma separated list of template names to render") configCmd.Flags().IntVarP(&printLines, "check", "c", 0, "render dry-run & print n lines of config") From af5c89207c32042ad427d82e06a8bc5c2c180808 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Tue, 29 Jun 2021 13:32:10 +0200 Subject: [PATCH 23/33] fix type name --- clab/config/template.go | 4 ++-- clab/config/transport/ssh.go | 2 +- clab/config/utils.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/clab/config/template.go b/clab/config/template.go index b576fdeb4..1c5310978 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -18,7 +18,7 @@ var TemplateNames []string var TemplatePaths []string type NodeConfig struct { - TargetNode *types.Node + TargetNode *types.NodeConfig // All the variables used to render the template Vars map[string]interface{} // the Rendered templates @@ -26,7 +26,7 @@ type NodeConfig struct { Info []string } -func RenderAll(nodes map[string]*types.Node, links map[int]*types.Link) (map[string]*NodeConfig, error) { +func RenderAll(nodes map[string]*types.NodeConfig, links map[int]*types.Link) (map[string]*NodeConfig, error) { res := make(map[string]*NodeConfig) if len(TemplateNames) == 0 { diff --git a/clab/config/transport/ssh.go b/clab/config/transport/ssh.go index 4a46f683f..f6b102a80 100644 --- a/clab/config/transport/ssh.go +++ b/clab/config/transport/ssh.go @@ -84,7 +84,7 @@ func HostKeyCallback(callback ...ssh.HostKeyCallback) SSHTransportOption { } } -func NewSSHTransport(node *types.Node, options ...SSHTransportOption) (*SSHTransport, error) { +func NewSSHTransport(node *types.NodeConfig, options ...SSHTransportOption) (*SSHTransport, error) { switch node.Kind { case "vr-sros", "srl": c := &SSHTransport{} diff --git a/clab/config/utils.go b/clab/config/utils.go index 40cfa83d6..58a6f16fe 100644 --- a/clab/config/utils.go +++ b/clab/config/utils.go @@ -17,7 +17,7 @@ const ( type Dict map[string]interface{} // Prepare variables for all nodes. This will also prepare all variables for the links -func PrepareVars(nodes map[string]*types.Node, links map[int]*types.Link) map[string]Dict { +func PrepareVars(nodes map[string]*types.NodeConfig, links map[int]*types.Link) map[string]Dict { res := make(map[string]Dict) From ffb5ca5d97d341857184d69236d2929c496a9743 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Tue, 29 Jun 2021 13:47:18 +0200 Subject: [PATCH 24/33] fix default credentials new location --- cmd/config.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 327bff3d1..56e86a320 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -8,6 +8,7 @@ import ( "github.com/srl-labs/containerlab/clab" "github.com/srl-labs/containerlab/clab/config" "github.com/srl-labs/containerlab/clab/config/transport" + "github.com/srl-labs/containerlab/nodes" ) // Only print config locally, dont send to the node @@ -71,8 +72,8 @@ var configCmd = &cobra.Command{ tx, err = transport.NewSSHTransport( cs.TargetNode, transport.WithUserNamePassword( - clab.DefaultCredentials[cs.TargetNode.Kind][0], - clab.DefaultCredentials[cs.TargetNode.Kind][1]), + nodes.DefaultCredentials[cs.TargetNode.Kind][0], + nodes.DefaultCredentials[cs.TargetNode.Kind][1]), transport.HostKeyCallback(), ) if err != nil { From a3e121c44724ea71941bac86323168b83478b75d Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Tue, 29 Jun 2021 14:15:29 +0200 Subject: [PATCH 25/33] namespace custom template module --- clab/config/template.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clab/config/template.go b/clab/config/template.go index 1c5310978..a24aff3f0 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/kellerza/template" + jT "github.com/kellerza/template" log "github.com/sirupsen/logrus" "github.com/srl-labs/containerlab/types" @@ -36,7 +36,7 @@ func RenderAll(nodes map[string]*types.NodeConfig, links map[int]*types.Link) (m return nil, fmt.Errorf("please specify one of more paths with --template-path") } - tmpl, err := template.New("", template.SearchPath(TemplatePaths...)) + tmpl, err := jT.New("", jT.SearchPath(TemplatePaths...)) if err != nil { return nil, err } From dc2396d28c8b116250d14d77190059c5ffbbc227 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Tue, 29 Jun 2021 15:36:20 +0200 Subject: [PATCH 26/33] align node interface changes --- clab/config/template.go | 5 +++-- clab/config/utils.go | 10 ++++++---- cmd/config.go | 5 +---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/clab/config/template.go b/clab/config/template.go index a24aff3f0..08cd4e0e4 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -8,6 +8,7 @@ import ( jT "github.com/kellerza/template" log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/nodes" "github.com/srl-labs/containerlab/types" ) @@ -26,7 +27,7 @@ type NodeConfig struct { Info []string } -func RenderAll(nodes map[string]*types.NodeConfig, links map[int]*types.Link) (map[string]*NodeConfig, error) { +func RenderAll(nodes map[string]nodes.Node, links map[int]*types.Link) (map[string]*NodeConfig, error) { res := make(map[string]*NodeConfig) if len(TemplateNames) == 0 { @@ -43,7 +44,7 @@ func RenderAll(nodes map[string]*types.NodeConfig, links map[int]*types.Link) (m for nodeName, vars := range PrepareVars(nodes, links) { res[nodeName] = &NodeConfig{ - TargetNode: nodes[nodeName], + TargetNode: nodes[nodeName].Config(), Vars: vars, } diff --git a/clab/config/utils.go b/clab/config/utils.go index 58a6f16fe..acbb214e2 100644 --- a/clab/config/utils.go +++ b/clab/config/utils.go @@ -6,6 +6,7 @@ import ( "strings" log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/nodes" "github.com/srl-labs/containerlab/types" "inet.af/netaddr" ) @@ -17,16 +18,17 @@ const ( type Dict map[string]interface{} // Prepare variables for all nodes. This will also prepare all variables for the links -func PrepareVars(nodes map[string]*types.NodeConfig, links map[int]*types.Link) map[string]Dict { +func PrepareVars(nodes map[string]nodes.Node, links map[int]*types.Link) map[string]Dict { res := make(map[string]Dict) // preparing all nodes vars for _, node := range nodes { - name := node.ShortName + nodeCfg := node.Config() + name := nodeCfg.ShortName // Init array for this node res[name] = make(map[string]interface{}) - nc := GetNodeConfigFromLabels(node.Labels) + nc := GetNodeConfigFromLabels(nodeCfg.Labels) for key := range nc.Vars { res[name][key] = nc.Vars[key] } @@ -34,7 +36,7 @@ func PrepareVars(nodes map[string]*types.NodeConfig, links map[int]*types.Link) res[name]["links"] = []interface{}{} // Ensure role or Kind if _, ok := res[name]["role"]; !ok { - res[name]["role"] = node.Kind + res[name]["role"] = nodeCfg.Kind } } diff --git a/cmd/config.go b/cmd/config.go index 56e86a320..c75061507 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -23,9 +23,6 @@ var configCmd = &cobra.Command{ SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { var err error - if err = topoSet(); err != nil { - return err - } transport.DebugCount = debugCount @@ -35,7 +32,7 @@ var configCmd = &cobra.Command{ clab.WithTopoFile(topo), ) - setFlags(c.Config) + // setFlags(c.Config) log.Debugf("Topology definition: %+v", c.Config) // Parse topology information if err = c.ParseTopology(); err != nil { From b5011a459d99ef3568f99a1b2ed3506bc0394c39 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Tue, 29 Jun 2021 22:06:07 +0200 Subject: [PATCH 27/33] added private dir to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ac94a04bc..9a180986e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ bin/* rpm rpm/* dist +private # Test binary, built with `go test -c` *.test From 3ec94ba0558aa786c9b13af5d23c5b4f95e4a089 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Wed, 30 Jun 2021 10:22:01 +0200 Subject: [PATCH 28/33] added an option to omit template names --- clab/config/template.go | 16 ++++++++++++---- clab/config/utils.go | 29 ++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/clab/config/template.go b/clab/config/template.go index 08cd4e0e4..ecf3cfd5c 100644 --- a/clab/config/template.go +++ b/clab/config/template.go @@ -30,13 +30,21 @@ type NodeConfig struct { func RenderAll(nodes map[string]nodes.Node, links map[int]*types.Link) (map[string]*NodeConfig, error) { res := make(map[string]*NodeConfig) - if len(TemplateNames) == 0 { - return nil, fmt.Errorf("please specify one of more templates with --template-list") - } if len(TemplatePaths) == 0 { return nil, fmt.Errorf("please specify one of more paths with --template-path") } + if len(TemplateNames) == 0 { + var err error + TemplateNames, err = GetTemplateNamesInDirs(TemplatePaths) + if err != nil { + return nil, err + } + if len(TemplateNames) == 0 { + return nil, fmt.Errorf("no templates files were found by %s path", TemplatePaths) + } + } + tmpl, err := jT.New("", jT.SearchPath(TemplatePaths...)) if err != nil { return nil, err @@ -49,7 +57,7 @@ func RenderAll(nodes map[string]nodes.Node, links map[int]*types.Link) (map[stri } for _, baseN := range TemplateNames { - tmplN := fmt.Sprintf("%s-%s.tmpl", baseN, vars["role"]) + tmplN := fmt.Sprintf("%s__%s.tmpl", baseN, vars["role"]) data1, err := tmpl.ExecuteTemplate(tmplN, vars) if err != nil { return nil, err diff --git a/clab/config/utils.go b/clab/config/utils.go index acbb214e2..eacff67d0 100644 --- a/clab/config/utils.go +++ b/clab/config/utils.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "os" "strconv" "strings" @@ -172,4 +173,30 @@ func ipFarEnd(in netaddr.IPPrefix) netaddr.IPPrefix { return netaddr.IPPrefixFrom(n, in.Bits()) } -// DictString +// GetTemplateNamesInDirs returns a list of template file names found in a dir p +// without traversing nested dirs +// template names are following the pattern __.tmpl +func GetTemplateNamesInDirs(paths []string) ([]string, error) { + var tnames []string + for _, p := range paths { + files, err := os.ReadDir(p) + if err != nil { + return nil, err + } + + for _, file := range files { + if file.IsDir() { + continue + } + var tn string + fn := file.Name() + if strings.Contains(fn, "__") { + tn = strings.Split(fn, "__")[0] + } + + tnames = append(tnames, tn) + } + } + + return tnames, nil +} From 1cb6b9bdf82e61bd3fd132310d72689b5f570bbf Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Wed, 30 Jun 2021 10:29:16 +0200 Subject: [PATCH 29/33] removed data race --- cmd/config.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/config.go b/cmd/config.go index c75061507..af2cb1fd0 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -59,6 +59,7 @@ var configCmd = &cobra.Command{ defer wg.Done() var tx transport.Transport + var err error ct, ok := cs.TargetNode.Labels["config.transport"] if !ok { @@ -83,7 +84,7 @@ var configCmd = &cobra.Command{ return } - err := transport.Write(tx, cs.TargetNode.LongName, cs.Data, cs.Info) + err = transport.Write(tx, cs.TargetNode.LongName, cs.Data, cs.Info) if err != nil { log.Errorf("%s\n", err) } From 2e2fcdffe9de97760f2a6316858ddd596c9111b8 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Wed, 30 Jun 2021 13:09:21 +0200 Subject: [PATCH 30/33] fixed method name --- clab/config.go | 4 ++-- types/topology.go | 2 +- types/topology_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/clab/config.go b/clab/config.go index 496d57c52..f04fe0a4c 100644 --- a/clab/config.go +++ b/clab/config.go @@ -166,7 +166,7 @@ func (c *CLab) createNodeCfg(nodeName string, nodeDef *types.NodeDefinition, idx log.Debugf("node config: %+v", nodeCfg) var err error // initialize config - nodeCfg.StartupConfig, err = c.Config.Topology.GetNodeConfig(nodeCfg.ShortName) + nodeCfg.StartupConfig, err = c.Config.Topology.GetNodeStartupConfig(nodeCfg.ShortName) if err != nil { return nil, err } @@ -221,7 +221,7 @@ func (c *CLab) NewEndpoint(e string) *types.Endpoint { if len(endpoint.EndpointName) > 15 { log.Fatalf("interface '%s' name exceeds maximum length of 15 characters", endpoint.EndpointName) } - // generate unqiue MAC + // generate unique MAC endpoint.MAC = utils.GenMac(clabOUI) // search the node pointer for a node name referenced in endpoint section diff --git a/types/topology.go b/types/topology.go index 90d339022..2b86dcaab 100644 --- a/types/topology.go +++ b/types/topology.go @@ -134,7 +134,7 @@ func (t *Topology) GetNodeLabels(name string) map[string]string { return nil } -func (t *Topology) GetNodeConfig(name string) (string, error) { +func (t *Topology) GetNodeStartupConfig(name string) (string, error) { var cfg string if ndef, ok := t.Nodes[name]; ok { var err error diff --git a/types/topology_test.go b/types/topology_test.go index 8be536be9..8da6bde32 100644 --- a/types/topology_test.go +++ b/types/topology_test.go @@ -264,7 +264,7 @@ func TestGetNodeType(t *testing.T) { func TestGetNodeConfig(t *testing.T) { for name, item := range topologyTestSet { t.Logf("%q test item", name) - config, err := item.input.GetNodeConfig("node1") + config, err := item.input.GetNodeStartupConfig("node1") if err != nil { t.Fatal(err) } From 6baf54db2d04ff97751f893b22a18f89d7f9fc71 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Wed, 30 Jun 2021 17:27:53 +0200 Subject: [PATCH 31/33] rework to use config.vars --- clab/config.go | 4 ++++ clab/config/helpers.go | 2 +- clab/config/utils.go | 16 ++++++++-------- types/node_definition.go | 27 +++++++++++++++++++-------- types/topology.go | 16 ++++++++++++++++ types/types.go | 9 +++++++++ 6 files changed, 57 insertions(+), 17 deletions(-) diff --git a/clab/config.go b/clab/config.go index f04fe0a4c..383f39d3e 100644 --- a/clab/config.go +++ b/clab/config.go @@ -188,6 +188,8 @@ func (c *CLab) createNodeCfg(nodeName string, nodeDef *types.NodeDefinition, idx } nodeCfg.Labels = c.Config.Topology.GetNodeLabels(nodeCfg.ShortName) + nodeCfg.Config = c.Config.Topology.GetNodeConfigDispatcher(nodeCfg.ShortName) + return nodeCfg, nil } @@ -196,11 +198,13 @@ func (c *CLab) NewLink(l *types.LinkConfig) *types.Link { if len(l.Endpoints) != 2 { log.Fatalf("endpoint %q has wrong syntax, unexpected number of items", l.Endpoints) } + return &types.Link{ A: c.NewEndpoint(l.Endpoints[0]), B: c.NewEndpoint(l.Endpoints[1]), MTU: defaultVethLinkMTU, Labels: l.Labels, + Vars: l.Vars, } } diff --git a/clab/config/helpers.go b/clab/config/helpers.go index 318b346a8..df3347378 100644 --- a/clab/config/helpers.go +++ b/clab/config/helpers.go @@ -22,7 +22,7 @@ type NodeSettings struct { // Temporary function to extract NodeSettings from the Labels // In the next phase node settings will be added to the clab file -func GetNodeConfigFromLabels(labels map[string]string) NodeSettings { +func GetNodeConfig(labels map[string]string) NodeSettings { nc := NodeSettings{ Vars: labels, Transport: "ssh", diff --git a/clab/config/utils.go b/clab/config/utils.go index eacff67d0..bacfeb5a1 100644 --- a/clab/config/utils.go +++ b/clab/config/utils.go @@ -29,7 +29,7 @@ func PrepareVars(nodes map[string]nodes.Node, links map[int]*types.Link) map[str name := nodeCfg.ShortName // Init array for this node res[name] = make(map[string]interface{}) - nc := GetNodeConfigFromLabels(nodeCfg.Labels) + nc := GetNodeConfig(nodeCfg.Config.Vars) for key := range nc.Vars { res[name][key] = nc.Vars[key] } @@ -57,9 +57,8 @@ func PrepareVars(nodes map[string]nodes.Node, links map[int]*types.Link) map[str // Prepare variables for a specific link func prepareLinkVars(lIdx int, link *types.Link, varsA, varsB map[string]interface{}) error { - ncA := GetNodeConfigFromLabels(link.A.Node.Labels) - ncB := GetNodeConfigFromLabels(link.B.Node.Labels) - linkVars := link.Labels + ncA := GetNodeConfig(link.A.Node.Config.Vars) + ncB := GetNodeConfig(link.B.Node.Config.Vars) addV := func(key string, v1 interface{}, v2 ...interface{}) { varsA[key] = v1 @@ -77,11 +76,12 @@ func prepareLinkVars(lIdx int, link *types.Link, varsA, varsB map[string]interfa if err != nil { return fmt.Errorf("%s: %s", link, err) } + addV("ip", ipA.String(), ipB.String()) addV(systemIP, ncA.Vars[systemIP], ncB.Vars[systemIP]) // Split all fields with a comma... - for k, v := range linkVars { + for k, v := range link.Vars { r := SplitTrim(v) switch len(r) { case 1: @@ -108,7 +108,7 @@ func prepareLinkVars(lIdx int, link *types.Link, varsA, varsB map[string]interfa func linkIPfromSystemIP(link *types.Link) (netaddr.IPPrefix, netaddr.IPPrefix, error) { var ipA netaddr.IPPrefix var err error - if linkIp, ok := link.Labels["ip"]; ok { + if linkIp, ok := link.Vars["ip"]; ok { // calc far end IP ipA, err = netaddr.ParseIPPrefix(linkIp) if err != nil { @@ -116,11 +116,11 @@ func linkIPfromSystemIP(link *types.Link) (netaddr.IPPrefix, netaddr.IPPrefix, e } } else { // Calculate link IP from the system IPs - sysA, err := netaddr.ParseIPPrefix(link.A.Node.Labels[systemIP]) + sysA, err := netaddr.ParseIPPrefix(link.A.Node.Config.Vars[systemIP]) if err != nil { return ipA, ipA, fmt.Errorf("no 'ip' on link & the '%s' of %s: %s", systemIP, link.A.Node.ShortName, err) } - sysB, err := netaddr.ParseIPPrefix(link.B.Node.Labels[systemIP]) + sysB, err := netaddr.ParseIPPrefix(link.B.Node.Config.Vars[systemIP]) if err != nil { return ipA, ipA, fmt.Errorf("no 'ip' on link & the '%s' of %s: %s", systemIP, link.B.Node.ShortName, err) } diff --git a/types/node_definition.go b/types/node_definition.go index 5bbdf1453..feb1431db 100644 --- a/types/node_definition.go +++ b/types/node_definition.go @@ -2,14 +2,15 @@ package types // NodeDefinition represents a configuration a given node can have in the lab definition file type NodeDefinition struct { - Kind string `yaml:"kind,omitempty"` - Group string `yaml:"group,omitempty"` - Type string `yaml:"type,omitempty"` - StartupConfig string `yaml:"startup-config,omitempty"` - Image string `yaml:"image,omitempty"` - License string `yaml:"license,omitempty"` - Position string `yaml:"position,omitempty"` - Cmd string `yaml:"cmd,omitempty"` + Kind string `yaml:"kind,omitempty"` + Group string `yaml:"group,omitempty"` + Type string `yaml:"type,omitempty"` + StartupConfig string `yaml:"startup-config,omitempty"` + Config *ConfigDispatcher `yaml:"config,omitempty"` + Image string `yaml:"image,omitempty"` + License string `yaml:"license,omitempty"` + Position string `yaml:"position,omitempty"` + Cmd string `yaml:"cmd,omitempty"` // list of bind mount compatible strings Binds []string `yaml:"binds,omitempty"` // list of port bindings @@ -58,6 +59,16 @@ func (n *NodeDefinition) GetStartupConfig() string { return n.StartupConfig } +func (n *NodeDefinition) GetConfigDispatcher() *ConfigDispatcher { + if n == nil { + return nil + } + if n.Config == nil { + return &ConfigDispatcher{} + } + return n.Config +} + func (n *NodeDefinition) GetImage() string { if n == nil { return "" diff --git a/types/topology.go b/types/topology.go index 2b86dcaab..41baf981c 100644 --- a/types/topology.go +++ b/types/topology.go @@ -29,6 +29,7 @@ func NewTopology() *Topology { type LinkConfig struct { Endpoints []string Labels map[string]string `yaml:"labels,omitempty"` + Vars map[string]string `yaml:"vars,omitempty"` } func (t *Topology) GetDefaults() *NodeDefinition { @@ -134,6 +135,21 @@ func (t *Topology) GetNodeLabels(name string) map[string]string { return nil } +func (t *Topology) GetNodeConfigDispatcher(name string) *ConfigDispatcher { + if ndef, ok := t.Nodes[name]; ok { + vars := utils.MergeStringMaps( + utils.MergeStringMaps(t.Defaults.GetConfigDispatcher().Vars, + t.GetKind(t.GetNodeKind(name)).GetConfigDispatcher().Vars), + ndef.Config.Vars) + + return &ConfigDispatcher{ + Vars: vars, + } + } + + return nil +} + func (t *Topology) GetNodeStartupConfig(name string) (string, error) { var cfg string if ndef, ok := t.Nodes[name]; ok { diff --git a/types/types.go b/types/types.go index 456b4fb5c..48b3381fc 100644 --- a/types/types.go +++ b/types/types.go @@ -24,6 +24,7 @@ type Link struct { B *Endpoint MTU int Labels map[string]string + Vars map[string]string } func (link *Link) String() string { @@ -61,6 +62,7 @@ type NodeConfig struct { Kind string StartupConfig string // path to config template file that is used for startup config generation ResStartupConfig string // path to config file that is actually mounted to the container and is a result of templation + Config *ConfigDispatcher NodeType string Position string License string @@ -197,3 +199,10 @@ func FilterFromLabelStrings(labels []string) []*GenericFilter { } return gfl } + +// ConfigDispatcher represents the config of a configuration machine +// that is responsible to execute configuration commands on the nodes +// after they started +type ConfigDispatcher struct { + Vars map[string]string `yaml:"vars,omitempty"` +} From e8425b7e3fedf3f2df1f754e7e01bea031580827 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Wed, 30 Jun 2021 17:39:51 +0200 Subject: [PATCH 32/33] fixed init of cfg dispatcher --- types/topology.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/topology.go b/types/topology.go index 41baf981c..1176817a4 100644 --- a/types/topology.go +++ b/types/topology.go @@ -140,7 +140,7 @@ func (t *Topology) GetNodeConfigDispatcher(name string) *ConfigDispatcher { vars := utils.MergeStringMaps( utils.MergeStringMaps(t.Defaults.GetConfigDispatcher().Vars, t.GetKind(t.GetNodeKind(name)).GetConfigDispatcher().Vars), - ndef.Config.Vars) + ndef.GetConfigDispatcher().Vars) return &ConfigDispatcher{ Vars: vars, From e88a5e793fdd7b6bef03b2994513d318fe787a49 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Wed, 30 Jun 2021 18:56:21 +0200 Subject: [PATCH 33/33] bring back templates --- lab-examples/vr05/sros4.clab.yml | 38 ++++++++-------- lab-examples/vr05/vr01.clab.yml | 24 +++++----- templates/base__srl.tmpl | 75 ++++++++++++++++++++++++++++++++ templates/base__vr-sros.tmpl | 0 templates/srl-ifaces__srl.tmpl | 32 ++++++++++++++ 5 files changed, 141 insertions(+), 28 deletions(-) create mode 100644 templates/base__srl.tmpl create mode 100644 templates/base__vr-sros.tmpl create mode 100644 templates/srl-ifaces__srl.tmpl diff --git a/lab-examples/vr05/sros4.clab.yml b/lab-examples/vr05/sros4.clab.yml index 91ef81431..767fa791c 100644 --- a/lab-examples/vr05/sros4.clab.yml +++ b/lab-examples/vr05/sros4.clab.yml @@ -4,43 +4,47 @@ topology: defaults: kind: vr-sros image: registry.srlinux.dev/pub/vr-sros:21.2.R1 - license: /home/kellerza/license/license-sros21.txt + license: license-sros21.txt labels: isis_iid: 0 nodes: sr1: - labels: - systemip: 10.0.50.31/32 - sid_idx: 1 + config: + vars: + systemip: 10.0.50.31/32 + sid_idx: 1 sr2: - labels: - systemip: 10.0.50.32/32 - sid_idx: 2 + config: + vars: + systemip: 10.0.50.32/32 + sid_idx: 2 sr3: - labels: - systemip: 10.0.50.33/32 - sid_idx: 3 + config: + vars: + systemip: 10.0.50.33/32 + sid_idx: 3 sr4: - labels: - systemip: 10.0.50.34/32 - sid_idx: 4 + config: + vars: + systemip: 10.0.50.34/32 + sid_idx: 4 links: - endpoints: [sr1:eth1, sr2:eth2] - labels: + vars: port: 1/1/c1/1, 1/1/c2/1 ip: 1.1.1.2/30 vlan: "99,99" isis_iid: 0 - endpoints: [sr2:eth1, sr3:eth2] - labels: + vars: port: 1/1/c1/1, 1/1/c2/1 vlan: 98 isis_iid: 0 - endpoints: [sr3:eth1, sr4:eth2] - labels: + vars: port: 1/1/c1/1, 1/1/c2/1 isis_iid: 0 - endpoints: [sr4:eth1, sr1:eth2] - labels: + vars: port: 1/1/c1/1, 1/1/c2/1 isis_iid: 0 diff --git a/lab-examples/vr05/vr01.clab.yml b/lab-examples/vr05/vr01.clab.yml index 5b5f05182..0da9da43c 100644 --- a/lab-examples/vr05/vr01.clab.yml +++ b/lab-examples/vr05/vr01.clab.yml @@ -5,24 +5,26 @@ topology: srl: kind: srl image: registry.srlinux.dev/pub/srlinux:21.3.1-410 - license: /home/kellerza/license/srl21.key - labels: - systemip: 10.0.50.50/32 - isis_iid: 0 - sid_idx: 11 + license: license.key + config: + vars: + systemip: 10.0.50.50/32 + isis_iid: 0 + sid_idx: 11 sros: kind: vr-sros image: registry.srlinux.dev/pub/vr-sros:21.2.R1 type: sr-1 - license: /home/kellerza/license/license-sros21.txt - labels: - systemip: 10.0.50.51/32 - sid_idx: 10 - isis_iid: 0 + license: license-sros21.txt + config: + vars: + systemip: 10.0.50.51/32 + sid_idx: 10 + isis_iid: 0 links: - endpoints: ["srl:e1-1", "sros:eth1"] - labels: + vars: port: ethernet-1/1, 1/1/c1/1 vlan: 10 isis_iid: 0 diff --git a/templates/base__srl.tmpl b/templates/base__srl.tmpl new file mode 100644 index 000000000..a54eeab5b --- /dev/null +++ b/templates/base__srl.tmpl @@ -0,0 +1,75 @@ +{{ expect .systemip "ip" }} +{{ optional .isis_iid "0-31" }} +{{ range .links }} + {{ expect .port "^(ethernet-\\d+/|e\\d+-)\\d+$" }} + {{ expect .name "string" }} + {{ expect .ip "ip" }} + {{ optional .vlan "0-4095" }} + {{ optional .metric "1-10000" }} +{{ end }} + +/interface lo0 { + admin-state enable + subinterface 0 { + ipv4 { + address {{ .systemip }} { + } + } + ipv6 { + address ::ffff:{{ ip .systemip }}/128 { + } + } + } +} + +/network-instance default { + router-id {{ ip .systemip }} + interface lo0.0 { + } + protocols { + isis { + instance default { + admin-state enable + level-capability L2 + set level 2 metric-style wide + # net should not be multiline (net [), becasue of the SRL ... prompt + net [ 49.0000.0000.0000.0{{ default 0 .isis_iid }} ] + interface lo0.0 { + } + } + } + } +} + + /system lldp admin-state enable + + +{{ range .links }} +/interface {{ .port }} { + admin-state enable + vlan-tagging true + subinterface {{ default 10 .vlan }} { + set vlan encap single-tagged vlan-id {{ default 10 .vlan }} + set ipv4 address {{ .ip }} + set ipv6 address ::FFFF:{{ ip .ip }}/127 + } +} + +/network-instance default { + interface {{ .port }}.{{ default 10 .vlan }} { + } + protocols { + isis { + instance default { + interface {{ .port }}.{{ default 10 .vlan }} { + circuit-type point-to-point + level 2 { + metric {{ default 10 .metric }} + } + } + } + } + } +} + +{{ end }} \ No newline at end of file diff --git a/templates/base__vr-sros.tmpl b/templates/base__vr-sros.tmpl new file mode 100644 index 000000000..e69de29bb diff --git a/templates/srl-ifaces__srl.tmpl b/templates/srl-ifaces__srl.tmpl new file mode 100644 index 000000000..88188c967 --- /dev/null +++ b/templates/srl-ifaces__srl.tmpl @@ -0,0 +1,32 @@ +{{ expect .systemip "ip" }} +{{ range .links }} + {{ expect .port "^(ethernet-\\d+/|e\\d+-)\\d+$" }} + {{ expect .name "string" }} +{{ end }} +/interface lo0 { + admin-state enable + subinterface 0 { + ipv4 { + address {{ .systemip }} { + } + } + ipv6 { + address ::c1ab:{{ ip .systemip }}/128 { + } + } + } +} +/network-instance default { + router-id {{ ip .systemip }} + interface lo0.0 { + } +} +/system lldp admin-state enable +/ network-instance mgmt { + description "set from clab" +} +{{ range .links }} +/interface {{ .port }} { + admin-state enable +} +{{ end }} \ No newline at end of file