From d2737db73739212b9c1b2d1a1590284928e199e0 Mon Sep 17 00:00:00 2001
From: "S.H." <sebastian.heiss94@proton.me>
Date: Mon, 24 Feb 2025 22:12:45 +0100
Subject: [PATCH] Rework the mechanism for creating twins (and virtual realnet)
 - rtdt-manager.go:     - LaunchRealnetVEnv() to take over functionality
 previously in       main.go and derive realnet clab configuration via      
 ProduceClabConfig()     - ProduceClabConfig() merge a base clab config
 (gosdn, rabbitmq, mongo       and so on) with a passed-in sdnconfig json to
 create the environment - clab-config.go:     - Make LoadConfig take full path
 like WriteConfig     - Implement mechanism to get templates of nodes that
 give       information not retrievable from the running containerlab      
 environment (nodeBlueprints and GetNodeTemplate()) - venv.go:     -
 CreateDevices() now uses sdnconfig networkElements to create       devices

---
 .../rtdt-manager/clab-config/clab-config.go   | 102 +++++++----
 .../rtdt-manager/data/realnet-clab.yaml       |   4 +-
 .../data/realnet-deploy-clab.yaml             |  88 ++++++++++
 applications/rtdt-manager/main.go             |  52 ++----
 .../rtdt-manager/rtdt-manager/rtdt-manager.go | 165 +++++++++++++++---
 .../rtdt-manager/sdnconfig/sdnconfig.go       |  10 +-
 applications/rtdt-manager/venv/venv.go        |  59 +++----
 scripts/setup-clab-slim.sh                    |   8 +-
 8 files changed, 358 insertions(+), 130 deletions(-)
 create mode 100644 applications/rtdt-manager/data/realnet-deploy-clab.yaml

diff --git a/applications/rtdt-manager/clab-config/clab-config.go b/applications/rtdt-manager/clab-config/clab-config.go
index df02f232c..d53ae7172 100644
--- a/applications/rtdt-manager/clab-config/clab-config.go
+++ b/applications/rtdt-manager/clab-config/clab-config.go
@@ -6,6 +6,7 @@ import (
 	"os/exec"
 	"os/signal"
 	"path/filepath"
+	"regexp"
 	"strconv"
 	"strings"
 	"syscall"
@@ -55,42 +56,99 @@ 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:        "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",
+	},
+}
+
+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
+		}
+		ipOffset := digit + 17
+		fmt.Println("ipv4subnet: ", ipv4subnet)
+		ipv4subnetSplit := strings.Split(ipv4subnet, ".")
+		fmt.Printf("ipv4subnetSplit[0],[1],[2],[3]=%s:%s", ipv4subnetSplit[0], ipv4subnetSplit[1])
+		ipv4subnetSplit[3] = strconv.Itoa(ipOffset)
+		ipv4 := strings.Join(ipv4subnetSplit, ".")
+		fmt.Println("ipv4 for centos node:", ipv4)
+		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("Couldn't get Gosdn Path: %w\n", err)
+		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
-// Only load gosdn environment: rabbitmq, mongodb,
+// Take FULL path
 func LoadConfig(filename string) (*ClabConfig, error) {
-	cwd, err := os.Getwd()
+	data, err := os.ReadFile(filename)
 	if err != nil {
-		return nil, fmt.Errorf("Failed to get working directory: %w", err)
-	}
-	absFilename := filepath.Join(cwd, filename)
-	if err != nil {
-		return nil, fmt.Errorf("Error in LoadConfig(): %w", err)
-	}
-	data, err := os.ReadFile(absFilename)
-	if err != nil {
-		return nil, fmt.Errorf("Failed to read file: %w", err)
+		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("Failed to unmarshal YAML: %w", err)
+		return nil, fmt.Errorf("Error: Failed to unmarshal YAML: %w", err)
 	}
-	fmt.Println("--- Successfully loaded clab file:", absFilename, "----")
 	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)
+	}
+
+	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)
@@ -183,21 +241,6 @@ func DeriveConfig(clabconfig *ClabConfig, newIPv4Subnet, newIPv6Subnet string, p
 	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)
