Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 162 additions & 1 deletion core/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package core

import (
"context"
"fmt"
"os"
"sync"

"github.com/charmbracelet/log"
clablinks "github.com/srl-labs/containerlab/links"
clabnetconf "github.com/srl-labs/containerlab/netconf"
clabnodes "github.com/srl-labs/containerlab/nodes"
"gopkg.in/yaml.v2"
)

func (c *CLab) Save(
Expand All @@ -17,6 +21,35 @@ func (c *CLab) Save(
return err
}

// Load credentials from ansible-inventory.yml for Nokia SROS/SRSIM nodes only
credsMap := make(map[string]*NodeCredentials)

// Check if we have any Nokia SROS/SRSIM nodes
hasTargetNodes := false
for _, node := range c.Nodes {
nodeKind := node.Config().Kind
if nodeKind == "nokia_sros" || nodeKind == "nokia_srsim" {
hasTargetNodes = true
break
}
}

// Only read ansible-inventory if we have target nodes
if hasTargetNodes {
inventoryPath := c.TopoPaths.AnsibleInventoryFileAbsPath()
if data, err := os.ReadFile(inventoryPath); err == nil {
var inventoryYAML AnsibleInventoryYAML
if err := yaml.Unmarshal(data, &inventoryYAML); err == nil {
// Process both nokia_sros and nokia_srsim
for _, kind := range []string{"nokia_sros", "nokia_srsim"} {
if group, ok := inventoryYAML.All.Children[kind]; ok {
c.loadGroupCredentials(kind, group, credsMap)
}
}
}
}
}

var wg sync.WaitGroup

wg.Add(len(c.Nodes))
Expand All @@ -25,9 +58,30 @@ func (c *CLab) Save(
go func(node clabnodes.Node) {
defer wg.Done()

nodeKind := node.Config().Kind
nodeName := node.Config().ShortName

// For Nokia SROS/SRSIM nodes, use NETCONF with credentials from ansible-inventory.yml
if nodeKind == "nokia_sros" || nodeKind == "nokia_srsim" {
if creds, ok := credsMap[nodeName]; ok {
err := c.saveNetconfConfig(
ctx,
node,
creds.Username,
creds.Password,
"nokia_sros",
)
if err != nil {
log.Errorf("Failed to save config for %s: %v", nodeName, err)
}
return
}
}

// For all other nodes, use default SaveConfig behavior
err := node.SaveConfig(ctx)
if err != nil {
log.Errorf("err: %v", err)
log.Errorf("Failed to save config for %s: %v", nodeName, err)
}
}(node)
}
Expand All @@ -36,3 +90,110 @@ func (c *CLab) Save(

return nil
}

// loadGroupCredentials loads credentials for a specific node kind from ansible inventory.
func (c *CLab) loadGroupCredentials(
kind string,
group AnsibleInventoryGroup,
credsMap map[string]*NodeCredentials,
) {
// Group-level credentials (default for all hosts)
groupUser := group.Vars.AnsibleUser
groupPass := group.Vars.AnsiblePassword

if groupUser != "" && groupPass != "" {
log.Infof("Using credentials from ansible-inventory.yml for %s nodes (user: %s)", kind, groupUser)
}

for nodeName, hostVars := range group.Hosts {
// Host-level credentials override group-level
username := groupUser
password := groupPass

if hostVars.AnsibleUser != "" {
username = hostVars.AnsibleUser
}
if hostVars.AnsiblePassword != "" {
password = hostVars.AnsiblePassword
}

if username != "" && password != "" {
credsMap[nodeName] = &NodeCredentials{
Username: username,
Password: password,
}
if hostVars.AnsibleUser != "" || hostVars.AnsiblePassword != "" {
log.Debugf("Loaded host-specific credentials for %s node %s (user: %s)", kind, nodeName, username)
} else {
log.Debugf("Loaded group credentials for %s node %s", kind, nodeName)
}
}
}
}

// NodeCredentials holds username and password for a node.
type NodeCredentials struct {
Username string
Password string
}

// AnsibleInventoryYAML represents the structure of the generated ansible-inventory.yml file.
type AnsibleInventoryYAML struct {
All struct {
Children map[string]AnsibleInventoryGroup `yaml:"children"`
} `yaml:"all"`
}

// AnsibleInventoryGroup represents a group in the ansible inventory YAML.
type AnsibleInventoryGroup struct {
Vars AnsibleInventoryVars `yaml:"vars"`
Hosts map[string]AnsibleInventoryHost `yaml:"hosts"`
}

// AnsibleInventoryVars represents the vars section in ansible inventory YAML.
type AnsibleInventoryVars struct {
AnsibleUser string `yaml:"ansible_user"`
AnsiblePassword string `yaml:"ansible_password"`
}

// AnsibleInventoryHost represents a host entry in ansible inventory YAML.
type AnsibleInventoryHost struct {
AnsibleHost string `yaml:"ansible_host"`
AnsibleUser string `yaml:"ansible_user"`
AnsiblePassword string `yaml:"ansible_password"`
}

// saveNetconfConfig saves configuration using NETCONF with custom credentials.
func (c *CLab) saveNetconfConfig(
ctx context.Context,
node clabnodes.Node,
username, password, platform string,
) error {
cfg := node.Config()

// Use management IPv4 address for NETCONF connection
addr := cfg.MgmtIPv4Address
if addr == "" {
addr = cfg.MgmtIPv6Address
}
if addr == "" {
addr = cfg.Fqdn
}
if addr == "" {
addr = cfg.LongName
}

// Call netconf SaveRunningConfig with custom credentials
err := clabnetconf.SaveRunningConfig(
fmt.Sprintf("[%s]", addr),
username,
password,
platform,
)
if err != nil {
return fmt.Errorf("failed to save config via NETCONF: %w", err)
}

log.Infof("saved %s running configuration to startup configuration file\n", cfg.ShortName)
return nil
}