Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
venv.go 16.75 KiB
package venv

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"

	"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/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"
)

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
	clabFilename         string                 // This is the name of the yaml file clabData is based on
	StopChan             <-chan struct{}
	waitGroup            *sync.WaitGroup
	topology             *rtdt_topology.Topology
	containerRegistryURL string
	savedChanges         []*networkelement.ChangeRequest
}

// Accepts a yaml filename to deploy a container lab environment
// TODO Split up into sub-functions
func NewVEnv(name, topoYamlFile, 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
	if err = clab.ClabDeploy(topoYamlFile); err != nil {
		fmt.Printf("[%s] - Failed to deploy the physical network\n", name)
		return nil
	}

	// After having deployed it, load the config into clabData
	var clabData *clab.ClabConfig
	clabData, err = clab.LoadConfig(topoYamlFile)
	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..", name)
		time.Sleep(time.Second * 2)
	}

	// load topo into DB via API
	return &VEnv{
		auth:                 gosdnauth,
		pnd:                  gosdn_pnd,
		conn:                 gosdnconn,
		clabData:             clabData,
		clabFilename:         topoYamlFile,
		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
}

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
}

func (v *VEnv) RetrieveTopology() error {
	conn := v.auth.GetConn()
	ctx := v.auth.CreateContextWithAuthorization()
	topoService := topoPb.NewTopologyServiceClient(conn)
	topoResponse, err := topoService.GetTopology(ctx, &topoPb.GetTopologyRequest{Timestamp: util.Now()})
	if err != nil {
		return fmt.Errorf("Couldn't retrieve topology from DB: %v\n", err)
	}
	sourceTopo := topoResponse.GetToplogy()
	targetTopo := rtdt_topology.NewTopology()
	for _, link := range sourceTopo.Links {
		var n0 rtdt_topology.Node
		var n1 rtdt_topology.Node
		if targetTopo.GetNodeByUUID(link.SourceNode.Id) == nil {
			snode := link.SourceNode
			n1 = rtdt_topology.Node{
				ID:       snode.Id,
				Name:     snode.Name,
				Kind:     "todo",
				Image:    "todo", // How to do this?
				MgmtIpv4: "todo",
			}
			v.topology.Nodes = append(v.topology.Nodes, &n1)
		}
		if targetTopo.GetNodeByUUID(link.TargetNode.Id) == nil {
			tnode := link.TargetNode
			n1 = rtdt_topology.Node{
				ID:       tnode.Id,
				Name:     tnode.Name,
				Kind:     "todo",
				Image:    "todo", // How to do this?
				MgmtIpv4: "todo",
			}
			v.topology.Nodes = append(v.topology.Nodes, &n1)
		}
		if targetTopo.GetLinkByUUID(link.Id) == nil {

			var newLink = rtdt_topology.Link{
				ID:         link.Id,
				Name:       link.Name,
				SourceNode: &n0,
				TargetNode: &n1,
			}
            v.topology.Links = append(v.topology.Links, &newLink)
		}
	}

	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
}