@@ -233,7 +276,6 @@ func ClabDeploy(fullPath string) error {
 	// 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 {
diff --git a/applications/rtdt-manager/data/realnet-clab.yaml b/applications/rtdt-manager/data/realnet-clab.yaml
index 0c4a7ecc1..772af1929 100644
--- a/applications/rtdt-manager/data/realnet-clab.yaml
+++ b/applications/rtdt-manager/data/realnet-clab.yaml
@@ -56,13 +56,13 @@ topology:
     centos0:
       kind: linux
       image: centos:8
-      mgmt-ipv4: 172.100.0.3
+      mgmt-ipv4: 172.100.0.16
       group: server
 
     centos1:
       kind: linux
       image: centos:8
-      mgmt-ipv4: 172.100.0.4
+      mgmt-ipv4: 172.100.0.17
       group: server
 
     mongodb:
diff --git a/applications/rtdt-manager/data/realnet-deploy-clab.yaml b/applications/rtdt-manager/data/realnet-deploy-clab.yaml
new file mode 100644
index 000000000..29ac6e680
--- /dev/null
+++ b/applications/rtdt-manager/data/realnet-deploy-clab.yaml
@@ -0,0 +1,88 @@
+name: gosdn_realnet
+mgmt:
+    network: gosdn-realnet-net
+    ipv4-subnet: 172.100.0.0/16
+    ipv6-subnet: 2001:db8::/64
+    mtu: 1500
+topology:
+    nodes:
+        centos0:
+            kind: linux
+            image: centos:8
+            mgmt-ipv4: 172.100.0.17
+            group: server
+        centos1:
+            kind: linux
+            image: centos:8
+            mgmt-ipv4: 172.100.0.18
+            group: server
+        gnmi-target-switch0:
+            kind: linux
+            image: registry.code.fbi.h-da.de/danet/gnmi-target/debian:interface-enabled-test
+            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
+            mgmt-ipv4: 172.100.0.11
+            binds:
+                - ../../../artifacts/ssl/gnmi-target:/etc/gnmi-target/ssl
+            startup-delay: 5
+        gnmi-target-switch1:
+            kind: linux
+            image: registry.code.fbi.h-da.de/danet/gnmi-target/debian:interface-enabled-test
+            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
+            mgmt-ipv4: 172.100.0.12
+            binds:
+                - ../../../artifacts/ssl/gnmi-target:/etc/gnmi-target/ssl
+            startup-delay: 5
+        gosdn:
+            kind: linux
+            image: gosdn
+            ports:
+                - 55055:55055
+                - 8080:8080
+                - 40000:40000
+            cmd: --config /app/configs/containerlab-gosdn.toml --plugin-registry clab-gosdn_realnet-plugin-registry:55057 -d mongodb://root:example@clab-gosdn_realnet-mongodb:27017
+            mgmt-ipv4: 172.100.0.5
+            env:
+                GOSDN_ADMIN_PASSWORD: TestPassword
+            binds:
+                - ../../../artifacts/ssl/gosdn:/app/ssl
+        mongodb:
+            kind: linux
+            image: mongo:7
+            ports:
+                - 27017:27017
+            mgmt-ipv4: 172.100.0.13
+            env:
+                MONGO_INITDB_ROOT_PASSWORD: example
+                MONGO_INITDB_ROOT_USERNAME: root
+        mongodb-express:
+            kind: linux
+            image: mongo-express:1.0.2
+            ports:
+                - 8081:8081
+            mgmt-ipv4: 172.100.0.14
+            env:
+                ME_CONFIG_BASICAUTH: "false"
+                ME_CONFIG_MONGODB_AUTH_PASSWORD: example
+                ME_CONFIG_MONGODB_AUTH_USERNAME: root
+                ME_CONFIG_MONGODB_SERVER: mongodb
+        plugin-registry:
+            kind: linux
+            image: plugin-registry
+            mgmt-ipv4: 172.100.0.16
+        rabbitmq:
+            kind: linux
+            image: rabbitmq:3-management
+            ports:
+                - 127.0.0.1:5672:5672
+                - 127.0.0.1:15672:15672
+            mgmt-ipv4: 172.100.0.15
+    links:
+        - endpoints:
+            - gnmi-target-switch0:eth1
+            - gnmi-target-switch1:eth1
+        - endpoints:
+            - gnmi-target-switch0:eth2
+            - centos0:eth1
+        - endpoints:
+            - gnmi-target-switch1:eth2
+            - centos1:eth1
diff --git a/applications/rtdt-manager/main.go b/applications/rtdt-manager/main.go
index b9ce5d595..e595f0b65 100644
--- a/applications/rtdt-manager/main.go
+++ b/applications/rtdt-manager/main.go
@@ -5,12 +5,12 @@ import (
 	"fmt"
 	"os"
 	"os/signal"
-	"sync"
 	"syscall"
 
+	clabconfig "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/clab-config"
 	RtdtMan "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/rtdt-manager"
+	"code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/sdnconfig"
 	//"code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/sdnconfig"
-	venv "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/venv"
 )
 
 func main() {
@@ -19,7 +19,6 @@ func main() {
 	// Global stop channel, should be passed to all venvs and App
 	stopChan := make(chan os.Signal, 1)
 	signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM)
-	var wg sync.WaitGroup
 
 	var address string
 	var pass string
@@ -51,49 +50,30 @@ func main() {
 		fmt.Println("--sdnconfig: Path to the sdnconfig .json file that contains information about network elements and links")
 	}
 	flag.Parse()
-	// Start virtual environment for realnet
-	// sdnConfig := sdnconfig.NewSdnConfig()
-	// sdnConfig.LoadSdnConfig(sdnConfigPath)
-	var realnet *venv.VEnv
-	realnet = venv.NewVEnv("realnet-test", clabConfigName, user, pass, &wg)
-	if realnet == nil {
-		fmt.Println("ERROR: Couldn't deploy the physical network")
-		return
-	} else {
-		fmt.Println("Successfully deployed physical network")
-	}
-	err = realnet.DeriveTopologyFromClabData()
-	if err != nil {
-		fmt.Printf("Error occured while trying to construct topology in realnet: %v\n", err)
+	// Register new RtdtManager
+	rtdtMan := RtdtMan.NewRtdtManager()
+	if rtdtMan == nil {
+		fmt.Println("Couldn't initialize rtdt-manager, quitting!")
 		return
 	}
-	err = realnet.UploadClabConfig()
-	if err != nil {
-		fmt.Printf("Error: Couldnt upload clab config: %v\n", err)
-	}
-	err = realnet.RetrieveClabConfig()
+	// Deploy the realnet as VEnv (containerlab)
+	sdnConfig := sdnconfig.NewSdnConfig()
+	sdnConfig.LoadSdnConfig(sdnConfigPath)
+	clabBaseConfig, err := clabconfig.LoadConfig(clabConfigName)
 	if err != nil {
-		fmt.Printf("Error: Couldn't retrieve clab config: %v\n", err)
-	}
-	err = realnet.CreateDevices()
-	if err != nil {
-		fmt.Printf("Error: Couldn't create devices!")
+		fmt.Printf("In main(): %v\n", err)
 		return
 	}
-	err = realnet.UploadTopology()
+	// Start virtual environment for realnet
+	err = rtdtMan.LaunchRealnetVEnv("realnet", sdnConfig, clabBaseConfig)
 	if err != nil {
-		fmt.Printf("Error occured while trying to upload realnet topology to DB: %v\n", err)
-		return
-	}
-	// Register realnet with a new rtdt-manager
-	rtdtMan := RtdtMan.NewRtdtManager(realnet, &wg, &stopChan)
-	if rtdtMan == nil {
-		fmt.Println("Couldn't initialize rtdt-manager, quitting!")
+		fmt.Printf("In main(): %v\n", err)
 		return
 	}
+
 	err = rtdtMan.InitEventSystem()
 	if err != nil {
-		fmt.Printf("Error occured while initializing event system: %v\n", err)
+        fmt.Printf("In main(): %v\n", err)
 		return
 	}
 	// Do performance tests of realnet before starting twin
diff --git a/applications/rtdt-manager/rtdt-manager/rtdt-manager.go b/applications/rtdt-manager/rtdt-manager/rtdt-manager.go
index 19d98feb5..0e8014523 100644
--- a/applications/rtdt-manager/rtdt-manager/rtdt-manager.go
+++ b/applications/rtdt-manager/rtdt-manager/rtdt-manager.go
@@ -16,30 +16,151 @@ import (
 	"code.fbi.h-da.de/danet/gosdn/application-framework/registration"
 	clabconfig "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/clab-config"
 	rtdt_topology "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/rtdt-topology"
+	"code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/sdnconfig"
 	"code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/util"
 	"code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/venv"
 )
 
 // Manages an environment containing one physical network (realnet) and a digital twin
 type RtdtManager struct {
-	realnet      *venv.VEnv
-	rtdt_twins   []*venv.VEnv
-	eventService event.ServiceInterface // Receive events from realnet gosdn (not used yet)
-	waitGroup    *sync.WaitGroup
-	stopChan     *chan os.Signal
+	realnet        *venv.VEnv
+	rtdt_twins     []*venv.VEnv
+	eventService   event.ServiceInterface // Receive events from realnet gosdn (not used yet)
+	waitGroup      sync.WaitGroup
+	stopChan       chan os.Signal
+	baseClabConfig *clabconfig.ClabConfig
 }
 
 // needs to be passed a running realnet VEnv
-func NewRtdtManager(realnet *venv.VEnv, wg *sync.WaitGroup, stopChan *chan os.Signal) *RtdtManager {
+func NewRtdtManager() *RtdtManager {
 	rMan := RtdtManager{
-		realnet:   realnet,
-		waitGroup: wg,
-		stopChan:  stopChan,
+		realnet:        nil,
+		waitGroup:      sync.WaitGroup{},
+		stopChan:       make(chan os.Signal, 1),
+		baseClabConfig: nil,
 	}
 	fmt.Println("Success: RtdtManager created")
 	return &rMan
 }
 
+// When realnet is clab venv, use this to create it
+func (r *RtdtManager) LaunchRealnetVEnv(realnetName string, sdnConfig *sdnconfig.SdnConfig, baseClabConfig *clabconfig.ClabConfig) error {
+	r.baseClabConfig = baseClabConfig
+	var realnetClabFName string
+	// Generate a clabconfig file based on the base config and sdnconfig
+	realnetClab, err := r.ProduceClabConfig(realnetName, r.baseClabConfig, sdnConfig)
+	if err != nil {
+		return fmt.Errorf("Error in LaunchRealnet(): %w", err)
+	}
+	fmt.Println("Produced realnet ClabConfig file from sdnconfig and base config")
+	// Need to write the clab struct to disk
+	realnetClabPath, err := clabconfig.ClabConfigPath()
+	if err != nil {
+		return fmt.Errorf("Error in LaunchRealnet() %w", err)
+	}
+	realnetClabFName = filepath.Join(realnetClabPath, realnetName+"-deploy-clab.yaml")
+	err = clabconfig.WriteConfig(realnetClabFName, realnetClab)
+	if err != nil {
+		return fmt.Errorf("Error in LaunchRealnet() %w", err)
+	} else {
+		fmt.Println("Successfully wrote realnet clab config file")
+	}
+
+	fmt.Printf("Trying to now create realnet with fname: %s -------------\n", realnetClabFName)
+	// NewVEnv tasks: deploy the given clab config,
+	r.realnet = venv.NewVEnv(realnetName, realnetClabFName, "admin", "TestPassword", &r.waitGroup)
+	if r.realnet == nil {
+		return fmt.Errorf("Error in LaunchRealnet: Couldn't deploy VEnv")
+	}
+	err = r.realnet.CreateDevices()
+	if err != nil {
+		fmt.Printf("Error: Couldn't create devices!")
+		return err
+	}
+	return nil
+
+	// Now setup the just-created environment
+	// Need to:
+	// - Upload managed network elements
+	err = r.realnet.DeriveTopologyFromClabData()
+	if err != nil {
+		fmt.Printf("Error occured while trying to construct topology in r.realnet: %v\n", err)
+		return err
+	}
+	err = r.realnet.UploadClabConfig()
+	if err != nil {
+		fmt.Printf("Error: Couldnt upload clab config: %v\n", err)
+		return err
+	}
+	err = r.realnet.RetrieveClabConfig()
+	if err != nil {
+		fmt.Printf("Error: Couldn't retrieve clab config: %v\n", err)
+		return err
+	}
+
+	err = r.realnet.UploadTopology()
+	if err != nil {
+		fmt.Printf("Error occured while trying to upload r.realnet topology to DB: %v\n", err)
+		return err
+	}
+
+	return nil
+}
+
+func (r *RtdtManager) ProduceClabConfig(clabName string, c *clabconfig.ClabConfig, sdnConfig *sdnconfig.SdnConfig) (*clabconfig.ClabConfig, error) {
+	var clabConfig *clabconfig.ClabConfig
+	if sdnConfig == nil {
+		return nil, fmt.Errorf("Can't produce clab config without loading sdnconfig\n")
+	}
+	// clabConfig can be nil, but wouldn't be very useful without base gosdn environment
+	if c != nil {
+		clabConfig = c
+		for nodeName, node := range clabConfig.Topology.Nodes {
+			// database and plugin-registry need to be loaded dynamically based on generated name
+			if strings.HasPrefix(nodeName, "gosdn") {
+				pluginRegistryAddress := fmt.Sprintf("clab-%s-plugin-registry:55057", clabConfig.Name)
+				dbAddress := fmt.Sprintf("mongodb://root:example@clab-%s-mongodb:27017", clabConfig.Name)
+				newCmd := fmt.Sprintf("%s --plugin-registry %s -d %s", node.Cmd, pluginRegistryAddress, dbAddress)
+				node.Cmd = newCmd
+				clabConfig.Topology.Nodes[nodeName] = node
+				fmt.Println("node.Cmd is now:", node.Cmd)
+			}
+		}
+	} else {
+		return nil, fmt.Errorf("No base clabconfig set\n")
+	}
+	if sdnConfig.Nodes == nil {
+		return nil, fmt.Errorf("sdnconfig exists, but Nodes is nil")
+	}
+	for _, node := range sdnConfig.Nodes {
+		var clabNode *clabconfig.Node
+		fmt.Println("Nodename:", node.Name)
+
+		clabNode = clabconfig.GetNodeTemplate(node.Name, c.Mgmt.IPv4Subnet)
+		if clabNode == nil {
+			return nil, fmt.Errorf("No template exists for node %s", node.Name)
+		}
+		mne := sdnConfig.GetMneByID(node.ID)
+		if mne != nil {
+			ipv4 := strings.Split(mne.TransportAddress, ":")
+			clabNode.MgmtIPv4 = ipv4[0] // Doesn't work wor centos hosts
+		} else {
+			fmt.Printf("ProduceClabConfig(): Warning: No MNE entry for node: %s\n", node.Name)
+		}
+
+		clabConfig.Topology.Nodes[node.Name] = *clabNode
+	}
+	for _, link := range sdnConfig.Links {
+		ep0 := link.SourceNode.Name + ":" + link.SourcePort.Name
+		ep1 := link.TargetNode.Name + ":" + link.TargetPort.Name
+		var clabLink = clabconfig.Link{
+			Endpoints: []string{ep0, ep1},
+		}
+		clabConfig.Topology.Links = append(clabConfig.Topology.Links, clabLink)
+	}
+	return clabConfig, nil
+}
+
 // To launch a new twin, this runs through the following steps:
 // - Load the clab config for current realnet gosdn (passed on cli)
 // - Derive the config we need for twin
@@ -48,7 +169,7 @@ func NewRtdtManager(realnet *venv.VEnv, wg *sync.WaitGroup, stopChan *chan os.Si
 // Launch a second gosdn instance which will manage the virtual network
 func (r *RtdtManager) LaunchTwin(twinSubnetIPv4, twinSubnetIPv6, twinName string) error {
 	var derivedConfig *clabconfig.ClabConfig
-	var baseConfig *clabconfig.ClabConfig
+	var twinClabConfig *clabconfig.ClabConfig
 	var clabConfigPath string
 	var err error
 	var topo *rtdt_topology.Topology
@@ -59,8 +180,8 @@ func (r *RtdtManager) LaunchTwin(twinSubnetIPv4, twinSubnetIPv6, twinName string
 		return fmt.Errorf("Error: Couldn't get topology since it hasn't been retrieved from DB yet")
 	}
 
-	// Get the basic containerlab config with gosdn, mongodb, rabbitmq
-	baseConfig, err = clabconfig.LoadConfig("base-clab.yaml")
+	// First add the base config's nodes
+	twinClabConfig = r.baseClabConfig
 	// Now retrieve the nodes (gnmi-targets and hosts + topology) via MNE Api
 	mneServiceRealnet := networkelement.NewNetworkElementServiceClient(conn)
 	getRequest := &networkelement.GetAllRequest{
@@ -72,7 +193,7 @@ func (r *RtdtManager) LaunchTwin(twinSubnetIPv4, twinSubnetIPv6, twinName string
 	elements := getAllResponse.GetMne()
 	// These now need to go into the new VEnv's clabData so clab yaml config can be created
 	if len(elements) > 0 {
-		baseConfig.InsertMNE(elements)
+		twinClabConfig.InsertMNE(elements)
 	}
 	// Also get topology from db
 	// TODO: Maybe update the topology first before retrieving it?
@@ -80,22 +201,26 @@ func (r *RtdtManager) LaunchTwin(twinSubnetIPv4, twinSubnetIPv6, twinName string
 	rtdt_topology.NewTopology().RetrieveTopology(r.realnet.GetPnd().Id, r.realnet.GetAuth())
 
 	// Could pass topo, clabconfig and elements into this
-	if derivedConfig, err = clabconfig.DeriveConfig(baseConfig, twinSubnetIPv4, twinSubnetIPv6, twinName); err != nil {
+	// Get the basic containerlab config with gosdn, mongodb, rabbitmq
+	clabConfigPath, err = clabconfig.ClabConfigPath()
+	if err != nil {
+		return err
+	}
+	if derivedConfig, err = clabconfig.DeriveConfig(twinClabConfig, twinSubnetIPv4, twinSubnetIPv6, twinName); err != nil {
 		return fmt.Errorf("Failed to derive config for twin: %w", err)
 	}
 	// Construct filepath for derived clab yaml
-	clabConfigPath, err = clabconfig.ClabConfigPath()
-	clabConfigFullPath := filepath.Join(clabConfigPath, "twin-clab.yaml")
-	if err = clabconfig.WriteConfig(clabConfigFullPath, derivedConfig); err != nil {
+	twinClabFName := filepath.Join(clabConfigPath, "twin-clab.yaml")
+	if err = clabconfig.WriteConfig(twinClabFName, derivedConfig); err != nil {
 		return fmt.Errorf("Failed to write modified twin clab config to disk: %w", err)
 	}
 
-	twin := venv.NewVEnv(twinName, clabConfigFullPath, "admin", "TestPassword", r.waitGroup)
+	twin := venv.NewVEnv(twinName, twinClabFName, "admin", "TestPassword", &r.waitGroup)
 	r.rtdt_twins = append(r.rtdt_twins, twin)
 	return nil
 }
 
-// Apply the changes from a twin back to realnet
+// Apply the changes from a twin back to realnet - NOT USED YET
 func (r *RtdtManager) ApplyChanges(twinName string) error {
 	var twin *venv.VEnv
 	for _, tw := range r.rtdt_twins {
@@ -153,7 +278,7 @@ func (r *RtdtManager) Run() error {
 		for {
 			select {
 			// idea: make channels for different events?
-			case stop := <-*r.stopChan:
+			case stop := <-r.stopChan:
 				fmt.Print("Received SIGINT/SIGSTOP, quitting..\n")
 				// this takes full path now
 				for _, twin := range r.rtdt_twins {
diff --git a/applications/rtdt-manager/sdnconfig/sdnconfig.go b/applications/rtdt-manager/sdnconfig/sdnconfig.go
index 0705d89e8..a6a64bd3f 100644
--- a/applications/rtdt-manager/sdnconfig/sdnconfig.go
+++ b/applications/rtdt-manager/sdnconfig/sdnconfig.go
@@ -8,7 +8,6 @@ import (
 	"time"
 
 	configPb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/configurationmanagement"
-	clabconfig "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/clab-config"
 	rtdt_auth "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/rtdt-auth"
 	"code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/util"
 )
@@ -160,8 +159,11 @@ func (s *SdnConfig) ApplySdnConfig(auth *rtdt_auth.RtdtAuth) error {
 	return nil
 }
 
-// Takes a base ClabConfig and expands it with topology
-func (s *SdnConfig) GenerateClabConfig(clabConfig *clabconfig.ClabConfig) error {
-
+func (s *SdnConfig) GetMneByID(ID string) *NetworkElement {
+	for _, mne := range s.NetworkElements {
+		if mne.ID == ID {
+			return &mne
+		}
+	}
 	return nil
 }
diff --git a/applications/rtdt-manager/venv/venv.go b/applications/rtdt-manager/venv/venv.go
index 15bb37a3a..1c9358cff 100644
--- a/applications/rtdt-manager/venv/venv.go
+++ b/applications/rtdt-manager/venv/venv.go
@@ -48,7 +48,8 @@ type VEnv struct {
 
 // Accepts a yaml filename to deploy a container lab environment
 // TODO Split up into sub-functions
-func NewVEnv(name, clabFilename, user, pass string, wg *sync.WaitGroup) *VEnv {
+// This takes FULL path name to clab config
+func NewVEnv(name string, clabFilename, user, pass string, wg *sync.WaitGroup) *VEnv {
 	fmt.Printf("[%s] - Creating new virtual environment\n", name)
 	wg.Add(1) // Register the venv and run atleast until it calls wg.Done()
 	var err error
@@ -61,12 +62,13 @@ func NewVEnv(name, clabFilename, user, pass string, wg *sync.WaitGroup) *VEnv {
 		fmt.Printf("[%s] - Failed to deploy the network\n", name)
 		return nil
 	}
+	fmt.Println("Successfully deployed containerlab environment")
 
 	// After having deployed it, load the config into clabData
 	var clabData *clab.ClabConfig
 	clabData, err = clab.LoadConfig(clabFilename)
 	if err != nil {
-		fmt.Printf("[%s] - Failed to load config from yaml file\n", name)
+		fmt.Printf("[%s] - Failed to load config from yaml file: %v\n", name, err)
 		return nil
 	}
 	// get gosdn address inside clab environment
@@ -120,41 +122,29 @@ func NewVEnv(name, clabFilename, user, pass string, wg *sync.WaitGroup) *VEnv {
 	}
 }
 
+// Based on saved sdnconfig, create devices
 func (v *VEnv) CreateDevices() error {
-	for _, node := range v.topology.Nodes {
-		if strings.HasPrefix(node.Name, "gnmi-target-") {
-			fmt.Printf("[%s] - Creating Network Element for node: %s\n", v.Name, node.Name)
-			//ports := strings.Split(node.Ports[0], ":")
-			//port := ports[1]
-			addr := node.MgmtIpv4 + ":7030" // gnmi targets will always listen on 7030
-			opt := &tpb.TransportOption{
-				Address:  addr,
-				Username: "admin",
-				Password: "admin",
-				Tls:      true,
-				TransportOption: &tpb.TransportOption_GnmiTransportOption{
-					GnmiTransportOption: &tpb.GnmiTransportOption{},
-				},
-			}
-			// Openconfig pluginid? TODO decide
-			pluginID, _ := uuid.Parse("d1c269a2-6482-4010-b0d8-679dff73153b") // TODO Get this dynamically
-			pndID, _ := uuid.Parse(v.pnd.Id)
-
-			fmt.Printf("[%s] - Found target: %s with addr: %s\n", v.Name, node.Name, addr)
-			fmt.Printf("[%s] - Gosdn controller at %s\n", v.Name, v.auth.GetAddress())
-			listResponse, err := gosdnutil.AddNetworkElement(v.auth, v.auth.GetAddress(), node.Name, node.ID, opt, pluginID, pndID, []string{"/"})
-			if err != nil {
-				fmt.Printf("[%s] - Failed to add network elements: %v\n", v.Name, err)
-				return nil
-			} else {
-				fmt.Printf("[%s] - Successfully created network element\n", v.Name)
-			}
-			fmt.Printf("[%s] - Got response from AddNetworkElement: %v\n", v.Name, listResponse)
-
-			fmt.Printf("[%s] - Success: registered mne with gosdn controller\n", v.Name)
+	// Alternative (better) approach
+	for _, mne := range v.sdnConfig.NetworkElements {
+		fmt.Printf("[%s] - Found mne target: %s with addr: %s\n", v.Name, mne.Name, mne.TransportAddress)
+		opt := &tpb.TransportOption{
+			Address:  mne.TransportAddress,
+			Username: "admin",
+			Password: "admin",
+			Tls:      true,
+			TransportOption: &tpb.TransportOption_GnmiTransportOption{
+				GnmiTransportOption: &tpb.GnmiTransportOption{},
+			},
 		}
+		pluginID, _ := uuid.Parse("d1c269a2-6482-4010-b0d8-679dff73153b") // TODO Get this dynamically
+		pndID, _ := uuid.Parse(v.pnd.Id)
+		listResponse, err := gosdnutil.AddNetworkElement(v.auth, v.auth.GetAddress(), mne.Name, mne.ID, opt, pluginID, pndID, []string{"/"})
+		if err != nil {
+			return fmt.Errorf("[%s] - Failed to add network element: %w\n", v.Name, err)
+		}
+		fmt.Printf("[%s] - Got response from AddNetworkElement: %v\n", v.Name, listResponse)
+		fmt.Printf("[%s] - Success: registered mne with gosdn controller\n", v.Name)
 	}
-
 	return nil
 }
 
@@ -331,6 +321,7 @@ func (v *VEnv) UploadTopology() error {
 // What this does: Upload the nodes that make up the gosdn environment
 // via the gosdn configuration service to the db (applies to db, message broker, etc.)
 // This is probably not the intended use for the configuration interface?
+// TODO Delete! Wrong approach
 func (v *VEnv) UploadClabConfig() error {
 	if v.clabData == nil {
 		return fmt.Errorf("Can't upload clabconfig when it hasn't been loaded yet (is nil pointer)")
diff --git a/scripts/setup-clab-slim.sh b/scripts/setup-clab-slim.sh
index 390ae6f8e..b7943d03f 100755
--- a/scripts/setup-clab-slim.sh
+++ b/scripts/setup-clab-slim.sh
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/usr/bin/env bash
 
 ## Note: This script does not clean up network elements already existing in the storage. Running it repeatedly will
 ## create a garbage storage.
@@ -9,7 +9,7 @@ GOSDN_ADDRESS="172.100.0.5:55055"
 ADMINPW="TestPassword"
 OPENCONFIG_PLUGIN="d1c269a2-6482-4010-b0d8-679dff73153b"
 ## Adjust this if timer is to short.
-SLEEP_TIMER=20
+SLEEP_TIMER=0
 
 ## Check for provided flags.
 while getopts s: flag
@@ -27,8 +27,8 @@ echo 'Logging in via gosdnc and setting up gnmi-targets in controller...'
 ## Call login and create entries for all the network elements.
 ## Could be a bit more automated in the future, but fine for now.
 $GOSDNC_PATH login --controller $GOSDN_ADDRESS --u admin --p $ADMINPW
-$GOSDNC_PATH mne create --address 172.100.0.11:7030 --name gnmi-target-switch0 --password admin --plugin-id $OPENCONFIG_PLUGIN --tls --username admin
-$GOSDNC_PATH mne create --address 172.100.0.12:7030 --name gnmi-target-switch1 --password admin --plugin-id $OPENCONFIG_PLUGIN --tls --username admin
+$GOSDNC_PATH mne create --address 172.100.0.11:7030 --name gnmi-target-switch0 --password admin --plugin-id $OPENCONFIG_PLUGIN --tls --username admin --uuid 1dc7f8b1-d520-4926-b0af-65c143607211
+$GOSDNC_PATH mne create --address 172.100.0.12:7030 --name gnmi-target-switch1 --password admin --plugin-id $OPENCONFIG_PLUGIN --tls --username admin --uuid d0d59768-b990-45e7-833f-06c525692cd1
 
 ## Start gosdnc in prompt mode if GOSDNC_START=true
 if $GOSDNC_START; then
-- 
GitLab