From 0b023dc882c8a562798b95d291bdecfefa8bbcda Mon Sep 17 00:00:00 2001 From: Markus Vahlenkamp Date: Mon, 14 Aug 2023 16:04:20 +0200 Subject: [PATCH] New link structs from raw (#1475) * fix reconfigure on exited nodes * update * update * update * update * update * update * fix * update * update * update * update * CheckInterfaceName to use NWEndpoints * update * update * update * update * MacvlanMtuOnSRL * update * resolve link-mgmt-net-raw to linkveth * refactor veth bridge attachment processing * link validation * cumulate validation errors * fix * fix mgmt bridge * fix test * fix * on destroy also resolve links * fix iptablesrules destroy cleanup * removed yaml type error during unmarshalling since the error was not present * rename pointer receiver * LinkConfig -> LinkBrief and moved it to its own file from the topology.go * added make target to create a bin suitable for debug * added Resolve to LinkBrief * added link common param to a test * put mtu on the right level * function rename to avoid meanings clash * move links to their own package * renamed LinkInterf to Link and added comments * rename Endpt -> Endpoint * rename endpoint structs * give LinkNodeResolver name to the interface * more renaming and comments * renamed NWEndpoints to Endpoints * added comments for Link interface methods * renamed linkNodeResolver to Node * delay initilaisation of the mgmt-net bridge name * removed endpoints from endpoin's verify as it is not used * sort unexported fields * renamed endpoint uniqueness check * added same link case * splice endpoints to different files * remove node filtering test cases that operated on links * set iface name for xrd ifEnvVar * added comment for runtime.Node interface * renamed SetupNetworking to DeployLinks * changed error msg * do not use endpoints to deploy links * add links to only "real" nodes * added comment * move net creation after redeploy * set link deployment state * added node state * remove commented methods and fields * add linkcommonparams to initial LinkDefinition struct * set deployment state upon node deployment finish * remove unused link deployment state * switch string based statuses to enum * catch unmarshall type error extend tests to extended link definition format * choose different interface for tools command test * added links to genericlinknode * strict unmarshal error bypass * add unittest * add tests, fix state race * fix * move GenericLinkNode to own file, resolve raw host link to veth link * remove commented out code * implement vethCleanup * fix podman * deepsource fix * fix unittest * Revert "deepsource fix" This reverts commit 4ed320bfbaf724105a67f6834ee31c153b645794. * deepsource skip line * fix json export * remove mocked runtime * fix graph * make generate use brief link format * reintroduce log message for link creation * update * remove mutex from veth link * run e2e tests with race * added cgo for debug bin * bump scrapligo * Update links/link_veth.go Co-authored-by: Roman Dodin * Update links/link_macvlan.go Co-authored-by: Roman Dodin * DefaultNode Nodestate mutex to RWMutex * logrus -> log * added endpoint mutex * Revert "added endpoint mutex" This reverts commit 935b5f5db82134c8786a13fd1e66ac5ba50011d3. * Introduce NewGenericEndpoint() to generate randifnames on construction * please deepsource * adjust link creation log message * explicit string method is not required * implicit break implied * added comments for veth endpoint deployment routine * move host and mgmt-net nodes to appropriate files * reintroduce state link mutex * renamed AddLinkToContainer * exclude fake name from host and mgmt-net link nodes * regen mocks * removed unused custom marshaller for vethraw * renamed ToLinkBriefRaw to match the return type * renamed linkVEthRawFromLinkBriefRaw * added some bad test for veth resolve * fix import * removed unused test topology * verifyRootNetNSLinks * removed unused receivers and args --------- Co-authored-by: Roman Dodin --- .github/workflows/cicd.yml | 2 +- Makefile | 15 +- border0_api/border0_test.go | 12 +- clab/clab.go | 167 ++++---- clab/clab_test.go | 211 +--------- clab/config.go | 199 ++-------- clab/config/utils.go | 26 +- clab/config_test.go | 73 +++- clab/graph.go | 13 +- clab/test_data/topo6.yml | 1 + clab/test_data/topo7-dup-rootnetns.yml | 8 +- cmd/deploy.go | 28 +- cmd/destroy.go | 20 +- cmd/exec.go | 6 + cmd/generate.go | 27 +- cmd/graph.go | 16 +- cmd/save.go | 6 + cmd/tools_veth.go | 17 +- go.mod | 4 +- go.sum | 4 +- links/endpoint.go | 142 +++++++ links/endpoint_bridge.go | 53 +++ links/endpoint_host.go | 23 ++ links/endpoint_macvlan.go | 10 + links/endpoint_raw.go | 79 ++++ links/endpoint_veth.go | 10 + links/generic_link_node.go | 62 +++ links/link.go | 361 ++++++++++++++++++ links/link_brief.go | 50 +++ links/link_host.go | 107 ++++++ links/link_macvlan.go | 195 ++++++++++ links/link_mgmt-net.go | 123 ++++++ {types => links}/link_test.go | 138 +++++-- links/link_veth.go | 180 +++++++++ links/link_veth_test.go | 240 ++++++++++++ mocks/{ => mocknodes}/default_node.go | 4 +- mocks/{ => mocknodes}/node.go | 130 ++++++- mocks/{ => mockruntime}/runtime.go | 58 ++- nodes/bridge/bridge.go | 46 ++- nodes/c8000/c8000.go | 7 +- nodes/ceos/ceos.go | 13 +- .../checkpoint_cloudguard.go | 2 +- nodes/cvx/cvx.go | 12 +- nodes/default_node.go | 107 +++++- nodes/ext_container/ext_container.go | 3 + nodes/host/host.go | 15 +- nodes/ipinfusion_ocnos/ipinfusion_ocnos.go | 2 +- nodes/linux/linux.go | 9 +- nodes/node.go | 23 +- nodes/ovs/ovs.go | 27 +- nodes/srl/srl.go | 97 +++-- nodes/state/state.go | 9 + nodes/vr_aoscx/vr-aoscx.go | 2 +- nodes/vr_csr/vr-csr.go | 2 +- nodes/vr_ftosv/vr-ftosv.go | 2 +- nodes/vr_n9kv/vr-n9kv.go | 2 +- nodes/vr_nxos/vr-nxos.go | 2 +- nodes/vr_pan/vr-pan.go | 2 +- nodes/vr_ros/vr-ros.go | 2 +- nodes/vr_sros/vr-sros.go | 6 +- nodes/vr_veos/vr-veos.go | 2 +- nodes/vr_vmx/vr-vmx.go | 2 +- nodes/vr_vqfx/vr-vqfx.go | 2 +- nodes/vr_vsrx/vr-vsrx.go | 2 +- nodes/vr_xrv/vr-xrv.go | 2 +- nodes/vr_xrv9k/vr-xrv9k.go | 2 +- nodes/xrd/xrd.go | 12 +- runtime/containerd/containerd.go | 78 ++-- runtime/docker/docker.go | 13 +- runtime/ignite/ignite.go | 38 +- runtime/podman/podman.go | 16 +- runtime/podman/util.go | 4 - runtime/runtime.go | 11 +- templates/export/auto.tmpl | 15 +- templates/export/full.tmpl | 15 +- tests/01-smoke/01-basic-flow.robot | 38 +- tests/01-smoke/01-linux-nodes.clab.yml | 24 ++ tests/01-smoke/08-tools-cmds.robot | 8 +- types/endpoint.go | 7 - types/link.go | 152 -------- types/link_host.go | 25 -- types/link_macvlan.go | 25 -- types/link_mgmt-net.go | 23 -- types/link_veth.go | 34 -- types/topology.go | 12 +- types/types.go | 2 - utils/netlink.go | 7 +- 87 files changed, 2811 insertions(+), 972 deletions(-) create mode 100644 links/endpoint.go create mode 100644 links/endpoint_bridge.go create mode 100644 links/endpoint_host.go create mode 100644 links/endpoint_macvlan.go create mode 100644 links/endpoint_raw.go create mode 100644 links/endpoint_veth.go create mode 100644 links/generic_link_node.go create mode 100644 links/link.go create mode 100644 links/link_brief.go create mode 100644 links/link_host.go create mode 100644 links/link_macvlan.go create mode 100644 links/link_mgmt-net.go rename {types => links}/link_test.go (53%) create mode 100644 links/link_veth.go create mode 100644 links/link_veth_test.go rename mocks/{ => mocknodes}/default_node.go (98%) rename mocks/{ => mocknodes}/node.go (70%) rename mocks/{ => mockruntime}/runtime.go (88%) create mode 100644 nodes/state/state.go delete mode 100644 types/endpoint.go delete mode 100644 types/link.go delete mode 100644 types/link_host.go delete mode 100644 types/link_macvlan.go delete mode 100644 types/link_mgmt-net.go delete mode 100644 types/link_veth.go diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 39a3397ad..09c5996ce 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -77,7 +77,7 @@ jobs: ${{ runner.os }}-go- - name: Build containerlab - run: make build-with-podman BINARY=containerlab + run: make build-with-podman-debug BINARY=containerlab # store clab binary as artifact - uses: actions/upload-artifact@v3 with: diff --git a/Makefile b/Makefile index 70ff33f95..747452339 100644 --- a/Makefile +++ b/Makefile @@ -22,10 +22,19 @@ build-with-cover: mkdir -p $(BIN_DIR) go build -cover -o $(BINARY) -ldflags="$(LDFLAGS)" main.go +build-debug: + mkdir -p $(BIN_DIR) + go build -o $(BINARY) -gcflags=all="-N -l" -race main.go + + build-with-podman: mkdir -p $(BIN_DIR) CGO_ENABLED=0 go build -o $(BINARY) -ldflags="$(LDFLAGS)" -trimpath -tags "podman exclude_graphdriver_btrfs btrfs_noversion exclude_graphdriver_devicemapper exclude_graphdriver_overlay containers_image_openpgp" main.go +build-with-podman-debug: + mkdir -p $(BIN_DIR) + CGO_ENABLED=1 go build -o $(BINARY) -gcflags=all="-N -l" -race -trimpath -tags "podman exclude_graphdriver_btrfs btrfs_noversion exclude_graphdriver_devicemapper exclude_graphdriver_overlay containers_image_openpgp" main.go + test: go test -race ./... -v @@ -33,10 +42,10 @@ MOCKDIR = ./mocks .PHONY: mocks-gen mocks-gen: mocks-rm ## Generate mocks for all the defined interfaces. go install github.com/golang/mock/mockgen@v1.6.0 - mockgen -package=mocks -source=nodes/node.go -destination=$(MOCKDIR)/node.go + mockgen -package=mocknodes -source=nodes/node.go -destination=$(MOCKDIR)/mocknodes/node.go mockgen -package=mocks -source=clab/dependency_manager/dependency_manager.go -destination=$(MOCKDIR)/dependency_manager.go - mockgen -package=mocks -source=runtime/runtime.go -destination=$(MOCKDIR)/runtime.go - mockgen -package=mocks -source=nodes/default_node.go -destination=$(MOCKDIR)/default_node.go + mockgen -package=mockruntime -source=runtime/runtime.go -destination=$(MOCKDIR)/mockruntime/runtime.go + mockgen -package=mocknodes -source=nodes/default_node.go -destination=$(MOCKDIR)/mocknodes/default_node.go mockgen -package=mocks -source=clab/exec/exec.go -destination=$(MOCKDIR)/exec.go .PHONY: mocks-rm diff --git a/border0_api/border0_test.go b/border0_api/border0_test.go index cc2e340bb..e55a4ae4f 100644 --- a/border0_api/border0_test.go +++ b/border0_api/border0_test.go @@ -12,7 +12,7 @@ import ( "github.com/golang/mock/gomock" "github.com/h2non/gock" - "github.com/srl-labs/containerlab/mocks" + "github.com/srl-labs/containerlab/mocks/mocknodes" "github.com/srl-labs/containerlab/nodes" "github.com/srl-labs/containerlab/types" ) @@ -211,7 +211,7 @@ func Test_createBorder0Config(t *testing.T) { // getNodeMap return a map of nodes for testing purpose. func getNodeMap(mockCtrl *gomock.Controller) map[string]nodes.Node { // instantiate Mock Node 1 - mockNode1 := mocks.NewMockNode(mockCtrl) + mockNode1 := mocknodes.NewMockNode(mockCtrl) mockNode1.EXPECT().Config().Return( &types.NodeConfig{ Image: "alpine:3", @@ -221,7 +221,7 @@ func getNodeMap(mockCtrl *gomock.Controller) map[string]nodes.Node { ).AnyTimes() // instantiate Mock Node 2 - mockNode2 := mocks.NewMockNode(mockCtrl) + mockNode2 := mocknodes.NewMockNode(mockCtrl) mockNode2.EXPECT().Config().Return( &types.NodeConfig{ Image: "alpine:3", @@ -236,7 +236,7 @@ func getNodeMap(mockCtrl *gomock.Controller) map[string]nodes.Node { ).AnyTimes() // instantiate Mock Node 3 - mockNode3 := mocks.NewMockNode(mockCtrl) + mockNode3 := mocknodes.NewMockNode(mockCtrl) mockNode3.EXPECT().Config().Return( &types.NodeConfig{ Image: "alpine:3", @@ -248,7 +248,7 @@ func getNodeMap(mockCtrl *gomock.Controller) map[string]nodes.Node { ).AnyTimes() // instantiate Mock Node 4 - mockNode4 := mocks.NewMockNode(mockCtrl) + mockNode4 := mocknodes.NewMockNode(mockCtrl) mockNode4.EXPECT().Config().Return( &types.NodeConfig{ Image: "alpine:3", @@ -259,7 +259,7 @@ func getNodeMap(mockCtrl *gomock.Controller) map[string]nodes.Node { ).AnyTimes() // instantiate Mock Node 5 - mockNode5 := mocks.NewMockNode(mockCtrl) + mockNode5 := mocknodes.NewMockNode(mockCtrl) mockNode5.EXPECT().Config().Return( &types.NodeConfig{ Image: "alpine:3", diff --git a/clab/clab.go b/clab/clab.go index 434e665ce..9723ab93f 100644 --- a/clab/clab.go +++ b/clab/clab.go @@ -17,6 +17,7 @@ import ( "github.com/srl-labs/containerlab/cert" "github.com/srl-labs/containerlab/clab/dependency_manager" errs "github.com/srl-labs/containerlab/errors" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/nodes" "github.com/srl-labs/containerlab/runtime" _ "github.com/srl-labs/containerlab/runtime/all" @@ -25,17 +26,15 @@ import ( "github.com/srl-labs/containerlab/types" "golang.org/x/crypto/ssh" "golang.org/x/exp/slices" - "golang.org/x/sync/semaphore" ) type CLab struct { - Config *Config `json:"config,omitempty"` - TopoPaths *types.TopoPaths - m *sync.RWMutex - Nodes map[string]nodes.Node `json:"nodes,omitempty"` - Links map[int]*types.Link `json:"links,omitempty"` - Runtimes map[string]runtime.ContainerRuntime `json:"runtimes,omitempty"` - globalRuntime string + Config *Config `json:"config,omitempty"` + TopoPaths *types.TopoPaths + Nodes map[string]nodes.Node `json:"nodes,omitempty"` + Links map[int]links.Link `json:"links,omitempty"` + Endpoints []links.Endpoint + Runtimes map[string]runtime.ContainerRuntime `json:"runtimes,omitempty"` // reg is a registry of node kinds Reg *nodes.NodeRegistry Cert *cert.Cert @@ -44,7 +43,9 @@ type CLab struct { // The keys are used to enable key-based SSH access for the nodes. SSHPubKeys []ssh.PublicKey - timeout time.Duration + m *sync.RWMutex + timeout time.Duration + globalRuntime string } type ClabOption func(c *CLab) error @@ -165,16 +166,6 @@ func filterClabNodes(c *CLab, nodeFilter []string) error { } } - // filter links - for id, l := range c.Links { - for _, nodeName := range []string{l.A.Node.ShortName, l.B.Node.ShortName} { - // if both endpoints of a link belong to the node filter, keep the link - if !slices.Contains(nodeFilter, nodeName) { - delete(c.Links, id) - break - } - } - } return nil } @@ -187,7 +178,7 @@ func NewContainerLab(opts ...ClabOption) (*CLab, error) { }, m: new(sync.RWMutex), Nodes: make(map[string]nodes.Node), - Links: make(map[int]*types.Link), + Links: make(map[int]links.Link), Runtimes: make(map[string]runtime.ContainerRuntime), Cert: &cert.Cert{}, } @@ -423,6 +414,12 @@ func (c *CLab) scheduleNodes(ctx context.Context, maxWorkers int, continue } + err = node.DeployLinks(ctx) + if err != nil { + log.Errorf("failed deploy links for node %q: %v", node.Config().ShortName, err) + continue + } + // signal to dependency manager that this node is done with creation dm.SignalDone(node.Config().ShortName, dependency_manager.NodeStateCreated) @@ -506,49 +503,6 @@ func (c *CLab) WaitForExternalNodeDependencies(ctx context.Context, nodeName str runtime.WaitForContainerRunning(ctx, c.Runtimes[c.globalRuntime], contName, nodeName) } -// CreateLinks creates links using the specified number of workers. -func (c *CLab) CreateLinks(ctx context.Context, workers uint, dm dependency_manager.DependencyManager) { - wg := new(sync.WaitGroup) - sem := semaphore.NewWeighted(int64(workers)) - - for _, link := range c.Links { - wg.Add(1) - go func(li *types.Link) { - defer wg.Done() - - var waitNodes []string - for _, n := range []*types.NodeConfig{li.A.Node, li.B.Node} { - // we should not wait for "host", "mgmt-net" and "macvlan" fake nodes - // as they are never managed by dependency manager (never really get created) - if n.Kind == "host" || n.ShortName == "mgmt-net" || n.Kind == "macvlan" { - continue - } - waitNodes = append(waitNodes, n.ShortName) - } - - err := dm.WaitForNodes(waitNodes, dependency_manager.NodeStateCreated) - if err != nil { - log.Error(err) - } - - // acquire Sem - err = sem.Acquire(ctx, 1) - if err != nil { - log.Error(err) - } - defer sem.Release(1) - // create the wiring - err = c.CreateVirtualWiring(li) - if err != nil { - log.Error(err) - } - }(link) - } - - // wait for all workers to finish - wg.Wait() -} - func (c *CLab) DeleteNodes(ctx context.Context, workers uint, serialNodes map[string]struct{}) { wg := new(sync.WaitGroup) @@ -632,6 +586,21 @@ func (c *CLab) ListNodesContainers(ctx context.Context) ([]runtime.GenericContai return containers, nil } +// ListNodesContainersIgnoreNotFound lists all containers based on the nodes stored in clab instance, ignoring errors for non found containers +func (c *CLab) ListNodesContainersIgnoreNotFound(ctx context.Context) ([]runtime.GenericContainer, error) { + var containers []runtime.GenericContainer + + for _, n := range c.Nodes { + cts, err := n.GetContainers(ctx) + if err != nil { + continue + } + containers = append(containers, cts...) + } + + return containers, nil +} + func (c *CLab) GetNodeRuntime(contName string) (runtime.ContainerRuntime, error) { shortName, err := getShortName(c.Config.Name, c.Config.Prefix, contName) if err != nil { @@ -648,12 +617,76 @@ func (c *CLab) GetNodeRuntime(contName string) (runtime.ContainerRuntime, error) // VethCleanup iterates over links found in clab topology to initiate removal of dangling veths // in host networking namespace or attached to linux bridge. // See https://github.com/srl-labs/containerlab/issues/842 for the reference. -func (c *CLab) VethCleanup(_ context.Context) error { - for _, link := range c.Links { - err := c.RemoveHostOrBridgeVeth(link) +func (c *CLab) VethCleanup(ctx context.Context) error { + hostBasedEndpoints := []links.Endpoint{} + + // collect the endpoints of regular nodes + for _, n := range c.Nodes { + if n.Config().IsRootNamespaceBased || n.Config().NetworkMode == "host" { + hostBasedEndpoints = append(hostBasedEndpoints, n.GetEndpoints()...) + } + } + + // collect the endpoints of the fake nodes + hostBasedEndpoints = append(hostBasedEndpoints, links.GetHostLinkNode().GetEndpoints()...) + hostBasedEndpoints = append(hostBasedEndpoints, links.GetMgmtBrLinkNode().GetEndpoints()...) + + var joinedErr error + for _, ep := range hostBasedEndpoints { + // finally remove all the collected endpoints + log.Debugf("removing endpoint %s", ep.String()) + err := ep.Remove() + if err != nil { + joinedErr = errors.Join(joinedErr, err) + } + } + + return joinedErr +} + +// ResolveLinks resolves raw links to the actual link types and stores them in the CLab.Links map. +func (c *CLab) ResolveLinks() error { + // resolveNodes is a map of all nodes in the topology + // that is artificially created to combat circular dependencies. + // If no circ deps were in place we could've used c.Nodes map instead. + // The map is used to resolve links between the nodes by passing it in the ResolveParams struct. + resolveNodes := make(map[string]links.Node, len(c.Nodes)) + for k, v := range c.Nodes { + resolveNodes[k] = v + } + + // add the virtual host and mgmt-bridge nodes to the resolve nodes + specialNodes := map[string]links.Node{ + "host": links.GetHostLinkNode(), + "mgmt-net": links.GetMgmtBrLinkNode(), + } + for _, n := range specialNodes { + resolveNodes[n.GetShortName()] = n + } + + resolveParams := &links.ResolveParams{ + Nodes: resolveNodes, + MgmtBridgeName: c.Config.Mgmt.Bridge, + } + + for i, l := range c.Config.Topology.Links { + l, err := l.Link.Resolve(resolveParams) if err != nil { - log.Infof("Error during veth cleanup: %v", err) + return err + } + + c.Endpoints = append(c.Endpoints, l.GetEndpoints()...) + c.Links[i] = l + + // add the link to the nodes connected with it + for _, ep := range l.GetEndpoints() { + // check if node is in the list of c.Nodes + // this will skip fake endpoints like host and mgmt-net + if n, ok := c.Nodes[ep.GetNode().GetShortName()]; ok { + n.AddLink(l) + } } } + return nil } diff --git a/clab/clab_test.go b/clab/clab_test.go index eb3c4ea70..dff9d7c38 100644 --- a/clab/clab_test.go +++ b/clab/clab_test.go @@ -13,6 +13,8 @@ import ( "github.com/google/go-cmp/cmp" errs "github.com/srl-labs/containerlab/errors" "github.com/srl-labs/containerlab/mocks" + "github.com/srl-labs/containerlab/mocks/mocknodes" + "github.com/srl-labs/containerlab/mocks/mockruntime" "github.com/srl-labs/containerlab/nodes" "github.com/srl-labs/containerlab/runtime" _ "github.com/srl-labs/containerlab/runtime/all" @@ -57,7 +59,7 @@ func Test_createStaticDynamicDependency(t *testing.T) { // getNodeMap return a map of nodes for testing purpose. func getNodeMap(mockCtrl *gomock.Controller) map[string]nodes.Node { // instantiate Mock Node 1 - mockNode1 := mocks.NewMockNode(mockCtrl) + mockNode1 := mocknodes.NewMockNode(mockCtrl) mockNode1.EXPECT().Config().Return( &types.NodeConfig{ Image: "alpine:3", @@ -66,7 +68,7 @@ func getNodeMap(mockCtrl *gomock.Controller) map[string]nodes.Node { ).AnyTimes() // instantiate Mock Node 2 - mockNode2 := mocks.NewMockNode(mockCtrl) + mockNode2 := mocknodes.NewMockNode(mockCtrl) mockNode2.EXPECT().Config().Return( &types.NodeConfig{ Image: "alpine:3", @@ -76,7 +78,7 @@ func getNodeMap(mockCtrl *gomock.Controller) map[string]nodes.Node { ).AnyTimes() // instantiate Mock Node 3 - mockNode3 := mocks.NewMockNode(mockCtrl) + mockNode3 := mocknodes.NewMockNode(mockCtrl) mockNode3.EXPECT().Config().Return( &types.NodeConfig{ Image: "alpine:3", @@ -87,7 +89,7 @@ func getNodeMap(mockCtrl *gomock.Controller) map[string]nodes.Node { ).AnyTimes() // instantiate Mock Node 4 - mockNode4 := mocks.NewMockNode(mockCtrl) + mockNode4 := mocknodes.NewMockNode(mockCtrl) mockNode4.EXPECT().Config().Return( &types.NodeConfig{ Image: "alpine:3", @@ -98,7 +100,7 @@ func getNodeMap(mockCtrl *gomock.Controller) map[string]nodes.Node { ).AnyTimes() // instantiate Mock Node 5 - mockNode5 := mocks.NewMockNode(mockCtrl) + mockNode5 := mocknodes.NewMockNode(mockCtrl) mockNode5.EXPECT().Config().Return( &types.NodeConfig{ Image: "alpine:3", @@ -148,7 +150,7 @@ func Test_WaitForExternalNodeDependencies_OK(t *testing.T) { defer mockCtrl.Finish() // init a ContainerRuntime mock - crMock := mocks.NewMockContainerRuntime(mockCtrl) + crMock := mockruntime.NewMockContainerRuntime(mockCtrl) // context parameter ctx := context.TODO() @@ -217,11 +219,10 @@ func Test_filterClabNodes(t *testing.T) { c *CLab nodesFilter []string wantNodes []string - wantLinks [][]string wantErr bool err error }{ - "two nodes, no links, one filter node": { + "two nodes, one filter node": { c: &CLab{ Config: &Config{ Topology: &types.Topology{ @@ -238,10 +239,9 @@ func Test_filterClabNodes(t *testing.T) { }, nodesFilter: []string{"node1"}, wantNodes: []string{"node1"}, - wantLinks: [][]string{}, wantErr: false, }, - "one node, no links, empty node filter": { + "one node, empty node filter": { c: &CLab{ Config: &Config{ Topology: &types.Topology{ @@ -255,188 +255,9 @@ func Test_filterClabNodes(t *testing.T) { }, nodesFilter: []string{}, wantNodes: []string{"node1"}, - wantLinks: [][]string{}, wantErr: false, }, - "two nodes, one link between them, one filter node": { - c: &CLab{ - Links: map[int]*types.Link{ - 0: { - A: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node1", - }, - EndpointName: "eth1", - }, - B: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node2", - }, - EndpointName: "eth2", - }, - }, - }, - Config: &Config{ - Topology: &types.Topology{ - Nodes: map[string]*types.NodeDefinition{ - "node1": { - Kind: "linux", - }, - "node2": { - Kind: "linux", - }, - }, - }, - }, - }, - nodesFilter: []string{"node1"}, - wantNodes: []string{"node1"}, - wantLinks: [][]string{}, - wantErr: false, - }, - "two nodes, one link between them, no filter": { - c: &CLab{ - Links: map[int]*types.Link{ - 0: { - A: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node1", - }, - EndpointName: "eth1", - }, - B: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node2", - }, - EndpointName: "eth1", - }, - }, - }, - Config: &Config{ - Topology: &types.Topology{ - Nodes: map[string]*types.NodeDefinition{ - "node1": { - Kind: "linux", - }, - "node2": { - Kind: "linux", - }, - }, - }, - }, - }, - nodesFilter: []string{}, - wantNodes: []string{"node1", "node2"}, - wantLinks: [][]string{{"node1:eth1", "node2:eth1"}}, - wantErr: false, - }, - "three nodes, two links, two nodes in the filter": { - c: &CLab{ - Links: map[int]*types.Link{ - 0: { - A: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node1", - }, - EndpointName: "eth1", - }, - B: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node2", - }, - EndpointName: "eth1", - }, - }, - 1: { - A: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node2", - }, - EndpointName: "eth2", - }, - B: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node3", - }, - EndpointName: "eth2", - }, - }, - }, - Config: &Config{ - Topology: &types.Topology{ - Nodes: map[string]*types.NodeDefinition{ - "node1": { - Kind: "linux", - }, - "node2": { - Kind: "linux", - }, - "node3": { - Kind: "linux", - }, - }, - }, - }, - }, - nodesFilter: []string{"node1", "node2"}, - wantNodes: []string{"node1", "node2"}, - wantLinks: [][]string{{"node1:eth1", "node2:eth1"}}, - wantErr: false, - }, - "three nodes, two links, one nodes in the filter": { - c: &CLab{ - Links: map[int]*types.Link{ - 0: { - A: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node1", - }, - EndpointName: "eth1", - }, - B: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node2", - }, - EndpointName: "eth1", - }, - }, - 1: { - A: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node2", - }, - EndpointName: "eth2", - }, - B: &types.Endpoint{ - Node: &types.NodeConfig{ - ShortName: "node3", - }, - EndpointName: "eth2", - }, - }, - }, - Config: &Config{ - Topology: &types.Topology{ - Nodes: map[string]*types.NodeDefinition{ - "node1": { - Kind: "linux", - }, - "node2": { - Kind: "linux", - }, - "node3": { - Kind: "linux", - }, - }, - }, - }, - }, - nodesFilter: []string{"node1"}, - wantNodes: []string{"node1"}, - wantLinks: [][]string{}, - wantErr: false, - }, - "two nodes, no links, one filter node with a wrong name": { + "two nodes, one filter node with a wrong name": { c: &CLab{ Config: &Config{ Topology: &types.Topology{ @@ -453,7 +274,6 @@ func Test_filterClabNodes(t *testing.T) { }, nodesFilter: []string{"wrongName"}, wantNodes: []string{"node1", "node2"}, - wantLinks: [][]string{}, wantErr: true, err: errs.ErrIncorrectInput, }, @@ -479,18 +299,9 @@ func Test_filterClabNodes(t *testing.T) { // sort the nodes to make the test deterministic slices.Sort(filteredNodes) - filteredLinks := make([][]string, 0, len(tt.c.Links)) - for _, l := range tt.c.Links { - filteredLinks = append(filteredLinks, []string{l.A.String(), l.B.String()}) - } - if cmp.Diff(filteredNodes, tt.wantNodes) != "" { t.Errorf("filterClabNodes() got = %v, want %v", filteredNodes, tt.wantNodes) } - - if cmp.Diff(filteredLinks, tt.wantLinks) != "" { - t.Errorf("filterClabNodes() got = %v, want %v", filteredLinks, tt.wantLinks) - } }) } } diff --git a/clab/config.go b/clab/config.go index 3a545b88a..4bbe40746 100644 --- a/clab/config.go +++ b/clab/config.go @@ -6,6 +6,7 @@ package clab import ( "context" + "errors" "fmt" "os" "sort" @@ -14,11 +15,11 @@ import ( "github.com/pmorjan/kmod" log "github.com/sirupsen/logrus" "github.com/srl-labs/containerlab/labels" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/nodes" clabRuntimes "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" "github.com/srl-labs/containerlab/utils" - "github.com/vishvananda/netlink" ) const ( @@ -32,8 +33,6 @@ const ( hostNSPath = "__host" // veth link mtu. DefaultVethLinkMTU = 9500 - // containerlab's reserved OUI. - ClabOUI = "aa:c1:ab" // clab specific topology variables. clabDirVar = "__clabDir__" @@ -67,7 +66,7 @@ func (c *CLab) parseTopology() error { // initialize Nodes and Links variable c.Nodes = make(map[string]nodes.Node) - c.Links = make(map[int]*types.Link) + c.Links = make(map[int]links.Link) // initialize the Node information from the topology map nodeNames := make([]string, 0, len(c.Config.Topology.Nodes)) @@ -124,10 +123,6 @@ func (c *CLab) parseTopology() error { return err } } - for i, l := range c.Config.Topology.Links { - // i represents the endpoint integer and l provide the link struct - c.Links[i] = c.NewLink(l) - } // set any containerlab defaults after we've parsed the input c.setDefaults() @@ -198,7 +193,6 @@ func (c *CLab) createNodeCfg(nodeName string, nodeDef *types.NodeDefinition, idx MgmtIPv6Address: nodeDef.GetMgmtIPv6(), Publish: c.Config.Topology.GetNodePublish(nodeName), Sysctls: c.Config.Topology.GetSysCtl(nodeName), - Endpoints: make([]types.Endpoint, 0), Sandbox: c.Config.Topology.GetNodeSandbox(nodeName), Kernel: c.Config.Topology.GetNodeKernel(nodeName), Runtime: c.Config.Topology.GetNodeRuntime(nodeName), @@ -326,141 +320,66 @@ func (c *CLab) processStartupConfig(nodeCfg *types.NodeConfig) error { return nil } -// NewLink initializes a new link object from the link definition provided via topology file. -func (c *CLab) NewLink(l *types.LinkDefinition) *types.Link { - if len(l.Endpoints) != 2 { - log.Fatalf("endpoint %q has wrong syntax, unexpected number of items", l.Endpoints) // skipcq: RVV-A0003 - } - - mtu := l.MTU - - if mtu == 0 { - mtu = DefaultVethLinkMTU - } - - return &types.Link{ - A: c.NewEndpoint(l.Endpoints[0]), - B: c.NewEndpoint(l.Endpoints[1]), - MTU: mtu, - Labels: l.Labels, - Vars: l.Vars, - } -} - -// NewEndpoint initializes a new endpoint object. -func (c *CLab) NewEndpoint(e string) *types.Endpoint { - // initialize a new endpoint - endpoint := new(types.Endpoint) - - // split the string to get node name and endpoint name - split := strings.Split(e, ":") - if len(split) != 2 { - log.Fatalf("endpoint %s has wrong syntax", e) // skipcq: GO-S0904, RVV-A0003 - } - nName := split[0] // node name - - // initialize the endpoint name based on the split function - endpoint.EndpointName = split[1] // endpoint name - if len(endpoint.EndpointName) > 15 { - log.Fatalf("interface '%s' name exceeds maximum length of 15 characters", - endpoint.EndpointName) // skipcq: RVV-A0003 - } - // generate unique MAC - endpoint.MAC = utils.GenMac(ClabOUI) - - // search the node pointer for a node name referenced in endpoint section - switch nName { - // "host" is a special reference to host namespace - // for which we create an special Node with kind "host" - case "host": - endpoint.Node = &types.NodeConfig{ - Kind: "host", - ShortName: "host", - NSPath: hostNSPath, - } - case "macvlan": - endpoint.Node = &types.NodeConfig{ - Kind: "macvlan", - ShortName: "macvlan", - NSPath: hostNSPath, - } - // mgmt-net is a special reference to a bridge of the docker network - // that is used as the management network - case "mgmt-net": - endpoint.Node = &types.NodeConfig{ - Kind: "bridge", - ShortName: "mgmt-net", - } - default: - c.m.Lock() - if n, ok := c.Nodes[nName]; ok { - endpoint.Node = n.Config() - n.Config().Endpoints = append(n.Config().Endpoints, *endpoint) - } - c.m.Unlock() - } - - // stop the deployment if the matching node element was not found - // "host" node name is an exception, it may exist without a matching node - if endpoint.Node == nil { - log.Fatalf("not all nodes are specified in the 'topology.nodes' section or the names don't match in the 'links.endpoints' section: %s", nName) // skipcq: GO-S0904, RVV-A0003 - } - - return endpoint -} - // CheckTopologyDefinition runs topology checks and returns any errors found. // This function runs after topology file is parsed and all nodes/links are initialized. func (c *CLab) CheckTopologyDefinition(ctx context.Context) error { var err error - + if err = c.verifyLinks(); err != nil { + return err + } + if err = c.verifyRootNetNSLinks(); err != nil { + return err + } for _, node := range c.Nodes { err := node.CheckDeploymentConditions(ctx) if err != nil { return err } } - if err = c.verifyLinks(); err != nil { - return err - } if err = c.verifyDuplicateAddresses(); err != nil { return err } - if err = c.verifyRootNetnsInterfaceUniqueness(); err != nil { - return err - } if err = c.VerifyContainersUniqueness(ctx); err != nil { return err } - if err = c.verifyHostIfaces(); err != nil { - return err + return nil +} + +// verifyRootNetNSLinks makes sure, that there will be no overlap in +// interface names for Root Network Namespace bases nodes. +func (c *CLab) verifyRootNetNSLinks() error { + rootEpNames := map[string]string{} + + // iterate through nodes + for _, n := range c.Nodes { + // check if they are RootNamespace based + if n.Config().IsRootNamespaceBased { + // if so, add their ep names to the list of rootEpNames + for _, e := range n.GetEndpoints() { + if val, exists := rootEpNames[e.GetIfaceName()]; exists { + return fmt.Errorf("root network namespace endpoint %q defined by multiple nodes [%s, %s]", e.GetIfaceName(), val, e.GetNode().GetShortName()) + } + rootEpNames[e.GetIfaceName()] = e.GetNode().GetShortName() + } + } } + return nil } // verifyLinks checks if all the endpoints in the links section of the topology file // appear only once. func (c *CLab) verifyLinks() error { - endpoints := map[string]struct{}{} - // dups accumulates duplicate links - dups := []string{} - for _, l := range c.Links { - for _, e := range []*types.Endpoint{l.A, l.B} { - e_string := e.String() - if _, ok := endpoints[e_string]; ok { - // macvlan interface can appear multiple times - if strings.Contains(e_string, "macvlan") { - continue - } - - dups = append(dups, e_string) - } - endpoints[e_string] = struct{}{} + var err error + verificationErrors := []error{} + for _, e := range c.Endpoints { + err = e.Verify(c.GlobalRuntime().Config().VerifyLinkParams) + if err != nil { + verificationErrors = append(verificationErrors, err) } } - if len(dups) != 0 { - sort.Strings(dups) // sort for deterministic error message - return fmt.Errorf("endpoints %q appeared more than once in the links section of the topology file", dups) + if len(verificationErrors) > 0 { + return errors.Join(verificationErrors...) } return nil } @@ -569,45 +488,6 @@ func (c *CLab) VerifyContainersUniqueness(ctx context.Context) error { return nil } -// verifyHostIfaces ensures that host interfaces referenced in the topology -// do not exist already in the root namespace -// and ensure that nodes that are configured with host networking mode do not have any interfaces defined. -func (c *CLab) verifyHostIfaces() error { - for _, l := range c.Links { - for _, ep := range []*types.Endpoint{l.A, l.B} { - if ep.Node.ShortName == "host" { - if nl, _ := netlink.LinkByName(ep.EndpointName); nl != nil { - return fmt.Errorf("host interface %s referenced in topology already exists", ep.EndpointName) - } - } - if ep.Node.NetworkMode == "host" { - return fmt.Errorf("node '%s' is defined with host network mode, it can't have any links. Remove '%s' node links from the topology definition", - ep.Node.ShortName, ep.Node.ShortName) - } - } - } - return nil -} - -// verifyRootNetnsInterfaceUniqueness ensures that interafaces that appear in the root ns (bridge, ovs-bridge and host) -// are uniquely defined in the topology file. -func (c *CLab) verifyRootNetnsInterfaceUniqueness() error { - rootNsIfaces := map[string]struct{}{} - for _, l := range c.Links { - endpoints := [2]*types.Endpoint{l.A, l.B} - for _, e := range endpoints { - if e.Node.IsRootNamespaceBased { - if _, ok := rootNsIfaces[e.EndpointName]; ok { - return fmt.Errorf(`interface %s defined for node %s has already been used in other bridges, ovs-bridges or host interfaces. - Make sure that nodes of these kinds use unique interface names`, e.EndpointName, e.Node.ShortName) - } - rootNsIfaces[e.EndpointName] = struct{}{} - } - } - } - return nil -} - // resolveBindPaths resolves the host paths in a bind string, such as /hostpath:/remotepath(:options) string // it allows host path to have `~` and relative path to an absolute path // the list of binds will be changed in place. @@ -644,10 +524,9 @@ func (c *CLab) setDefaults() { for _, n := range c.Nodes { // Injecting the env var with expected number of links numLinks := map[string]string{ - types.CLAB_ENV_INTFS: fmt.Sprintf("%d", len(n.Config().Endpoints)), + types.CLAB_ENV_INTFS: fmt.Sprintf("%d", len(n.GetEndpoints())), } n.Config().Env = utils.MergeStringMaps(n.Config().Env, numLinks) - } } diff --git a/clab/config/utils.go b/clab/config/utils.go index 5972e2f0f..53092233f 100644 --- a/clab/config/utils.go +++ b/clab/config/utils.go @@ -73,19 +73,19 @@ func PrepareVars(c *clab.CLab) map[string]*NodeConfig { } } - // prepare all links - for lIdx, link := range c.Links { - varsA := make(Dict) - varsB := make(Dict) - err := prepareLinkVars(link, varsA, varsB) - if err != nil { - log.Errorf("cannot prepare link vars for %d. %s: %s", lIdx, link.String(), err) - } - res[link.A.Node.ShortName].Vars[vkLinks] = - append(res[link.A.Node.ShortName].Vars[vkLinks].([]interface{}), varsA) - res[link.B.Node.ShortName].Vars[vkLinks] = - append(res[link.B.Node.ShortName].Vars[vkLinks].([]interface{}), varsB) - } + // // prepare all links + // for lIdx, link := range c.Links { + // varsA := make(Dict) + // varsB := make(Dict) + // err := prepareLinkVars(link, varsA, varsB) + // if err != nil { + // log.Errorf("cannot prepare link vars for %d. %s: %s", lIdx, link.String(), err) + // } + // res[link.A.Node.ShortName].Vars[vkLinks] = + // append(res[link.A.Node.ShortName].Vars[vkLinks].([]interface{}), varsA) + // res[link.B.Node.ShortName].Vars[vkLinks] = + // append(res[link.B.Node.ShortName].Vars[vkLinks].([]interface{}), varsB) + // } // Prepare top-level map of nodes // copy 1-level deep diff --git a/clab/config_test.go b/clab/config_test.go index 866a0f9a9..a43e0b401 100644 --- a/clab/config_test.go +++ b/clab/config_test.go @@ -16,8 +16,10 @@ import ( "github.com/golang/mock/gomock" "github.com/google/go-cmp/cmp" "github.com/srl-labs/containerlab/labels" - "github.com/srl-labs/containerlab/mocks" + "github.com/srl-labs/containerlab/links" + "github.com/srl-labs/containerlab/mocks/mockruntime" "github.com/srl-labs/containerlab/runtime" + "github.com/srl-labs/containerlab/runtime/docker" "github.com/srl-labs/containerlab/utils" "github.com/stretchr/testify/assert" ) @@ -339,7 +341,7 @@ func TestVerifyLinks(t *testing.T) { }{ "two_duplicated_links": { got: "test_data/topo6.yml", - want: "endpoints [\"lin1:eth1\" \"lin2:eth2\"] appeared more than once in the links section of the topology file", + want: "duplicate endpoint lin1:eth1\nduplicate endpoint lin1:eth1\nduplicate endpoint lin2:eth2\nduplicate endpoint lin2:eth2\nduplicate endpoint lin1:eth4\nduplicate endpoint lin1:eth4", }, "no_duplicated_links": { got: "test_data/topo1.yml", @@ -354,12 +356,21 @@ func TestVerifyLinks(t *testing.T) { t.Run(name, func(t *testing.T) { opts := []ClabOption{ WithTopoFile(tc.got, ""), + WithRuntime(docker.RuntimeName, + &runtime.RuntimeConfig{ + VerifyLinkParams: links.NewVerifyLinkParams(), + }, + ), } c, err := NewContainerLab(opts...) if err != nil { t.Fatal(err) } + err = c.ResolveLinks() + if err != nil { + t.Fatal(err) + } err = c.verifyLinks() if err != nil && err.Error() != tc.want { t.Fatalf("wanted %q got %q", tc.want, err.Error()) @@ -471,20 +482,54 @@ func TestLabelsInit(t *testing.T) { } } -func TestVerifyRootNetnsInterfaceUniqueness(t *testing.T) { - opts := []ClabOption{ - WithTopoFile("test_data/topo7-dup-rootnetns.yml", ""), - } - c, err := NewContainerLab(opts...) - if err != nil { - t.Fatal(err) +func TestVerifyRootNetNSLinks(t *testing.T) { + + tests := map[string]struct { + topo string + wantError bool + }{ + "dup rootnetns": { + topo: "test_data/topo7-dup-rootnetns.yml", + wantError: true, + }, + "topo1": { + topo: "test_data/topo1.yml", + wantError: false, + }, + "topo3": { + topo: "test_data/topo3.yml", + wantError: false, + }, + "topo4": { + topo: "test_data/topo4.yml", + wantError: false, + }, } - err = c.verifyRootNetnsInterfaceUniqueness() - if err == nil { - t.Fatalf("expected duplicate rootns links error") + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + opts := []ClabOption{ + WithTopoFile(tc.topo, ""), + } + c, err := NewContainerLab(opts...) + if err != nil { + t.Fatal(err) + } + + err = c.ResolveLinks() + if err != nil { + t.Fatal(err) + } + + err = c.verifyRootNetNSLinks() + if tc.wantError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) } - t.Logf("error: %v", err) + } func TestVerifyContainersUniqueness(t *testing.T) { @@ -573,7 +618,7 @@ func TestVerifyContainersUniqueness(t *testing.T) { } // set mockRuntime parameters - mockRuntime := mocks.NewMockContainerRuntime(ctrl) + mockRuntime := mockruntime.NewMockContainerRuntime(ctrl) c.Runtimes[rtName] = mockRuntime c.globalRuntime = rtName diff --git a/clab/graph.go b/clab/graph.go index b64c920bc..9721900b0 100644 --- a/clab/graph.go +++ b/clab/graph.go @@ -94,11 +94,15 @@ func (c *CLab) GenerateDotGraph() error { attr = make(map[string]string) attr["color"] = "black" - if (strings.Contains(link.A.Node.ShortName, "client")) || - (strings.Contains(link.B.Node.ShortName, "client")) { + eps := link.GetEndpoints() + ANodeName := eps[0].GetNode().GetShortName() + BNodeName := eps[1].GetNode().GetShortName() + + if (strings.Contains(ANodeName, "client")) || + (strings.Contains(BNodeName, "client")) { attr["color"] = "blue" } - if err := g.AddEdge(link.A.Node.ShortName, link.B.Node.ShortName, false, attr); err != nil { + if err := g.AddEdge(ANodeName, BNodeName, false, attr); err != nil { return err } // log.Info(link.A.Node.ShortName, " <-> ", link.B.Node.ShortName) @@ -221,7 +225,8 @@ func (c *CLab) GenerateMermaidGraph(direction string) error { // Process the links between Nodes for _, link := range c.Links { - fc.AddEdge(link.A.Node.ShortName, link.B.Node.ShortName) + eps := link.GetEndpoints() + fc.AddEdge(eps[0].GetNode().GetShortName(), eps[1].GetNode().GetShortName()) } // create graph directory diff --git a/clab/test_data/topo6.yml b/clab/test_data/topo6.yml index a3a08b0f8..669573434 100644 --- a/clab/test_data/topo6.yml +++ b/clab/test_data/topo6.yml @@ -13,3 +13,4 @@ topology: - endpoints: ["lin1:eth1", lin2:eth1] - endpoints: ["lin1:eth1", lin2:eth2] - endpoints: ["lin1:eth3", lin2:eth2] + - endpoints: ["lin1:eth4", lin1:eth4] diff --git a/clab/test_data/topo7-dup-rootnetns.yml b/clab/test_data/topo7-dup-rootnetns.yml index 150bb2b99..d17e56d01 100644 --- a/clab/test_data/topo7-dup-rootnetns.yml +++ b/clab/test_data/topo7-dup-rootnetns.yml @@ -6,6 +6,12 @@ topology: kind: bridge br2: kind: bridge + l1: + kind: linux + image: alpine:latest + cmd: sleep infinity links: - - endpoints: ["br1:eth1", "br2:eth1"] + - endpoints: ["l1:eth1", "br1:eth76"] + - endpoints: ["l1:eth2", "br2:eth76"] + - endpoints: ["br1:eth77", "br2:eth77"] diff --git a/cmd/deploy.go b/cmd/deploy.go index 2aa9872c7..c9dd1a1db 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -19,6 +19,7 @@ import ( "github.com/srl-labs/containerlab/clab" "github.com/srl-labs/containerlab/clab/dependency_manager" "github.com/srl-labs/containerlab/clab/exec" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/nodes" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/utils" @@ -127,6 +128,11 @@ func deployFn(_ *cobra.Command, _ []string) error { return err } + err = c.ResolveLinks() + if err != nil { + return err + } + setFlags(c.Config) log.Debugf("lab Conf: %+v", c.Config) @@ -134,9 +140,6 @@ func deployFn(_ *cobra.Command, _ []string) error { vCh := getLatestClabVersion(ctx) if reconfigure { - if err != nil { - return err - } _ = destroyLab(ctx, c) log.Infof("Removing %s directory...", c.TopoPaths.TopologyLabDir()) if err := os.RemoveAll(c.TopoPaths.TopologyLabDir()); err != nil { @@ -144,6 +147,16 @@ func deployFn(_ *cobra.Command, _ []string) error { } } + // create management network or use existing one + if err = c.CreateNetwork(ctx); err != nil { + return err + } + + err = links.SetMgmtNetUnderlayingBridge(c.Config.Mgmt.Bridge) + if err != nil { + return err + } + if err = c.CheckTopologyDefinition(ctx); err != nil { return err } @@ -190,13 +203,8 @@ func deployFn(_ *cobra.Command, _ []string) error { return err } - // create management network or use existing one - if err = c.CreateNetwork(ctx); err != nil { - return err - } - // determine the number of node and link worker - nodeWorkers, linkWorkers, err := countWorkers(uint(len(c.Nodes)), uint(len(c.Links)), maxWorkers) + nodeWorkers, _, err := countWorkers(uint(len(c.Nodes)), uint(len(c.Links)), maxWorkers) if err != nil { return err } @@ -229,7 +237,7 @@ func deployFn(_ *cobra.Command, _ []string) error { if err != nil { return err } - c.CreateLinks(ctx, linkWorkers, dm) + if nodesWg != nil { nodesWg.Wait() } diff --git a/cmd/destroy.go b/cmd/destroy.go index 4407fba2f..5b9250782 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -13,6 +13,7 @@ import ( "github.com/spf13/cobra" "github.com/srl-labs/containerlab/clab" "github.com/srl-labs/containerlab/labels" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/runtime/ignite" "github.com/srl-labs/containerlab/types" @@ -120,6 +121,23 @@ func destroyFn(_ *cobra.Command, _ []string) error { return err } + err = links.SetMgmtNetUnderlayingBridge(nc.Config.Mgmt.Bridge) + if err != nil { + return err + } + + // create management network or use existing one + // we call this to populate the nc.cfg.mgmt.bridge variable + // which is needed for the removal of the iptables rules + if err = nc.CreateNetwork(ctx); err != nil { + return err + } + + err = nc.ResolveLinks() + if err != nil { + return err + } + labs = append(labs, nc) } @@ -147,7 +165,7 @@ func destroyFn(_ *cobra.Command, _ []string) error { } func destroyLab(ctx context.Context, c *clab.CLab) (err error) { - containers, err := c.ListNodesContainers(ctx) + containers, err := c.ListNodesContainersIgnoreNotFound(ctx) if err != nil { return err } diff --git a/cmd/exec.go b/cmd/exec.go index bc189859a..977229ecb 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -13,6 +13,7 @@ import ( "github.com/srl-labs/containerlab/clab" "github.com/srl-labs/containerlab/clab/exec" "github.com/srl-labs/containerlab/labels" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" ) @@ -59,6 +60,11 @@ func execFn(_ *cobra.Command, _ []string) error { return err } + err = links.SetMgmtNetUnderlayingBridge(c.Config.Mgmt.Bridge) + if err != nil { + return err + } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/cmd/generate.go b/cmd/generate.go index 07fcd5dfe..0a3421ef8 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -15,6 +15,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/srl-labs/containerlab/clab" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/types" "gopkg.in/yaml.v2" ) @@ -214,16 +215,22 @@ func generateTopologyConfig(name, network, ipv4range, ipv6range string, Type: nodes[i+1].typ, } } - config.Topology.Links = append(config.Topology.Links, - &types.LinkDefinition{ - // Type: string(types.LinkTypeBrief), - LinkConfig: types.LinkConfig{ - Endpoints: []string{ - node1 + ":" + fmt.Sprintf(interfaceFormat[nodes[i].kind], k+1+interfaceOffset), - node2 + ":" + fmt.Sprintf(interfaceFormat[nodes[i+1].kind], j+1), - }, - }, - }) + + // create a raw veth link + l := &links.LinkVEthRaw{ + Endpoints: []*links.EndpointRaw{ + links.NewEndpointRaw(node1, fmt.Sprintf(interfaceFormat[nodes[i].kind], k+1+interfaceOffset), ""), + links.NewEndpointRaw(node2, fmt.Sprintf(interfaceFormat[nodes[i+1].kind], j+1), ""), + }, + } + + // encapsulate the brief rawlink in a linkdefinition + ld := &links.LinkDefinition{ + Link: l.ToLinkBriefRaw(), + } + + // add the link to the topology + config.Topology.Links = append(config.Topology.Links, ld) } } } diff --git a/cmd/graph.go b/cmd/graph.go index 65bd2821f..57079f265 100644 --- a/cmd/graph.go +++ b/cmd/graph.go @@ -63,6 +63,11 @@ func graphFn(_ *cobra.Command, _ []string) error { return err } + err = c.ResolveLinks() + if err != nil { + return err + } + if dot { return c.GenerateDotGraph() } @@ -105,11 +110,14 @@ func graphFn(_ *cobra.Command, _ []string) error { return gtopo.Nodes[i].Name < gtopo.Nodes[j].Name }) for _, l := range c.Links { + + eps := l.GetEndpoints() + gtopo.Links = append(gtopo.Links, clab.Link{ - Source: l.A.Node.ShortName, - SourceEndpoint: l.A.EndpointName, - Target: l.B.Node.ShortName, - TargetEndpoint: l.B.EndpointName, + Source: eps[0].GetNode().GetShortName(), + SourceEndpoint: eps[0].GetIfaceName(), + Target: eps[1].GetNode().GetShortName(), + TargetEndpoint: eps[1].GetIfaceName(), }) } diff --git a/cmd/save.go b/cmd/save.go index e8588a32d..327b4d63d 100644 --- a/cmd/save.go +++ b/cmd/save.go @@ -12,6 +12,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/srl-labs/containerlab/clab" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/nodes" "github.com/srl-labs/containerlab/runtime" ) @@ -45,6 +46,11 @@ Refer to the https://containerlab.dev/cmd/save/ documentation to see the exact c return err } + err = links.SetMgmtNetUnderlayingBridge(c.Config.Mgmt.Bridge) + if err != nil { + return err + } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/cmd/tools_veth.go b/cmd/tools_veth.go index 9caa9c2d8..1b478bf0c 100644 --- a/cmd/tools_veth.go +++ b/cmd/tools_veth.go @@ -13,6 +13,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/srl-labs/containerlab/clab" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" "github.com/srl-labs/containerlab/utils" @@ -99,16 +100,26 @@ var vethCreateCmd = &cobra.Command{ return err } } - + // generate mac for endpoint A + aMac, err := utils.GenMac(links.ClabOUI) + if err != nil { + return err + } endpointA := types.Endpoint{ Node: aNode, EndpointName: vethAEndpoint.iface, - MAC: utils.GenMac(clab.ClabOUI), + MAC: aMac.String(), + } + + // generate mac for endpoint B + bMac, err := utils.GenMac(links.ClabOUI) + if err != nil { + return err } endpointB := types.Endpoint{ Node: bNode, EndpointName: vethBEndpoint.iface, - MAC: utils.GenMac(clab.ClabOUI), + MAC: bMac.String(), } link := &types.Link{ diff --git a/go.mod b/go.mod index 0ee479365..8e90f2e7b 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/opencontainers/runtime-spec v1.1.0 github.com/pkg/errors v0.9.1 github.com/pmorjan/kmod v1.1.0 - github.com/scrapli/scrapligo v1.1.10 + github.com/scrapli/scrapligo v1.1.11 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 @@ -269,7 +269,7 @@ require ( golang.org/x/mod v0.12.0 golang.org/x/net v0.14.0 golang.org/x/oauth2 v0.9.0 // indirect - golang.org/x/sync v0.3.0 + golang.org/x/sync v0.3.0 // indirect golang.org/x/text v0.12.0 golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.9.3 // indirect diff --git a/go.sum b/go.sum index d54a8f76f..9e486cec7 100644 --- a/go.sum +++ b/go.sum @@ -2296,8 +2296,8 @@ github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9/go.mod h1:fCa7OJZ/9DRTnOKmxvT6 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.12/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= -github.com/scrapli/scrapligo v1.1.10 h1:DMX6UiCCYN77YsbLaMGauYOm89LldmUiTA8tc0Edw9o= -github.com/scrapli/scrapligo v1.1.10/go.mod h1:XrSom4Gd87B110QkyTaTkuL2EbzEVOlgCJGKIZa6wns= +github.com/scrapli/scrapligo v1.1.11 h1:ATvpF2LDoxnd/HlfSj5A0IiJDro75D6nuCx8m6S44vU= +github.com/scrapli/scrapligo v1.1.11/go.mod h1:XrSom4Gd87B110QkyTaTkuL2EbzEVOlgCJGKIZa6wns= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= diff --git a/links/endpoint.go b/links/endpoint.go new file mode 100644 index 000000000..e18dc45cd --- /dev/null +++ b/links/endpoint.go @@ -0,0 +1,142 @@ +package links + +import ( + "fmt" + "net" + + "github.com/containernetworking/plugins/pkg/ns" + "github.com/vishvananda/netlink" +) + +const ( + // containerlab's reserved OUI. + ClabOUI = "aa:c1:ab" +) + +// Endpoint is the interface that all endpoint types implement. +// Endpoints like bridge, host, veth and macvlan are the types implementing this interface. +type Endpoint interface { + GetNode() Node + GetIfaceName() string + GetRandIfaceName() string + GetMac() net.HardwareAddr + String() string + // GetLink retrieves the link that the endpoint is assigned to + GetLink() Link + // Verify verifies that the endpoint is valid and can be deployed + Verify(*VerifyLinkParams) error + // HasSameNodeAndInterface returns true if an endpoint that implements this interface + // has the same node and interface name as the given endpoint. + HasSameNodeAndInterface(ept Endpoint) bool + Remove() error +} + +// EndpointGeneric is the generic endpoint struct that is used by all endpoint types. +type EndpointGeneric struct { + Node Node + IfaceName string + // Link is the link this endpoint belongs to. + Link Link + MAC net.HardwareAddr + randName string +} + +func NewEndpointGeneric(node Node, iface string) *EndpointGeneric { + return &EndpointGeneric{ + Node: node, + IfaceName: iface, + // random name is generated for the endpoint to avoid name collisions + // when it is first deployed in the root namespace + randName: genRandomIfName(), + } +} + +func (e *EndpointGeneric) GetRandIfaceName() string { + return e.randName +} + +func (e *EndpointGeneric) GetIfaceName() string { + return e.IfaceName +} + +func (e *EndpointGeneric) GetMac() net.HardwareAddr { + return e.MAC +} + +func (e *EndpointGeneric) GetLink() Link { + return e.Link +} + +func (e *EndpointGeneric) GetNode() Node { + return e.Node +} + +func (e *EndpointGeneric) Remove() error { + return e.GetNode().ExecFunction(func(_ ns.NetNS) error { + brSideEp, err := netlink.LinkByName(e.GetIfaceName()) + _, notfound := err.(netlink.LinkNotFoundError) + + switch { + case notfound: + // interface is not present, all good + return nil + case err != nil: + return err + } + + return netlink.LinkDel(brSideEp) + }) +} + +// HasSameNodeAndInterface returns true if the given endpoint has the same node and interface name +// as the `ept` endpoint. +func (e *EndpointGeneric) HasSameNodeAndInterface(ept Endpoint) bool { + return e.Node == ept.GetNode() && e.IfaceName == ept.GetIfaceName() +} + +func (e *EndpointGeneric) String() string { + return fmt.Sprintf("%s:%s", e.Node.GetShortName(), e.IfaceName) +} + +// CheckEndpointUniqueness checks that the given endpoint appears only once for the node +// it is assigned to. +func CheckEndpointUniqueness(e Endpoint) error { + for _, ept := range e.GetNode().GetEndpoints() { + if e == ept { + // since node contains all endpoints including the one we are checking + // we skip it + continue + } + // if `e` has the same node and interface name as `ept` then we have a duplicate + if e.HasSameNodeAndInterface(ept) { + return fmt.Errorf("duplicate endpoint %s", e) + } + } + + return nil +} + +// CheckEndpointExists checks that a certain +// interface exists in the network namespace of the given node. +func CheckEndpointExists(e Endpoint) error { + err := CheckEndpointDoesNotExistYet(e) + if err == nil { + return fmt.Errorf("interface %q does not exist", e.String()) + } + return nil +} + +// CheckEndpointDoesNotExistYet verifies that the interface referenced in the +// provided endpoint does not yet exist in the referenced node. +func CheckEndpointDoesNotExistYet(e Endpoint) error { + return e.GetNode().ExecFunction(func(_ ns.NetNS) error { + // we expect a netlink.LinkNotFoundError when querying for + // the interface with the given endpoints name + _, err := netlink.LinkByName(e.GetIfaceName()) + if _, notfound := err.(netlink.LinkNotFoundError); notfound { + return nil + } + + return fmt.Errorf("interface %s is defined via topology but does already exist", e.String()) + }) +} diff --git a/links/endpoint_bridge.go b/links/endpoint_bridge.go new file mode 100644 index 000000000..c160adc91 --- /dev/null +++ b/links/endpoint_bridge.go @@ -0,0 +1,53 @@ +package links + +import ( + "errors" + "fmt" + + "github.com/containernetworking/plugins/pkg/ns" + "github.com/vishvananda/netlink" +) + +type EndpointBridge struct { + EndpointGeneric +} + +func (e *EndpointBridge) Verify(p *VerifyLinkParams) error { + errs := []error{} + err := CheckEndpointUniqueness(e) + if err != nil { + errs = append(errs, err) + } + if p.RunBridgeExistsCheck { + err = CheckBridgeExists(e.GetNode()) + if err != nil { + errs = append(errs, err) + } + } + err = CheckEndpointDoesNotExistYet(e) + if err != nil { + errs = append(errs, err) + } + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} + +// CheckBridgeExists verifies that the given bridge is present in the +// network namespace referenced via the provided nspath handle. +func CheckBridgeExists(n Node) error { + return n.ExecFunction(func(_ ns.NetNS) error { + br, err := netlink.LinkByName(n.GetShortName()) + _, notfound := err.(netlink.LinkNotFoundError) + switch { + case notfound: + return fmt.Errorf("bridge %q referenced in topology but does not exist", n.GetShortName()) + case err != nil: + return err + case br.Type() != "bridge": + return fmt.Errorf("interface %s found. expected type \"bridge\", actual is %q", n.GetShortName(), br.Type()) + } + return nil + }) +} diff --git a/links/endpoint_host.go b/links/endpoint_host.go new file mode 100644 index 000000000..65534ae1b --- /dev/null +++ b/links/endpoint_host.go @@ -0,0 +1,23 @@ +package links + +import "errors" + +type EndpointHost struct { + EndpointGeneric +} + +func (e *EndpointHost) Verify(_ *VerifyLinkParams) error { + errs := []error{} + err := CheckEndpointUniqueness(e) + if err != nil { + errs = append(errs, err) + } + err = CheckEndpointDoesNotExistYet(e) + if err != nil { + errs = append(errs, err) + } + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} diff --git a/links/endpoint_macvlan.go b/links/endpoint_macvlan.go new file mode 100644 index 000000000..6b6f75298 --- /dev/null +++ b/links/endpoint_macvlan.go @@ -0,0 +1,10 @@ +package links + +type EndpointMacVlan struct { + EndpointGeneric +} + +// Verify verifies the veth based deployment pre-conditions +func (e *EndpointMacVlan) Verify(_ *VerifyLinkParams) error { + return CheckEndpointExists(e) +} diff --git a/links/endpoint_raw.go b/links/endpoint_raw.go new file mode 100644 index 000000000..6e0fdceb9 --- /dev/null +++ b/links/endpoint_raw.go @@ -0,0 +1,79 @@ +package links + +import ( + "fmt" + "net" + + "github.com/srl-labs/containerlab/utils" +) + +// EndpointRaw is the raw (string) representation of an endpoint as defined in the topology file +// for a given link definition. +type EndpointRaw struct { + Node string `yaml:"node"` + Iface string `yaml:"interface"` + MAC string `yaml:"mac,omitempty"` +} + +// NewEndpointRaw creates a new EndpointRaw struct. +func NewEndpointRaw(node, nodeIf, Mac string) *EndpointRaw { + return &EndpointRaw{ + Node: node, + Iface: nodeIf, + MAC: Mac, + } +} + +// Resolve resolves the EndpointRaw into an Endpoint interface that is implemented +// by a concrete endpoint struct such as EndpointBridge, EndpointHost, EndpointVeth. +// The type of an endpoint is determined by the node it belongs to. +// Resolving a raw endpoint adds an associated Link and Node to the endpoint. +// It also adds the endpoint to the node. +func (er *EndpointRaw) Resolve(params *ResolveParams, l Link) (Endpoint, error) { + // check if the referenced node does exist + node, exists := params.Nodes[er.Node] + if !exists { + return nil, fmt.Errorf("unable to find node %s", er.Node) + } + + genericEndpoint := NewEndpointGeneric(node, er.Iface) + genericEndpoint.Link = l + + var err error + if er.MAC == "" { + // if mac is not present generate one + genericEndpoint.MAC, err = utils.GenMac(ClabOUI) + if err != nil { + return nil, err + } + } else { + // if MAC is present, set it + m, err := net.ParseMAC(er.MAC) + if err != nil { + return nil, err + } + genericEndpoint.MAC = m + } + + var e Endpoint + + switch node.GetLinkEndpointType() { + case LinkEndpointTypeBridge: + e = &EndpointBridge{ + EndpointGeneric: *genericEndpoint, + } + case LinkEndpointTypeHost: + e = &EndpointHost{ + EndpointGeneric: *genericEndpoint, + } + case LinkEndpointTypeVeth: + e = &EndpointVeth{ + EndpointGeneric: *genericEndpoint, + } + } + + // also add the endpoint to the node + node.AddEndpoint(e) + + return e, nil +} diff --git a/links/endpoint_veth.go b/links/endpoint_veth.go new file mode 100644 index 000000000..0531a48d7 --- /dev/null +++ b/links/endpoint_veth.go @@ -0,0 +1,10 @@ +package links + +type EndpointVeth struct { + EndpointGeneric +} + +// Verify verifies the veth based deployment pre-conditions +func (e *EndpointVeth) Verify(_ *VerifyLinkParams) error { + return CheckEndpointUniqueness(e) +} diff --git a/links/generic_link_node.go b/links/generic_link_node.go new file mode 100644 index 000000000..4729a9885 --- /dev/null +++ b/links/generic_link_node.go @@ -0,0 +1,62 @@ +package links + +import ( + "context" + + "github.com/containernetworking/plugins/pkg/ns" + "github.com/srl-labs/containerlab/nodes/state" + "github.com/vishvananda/netlink" +) + +type GenericLinkNode struct { + shortname string + links []Link + endpoints []Endpoint + nspath string +} + +func (g *GenericLinkNode) AddLinkToContainer(_ context.Context, link netlink.Link, f func(ns.NetNS) error) error { + // retrieve the namespace handle + netns, err := ns.GetNS(g.nspath) + if err != nil { + return err + } + // move veth endpoint to namespace + if err = netlink.LinkSetNsFd(link, int(netns.Fd())); err != nil { + return err + } + // execute the given function + return netns.Do(f) +} + +func (g *GenericLinkNode) ExecFunction(f func(ns.NetNS) error) error { + // retrieve the namespace handle + netns, err := ns.GetNS(g.nspath) + if err != nil { + return err + } + // execute the given function + return netns.Do(f) +} + +func (g *GenericLinkNode) AddLink(l Link) { + g.links = append(g.links, l) +} + +func (g *GenericLinkNode) AddEndpoint(e Endpoint) { + g.endpoints = append(g.endpoints, e) +} + +func (g *GenericLinkNode) GetShortName() string { + return g.shortname +} + +func (g *GenericLinkNode) GetEndpoints() []Endpoint { + return g.endpoints +} + +func (g *GenericLinkNode) GetState() state.NodeState { + // The GenericLinkNode is the basis for Mgmt-Bridge and Host fake node. + // Both of these do generally exist. Hence the Deployed state in generally returned + return state.Deployed +} diff --git a/links/link.go b/links/link.go new file mode 100644 index 000000000..c94f07c25 --- /dev/null +++ b/links/link.go @@ -0,0 +1,361 @@ +package links + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/containernetworking/plugins/pkg/ns" + "github.com/google/uuid" + "github.com/srl-labs/containerlab/nodes/state" + "github.com/vishvananda/netlink" + "gopkg.in/yaml.v2" +) + +type LinkDeploymentState uint8 + +const ( + LinkDeploymentStateNotDeployed = iota + LinkDeploymentStateDeployed +) + +// LinkCommonParams represents the common parameters for all link types. +type LinkCommonParams struct { + MTU int `yaml:"mtu,omitempty"` + Labels map[string]string `yaml:"labels,omitempty"` + Vars map[string]interface{} `yaml:"vars,omitempty"` +} + +// LinkDefinition represents a link definition in the topology file. +type LinkDefinition struct { + Type string `yaml:"type,omitempty"` + Link RawLink `yaml:",inline"` +} + +// LinkType represents the type of a link definition. +type LinkType string + +const ( + LinkTypeVEth LinkType = "veth" + LinkTypeMgmtNet LinkType = "mgmt-net" + LinkTypeMacVLan LinkType = "macvlan" + LinkTypeHost LinkType = "host" + + // LinkTypeBrief is a link definition where link types + // are encoded in the endpoint definition as string and allow users + // to quickly type out link endpoints in a yaml file. + LinkTypeBrief LinkType = "brief" +) + +// parseLinkType parses a string representation of a link type into a LinkDefinitionType. +func parseLinkType(s string) (LinkType, error) { + switch strings.TrimSpace(strings.ToLower(s)) { + case string(LinkTypeMacVLan): + return LinkTypeMacVLan, nil + case string(LinkTypeVEth): + return LinkTypeVEth, nil + case string(LinkTypeMgmtNet): + return LinkTypeMgmtNet, nil + case string(LinkTypeHost): + return LinkTypeHost, nil + case string(LinkTypeBrief): + return LinkTypeBrief, nil + default: + return "", fmt.Errorf("unable to parse %q as LinkType", s) + } +} + +var _ yaml.Unmarshaler = (*LinkDefinition)(nil) + +// UnmarshalYAML deserializes links passed via topology file into LinkDefinition struct. +// It supports both the brief and specific link type notations. +func (ld *LinkDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error { + // alias struct to avoid recursion used only need to unmarshal + // the type field. + var a struct { + Type string `yaml:"type"` + } + + // yaml.TypeError is returned when the yaml parser encounters + // an unknown field. We want to ignore this error and continue + // parsing the rest of the fields as we only care about the type field + // in the a struct. + var e *yaml.TypeError + + err := unmarshal(&a) + if err != nil && !errors.As(err, &e) { + return err + } + + var lt LinkType + + // if no type is specified, we assume that brief notation of a link definition is used. + if a.Type == "" { + lt = LinkTypeBrief + ld.Type = string(LinkTypeBrief) + } else { + ld.Type = a.Type + + lt, err = parseLinkType(a.Type) + if err != nil { + return err + } + } + + switch lt { + case LinkTypeVEth: + var l struct { + // the Type field is injected artificially + // to allow strict yaml parsing to work. + Type string `yaml:"type"` + LinkVEthRaw `yaml:",inline"` + } + err := unmarshal(&l) + if err != nil { + return err + } + ld.Link = &l.LinkVEthRaw + case LinkTypeMgmtNet: + var l struct { + Type string `yaml:"type"` + LinkMgmtNetRaw `yaml:",inline"` + } + err := unmarshal(&l) + if err != nil { + return err + } + ld.Link = &l.LinkMgmtNetRaw + case LinkTypeHost: + var l struct { + Type string `yaml:"type"` + LinkHostRaw `yaml:",inline"` + } + err := unmarshal(&l) + if err != nil { + return err + } + ld.Link = &l.LinkHostRaw + case LinkTypeMacVLan: + var l struct { + Type string `yaml:"type"` + LinkMacVlanRaw `yaml:",inline"` + } + err := unmarshal(&l) + if err != nil { + return err + } + ld.Link = &l.LinkMacVlanRaw + case LinkTypeBrief: + // brief link's endpoint format + var l struct { + Type string `yaml:"type"` + LinkBriefRaw `yaml:",inline"` + } + + err := unmarshal(&l) + if err != nil { + return err + } + + ld.Type = string(LinkTypeBrief) + + ld.Link, err = l.LinkBriefRaw.ToTypeSpecificRawLink() + if err != nil { + return err + } + default: + return fmt.Errorf("unknown link type %q", lt) + } + + return nil +} + +// MarshalYAML serializes LinkDefinition (e.g when used with generate command). +// As of now it falls back to converting the LinkConfig into a +// RawVEthLink, such that the generated LinkConfigs adhere to the new LinkDefinition +// format instead of the brief one. +func (r *LinkDefinition) MarshalYAML() (interface{}, error) { + switch r.Link.GetType() { + case LinkTypeHost: + x := struct { + LinkHostRaw `yaml:",inline"` + Type string `yaml:"type"` + }{ + LinkHostRaw: *r.Link.(*LinkHostRaw), + Type: string(LinkTypeVEth), + } + return x, nil + case LinkTypeVEth: + x := struct { + // the Type field is injected artificially + // to allow strict yaml parsing to work. + Type string `yaml:"type"` + LinkVEthRaw `yaml:",inline"` + }{ + LinkVEthRaw: *r.Link.(*LinkVEthRaw), + Type: string(LinkTypeVEth), + } + return x, nil + case LinkTypeMgmtNet: + x := struct { + Type string `yaml:"type"` + LinkMgmtNetRaw `yaml:",inline"` + }{ + LinkMgmtNetRaw: *r.Link.(*LinkMgmtNetRaw), + Type: string(LinkTypeMgmtNet), + } + return x, nil + case LinkTypeMacVLan: + x := struct { + Type string `yaml:"type"` + LinkMacVlanRaw `yaml:",inline"` + }{ + LinkMacVlanRaw: *r.Link.(*LinkMacVlanRaw), + Type: string(LinkTypeMacVLan), + } + return x, nil + case LinkTypeBrief: + return r.Link, nil + } + + return nil, fmt.Errorf("unable to marshall") +} + +// RawLink is an interface that all raw link types must implement. +// Raw link types define the links as they are defined in the topology file +// and solely a product of unmarshalling. +// Raw links are later "resolved" to concrete link types (e.g LinkVeth). +type RawLink interface { + Resolve(params *ResolveParams) (Link, error) + GetType() LinkType +} + +// Link is an interface that all concrete link types must implement. +// Concrete link types are resolved from raw links and become part of CLab.Links. +type Link interface { + // Deploy deploys the link. + Deploy(context.Context) error + // Remove removes the link. + Remove(context.Context) error + // GetType returns the type of the link. + GetType() LinkType + // GetEndpoints returns the endpoints of the link. + GetEndpoints() []Endpoint +} + +func extractHostNodeInterfaceData(lb *LinkBriefRaw, specialEPIndex int) (host, hostIf, node, nodeIf string) { + // the index of the node is the specialEndpointIndex +1 modulo 2 + nodeindex := (specialEPIndex + 1) % 2 + + hostData := strings.SplitN(lb.Endpoints[specialEPIndex], ":", 2) + nodeData := strings.SplitN(lb.Endpoints[nodeindex], ":", 2) + + host = hostData[0] + hostIf = hostData[1] + node = nodeData[0] + nodeIf = nodeData[1] + + return host, hostIf, node, nodeIf +} + +func genRandomIfName() string { + s, _ := uuid.New().MarshalText() // .MarshalText() always return a nil error + return "clab-" + string(s[:8]) +} + +// Node interface is an interface that is satisfied by all nodes. +// It is used a subset of the nodes.Node interface and is used to pass nodes.Nodes +// to the link resolver without causing a circular dependency. +type Node interface { + // AddLinkToContainer adds a link to the node (container). + // In case of a regular container, it will push the link into the + // network namespace and then run the function f within the namespace + // this is to rename the link, set mtu, set the interface up, e.g. see link.SetNameMACAndUpInterface() + // + // In case of a bridge node (ovs or regular linux bridge) it will take the interface and make the bridge + // the master of the interface and bring the interface up. + AddLinkToContainer(ctx context.Context, link netlink.Link, f func(ns.NetNS) error) error + AddLink(l Link) + // AddEndpoint adds the Endpoint to the node + AddEndpoint(e Endpoint) + GetLinkEndpointType() LinkEndpointType + GetShortName() string + GetEndpoints() []Endpoint + ExecFunction(func(ns.NetNS) error) error + GetState() state.NodeState +} + +type LinkEndpointType string + +const ( + LinkEndpointTypeVeth = "veth" + LinkEndpointTypeBridge = "bridge" + LinkEndpointTypeHost = "host" +) + +// SetNameMACAndUpInterface is a helper function that will bind interface name and Mac +// and return a function that can run in the netns.Do() call for execution in a network namespace +func SetNameMACAndUpInterface(l netlink.Link, endpt Endpoint) func(ns.NetNS) error { + return func(_ ns.NetNS) error { + // rename the given link + err := netlink.LinkSetName(l, endpt.GetIfaceName()) + if err != nil { + return fmt.Errorf( + "failed to rename link: %v", err) + } + + // lets set the MAC address if provided + if len(endpt.GetMac()) == 6 { + err = netlink.LinkSetHardwareAddr(l, endpt.GetMac()) + if err != nil { + return err + } + } + + // bring the given link up + if err = netlink.LinkSetUp(l); err != nil { + return fmt.Errorf("failed to set %q up: %v", + endpt.GetIfaceName(), err) + } + return nil + } +} + +// SetNameMACMasterAndUpInterface is a helper function that will bind interface name and Mac +// and return a function that can run in the netns.Do() call for execution in a network namespace +func SetNameMACMasterAndUpInterface(l netlink.Link, endpt Endpoint, master string) func(ns.NetNS) error { + baseFunc := SetNameMACAndUpInterface(l, endpt) + + return func(n ns.NetNS) error { + // retrieve the bridge link + bridge, err := netlink.LinkByName(master) + if err != nil { + return err + } + // set the retrieved bridge as the master for the actual link + err = netlink.LinkSetMaster(l, bridge) + if err != nil { + return err + } + return baseFunc(n) + } +} + +// ResolveParams is a struct that is passed to the Resolve() function of a raw link +// to resolve it to a concrete link type. +// Parameters include all nodes of a topology and the name of the management bridge. +type ResolveParams struct { + Nodes map[string]Node + MgmtBridgeName string +} + +type VerifyLinkParams struct { + RunBridgeExistsCheck bool +} + +func NewVerifyLinkParams() *VerifyLinkParams { + return &VerifyLinkParams{ + RunBridgeExistsCheck: true, + } +} diff --git a/links/link_brief.go b/links/link_brief.go new file mode 100644 index 000000000..62dda5a00 --- /dev/null +++ b/links/link_brief.go @@ -0,0 +1,50 @@ +package links + +import ( + "fmt" + "strings" +) + +// LinkBriefRaw is the representation of any supported link in a brief format as defined in the topology file. +type LinkBriefRaw struct { + Endpoints []string `yaml:"endpoints"` + LinkCommonParams `yaml:",inline,omitempty"` +} + +// ToTypeSpecificRawLink resolves the brief link into a concrete RawLink implementation. +// LinkBrief is only used to have a short version of a link definition in the topology file, +// with ToRawLink we convert it into one of the supported link types. +func (l *LinkBriefRaw) ToTypeSpecificRawLink() (RawLink, error) { + // check two endpoints defined + if len(l.Endpoints) != 2 { + return nil, fmt.Errorf("endpoint definition should consist of exactly 2 entries. %d provided", len(l.Endpoints)) + } + for x, v := range l.Endpoints { + parts := strings.SplitN(v, ":", 2) + node := parts[0] + + lt, err := parseLinkType(node) + if err != nil { + continue + } + + switch lt { + case LinkTypeMacVLan: + return macVlanLinkFromBrief(l, x) + case LinkTypeMgmtNet: + return mgmtNetLinkFromBrief(l, x) + case LinkTypeHost: + return hostLinkFromBrief(l, x) + } + } + + return linkVEthRawFromLinkBriefRaw(l) +} + +func (l *LinkBriefRaw) GetType() LinkType { + return LinkTypeBrief +} + +func (l *LinkBriefRaw) Resolve(_ *ResolveParams) (Link, error) { + return nil, fmt.Errorf("resolve unimplemented on LinkBriefRaw. Use .ToTypeSpecificRawLink() and call resolve on the result") +} diff --git a/links/link_host.go b/links/link_host.go new file mode 100644 index 000000000..16af70551 --- /dev/null +++ b/links/link_host.go @@ -0,0 +1,107 @@ +package links + +import ( + "fmt" + + "github.com/containernetworking/plugins/pkg/ns" + log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/utils" +) + +// LinkHostRaw is the raw (string) representation of a host link as defined in the topology file. +type LinkHostRaw struct { + LinkCommonParams `yaml:",inline"` + HostInterface string `yaml:"host-interface"` + Endpoint *EndpointRaw `yaml:"endpoint"` +} + +// ToLinkBriefRaw converts the raw link into a LinkConfig. +func (r *LinkHostRaw) ToLinkBriefRaw() *LinkBriefRaw { + lc := &LinkBriefRaw{ + Endpoints: make([]string, 2), + LinkCommonParams: LinkCommonParams{ + MTU: r.MTU, + Labels: r.Labels, + Vars: r.Vars, + }, + } + + lc.Endpoints[0] = fmt.Sprintf("%s:%s", r.Endpoint.Node, r.Endpoint.Iface) + lc.Endpoints[1] = fmt.Sprintf("%s:%s", "host", r.HostInterface) + + return lc +} + +func hostLinkFromBrief(lb *LinkBriefRaw, specialEPIndex int) (*LinkHostRaw, error) { + _, hostIf, node, nodeIf := extractHostNodeInterfaceData(lb, specialEPIndex) + + result := &LinkHostRaw{ + LinkCommonParams: LinkCommonParams{ + MTU: lb.MTU, + Labels: lb.Labels, + Vars: lb.Vars, + }, + HostInterface: hostIf, + Endpoint: NewEndpointRaw(node, nodeIf, ""), + } + return result, nil +} + +func (r *LinkHostRaw) GetType() LinkType { + return LinkTypeHost +} + +func (r *LinkHostRaw) Resolve(params *ResolveParams) (Link, error) { + link := &LinkVEth{ + LinkCommonParams: r.LinkCommonParams, + } + // resolve and populate the endpoint + ep, err := r.Endpoint.Resolve(params, link) + if err != nil { + return nil, err + } + hostEp := &EndpointHost{ + EndpointGeneric: *NewEndpointGeneric(GetHostLinkNode(), r.HostInterface), + } + hostEp.Link = link + + hostEp.MAC, err = utils.GenMac(ClabOUI) + if err != nil { + return nil, err + } + // set the end point in the link + link.Endpoints = []Endpoint{ep, hostEp} + + return link, nil +} + +var _hostLinkNodeInstance *hostLinkNode + +// hostLinkNode represents a host node which is implicitly used when +// a host link is defined in the topology file. +type hostLinkNode struct { + GenericLinkNode +} + +func (*hostLinkNode) GetLinkEndpointType() LinkEndpointType { + return LinkEndpointTypeHost +} + +// GetHostLinkNode returns the host link node singleton. +func GetHostLinkNode() Node { + if _hostLinkNodeInstance == nil { + currns, err := ns.GetCurrentNS() + if err != nil { + log.Error(err) + } + nspath := currns.Path() + + _hostLinkNodeInstance = &hostLinkNode{ + GenericLinkNode: GenericLinkNode{shortname: "host", + endpoints: []Endpoint{}, + nspath: nspath, + }, + } + } + return _hostLinkNodeInstance +} diff --git a/links/link_macvlan.go b/links/link_macvlan.go new file mode 100644 index 000000000..55e8458eb --- /dev/null +++ b/links/link_macvlan.go @@ -0,0 +1,195 @@ +package links + +import ( + "context" + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/utils" + "github.com/vishvananda/netlink" +) + +// LinkMacVlanRaw is the raw (string) representation of a macvlan link as defined in the topology file. +type LinkMacVlanRaw struct { + LinkCommonParams `yaml:",inline"` + HostInterface string `yaml:"host-interface"` + Endpoint *EndpointRaw `yaml:"endpoint"` + Mode string `yaml:"mode"` +} + +// ToLinkBriefRaw converts the raw link into a LinkConfig. +func (r *LinkMacVlanRaw) ToLinkBriefRaw() *LinkBriefRaw { + lc := &LinkBriefRaw{ + Endpoints: make([]string, 2), + LinkCommonParams: LinkCommonParams{ + MTU: r.MTU, + Labels: r.Labels, + Vars: r.Vars, + }, + } + + lc.Endpoints[0] = fmt.Sprintf("%s:%s", r.Endpoint.Node, r.Endpoint.Iface) + lc.Endpoints[1] = fmt.Sprintf("%s:%s", "macvlan", r.HostInterface) + + return lc +} + +func (r *LinkMacVlanRaw) GetType() LinkType { + return LinkTypeMacVLan +} + +func macVlanLinkFromBrief(lb *LinkBriefRaw, specialEPIndex int) (*LinkMacVlanRaw, error) { + _, hostIf, node, nodeIf := extractHostNodeInterfaceData(lb, specialEPIndex) + + result := &LinkMacVlanRaw{ + LinkCommonParams: LinkCommonParams{ + MTU: lb.MTU, + Labels: lb.Labels, + Vars: lb.Vars, + }, + HostInterface: hostIf, + Endpoint: NewEndpointRaw(node, nodeIf, ""), + } + + return result, nil +} + +func (r *LinkMacVlanRaw) Resolve(params *ResolveParams) (Link, error) { + + ep := &EndpointMacVlan{ + EndpointGeneric: *NewEndpointGeneric(GetHostLinkNode(), r.HostInterface), + } + + var err error + ep.MAC, err = utils.GenMac(ClabOUI) + if err != nil { + return nil, err + } + + link := &LinkMacVlan{ + LinkCommonParams: r.LinkCommonParams, + HostEndpoint: ep, + } + ep.Link = link + // parse the MacVlanMode + mode, err := MacVlanModeParse(r.Mode) + if err != nil { + return nil, err + } + // set the mode in the link struct + link.Mode = mode + // resolve the endpoint + link.NodeEndpoint, err = r.Endpoint.Resolve(params, link) + if err != nil { + return nil, err + } + return link, nil +} + +type LinkMacVlan struct { + LinkCommonParams + HostEndpoint Endpoint + NodeEndpoint Endpoint + Mode MacVlanMode +} + +type MacVlanMode string + +const ( + MacVlanModeBridge = "bridge" + MacVlanModeVepa = "vepa" + MacVlanModePassthru = "passthru" + MacVlanModePrivate = "private" + MacVlanModeSource = "source" +) + +func MacVlanModeParse(s string) (MacVlanMode, error) { + switch s { + case MacVlanModeBridge: + return MacVlanModeBridge, nil + case MacVlanModeVepa: + return MacVlanModeVepa, nil + case MacVlanModePassthru: + return MacVlanModePassthru, nil + case MacVlanModePrivate: + return MacVlanModePrivate, nil + case MacVlanModeSource: + return MacVlanModeSource, nil + case "": + return MacVlanModeBridge, nil + } + return "", fmt.Errorf("unknown MacVlanMode %q", s) +} + +func (l *LinkMacVlan) GetType() LinkType { + return LinkTypeMacVLan +} + +func (l *LinkMacVlan) GetParentInterfaceMtu() (int, error) { + hostLink, err := netlink.LinkByName(l.HostEndpoint.GetIfaceName()) + if err != nil { + return 0, err + } + return hostLink.Attrs().MTU, nil +} + +func (l *LinkMacVlan) Deploy(ctx context.Context) error { + // lookup the parent host interface + parentInterface, err := netlink.LinkByName(l.HostEndpoint.GetIfaceName()) + if err != nil { + return err + } + + log.Infof("Creating MACVLAN link: %s <--> %s", l.HostEndpoint, l.NodeEndpoint) + + // set MacVlanMode + mode := netlink.MACVLAN_MODE_BRIDGE + switch l.Mode { + case MacVlanModeBridge: + case MacVlanModePassthru: + mode = netlink.MACVLAN_MODE_PASSTHRU + case MacVlanModeVepa: + mode = netlink.MACVLAN_MODE_VEPA + case MacVlanModePrivate: + mode = netlink.MACVLAN_MODE_PRIVATE + case MacVlanModeSource: + mode = netlink.MACVLAN_MODE_SOURCE + } + + // build Netlink Macvlan struct + link := &netlink.Macvlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: l.NodeEndpoint.GetRandIfaceName(), + ParentIndex: parentInterface.Attrs().Index, + MTU: l.MTU, + }, + Mode: mode, + } + // add the link in the Host NetNS + err = netlink.LinkAdd(link) + if err != nil { + return err + } + + // retrieve the Link by name + mvInterface, err := netlink.LinkByName(l.NodeEndpoint.GetRandIfaceName()) + if err != nil { + return fmt.Errorf("failed to lookup %q: %v", l.NodeEndpoint.GetRandIfaceName(), err) + } + + // add the link to the Node Namespace + err = l.NodeEndpoint.GetNode().AddLinkToContainer(ctx, mvInterface, SetNameMACAndUpInterface(mvInterface, l.NodeEndpoint)) + return err +} + +func (l *LinkMacVlan) Remove(_ context.Context) error { + // TODO + return nil +} + +func (l *LinkMacVlan) GetEndpoints() []Endpoint { + return []Endpoint{ + l.NodeEndpoint, + l.HostEndpoint, + } +} diff --git a/links/link_mgmt-net.go b/links/link_mgmt-net.go new file mode 100644 index 000000000..e73018e29 --- /dev/null +++ b/links/link_mgmt-net.go @@ -0,0 +1,123 @@ +package links + +import ( + "fmt" + + "github.com/containernetworking/plugins/pkg/ns" + log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/utils" +) + +type LinkMgmtNetRaw struct { + LinkCommonParams `yaml:",inline"` + HostInterface string `yaml:"host-interface"` + Endpoint *EndpointRaw `yaml:"endpoint"` +} + +func (r *LinkMgmtNetRaw) ToLinkBriefRaw() *LinkBriefRaw { + lc := &LinkBriefRaw{ + Endpoints: make([]string, 2), + LinkCommonParams: LinkCommonParams{ + MTU: r.MTU, + Labels: r.Labels, + Vars: r.Vars, + }, + } + + lc.Endpoints[0] = fmt.Sprintf("%s:%s", r.Endpoint.Node, r.Endpoint.Iface) + lc.Endpoints[1] = fmt.Sprintf("%s:%s", "mgmt-net", r.HostInterface) + + return lc +} + +func (r *LinkMgmtNetRaw) Resolve(params *ResolveParams) (Link, error) { + + // create the LinkMgmtNet struct + link := &LinkVEth{ + LinkCommonParams: r.LinkCommonParams, + } + + mgmtBridgeNode := GetMgmtBrLinkNode() + + bridgeEp := &EndpointBridge{ + EndpointGeneric: *NewEndpointGeneric(mgmtBridgeNode, r.HostInterface), + } + bridgeEp.Link = link + + var err error + bridgeEp.MAC, err = utils.GenMac(ClabOUI) + if err != nil { + return nil, err + } + + // add endpoint to fake mgmt bridge node + mgmtBridgeNode.AddEndpoint(bridgeEp) + + // resolve and populate the endpoint + contEp, err := r.Endpoint.Resolve(params, link) + if err != nil { + return nil, err + } + + link.Endpoints = []Endpoint{bridgeEp, contEp} + + return link, nil +} + +func (r *LinkMgmtNetRaw) GetType() LinkType { + return LinkTypeMgmtNet +} + +func mgmtNetLinkFromBrief(lb *LinkBriefRaw, specialEPIndex int) (*LinkMgmtNetRaw, error) { + _, hostIf, node, nodeIf := extractHostNodeInterfaceData(lb, specialEPIndex) + + result := &LinkMgmtNetRaw{ + LinkCommonParams: LinkCommonParams{ + MTU: lb.MTU, + Labels: lb.Labels, + Vars: lb.Vars, + }, + HostInterface: hostIf, + Endpoint: NewEndpointRaw(node, nodeIf, ""), + } + return result, nil +} + +var _mgmtBrLinkMgmtBrInstance *mgmtBridgeLinkNode + +// mgmtBridgeLinkNode is a special node that represents the mgmt bridge node +// that is used when mgmt-net link is defined in the topology. +type mgmtBridgeLinkNode struct { + GenericLinkNode +} + +func (*mgmtBridgeLinkNode) GetLinkEndpointType() LinkEndpointType { + return LinkEndpointTypeBridge +} + +func getMgmtBrLinkNode() *mgmtBridgeLinkNode { + if _mgmtBrLinkMgmtBrInstance == nil { + currns, err := ns.GetCurrentNS() + if err != nil { + log.Error(err) + } + nspath := currns.Path() + _mgmtBrLinkMgmtBrInstance = &mgmtBridgeLinkNode{ + GenericLinkNode: GenericLinkNode{ + shortname: "mgmt-net", + endpoints: []Endpoint{}, + nspath: nspath, + }, + } + } + return _mgmtBrLinkMgmtBrInstance +} + +func GetMgmtBrLinkNode() Node { // skipcq: RVV-B0001 + return getMgmtBrLinkNode() +} + +func SetMgmtNetUnderlayingBridge(bridge string) error { + getMgmtBrLinkNode().GenericLinkNode.shortname = bridge + return nil +} diff --git a/types/link_test.go b/links/link_test.go similarity index 53% rename from types/link_test.go rename to links/link_test.go index a6aaac97e..c29ae523d 100644 --- a/types/link_test.go +++ b/links/link_test.go @@ -1,4 +1,4 @@ -package types +package links import ( "testing" @@ -14,7 +14,7 @@ func TestParseLinkType(t *testing.T) { tests := []struct { name string args args - want LinkDefinitionType + want LinkType wantErr bool }{ { @@ -90,7 +90,7 @@ func TestUnmarshalRawLinksYaml(t *testing.T) { wantErr bool }{ { - name: "legacy link", + name: "brief link with veth endpoints", args: args{ yaml: []byte(` endpoints: @@ -101,10 +101,110 @@ func TestUnmarshalRawLinksYaml(t *testing.T) { wantErr: false, want: LinkDefinition{ Type: string(LinkTypeBrief), - LinkConfig: LinkConfig{ - Endpoints: []string{ - "srl1:e1-5", - "srl2:e1-5", + Link: &LinkVEthRaw{ + Endpoints: []*EndpointRaw{ + NewEndpointRaw("srl1", "e1-5", ""), + NewEndpointRaw("srl2", "e1-5", ""), + }, + }, + }, + }, + { + name: "brief link with veth endpoints and mtu", + args: args{ + yaml: []byte(` + endpoints: + - "srl1:e1-5" + - "srl2:e1-5" + mtu: 1500 + `), + }, + wantErr: false, + want: LinkDefinition{ + Type: string(LinkTypeBrief), + Link: &LinkVEthRaw{ + Endpoints: []*EndpointRaw{ + NewEndpointRaw("srl1", "e1-5", ""), + NewEndpointRaw("srl2", "e1-5", ""), + }, + LinkCommonParams: LinkCommonParams{ + MTU: 1500, + }, + }, + }, + }, + { + name: "brief link with macvlan endpoint", + args: args{ + yaml: []byte(` + endpoints: ["srl1:e1-1", "macvlan:eth0"]`, + ), + }, + wantErr: false, + want: LinkDefinition{ + Type: string(LinkTypeBrief), + Link: &LinkMacVlanRaw{ + HostInterface: "eth0", + Endpoint: NewEndpointRaw("srl1", "e1-1", ""), + }, + }, + }, + { + name: "brief link with mgmt-net endpoint", + args: args{ + yaml: []byte(` + endpoints: ["srl1:e1-1", "mgmt-net:srl1-e1-1"]`, + ), + }, + wantErr: false, + want: LinkDefinition{ + Type: string(LinkTypeBrief), + Link: &LinkMgmtNetRaw{ + HostInterface: "srl1-e1-1", + Endpoint: NewEndpointRaw("srl1", "e1-1", ""), + }, + }, + }, + { + name: "brief link with host endpoint", + args: args{ + yaml: []byte(` + endpoints: ["srl1:e1-1", "host:srl1-e1-1"]`, + ), + }, + wantErr: false, + want: LinkDefinition{ + Type: string(LinkTypeBrief), + Link: &LinkHostRaw{ + HostInterface: "srl1-e1-1", + Endpoint: NewEndpointRaw("srl1", "e1-1", ""), + }, + }, + }, + { + name: "veth link", + args: args{ + yaml: []byte(` + type: veth + mtu: 1400 + endpoints: + - node: srl1 + interface: e1-1 + mac: 02:00:00:00:00:01 + - node: srl2 + interface: e1-2 + `), + }, + wantErr: false, + want: LinkDefinition{ + Type: string(LinkTypeVEth), + Link: &LinkVEthRaw{ + Endpoints: []*EndpointRaw{ + NewEndpointRaw("srl1", "e1-1", "02:00:00:00:00:01"), + NewEndpointRaw("srl2", "e1-2", ""), + }, + LinkCommonParams: LinkCommonParams{ + MTU: 1400, }, }, }, @@ -123,11 +223,9 @@ func TestUnmarshalRawLinksYaml(t *testing.T) { wantErr: false, want: LinkDefinition{ Type: string(LinkTypeMgmtNet), - LinkConfig: LinkConfig{ - Endpoints: []string{ - "srl1:e1-5", - "mgmt-net:srl1_e1-5", - }, + Link: &LinkMgmtNetRaw{ + HostInterface: "srl1_e1-5", + Endpoint: NewEndpointRaw("srl1", "e1-5", ""), }, }, }, @@ -145,11 +243,9 @@ func TestUnmarshalRawLinksYaml(t *testing.T) { wantErr: false, want: LinkDefinition{ Type: string(LinkTypeHost), - LinkConfig: LinkConfig{ - Endpoints: []string{ - "srl1:e1-5", - "host:srl1_e1-5", - }, + Link: &LinkHostRaw{ + HostInterface: "srl1_e1-5", + Endpoint: NewEndpointRaw("srl1", "e1-5", ""), }, }, }, @@ -167,11 +263,9 @@ func TestUnmarshalRawLinksYaml(t *testing.T) { wantErr: false, want: LinkDefinition{ Type: string(LinkTypeMacVLan), - LinkConfig: LinkConfig{ - Endpoints: []string{ - "srl1:e1-5", - "macvlan:srl1_e1-5", - }, + Link: &LinkMacVlanRaw{ + HostInterface: "srl1_e1-5", + Endpoint: NewEndpointRaw("srl1", "e1-5", ""), }, }, }, diff --git a/links/link_veth.go b/links/link_veth.go new file mode 100644 index 000000000..af60a7561 --- /dev/null +++ b/links/link_veth.go @@ -0,0 +1,180 @@ +package links + +import ( + "context" + "fmt" + "sync" + + "github.com/containernetworking/plugins/pkg/ns" + log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/nodes/state" + "github.com/vishvananda/netlink" +) + +// LinkVEthRaw is the raw (string) representation of a veth link as defined in the topology file. +type LinkVEthRaw struct { + LinkCommonParams `yaml:",inline"` + Endpoints []*EndpointRaw `yaml:"endpoints"` +} + +// ToLinkBriefRaw converts the raw link into a LinkConfig. +func (r *LinkVEthRaw) ToLinkBriefRaw() *LinkBriefRaw { + lc := &LinkBriefRaw{ + Endpoints: []string{}, + LinkCommonParams: LinkCommonParams{ + MTU: r.MTU, + Labels: r.Labels, + Vars: r.Vars, + }, + } + + for _, e := range r.Endpoints { + lc.Endpoints = append(lc.Endpoints, fmt.Sprintf("%s:%s", e.Node, e.Iface)) + } + return lc +} + +func (r *LinkVEthRaw) GetType() LinkType { + return LinkTypeVEth +} + +// Resolve resolves the raw veth link definition into a Link interface that is implemented +// by a concrete LinkVEth struct. +// Resolving a veth link resolves its endpoints. +func (r *LinkVEthRaw) Resolve(params *ResolveParams) (Link, error) { + // create LinkVEth struct + l := &LinkVEth{ + LinkCommonParams: r.LinkCommonParams, + Endpoints: make([]Endpoint, 0, 2), + } + + // resolve raw endpoints (epr) to endpoints (ep) + for _, epr := range r.Endpoints { + ep, err := epr.Resolve(params, l) + if err != nil { + return nil, err + } + + l.Endpoints = append(l.Endpoints, ep) + } + + return l, nil +} + +// linkVEthRawFromLinkBriefRaw creates a raw veth link from a LinkBriefRaw. +func linkVEthRawFromLinkBriefRaw(lb *LinkBriefRaw) (*LinkVEthRaw, error) { + host, hostIf, node, nodeIf := extractHostNodeInterfaceData(lb, 0) + + result := &LinkVEthRaw{ + LinkCommonParams: LinkCommonParams{ + MTU: lb.MTU, + Labels: lb.Labels, + Vars: lb.Vars, + }, + Endpoints: []*EndpointRaw{ + NewEndpointRaw(host, hostIf, ""), + NewEndpointRaw(node, nodeIf, ""), + }, + } + return result, nil +} + +type LinkVEth struct { + LinkCommonParams + Endpoints []Endpoint + + deploymentState LinkDeploymentState + stateMutex sync.RWMutex +} + +func (*LinkVEth) GetType() LinkType { + return LinkTypeVEth +} + +func (l *LinkVEth) Verify() { + +} + +func (l *LinkVEth) Deploy(ctx context.Context) error { + // since each node calls deploy on its links, we need to make sure that we only deploy + // the link once, even if multiple nodes call deploy on the same link. + l.stateMutex.RLock() + if l.deploymentState == LinkDeploymentStateDeployed { + return nil + } + l.stateMutex.RUnlock() + + for _, ep := range l.GetEndpoints() { + if ep.GetNode().GetState() != state.Deployed { + return nil + } + } + + log.Infof("Creating link: %s <--> %s", l.GetEndpoints()[0], l.GetEndpoints()[1]) + + // build the netlink.Veth struct for the link provisioning + linkA := &netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{ + Name: l.Endpoints[0].GetRandIfaceName(), + MTU: l.MTU, + // Mac address is set later on + }, + PeerName: l.Endpoints[1].GetRandIfaceName(), + // PeerMac address is set later on + } + + // add the link + err := netlink.LinkAdd(linkA) + if err != nil { + return err + } + + // retrieve the netlink.Link for the B / Peer side of the link + linkB, err := netlink.LinkByName(l.Endpoints[1].GetRandIfaceName()) + if err != nil { + return err + } + + // both ends of the link need to be moved to the relevant network namespace + // and enabled (up). This is done via linkSetupFunc. + // based on the endpoint type the link setup function is different. + // linkSetupFunc is executed in a netns of a node. + for idx, link := range []netlink.Link{linkA, linkB} { + var linkSetupFunc func(ns.NetNS) error + switch l.Endpoints[idx].GetNode().GetLinkEndpointType() { + + // if the endpoint is a bridge we also need to set the master of the interface to the bridge + case LinkEndpointTypeBridge: + bridgeName := l.Endpoints[idx].GetNode().GetShortName() + // set the adjustmentFunc to the function that, besides the name, mac and up state + // also sets the Master of the interface to the bridge + linkSetupFunc = SetNameMACMasterAndUpInterface(link, l.Endpoints[idx], bridgeName) + default: + // default case is a regular veth link where both ends are regular linux interfaces + // in the relevant containers. + linkSetupFunc = SetNameMACAndUpInterface(link, l.Endpoints[idx]) + } + + // if the node is a regular namespace node + // add link to node, rename, set mac and Up + err = l.Endpoints[idx].GetNode().AddLinkToContainer(ctx, link, linkSetupFunc) + if err != nil { + return err + } + } + + l.stateMutex.Lock() + l.deploymentState = LinkDeploymentStateDeployed + l.stateMutex.Unlock() + + return nil +} + +func (l *LinkVEth) Remove(_ context.Context) error { + // TODO + return nil +} + +func (l *LinkVEth) GetEndpoints() []Endpoint { + return l.Endpoints +} diff --git a/links/link_veth_test.go b/links/link_veth_test.go new file mode 100644 index 000000000..064bf6ed8 --- /dev/null +++ b/links/link_veth_test.go @@ -0,0 +1,240 @@ +package links + +import ( + "context" + "testing" + + "github.com/containernetworking/plugins/pkg/ns" + "github.com/google/go-cmp/cmp" + "github.com/srl-labs/containerlab/nodes/state" + "github.com/vishvananda/netlink" +) + +func TestLinkVEthRaw_ToLinkBriefRaw(t *testing.T) { + type fields struct { + LinkCommonParams LinkCommonParams + Endpoints []*EndpointRaw + } + tests := []struct { + name string + fields fields + want *LinkBriefRaw + }{ + { + name: "test1", + fields: fields{ + LinkCommonParams: LinkCommonParams{ + MTU: 1500, + Labels: map[string]string{"foo": "bar"}, + Vars: map[string]any{"foo": "bar"}, + }, + Endpoints: []*EndpointRaw{ + { + Node: "node1", + Iface: "eth1", + }, + { + Node: "node2", + Iface: "eth2", + }, + }, + }, + want: &LinkBriefRaw{ + Endpoints: []string{"node1:eth1", "node2:eth2"}, + LinkCommonParams: LinkCommonParams{ + MTU: 1500, + Labels: map[string]string{"foo": "bar"}, + Vars: map[string]any{"foo": "bar"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &LinkVEthRaw{ + LinkCommonParams: tt.fields.LinkCommonParams, + Endpoints: tt.fields.Endpoints, + } + + got := r.ToLinkBriefRaw() + + if d := cmp.Diff(got, tt.want); d != "" { + t.Errorf("LinkVEthRaw.ToLinkBriefRaw() = %s", d) + } + }) + } +} + +func TestLinkVEthRaw_GetType(t *testing.T) { + type fields struct { + LinkCommonParams LinkCommonParams + Endpoints []*EndpointRaw + } + tests := []struct { + name string + fields fields + want LinkType + }{ + { + name: "test1", + fields: fields{ + LinkCommonParams: LinkCommonParams{}, + Endpoints: []*EndpointRaw{}, + }, + want: LinkTypeVEth, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &LinkVEthRaw{ + LinkCommonParams: tt.fields.LinkCommonParams, + Endpoints: tt.fields.Endpoints, + } + if got := r.GetType(); got != tt.want { + t.Errorf("LinkVEthRaw.GetType() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLinkVEthRaw_Resolve(t *testing.T) { + fn1 := newFakeNode("node1") + fn2 := newFakeNode("node2") + + type fields struct { + LinkCommonParams LinkCommonParams + Endpoints []*EndpointRaw + } + type args struct { + params *ResolveParams + } + tests := []struct { + name string + fields fields + args args + want *LinkVEth + wantErr bool + }{ + { + name: "test1", + fields: fields{ + LinkCommonParams: LinkCommonParams{ + MTU: 1500, + Labels: map[string]string{"foo": "bar"}, + Vars: map[string]any{"foo": "bar"}, + }, + Endpoints: []*EndpointRaw{ + { + Node: "node1", + Iface: "eth1", + }, + { + Node: "node2", + Iface: "eth2", + }, + }, + }, + args: args{ + params: &ResolveParams{ + Nodes: map[string]Node{ + "node1": fn1, + "node2": fn2, + }, + }, + }, + want: &LinkVEth{ + LinkCommonParams: LinkCommonParams{ + MTU: 1500, + Labels: map[string]string{"foo": "bar"}, + Vars: map[string]any{"foo": "bar"}, + }, + Endpoints: []Endpoint{ + &EndpointVeth{ + EndpointGeneric: EndpointGeneric{ + Node: fn1, + IfaceName: "eth1", + }, + }, + &EndpointVeth{ + EndpointGeneric: EndpointGeneric{ + Node: fn2, + IfaceName: "eth2", + }, + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &LinkVEthRaw{ + LinkCommonParams: tt.fields.LinkCommonParams, + Endpoints: tt.fields.Endpoints, + } + got, err := r.Resolve(tt.args.params) + if (err != nil) != tt.wantErr { + t.Errorf("LinkVEthRaw.Resolve() error = %v, wantErr %v", err, tt.wantErr) + return + } + l := got.(*LinkVEth) + if d := cmp.Diff(l.LinkCommonParams, tt.want.LinkCommonParams); d != "" { + t.Errorf("LinkVEthRaw.Resolve() LinkCommonParams diff = %s", d) + } + + for i, e := range l.Endpoints { + if e.(*EndpointVeth).IfaceName != tt.want.Endpoints[i].(*EndpointVeth).IfaceName { + t.Errorf("LinkVEthRaw.Resolve() EndpointVeth got %s, want %s", e.(*EndpointVeth).IfaceName, tt.want.Endpoints[i].(*EndpointVeth).IfaceName) + } + + if e.(*EndpointVeth).Node != tt.want.Endpoints[i].(*EndpointVeth).Node { + t.Errorf("LinkVEthRaw.Resolve() EndpointVeth got %s, want %s", e.(*EndpointVeth).Node, tt.want.Endpoints[i].(*EndpointVeth).Node) + } + } + }) + } +} + +// fakeNode is a fake implementation of Node for testing. +type fakeNode struct { + Name string + Endpoints []Endpoint +} + +func newFakeNode(name string) *fakeNode { + return &fakeNode{Name: name} +} + +func (*fakeNode) AddLinkToContainer(_ context.Context, _ netlink.Link, _ func(ns.NetNS) error) error { + panic("not implemented") +} + +func (*fakeNode) AddLink(_ Link) { + panic("not implemented") +} + +// AddEndpoint adds the Endpoint to the node +func (f *fakeNode) AddEndpoint(e Endpoint) { + f.Endpoints = append(f.Endpoints, e) +} + +func (*fakeNode) GetLinkEndpointType() LinkEndpointType { + return LinkEndpointTypeVeth +} + +func (*fakeNode) GetShortName() string { + panic("not implemented") +} + +func (*fakeNode) GetEndpoints() []Endpoint { + panic("not implemented") +} + +func (*fakeNode) ExecFunction(_ func(ns.NetNS) error) error { + panic("not implemented") +} + +func (*fakeNode) GetState() state.NodeState { + panic("not implemented") +} diff --git a/mocks/default_node.go b/mocks/mocknodes/default_node.go similarity index 98% rename from mocks/default_node.go rename to mocks/mocknodes/default_node.go index 2db9efb91..294a9cf61 100644 --- a/mocks/default_node.go +++ b/mocks/mocknodes/default_node.go @@ -1,8 +1,8 @@ // Code generated by MockGen. DO NOT EDIT. // Source: nodes/default_node.go -// Package mocks is a generated GoMock package. -package mocks +// Package mocknodes is a generated GoMock package. +package mocknodes import ( context "context" diff --git a/mocks/node.go b/mocks/mocknodes/node.go similarity index 70% rename from mocks/node.go rename to mocks/mocknodes/node.go index 530c1bf52..a8d6284b5 100644 --- a/mocks/node.go +++ b/mocks/mocknodes/node.go @@ -1,18 +1,22 @@ // Code generated by MockGen. DO NOT EDIT. // Source: nodes/node.go -// Package mocks is a generated GoMock package. -package mocks +// Package mocknodes is a generated GoMock package. +package mocknodes import ( context "context" reflect "reflect" + ns "github.com/containernetworking/plugins/pkg/ns" gomock "github.com/golang/mock/gomock" exec "github.com/srl-labs/containerlab/clab/exec" + links "github.com/srl-labs/containerlab/links" nodes "github.com/srl-labs/containerlab/nodes" + state "github.com/srl-labs/containerlab/nodes/state" runtime "github.com/srl-labs/containerlab/runtime" types "github.com/srl-labs/containerlab/types" + netlink "github.com/vishvananda/netlink" ) // MockNode is a mock of Node interface. @@ -38,6 +42,44 @@ func (m *MockNode) EXPECT() *MockNodeMockRecorder { return m.recorder } +// AddEndpoint mocks base method. +func (m *MockNode) AddEndpoint(e links.Endpoint) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddEndpoint", e) +} + +// AddEndpoint indicates an expected call of AddEndpoint. +func (mr *MockNodeMockRecorder) AddEndpoint(e interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEndpoint", reflect.TypeOf((*MockNode)(nil).AddEndpoint), e) +} + +// AddLink mocks base method. +func (m *MockNode) AddLink(l links.Link) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddLink", l) +} + +// AddLink indicates an expected call of AddLink. +func (mr *MockNodeMockRecorder) AddLink(l interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLink", reflect.TypeOf((*MockNode)(nil).AddLink), l) +} + +// AddLinkToContainer mocks base method. +func (m *MockNode) AddLinkToContainer(ctx context.Context, link netlink.Link, f func(ns.NetNS) error) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddLinkToContainer", ctx, link, f) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddLinkToContainer indicates an expected call of AddLinkToContainer. +func (mr *MockNodeMockRecorder) AddLinkToContainer(ctx, link, f interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLinkToContainer", reflect.TypeOf((*MockNode)(nil).AddLinkToContainer), ctx, link, f) +} + // CheckDeploymentConditions mocks base method. func (m *MockNode) CheckDeploymentConditions(arg0 context.Context) error { m.ctrl.T.Helper() @@ -122,6 +164,34 @@ func (mr *MockNodeMockRecorder) Deploy(arg0, arg1 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deploy", reflect.TypeOf((*MockNode)(nil).Deploy), arg0, arg1) } +// DeployLinks mocks base method. +func (m *MockNode) DeployLinks(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeployLinks", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeployLinks indicates an expected call of DeployLinks. +func (mr *MockNodeMockRecorder) DeployLinks(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeployLinks", reflect.TypeOf((*MockNode)(nil).DeployLinks), ctx) +} + +// ExecFunction mocks base method. +func (m *MockNode) ExecFunction(arg0 func(ns.NetNS) error) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecFunction", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// ExecFunction indicates an expected call of ExecFunction. +func (mr *MockNodeMockRecorder) ExecFunction(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecFunction", reflect.TypeOf((*MockNode)(nil).ExecFunction), arg0) +} + // GenerateConfig mocks base method. func (m *MockNode) GenerateConfig(dst, templ string) error { m.ctrl.T.Helper() @@ -151,6 +221,20 @@ func (mr *MockNodeMockRecorder) GetContainers(ctx interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContainers", reflect.TypeOf((*MockNode)(nil).GetContainers), ctx) } +// GetEndpoints mocks base method. +func (m *MockNode) GetEndpoints() []links.Endpoint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEndpoints") + ret0, _ := ret[0].([]links.Endpoint) + return ret0 +} + +// GetEndpoints indicates an expected call of GetEndpoints. +func (mr *MockNodeMockRecorder) GetEndpoints() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEndpoints", reflect.TypeOf((*MockNode)(nil).GetEndpoints)) +} + // GetImages mocks base method. func (m *MockNode) GetImages(arg0 context.Context) map[string]string { m.ctrl.T.Helper() @@ -165,6 +249,20 @@ func (mr *MockNodeMockRecorder) GetImages(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImages", reflect.TypeOf((*MockNode)(nil).GetImages), arg0) } +// GetLinkEndpointType mocks base method. +func (m *MockNode) GetLinkEndpointType() links.LinkEndpointType { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLinkEndpointType") + ret0, _ := ret[0].(links.LinkEndpointType) + return ret0 +} + +// GetLinkEndpointType indicates an expected call of GetLinkEndpointType. +func (mr *MockNodeMockRecorder) GetLinkEndpointType() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLinkEndpointType", reflect.TypeOf((*MockNode)(nil).GetLinkEndpointType)) +} + // GetRuntime mocks base method. func (m *MockNode) GetRuntime() runtime.ContainerRuntime { m.ctrl.T.Helper() @@ -179,6 +277,34 @@ func (mr *MockNodeMockRecorder) GetRuntime() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRuntime", reflect.TypeOf((*MockNode)(nil).GetRuntime)) } +// GetShortName mocks base method. +func (m *MockNode) GetShortName() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetShortName") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetShortName indicates an expected call of GetShortName. +func (mr *MockNodeMockRecorder) GetShortName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetShortName", reflect.TypeOf((*MockNode)(nil).GetShortName)) +} + +// GetState mocks base method. +func (m *MockNode) GetState() state.NodeState { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetState") + ret0, _ := ret[0].(state.NodeState) + return ret0 +} + +// GetState indicates an expected call of GetState. +func (mr *MockNodeMockRecorder) GetState() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetState", reflect.TypeOf((*MockNode)(nil).GetState)) +} + // Init mocks base method. func (m *MockNode) Init(arg0 *types.NodeConfig, arg1 ...nodes.NodeOption) error { m.ctrl.T.Helper() diff --git a/mocks/runtime.go b/mocks/mockruntime/runtime.go similarity index 88% rename from mocks/runtime.go rename to mocks/mockruntime/runtime.go index 64b84b8f5..72a4d2623 100644 --- a/mocks/runtime.go +++ b/mocks/mockruntime/runtime.go @@ -1,8 +1,8 @@ // Code generated by MockGen. DO NOT EDIT. // Source: runtime/runtime.go -// Package mocks is a generated GoMock package. -package mocks +// Package mockruntime is a generated GoMock package. +package mockruntime import ( context "context" @@ -10,6 +10,7 @@ import ( gomock "github.com/golang/mock/gomock" exec "github.com/srl-labs/containerlab/clab/exec" + links "github.com/srl-labs/containerlab/links" runtime "github.com/srl-labs/containerlab/runtime" types "github.com/srl-labs/containerlab/types" ) @@ -271,7 +272,7 @@ func (mr *MockContainerRuntimeMockRecorder) PullImage(arg0, arg1, arg2 interface } // StartContainer mocks base method. -func (m *MockContainerRuntime) StartContainer(arg0 context.Context, arg1 string, arg2 *types.NodeConfig) (interface{}, error) { +func (m *MockContainerRuntime) StartContainer(arg0 context.Context, arg1 string, arg2 runtime.Node) (interface{}, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "StartContainer", arg0, arg1, arg2) ret0, _ := ret[0].(interface{}) @@ -348,3 +349,54 @@ func (mr *MockContainerRuntimeMockRecorder) WithMgmtNet(arg0 interface{}) *gomoc mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithMgmtNet", reflect.TypeOf((*MockContainerRuntime)(nil).WithMgmtNet), arg0) } + +// MockNode is a mock of Node interface. +type MockNode struct { + ctrl *gomock.Controller + recorder *MockNodeMockRecorder +} + +// MockNodeMockRecorder is the mock recorder for MockNode. +type MockNodeMockRecorder struct { + mock *MockNode +} + +// NewMockNode creates a new mock instance. +func NewMockNode(ctrl *gomock.Controller) *MockNode { + mock := &MockNode{ctrl: ctrl} + mock.recorder = &MockNodeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockNode) EXPECT() *MockNodeMockRecorder { + return m.recorder +} + +// Config mocks base method. +func (m *MockNode) Config() *types.NodeConfig { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Config") + ret0, _ := ret[0].(*types.NodeConfig) + return ret0 +} + +// Config indicates an expected call of Config. +func (mr *MockNodeMockRecorder) Config() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Config", reflect.TypeOf((*MockNode)(nil).Config)) +} + +// GetEndpoints mocks base method. +func (m *MockNode) GetEndpoints() []links.Endpoint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEndpoints") + ret0, _ := ret[0].([]links.Endpoint) + return ret0 +} + +// GetEndpoints indicates an expected call of GetEndpoints. +func (mr *MockNodeMockRecorder) GetEndpoints() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEndpoints", reflect.TypeOf((*MockNode)(nil).GetEndpoints)) +} diff --git a/nodes/bridge/bridge.go b/nodes/bridge/bridge.go index e848a9231..2260b7fab 100644 --- a/nodes/bridge/bridge.go +++ b/nodes/bridge/bridge.go @@ -11,12 +11,16 @@ import ( "regexp" "strings" + "github.com/containernetworking/plugins/pkg/ns" log "github.com/sirupsen/logrus" cExec "github.com/srl-labs/containerlab/clab/exec" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/nodes" + "github.com/srl-labs/containerlab/nodes/state" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" "github.com/srl-labs/containerlab/utils" + "github.com/vishvananda/netlink" ) var kindnames = []string{"bridge"} @@ -45,14 +49,17 @@ func (s *bridge) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error { for _, o := range opts { o(s) } - s.Cfg.IsRootNamespaceBased = true return nil } -func (*bridge) Deploy(_ context.Context, _ *nodes.DeployParams) error { return nil } -func (*bridge) Delete(_ context.Context) error { return nil } -func (*bridge) GetImages(_ context.Context) map[string]string { return map[string]string{} } +func (n *bridge) Deploy(_ context.Context, _ *nodes.DeployParams) error { + n.SetState(state.Deployed) + return nil +} + +func (*bridge) Delete(_ context.Context) error { return nil } +func (*bridge) GetImages(_ context.Context) map[string]string { return map[string]string{} } // DeleteNetnsSymlink is a noop for bridge nodes. func (b *bridge) DeleteNetnsSymlink() (err error) { return nil } @@ -120,3 +127,34 @@ func (b *bridge) RunExec(_ context.Context, _ *cExec.ExecCmd) (*cExec.ExecResult return nil, cExec.ErrRunExecNotSupported } + +func (b *bridge) AddLinkToContainer(ctx context.Context, link netlink.Link, f func(ns.NetNS) error) error { + return BridgeAddLink(ctx, link, b.Cfg.ShortName, f) +} + +func BridgeAddLink(_ context.Context, link netlink.Link, bridgeName string, f func(ns.NetNS) error) error { + // retrieve the namespace handle + ns, err := ns.GetCurrentNS() + if err != nil { + return err + } + + // get the bridge as netlink.Link + br, err := netlink.LinkByName(bridgeName) + if err != nil { + return err + } + + // assign the bridge to the link as master + err = netlink.LinkSetMaster(link, br) + if err != nil { + return err + } + + // execute the given function + return ns.Do(f) +} + +func (b *bridge) GetLinkEndpointType() links.LinkEndpointType { + return links.LinkEndpointTypeBridge +} diff --git a/nodes/c8000/c8000.go b/nodes/c8000/c8000.go index 2cf91a68b..55adcbdf1 100644 --- a/nodes/c8000/c8000.go +++ b/nodes/c8000/c8000.go @@ -117,9 +117,10 @@ func (n *c8000) create8000Files(_ context.Context) error { // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *c8000) CheckInterfaceName() error { ifRe := regexp.MustCompile(`^(Hu|FH)0_0_0_\d+$`) - for _, e := range n.Config().Endpoints { - if !ifRe.MatchString(e.EndpointName) { - return fmt.Errorf("cisco 8000 interface name %q doesn't match the required pattern. Cisco 8000 interfaces should be named as Hu0_0_0_X (100G interfaces) or FH0_0_0_X (400G interfaces) where X is the interface number", e.EndpointName) + + for _, e := range n.Endpoints { + if !ifRe.MatchString(e.GetIfaceName()) { + return fmt.Errorf("cisco 8000 interface name %q doesn't match the required pattern. Cisco 8000 interfaces should be named as Hu0_0_0_X (100G interfaces) or FH0_0_0_X (400G interfaces) where X is the interface number", e.GetIfaceName()) } } diff --git a/nodes/ceos/ceos.go b/nodes/ceos/ceos.go index da77594b1..7d946b7a2 100644 --- a/nodes/ceos/ceos.go +++ b/nodes/ceos/ceos.go @@ -91,8 +91,13 @@ func (n *ceos) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error { envSb.WriteString("systemd.setenv=" + k + "=" + v + " ") } envSb.WriteString("'") + n.Cfg.Cmd = envSb.String() - n.Cfg.MacAddress = utils.GenMac("00:1c:73") + hwa, err := utils.GenMac("00:1c:73") + if err != nil { + return err + } + n.Cfg.MacAddress = hwa.String() // mount config dir cfgPath := filepath.Join(n.Cfg.LabDir, "flash") @@ -284,9 +289,9 @@ func (n *ceos) CheckInterfaceName() error { // allow eth and et interfaces // https://regex101.com/r/umQW5Z/2 ifRe := regexp.MustCompile(`eth[1-9][\w\.]*$|et[1-9][\w\.]*$`) - for _, e := range n.Config().Endpoints { - if !ifRe.MatchString(e.EndpointName) { - return fmt.Errorf("arista cEOS node %q has an interface named %q which doesn't match the required pattern. Interfaces should be named as ethX or etX, where X consists of alpanumerical characters", n.Cfg.ShortName, e.EndpointName) + for _, e := range n.Endpoints { + if !ifRe.MatchString(e.GetIfaceName()) { + return fmt.Errorf("arista cEOS node %q has an interface named %q which doesn't match the required pattern. Interfaces should be named as ethX or etX, where X consists of alpanumerical characters", n.Cfg.ShortName, e.GetIfaceName()) } } diff --git a/nodes/checkpoint_cloudguard/checkpoint_cloudguard.go b/nodes/checkpoint_cloudguard/checkpoint_cloudguard.go index 2a140bef4..89fe699af 100644 --- a/nodes/checkpoint_cloudguard/checkpoint_cloudguard.go +++ b/nodes/checkpoint_cloudguard/checkpoint_cloudguard.go @@ -66,5 +66,5 @@ func (n *CheckpointCloudguard) PreDeploy(_ context.Context, params *nodes.PreDep // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *CheckpointCloudguard) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/cvx/cvx.go b/nodes/cvx/cvx.go index 170f8c747..0dbe6f2a4 100644 --- a/nodes/cvx/cvx.go +++ b/nodes/cvx/cvx.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/srl-labs/containerlab/nodes" + "github.com/srl-labs/containerlab/nodes/state" "github.com/srl-labs/containerlab/runtime/ignite" "github.com/srl-labs/containerlab/types" meta "github.com/weaveworks/ignite/pkg/apis/meta/v1alpha1" @@ -79,13 +80,16 @@ func (c *cvx) Deploy(ctx context.Context, _ *nodes.DeployParams) error { if err != nil { return err } - intf, err := c.Runtime.StartContainer(ctx, cID, c.Cfg) + intf, err := c.Runtime.StartContainer(ctx, cID, c) if err != nil { return err } if vmChans, ok := intf.(*operations.VMChannels); ok { c.vmChans = vmChans } + + c.SetState(state.Deployed) + return nil } @@ -116,9 +120,9 @@ func (c *cvx) CheckInterfaceName() error { // allow swpX interface names // https://regex101.com/r/SV0k1J/1 ifRe := regexp.MustCompile(`swp[\d\.]+$`) - for _, e := range c.Config().Endpoints { - if !ifRe.MatchString(e.EndpointName) { - return fmt.Errorf("%q interface name %q doesn't match the required pattern. It should be named as swpX, where X is >=0", c.Cfg.ShortName, e.EndpointName) + for _, e := range c.Endpoints { + if !ifRe.MatchString(e.GetIfaceName()) { + return fmt.Errorf("%q interface name %q doesn't match the required pattern. It should be named as swpX, where X is >=0", c.Cfg.ShortName, e.GetIfaceName()) } } diff --git a/nodes/default_node.go b/nodes/default_node.go index 4a2c0facf..0109e5452 100644 --- a/nodes/default_node.go +++ b/nodes/default_node.go @@ -11,16 +11,21 @@ import ( "os" "path" "path/filepath" + "sync" "text/template" + "github.com/containernetworking/plugins/pkg/ns" "github.com/hairyhenderson/gomplate/v3" "github.com/hairyhenderson/gomplate/v3/data" log "github.com/sirupsen/logrus" "github.com/srl-labs/containerlab/cert" "github.com/srl-labs/containerlab/clab/exec" + "github.com/srl-labs/containerlab/links" + "github.com/srl-labs/containerlab/nodes/state" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" "github.com/srl-labs/containerlab/utils" + "github.com/vishvananda/netlink" ) // DefaultNode implements the Node interface and is embedded to the structs of all other nodes. @@ -35,6 +40,13 @@ type DefaultNode struct { // OverwriteNode stores the interface used to overwrite methods defined // for DefaultNode, so that particular nodes can provide custom implementations. OverwriteNode NodeOverwrites + // List of links that reference the node. + Links []links.Link + // List of link endpoints that are connected to the node. + Endpoints []links.Endpoint + // State of the node + state state.NodeState + statemutex sync.RWMutex } // NewDefaultNode initializes the DefaultNode structure and receives a NodeOverwrites interface @@ -120,8 +132,14 @@ func (d *DefaultNode) Deploy(ctx context.Context, _ *DeployParams) error { if err != nil { return err } - _, err = d.Runtime.StartContainer(ctx, cID, d.Cfg) - return err + _, err = d.Runtime.StartContainer(ctx, cID, d) + if err != nil { + return err + } + + d.SetState(state.Deployed) + + return nil } func (d *DefaultNode) Delete(ctx context.Context) error { @@ -394,3 +412,88 @@ func (d *DefaultNode) LoadOrGenerateCertificate(certInfra *cert.Cert, topoName s return nodeCert, nil } + +func (d *DefaultNode) AddLinkToContainer(_ context.Context, link netlink.Link, f func(ns.NetNS) error) error { + // retrieve the namespace handle + netns, err := ns.GetNS(d.Cfg.NSPath) + if err != nil { + return err + } + // move veth endpoint to namespace + if err = netlink.LinkSetNsFd(link, int(netns.Fd())); err != nil { + return err + } + // execute the given function + return netns.Do(f) +} + +// ExecFunction executes the given function in the nodes network namespace +func (d *DefaultNode) ExecFunction(f func(ns.NetNS) error) error { + nspath := d.Cfg.NSPath + + if d.Cfg.IsRootNamespaceBased { + nshandle, err := ns.GetCurrentNS() + if err != nil { + return err + } + nspath = nshandle.Path() + } + + if nspath == "" { + return fmt.Errorf("nspath is not set for node %q", d.GetShortName()) + } + + // retrieve the namespace handle + netns, err := ns.GetNS(nspath) + if err != nil { + return err + } + // execute the given function + return netns.Do(f) +} + +func (d *DefaultNode) AddLink(l links.Link) { + d.Links = append(d.Links, l) +} + +func (d *DefaultNode) AddEndpoint(e links.Endpoint) { + d.Endpoints = append(d.Endpoints, e) +} + +func (d *DefaultNode) GetEndpoints() []links.Endpoint { + return d.Endpoints +} + +// GetLinkEndpointType returns a veth link endpoint type for default nodes. +// The LinkEndpointTypeVeth indicates a veth endpoint which doesn't require special handling. +func (d *DefaultNode) GetLinkEndpointType() links.LinkEndpointType { + return links.LinkEndpointTypeVeth +} + +func (d *DefaultNode) GetShortName() string { + return d.Cfg.ShortName +} + +// DeployLinks deploys links associated with the node. +func (d *DefaultNode) DeployLinks(ctx context.Context) error { + for _, l := range d.Links { + err := l.Deploy(ctx) + if err != nil { + return err + } + } + + return nil +} + +func (d *DefaultNode) GetState() state.NodeState { + d.statemutex.RLock() + defer d.statemutex.RUnlock() + return d.state +} + +func (d *DefaultNode) SetState(s state.NodeState) { + d.statemutex.Lock() + defer d.statemutex.Unlock() + d.state = s +} diff --git a/nodes/ext_container/ext_container.go b/nodes/ext_container/ext_container.go index 5ff180fa7..69c3ca0f7 100644 --- a/nodes/ext_container/ext_container.go +++ b/nodes/ext_container/ext_container.go @@ -11,6 +11,7 @@ import ( "github.com/srl-labs/containerlab/labels" "github.com/srl-labs/containerlab/nodes" + "github.com/srl-labs/containerlab/nodes/state" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" ) @@ -55,6 +56,8 @@ func (e *extcont) Deploy(ctx context.Context, _ *nodes.DeployParams) error { // set nspath in node config e.Cfg.NSPath = nspath + e.SetState(state.Deployed) + return nil } diff --git a/nodes/host/host.go b/nodes/host/host.go index 190ad3cde..eba099115 100644 --- a/nodes/host/host.go +++ b/nodes/host/host.go @@ -16,6 +16,7 @@ import ( cExec "github.com/srl-labs/containerlab/clab/exec" "github.com/srl-labs/containerlab/labels" "github.com/srl-labs/containerlab/nodes" + "github.com/srl-labs/containerlab/nodes/state" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" ) @@ -44,11 +45,15 @@ func (n *host) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error { n.Cfg.IsRootNamespaceBased = true return nil } -func (*host) Deploy(_ context.Context, _ *nodes.DeployParams) error { return nil } -func (*host) GetImages(_ context.Context) map[string]string { return map[string]string{} } -func (*host) PullImage(_ context.Context) error { return nil } -func (*host) Delete(_ context.Context) error { return nil } -func (*host) WithMgmtNet(*types.MgmtNet) {} +func (n *host) Deploy(_ context.Context, _ *nodes.DeployParams) error { + n.SetState(state.Deployed) + return nil +} + +func (*host) GetImages(_ context.Context) map[string]string { return map[string]string{} } +func (*host) PullImage(_ context.Context) error { return nil } +func (*host) Delete(_ context.Context) error { return nil } +func (*host) WithMgmtNet(*types.MgmtNet) {} // UpdateConfigWithRuntimeInfo is a noop for hosts. func (*host) UpdateConfigWithRuntimeInfo(_ context.Context) error { return nil } diff --git a/nodes/ipinfusion_ocnos/ipinfusion_ocnos.go b/nodes/ipinfusion_ocnos/ipinfusion_ocnos.go index 9d7e7b52d..b02a30fa8 100644 --- a/nodes/ipinfusion_ocnos/ipinfusion_ocnos.go +++ b/nodes/ipinfusion_ocnos/ipinfusion_ocnos.go @@ -86,5 +86,5 @@ func (n *IPInfusionOcNOS) SaveConfig(_ context.Context) error { // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *IPInfusionOcNOS) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/linux/linux.go b/nodes/linux/linux.go index 92417466b..ae0509558 100644 --- a/nodes/linux/linux.go +++ b/nodes/linux/linux.go @@ -11,6 +11,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/srl-labs/containerlab/nodes" + "github.com/srl-labs/containerlab/nodes/state" "github.com/srl-labs/containerlab/runtime/ignite" "github.com/srl-labs/containerlab/types" "github.com/weaveworks/ignite/pkg/operations" @@ -53,12 +54,14 @@ func (n *linux) Deploy(ctx context.Context, _ *nodes.DeployParams) error { if err != nil { return err } - intf, err := n.Runtime.StartContainer(ctx, cID, n.Cfg) + intf, err := n.Runtime.StartContainer(ctx, cID, n) if vmChans, ok := intf.(*operations.VMChannels); ok { n.vmChans = vmChans } + n.SetState(state.Deployed) + return err } @@ -93,8 +96,8 @@ func (n *linux) GetImages(_ context.Context) map[string]string { // if eth0 is only used with network-mode=none. func (n *linux) CheckInterfaceName() error { nm := strings.ToLower(n.Cfg.NetworkMode) - for _, e := range n.Config().Endpoints { - if e.EndpointName == "eth0" && nm != "none" { + for _, e := range n.Endpoints { + if e.GetIfaceName() == "eth0" && nm != "none" { return fmt.Errorf("eth0 interface name is not allowed for %s node when network mode is not set to none", n.Cfg.ShortName) } } diff --git a/nodes/node.go b/nodes/node.go index e2d208d14..e45540ecf 100644 --- a/nodes/node.go +++ b/nodes/node.go @@ -10,10 +10,14 @@ import ( "fmt" "regexp" + "github.com/containernetworking/plugins/pkg/ns" "github.com/srl-labs/containerlab/cert" "github.com/srl-labs/containerlab/clab/exec" + "github.com/srl-labs/containerlab/links" + "github.com/srl-labs/containerlab/nodes/state" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" + "github.com/vishvananda/netlink" "golang.org/x/crypto/ssh" ) @@ -89,6 +93,19 @@ type Node interface { UpdateConfigWithRuntimeInfo(context.Context) error // RunExec execute a single command for a given node. RunExec(ctx context.Context, execCmd *exec.ExecCmd) (*exec.ExecResult, error) + // Adds the given link to the Node (container). After adding the Link to the node, + // the given function f is called within the Nodes namespace to setup the link. + AddLinkToContainer(ctx context.Context, link netlink.Link, f func(ns.NetNS) error) error + AddLink(l links.Link) + AddEndpoint(e links.Endpoint) + GetEndpoints() []links.Endpoint + GetLinkEndpointType() links.LinkEndpointType + GetShortName() string + // DeployLinks deploys the links for the node. + DeployLinks(ctx context.Context) error + // ExecFunction executes the given function within the nodes network namespace + ExecFunction(func(ns.NetNS) error) error + GetState() state.NodeState } type NodeOption func(Node) @@ -111,11 +128,11 @@ func WithRuntime(r runtime.ContainerRuntime) NodeOption { // GenericVMInterfaceCheck checks interface names for generic VM-based nodes. // These nodes could only have interfaces named ethX, where X is >0. -func GenericVMInterfaceCheck(nodeName string, eps []types.Endpoint) error { +func GenericVMInterfaceCheck(nodeName string, eps []links.Endpoint) error { ifRe := regexp.MustCompile(`eth[1-9][0-9]*$`) for _, e := range eps { - if !ifRe.MatchString(e.EndpointName) { - return fmt.Errorf("%q interface name %q doesn't match the required pattern. It should be named as ethX, where X is >0", nodeName, e.EndpointName) + if !ifRe.MatchString(e.GetIfaceName()) { + return fmt.Errorf("%q interface name %q doesn't match the required pattern. It should be named as ethX, where X is >0", nodeName, e.GetIfaceName()) } } diff --git a/nodes/ovs/ovs.go b/nodes/ovs/ovs.go index 74225ad24..62f0847d9 100644 --- a/nodes/ovs/ovs.go +++ b/nodes/ovs/ovs.go @@ -8,12 +8,17 @@ import ( "context" "fmt" + "github.com/containernetworking/plugins/pkg/ns" goOvs "github.com/digitalocean/go-openvswitch/ovs" log "github.com/sirupsen/logrus" cExec "github.com/srl-labs/containerlab/clab/exec" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/nodes" + "github.com/srl-labs/containerlab/nodes/bridge" + "github.com/srl-labs/containerlab/nodes/state" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" + "github.com/vishvananda/netlink" ) var kindnames = []string{"ovs-bridge"} @@ -57,11 +62,15 @@ func (n *ovs) CheckDeploymentConditions(_ context.Context) error { return nil } -func (*ovs) Deploy(_ context.Context, _ *nodes.DeployParams) error { return nil } -func (*ovs) PullImage(_ context.Context) error { return nil } -func (*ovs) GetImages(_ context.Context) map[string]string { return map[string]string{} } -func (*ovs) Delete(_ context.Context) error { return nil } -func (*ovs) DeleteNetnsSymlink() (err error) { return nil } +func (n *ovs) Deploy(_ context.Context, _ *nodes.DeployParams) error { + n.SetState(state.Deployed) + return nil +} + +func (*ovs) PullImage(_ context.Context) error { return nil } +func (*ovs) GetImages(_ context.Context) map[string]string { return map[string]string{} } +func (*ovs) Delete(_ context.Context) error { return nil } +func (*ovs) DeleteNetnsSymlink() (err error) { return nil } // UpdateConfigWithRuntimeInfo is a noop for bridges. func (*ovs) UpdateConfigWithRuntimeInfo(_ context.Context) error { return nil } @@ -75,3 +84,11 @@ func (n *ovs) RunExec(_ context.Context, _ *cExec.ExecCmd) (*cExec.ExecResult, e return nil, cExec.ErrRunExecNotSupported } + +func (n *ovs) AddLinkToContainer(ctx context.Context, link netlink.Link, f func(ns.NetNS) error) error { + return bridge.BridgeAddLink(ctx, link, n.Cfg.ShortName, f) +} + +func (m *ovs) GetLinkEndpointType() links.LinkEndpointType { + return links.LinkEndpointTypeBridge +} diff --git a/nodes/srl/srl.go b/nodes/srl/srl.go index f5f674d99..16f572ee5 100644 --- a/nodes/srl/srl.go +++ b/nodes/srl/srl.go @@ -26,6 +26,7 @@ import ( "github.com/srl-labs/containerlab/cert" "github.com/srl-labs/containerlab/clab/exec" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/nodes" "github.com/srl-labs/containerlab/types" "github.com/srl-labs/containerlab/utils" @@ -57,15 +58,18 @@ set / system snmp network-instance mgmt set / system snmp network-instance mgmt admin-state enable set / system lldp admin-state enable set / system aaa authentication idle-timeout 7200 -{{/* enabling interfaces referenced as endpoints for a node (both e1-2 and e1-3-1 notations) */}} -{{- range $ep := .Endpoints }} -{{- if eq $ep.EndpointName "mgmt0" }}{{- continue }}{{- end}} -{{- $parts := ($ep.EndpointName | strings.ReplaceAll "e" "" | strings.Split "-") -}} -set / interface ethernet-{{index $parts 0}}/{{index $parts 1}} admin-state enable - {{- if eq (len $parts) 3 }} -set / interface ethernet-{{index $parts 0}}/{{index $parts 1}} breakout-mode num-channels 4 channel-speed 25G -set / interface ethernet-{{index $parts 0}}/{{index $parts 1}}/{{index $parts 2}} admin-state enable +{{- /* enabling interfaces referenced as endpoints for a node (both e1-2 and e1-3-1 notations) */}} +{{- range $epName, $ep := .IFaces }} +set / interface ethernet-{{ $ep.Slot }}/{{ $ep.Port }} admin-state enable + {{- if ne $ep.Mtu 0 }} +set / interface ethernet-{{ $ep.Slot }}/{{ $ep.Port }} mtu {{ $ep.Mtu }} {{- end }} + + {{- if ne $ep.BreakoutNo "" }} +set / interface ethernet-{{ $ep.Slot }}/{{ $ep.Port }} breakout-mode num-channels 4 channel-speed 25G +set / interface ethernet-{{ $ep.Slot }}/{{ $ep.Port }}/{{ $ep.BreakoutNo }} admin-state enable + {{- end }} + {{ end -}} {{- if .SSHPubKeys }} set / system aaa authentication linuxadmin-user ssh-key [ {{ .SSHPubKeys }} ] @@ -522,6 +526,24 @@ func generateSRLTopologyFile(cfg *types.NodeConfig) error { return f.Close() } +// srlTemplateData top level data struct +type srlTemplateData struct { + TLSKey string + TLSCert string + TLSAnchor string + Banner string + IFaces map[string]tplIFace + SSHPubKeys string +} + +// tplIFace template interface struct +type tplIFace struct { + Slot string + Port string + BreakoutNo string + Mtu int +} + // addDefaultConfig adds srl default configuration such as tls certs, gnmi/json-rpc, login-banner. func (n *srl) addDefaultConfig(ctx context.Context) error { b, err := n.banner() @@ -530,14 +552,13 @@ func (n *srl) addDefaultConfig(ctx context.Context) error { } // struct that holds data used in templating of the default config snippet - tplData := struct { - *types.NodeConfig - Banner string - SSHPubKeys string - }{ - n.Cfg, - b, - "", + + tplData := srlTemplateData{ + TLSKey: n.Cfg.TLSKey, + TLSCert: n.Cfg.TLSCert, + TLSAnchor: n.Cfg.TLSAnchor, + Banner: b, + IFaces: map[string]tplIFace{}, } n.filterSSHPubKeys() @@ -548,10 +569,38 @@ func (n *srl) addDefaultConfig(ctx context.Context) error { tplData.SSHPubKeys = catenateKeys(n.sshPubKeys) } - // remove newlines from tls key/cert so that they nicely apply via the cli provisioning - // during the template execution - tplData.TLSKey = strings.TrimSpace(tplData.TLSKey) - tplData.TLSCert = strings.TrimSpace(tplData.TLSCert) + // prepare the endpoints + for _, e := range n.Endpoints { + ifName := e.GetIfaceName() + // split the interface identifier into their parts + ifNameParts := strings.SplitN(strings.TrimLeft(ifName, "e"), "-", 3) + + // create a template interface struct + iface := tplIFace{ + Slot: ifNameParts[0], + Port: ifNameParts[1], + } + // if it is a breakout port add the breakout identifier + if len(ifNameParts) == 3 { + iface.BreakoutNo = ifNameParts[2] + } + + // for MACVlan interfaces we need to figure out the parent interface MTU + // and specifically define it in the config + // + // via the endpoint we acquire the link, and check if the link is of type LinkMacVlan + // if so cast it and get the parent Interface MTU and finally set that for the interface + if link, ok := e.GetLink().(*links.LinkMacVlan); ok { + mtu, err := link.GetParentInterfaceMtu() + if err != nil { + return err + } + iface.Mtu = mtu + } + + // add the template interface definition to the template data + tplData.IFaces[ifName] = iface + } buf := new(bytes.Buffer) err = srlCfgTpl.Execute(buf, tplData) @@ -688,12 +737,12 @@ func (s *srl) CheckInterfaceName() error { ifRe := regexp.MustCompile(`e\d+-\d+(-\d+)?|mgmt0`) nm := strings.ToLower(s.Cfg.NetworkMode) - for _, e := range s.Config().Endpoints { - if !ifRe.MatchString(e.EndpointName) { - return fmt.Errorf("nokia sr linux interface name %q doesn't match the required pattern. SR Linux interfaces should be named as e1-1 or e1-1-1", e.EndpointName) + for _, e := range s.Endpoints { + if !ifRe.MatchString(e.GetIfaceName()) { + return fmt.Errorf("nokia sr linux interface name %q doesn't match the required pattern. SR Linux interfaces should be named as e1-1 or e1-1-1", e.GetIfaceName()) } - if e.EndpointName == "mgmt0" && nm != "none" { + if e.GetIfaceName() == "mgmt0" && nm != "none" { return fmt.Errorf("mgmt0 interface name is not allowed for %s node when network mode is not set to none", s.Cfg.ShortName) } } diff --git a/nodes/state/state.go b/nodes/state/state.go new file mode 100644 index 000000000..e38da2d8b --- /dev/null +++ b/nodes/state/state.go @@ -0,0 +1,9 @@ +package state + +type NodeState uint + +const ( + Unknown NodeState = iota + // Deployed means the underlying container has been started and deploy function succeeded + Deployed +) diff --git a/nodes/vr_aoscx/vr-aoscx.go b/nodes/vr_aoscx/vr-aoscx.go index bb7501756..0e1197237 100644 --- a/nodes/vr_aoscx/vr-aoscx.go +++ b/nodes/vr_aoscx/vr-aoscx.go @@ -76,5 +76,5 @@ func (n *vrAosCX) PreDeploy(_ context.Context, params *nodes.PreDeployParams) er // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrAosCX) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_csr/vr-csr.go b/nodes/vr_csr/vr-csr.go index 583a52dca..ae24f86c1 100644 --- a/nodes/vr_csr/vr-csr.go +++ b/nodes/vr_csr/vr-csr.go @@ -98,5 +98,5 @@ func (n *vrCsr) SaveConfig(_ context.Context) error { // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrCsr) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_ftosv/vr-ftosv.go b/nodes/vr_ftosv/vr-ftosv.go index 9f6e6d34a..5f7047716 100644 --- a/nodes/vr_ftosv/vr-ftosv.go +++ b/nodes/vr_ftosv/vr-ftosv.go @@ -80,5 +80,5 @@ func (n *vrFtosv) PreDeploy(_ context.Context, params *nodes.PreDeployParams) er // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrFtosv) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_n9kv/vr-n9kv.go b/nodes/vr_n9kv/vr-n9kv.go index ed3196c65..541f75179 100644 --- a/nodes/vr_n9kv/vr-n9kv.go +++ b/nodes/vr_n9kv/vr-n9kv.go @@ -81,5 +81,5 @@ func (n *vrN9kv) PreDeploy(_ context.Context, params *nodes.PreDeployParams) err // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrN9kv) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_nxos/vr-nxos.go b/nodes/vr_nxos/vr-nxos.go index 0b4d10528..6ee27010d 100644 --- a/nodes/vr_nxos/vr-nxos.go +++ b/nodes/vr_nxos/vr-nxos.go @@ -77,5 +77,5 @@ func (n *vrNXOS) PreDeploy(_ context.Context, params *nodes.PreDeployParams) err // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrNXOS) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_pan/vr-pan.go b/nodes/vr_pan/vr-pan.go index b8c4e18ce..7d6b87807 100644 --- a/nodes/vr_pan/vr-pan.go +++ b/nodes/vr_pan/vr-pan.go @@ -82,5 +82,5 @@ func (n *vrPan) PreDeploy(_ context.Context, params *nodes.PreDeployParams) erro // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrPan) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_ros/vr-ros.go b/nodes/vr_ros/vr-ros.go index 1e59e3b22..a2ceb4fa6 100644 --- a/nodes/vr_ros/vr-ros.go +++ b/nodes/vr_ros/vr-ros.go @@ -78,5 +78,5 @@ func (n *vrRos) PreDeploy(_ context.Context, params *nodes.PreDeployParams) erro // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrRos) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_sros/vr-sros.go b/nodes/vr_sros/vr-sros.go index d915f3b8f..e254098db 100644 --- a/nodes/vr_sros/vr-sros.go +++ b/nodes/vr_sros/vr-sros.go @@ -138,9 +138,9 @@ func (s *vrSROS) CheckInterfaceName() error { // vsim doesn't seem to support >20 interfaces, yet we allow to set max if number 32 just in case. // https://regex101.com/r/bx6kzM/1 ifRe := regexp.MustCompile(`eth([1-9]|[12][0-9]|3[0-2])$`) - for _, e := range s.Config().Endpoints { - if !ifRe.MatchString(e.EndpointName) { - return fmt.Errorf("nokia SR OS interface name %q doesn't match the required pattern. SR OS interfaces should be named as ethX, where X is from 1 to 32", e.EndpointName) + for _, e := range s.Endpoints { + if !ifRe.MatchString(e.GetIfaceName()) { + return fmt.Errorf("nokia SR OS interface name %q doesn't match the required pattern. SR OS interfaces should be named as ethX, where X is from 1 to 32", e.GetIfaceName()) } } diff --git a/nodes/vr_veos/vr-veos.go b/nodes/vr_veos/vr-veos.go index 347cc6200..51d7c2731 100644 --- a/nodes/vr_veos/vr-veos.go +++ b/nodes/vr_veos/vr-veos.go @@ -100,5 +100,5 @@ func (n *vrVEOS) SaveConfig(_ context.Context) error { // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrVEOS) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_vmx/vr-vmx.go b/nodes/vr_vmx/vr-vmx.go index ad3301aa0..121fc4637 100644 --- a/nodes/vr_vmx/vr-vmx.go +++ b/nodes/vr_vmx/vr-vmx.go @@ -93,5 +93,5 @@ func (n *vrVMX) SaveConfig(_ context.Context) error { // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrVMX) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_vqfx/vr-vqfx.go b/nodes/vr_vqfx/vr-vqfx.go index 5430cd10f..7fba859b4 100644 --- a/nodes/vr_vqfx/vr-vqfx.go +++ b/nodes/vr_vqfx/vr-vqfx.go @@ -98,5 +98,5 @@ func (n *vrVQFX) SaveConfig(_ context.Context) error { // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrVQFX) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_vsrx/vr-vsrx.go b/nodes/vr_vsrx/vr-vsrx.go index 12eff38e2..7a666471a 100644 --- a/nodes/vr_vsrx/vr-vsrx.go +++ b/nodes/vr_vsrx/vr-vsrx.go @@ -98,5 +98,5 @@ func (n *vrVSRX) SaveConfig(_ context.Context) error { // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrVSRX) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_xrv/vr-xrv.go b/nodes/vr_xrv/vr-xrv.go index 09d5a81c9..e2fe642f2 100644 --- a/nodes/vr_xrv/vr-xrv.go +++ b/nodes/vr_xrv/vr-xrv.go @@ -98,5 +98,5 @@ func (n *vrXRV) SaveConfig(_ context.Context) error { // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrXRV) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/vr_xrv9k/vr-xrv9k.go b/nodes/vr_xrv9k/vr-xrv9k.go index b59604d89..0eceed924 100644 --- a/nodes/vr_xrv9k/vr-xrv9k.go +++ b/nodes/vr_xrv9k/vr-xrv9k.go @@ -101,5 +101,5 @@ func (n *vrXRV9K) SaveConfig(_ context.Context) error { // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *vrXRV9K) CheckInterfaceName() error { - return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Cfg.Endpoints) + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) } diff --git a/nodes/xrd/xrd.go b/nodes/xrd/xrd.go index 2ce263131..70798988c 100644 --- a/nodes/xrd/xrd.go +++ b/nodes/xrd/xrd.go @@ -130,10 +130,10 @@ func (n *xrd) genInterfacesEnv() { // here we take the number of links users set in the topology to get the right # of links var interfaceEnvVar string - for _, ep := range n.Config().Endpoints { + for _, ep := range n.Endpoints { // ifName is a linux interface name with dashes swapped for slashes to be used in the config - ifName := strings.ReplaceAll(ep.EndpointName, "-", "/") - interfaceEnvVar += fmt.Sprintf("linux:%s,xr_name=%s;", ep.EndpointName, ifName) + ifName := strings.ReplaceAll(ep.GetIfaceName(), "-", "/") + interfaceEnvVar += fmt.Sprintf("linux:%s,xr_name=%s;", ep.GetIfaceName(), ifName) } interfaceEnv := map[string]string{"XR_INTERFACES": interfaceEnvVar} @@ -144,9 +144,9 @@ func (n *xrd) genInterfacesEnv() { // CheckInterfaceName checks if a name of the interface referenced in the topology file correct. func (n *xrd) CheckInterfaceName() error { ifRe := regexp.MustCompile(`^Gi0-0-0-\d+$`) - for _, e := range n.Config().Endpoints { - if !ifRe.MatchString(e.EndpointName) { - return fmt.Errorf("cisco XRd interface name %q doesn't match the required pattern. XRd interfaces should be named as Gi0-0-0-X where X is the interface number", e.EndpointName) + for _, e := range n.Endpoints { + if !ifRe.MatchString(e.GetIfaceName()) { + return fmt.Errorf("cisco XRd interface name %q doesn't match the required pattern. XRd interfaces should be named as Gi0-0-0-X where X is the interface number", e.GetIfaceName()) } } diff --git a/runtime/containerd/containerd.go b/runtime/containerd/containerd.go index 26961f736..5d80e75ec 100644 --- a/runtime/containerd/containerd.go +++ b/runtime/containerd/containerd.go @@ -26,6 +26,7 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/srl-labs/containerlab/clab/exec" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" "github.com/srl-labs/containerlab/utils" @@ -66,6 +67,7 @@ func (c *ContainerdRuntime) Init(opts ...runtime.RuntimeOption) error { for _, o := range opts { o(c) } + c.config.VerifyLinkParams = links.NewVerifyLinkParams() return nil } @@ -172,28 +174,30 @@ func (c *ContainerdRuntime) CreateContainer(_ context.Context, _ *types.NodeConf return "", nil } -func (c *ContainerdRuntime) StartContainer(ctx context.Context, _ string, node *types.NodeConfig) (interface{}, error) { +func (c *ContainerdRuntime) StartContainer(ctx context.Context, _ string, node runtime.Node) (interface{}, error) { ctx = namespaces.WithNamespace(ctx, containerdNamespace) + nodecfg := node.Config() + var img containerd.Image - img, err := c.client.GetImage(ctx, node.Image) + img, err := c.client.GetImage(ctx, nodecfg.Image) if err != nil { // try fetching the image with canonical name // as it might be that we pulled this image with canonical name - img, err = c.client.GetImage(ctx, utils.GetCanonicalImageName(node.Image)) + img, err = c.client.GetImage(ctx, utils.GetCanonicalImageName(nodecfg.Image)) if err != nil { return nil, err } } - cmd, err := shlex.Split(node.Cmd) + cmd, err := shlex.Split(nodecfg.Cmd) if err != nil { return nil, err } - mounts := make([]specs.Mount, len(node.Binds)) + mounts := make([]specs.Mount, len(nodecfg.Binds)) - for idx, mount := range node.Binds { + for idx, mount := range nodecfg.Binds { s := strings.Split(mount, ":") m := specs.Mount{ @@ -209,9 +213,9 @@ func (c *ContainerdRuntime) StartContainer(ctx context.Context, _ string, node * opts := []oci.SpecOpts{ oci.WithImageConfig(img), - oci.WithEnv(utils.ConvertEnvs(node.Env)), - oci.WithHostname(node.ShortName), - WithSysctls(node.Sysctls), + oci.WithEnv(utils.ConvertEnvs(nodecfg.Env)), + oci.WithHostname(nodecfg.ShortName), + WithSysctls(nodecfg.Sysctls), oci.WithoutRunMount, oci.WithPrivileged, oci.WithHostLocaltime, @@ -223,21 +227,21 @@ func (c *ContainerdRuntime) StartContainer(ctx context.Context, _ string, node * if len(cmd) > 0 { opts = append(opts, oci.WithProcessArgs(cmd...)) } - if node.User != "" { - opts = append(opts, oci.WithUser(node.User)) + if nodecfg.User != "" { + opts = append(opts, oci.WithUser(nodecfg.User)) } - if node.Memory != "" { - mem, err := humanize.ParseBytes(node.Memory) + if nodecfg.Memory != "" { + mem, err := humanize.ParseBytes(nodecfg.Memory) if err != nil { return nil, err } opts = append(opts, oci.WithMemoryLimit(mem)) } - if node.CPU != 0 { - opts = append(opts, oci.WithCPUCFS(int64(node.CPU*100000), 100000)) + if nodecfg.CPU != 0 { + opts = append(opts, oci.WithCPUCFS(int64(nodecfg.CPU*100000), 100000)) } - if node.CPUSet != "" { - opts = append(opts, oci.WithCPUs(node.CPUSet)) + if nodecfg.CPUSet != "" { + opts = append(opts, oci.WithCPUs(nodecfg.CPUSet)) } if len(mounts) > 0 { opts = append(opts, oci.WithMounts(mounts)) @@ -247,7 +251,7 @@ func (c *ContainerdRuntime) StartContainer(ctx context.Context, _ string, node * var cncl *libcni.NetworkConfigList var cnirc *libcni.RuntimeConf - switch node.NetworkMode { + switch nodecfg.NetworkMode { case "host": opts = append(opts, oci.WithHostNamespace(specs.NetworkNamespace), @@ -256,19 +260,19 @@ func (c *ContainerdRuntime) StartContainer(ctx context.Context, _ string, node * case "none": // Done! default: - cnic, cncl, cnirc, err = cniInit(node.LongName, "eth0", c.mgmt) + cnic, cncl, cnirc, err = cniInit(nodecfg.LongName, "eth0", c.mgmt) if err != nil { return nil, err } // set mac if defined in node - if node.MacAddress != "" { - cnirc.CapabilityArgs["mac"] = node.MacAddress + if nodecfg.MacAddress != "" { + cnirc.CapabilityArgs["mac"] = nodecfg.MacAddress } portmappings := []portMapping{} - for contdatasl, hostdata := range node.PortBindings { + for contdatasl, hostdata := range nodecfg.PortBindings { // fmt.Printf("%+v", hostdata) // fmt.Printf("%+v", contdatasl) for _, x := range hostdata { @@ -290,28 +294,28 @@ func (c *ContainerdRuntime) StartContainer(ctx context.Context, _ string, node * var cOpts []containerd.NewContainerOpts cOpts = append(cOpts, containerd.WithImage(img), - containerd.WithNewSnapshot(node.LongName+"-snapshot", img), - containerd.WithAdditionalContainerLabels(node.Labels), + containerd.WithNewSnapshot(nodecfg.LongName+"-snapshot", img), + containerd.WithAdditionalContainerLabels(nodecfg.Labels), containerd.WithNewSpec(opts...), ) newContainer, err := c.client.NewContainer( ctx, - node.LongName, + nodecfg.LongName, cOpts..., ) if err != nil { return nil, err } - log.Debugf("Container '%s' created", node.LongName) - log.Debugf("Start container: %s", node.LongName) + log.Debugf("Container '%s' created", nodecfg.LongName) + log.Debugf("Start container: %s", nodecfg.LongName) - container, err := c.client.LoadContainer(ctx, node.LongName) + container, err := c.client.LoadContainer(ctx, nodecfg.LongName) if err != nil { return nil, err } - task, err := container.NewTask(ctx, cio.LogFile("/tmp/clab/"+node.LongName+".log")) + task, err := container.NewTask(ctx, cio.LogFile("/tmp/clab/"+nodecfg.LongName+".log")) if err != nil { return nil, err } @@ -319,14 +323,14 @@ func (c *ContainerdRuntime) StartContainer(ctx context.Context, _ string, node * if err != nil { return nil, err } - log.Debugf("Container started: %s", node.LongName) + log.Debugf("Container started: %s", nodecfg.LongName) - node.NSPath, err = c.GetNSPath(ctx, node.LongName) + nodecfg.NSPath, err = c.GetNSPath(ctx, nodecfg.LongName) if err != nil { return nil, err } - err = utils.LinkContainerNS(node.NSPath, node.LongName) + err = utils.LinkContainerNS(nodecfg.NSPath, nodecfg.LongName) if err != nil { return nil, err } @@ -335,7 +339,7 @@ func (c *ContainerdRuntime) StartContainer(ctx context.Context, _ string, node * // we have prepared a lot of stuff further up, which // is now to be applied if cnic != nil { - cnirc.NetNS = node.NSPath + cnirc.NetNS = nodecfg.NSPath res, err := cnic.AddNetworkList(ctx, cncl, cnirc) if err != nil { return nil, err @@ -343,10 +347,10 @@ func (c *ContainerdRuntime) StartContainer(ctx context.Context, _ string, node * result, _ := current.NewResultFromResult(res) // set DNS configuration defined in topology - if node.DNS != nil { - result.DNS.Nameservers = node.DNS.Servers - result.DNS.Options = node.DNS.Options - result.DNS.Search = node.DNS.Search + if nodecfg.DNS != nil { + result.DNS.Nameservers = nodecfg.DNS.Servers + result.DNS.Options = nodecfg.DNS.Options + result.DNS.Search = nodecfg.DNS.Search } ipv4, ipv6, ipv4Gw := "", "", "" diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 35a9912e0..c1976d940 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -29,6 +29,7 @@ import ( "github.com/google/shlex" log "github.com/sirupsen/logrus" "github.com/srl-labs/containerlab/clab/exec" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" "github.com/srl-labs/containerlab/utils" @@ -68,6 +69,7 @@ func (d *DockerRuntime) Init(opts ...runtime.RuntimeOption) error { for _, o := range opts { o(d) } + d.config.VerifyLinkParams = links.NewVerifyLinkParams() return nil } @@ -546,10 +548,13 @@ func (d *DockerRuntime) PullImage(ctx context.Context, imageName string, pullpol } // StartContainer starts a docker container. -func (d *DockerRuntime) StartContainer(ctx context.Context, cID string, node *types.NodeConfig) (interface{}, error) { +func (d *DockerRuntime) StartContainer(ctx context.Context, cID string, node runtime.Node) (interface{}, error) { nctx, cancel := context.WithTimeout(ctx, d.config.Timeout) defer cancel() - log.Debugf("Start container: %q", node.LongName) + + nodecfg := node.Config() + + log.Debugf("Start container: %q", nodecfg.LongName) err := d.Client.ContainerStart(nctx, cID, dockerTypes.ContainerStartOptions{ @@ -560,8 +565,8 @@ func (d *DockerRuntime) StartContainer(ctx context.Context, cID string, node *ty if err != nil { return nil, err } - log.Debugf("Container started: %q", node.LongName) - err = d.postStartActions(ctx, cID, node) + log.Debugf("Container started: %q", nodecfg.LongName) + err = d.postStartActions(ctx, cID, nodecfg) return nil, err } diff --git a/runtime/ignite/ignite.go b/runtime/ignite/ignite.go index 3a982eaa3..99d30a6ec 100644 --- a/runtime/ignite/ignite.go +++ b/runtime/ignite/ignite.go @@ -153,27 +153,29 @@ func (*IgniteRuntime) PullImage(_ context.Context, imageName string, pullPolicy return nil } -func (c *IgniteRuntime) StartContainer(ctx context.Context, _ string, node *types.NodeConfig) (interface{}, error) { +func (c *IgniteRuntime) StartContainer(ctx context.Context, _ string, node runtime.Node) (interface{}, error) { vm := c.baseVM.DeepCopy() + nodecfg := node.Config() + // updating the node RAM if it's set - if node.Memory != "" { - ram, err := meta.NewSizeFromString(node.Memory) + if nodecfg.Memory != "" { + ram, err := meta.NewSizeFromString(nodecfg.Memory) if err != nil { - return nil, fmt.Errorf("failed to parse %q as memory value: %s", node.Memory, err) + return nil, fmt.Errorf("failed to parse %q as memory value: %s", nodecfg.Memory, err) } vm.Spec.Memory = ram } - ociRef, err := meta.NewOCIImageRef(node.Sandbox) + ociRef, err := meta.NewOCIImageRef(nodecfg.Sandbox) if err != nil { - return nil, fmt.Errorf("failed to parse OCI image ref %q: %s", node.Sandbox, err) + return nil, fmt.Errorf("failed to parse OCI image ref %q: %s", nodecfg.Sandbox, err) } vm.Spec.Sandbox.OCI = ociRef - ociRef, err = meta.NewOCIImageRef(node.Kernel) + ociRef, err = meta.NewOCIImageRef(nodecfg.Kernel) if err != nil { - return nil, fmt.Errorf("failed to parse OCI image ref %q: %s", node.Kernel, err) + return nil, fmt.Errorf("failed to parse OCI image ref %q: %s", nodecfg.Kernel, err) } c.baseVM.Spec.Kernel.OCI = ociRef k, err := operations.FindOrImportKernel(providers.Client, ociRef) @@ -182,9 +184,9 @@ func (c *IgniteRuntime) StartContainer(ctx context.Context, _ string, node *type } vm.SetKernel(k) - ociRef, err = meta.NewOCIImageRef(node.Image) + ociRef, err = meta.NewOCIImageRef(nodecfg.Image) if err != nil { - return nil, fmt.Errorf("failed to parse OCI image ref %q: %s", node.Image, err) + return nil, fmt.Errorf("failed to parse OCI image ref %q: %s", nodecfg.Image, err) } img, err := operations.FindOrImportImage(providers.Client, ociRef) if err != nil { @@ -192,12 +194,12 @@ func (c *IgniteRuntime) StartContainer(ctx context.Context, _ string, node *type } vm.SetImage(img) - vm.Name = node.LongName - vm.Labels = node.Labels + vm.Name = nodecfg.LongName + vm.Labels = nodecfg.Labels metadata.SetNameAndUID(vm, providers.Client) copyFiles := []api.FileMapping{} - for _, bind := range node.Binds { + for _, bind := range nodecfg.Binds { parts := strings.Split(bind, ":") if len(parts) < 2 { continue @@ -211,9 +213,9 @@ func (c *IgniteRuntime) StartContainer(ctx context.Context, _ string, node *type // Create udev rules to rename interfaces var extraIntfs []string var udevRules []string - for _, ep := range node.Endpoints { - extraIntfs = append(extraIntfs, ep.EndpointName) - udevRules = append(udevRules, fmt.Sprintf(udevRuleTemplate, ep.MAC, ep.EndpointName)) + for _, ep := range node.GetEndpoints() { + extraIntfs = append(extraIntfs, ep.GetIfaceName()) + udevRules = append(udevRules, fmt.Sprintf(udevRuleTemplate, ep.GetMac(), ep.GetIfaceName())) } udevFile, err := os.CreateTemp("/tmp", fmt.Sprintf("%s-udev", vm.Name)) @@ -262,12 +264,12 @@ func (c *IgniteRuntime) StartContainer(ctx context.Context, _ string, node *type return nil, err } - node.NSPath, err = c.GetNSPath(ctx, vm.PrefixedID()) + nodecfg.NSPath, err = c.GetNSPath(ctx, vm.PrefixedID()) if err != nil { return nil, err } - return vmChans, utils.LinkContainerNS(node.NSPath, node.LongName) + return vmChans, utils.LinkContainerNS(nodecfg.NSPath, nodecfg.LongName) } func (*IgniteRuntime) CreateContainer(_ context.Context, node *types.NodeConfig) (string, error) { diff --git a/runtime/podman/podman.go b/runtime/podman/podman.go index 960621a48..4f8848dc3 100644 --- a/runtime/podman/podman.go +++ b/runtime/podman/podman.go @@ -15,6 +15,7 @@ import ( dockerTypes "github.com/docker/docker/api/types" log "github.com/sirupsen/logrus" "github.com/srl-labs/containerlab/clab/exec" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" "github.com/srl-labs/containerlab/utils" @@ -45,6 +46,9 @@ func (r *PodmanRuntime) Init(opts ...runtime.RuntimeOption) error { for _, f := range opts { f(r) } + r.config.VerifyLinkParams = links.NewVerifyLinkParams() + r.config.VerifyLinkParams.RunBridgeExistsCheck = false + return nil } @@ -104,6 +108,14 @@ func (r *PodmanRuntime) CreateNet(ctx context.Context) error { } log.Debugf("Create network response was: %+v", resp) } + // set bridge name = network name if explicit name was not provided + if r.mgmt.Bridge == "" && r.mgmt.Network != "" { + details, err := network.Inspect(ctx, r.mgmt.Network, &network.InspectOptions{}) + if err != nil { + return err + } + r.mgmt.Bridge = details.NetworkInterface + } return err } @@ -180,11 +192,13 @@ func (r *PodmanRuntime) CreateContainer(ctx context.Context, cfg *types.NodeConf } // StartContainer starts a previously created container by ID or its name and executes post-start actions method. -func (r *PodmanRuntime) StartContainer(ctx context.Context, cID string, cfg *types.NodeConfig) (interface{}, error) { +func (r *PodmanRuntime) StartContainer(ctx context.Context, cID string, node runtime.Node) (interface{}, error) { ctx, err := r.connect(ctx) if err != nil { return nil, err } + cfg := node.Config() + err = containers.Start(ctx, cID, &containers.StartOptions{}) if err != nil { return nil, fmt.Errorf("error while starting a container %q: %w", cfg.LongName, err) diff --git a/runtime/podman/util.go b/runtime/podman/util.go index e6a2e0486..71c7d7eee 100644 --- a/runtime/podman/util.go +++ b/runtime/podman/util.go @@ -390,10 +390,6 @@ func (r *PodmanRuntime) disableTXOffload(_ context.Context) error { // netOpts is an accessory function that returns a network.CreateOptions struct // filled with all parameters for CreateNet function. func (r *PodmanRuntime) netOpts(_ context.Context) (netTypes.Network, error) { - // set bridge name = network name if explicit name was not provided - if r.mgmt.Bridge == "" && r.mgmt.Network != "" { - r.mgmt.Bridge = r.mgmt.Network - } var ( name = r.mgmt.Network intName = r.mgmt.Bridge diff --git a/runtime/runtime.go b/runtime/runtime.go index 7ff16d2ca..69c21b0eb 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -11,6 +11,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/srl-labs/containerlab/clab/exec" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/types" ) @@ -35,7 +36,7 @@ type ContainerRuntime interface { CreateContainer(context.Context, *types.NodeConfig) (string, error) // Start pre-created container by its name. Returns an extra interface that can be used to receive signals // about the container life-cycle after it was created, e.g. for post-deploy tasks - StartContainer(context.Context, string, *types.NodeConfig) (interface{}, error) + StartContainer(context.Context, string, Node) (interface{}, error) // Stop running container by its name StopContainer(context.Context, string) error // Pause a container identified by its name @@ -78,6 +79,7 @@ type RuntimeConfig struct { GracefulShutdown bool Debug bool KeepMgmtNet bool + VerifyLinkParams *links.VerifyLinkParams } var ContainerRuntimes = map[string]Initializer{} @@ -146,3 +148,10 @@ TIMEOUT_LOOP: } return resultErr } + +// Node is an interface that represents a node in the lab +// and is implemented by containerlab nodes. +type Node interface { + Config() *types.NodeConfig + GetEndpoints() []links.Endpoint +} diff --git a/templates/export/auto.tmpl b/templates/export/auto.tmpl index a8aa3d4e7..497f26a7f 100644 --- a/templates/export/auto.tmpl +++ b/templates/export/auto.tmpl @@ -37,17 +37,20 @@ }{{$i = add $i 1}}{{end}} }, "links": [{{range $i, $l := .Clab.Links}}{{if $i}},{{end}} + {{- $eps := $l.GetEndpoints }} + {{- $ep := index $eps 0 }} { "a": { - "node": "{{ $l.A.Node.ShortName }}", - "interface": "{{ $l.A.EndpointName }}", - "mac": "{{ $l.A.MAC }}", + "node": "{{ $ep.GetNode.GetShortName }}", + "interface": "{{ $ep.GetIfaceName }}", + "mac": "{{ $ep.GetMac }}", "peer": "z" }, + {{- $ep := index $eps 1 }} "z": { - "node": "{{ $l.B.Node.ShortName }}", - "interface": "{{ $l.B.EndpointName }}", - "mac": "{{ $l.B.MAC }}", + "node": "{{ $ep.GetNode.GetShortName }}", + "interface": "{{ $ep.GetIfaceName }}", + "mac": "{{ $ep.GetMac }}", "peer": "a" } }{{end}} diff --git a/templates/export/full.tmpl b/templates/export/full.tmpl index 19087689e..3699ec315 100644 --- a/templates/export/full.tmpl +++ b/templates/export/full.tmpl @@ -6,17 +6,20 @@ "{{$n}}":{{ $cj := $c | data.ToJSON | data.JSON }} {{ $dst := coll.Merge $cj $k }}{{ ToJSONPretty $dst " " " " }}{{$i = add $i 1}}{{end}} }, "links": [{{range $i, $l := .Clab.Links}}{{if $i}},{{end}} + {{- $eps := $l.GetEndpoints }} + {{- $ep := index $eps 0 }} { "a": { - "node": "{{ $l.A.Node.ShortName }}", - "interface": "{{ $l.A.EndpointName }}", - "mac": "{{ $l.A.MAC }}", + "node": "{{ $ep.GetNode.GetShortName }}", + "interface": "{{ $ep.GetIfaceName }}", + "mac": "{{ $ep.GetMac }}", "peer": "z" }, + {{- $ep := index $eps 1 }} "z": { - "node": "{{ $l.B.Node.ShortName }}", - "interface": "{{ $l.B.EndpointName }}", - "mac": "{{ $l.B.MAC }}", + "node": "{{ $ep.GetNode.GetShortName }}", + "interface": "{{ $ep.GetIfaceName }}", + "mac": "{{ $ep.GetMac }}", "peer": "a" } }{{end}} diff --git a/tests/01-smoke/01-basic-flow.robot b/tests/01-smoke/01-basic-flow.robot index ffe32db2c..d318a40d2 100644 --- a/tests/01-smoke/01-basic-flow.robot +++ b/tests/01-smoke/01-basic-flow.robot @@ -112,6 +112,12 @@ Verify links in node l1 Log ${output} Should Be Equal As Integers ${rc} 0 Should Contain ${output} state UP + ${rc} ${output} = Run And Return Rc And Output + ... ${runtime-cli-exec-cmd} clab-${lab-name}-l1 ip link show eth3 + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} state UP + Should Contain ${output} 02:00:00:00:00:00 Verify links in node l2 ${rc} ${output} = Run And Return Rc And Output @@ -124,6 +130,36 @@ Verify links in node l2 Log ${output} Should Be Equal As Integers ${rc} 0 Should Contain ${output} state UP + ${rc} ${output} = Run And Return Rc And Output + ... ${runtime-cli-exec-cmd} clab-${lab-name}-l2 ip link show eth3 + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} state UP + Should Contain ${output} 02:00:00:00:00:01 + ${rc} ${output} = Run And Return Rc And Output + ... ${runtime-cli-exec-cmd} clab-${lab-name}-l2 ip link show eth4 + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} state UP + Should Contain ${output} 02:00:00:00:00:04 + ${rc} ${output} = Run And Return Rc And Output + ... ${runtime-cli-exec-cmd} clab-${lab-name}-l2 ip link show eth5 + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} state UP + Should Contain ${output} 02:00:00:00:00:05 + +Verify links on host + ${rc} ${output} = Run And Return Rc And Output + ... ip link show l2eth4 + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} state UP + ${rc} ${output} = Run And Return Rc And Output + ... ip link show l2eth5mgmt + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} state UP Ensure "inspect all" outputs IP addresses ${rc} ${output} = Run And Return Rc And Output @@ -286,7 +322,7 @@ Verify iptables allow rule are gone *** Keywords *** Setup - Run rm -rf ${bind-orig-path} + Run sudo rm -rf ${bind-orig-path} OperatingSystem.Create File ${bind-orig-path} Hello, containerlab Match IPv6 Address diff --git a/tests/01-smoke/01-linux-nodes.clab.yml b/tests/01-smoke/01-linux-nodes.clab.yml index da87187c1..2a6eadcfd 100644 --- a/tests/01-smoke/01-linux-nodes.clab.yml +++ b/tests/01-smoke/01-linux-nodes.clab.yml @@ -44,4 +44,28 @@ topology: links: - endpoints: ["l1:eth1", "l2:some1"] + mtu: 1500 + vars: + foo: bar + some: ["thing", "else"] - endpoints: ["l1:eth2", "l2:eth2"] + - type: veth + endpoints: + - node: l1 + interface: eth3 + mac: 02:00:00:00:00:00 + - node: l2 + interface: eth3 + mac: 02:00:00:00:00:01 + - type: host + host-interface: l2eth4 + endpoint: + node: l2 + interface: eth4 + mac: 02:00:00:00:00:04 + - type: mgmt-net + host-interface: l2eth5mgmt + endpoint: + node: l2 + interface: eth5 + mac: 02:00:00:00:00:05 diff --git a/tests/01-smoke/08-tools-cmds.robot b/tests/01-smoke/08-tools-cmds.robot index a1d95897d..5610d3663 100644 --- a/tests/01-smoke/08-tools-cmds.robot +++ b/tests/01-smoke/08-tools-cmds.robot @@ -27,20 +27,20 @@ Deploy ${lab-name} lab Create new veth pair between nodes ${rc} ${output} = Run And Return Rc And Output - ... sudo ${CLAB_BIN} --runtime ${runtime} tools veth create -a clab-${lab-name}-l1:eth3 -b clab-${lab-name}-l2:eth3 + ... sudo ${CLAB_BIN} --runtime ${runtime} tools veth create -a clab-${lab-name}-l1:eth63 -b clab-${lab-name}-l2:eth63 Log ${output} Should Be Equal As Integers ${rc} 0 Check the new interface has been created ${rc} ${output} = Run And Return Rc And Output - ... sudo ip netns exec clab-${lab-name}-l1 ip l show dev eth3 + ... sudo ip netns exec clab-${lab-name}-l1 ip l show dev eth63 Log ${output} Should Be Equal As Integers ${rc} 0 - Should Contain ${output} eth3 + Should Contain ${output} eth63 Add link impairments ${rc} ${output} = Run And Return Rc And Output - ... sudo ${CLAB_BIN} --runtime ${runtime} tools netem set -n clab-${lab-name}-l1 -i eth3 --delay 100ms --jitter 2ms --loss 10 --rate 1000 + ... sudo ${CLAB_BIN} --runtime ${runtime} tools netem set -n clab-${lab-name}-l1 -i eth63 --delay 100ms --jitter 2ms --loss 10 --rate 1000 Log ${output} Should Be Equal As Integers ${rc} 0 Should Contain ${output} 100ms diff --git a/types/endpoint.go b/types/endpoint.go deleted file mode 100644 index d18c3447e..000000000 --- a/types/endpoint.go +++ /dev/null @@ -1,7 +0,0 @@ -package types - -type EndpointRaw struct { - Node string `yaml:"node"` - Iface string `yaml:"interface"` - Mac string `yaml:"mac,omitempty"` -} diff --git a/types/link.go b/types/link.go deleted file mode 100644 index b94d53290..000000000 --- a/types/link.go +++ /dev/null @@ -1,152 +0,0 @@ -package types - -import ( - "fmt" - "strings" - - "gopkg.in/yaml.v2" -) - -// LinkCommonParams represents the common parameters for all link types. -type LinkCommonParams struct { - Mtu int `yaml:"mtu,omitempty"` - Labels map[string]string `yaml:"labels,omitempty"` - Vars map[string]interface{} `yaml:"vars,omitempty"` -} - -// LinkDefinition represents a link definition in the topology file. -type LinkDefinition struct { - Type string `yaml:"type,omitempty"` - LinkConfig `yaml:",inline"` -} - -// LinkDefinitionType represents the type of a link definition. -type LinkDefinitionType string - -const ( - LinkTypeVEth LinkDefinitionType = "veth" - LinkTypeMgmtNet LinkDefinitionType = "mgmt-net" - LinkTypeMacVLan LinkDefinitionType = "macvlan" - LinkTypeHost LinkDefinitionType = "host" - - // LinkTypeBrief is a link definition where link types - // are encoded in the endpoint definition as string and allow users - // to quickly type out link endpoints in a yaml file. - LinkTypeBrief LinkDefinitionType = "brief" -) - -// parseLinkType parses a string representation of a link type into a LinkDefinitionType. -func parseLinkType(s string) (LinkDefinitionType, error) { - switch strings.TrimSpace(strings.ToLower(s)) { - case string(LinkTypeMacVLan): - return LinkTypeMacVLan, nil - case string(LinkTypeVEth): - return LinkTypeVEth, nil - case string(LinkTypeMgmtNet): - return LinkTypeMgmtNet, nil - case string(LinkTypeHost): - return LinkTypeHost, nil - case string(LinkTypeBrief): - return LinkTypeBrief, nil - default: - return "", fmt.Errorf("unable to parse %q as LinkType", s) - } -} - -var _ yaml.Unmarshaler = (*LinkDefinition)(nil) - -// UnmarshalYAML deserializes links passed via topology file into LinkDefinition struct. -// It supports both the brief and specific link type notations. -func (r *LinkDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error { - // alias struct to avoid recursion and pass strict yaml unmarshalling - // we don't care about the embedded LinkConfig, as we only need to unmarshal - // the type field. - var a struct { - Type string `yaml:"type"` - // Throwaway endpoints field, as we don't care about it. - Endpoints any `yaml:"endpoints"` - } - err := unmarshal(&a) - if err != nil { - return err - } - - var lt LinkDefinitionType - - // if no type is specified, we assume that brief notation of a link definition is used. - if a.Type == "" { - lt = LinkTypeBrief - r.Type = string(LinkTypeBrief) - } else { - r.Type = a.Type - - lt, err = parseLinkType(a.Type) - if err != nil { - return err - } - } - - switch lt { - case LinkTypeVEth: - var l struct { - // the Type field is injected artificially - // to allow strict yaml parsing to work. - Type string `yaml:"type"` - LinkVEthRaw `yaml:",inline"` - } - err := unmarshal(&l) - if err != nil { - return err - } - r.LinkConfig = *l.LinkVEthRaw.ToLinkConfig() - case LinkTypeMgmtNet: - var l struct { - Type string `yaml:"type"` - LinkMgmtNetRaw `yaml:",inline"` - } - err := unmarshal(&l) - if err != nil { - return err - } - r.LinkConfig = *l.LinkMgmtNetRaw.ToLinkConfig() - case LinkTypeHost: - var l struct { - Type string `yaml:"type"` - LinkHostRaw `yaml:",inline"` - } - err := unmarshal(&l) - if err != nil { - return err - } - r.LinkConfig = *l.LinkHostRaw.ToLinkConfig() - case LinkTypeMacVLan: - var l struct { - Type string `yaml:"type"` - LinkMACVLANRaw `yaml:",inline"` - } - err := unmarshal(&l) - if err != nil { - return err - } - r.LinkConfig = *l.LinkMACVLANRaw.ToLinkConfig() - case LinkTypeBrief: - // brief link's endpoint format - var l struct { - Type string `yaml:"type"` - LinkConfig `yaml:",inline"` - } - - err := unmarshal(&l) - if err != nil { - return err - } - - r.Type = string(LinkTypeBrief) - - r.LinkConfig = l.LinkConfig - default: - return fmt.Errorf("unknown link type %q", lt) - } - - return nil -} diff --git a/types/link_host.go b/types/link_host.go deleted file mode 100644 index ca1007659..000000000 --- a/types/link_host.go +++ /dev/null @@ -1,25 +0,0 @@ -package types - -import "fmt" - -// LinkHostRaw is the raw (string) representation of a host link as defined in the topology file. -type LinkHostRaw struct { - LinkCommonParams `yaml:",inline"` - HostInterface string `yaml:"host-interface"` - Endpoint *EndpointRaw `yaml:"endpoint"` -} - -// ToLinkConfig converts the raw link into a LinkConfig. -func (r *LinkHostRaw) ToLinkConfig() *LinkConfig { - lc := &LinkConfig{ - Vars: r.Vars, - Labels: r.Labels, - MTU: r.Mtu, - Endpoints: make([]string, 2), - } - - lc.Endpoints[0] = fmt.Sprintf("%s:%s", r.Endpoint.Node, r.Endpoint.Iface) - lc.Endpoints[1] = fmt.Sprintf("%s:%s", "host", r.HostInterface) - - return lc -} diff --git a/types/link_macvlan.go b/types/link_macvlan.go deleted file mode 100644 index 4ebec3fad..000000000 --- a/types/link_macvlan.go +++ /dev/null @@ -1,25 +0,0 @@ -package types - -import "fmt" - -// LinkMACVLANRaw is the raw (string) representation of a macvlan link as defined in the topology file. -type LinkMACVLANRaw struct { - LinkCommonParams `yaml:",inline"` - HostInterface string `yaml:"host-interface"` - Endpoint *EndpointRaw `yaml:"endpoint"` -} - -// ToLinkConfig converts the raw link into a LinkConfig. -func (r *LinkMACVLANRaw) ToLinkConfig() *LinkConfig { - lc := &LinkConfig{ - Vars: r.Vars, - Labels: r.Labels, - MTU: r.Mtu, - Endpoints: make([]string, 2), - } - - lc.Endpoints[0] = fmt.Sprintf("%s:%s", r.Endpoint.Node, r.Endpoint.Iface) - lc.Endpoints[1] = fmt.Sprintf("%s:%s", "macvlan", r.HostInterface) - - return lc -} diff --git a/types/link_mgmt-net.go b/types/link_mgmt-net.go deleted file mode 100644 index 1e463eaed..000000000 --- a/types/link_mgmt-net.go +++ /dev/null @@ -1,23 +0,0 @@ -package types - -import "fmt" - -type LinkMgmtNetRaw struct { - LinkCommonParams `yaml:",inline"` - HostInterface string `yaml:"host-interface"` - Endpoint *EndpointRaw `yaml:"endpoint"` -} - -func (r *LinkMgmtNetRaw) ToLinkConfig() *LinkConfig { - lc := &LinkConfig{ - Vars: r.Vars, - Labels: r.Labels, - MTU: r.Mtu, - Endpoints: make([]string, 2), - } - - lc.Endpoints[0] = fmt.Sprintf("%s:%s", r.Endpoint.Node, r.Endpoint.Iface) - lc.Endpoints[1] = fmt.Sprintf("%s:%s", "mgmt-net", r.HostInterface) - - return lc -} diff --git a/types/link_veth.go b/types/link_veth.go deleted file mode 100644 index 1e0939c7e..000000000 --- a/types/link_veth.go +++ /dev/null @@ -1,34 +0,0 @@ -package types - -import "fmt" - -// LinkVEthRaw is the raw (string) representation of a veth link as defined in the topology file. -type LinkVEthRaw struct { - LinkCommonParams `yaml:",inline"` - Endpoints []*EndpointRaw `yaml:"endpoints"` -} - -func (r *LinkVEthRaw) MarshalYAML() (interface{}, error) { - x := struct { - Type string `yaml:"type"` - LinkVEthRaw `yaml:",inline"` - }{ - Type: string(LinkTypeVEth), - LinkVEthRaw: *r, - } - return x, nil -} - -// ToLinkConfig converts the raw link into a LinkConfig. -func (r *LinkVEthRaw) ToLinkConfig() *LinkConfig { - lc := &LinkConfig{ - Vars: r.Vars, - Labels: r.Labels, - MTU: r.Mtu, - Endpoints: []string{}, - } - for _, e := range r.Endpoints { - lc.Endpoints = append(lc.Endpoints, fmt.Sprintf("%s:%s", e.Node, e.Iface)) - } - return lc -} diff --git a/types/topology.go b/types/topology.go index d5d467121..e040f8d26 100644 --- a/types/topology.go +++ b/types/topology.go @@ -2,6 +2,7 @@ package types import ( "github.com/docker/go-connections/nat" + "github.com/srl-labs/containerlab/links" "github.com/srl-labs/containerlab/utils" ) @@ -10,7 +11,7 @@ type Topology struct { Defaults *NodeDefinition `yaml:"defaults,omitempty"` Kinds map[string]*NodeDefinition `yaml:"kinds,omitempty"` Nodes map[string]*NodeDefinition `yaml:"nodes,omitempty"` - Links []*LinkDefinition `yaml:"links,omitempty"` + Links []*links.LinkDefinition `yaml:"links,omitempty"` } func NewTopology() *Topology { @@ -18,17 +19,10 @@ func NewTopology() *Topology { Defaults: new(NodeDefinition), Kinds: make(map[string]*NodeDefinition), Nodes: make(map[string]*NodeDefinition), - Links: make([]*LinkDefinition, 0), + Links: make([]*links.LinkDefinition, 0), } } -type LinkConfig struct { - Endpoints []string - Labels map[string]string `yaml:"labels,omitempty"` - Vars map[string]interface{} `yaml:"vars,omitempty"` - MTU int `yaml:"mtu,omitempty"` -} - func (t *Topology) GetDefaults() *NodeDefinition { if t.Defaults != nil { return t.Defaults diff --git a/types/types.go b/types/types.go index e05cd9b59..633730330 100644 --- a/types/types.go +++ b/types/types.go @@ -165,8 +165,6 @@ type NodeConfig struct { // Extra /etc/hosts entries for all nodes. ExtraHosts []string `json:"extra-hosts,omitempty"` Labels map[string]string `json:"labels,omitempty"` // container labels - // Slice of pointers to local endpoints, DO NOT marshal into JSON as it creates a cyclical error - Endpoints []Endpoint `json:"-"` // List of Subject Alternative Names (SAN) to be added to the node's TLS certificate SANs []string `json:"SANs,omitempty"` // Ignite sandbox and kernel imageNames diff --git a/utils/netlink.go b/utils/netlink.go index 0c228496f..9dd58e682 100644 --- a/utils/netlink.go +++ b/utils/netlink.go @@ -7,6 +7,7 @@ package utils import ( "crypto/rand" "fmt" + "net" "os" log "github.com/sirupsen/logrus" @@ -70,10 +71,12 @@ func DeleteLinkByName(name string) error { } // GenMac generates a random MAC address for a given OUI. -func GenMac(oui string) string { +func GenMac(oui string) (net.HardwareAddr, error) { buf := make([]byte, 3) _, _ = rand.Read(buf) - return fmt.Sprintf("%s:%02x:%02x:%02x", oui, buf[0], buf[1], buf[2]) + + hwa, err := net.ParseMAC(fmt.Sprintf("%s:%02x:%02x:%02x", oui, buf[0], buf[1], buf[2])) + return hwa, err } // DeleteNetnsSymlink deletes a network namespace and removes the symlink created by LinkContainerNS func.