Newer
Older
package clabconfig
import (
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"gopkg.in/yaml.v3"
"code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement"
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"`
}
// 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("Couldn't get Gosdn Path: %w\n", err)
}
return filepath.Join(gosdnPath, "/applications/rtdt-manager/data"), nil
}
// Read file and parse into ClabConfig struct
// Only load gosdn environment: rabbitmq, mongodb,
func LoadConfig(filename string) (*ClabConfig, error) {
absFilepath, err := filepath.Abs(filename)
if err != nil {
return nil, fmt.Errorf("Failed to get absolute path: %w", err)
}
data, err := os.ReadFile(absFilepath)
if err != nil {
return nil, fmt.Errorf("Failed to read file: %w", err)
}
var clabconfig ClabConfig
err = yaml.Unmarshal(data, &clabconfig)
if err != nil {
return nil, fmt.Errorf("Failed to unmarshal YAML: %w", err)
}
S.H.
committed
fmt.Println("--- Successfully loaded file:", absFilepath, "----")
return &clabconfig, 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
}
// TODO
func (c *ClabConfig) InsertMNE(mnes []*networkelement.ManagedNetworkElement) {
for _, mne := range mnes {
c.Topology.Nodes[mne.Name] = Node{
Kind: "TBD",
Image: "TBD",
}
}
}
// Take a clab yaml config file and derive another clab config from it
func DeriveConfig(clabconfig *ClabConfig, newIPv4Subnet, newIPv6Subnet string, postfix 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
S.H.
committed
derivedConfig.Name = fmt.Sprintf("%s-%s", clabconfig.Name, postfix)
subnetParts := strings.Split(newIPv4Subnet, ".")
derivedConfig.Mgmt.IPv4Subnet = newIPv4Subnet
derivedConfig.Mgmt.IPv6Subnet = newIPv6Subnet
derivedConfig.Mgmt.Network = fmt.Sprintf("%s-%s", clabconfig.Name, postfix)
// 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, ".")
S.H.
committed
if strings.HasPrefix(name, "gosdn") {
node.Cmd = "--config /app/configs/containerlab-gosdn-twin.toml"
//node.Binds = []string{"../../../applications/rtdt-manager/data/ssl/gosdn:/app/ssl"}
S.H.
committed
}
//
// //use separate ssl folders, testing
// if strings.HasPrefix(name, "gnmi-target") {
// node.Binds = []string{"../../../applications/rtdt-manager/data/ssl/gnmi-target:/etc/gnmi-target/ssl"}
// }
// 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
}
// Update Links so they reference the correct nodes
S.H.
committed
// for i, link := range clabconfig.Topology.Links {
// for j, endpoint := range link.Endpoints {
// parts := strings.Split(endpoint, ":")
// if len(parts) != 2 {
// return nil, fmt.Errorf("invalid link endpoint: %s", endpoint)
// }
// clabconfig.Topology.Links[i].Endpoints[j] = strings.Join(parts, ":")
// }
// }
return &derivedConfig, nil
}
// WriteConfig writes the Config struct to a YAML file
// Takes config file as absolute 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)
}
err = os.WriteFile(filename, data, 0644)
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return 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
fmt.Println("Successfully deployed containerlab environment")
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
}