Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added SSH key provisioning for SR OS #1706

Merged
merged 10 commits into from
Nov 13, 2023
15 changes: 10 additions & 5 deletions clab/sshconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,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 @@ -29,8 +30,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 All @@ -56,8 +60,9 @@ func (c *CLab) AddSSHConfig(topoPaths *types.TopoPaths) 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
12 changes: 12 additions & 0 deletions nodes/node_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"sort"
"strings"

log "github.com/sirupsen/logrus"
)

type Initializer func() Node
Expand Down Expand Up @@ -82,6 +84,16 @@ func (e *NodeRegistryEntry) Credentials() *Credentials {
return e.credentials
}

// GetMainKindName returns kind's prime kind name, which is the first element in kind names list.
func (e *NodeRegistryEntry) GetMainKindName() string {
if e == nil || len(e.nodeKindNames) == 0 {
log.Warn("here101")
return ""
}
log.Warn(e.nodeKindNames[0])
return e.nodeKindNames[0]
}

func newRegistryEntry(nodeKindNames []string, initFunction Initializer, credentials *Credentials) *NodeRegistryEntry {
return &NodeRegistryEntry{
nodeKindNames: nodeKindNames,
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 }}
44 changes: 37 additions & 7 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,10 +27,11 @@ 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 (
kindnames = []string{"vr-sros", "vr-nokia_sros"}
kindnames = []string{"sros", "vr-sros", "vr-nokia_sros"}
defaultCredentials = nodes.NewCredentials("admin", "admin")
)

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{}
}
hellt marked this conversation as resolved.
Show resolved Hide resolved