diff --git a/clab/export.go b/clab/export.go index 371ca186c..50cd1c48c 100644 --- a/clab/export.go +++ b/clab/export.go @@ -17,9 +17,10 @@ import ( log "github.com/sirupsen/logrus" "github.com/srl-labs/containerlab/types" + "github.com/srl-labs/containerlab/utils" ) -// GenerateExports generates various export files and writes it to a lab location. +// GenerateExports generates various export files and writes it to a file in the lab directory. func (c *CLab) GenerateExports(ctx context.Context, f io.Writer, p string) error { err := c.exportTopologyDataWithTemplate(ctx, f, p) if err != nil { @@ -36,9 +37,11 @@ func (c *CLab) GenerateExports(ctx context.Context, f io.Writer, p string) error // TopologyExport holds a combination of CLab structure and map of NodeConfig types, // which expands Node definitions with dynamically created values. type TopologyExport struct { - Name string `json:"name"` - Type string `json:"type"` - Clab *CLab `json:"clab,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Clab *CLab `json:"clab,omitempty"` + // SSHPubKeys is a list of string representations of SSH public keys. + SSHPubKeys []string `json:"SSHPubKeys,omitempty"` NodeConfigs map[string]*types.NodeConfig `json:"nodeconfigs,omitempty"` } @@ -87,6 +90,7 @@ func (c *CLab) exportTopologyDataWithTemplate(_ context.Context, w io.Writer, p Name: c.Config.Name, Type: "clab", Clab: c, + SSHPubKeys: utils.MarshalSSHPubKeys(c.SSHPubKeys), NodeConfigs: make(map[string]*types.NodeConfig), } diff --git a/clab/export_templates/auto.tmpl b/clab/export_templates/auto.tmpl index 497f26a7f..a95e0935b 100644 --- a/clab/export_templates/auto.tmpl +++ b/clab/export_templates/auto.tmpl @@ -7,6 +7,7 @@ "mgmt": {{ ToJSONPretty .Clab.Config.Mgmt " " " "}} } }, + "ssh-pub-keys": {{ ToJSON .SSHPubKeys }}, "nodes": { {{- $i:=0 }}{{range $n, $c := .NodeConfigs}}{{if $i}},{{end}} "{{$n}}": { "index": "{{$c.Index}}", diff --git a/clab/export_templates/full.tmpl b/clab/export_templates/full.tmpl index 3699ec315..2e03a84a4 100644 --- a/clab/export_templates/full.tmpl +++ b/clab/export_templates/full.tmpl @@ -2,6 +2,7 @@ "name": "{{ .Name }}", "type": "{{ .Type }}", "clab": {{ ToJSONPretty .Clab " " " "}}, + "ssh-pub-keys": {{ ToJSON .SSHPubKeys }}, "nodes": { {{- $i:=0 }}{{range $n, $c := .NodeConfigs}}{{if $i}},{{end}}{{ $k := dict "tls-key" $c.TLSKey }} "{{$n}}":{{ $cj := $c | data.ToJSON | data.JSON }} {{ $dst := coll.Merge $cj $k }}{{ ToJSONPretty $dst " " " " }}{{$i = add $i 1}}{{end}} }, diff --git a/nodes/srl/sshkey.go b/nodes/srl/sshkey.go deleted file mode 100644 index 1b3daf349..000000000 --- a/nodes/srl/sshkey.go +++ /dev/null @@ -1,32 +0,0 @@ -package srl - -import ( - "bytes" - "strings" - - "golang.org/x/crypto/ssh" -) - -// catenateKeys catenates the ssh public keys -// and produces a string that can be used in the -// cli config command to set the ssh public keys -// for users. -func catenateKeys(in []ssh.PublicKey) string { - var keys strings.Builder - // pre-allocate the string builder capacity - keys.Grow(len(in) * 100) - // iterate through keys - for i, k := range in { - // extract the keys in AuthorizedKeys format (e.g. "ssh-rsa ") - ks := bytes.TrimSpace(ssh.MarshalAuthorizedKey(k)) - // add a separator, leading quote, the key string and trailing quote - if i > 0 { - keys.WriteByte(' ') - } - keys.WriteByte('"') - keys.Write(ks) - keys.WriteByte('"') - } - // return the string builders content as string - return keys.String() -} diff --git a/nodes/srl/sshkey_test.go b/nodes/srl/sshkey_test.go deleted file mode 100644 index c630c768d..000000000 --- a/nodes/srl/sshkey_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package srl - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/srl-labs/containerlab/utils" -) - -func Test_srl_catenateKeys(t *testing.T) { - type fields struct { - keyFiles []string - } - tests := []struct { - name string - fields fields - want string - }{ - { - name: "test1", - fields: fields{ - keyFiles: []string{"test_data/keys"}, - }, - want: "\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCs4Qv1yrBk6ygt+o7J4sUcYv+WfDjdAyABDoinOt3PgSmCcVqqAP2qS8UtTnMNuy93Orp6+/R/7/R3O5xdY6I4YViK3WVlKTAUVm7vdeTKp9uq1tNeWgo7+J3baSbQ3INp85ScTfFvRzRCFkr/W97Wh6pTa7ysgkcPvc2/tXG2z36Mx7/TFBk3Q1LY3ByKLtGrC5JnVpMTrqrsCwcLEVHHEZ4z5R4FZED/lpz+wTNFnR/l9HA6yDkKYensHynx+guqYpYD6y4yEGY/LcUnwBg0zIlUhmOsvdmxWBz12Lp7EBiNjSwhnPfe+o3efLGGnjWUAa4TgO8Sa8PQP0pK/ZNd\" \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILKdXYzPIq8kHRJtDrh21wMVI76AnuPk7HDLeDteKN74\"", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - keys, err := utils.LoadSSHPubKeysFromFiles(tt.fields.keyFiles) - if err != nil { - t.Errorf("failed to load keys: %v", err) - } - - n := &srl{ - sshPubKeys: keys, - } - - got := catenateKeys(n.sshPubKeys) - - if d := cmp.Diff(got, tt.want); d != "" { - t.Errorf("srl.catenateKeys() = %s", d) - } - }) - } -} diff --git a/nodes/srl/version.go b/nodes/srl/version.go index ea4f87d7e..679d45e0e 100644 --- a/nodes/srl/version.go +++ b/nodes/srl/version.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/srl-labs/containerlab/clab/exec" + "github.com/srl-labs/containerlab/utils" "golang.org/x/mod/semver" ) @@ -163,7 +164,7 @@ func (n *srl) setVersionSpecificParams(tplData *srlTemplateData) { // 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) + tplData.SSHPubKeys = utils.MarshalAndCatenateSSHPubKeys(n.sshPubKeys) } // in srlinux >= v24.3+ we add ACL rules to enable http and telnet access diff --git a/utils/ssh.go b/utils/ssh.go index ed6399576..ae8ebb041 100644 --- a/utils/ssh.go +++ b/utils/ssh.go @@ -1,10 +1,13 @@ package utils import ( + "bytes" "os/exec" "regexp" + "strings" log "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" ) // GetSSHVersion returns the version of the ssh client @@ -32,3 +35,34 @@ func parseSSHVersion(in string) string { return match[1] } + +// MarshalSSHPubKeys marshales the ssh public keys +// and a string slice that contains string representations of the keys. +func MarshalSSHPubKeys(in []ssh.PublicKey) []string { + r := []string{} + + for _, k := range in { + // extract the keys in AuthorizedKeys format (e.g. "ssh-rsa ") + ks := bytes.TrimSpace(ssh.MarshalAuthorizedKey(k)) + r = append(r, string(ks)) + + } + + return r +} + +// MarshalAndCatenateSSHPubKeys catenates the ssh public keys +// and produces a string that can be used in the +// cli config command to set the ssh public keys +// for users. +// Each key value in the catenated string will be double quoted. +func MarshalAndCatenateSSHPubKeys(in []ssh.PublicKey) string { + keysSlice := MarshalSSHPubKeys(in) + quotedKeys := make([]string, len(keysSlice)) + + for i, k := range keysSlice { + quotedKeys[i] = "\"" + k + "\"" + } + + return strings.Join(quotedKeys, " ") +} diff --git a/utils/ssh_test.go b/utils/ssh_test.go index ed1d04b5f..b6a5aa0cf 100644 --- a/utils/ssh_test.go +++ b/utils/ssh_test.go @@ -2,6 +2,8 @@ package utils import ( "testing" + + "github.com/google/go-cmp/cmp" ) func TestParseSSHVersion(t *testing.T) { @@ -36,3 +38,36 @@ func TestParseSSHVersion(t *testing.T) { }) } } + +func Test_MarshalAndCatenateSSHPubKeys(t *testing.T) { + type fields struct { + keyFiles []string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "test1", + fields: fields{ + keyFiles: []string{"test_data/keys"}, + }, + want: "\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCs4Qv1yrBk6ygt+o7J4sUcYv+WfDjdAyABDoinOt3PgSmCcVqqAP2qS8UtTnMNuy93Orp6+/R/7/R3O5xdY6I4YViK3WVlKTAUVm7vdeTKp9uq1tNeWgo7+J3baSbQ3INp85ScTfFvRzRCFkr/W97Wh6pTa7ysgkcPvc2/tXG2z36Mx7/TFBk3Q1LY3ByKLtGrC5JnVpMTrqrsCwcLEVHHEZ4z5R4FZED/lpz+wTNFnR/l9HA6yDkKYensHynx+guqYpYD6y4yEGY/LcUnwBg0zIlUhmOsvdmxWBz12Lp7EBiNjSwhnPfe+o3efLGGnjWUAa4TgO8Sa8PQP0pK/ZNd\" \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILKdXYzPIq8kHRJtDrh21wMVI76AnuPk7HDLeDteKN74\"", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + keys, err := LoadSSHPubKeysFromFiles(tt.fields.keyFiles) + if err != nil { + t.Errorf("failed to load keys: %v", err) + } + + got := MarshalAndCatenateSSHPubKeys(keys) + + if d := cmp.Diff(got, tt.want); d != "" { + t.Errorf("MarshalAndCatenateSSHPubKeys() = %s", d) + } + }) + } +} diff --git a/nodes/srl/test_data/keys b/utils/test_data/keys similarity index 100% rename from nodes/srl/test_data/keys rename to utils/test_data/keys