Skip to content
Snippets Groups Projects
  • S.H.'s avatar
    63ab11b3
    sdnconfig.go: Add sdnconfig structures based on json that is generated by · 63ab11b3
    S.H. authored
    configurationManagementService. This is a better way of managing network
    elements and topology than the structs in topology.go. Also added a
    sdnconfig json file venv_sdnconfig_full.json that was retrieved by
    filling the db via topology and network element services and then using
    configurationManagementService to get the json data.
    63ab11b3
    History
    sdnconfig.go: Add sdnconfig structures based on json that is generated by
    S.H. authored
    configurationManagementService. This is a better way of managing network
    elements and topology than the structs in topology.go. Also added a
    sdnconfig json file venv_sdnconfig_full.json that was retrieved by
    filling the db via topology and network element services and then using
    configurationManagementService to get the json data.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
venv.go 18.43 KiB
package venv

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"

	configPb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/configurationmanagement"
	"code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement"
	"code.fbi.h-da.de/danet/gosdn/api/go/gosdn/pnd"
	topoPb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/topology"
	tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport"
	clab "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/clab-config"
	clabconfig "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/clab-config"
	"code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/gosdnutil"
	rtdt_auth "code.fbi.h-da.de/danet/gosdn/applications/rtdt-manager/rtdt-auth"
	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"
	yangparser "code.fbi.h-da.de/danet/gosdn/applications/venv-manager/yang-parser"
	"code.fbi.h-da.de/danet/gosdn/models/generated/openconfig"
	uuid "github.com/google/uuid"
	gnmi "github.com/openconfig/gnmi/proto/gnmi"
	"github.com/openconfig/ygot/ygot"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"gopkg.in/yaml.v3"
)

type VEnv struct {
	Name                 string
	auth                 *rtdt_auth.RtdtAuth
	conn                 *grpc.ClientConn // The connection to this specific environment's gosdn
	pnd                  *pnd.PrincipalNetworkDomain
	clabData             *clabconfig.ClabConfig // Represents yaml file that was used to deploy
	sdnConfig            *sdnconfig.SdnConfig   // Represents json config file for configuration grpc interface
	topology             *rtdt_topology.Topology
	clabFilename         string // This is the name of the yaml file clabData is based on
	StopChan             <-chan struct{}
	waitGroup            *sync.WaitGroup
	containerRegistryURL string
	savedChanges         []*networkelement.ChangeRequest
}

// 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 {
	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
	//clabFilename := name + "-clab.yaml"

	// TODO:
	// Use baseClab and sdnConfig to construct a config file for containerlab

	if err = clab.ClabDeploy(clabFilename); err != nil {
		fmt.Printf("[%s] - Failed to deploy the network\n", name)
		return nil
	}

	// 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)
		return nil
	}
	// get gosdn address inside clab environment
	var gosdnAddress string
	for nodename, val := range clabData.Topology.Nodes {
		if strings.HasPrefix(nodename, "gosdn") {
			gosdnAddress = val.MgmtIPv4 + ":55055"
			fmt.Printf("[%s} - Found gosdn ipv4: %s\n", name, gosdnAddress)
		}
	}

	fmt.Printf("[%s] - Sleep for 15 seconds to give containers time to settle..\n", name)
	time.Sleep(time.Second * 15)
	// Now log into gosdn physical network
	dialOption := grpc.WithTransportCredentials(insecure.NewCredentials())
	gosdnconn, err := grpc.NewClient(gosdnAddress, dialOption, grpc.WithDefaultCallOptions())
	if err != nil {
		fmt.Printf("[%s] - Failed to create connection to gosdn\n", name)
		return nil
	} else {
		fmt.Printf("[%s] - Successfully created connection to gosdn\n", name)
	}
	gosdnauth := rtdt_auth.NewRtdtAuth(user, gosdnAddress, pass, gosdnconn) // logs in and stores token
	if gosdnauth == nil {
		fmt.Printf("[%s] - Couldn't log in to gosdn, quitting!\n", name)
		return nil
	} else {
		fmt.Printf("[%s] - Successfully logged into gosdn as user: %s, with password: %s, session token: %v\n", name, gosdnauth.GetUsername(), gosdnauth.GetPassword(), gosdnauth.GetSessionToken())
	}
	// Get PND of gosdn in created venv
	var gosdn_pnd *pnd.PrincipalNetworkDomain
	for {
		gosdn_pnd, err = gosdnutil.FetchPnd(gosdnconn, gosdnauth)
		if err == nil {
			break
		}
		fmt.Printf("[%s] - Couldn't retrieve PND, retrying in 2 seconds..\n", name)
		time.Sleep(time.Second * 2)
	}

	// load topo into DB via API
	return &VEnv{
		auth:                 gosdnauth,
		pnd:                  gosdn_pnd,
		conn:                 gosdnconn,
		clabData:             clabData,
		clabFilename:         clabFilename,
		waitGroup:            wg,
		topology:             nil,                                            // set this later
		containerRegistryURL: "registry.code.fbi.h-da.de/danet/gnmi-target/", // TODO: Could let user choose
	}
}

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)
		}
	}

	return nil
}

