Skip to content
Snippets Groups Projects
clab-config.go 9.64 KiB
Newer Older
  • Learn to ignore specific revisions
  • package clabconfig
    
    import (
    	"fmt"
    	"os"
    	"os/exec"
    
    
    	util "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/util"
    
    )
    
    // 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",
    	},
    }
    
    
    // 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)
    
    		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)
    
    // 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) {
    
    	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, ":")
    		}
    
    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()
    
    		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)
    
    		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)