Skip to content

Commit

Permalink
Added SSH key provisioning for SR OS (#1706)
Browse files Browse the repository at this point in the history
* added key provisioning for sros

Co-authored-by: Mathis Bramkamp <[email protected]>

* use O(n) when filtering pub keys

* use slice pointers

Co-authored-by: steiler <[email protected]>

* additional log message to indicate booting period

* added main kind names concept and tune ssh config for sros nodes

* Introduce KindSpecifics

* use ssh config struct instead of kindSpecifics

KindSpecifics seems to not work when we will have ssh config specific to a particular node version. So it is a bit over generalizing

* remove main kind and nokia_sros kind name

for now. will be addressed in a separate PR

---------

Co-authored-by: Mathis Bramkamp <[email protected]>
Co-authored-by: steiler <[email protected]>
  • Loading branch information
3 people authored Nov 13, 2023
1 parent 7c84208 commit 4e4748c
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 12 deletions.
15 changes: 10 additions & 5 deletions clab/sshconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ type SSHConfigTmpl struct {
// SSHConfigNodeTmpl represents values for a single node
// in the sshconfig template.
type SSHConfigNodeTmpl struct {
Name string
Username string
Name string
Username string
SSHConfig *types.SSHConfig
}

// tmplSshConfig is the SSH config template.
Expand All @@ -32,8 +33,11 @@ Host {{ .Name }}
{{- if ne .Username ""}}
User {{ .Username }}
{{- end }}
StrictHostKeyChecking=no
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
{{- if ne .SSHConfig.PubkeyAuthentication "" }}
PubkeyAuthentication={{ .SSHConfig.PubkeyAuthentication.String }}
{{- end }}
{{ end }}`

// RemoveSSHConfig removes the lab specific ssh config file
Expand Down Expand Up @@ -65,8 +69,9 @@ func (c *CLab) AddSSHConfig() error {
// the kind registered Username
NodeRegistryEntry := c.Reg.Kind(n.Config().Kind)
nodeData := SSHConfigNodeTmpl{
Name: n.Config().LongName,
Username: NodeRegistryEntry.Credentials().GetUsername(),
Name: n.Config().LongName,
Username: NodeRegistryEntry.Credentials().GetUsername(),
SSHConfig: n.GetSSHConfig(),
}
tmpl.Nodes = append(tmpl.Nodes, nodeData)
}
Expand Down
14 changes: 14 additions & 0 deletions mocks/mocknodes/node.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions nodes/default_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type DefaultNode struct {
Mgmt *types.MgmtNet
Runtime runtime.ContainerRuntime
HostRequirements *types.HostRequirements
// SSHConfig is the SSH client configuration that a clab node requires.
SSHConfig *types.SSHConfig
// Indicates that the node should not start without no license file defined
LicensePolicy types.LicensePolicy
// OverwriteNode stores the interface used to overwrite methods defined
Expand All @@ -57,6 +59,7 @@ func NewDefaultNode(n NodeOverwrites) *DefaultNode {
HostRequirements: types.NewHostRequirements(),
OverwriteNode: n,
LicensePolicy: types.LicensePolicyNone,
SSHConfig: types.NewSSHConfig(),
}

return dn
Expand Down Expand Up @@ -509,3 +512,7 @@ func (d *DefaultNode) SetState(s state.NodeState) {
defer d.statemutex.Unlock()
d.state = s
}

func (d *DefaultNode) GetSSHConfig() *types.SSHConfig {
return d.SSHConfig
}
1 change: 0 additions & 1 deletion nodes/linux/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ type linux struct {
func (n *linux) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error {
// Init DefaultNode
n.DefaultNode = *nodes.NewDefaultNode(n)

n.Cfg = cfg
for _, o := range opts {
o(n)
Expand Down
1 change: 1 addition & 0 deletions nodes/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ type Node interface {
ExecFunction(func(ns.NetNS) error) error
GetState() state.NodeState
SetState(state.NodeState)
GetSSHConfig() *types.SSHConfig
}

type NodeOption func(Node)
Expand Down
86 changes: 86 additions & 0 deletions nodes/vr_sros/sshKey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package vr_sros

import (
"bytes"
"context"
_ "embed"
"strings"
"text/template"

"github.com/hairyhenderson/gomplate/v3"
"github.com/hairyhenderson/gomplate/v3/data"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)

// importing Default Config template at compile time
//
//go:embed ssh_keys.go.tpl
var SROSSSHKeysTemplate string

// mapSSHPubKeys goes over s.sshPubKeys and puts the supported keys to the corresponding
// slices associated with the supported SSH key algorithms.
// supportedSSHKeyAlgos key is a SSH key algorithm and the value is a pointer to the slice
// that is used to store the keys of the corresponding algorithm family.
// Two slices are used to store RSA and ECDSA keys separately.
// The slices are modified in place by reference, so no return values are needed.
func (s *vrSROS) mapSSHPubKeys(supportedSSHKeyAlgos map[string]*[]string) {
for _, k := range s.sshPubKeys {
sshKeys, ok := supportedSSHKeyAlgos[k.Type()]
if !ok {
log.Debugf("unsupported SSH Key Algo %q, skipping key", k.Type())
continue
}

// extract the fields
// <keytype> <key> <comment>
keyFields := strings.Fields(string(ssh.MarshalAuthorizedKey(k)))

*sshKeys = append(*sshKeys, keyFields[1])
}
}

// SROSTemplateData holds ssh keys for template generation.
type SROSTemplateData struct {
SSHPubKeysRSA []string
SSHPubKeysECDSA []string
}

// configureSSHPublicKeys cofigures public keys extracted from clab host
// on SR OS node using SSH.
func (s *vrSROS) configureSSHPublicKeys(
ctx context.Context, addr, platformName,
username, password string, pubKeys []ssh.PublicKey) error {
tplData := SROSTemplateData{}

// a map of supported SSH key algorithms and the template slices
// the keys should be added to.
// In mapSSHPubKeys we map supported SSH key algorithms to the template slices.
supportedSSHKeyAlgos := map[string]*[]string{
ssh.KeyAlgoRSA: &tplData.SSHPubKeysRSA,
ssh.KeyAlgoECDSA521: &tplData.SSHPubKeysECDSA,
ssh.KeyAlgoECDSA384: &tplData.SSHPubKeysECDSA,
ssh.KeyAlgoECDSA256: &tplData.SSHPubKeysECDSA,
}

s.mapSSHPubKeys(supportedSSHKeyAlgos)

t, err := template.New("SSHKeys").Funcs(
gomplate.CreateFuncs(context.Background(), new(data.Data))).Parse(SROSSSHKeysTemplate)
if err != nil {
return err
}

buf := new(bytes.Buffer)
err = t.Execute(buf, tplData)
if err != nil {
return err
}

err = s.applyPartialConfig(ctx, s.Cfg.MgmtIPv4Address, scrapliPlatformName,
defaultCredentials.GetUsername(), defaultCredentials.GetPassword(),
buf,
)

return err
}
12 changes: 12 additions & 0 deletions nodes/vr_sros/ssh_keys.go.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{{/* this is a template for sros public key config for ssh admin user access */}}

{{/* to enable long list of keys from agent where the configured key may not be in the default first three keys */}}
/configure system security user-params attempts count 64

{{ range $index, $key := .SSHPubKeysRSA }}
/configure system security user-params local-user user "admin" public-keys rsa rsa-key {{ add $index 1 }} key-value {{ $key }}
{{ end }}

{{ range $index, $key := .SSHPubKeysECDSA }}
/configure system security user-params local-user user "admin" public-keys ecdsa ecdsa-key {{ add $index 1 }} key-value {{ $key }}
{{ end }}
42 changes: 36 additions & 6 deletions nodes/vr_sros/vr-sros.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package vr_sros
import (
"context"
"fmt"
"io"
"os"
"path"
"path/filepath"
"regexp"
Expand All @@ -25,6 +27,7 @@ import (
"github.com/srl-labs/containerlab/nodes"
"github.com/srl-labs/containerlab/types"
"github.com/srl-labs/containerlab/utils"
"golang.org/x/crypto/ssh"
)

var (
Expand All @@ -49,6 +52,8 @@ func Register(r *nodes.NodeRegistry) {

type vrSROS struct {
nodes.DefaultNode
// SSH public keys extracted from the clab host
sshPubKeys []ssh.PublicKey
}

func (s *vrSROS) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error {
Expand All @@ -57,6 +62,9 @@ func (s *vrSROS) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error {
// set virtualization requirement
s.HostRequirements.VirtRequired = true
s.LicensePolicy = types.LicensePolicyWarn
// SR OS requires unbound pubkey authentication mode until this is
// gets fixed in later SR OS relase.
s.SSHConfig.PubkeyAuthentication = types.PubkeyAuthValueUnbound

s.Cfg = cfg
for _, o := range opts {
Expand Down Expand Up @@ -95,19 +103,28 @@ func (s *vrSROS) PreDeploy(_ context.Context, params *nodes.PreDeployParams) err
if err != nil {
return nil
}

// store public keys extracted from clab host
s.sshPubKeys = params.SSHPubKeys

return createVrSROSFiles(s)
}

func (s *vrSROS) PostDeploy(ctx context.Context, _ *nodes.PostDeployParams) error {
if isPartialConfigFile(s.Cfg.StartupConfig) {
log.Infof("Waiting for %s to boot and apply config from %s", s.Cfg.LongName, s.Cfg.StartupConfig)
log.Infof("%s: applying config from %s", s.Cfg.LongName, s.Cfg.StartupConfig)

ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()

err := s.applyPartialConfig(ctx, s.Cfg.MgmtIPv4Address, scrapliPlatformName,
r, err := os.Open(s.Cfg.StartupConfig)
if err != nil {
return err
}

err = s.applyPartialConfig(ctx, s.Cfg.MgmtIPv4Address, scrapliPlatformName,
defaultCredentials.GetUsername(), defaultCredentials.GetPassword(),
s.Cfg.StartupConfig,
r,
)
if err != nil {
return err
Expand All @@ -116,6 +133,16 @@ func (s *vrSROS) PostDeploy(ctx context.Context, _ *nodes.PostDeployParams) erro
log.Infof("%s: configuration applied", s.Cfg.LongName)
}

if len(s.sshPubKeys) > 0 {
err := s.configureSSHPublicKeys(ctx, s.Cfg.MgmtIPv4Address, scrapliPlatformName,
defaultCredentials.GetUsername(), defaultCredentials.GetPassword(),
s.sshPubKeys,
)
if err != nil {
return err
}
}

return nil
}

Expand Down Expand Up @@ -188,11 +215,11 @@ func (s *vrSROS) isHealthy(ctx context.Context) bool {
}

// applyPartialConfig applies partial configuration to the SR OS.
func (s *vrSROS) applyPartialConfig(ctx context.Context, addr, platformName, username, password string, configFile string) error {
func (s *vrSROS) applyPartialConfig(ctx context.Context, addr, platformName, username, password string, config io.Reader) error {
var err error
var d *network.Driver

configContent, err := utils.ReadFileContent(configFile)
configContent, err := io.ReadAll(config)
if err != nil {
return err
}
Expand All @@ -202,6 +229,7 @@ func (s *vrSROS) applyPartialConfig(ctx context.Context, addr, platformName, use
return nil
}

log.Infof("Waiting for %[1]s to be ready. This may take a while. Monitor boot log with `sudo docker logs -f %[1]s`", s.Cfg.LongName)
for loop := true; loop; {
if !s.isHealthy(ctx) {
time.Sleep(5 * time.Second) // cool-off period
Expand Down Expand Up @@ -249,8 +277,10 @@ func (s *vrSROS) applyPartialConfig(ctx context.Context, addr, platformName, use
}
}
}
// converting byte slice to newline delimited string slice
cfgs := strings.Split(string(configContent), "\n")

mr, err := d.SendConfigsFromFile(configFile)
mr, err := d.SendConfigs(cfgs)
if err != nil || mr.Failed != nil {
return fmt.Errorf("failed to apply config; error: %+v %+v", err, mr.Failed)
}
Expand Down
23 changes: 23 additions & 0 deletions types/ssh_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package types

type PubkeyAuthValue string

const (
PubkeyAuthValueYes PubkeyAuthValue = "yes"
PubkeyAuthValueNo PubkeyAuthValue = "no"
PubkeyAuthValueHostBound PubkeyAuthValue = "host-bound"
PubkeyAuthValueUnbound PubkeyAuthValue = "unbound"
)

func (p PubkeyAuthValue) String() string {
return string(p)
}

// SSHConfig is the SSH client configuration that a clab node requires.
type SSHConfig struct {
PubkeyAuthentication PubkeyAuthValue
}

func NewSSHConfig() *SSHConfig {
return &SSHConfig{}
}

0 comments on commit 4e4748c

Please sign in to comment.