// Source: "code.fbi.h-da.de/danet/gosdn/applications/venv-manager/venv-manager/venv-manager.go"
// commit: 0264b698286b6cbb965d743078c681f8af55edf6
func (v *VEnv) loadNetworkElementModelPathsIntoGosdn(ctx context.Context, conn *grpc.ClientConn, nodes *[]*rtdt_topology.Node) error {
	networkElementService := networkelement.NewNetworkElementServiceClient(conn)

	paths := [2]string{"/lldp/config/system-description", "/system/state/"}
	for _, path := range paths {
		for _, node := range *nodes {
			_, err := networkElementService.GetPath(ctx, &networkelement.GetPathRequest{Mneid: node.ID, Pid: v.pnd.Id, Path: path})
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// Based on getAndAddMoreData() in: "code.fbi.h-da.de/danet/gosdn/applications/venv-manager/venv-manager/venv-manager.go"
// commit: 0264b698286b6cbb965d743078c681f8af55edf6
// What this does: Load yang paths into topo, then iterate over
// nodes and fill data in
func (v *VEnv) ConstructTopology() error {
	if v.clabData == nil {
		return fmt.Errorf("Error: Trying to construct topology without containerlab file being loaded first")
	}
	// Either fill topology from clabData or from database?
	ctx := v.auth.CreateContextWithAuthorization()
	conn := v.conn
	var path = "/"
	var ygotPath *gnmi.Path

	// Create 'root' path to be able to load the whole model from the store.
	var err error
	ygotPath, err = ygot.StringToPath(path, ygot.StructuredPath)
	if err != nil {
		return err
	}

	// just to load model data into goSDN to guaranteed have new data available for get request
	err = v.loadNetworkElementModelPathsIntoGosdn(ctx, conn, &v.topology.Nodes)
	if err != nil {
		return err
	}

	networkElementService := networkelement.NewNetworkElementServiceClient(conn)

	for iterator, node := range v.topology.Nodes {
		getNetworkElementResponse, _ := networkElementService.Get(ctx, &networkelement.GetRequest{Mneid: node.ID})
		if err != nil {
			return err
		}

		var marshalledYangData openconfig.Device

		err = yangparser.Unmarshal([]byte(getNetworkElementResponse.Mne.Model), ygotPath, &marshalledYangData)
		if err != nil {
			return err
		}

		mgmntAddress := strings.Split(getNetworkElementResponse.Mne.TransportAddress, ":")
		v.topology.Nodes[iterator].MgmtIpv4 = mgmntAddress[0]
		v.topology.Nodes[iterator].YangData = marshalledYangData
		v.topology.Nodes[iterator].FillAllFields(v.containerRegistryURL)
	}

	return nil
}

// Create rtdt_topology.Topology from clabconfig.Topology
func (v *VEnv) DeriveTopologyFromClabData() error {
	if v.clabData == nil {
		return fmt.Errorf("Can't derive topology without clabData\n")
	}
	v.topology = &rtdt_topology.Topology{}
	var topoNodes []*rtdt_topology.Node
	// Get all the nodes from clab structure
	for nodeName, node := range v.clabData.Topology.Nodes {
		topoNode := rtdt_topology.Node{
			ID:       uuid.NewString(),
			Name:     nodeName,
			Kind:     node.Kind,
			Image:    node.Image,
			MgmtIpv4: node.MgmtIPv4,
		}
		topoNodes = append(topoNodes, &topoNode)
	}
	v.topology.Nodes = topoNodes
	for _, link := range v.clabData.Topology.Links {
		if len(link.Endpoints) != 2 {
			return fmt.Errorf("Couldn't parse clabData into topology\n")
		}
		var topoLink = rtdt_topology.Link{}
		// determine Node name and port
		node0Full := strings.Split(link.Endpoints[0], ":")
		node0Name := node0Full[0]
		node0Port := node0Full[1]
		node1Full := strings.Split(link.Endpoints[1], ":")
		node1Name := node1Full[0]
		node1Port := node1Full[1]

		// find the node that has right name, add links and ports
		topoLink.SourceNode = v.topology.GetNodeByName(node0Name)
		topoLink.SourcePort = &rtdt_topology.Port{Name: node0Port, ID: uuid.NewString()}
		v.topology.Ports = append(v.topology.Ports, topoLink.SourcePort)
		topoLink.TargetNode = v.topology.GetNodeByName(node1Name)
		topoLink.TargetPort = &rtdt_topology.Port{Name: node1Port, ID: uuid.NewString()}
		v.topology.Ports = append(v.topology.Ports, topoLink.TargetPort)
		v.topology.Links = append(v.topology.Links, &topoLink)
		v.topology.Nodes = topoNodes
	}
	return nil
}

func (v *VEnv) ApplyRoutes() error {
	conn := v.auth.GetConn()
	hostIP := int64(1)
	for _, link := range v.topology.Links {
		req := topoPb.AddRoutingTableRequest{
			Timestamp: util.Now(),
			RoutingTable: &topoPb.RoutingTable{
				Id: uuid.NewString(),
				Routes: []*topoPb.Route{
					{
						Id:            uuid.NewString(),
						TargetIPRange: "16",
						NextHopIP:     "192.168.178." + strconv.FormatInt(hostIP, 10),
						PortID:        link.SourcePort.ID,
					},
				},
			},
		}
		hostIP++
		routeService := topoPb.NewRoutingTableServiceClient(conn)

		ctx := v.auth.CreateContextWithAuthorization()
		reply, err := routeService.AddRoutingTable(ctx, &req)
		if err != nil {
			return fmt.Errorf("AddRoutingTable failed: %v\n", err)
		}
		fmt.Println("Successfully added Link:", reply)
	}
	return nil
}

// TODO: Fix hardcoded IP addresses
func (v *VEnv) UploadTopology() error {
	conn := v.auth.GetConn()
	topoService := topoPb.NewTopologyServiceClient(conn)
	for _, link := range v.topology.Links {
		ctx := v.auth.CreateContextWithAuthorization()

		l := link.Convert()
		l.Name = l.SourceNode.Name + ":" + l.TargetNode.Name
		l.SourcePort.Configuration = &topoPb.Configuration{Ip: "192.168.178.2", PrefixLength: 24}
		l.TargetPort.Configuration = &topoPb.Configuration{Ip: "192.168.178.3", PrefixLength: 24}

		addLinkRequest := &topoPb.AddLinkRequest{
			Timestamp: util.Now(),
			Link:      l,
		}
		fmt.Println("AddLink is:", addLinkRequest.String())
		topoResponse, err := topoService.AddLink(ctx, addLinkRequest)
		if err != nil {
			return err
		}
		fmt.Printf("Successfully uploaded Link to DB: %s\n", topoResponse.String())
	}
	return nil
}

// 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?
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)")
	}
	var clabToUpload = *v.clabData
	for nodename, node := range v.clabData.Topology.Nodes {
		if !strings.Contains(nodename, "gosdn") &&
			!strings.Contains(nodename, "mongodb") &&
			!strings.Contains(nodename, "rabbitmq") &&
			!strings.Contains(nodename, "plugin-registry") {
			fmt.Println("Removing node:", node)
			delete(clabToUpload.Topology.Nodes, nodename)
		}
	}
	clabToUpload.Topology.Links = nil

	clabConfig, err := yaml.Marshal(clabToUpload)
	if err != nil {
		return fmt.Errorf("Encountered error marshalling clabConfig: %w", err)
	}
	var jsonData interface{}
	if err := yaml.Unmarshal(clabConfig, &jsonData); err != nil {
		return fmt.Errorf("failed to unmarshal YAML to JSON: %w", err)
	}
	jsonBytes, err := json.MarshalIndent(jsonData, "", " ")
	if err != nil {
		return fmt.Errorf("Failed to marshal JSON: %w", err)
	}

	req := &configPb.ImportSDNConfigRequest{
		Timestamp:     util.Now(),
		Pid:           v.pnd.GetId(), // TODO: check if this can be nil
		SdnConfigData: string(jsonBytes),
	}
	conn := v.auth.GetConn()
	configService := configPb.NewConfigurationManagementServiceClient(conn)
	ctx := v.auth.CreateContextWithAuthorization()
	configResponse, err := configService.ImportSDNConfig(ctx, req)
	if err != nil {
		return err
	}
	fmt.Println("Successfully uploaded clab config:", configResponse.String())
	return nil
}

