diff --git a/applications/rtdt-manager/clab-config/clab-config.go b/applications/rtdt-manager/clab-config/clab-config.go new file mode 100644 index 0000000000000000000000000000000000000000..97ff714c7cd55045abe44fe40961d713b778f20d --- /dev/null +++ b/applications/rtdt-manager/clab-config/clab-config.go @@ -0,0 +1,187 @@ +package clabconfig + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "gopkg.in/yaml.v3" +) + +// 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"` +} + +// Read file and parse into ClabConfig struct +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) + } + + 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 +} +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 + + 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, ".") + + // 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, ":") + } + newName := fmt.Sprintf("%s-%s", name, postfix) + derivedConfig.Topology.Nodes[newName] = node + } + + // Update Links so they reference the correct nodes + 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) + } + parts[0] = fmt.Sprintf("%s-%s", parts[0], postfix) // Update the node name + clabconfig.Topology.Links[i].Endpoints[j] = strings.Join(parts, ":") + } + } + + return &derivedConfig, nil +} + +// WriteConfig writes the Config struct to a YAML file +func WriteConfig(filename string, dirname string, config *ClabConfig) error { + data, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("failed to marshal YAML: %w", err) + } + + fullPath := filepath.Join(dirname, filename) + err = os.WriteFile(fullPath, data, 0644) + if err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + return nil +} + +func ClabDeploy(filename string, dirname string, stopchan chan os.Signal) error { + fullPath := filepath.Join(dirname, filename) + cmd := exec.Command("sudo", "containerlab", "deploy", "-t", fullPath, "--reconfigure") + + // Run the command in a Goroutine + done := make(chan error, 1) + go func() { + output, err := cmd.CombinedOutput() // Use CombinedOutput to capture stdout and stderr + if err != nil { + fmt.Printf("Error during deployment: %s\n", err) + } else { + fmt.Println(string(output)) + } + 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 <-stopchan: // Signal received to interrupt + if err := cmd.Process.Kill(); err != nil { + fmt.Printf("Failed to kill process: %v\n", err) + } + return fmt.Errorf("deployment interrupted by signal") + } +} diff --git a/applications/rtdt-manager/data/gosdn_slim.clab.yaml b/applications/rtdt-manager/data/gosdn_slim.clab.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f4d76846cddf22b054f8cee3c4e39d70589cb7ea --- /dev/null +++ b/applications/rtdt-manager/data/gosdn_slim.clab.yaml @@ -0,0 +1,101 @@ +name: gosdn_csbi_arista_base + +mgmt: + network: gosdn-csbi-arista-base-net + ipv4-subnet: 172.100.0.0/16 + ipv6-subnet: 2001:db8::/64 + mtu: 1500 + +topology: + nodes: + plugin-registry: + kind: linux + image: plugin-registry + mgmt-ipv4: 172.100.0.16 + + gosdn: + kind: linux + image: gosdn + ports: + - 55055:55055 + - 8080:8080 + - 40000:40000 + cmd: --config /app/configs/containerlab-gosdn.toml + mgmt-ipv4: 172.100.0.5 + env: + GOSDN_ADMIN_PASSWORD: TestPassword + binds: + - ../../artifacts/ssl/gosdn:/app/ssl + + gnmi-target-switch0: + kind: linux + image: registry.code.fbi.h-da.de/danet/gnmi-target/debian:master + #only for local use + #image: gnmi-target:latest + binds: + - ../../artifacts/ssl/gnmi-target:/etc/gnmi-target/ssl + ports: + - 7030:7030 + 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 + startup-delay: 5 + + gnmi-target-switch1: + kind: linux + image: registry.code.fbi.h-da.de/danet/gnmi-target/debian:master + #only for local use + #image: gnmi-target:latest + binds: + - ../../artifacts/ssl/gnmi-target:/etc/gnmi-target/ssl + ports: + - 7031:7030 + 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 + startup-delay: 5 + + centos0: + kind: linux + image: centos:8 + mgmt-ipv4: 172.100.0.3 + group: server + + centos1: + kind: linux + image: centos:8 + mgmt-ipv4: 172.100.0.4 + group: server + + mongodb: + kind: linux + image: mongo:7 + ports: + - 27017:27017 + env: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: example + mgmt-ipv4: 172.100.0.13 + + mongodb-express: + kind: linux + image: mongo-express:1.0.2 + ports: + - 8081:8081 + env: + ME_CONFIG_MONGODB_AUTH_USERNAME: root + ME_CONFIG_MONGODB_AUTH_PASSWORD: example + ME_CONFIG_MONGODB_SERVER: mongodb + ME_CONFIG_BASICAUTH: "false" + mgmt-ipv4: 172.100.0.14 + + 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 ae0284da71996b51b093e3a08cf01336457361a4..5c58ca8f4a22f5f7eeec0db08c079cf05a09969d 100644 --- a/applications/rtdt-manager/main.go +++ b/applications/rtdt-manager/main.go @@ -16,11 +16,14 @@ func main() { var url string var pass string var user string + var topology_file string flag.StringVar(&url, "url", "172.100.0.5:55055", "Address of the gosdn controller") flag.StringVar(&pass, "p", "TestPassword", "Password for admin user") flag.StringVar(&user, "u", "admin", "Username") + flag.StringVar(&topology_file, "topology", "data/clab.yaml", "Containerlab file on the basis of which to create topo") flag.Parse() fmt.Println("Trying to connect to gosdn controller at ", url) + fmt.Println("Topology file path: ", topology_file) dialOption := grpc.WithTransportCredentials(insecure.NewCredentials()) conn, err := grpc.NewClient(url, dialOption, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(100*1024*1024))) @@ -33,11 +36,11 @@ func main() { auth := rtdt_auth.NewRtdtAuth(user, url, pass, conn) // logs in and stores token rtdt_app := app.NewApp(conn, auth) - rtdtMan := RtdtMan.NewRtdtManager(conn, auth) - if rtdtMan == nil { - fmt.Println("Couldn't initialize rtdt-manager, quitting!") - return - } + rtdtMan := RtdtMan.NewRtdtManager(conn, auth, topology_file) + if rtdtMan == nil { + fmt.Println("Couldn't initialize rtdt-manager, quitting!") + return + } rtdt_app.AddManager(rtdtMan) _ = rtdtMan diff --git a/applications/rtdt-manager/rtdt-manager/rtdt-manager.go b/applications/rtdt-manager/rtdt-manager/rtdt-manager.go index aa44fd9c0c61fdc74a47efe4e227c18d50a6aa39..bdd440074b754efc92138eccd45734ee16f52235 100644 --- a/applications/rtdt-manager/rtdt-manager/rtdt-manager.go +++ b/applications/rtdt-manager/rtdt-manager/rtdt-manager.go @@ -4,8 +4,11 @@ import ( "fmt" "os" "os/exec" + "os/signal" "path" "path/filepath" + "strings" + "syscall" "time" "code.fbi.h-da.de/danet/gosdn/application-framework/event" @@ -16,31 +19,96 @@ import ( confManPb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/configurationmanagement" pnd "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/pnd" topoPb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/topology" + "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/clab-config" "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/rtdt-auth" + "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/rtdt-topology" + "code.fbi.h-da.de/danet/gosdn/applications/venv-manager/containerlab" "google.golang.org/grpc" + "gopkg.in/yaml.v3" ) +func now() int64 { + return int64(time.Now().Nanosecond()) +} + +func getGosdnPath() (string, error) { + var execPath string + var absExecPath string + var err error + if execPath, err = os.Executable(); err != nil { + return "", nil + } + if absExecPath, err = filepath.Abs(execPath); err != nil { + return "", nil + } + executableDir := filepath.Dir(absExecPath) + projectRoot := filepath.Dir(executableDir) + + fmt.Println("Project Root Path:", projectRoot) + return projectRoot, nil + +} + type RtdtManager struct { auth *rtdt_auth.RtdtAuth // auth struct for realnet gosdn conn *grpc.ClientConn // connection to twin's gosdn instance Pnd *pnd.PrincipalNetworkDomain // PND for realnet gosdn topo *topoPb.Topology // Topology with which to create clab.yaml sdnConfig string + clabFilename string // Clab file that exemplifies the topology + topoData *containerlab.YamlStruct + clabData *clabconfig.ClabConfig eventService event.ServiceInterface // Receive events from realnet gosdn - stopChan *chan os.Signal - // TODO auth and conn for virtual net + stopChan chan os.Signal // Global stop channel TODO Can I use that like this? + // TODO auth and conn for virtual net? } -func NewRtdtManager(conn *grpc.ClientConn, auth *rtdt_auth.RtdtAuth) *RtdtManager { +func NewRtdtManager(conn *grpc.ClientConn, auth *rtdt_auth.RtdtAuth, clabFilename string) *RtdtManager { + projectRoot, _ := getGosdnPath() + rMan := RtdtManager{ - conn: conn, - auth: auth, + conn: conn, + auth: auth, + clabFilename: clabFilename, + stopChan: make(chan os.Signal, 1), } + signal.Notify(rMan.stopChan, os.Interrupt, syscall.SIGTERM) + if err := rMan.fetchPndUUID(); err != nil { fmt.Println(err) return nil } - if err := rMan.fetchTopology(); err != nil { + // The topology of the currently running realnet needs to be parsed. We get this + // from the clab.yaml file that was used to create it, that means that --topolog needs to point + // to it + var err error + if rMan.clabData, err = clabconfig.LoadConfig(rMan.clabFilename); err != nil { + fmt.Println(err) + return nil + } + var derivedConfig *clabconfig.ClabConfig + if derivedConfig, err = clabconfig.DeriveConfig(rMan.clabData, "172.101.0.0/16", "2001:db9::/64", "twin"); err != nil { + fmt.Println(err) + return nil + } + + clabConfigDir := filepath.Join(projectRoot, "dev_env_data/clab") + if err := clabconfig.WriteConfig("clab-derived.yaml", clabConfigDir, derivedConfig); err != nil { + return nil + } + if err := clabconfig.ClabDeploy("clab-derived.yaml", clabConfigDir, rMan.stopChan); err != nil { + return nil + } + fmt.Println("Success: RtdtManager created") + return &rMan + // Now we need to apply the topology to gosdn because it is not done automatically when creating + // devices + if err := rtdt_topology.ApplyTopology(rMan.topoData); err != nil { + fmt.Println(err) + return nil + } + // Write the topology to a file to check + if err := rMan.writeModifiedTopologyToFile(rMan.topoData); err != nil { fmt.Println(err) return nil } @@ -52,14 +120,13 @@ func NewRtdtManager(conn *grpc.ClientConn, auth *rtdt_auth.RtdtAuth) *RtdtManage // fmt.Println(err) // return nil // } - fmt.Println("Success: RtdtManager created") - return &rMan + return nil } func (rMan *RtdtManager) fetchPndUUID() error { pndService := pnd.NewPndServiceClient(rMan.conn) ctx := rMan.auth.CreateContextWithAuthorization() - pndResponse, err := pndService.GetPndList(ctx, &pnd.GetPndListRequest{Timestamp: int64(time.Now().Nanosecond())}) + pndResponse, err := pndService.GetPndList(ctx, &pnd.GetPndListRequest{Timestamp: now()}) if err != nil { return fmt.Errorf("Failed to retrieve PND information: %w", err) } @@ -72,6 +139,48 @@ func (rMan *RtdtManager) fetchPndUUID() error { } } +func (rMan *RtdtManager) applyTopology() error { + topoService := topoPb.NewTopologyServiceClient(rMan.conn) + ctx := rMan.auth.CreateContextWithAuthorization() + topoService.AddLink(ctx, &topoPb.AddLinkRequest{Timestamp: now()}) + + return nil +} + +// \cite venv-manager +// Write a clab.yaml file to launch a different +func (rMan *RtdtManager) writeModifiedTopologyToFile(clabStruct *containerlab.YamlStruct) error { + rMan.topoData.Mgmt.Network = "gosdn-csbi-arist-twin-net" + + splitMainNetwork := strings.Split(rMan.topoData.Mgmt.Ipv4Subnet, ".") + splitMainNetwork[1] = "101" + rMan.topoData.Mgmt.Ipv4Subnet = strings.Join(splitMainNetwork, ".") + + splitMainNetworkIPv6 := strings.Split(rMan.topoData.Mgmt.Ipv6Subnet, ":") + splitMainNetworkIPv6[1] = "db9" + rMan.topoData.Mgmt.Ipv6Subnet = strings.Join(splitMainNetworkIPv6, ":") + + // Different network for our twin + for i, node := range rMan.topoData.Topology.Nodes { + splitIPv4 := strings.Split(node.MgmtIpv4, ".") + splitIPv4[1] = "101" + node.MgmtIpv4 = strings.Join(splitIPv4, ".") + rMan.topoData.Topology.Nodes[i] = node + } + yaml, err := yaml.Marshal(clabStruct) + if err != nil { + return err + } + + fname := "./topo.clab.tmp.yaml" + err = os.WriteFile(fname, yaml, 0600) + if err != nil { + return err + } + + return nil +} + // This retrieves the topology from the running realnet gosdn instance // This is needed to generate the clab file to be used with the virtual net // TODO Solve this not returning anything diff --git a/applications/rtdt-manager/rtdt-topology/rtdt-topology.go b/applications/rtdt-manager/rtdt-topology/rtdt-topology.go new file mode 100644 index 0000000000000000000000000000000000000000..e7a0b3d94f0d8e41f1e0bed27db8d04accb7dd54 --- /dev/null +++ b/applications/rtdt-manager/rtdt-topology/rtdt-topology.go @@ -0,0 +1,78 @@ +package rtdt_topology + +import ( + "fmt" + "os" + "path/filepath" + + "code.fbi.h-da.de/danet/gosdn/applications/venv-manager/containerlab" + "gopkg.in/yaml.v3" +) + +// Probably don't need a package, just use api inside rtdt-manager.go +// import ( +// "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/topology" +// "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/rtdt-auth" +// "google.golang.org/grpc" +// ) +// +// type Link struct { +// Id string +// name string +// } +// type Port struct { +// } +// +// type Node struct { +// } +// +// type Topology struct { +// Links *[]Link +// Ports *[]Port +// Nodes *[]Node +// } +// +// func NewTopology() { +// +// return &Topology{} +// } +// +// func ApplyTopology(conn grpc.ClientConnInterface, auth rtdt_auth.RtdtAuth) { +// ctx := auth.CreateContextWithAuthorization() +// topoService := topology.NewTopologyServiceClient(conn) +// addLink := topology.AddLinkRequest{} +// topoService.AddLink(ctx, addLink) +// } +// +// func LoadTopologyFromFile() + +func ParseTopology(filename string) (*containerlab.YamlStruct, error) { + fmt.Println("Parsing file: ", filename) + var absFilepath string + var err error + if absFilepath, err = filepath.Abs(filename); err != nil { + return nil, fmt.Errorf("Failed to convert filename %v to absolute path", filename) + } + fmt.Printf("absolute filepath: %v\n", absFilepath) + + file, err := os.Open(absFilepath) + if err != nil { + return nil, fmt.Errorf("Encountered error while trying to parse clab file into topology: %v", err) + } + decoder := yaml.NewDecoder(file) + var topoData containerlab.YamlStruct + if err := decoder.Decode(&topoData); err != nil { + return nil, fmt.Errorf("Failed to decode YAML: %v", err) + } + + fmt.Println("Successfully parsed given clab file into topology, nice!") + + return &topoData, nil +} + + +// Needs to be in rtdt-manager +func ApplyTopology(topoYaml *containerlab.YamlStruct) error { + + return nil +}