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

Nornir inventory file support #2495

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions clab/clab.go
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,14 @@ func (c *CLab) Deploy(ctx context.Context, options *DeployOptions) ([]runtime.Ge
return nil, err
}

// create an empty nornir simple inventory file that will get populated later
// we create it here first, so that bind mounts of nornir-simple-inventory.yml file could work
nornirSimpleInvFPath := c.TopoPaths.NornirSimpleInventoryFileAbsPath()
_, err = os.Create(nornirSimpleInvFPath)
if err != nil {
return nil, err
}

// in an similar fashion, create an empty topology data file
topoDataFPath := c.TopoPaths.TopoExportFile()
topoDataF, err := os.Create(topoDataFPath)
Expand Down
154 changes: 137 additions & 17 deletions clab/inventory.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ type AnsibleInventoryNode struct {
*types.NodeConfig
}

// KindProps is the kind properties structure used to generate the ansible inventory file.
type KindProps struct {
// AnsibleKindProps is the kind properties structure used to generate the ansible inventory file.
type AnsibleKindProps struct {
Username string
Password string
NetworkOS string
Expand All @@ -35,7 +35,7 @@ type KindProps struct {
// AnsibleInventory represents the data structure used to generate the ansible inventory file.
type AnsibleInventory struct {
// clab node kinds
Kinds map[string]*KindProps
Kinds map[string]*AnsibleKindProps
// clab nodes aggregated by their kind
Nodes map[string][]*AnsibleInventoryNode
// clab nodes aggregated by user-defined groups
Expand All @@ -44,24 +44,44 @@ type AnsibleInventory struct {

// GenerateInventories generate various inventory files and writes it to a lab location.
func (c *CLab) GenerateInventories() error {
// generate Ansible Inventory
ansibleInvFPath := c.TopoPaths.AnsibleInventoryFileAbsPath()
f, err := os.Create(ansibleInvFPath)

var err error
ansibleFile, err := os.Create(ansibleInvFPath)
if err != nil {
return err
}

err = c.generateAnsibleInventory(ansibleFile)
if err != nil {
return err
}

err = ansibleFile.Close()
if err != nil {
return err
}

err = c.generateAnsibleInventory(f)
// generate Nornir Simple Inventory
nornirSimpleInvFPath := c.TopoPaths.NornirSimpleInventoryFileAbsPath()
nornirFile, err := os.Create(nornirSimpleInvFPath)
if err != nil {
return err
}

return f.Close()
err = c.generateNornirSimpleInventory(nornirFile)
if err != nil {
return err
}

return nornirFile.Close()
}

// generateAnsibleInventory generates and writes ansible inventory file to w.
func (c *CLab) generateAnsibleInventory(w io.Writer) error {
inv := AnsibleInventory{
Kinds: make(map[string]*KindProps),
Kinds: make(map[string]*AnsibleKindProps),
Nodes: make(map[string][]*AnsibleInventoryNode),
Groups: make(map[string][]*AnsibleInventoryNode),
}
Expand All @@ -71,24 +91,24 @@ func (c *CLab) generateAnsibleInventory(w io.Writer) error {
NodeConfig: n.Config(),
}

// add kindprops to the inventory struct
// the kindProps is passed as a ref and is populated
// add AnsibleKindProps to the inventory struct
// the ansibleKindProps is passed as a ref and is populated
// down below
kindProps := &KindProps{}
inv.Kinds[n.Config().Kind] = kindProps
ansibleKindProps := &AnsibleKindProps{}
inv.Kinds[n.Config().Kind] = ansibleKindProps

// add username and password to kind properties
// assumption is that all nodes of the same kind have the same credentials
nodeRegEntry := c.Reg.Kind(n.Config().Kind)
if nodeRegEntry != nil {
kindProps.Username = nodeRegEntry.GetCredentials().GetUsername()
kindProps.Password = nodeRegEntry.GetCredentials().GetPassword()
ansibleKindProps.Username = nodeRegEntry.GetCredentials().GetUsername()
ansibleKindProps.Password = nodeRegEntry.GetCredentials().GetPassword()
}

// add network_os to the node
kindProps.setNetworkOS(n.Config().Kind)
ansibleKindProps.setNetworkOS(n.Config().Kind)
// add ansible_connection to the node
kindProps.setAnsibleConnection(n.Config().Kind)
ansibleKindProps.setAnsibleConnection(n.Config().Kind)

inv.Nodes[n.Config().Kind] = append(inv.Nodes[n.Config().Kind], ansibleNode)

Expand Down Expand Up @@ -125,7 +145,7 @@ func (c *CLab) generateAnsibleInventory(w io.Writer) error {
}

// setNetworkOS sets the network_os variable for the kind.
func (n *KindProps) setNetworkOS(kind string) {
func (n *AnsibleKindProps) setNetworkOS(kind string) {
switch kind {
case "nokia_srlinux", "srl":
n.NetworkOS = "nokia.srlinux.srlinux"
Expand All @@ -135,11 +155,111 @@ func (n *KindProps) setNetworkOS(kind string) {
}

// setAnsibleConnection sets the ansible_connection variable for the kind.
func (n *KindProps) setAnsibleConnection(kind string) {
func (n *AnsibleKindProps) setAnsibleConnection(kind string) {
switch kind {
case "nokia_srlinux", "srl":
n.AnsibleConn = "ansible.netcommon.httpapi"
case "nokia_sros", "vr-sros":
n.AnsibleConn = "ansible.netcommon.network_cli"
}
}

// Nornir Simple Inventory
// https://nornir.readthedocs.io/en/latest/tutorial/inventory.html

//go:embed inventory_nornir_simple.go.tpl
var nornirSimpleInvT string

// NornirSimpleInventoryKindProps is the kind properties structure used to generate the nornir simple inventory file.
type NornirSimpleInventoryKindProps struct {
Username string
Password string
Hostname string
Platform string
}

// NornirSimpleInventoryNode represents the data structure used to generate the nornir simple inventory file.
// It embeds the NodeConfig struct and adds the Username and Password fields extracted from
// the node registry.
type NornirSimpleInventoryNode struct {
*types.NodeConfig
}

// NornirSimpleInventory represents the data structure used to generate the nornir simple inventory file.
type NornirSimpleInventory struct {
// clab node kinds
Kinds map[string]*NornirSimpleInventoryKindProps
// clab nodes aggregated by their kind
Nodes map[string][]*NornirSimpleInventoryNode
// clab nodes aggregated by user-defined groups
Groups map[string][]*NornirSimpleInventoryNode
}

// generateNornirSimpleInventory generates and writes a Nornir Simple inventory file to w.
func (c *CLab) generateNornirSimpleInventory(w io.Writer) error {
inv := NornirSimpleInventory{
Kinds: make(map[string]*NornirSimpleInventoryKindProps),
Nodes: make(map[string][]*NornirSimpleInventoryNode),
Groups: make(map[string][]*NornirSimpleInventoryNode),
}

platformNameSchema := os.Getenv("CLAB_NORNIR_PLATFORM_NAME_SCHEMA")

for _, n := range c.Nodes {
nornirNode := &NornirSimpleInventoryNode{
NodeConfig: n.Config(),
}

// add nornirSimpleInventoryKindProps to the inventory struct
// the nornirSimpleInventoryKindProps is passed as a ref and is populated
// down below
nornirSimpleInventoryKindProps := &NornirSimpleInventoryKindProps{}
inv.Kinds[n.Config().Kind] = nornirSimpleInventoryKindProps

// the nornir platform is set by default to the node's kind
// and is overwritten with the proper Nornir Inventory Platform
// based on the the value of CLAB_PLATFORM_NAME_SCHEMA (nornir or scrapi).
// defaults to Nornir-Napalm/Netmiko compatible platform name
nornirSimpleInventoryKindProps.Platform = n.Config().Kind

// add username and password to kind properties
// assumption is that all nodes of the same kind have the same credentials
nodeRegEntry := c.Reg.Kind(n.Config().Kind)
if nodeRegEntry != nil {
nornirSimpleInventoryKindProps.Username =
nodeRegEntry.GetCredentials().GetUsername()
nornirSimpleInventoryKindProps.Password =
nodeRegEntry.GetCredentials().GetPassword()

if nodeRegEntry.PlatformAttrs() != nil {
switch platformNameSchema {
case "napalm":
nornirSimpleInventoryKindProps.Platform = nodeRegEntry.PlatformAttrs().NapalmPlatformName
case "scrapi":
nornirSimpleInventoryKindProps.Platform = nodeRegEntry.PlatformAttrs().ScrapliPlatformName
}
}

}

inv.Nodes[n.Config().Kind] = append(inv.Nodes[n.Config().Kind], nornirNode)
}

// sort nodes by name as they are not sorted originally
for _, nodes := range inv.Nodes {
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].ShortName < nodes[j].ShortName
})
}

t, err := template.New("nornir_simple").Parse(nornirSimpleInvT)
if err != nil {
return err
}
err = t.Execute(w, inv)
if err != nil {
return err
}

return err
}
11 changes: 11 additions & 0 deletions clab/inventory_nornir_simple.go.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
{{- range $kind, $nodes := .Nodes -}}
{{- $kindProps := index $.Kinds $kind -}}
{{- range $node := $nodes }}
{{ $node.ShortName }}:
username: {{ $kindProps.Username }}
password: {{ $kindProps.Password }}
platform: {{ $kindProps.Platform }}
hostname: {{ $node.MgmtIPv4Address }}
{{- end }}
{{- end }}
104 changes: 104 additions & 0 deletions clab/inventory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package clab

import (
"os"
"strings"
"testing"

Expand Down Expand Up @@ -101,3 +102,106 @@ func TestGenerateAnsibleInventory(t *testing.T) {
})
}
}

func TestGeneratNornirSimpleInventory(t *testing.T) {
tests := map[string]struct {
got string
want string
clab_nornir_platform_name_schema string // environment variable feature flag to direct platform name
}{
"case1-default-platform": {
got: "test_data/topo1.yml",
clab_nornir_platform_name_schema: "",
want: `---
node1:
username: admin
password: NokiaSrl1!
platform: nokia_srlinux
hostname: 172.100.100.11
node2:
username: admin
password: NokiaSrl1!
platform: nokia_srlinux
hostname: 172.100.100.12`,
},
"case2-scrapi-platform": {
got: "test_data/topo12.yml",
clab_nornir_platform_name_schema: "scrapi",
want: `---
node1:
username: admin
password: admin
platform: arista_eos
hostname:
node2:
username: admin
password: admin
platform: arista_eos
hostname:
node3:
username: admin
password: admin
platform: arista_eos
hostname:
node4:
username:
password:
platform: linux
hostname: `,
},
"case3-nornir-platform": {
got: "test_data/topo12.yml",
clab_nornir_platform_name_schema: "napalm",
want: `---
node1:
username: admin
password: admin
platform: eos
hostname:
node2:
username: admin
password: admin
platform: eos
hostname:
node3:
username: admin
password: admin
platform: eos
hostname:
node4:
username:
password:
platform: linux
hostname: `,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// Set the environment variable
if err := os.Setenv("CLAB_NORNIR_PLATFORM_NAME_SCHEMA", tc.clab_nornir_platform_name_schema); err != nil {
t.Fatalf("failed to set environment variable: %v", err)
}
// Unset the environment variable after the test
defer os.Unsetenv("CLAB_NORNIR_PLATFORM_NAME_SCHEMA")

opts := []ClabOption{
WithTopoPath(tc.got, ""),
}
c, err := NewContainerLab(opts...)
if err != nil {
t.Fatal(err)
}

var s strings.Builder
err = c.generateNornirSimpleInventory(&s)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(tc.want, s.String()); diff != "" {
t.Errorf("failed at '%s', diff: (-want +got)\n%s", name, diff)
}
})
}
}
Loading