// TODO This doesn't really work this way..
func (v *VEnv) RetrieveClabConfig() error {
	ctx := v.auth.CreateContextWithAuthorization()
	conn := v.auth.GetConn()
	configService := configPb.NewConfigurationManagementServiceClient(conn)

	req := &configPb.ExportSDNConfigRequest{
		Timestamp: util.Now(),
		Pid:       v.pnd.GetId(),
	}

	configResponse, err := configService.ExportSDNConfig(ctx, req)
	if err != nil {
		return fmt.Errorf("Failed retrieving config clab data: %w", err)
	}
	var jsonData map[string]interface{}
	err = json.Unmarshal([]byte(configResponse.SdnConfigData), &jsonData)
	if err != nil {
		return fmt.Errorf("Failed to unmarshal SDN config data: %w", err)
	}
	// Pretty-print the JSON data
	formattedJSON, err := json.MarshalIndent(jsonData, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to format JSON data: %w", err)
	}

	fmt.Println("Retrieved SDN Config Data:")
	fmt.Println(string(formattedJSON)) // Print the formatted JSON

	return nil

}

func getTypedValue(value string) *gnmi.TypedValue {
	if boolVal, err := strconv.ParseBool(value); err == nil {
		return &gnmi.TypedValue{Value: &gnmi.TypedValue_BoolVal{BoolVal: boolVal}}
	} else if uintVal, err := strconv.ParseUint(value, 10, 64); err == nil {
		return &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: uintVal}}
	} else if intVal, err := strconv.ParseInt(value, 10, 64); err == nil {
		return &gnmi.TypedValue{Value: &gnmi.TypedValue_IntVal{IntVal: intVal}}
	} else {
		return &gnmi.TypedValue{Value: &gnmi.TypedValue_StringVal{StringVal: value}}
	}
}

