Newer
Older
package clabconfig
import (
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"gopkg.in/yaml.v3"
util "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/util"
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
)
// 1st level (Root entry)
type ClabConfig struct {
Name string `yaml:"name"`
Mgmt Mgmt `yaml:"mgmt"`
Topology Topology `yaml:"topology"`
}
// 2nd level
type Mgmt struct {
Network string `yaml:"network"`
IPv4Subnet string `yaml:"ipv4-subnet"`
IPv6Subnet string `yaml:"ipv6-subnet"`
MTU int `yaml:"mtu"`
}
// 2nd level
type Topology struct {
Nodes map[string]Node `yaml:"nodes"`
Links []Link `yaml:"links"`
}
// topology.nodes
type Node struct {
Kind string `yaml:"kind"`
Image string `yaml:"image"`
Ports []string `yaml:"ports,omitempty"`
Cmd string `yaml:"cmd,omitempty"`
MgmtIPv4 string `yaml:"mgmt-ipv4"`
Env map[string]string `yaml:"env,omitempty"`
Binds []string `yaml:"binds,omitempty"`
StartupDelay int `yaml:"startup-delay,omitempty"`
Group string `yaml:"group,omitempty"`
}
// topology.links
type Link struct {
Endpoints []string `yaml:"endpoints"`
}
// Pre-defined Node definitions for fall-back. Necessary because it's just not possible to retrieve all the fields
// from the DB
var nodeBlueprints = map[string]Node{
"gnmi-target-switch": {
Kind: "linux",
Image: "gnmi-target-local",
//Image: "registry.code.fbi.h-da.de/danet/gnmi-target/debian:interface-enabled-test",
Binds: []string{"../../../../artifacts/ssl/gnmi-target:/etc/gnmi-target/ssl"},
Cmd: "start --ca_file /etc/gnmi-target/ssl/ca.crt --cert /etc/gnmi-target/ssl/certs/gnmi-target-selfsigned.crt --key /etc/gnmi-target/ssl/private/gnmi-target-selfsigned.key",
StartupDelay: 5,
},
"ceos": {
Kind: "ceos",
},
}
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// Function for deep copying clab config struct
func (c *ClabConfig) Copy() *ClabConfig {
newC := &ClabConfig{
Name: c.Name,
Mgmt: c.Mgmt,
Topology: Topology{
Nodes: make(map[string]Node),
Links: make([]Link, len(c.Topology.Links)),
},
}
for name, node := range c.Topology.Nodes {
newNode := Node{
Kind: node.Kind,
Image: node.Image,
Ports: append([]string{}, node.Ports...),
Cmd: node.Cmd,
MgmtIPv4: node.MgmtIPv4,
Env: make(map[string]string),
Binds: append([]string{}, node.Binds...),
StartupDelay: node.StartupDelay,
Group: node.Group,
}
for envName, env := range node.Env {
newNode.Env[envName] = env
}
newC.Topology.Nodes[name] = newNode
}
for i, link := range c.Topology.Links {
newC.Topology.Links[i] = Link{
Endpoints: append([]string{}, link.Endpoints...),
}
}
return newC
}
func GetNodeTemplate(kind, ipv4subnet string) *Node {
re := regexp.MustCompile("(^\\D+)(\\d)")
matches := re.FindStringSubmatch(kind)
if matches == nil {
fmt.Println("GetNodeTemplate(): Couldn't extract regex for string", kind)
return nil
}
if node, exists := nodeBlueprints[matches[1]]; exists {
// make a deep copy of the blueprint, caller can choose what to do with it:
template := node
return &template
}
// This is a hacky way of supporting non-gnmi target hosts
if strings.HasPrefix(kind, "centos") {
digit, err := strconv.Atoi(matches[2])
if err != nil {
fmt.Printf("Error trying to get centos digit: %v\n", err)
return nil
}
// Alter the ideitifying octet in the ipaddress, not ideal but works for now
// Has to be done this way because sdnconfig doesn't save an address for non-gnmi-targets
ipOffset := digit + 17
ipv4subnetSplit := strings.Split(ipv4subnet, ".")
ipv4subnetSplit[3] = strconv.Itoa(ipOffset)
ipv4 := strings.Join(ipv4subnetSplit, ".")
return &Node{
MgmtIPv4: ipv4,
Kind: "linux",
Image: "centos:8",
Group: "server",
}
}
return nil
}
// return absolute clab config path based on gosdn root
// should return /home/user/path/to/gosdn/dev_env_data/clab
func ClabConfigPath() (string, error) {
gosdnPath, err := util.GenerateGosdnPath()
if err != nil {
return "", fmt.Errorf("Error: Couldn't get Gosdn Path: %w\n", err)
}
return filepath.Join(gosdnPath, "/applications/rtdt-manager/data"), nil
}
// Read file and parse into ClabConfig struct
func LoadConfig(filename string) (*ClabConfig, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("Error: Failed to read file: %w", err)
}
var clabconfig ClabConfig
err = yaml.Unmarshal(data, &clabconfig)
if err != nil {
return nil, fmt.Errorf("Error: Failed to unmarshal YAML: %w", err)
}
return &clabconfig, nil
}
// WriteConfig writes the Config struct to a YAML file
// Takes config file as absolute FULL path
func WriteConfig(filename string, config *ClabConfig) error {
data, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("Failed to marshal YAML: %w", err)
}
dir := filepath.Dir(filename)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0700); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}
err = os.WriteFile(filename, data, 0644)
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
// incrementPort takes a port as a string, adds an offset, and returns the new port as a string.
func incrementPort(port string, offset int) (string, error) {
portNum, err := strconv.Atoi(port)
if err != nil {
return "", fmt.Errorf("invalid port number: %s", port)
}
return strconv.Itoa(portNum + offset), nil
}
// Take a clab yaml config file and derive another clab config from it (only use to derive base clab)
func DeriveConfig(clabConfig *ClabConfig, newIPv4Subnet, newIPv6Subnet string, clabName string) (*ClabConfig, error) {
// Create deep copy
derivedConfig := *clabConfig
derivedConfig.Topology.Nodes = make(map[string]Node)
derivedConfig.Topology.Links = append([]Link{}, clabConfig.Topology.Links...) // Copy links
portOffset := 5 // TODO set dynamically in some way
derivedConfig.Name = fmt.Sprintf("gosdn_%s", clabName)
derivedConfig.Mgmt.Network = fmt.Sprintf("gosdn-%s-net", clabName)
subnetParts := strings.Split(newIPv4Subnet, ".")
derivedConfig.Mgmt.IPv4Subnet = newIPv4Subnet
derivedConfig.Mgmt.IPv6Subnet = newIPv6Subnet
// Adjust all nodes
for name, node := range clabConfig.Topology.Nodes {
splitIPv4 := strings.Split(node.MgmtIPv4, ".")
splitIPv4[0], splitIPv4[1], splitIPv4[2] = subnetParts[0], subnetParts[1], subnetParts[2]
node.MgmtIPv4 = strings.Join(splitIPv4, ".")
// if strings.HasPrefix(name, "gosdn") {
// pluginRegistryAddress := fmt.Sprintf("clab-%s-plugin-registry:55057", clabConfig.Name)
// dbAddress := fmt.Sprintf("mongodb://root:example@clab-%s-mongodb:27017", clabConfig.Name)
// node.Cmd = fmt.Sprintf("%s --plugin-registry %s -d %s", node.Cmd, pluginRegistryAddress, dbAddress)
// }
// Ports: host side needs to be incremented or there will be conflicts
// for now just use 5 as increment
for i, portBinding := range node.Ports {
parts := strings.Split(portBinding, ":")
if len(parts) == 3 {
// Format: <host-ip>:<host-port>:<container-port>
hostPort := parts[1]
newHostPort, err := incrementPort(hostPort, portOffset)
if err != nil {
return nil, fmt.Errorf("invalid port binding: %s", portBinding)
}
parts[1] = newHostPort
} else if len(parts) == 2 {
// Format: <host-port>:<container-port>
hostPort := parts[0]
newHostPort, err := incrementPort(hostPort, portOffset)
if err != nil {
return nil, fmt.Errorf("invalid port binding: %s", portBinding)
}
parts[0] = newHostPort
} else {
return nil, fmt.Errorf("invalid port binding format: %s", portBinding)
}
node.Ports[i] = strings.Join(parts, ":")
}
S.H.
committed
derivedConfig.Topology.Nodes[name] = node
}
return &derivedConfig, nil
}
func ClabDestroy(fullPath string) error {
fmt.Println("Trying to destroy venv: ", fullPath)
cmd := exec.Command("sudo", "containerlab", "destroy", "-t", fullPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("Error(s) occured while destroying clab environment: %w", err)
}
return err
}
// Launch a containerlab environment, pass in absolute path of clab yaml
func ClabDeploy(fullPath string) error {
fmt.Println("Deploying file: ", fullPath)
cmd := exec.Command("sudo", "containerlab", "deploy", "-t", fullPath, "--reconfigure")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Run the command in a Goroutine
done := make(chan error, 1)
stopdeploy := make(chan os.Signal, 1)
signal.Notify(stopdeploy, os.Interrupt, syscall.SIGTERM)
go func() {
err := cmd.Run() // Use CombinedOutput to capture stdout and stderr
if err != nil {
fmt.Printf("Error during deployment: %s\n", err)
}
done <- err
close(done) // Ensure the channel is closed after sending the result
}()
// Wait for the deployment to finish or a signal to stop
select {
case err := <-done: // Command finished
return err
case <-stopdeploy: // Signal received to interrupt
if err := cmd.Process.Kill(); err != nil {
fmt.Printf("Failed to kill process: %v\n", err)
}
err := ClabDestroy(fullPath)
if err != nil {
return fmt.Errorf("Deploying containerlab environment was interrupted and couldn't be cleaned up: %v", err)
}
return fmt.Errorf("Deployment interrupted by signal")
}
}
func (c *ClabConfig) GetNodeByName(name string) *Node {
for nodename, node := range c.Topology.Nodes {
if nodename == name {
return &node
}
}
fmt.Printf("Couldn't find a node with name %s!\n", name)
return nil
}