Skip to content

Commit

Permalink
Generate SSH config (#1660)
Browse files Browse the repository at this point in the history
* Generate SSH config that:

- Set the Usernaem for the nodes
- Set StrictHostKeyChecking=no
- Set UserKnownHostsFile=/dev/null

* improve comments

* align field names

* align template naming

* deploy->add renaming

* remove hostname template entry and use os.Create

* ssh config template changes

make Username optional
silence ssh config removal not exists error

* source ssh config path from TopoPaths

* comment

* added doc entry

---------

Co-authored-by: Roman Dodin <dodin.roman@gmail.com>
  • Loading branch information
steiler and hellt authored Oct 20, 2023
1 parent 591f858 commit ce64f6d
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 5 deletions.
82 changes: 82 additions & 0 deletions clab/sshconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package clab

import (
"os"
"text/template"

"github.com/srl-labs/containerlab/types"
)

// SSHConfigTmpl is the top-level data structure for the
// sshconfig template.
type SSHConfigTmpl struct {
Nodes []SSHConfigNodeTmpl
TopologyName string
}

// SSHConfigNodeTmpl represents values for a single node
// in the sshconfig template.
type SSHConfigNodeTmpl struct {
Name string
Username string
}

// tmplSshConfig is the SSH config template.
const tmplSshConfig = `# Containerlab SSH Config for the {{ .TopologyName }} lab
{{- range .Nodes }}
Host {{ .Name }}
{{- if ne .Username ""}}
User {{ .Username }}
{{- end }}
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
{{ end }}`

// RemoveSSHConfig removes the lab specific ssh config file
func (c *CLab) RemoveSSHConfig(topoPaths *types.TopoPaths) error {
err := os.Remove(topoPaths.SSHConfigPath())
// if there is an error, thats not "Not Exists", then return it
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}

// AddSSHConfig adds the lab specific ssh config file.
func (c *CLab) AddSSHConfig(topoPaths *types.TopoPaths) error {
tmpl := &SSHConfigTmpl{
TopologyName: c.Config.Name,
Nodes: make([]SSHConfigNodeTmpl, 0, len(c.Nodes)),
}

// add the data for all nodes to the template input
for _, n := range c.Nodes {
// get the Kind from the KindRegistry and and extract
// the kind registered Username
NodeRegistryEntry := c.Reg.Kind(n.Config().Kind)
nodeData := SSHConfigNodeTmpl{
Name: n.Config().LongName,
Username: NodeRegistryEntry.Credentials().GetUsername(),
}
tmpl.Nodes = append(tmpl.Nodes, nodeData)
}

t, err := template.New("sshconfig").Parse(tmplSshConfig)
if err != nil {
return err
}

f, err := os.Create(topoPaths.SSHConfigPath())
if err != nil {
return err
}
defer f.Close()

err = t.Execute(f, tmpl)
if err != nil {
return err
}

return nil
}
6 changes: 6 additions & 0 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ func deployFn(_ *cobra.Command, _ []string) error {
log.Errorf("failed to create hosts file: %v", err)
}

log.Info("Adding ssh config for containerlab nodes")
err = c.AddSSHConfig(c.TopoPaths)
if err != nil {
log.Errorf("failed to create ssh config file: %v", err)
}

// execute commands specified for nodes with `exec` node parameter
execCollection := exec.NewExecCollection()
for _, n := range c.Nodes {
Expand Down
6 changes: 6 additions & 0 deletions cmd/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ func destroyLab(ctx context.Context, c *clab.CLab) (err error) {
return fmt.Errorf("error while trying to clean up the hosts file: %w", err)
}

log.Info("Removing ssh config for containerlab nodes")
err = c.RemoveSSHConfig(c.TopoPaths)
if err != nil {
log.Errorf("failed to remove ssh config file: %v", err)
}

// delete lab management network
if c.Config.Mgmt.Network != "bridge" && !keepMgmtNet {
log.Debugf("Calling DeleteNet method. *CLab.Config.Mgmt value is: %+v", c.Config.Mgmt)
Expand Down
54 changes: 49 additions & 5 deletions docs/manual/inventory.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
To accommodate for smooth transition from lab deployment to subsequent automation activities, containerlab generates inventory files for different automation tools.

## Ansible

Ansible inventory is generated automatically for every lab. The inventory file can be found in the lab directory under the `ansible-inventory.yml` name.

Lab nodes are grouped under their kinds in the inventory so that the users can selectively choose the right group of nodes in the playbooks.
Expand Down Expand Up @@ -47,6 +48,7 @@ Lab nodes are grouped under their kinds in the inventory so that the users can s
```

## Removing `ansible_host` var

If you want to use a plugin[^1] that doesn't play well with the `ansible_host` variable injected by containerlab in the inventory file, you can leverage the `ansible-no-host-var` label. The label can be set on per-node, kind, or default levels; if set, containerlab will not generate the `ansible_host` variable in the inventory for the nodes with that label.
Note that without the `ansible_host` variable, the connection plugin will use the `inventory_hostname` and resolve the name accordingly if network reachability is needed.

Expand All @@ -72,6 +74,7 @@ Note that without the `ansible_host` variable, the connection plugin will use th
```

## User-defined groups

Users can enforce custom grouping of nodes in the inventory by adding the `ansible-inventory` label to the node definition:

```yaml
Expand Down Expand Up @@ -109,16 +112,17 @@ As a result of this configuration, the generated inventory will look like this:
```
## Topology Data
Every time a user runs a `deploy` command, containerlab automatically exports information about the topology into `topology-data.json` file in the lab directory. Schema of exported data is determined based on a Go template specified in `--export-template` parameter, or a default template `/etc/containerlab/templates/export/auto.tmpl`, if the parameter is not provided.

Containerlab internal data that is submitted for export via the template, has the following structure:

```golang
type TopologyExport struct {
Name string `json:"name"` // Containerlab topology name
Type string `json:"type"` // Always 'clab'
Clab *CLab `json:"clab,omitempty"` // Data parsed from a topology definitions yaml file
NodeConfigs map[string]*types.NodeConfig `json:"nodeconfigs,omitempty"` // Definitions of nodes expanded with dynamically created data
Name string `json:"name"` // Containerlab topology name
Type string `json:"type"` // Always 'clab'
Clab *CLab `json:"clab,omitempty"` // Data parsed from a topology definitions yaml file
NodeConfigs map[string]*types.NodeConfig `json:"nodeconfigs,omitempty"` // Definitions of nodes expanded with dynamically created data
}
```

Expand Down Expand Up @@ -238,4 +242,44 @@ Example of exported data when using default `auto.tmpl` template:
}
```

[^1]: For example [Ansible Docker connection](https://docs.ansible.com/ansible/latest/collections/community/docker/docker_connection.html) plugin.
## SSH Config

To simplify SSH access to the nodes started by Containerlab an SSH config file is generated per each deployed lab. The config file instructs SSH clients to not warn users about the changed host keys and also sets the username to the one known by Containerlab:

```title="/etc/ssh/ssh_config.d/clab-<lab-name>.conf"
# Containerlab SSH Config for the srl lab
Host clab-srl-srl
User admin
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
```

Now you can SSH to the nodes without being prompted to accept the host key and even omitting the username.

```srl
❯ ssh clab-srl-srl
Warning: Permanently added 'clab-srl-srl' (ED25519) to the list of known hosts.
................................................................
: Welcome to Nokia SR Linux! :
: Open Network OS for the NetOps era. :
: :
: This is a freely distributed official container image. :
: Use it - Share it :
: :
: Get started: https://learn.srlinux.dev :
: Container: https://go.srlinux.dev/container-image :
: Docs: https://doc.srlinux.dev/23-7 :
: Rel. notes: https://doc.srlinux.dev/rn23-7-1 :
: YANG: https://yang.srlinux.dev/release/v23.7.1 :
: Discord: https://go.srlinux.dev/discord :
: Contact: https://go.srlinux.dev/contact-sales :
................................................................
Using configuration file(s): []
Welcome to the srlinux CLI.
Type 'help' (and press <ENTER>) if you need any help using this.
--{ running }--[ ]--
A:srl#
```

[^1]: For example [Ansible Docker connection](https://docs.ansible.com/ansible/latest/collections/community/docker/docker_connection.html) plugin.
6 changes: 6 additions & 0 deletions types/topo_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
CertFileSuffix = ".pem"
KeyFileSuffix = ".key"
CSRFileSuffix = ".csr"
sshConfigFilePathTmpl = "/etc/ssh/ssh_config.d/clab-%s.conf"
)

// clabTmpDir is the directory where clab stores temporary and/or downloaded files.
Expand Down Expand Up @@ -110,6 +111,11 @@ func (t *TopoPaths) SetExternalCaFiles(certFile, keyFile string) error {
return nil
}

// SSHConfigPath returns the topology dependent ssh config file name
func (t *TopoPaths) SSHConfigPath() string {
return fmt.Sprintf(sshConfigFilePathTmpl, t.topoName)
}

// TLSBaseDir returns the path of the TLS directory structure.
func (t *TopoPaths) TLSBaseDir() string {
return path.Join(t.labDir, tlsDir)
Expand Down

0 comments on commit ce64f6d

Please sign in to comment.