Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
81 changes: 81 additions & 0 deletions core/inventory_reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2020 Nokia
// Licensed under the BSD 3-Clause License.
// SPDX-License-Identifier: BSD-3-Clause

package core

import (
"fmt"
"os"

"gopkg.in/yaml.v2"
)

// AnsibleInventoryCredentials holds the credentials read from ansible inventory.
type AnsibleInventoryCredentials struct {
Username string
Password string
}

// AnsibleInventoryData represents the structure of the ansible-inventory.yml file
// for the purpose of reading credentials.
type AnsibleInventoryData struct {
All struct {
Children map[string]AnsibleInventoryKind `yaml:"children"`
} `yaml:"all"`
}

// AnsibleInventoryKind represents a node kind in the inventory.
type AnsibleInventoryKind struct {
Vars AnsibleInventoryVars `yaml:"vars,omitempty"`
Hosts map[string]AnsibleInventoryHost `yaml:"hosts,omitempty"`
}

// AnsibleInventoryVars represents the vars section for a kind.
type AnsibleInventoryVars struct {
User string `yaml:"ansible_user,omitempty"`
Password string `yaml:"ansible_password,omitempty"`
}

// AnsibleInventoryHost represents a host entry.
type AnsibleInventoryHost struct {
Host string `yaml:"ansible_host,omitempty"`
}

// ReadAnsibleInventoryCredentials reads the ansible-inventory.yml file and returns
// credentials for the specified node kind.
func ReadAnsibleInventoryCredentials(
inventoryPath, nodeKind string,
) (*AnsibleInventoryCredentials, error) {
// Read the inventory file
data, err := os.ReadFile(inventoryPath)
if err != nil {
return nil, fmt.Errorf("failed to read ansible inventory file: %w", err)
}

// Parse the YAML
var inventory AnsibleInventoryData
err = yaml.Unmarshal(data, &inventory)
if err != nil {
return nil, fmt.Errorf("failed to parse ansible inventory: %w", err)
}

// Look for the node kind in the inventory
kind, exists := inventory.All.Children[nodeKind]
if !exists {
return nil, fmt.Errorf("node kind %q not found in ansible inventory", nodeKind)
}

// Extract credentials
creds := &AnsibleInventoryCredentials{
Username: kind.Vars.User,
Password: kind.Vars.Password,
}

// Validate that we have credentials
if creds.Username == "" && creds.Password == "" {
return nil, fmt.Errorf("no credentials found for kind %q in ansible inventory", nodeKind)
}

return creds, nil
}
165 changes: 164 additions & 1 deletion core/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ package core

import (
"context"
"fmt"
"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"
)

// contextKey is a custom type for context keys to avoid collisions.
type contextKey string

const (
// InventoryCredsKey is the context key for the inventory credentials map.
InventoryCredsKey contextKey = "inventoryCredentials"
)

