Skip to content

Commit

Permalink
Envsubst for startup configs (#2471)
Browse files Browse the repository at this point in the history
* envsubst for startup configs

* correct vars

* squash linter errors

* template and envsubst partials for sros

* added sros templating doc
  • Loading branch information
hellt authored Feb 20, 2025
1 parent 81b91fc commit d54989c
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 93 deletions.
15 changes: 7 additions & 8 deletions clab/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func (c *CLab) createNodeCfg(nodeName string, nodeDef *types.NodeDefinition, idx

// processStartupConfig processes the raw path of the startup-config as it is defined in the topology file.
// It handles remote files, local files and embedded configs.
// Returns an absolute path to the startup-config file.
// As a result the `nodeCfg.StartupConfig` will be set to an absPath of the startup config file.
func (c *CLab) processStartupConfig(nodeCfg *types.NodeConfig) error {
// replace __clabNodeName__ magic var in startup-config path with node short name
r := c.magicVarReplacer(nodeCfg.ShortName)
Expand Down Expand Up @@ -302,7 +302,7 @@ func (c *CLab) processStartupConfig(nodeCfg *types.NodeConfig) error {
return err
}

// adjust the nodeconfig by pointing startup-config to the local downloaded file
// adjust the NodeConfig by pointing startup-config to the local downloaded file
p = absDestFile
}
}
Expand Down Expand Up @@ -331,10 +331,8 @@ func (c *CLab) checkTopologyDefinition(ctx context.Context) error {
if err = c.verifyDuplicateAddresses(); err != nil {
return err
}
if err = c.verifyContainersUniqueness(ctx); err != nil {
return err
}
return nil

return c.verifyContainersUniqueness(ctx)
}

// verifyRootNetNSLinks makes sure, that there will be no overlap in
Expand Down Expand Up @@ -376,7 +374,8 @@ func (c *CLab) verifyRootNetNSLinks() error {
// appear only once.
func (c *CLab) verifyLinks(ctx context.Context) error {
var err error
verificationErrors := []error{}
var verificationErrors []error

for _, e := range c.Endpoints {
err = e.Verify(ctx, c.globalRuntime().Config().VerifyLinkParams)
if err != nil {
Expand Down Expand Up @@ -468,7 +467,7 @@ func (c *CLab) verifyContainersUniqueness(ctx context.Context) error {
return nil
}

dups := []string{}
var dups []string
for _, n := range c.Nodes {
if n.Config().SkipUniquenessCheck {
continue
Expand Down
11 changes: 8 additions & 3 deletions docs/manual/kinds/vr-sros.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,14 @@ Nokia SR OS nodes come up with a basic "blank" configuration where only the card

#### User-defined config

SR OS nodes launched with hellt/vrnetlab come up with some basic configuration that configures the management interfaces, line cards, mdas and power modules. This configuration is applied right after the node is booted.
SR OS nodes launched with [hellt/vrnetlab](https://github.com/hellt/vrnetlab) come up with some basic configuration that configures the management interfaces, line cards, mdas and power modules. This configuration is applied right after the node is booted.

Since this initial configuration is meant to provide a bare minimum configuration to make the node operational, users will likely want to apply their own configuration to the node to enable some features or to configure some interfaces. This can be done by providing a user-defined configuration file using [`startup-config`](../nodes.md#startup-config) property of the node/kind.

/// tip
Configuration text can contain Go template logic as well as make use of [environment variables](../topo-def-file.md#environment-variables) allowing for runtime customization of the configuration.
///

##### Full startup-config

When a user provides a path to a file that has a complete configuration for the node, containerlab will copy that file to the lab directory for that specific node under the `/tftpboot/config.txt` name and mount that dir to the container. This will result in this config to act as a startup-config for the node:
Expand All @@ -232,8 +236,9 @@ topology:
startup-config: myconfig.txt
```

!!!note
With the above configuration, the node will boot with the configuration specified in `myconfig.txt`, no other configuration will be applied. You have to provision interfaces, cards, power-shelves, etc. yourself.
/// note
With the above configuration, the node will boot with the configuration specified in `myconfig.txt`, no other configuration will be applied. You have to provision interfaces, cards, power-shelves, etc. yourself.
///

##### Partial startup-config

Expand Down
127 changes: 66 additions & 61 deletions docs/manual/nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ For all Network OS kinds, it's possible to provide startup configuration that th
1. As a path to a file that is available on the host machine and contains the config blob that the node understands.
2. As an embedded config blob that is provided as a multiline string.

The environment variables in the config files will be substituted before the config is applied. This gives the user the ability to customize the config at runtime based on the present environment variables.
The env vars are defined as explained in the [Topology Definition section](topo-def-file.md#environment-variables).

#### path to a startup-config file

When a path to a startup-config file is provided, containerlab either mounts the file to the container by a path that NOS expects to have its startup-config file, or it will apply the config via using the NOS-dependent method.
Expand All @@ -138,72 +141,74 @@ topology:

Check the particular kind documentation to see if the startup-config is supported and how it is applied.

???info "Startup-config path variable"
By default, the startup-config references are either provided as an absolute or a relative (to the current working dir) path to the file to be used.

Consider a two-node lab `mylab.clab.yml` with seed configs that the user may wish to use in their lab. A user could create a directory for such files similar to this:

```
.
├── cfgs
│   ├── node1.partial.cfg
│   └── node2.partial.cfg
└── mylab.clab.yml

2 directories, 3 files
```

Then to leverage these configs, the node could be configured with startup-config references like this:

```yaml
name: mylab
topology:
nodes:
node1:
startup-config: cfgs/node1.partial.cfg
node2:
startup-config: cfgs/node2.partial.cfg
```
/// details | Startup-config path variable
By default, the startup-config references are either provided as an absolute or a relative (to the current working dir) path to the file to be used.

Consider a two-node lab `mylab.clab.yml` with seed configs that the user may wish to use in their lab. A user could create a directory for such files similar to this:

```
.
├── cfgs
│   ├── node1.partial.cfg
│   └── node2.partial.cfg
└── mylab.clab.yml

2 directories, 3 files
```
Then to leverage these configs, the node could be configured with startup-config references like this:
```yaml
name: mylab
topology:
nodes:
node1:
startup-config: cfgs/node1.partial.cfg
node2:
startup-config: cfgs/node2.partial.cfg
```

while this configuration is correct, it might be considered verbose as the number of nodes grows. To remove this verbosity, the users can use a special variable `__clabNodeName__` in their startup-config paths. This variable will expand to the node-name for the parent node that the startup-config reference falls under.

```yaml
name: mylab
topology:
nodes:
node1:
startup-config: cfg/__clabNodeName__.partial.cfg
node2:
startup-config: cfgs/__clabNodeName__.partial.cfg
```
while this configuration is correct, it might be considered verbose as the number of nodes grows. To remove this verbosity, the users can use a special variable `__clabNodeName__` in their startup-config paths. This variable will expand to the node-name for the parent node that the startup-config reference falls under.
The `__clabNodeName__` variable can also be used in the kind and default sections of the config. Using the same directory structure from the example above, the following shows how to use the magic variable for a kind.

```yaml
name: mylab
topology:
nodes:
node1:
startup-config: cfg/__clabNodeName__.partial.cfg
node2:
startup-config: cfgs/__clabNodeName__.partial.cfg
```

The `__clabNodeName__` variable can also be used in the kind and default sections of the config. Using the same directory structure from the example above, the following shows how to use the magic variable for a kind.
```yaml
name: mylab
topology:
defaults:
kind: nokia_srlinux
kinds:
nokia_srlinux:
startup-config: cfgs/__clabNodeName__.partial.cfg
nodes:
node1:
node2:
```

```yaml
name: mylab
topology:
defaults:
kind: nokia_srlinux
kinds:
nokia_srlinux:
startup-config: cfgs/__clabNodeName__.partial.cfg
nodes:
node1:
node2:
```
The following example shows how one would do it using defaults.

The following example shows how one would do it using defaults.
```yaml
name: mylab
topology:
defaults:
kind: nokia_srlinux
startup-config: cfgs/__clabNodeName__.partial.cfg
nodes:
node1:
node2:
```

```yaml
name: mylab
topology:
defaults:
kind: nokia_srlinux
startup-config: cfgs/__clabNodeName__.partial.cfg
nodes:
node1:
node2:
```
///

#### embedded startup-config

Expand Down
10 changes: 10 additions & 0 deletions docs/stylesheets/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
fill: var(--logo-outline-color);
}

.md-grid {
/* a slightly increased width for the main content ~1300px */
max-width: 65rem;
}

.md-post--excerpt {
/* a slightly increased width for the main content ~1300px */
max-width: 40rem;
}

.mdx-content__footer {
margin-top: 20px;
text-align: center;
Expand Down
22 changes: 7 additions & 15 deletions nodes/default_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
package nodes

import (
"bytes"
"context"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"text/template"

"github.com/charmbracelet/log"
"github.com/containernetworking/plugins/pkg/ns"
Expand Down Expand Up @@ -349,8 +348,8 @@ func (d *DefaultNode) VerifyStartupConfig(topoDir string) error {
}

// GenerateConfig generates configuration for the nodes
// out of the template based on the node configuration and saves the result to dst.
func (d *DefaultNode) GenerateConfig(dst, templ string) error {
// out of the template `t` based on the node configuration and saves the result to dst.
func (d *DefaultNode) GenerateConfig(dst, t string) error {
// If the config file is already present in the node dir
// we do not regenerate the config unless EnforceStartupConfig is explicitly set to true and startup-config points to a file
// this will persist the changes that users make to a running config when booted from some startup config
Expand All @@ -365,27 +364,20 @@ func (d *DefaultNode) GenerateConfig(dst, templ string) error {
return nil
}

log.Debugf("generating config for node %s from file %s", d.Cfg.ShortName, d.Cfg.StartupConfig)
log.Debug("Generating config", "node", d.Cfg.ShortName, "file", d.Cfg.StartupConfig)

tpl, err := template.New(filepath.Base(d.Cfg.StartupConfig)).Funcs(utils.TemplateFuncs).Parse(templ)
cfgBuf, err := utils.SubstituteEnvsAndTemplate(strings.NewReader(t), d.Cfg)
if err != nil {
return err
}

dstBytes := new(bytes.Buffer)

err = tpl.Execute(dstBytes, d.Cfg)
if err != nil {
return err
}
log.Debugf("node '%s' generated config: %s", d.Cfg.ShortName, dstBytes.String())
log.Debugf("node '%s' generated config: %s", d.Cfg.ShortName, cfgBuf.String())

f, err := os.Create(dst)
if err != nil {
return err
}

_, err = f.Write(dstBytes.Bytes())
_, err = f.Write(cfgBuf.Bytes())
if err != nil {
f.Close()
return err
Expand Down
9 changes: 7 additions & 2 deletions nodes/srl/srl.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ func (s *srl) createSRLFiles() error {
var cfgTemplate string
cfgPath := filepath.Join(s.Cfg.LabDir, "config", "config.json")
if s.Cfg.StartupConfig != "" {
log.Debugf("Reading startup-config %s", s.Cfg.StartupConfig)
log.Debug("Reading startup-config", "file", s.Cfg.StartupConfig)

c, err := os.ReadFile(s.Cfg.StartupConfig)
if err != nil {
Expand All @@ -461,7 +461,12 @@ func (s *srl) createSRLFiles() error {
log.Debugf("startup-config passed to %s is in the CLI format. Will apply it in post-deploy stage",
s.Cfg.ShortName)

s.startupCliCfg = c
cBuf, err := utils.SubstituteEnvsAndTemplate(bytes.NewReader(c), s.Cfg)
if err != nil {
return err
}

s.startupCliCfg = cBuf.Bytes()

// no need to generate and mount startup-config passed in a CLI format
// as we will apply it over the top of a default config in the post deploy stage
Expand Down
8 changes: 5 additions & 3 deletions nodes/vr_sros/vr-sros.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,12 @@ func (s *vrSROS) applyPartialConfig(ctx context.Context, addr, platformName,
var err error
var d *network.Driver

configContent, err := io.ReadAll(config)
configContent, err := utils.SubstituteEnvsAndTemplate(config, s.Cfg)
if err != nil {
return err
}

configContentStr := string(configContent)
configContentStr := configContent.String()

// check file contains content, otherwise exit early
if strings.TrimSpace(configContentStr) == "" {
Expand All @@ -288,7 +288,9 @@ func (s *vrSROS) applyPartialConfig(ctx context.Context, addr, platformName,
case <-ctx.Done():
return fmt.Errorf("%s: timed out waiting to accept configs", addr)
default:
sl := log.StandardLog()
sl := log.StandardLog(log.StandardLogOptions{
ForceLevel: log.DebugLevel,
})
li, err := scraplilogging.NewInstance(
scraplilogging.WithLevel("debug"),
scraplilogging.WithLogger(sl.Print))
Expand Down
Loading

0 comments on commit d54989c

Please sign in to comment.