Skip to content

Commit

Permalink
Version-specific config tunings and custom prompt for SR Linux (#1740)
Browse files Browse the repository at this point in the history
* carved out the template and added version specific config func

* remove commit from the default template since commit step is executed later on

* rename func

* added custom prompt

* fixed default config template location and added a note about unix-socket access
  • Loading branch information
hellt authored Nov 23, 2023
1 parent 27fad67 commit 47b89b9
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 88 deletions.
6 changes: 3 additions & 3 deletions docs/manual/kinds/srl.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ SR Linux uses a `/etc/opt/srlinux/config.json` file to persist its configuration

#### Default node configuration

When a node is defined without the `startup-config` statement present, containerlab will make [additional configurations](https://github.com/srl-labs/containerlab/blob/main/nodes/srl/srl.go#L38) on top of the factory config:
When a node is defined without the `startup-config` statement present, containerlab will make [additional configurations](https://github.com/srl-labs/containerlab/blob/srl-template-in-a-file/nodes/srl/srl_default_config.go.tpl) on top of the factory config:

```yaml
# example of a topo file that does not define a custom startup-config
Expand All @@ -144,13 +144,13 @@ topology:
type: ixrd3
```

The generated config will be saved by the path `clab-<lab_name>/<node-name>/config/config.json`. Using the example topology presented above, the exact path to the config will be `clab-srl_lab/srl1/config/config.json`.
The rendered config can be found at `/tmp/clab-default-config` path on SR Linux filesystem and will be saved by the path `clab-<lab_name>/<node-name>/config/config.json`. Using the example topology presented above, the exact path to the config will be `clab-srl_lab/srl1/config/config.json`.

Additional configurations that containerlab adds on top of the factory config:

* enabling interfaces (`admin-state enable`) referenced in the topology's `links` section
* enabling LLDP
* enabling gNMI/JSON-RPC
* enabling gNMI/gNOI/JSON-RPC as well as enabling unix-socket access for gRPC services
* creating tls server certificate
* setting `mgmt0 subinterface 0 ip-mtu` to the MTU value of the underlying container runtime network

Expand Down
60 changes: 60 additions & 0 deletions nodes/srl/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package srl

import (
"context"
"fmt"
"os"
"regexp"
"strings"

log "github.com/sirupsen/logrus"
"github.com/srl-labs/containerlab/clab/exec"
)

func (n *srl) setCustomPrompt(tplData *srlTemplateData) {
// when CLAB_CUSTOM_PROMPT is set to false, we don't generate custom prompt
if strings.ToLower(os.Getenv("CLAB_CUSTOM_PROMPT")) == "false" {
return
}

tplData.EnableCustomPrompt = true

// get the current prompt
prompt, err := n.currentPrompt(context.Background())
if err != nil {
log.Errorf("failed to get current prompt: %v", err)
tplData.EnableCustomPrompt = false
return
}

// adding newline to the prompt for better visual separation
tplData.CustomPrompt = "\\n" + prompt

}

// currentPrompt returns the current prompt extracted from the environment.
func (n *srl) currentPrompt(ctx context.Context) (string, error) {
cmd, _ := exec.NewExecCmdFromString(`sr_cli -d "environment show | grep -A 2 prompt"`)

execResult, err := n.RunExec(ctx, cmd)
if err != nil {
return "", err
}

log.Debugf("fetching prompt for node %s. stdout: %s, stderr: %s", n.Cfg.ShortName, execResult.GetStdOutString(), execResult.GetStdErrString())

return getPrompt(execResult.GetStdOutString())
}

// getPrompt returns the prompt value from a string blob containing the prompt.
// The s is the output of the "environment show | grep -A 2 prompt" command.
func getPrompt(s string) (string, error) {
re, _ := regexp.Compile(`value\s+=\s+"(.+)"`)
v := re.FindStringSubmatch(s)

if len(v) != 2 {
return "", fmt.Errorf("failed to parse prompt from string: %s", s)
}

return v[1], nil
}
39 changes: 39 additions & 0 deletions nodes/srl/prompt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package srl

import (
"testing"
)

func Test_getPrompt(t *testing.T) {
tests := []struct {
name string
s string
want string
wantErr bool
}{
{
name: "Test with valid input",
s: `value = "test-prompt"`,
want: "test-prompt",
wantErr: false,
},
{
name: "Test with invalid input",
s: `invalid input`,
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getPrompt(tt.s)
if (err != nil) != tt.wantErr {
t.Errorf("getPrompt() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("getPrompt() = %v, want %v", got, tt.want)
}
})
}
}
147 changes: 62 additions & 85 deletions nodes/srl/srl.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,61 +44,14 @@ const (
// overlayCfgPath is a path to a file with additional config that clab adds on top of the default config.
// Partial config provided via startup-config parameter is an overlay config.
overlayCfgPath = "/tmp/clab-overlay-config"

// additional config that clab adds on top of the factory config.
srlConfigCmdsTpl = `set / system tls server-profile clab-profile
set / system tls server-profile clab-profile key "{{ .TLSKey }}"
set / system tls server-profile clab-profile certificate "{{ .TLSCert }}"
{{- if .TLSAnchor }}
set / system tls server-profile clab-profile authenticate-client true
set / system tls server-profile clab-profile trust-anchor "{{ .TLSAnchor }}"
{{- else }}
set / system tls server-profile clab-profile authenticate-client false
{{- end }}
set / system gnmi-server admin-state enable network-instance mgmt admin-state enable tls-profile clab-profile
set / system gnmi-server rate-limit 65000
set / system gnmi-server trace-options [ request response common ]
set / system gnmi-server unix-socket admin-state enable
{{- if .DNSServers }}
set / system dns network-instance mgmt
set / system dns server-list [ {{ range $dnsserver := .DNSServers}}{{$dnsserver}} {{ end }}]
{{- end }}
set / system json-rpc-server admin-state enable network-instance mgmt http admin-state enable
set / system json-rpc-server admin-state enable network-instance mgmt https admin-state enable tls-profile clab-profile
set / system snmp community public
set / system snmp network-instance mgmt
set / system snmp network-instance mgmt admin-state enable
set / system lldp admin-state enable
set / system aaa authentication idle-timeout 7200
{{- /* if e.g. node is run with none mgmt networking but a macvlan interface is attached as mgmt0, we need to adjust the mtu */}}
{{- if ne .MgmtMTU 0 }}
set / interface mgmt0 mtu {{ .MgmtMTU }}
{{- end }}
{{- if ne .MgmtIPMTU 0 }}
set / interface mgmt0 subinterface 0 ip-mtu {{ .MgmtIPMTU }}
{{- end }}
{{- /* enabling interfaces referenced as endpoints for a node (both e1-2 and e1-3-1 notations) */}}
{{- range $epName, $ep := .IFaces }}
set / interface ethernet-{{ $ep.Slot }}/{{ $ep.Port }} admin-state enable
{{- if ne $ep.Mtu 0 }}
set / interface ethernet-{{ $ep.Slot }}/{{ $ep.Port }} mtu {{ $ep.Mtu }}
{{- end }}
{{- if ne $ep.BreakoutNo "" }}
set / interface ethernet-{{ $ep.Slot }}/{{ $ep.Port }} breakout-mode num-channels 4 channel-speed 25G
set / interface ethernet-{{ $ep.Slot }}/{{ $ep.Port }}/{{ $ep.BreakoutNo }} admin-state enable
{{- end }}
{{ end -}}
{{- if .SSHPubKeys }}
set / system aaa authentication linuxadmin-user ssh-key [ {{ .SSHPubKeys }} ]
set / system aaa authentication admin-user ssh-key [ {{ .SSHPubKeys }} ]
{{- end }}
set / system banner login-banner "{{ .Banner }}"
commit save`
)

var (

// additional config that clab adds on top of the factory config.
//go:embed srl_default_config.go.tpl
srlConfigCmdsTpl string

KindNames = []string{"srl", "nokia_srlinux"}
srlSysctl = map[string]string{
"net.ipv4.ip_forward": "0",
Expand Down Expand Up @@ -140,7 +93,7 @@ var (
// readyForConfigCmd checks the output of a file on srlinux which will be populated once the mgmt server is ready to accept config.
readyForConfigCmd = "cat /etc/opt/srlinux/devices/app_ephemeral.mgmt_server.ready_for_config"

srlCfgTpl, _ = template.New("srl-tls-profile").
srlCfgTpl, _ = template.New("clab-srl-default-config").
Funcs(gomplate.CreateFuncs(context.Background(), new(data.Data))).
Parse(srlConfigCmdsTpl)

Expand Down Expand Up @@ -173,67 +126,67 @@ type srl struct {
swVersion *SrlVersion
}

func (s *srl) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error {
func (n *srl) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error {
// Init DefaultNode
s.DefaultNode = *nodes.NewDefaultNode(s)
n.DefaultNode = *nodes.NewDefaultNode(n)
// set virtualization requirement
s.HostRequirements.SSSE3 = true
s.HostRequirements.MinVCPU = 2
s.HostRequirements.MinVCPUFailAction = types.FailBehaviourError
s.HostRequirements.MinAvailMemoryGb = 2
s.HostRequirements.MinAvailMemoryGbFailAction = types.FailBehaviourLog
n.HostRequirements.SSSE3 = true
n.HostRequirements.MinVCPU = 2
n.HostRequirements.MinVCPUFailAction = types.FailBehaviourError
n.HostRequirements.MinAvailMemoryGb = 2
n.HostRequirements.MinAvailMemoryGbFailAction = types.FailBehaviourLog

s.Cfg = cfg
n.Cfg = cfg

// force cert creation for srlinux nodes as they by make use of tls certificate in the default config
s.Cfg.Certificate.Issue = utils.BoolPointer(true)
n.Cfg.Certificate.Issue = utils.BoolPointer(true)

for _, o := range opts {
o(s)
o(n)
}

if s.Cfg.NodeType == "" {
s.Cfg.NodeType = srlDefaultType
if n.Cfg.NodeType == "" {
n.Cfg.NodeType = srlDefaultType
}

if _, found := srlTypes[s.Cfg.NodeType]; !found {
if _, found := srlTypes[n.Cfg.NodeType]; !found {
keys := make([]string, 0, len(srlTypes))
for key := range srlTypes {
keys = append(keys, key)
}
return fmt.Errorf("wrong node type. '%s' doesn't exist. should be any of %s",
s.Cfg.NodeType, strings.Join(keys, ", "))
n.Cfg.NodeType, strings.Join(keys, ", "))
}

if s.Cfg.Cmd == "" {
if n.Cfg.Cmd == "" {
// set default Cmd if it was not provided by a user
// the additional touch is needed to support non docker runtimes
s.Cfg.Cmd = "sudo bash -c 'touch /.dockerenv && /opt/srlinux/bin/sr_linux'"
n.Cfg.Cmd = "sudo bash -c 'touch /.dockerenv && /opt/srlinux/bin/sr_linux'"
}

s.Cfg.Env = utils.MergeStringMaps(srlEnv, s.Cfg.Env)
n.Cfg.Env = utils.MergeStringMaps(srlEnv, n.Cfg.Env)

// if user was not initialized to a value, use root
if s.Cfg.User == "" {
s.Cfg.User = "0:0"
if n.Cfg.User == "" {
n.Cfg.User = "0:0"
}
for k, v := range srlSysctl {
s.Cfg.Sysctls[k] = v
n.Cfg.Sysctls[k] = v
}

if s.Cfg.License != "" {
if n.Cfg.License != "" {
// we mount a fixed path node.Labdir/license.key as the license referenced in topo file will be copied to that path
s.Cfg.Binds = append(s.Cfg.Binds, fmt.Sprint(
filepath.Join(s.Cfg.LabDir, "license.key"), ":/opt/srlinux/etc/license.key:ro"))
n.Cfg.Binds = append(n.Cfg.Binds, fmt.Sprint(
filepath.Join(n.Cfg.LabDir, "license.key"), ":/opt/srlinux/etc/license.key:ro"))
}

// mount config directory
cfgPath := filepath.Join(s.Cfg.LabDir, "config")
s.Cfg.Binds = append(s.Cfg.Binds, fmt.Sprint(cfgPath, ":/etc/opt/srlinux/:rw"))
cfgPath := filepath.Join(n.Cfg.LabDir, "config")
n.Cfg.Binds = append(n.Cfg.Binds, fmt.Sprint(cfgPath, ":/etc/opt/srlinux/:rw"))

// mount srlinux topology
topoPath := filepath.Join(s.Cfg.LabDir, "topology.yml")
s.Cfg.Binds = append(s.Cfg.Binds, fmt.Sprint(topoPath, ":/tmp/topology.yml:ro"))
topoPath := filepath.Join(n.Cfg.LabDir, "topology.yml")
n.Cfg.Binds = append(n.Cfg.Binds, fmt.Sprint(topoPath, ":/tmp/topology.yml:ro"))

return nil
}
Expand Down Expand Up @@ -569,6 +522,13 @@ type srlTemplateData struct {
MgmtMTU int
MgmtIPMTU int
DNSServers []string
// EnableGNMIUnixSockServices enables GNMI unix socket services
// for the node. This is needed for "23.10 <= ver < 24.3" versions
EnableGNMIUnixSockServices bool
// EnableCustomPrompt enables custom prompt with added newline
// before the prompt.
EnableCustomPrompt bool
CustomPrompt string
}

// tplIFace template interface struct.
Expand Down Expand Up @@ -598,11 +558,9 @@ func (n *srl) addDefaultConfig(ctx context.Context) error {
DNSServers: n.Config().DNS.Servers,
}

// in srlinux >= v23.10+ linuxadmin and admin user ssh keys can only be configured via the cli
// so we add the keys to the template data for rendering.
if len(n.sshPubKeys) > 0 && (semver.Compare(n.swVersion.String(), "v23.10") >= 0 || n.swVersion.major == "0") {
tplData.SSHPubKeys = catenateKeys(n.sshPubKeys)
}
n.setVersionSpecificParams(&tplData, n.swVersion)

n.setCustomPrompt(&tplData)

// set MgmtMTU to the MTU value of the runtime management network
// so that the two MTUs match.
Expand Down Expand Up @@ -856,3 +814,22 @@ gpgcheck=0`

return nil
}

// setVersionSpecificParams sets version specific parameters in the template data struct
// to enable/disable version-specific configuration blocks in the config template
// or prepares data to conform to the expected format per specific version.
func (n *srl) setVersionSpecificParams(tplData *srlTemplateData, swVersion *SrlVersion) {
v := n.swVersion.String()

// in srlinux >= v23.10+ linuxadmin and admin user ssh keys can only be configured via the cli
// so we add the keys to the template data for rendering.
if len(n.sshPubKeys) > 0 && (semver.Compare(v, "v23.10") >= 0 || n.swVersion.major == "0") {
tplData.SSHPubKeys = catenateKeys(n.sshPubKeys)
}

// in srlinux v23.10+ till 24.3 we need to enable GNMI unix socket services to enable
// communications over unix socket (e.g. NDK agents)
if semver.Compare(v, "v23.10") >= 0 && semver.Compare(v, "v24.3") < 0 {
tplData.EnableGNMIUnixSockServices = true
}
}
Loading

0 comments on commit 47b89b9

Please sign in to comment.