func (c *CLab) Save(
ctx context.Context,
) error {
Expand All @@ -17,6 +27,56 @@ func (c *CLab) Save(
return err
}

// Read ansible inventory to get credentials for all NETCONF-based nodes
inventoryPath := c.TopoPaths.AnsibleInventoryFileAbsPath()

// Load credentials into a map for all NETCONF-based node kinds
// These node kinds use NETCONF for save operations and need credentials
credsMap := make(map[string]*AnsibleInventoryCredentials)
netconfKinds := []string{
"nokia_sros", "vr-sros", "nokia_srsim", // Nokia SROS variants
"nokia_srlinux", "srl", // Nokia SRL (for consistency, though uses local CLI)
"c8000", // Cisco 8000
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are not conform with the schema and should not use deprecated kinds

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can keep only : "nokia_sros", "nokia_srsim"

"xrd", // Cisco XRd
"vr-vmx", "vr-veos", "vr-sros", "vr-xrv", "vr-xrv9k", "vr-vqfx", "vr-csr", "vr-nxos", // vrnetlab variants
"vr-ros", "vr-openbsd", "vr-freebsd", // other vrnetlab variants
}

for _, nodeKind := range netconfKinds {
// Check if this kind exists in topology
hasKind := false
for _, node := range c.Nodes {
if node.Config().Kind == nodeKind {
hasKind = true
break
}
}

if !hasKind {
continue
}

// Try to read credentials from inventory
creds, err := ReadAnsibleInventoryCredentials(inventoryPath, nodeKind)
if err != nil {
log.Debugf("Could not read credentials for kind %s from inventory, using defaults", nodeKind)
// Use default credentials from registry
if regEntry := c.Reg.Kind(nodeKind); regEntry != nil &&
regEntry.GetCredentials() != nil {
credsMap[nodeKind] = &AnsibleInventoryCredentials{
Username: regEntry.GetCredentials().GetUsername(),
Password: regEntry.GetCredentials().GetPassword(),
}
}
} else {
log.Infof("Using credentials from ansible-inventory.yml for %s nodes (user: %s)", nodeKind, creds.Username)
credsMap[nodeKind] = creds
}
}

// Add credentials map to context
ctx = context.WithValue(ctx, InventoryCredsKey, credsMap)

var wg sync.WaitGroup

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

nodeKind := node.Config().Kind

// For NETCONF-based nodes (except SRL which uses local CLI), intercept and save with custom credentials
// SROS variants
if nodeKind == "nokia_sros" || nodeKind == "vr-sros" || nodeKind == "nokia_srsim" {
creds := credsMap[nodeKind]
if creds != nil {
err := c.saveNetconfConfig(ctx, node, creds.Username, creds.Password, "nokia_sros")
if err != nil {
log.Errorf("Failed to save config for %s: %v", node.Config().ShortName, err)
}
return
}
}

// Cisco c8000
if nodeKind == "c8000" {
creds := credsMap[nodeKind]
if creds != nil {
err := c.saveNetconfConfig(ctx, node, creds.Username, creds.Password, "cisco_iosxe")
if err != nil {
log.Errorf("Failed to save config for %s: %v", node.Config().ShortName, err)
}
return
}
}

// Cisco XRd
if nodeKind == "xrd" {
creds := credsMap[nodeKind]
if creds != nil {
err := c.saveNetconfConfig(ctx, node, creds.Username, creds.Password, "cisco_iosxr")
if err != nil {
log.Errorf("Failed to save config for %s: %v", node.Config().ShortName, err)
}
return
}
}

// vrnetlab-based nodes (generic handling)
if isVrnetlabKind(nodeKind) {
creds := credsMap[nodeKind]
if creds != nil {
// vrnetlab nodes use their own SaveConfig which calls GetConfig
// We can't easily override this without modifying VRNode.SaveConfig
// So for now, let them use their default SaveConfig which reads from registry
// TODO: Could be enhanced to pass credentials to VRNode.SaveConfig
}
}

// For all other nodes (including SRL which uses local CLI), use default SaveConfig
err := node.SaveConfig(ctx)
if err != nil {
log.Errorf("err: %v", err)
log.Errorf("Failed to save config for %s: %v", node.Config().ShortName, err)
}
}(node)
}
Expand All @@ -36,3 +147,55 @@ func (c *CLab) Save(

return nil
}

// isVrnetlabKind checks if a node kind is a vrnetlab variant.
func isVrnetlabKind(kind string) bool {
vrnetlabKinds := []string{
"vr-vmx", "vr-veos", "vr-sros", "vr-xrv", "vr-xrv9k",
"vr-vqfx", "vr-csr", "vr-nxos", "vr-ros", "vr-openbsd", "vr-freebsd",
}
for _, vr := range vrnetlabKinds {
if kind == vr {
return true
}
}
return false
}

// saveNetconfConfig saves configuration using NETCONF with credentials from inventory.
// This handles the NETCONF-specific save logic 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
// IPv6 addresses need to be enclosed in brackets for netconf library
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 using the same format as the original SaveConfig methods
log.Info("Saved running configuration", "node", cfg.ShortName, "addr", cfg.ShortName)
return nil
}
Loading