func (v *VEnv) SetGnmiPath(path, value, mneid string, save bool) error {
	ctx := v.auth.CreateContextWithAuthorization()
	fmt.Println("--IN SETGNMIPATH-----------------------")
	mneService := networkelement.NewNetworkElementServiceClient(v.conn)
	gnmiPath, err := ygot.StringToStructuredPath(path)
	if err != nil {
		return fmt.Errorf("Encountered error while trying to parse string path into gnmi path: %w", err)
	}
	//gosdnAddr := v.auth.GetAddress()
	gnmiVal := getTypedValue(value)

	changeRequest := &networkelement.ChangeRequest{
		Mneid: mneid,
		Path:  gnmiPath,
		Value: gnmiVal,
		ApiOp: networkelement.ApiOperation_API_OPERATION_UPDATE,
	}

	changeRequests := []*networkelement.ChangeRequest{changeRequest}

	pid := v.pnd.Id
	setPathResponse, err := mneService.SetPathList(ctx, &networkelement.SetPathListRequest{
		Timestamp:     util.Now(),
		Pid:           pid,
		ChangeRequest: changeRequests,
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return err
	}
	fmt.Println("setPathResponse: ", setPathResponse.String())

	// Now try to commit the change
	fmt.Println("Now trying to commit change with ID:", setPathResponse.GetResponses()[0].GetId())
	setChange := &networkelement.SetChange{
		Cuid: setPathResponse.GetResponses()[0].GetId(),
		Op:   networkelement.Operation_OPERATION_COMMIT,
	}
	setChangeListRequest := networkelement.SetChangeListRequest{
		Change:    []*networkelement.SetChange{setChange},
		Timestamp: util.Now(),
		Pid:       pid,
	}
	clResponse, err := mneService.SetChangeList(ctx, &setChangeListRequest)
	if err != nil {
		fmt.Println("Error, failed to commit changes:", err)
		return err
	} else {
		fmt.Println("Successfully applied changes:", clResponse)
	}
	if save {
		v.savedChanges = append(v.savedChanges, changeRequest)
	}
	return nil
}

// For later, topology stuff
// func (rMan *RtdtManager) applyTopology() error {
// 	topoService := topoPb.NewTopologyServiceClient(rMan.conn)
// 	ctx := rMan.auth.CreateContextWithAuthorization()
// 	topoService.AddLink(ctx, &topoPb.AddLinkRequest{Timestamp: util.Now()})
//
// 	return nil
// }

// This retrieves the topology from the running gosdn instance
// This is needed to generate the clab file to be used with the virtual net
func (v *VEnv) fetchTopology() error {
	topoService := topoPb.NewTopologyServiceClient(v.conn)
	ctx := v.auth.CreateContextWithAuthorization()
	topoResponse, err := topoService.GetTopology(ctx, &topoPb.GetTopologyRequest{Timestamp: util.Now()})
	if err != nil {
		return fmt.Errorf("Failed to retrieve Topology: %w", err)
	}
	fmt.Printf("Successfully read topology, state is: %s\n", topoResponse.String())
	fmt.Printf("Length of Links: %d\n", len(topoResponse.Toplogy.Links))
	fmt.Printf("String of Topology: %s\n", topoResponse.Toplogy.String())
	for i, link := range topoResponse.Toplogy.GetLinks() {
		fmt.Printf("index %d, linkID: %s, linkName: %s\n", i, link.GetId(), link.GetName())
	}
	// TODO: Needs to set topo
	return nil
}

// {G,S}ETTERS
func (v VEnv) GetName() string {
	return v.Name
}
func (v VEnv) GetClabData() *clab.ClabConfig {
	return v.clabData
}
func (v VEnv) GetClabFilePath() string {
	return v.clabFilename
}
func (v *VEnv) GetConn() *grpc.ClientConn {
	return v.conn
}
func (v VEnv) GetPnd() *pnd.PrincipalNetworkDomain {
	return v.pnd
}
func (v VEnv) GetAuth() *rtdt_auth.RtdtAuth {
	return v.auth
}
func (v *VEnv) GetWaitgroup() *sync.WaitGroup {
	return v.waitGroup
}
func (v *VEnv) GetTopology() *rtdt_topology.Topology {
	return v.topology
}

func (v *VEnv) GetSavedChanges() *[]*networkelement.ChangeRequest {
	return &v.savedChanges
}