diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 19d7118ec..b53f30b16 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -408,7 +408,14 @@ jobs: path: ./tests/coverage/* retention-days: 7 - # create a job that downloads coverage artifact and uses codecov to upload it + vxlan-tests: + uses: ./.github/workflows/vxlan-tests.yml + needs: + - unit-test + - staticcheck + - build-containerlab + + # a job that downloads coverage artifact and uses codecov to upload it coverage: runs-on: ubuntu-22.04 needs: @@ -418,6 +425,7 @@ jobs: - ceos-basic-tests - srlinux-basic-tests - ixiac-one-basic-tests + - vxlan-tests steps: - name: Checkout uses: actions/checkout@v4 @@ -473,11 +481,13 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') needs: - docs-test + - unit-test - smoke-tests - ceos-basic-tests - srlinux-basic-tests - ixiac-one-basic-tests - ext-container-tests + - vxlan-tests steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/vxlan-tests.yml b/.github/workflows/vxlan-tests.yml new file mode 100644 index 000000000..ab2bf93b5 --- /dev/null +++ b/.github/workflows/vxlan-tests.yml @@ -0,0 +1,64 @@ +name: vxlan-test + +"on": + workflow_call: + +jobs: + vxlan-tests: + runs-on: ubuntu-22.04 + strategy: + matrix: + runtime: + - "docker" + test-suite: + - "01*.robot" + - "02*.robot" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/download-artifact@v3 + with: + name: containerlab + + - name: Move containerlab to usr/bin + run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab + + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + cache: pip + cache-dependency-path: "tests/requirements.txt" + + - name: Install robotframework + run: | + pip install -r tests/requirements.txt + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Run tests + run: | + bash ./tests/rf-run.sh ${{ matrix.runtime }} ./tests/08-vxlan/${{ matrix.test-suite }} + + # upload test reports as a zip file + - uses: actions/upload-artifact@v3 + if: always() + with: + name: 08-${{ matrix.runtime }}-vxlan-log + path: ./tests/out/*.html + + # upload coverage report from unit tests, as they are then + # merged with e2e tests coverage + - uses: actions/upload-artifact@v3 + if: always() + with: + name: coverage + path: ./tests/coverage/* + retention-days: 7 diff --git a/clab/clab.go b/clab/clab.go index 51abea662..1b7ad953a 100644 --- a/clab/clab.go +++ b/clab/clab.go @@ -602,6 +602,14 @@ func (c *CLab) DeleteNodes(ctx context.Context, workers uint, serialNodes map[st close(concurrentChan) close(serialChan) + // also call delete on the special nodes + for _, n := range c.GetSpecialLinkNodes() { + err := n.Delete(ctx) + if err != nil { + log.Warn(err) + } + } + wg.Wait() } @@ -663,38 +671,9 @@ func (c *CLab) GetNodeRuntime(contName string) (runtime.ContainerRuntime, error) return nil, fmt.Errorf("could not find a container matching name %q", contName) } -// 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(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 { +// GetLinkNodes returns all CLab.Nodes nodes as links.Nodes enriched with the special nodes - host and mgmt-net. +// The CLab nodes are copied to a new map and thus clab.Node interface is converted to link.Node. +func (c *CLab) GetLinkNodes() map[string]links.Node { // 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. @@ -704,17 +683,32 @@ func (c *CLab) ResolveLinks() error { resolveNodes[k] = v } + // add the virtual host and mgmt-bridge nodes to the resolve nodes + specialNodes := c.GetSpecialLinkNodes() + for _, n := range specialNodes { + resolveNodes[n.GetShortName()] = n + } + + return resolveNodes +} + +// GetSpecialLinkNodes returns a map of special nodes that are used to resolve links. +// Special nodes are host and mgmt-bridge nodes that are not typically present in the topology file +// but are required to resolve links. +func (c *CLab) GetSpecialLinkNodes() map[string]links.Node { // 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 - } + return specialNodes +} + +// ResolveLinks resolves raw links to the actual link types and stores them in the CLab.Links map. +func (c *CLab) ResolveLinks() error { resolveParams := &links.ResolveParams{ - Nodes: resolveNodes, + Nodes: c.GetLinkNodes(), MgmtBridgeName: c.Config.Mgmt.Bridge, NodesFilter: c.nodeFilter, } diff --git a/clab/netlink.go b/clab/netlink.go index 01b336d18..a456a06a7 100644 --- a/clab/netlink.go +++ b/clab/netlink.go @@ -7,7 +7,6 @@ package clab import ( "fmt" "net" - "strings" "github.com/containernetworking/plugins/pkg/ns" "github.com/google/uuid" @@ -154,7 +153,7 @@ func (c *CLab) CreateVirtualWiring(l *types.Link) (err error) { func (c *CLab) RemoveHostOrBridgeVeth(l *types.Link) (err error) { switch { case l.A.Node.Kind == "host" || l.A.Node.Kind == "bridge": - link, err := netlink.LinkByName(l.A.EndpointName) + link, err := utils.LinkByNameOrAlias(l.A.EndpointName) if err != nil { log.Debugf("Link %q is already gone: %v", l.A.EndpointName, err) break @@ -168,7 +167,7 @@ func (c *CLab) RemoveHostOrBridgeVeth(l *types.Link) (err error) { log.Debugf("Link %q is already gone: %v", l.A.EndpointName, err) } case l.B.Node.Kind == "host" || l.B.Node.Kind == "bridge": - link, err := netlink.LinkByName(l.B.EndpointName) + link, err := utils.LinkByNameOrAlias(l.B.EndpointName) if err != nil { log.Debugf("Link %q is already gone: %v", l.B.EndpointName, err) break @@ -187,7 +186,7 @@ func (c *CLab) RemoveHostOrBridgeVeth(l *types.Link) (err error) { // createMACVLANInterface creates a macvlan interface in the root netns. func createMACVLANInterface(ifName, parentIfName string, mtu int, MAC net.HardwareAddr) (netlink.Link, error) { - parentInterface, err := netlink.LinkByName(parentIfName) + parentInterface, err := utils.LinkByNameOrAlias(parentIfName) if err != nil { return nil, err } @@ -227,7 +226,7 @@ func createVethIface(ifName, peerName string, mtu int, aMAC, bMAC net.HardwareAd return nil, nil, err } - if linkB, err = netlink.LinkByName(peerName); err != nil { + if linkB, err = utils.LinkByNameOrAlias(peerName); err != nil { err = fmt.Errorf("failed to lookup %q: %v", peerName, err) } @@ -316,26 +315,3 @@ func genIfName() string { s, _ := uuid.New().MarshalText() // .MarshalText() always return a nil error return string(s[:8]) } - -// GetLinksByNamePrefix returns a list of links whose name matches a prefix. -func GetLinksByNamePrefix(prefix string) ([]netlink.Link, error) { - // filtered list of interfaces - if prefix == "" { - return nil, fmt.Errorf("prefix is not specified") - } - var fls []netlink.Link - - ls, err := netlink.LinkList() - if err != nil { - return nil, err - } - for _, l := range ls { - if strings.HasPrefix(l.Attrs().Name, prefix) { - fls = append(fls, l) - } - } - if len(fls) == 0 { - return nil, fmt.Errorf("no links found by specified prefix %s", prefix) - } - return fls, nil -} diff --git a/clab/ovs.go b/clab/ovs.go index de8c8bcb4..7eac905f6 100644 --- a/clab/ovs.go +++ b/clab/ovs.go @@ -9,6 +9,7 @@ import ( "github.com/containernetworking/plugins/pkg/ns" "github.com/digitalocean/go-openvswitch/ovs" + "github.com/srl-labs/containerlab/utils" "github.com/vishvananda/netlink" ) @@ -20,7 +21,7 @@ func (veth *vEthEndpoint) toOvsBridge() error { return err } err = vethNS.Do(func(_ ns.NetNS) error { - _, err := netlink.LinkByName(veth.OvsBridge) + _, err := utils.LinkByNameOrAlias(veth.OvsBridge) if err != nil { return fmt.Errorf("could not find ovs bridge %q: %v", veth.OvsBridge, err) } diff --git a/clab/tc.go b/clab/tc.go index d8f71ba83..b67ee5cf6 100644 --- a/clab/tc.go +++ b/clab/tc.go @@ -10,6 +10,7 @@ import ( "syscall" log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/utils" "github.com/vishvananda/netlink" ) @@ -34,12 +35,12 @@ func SetIngressMirror(src, dst string) (err error) { var linkSrc, linkDest netlink.Link log.Infof("configuring ingress mirroring with tc in the direction of %s -> %s", src, dst) - if linkSrc, err = netlink.LinkByName(src); err != nil { + if linkSrc, err = utils.LinkByNameOrAlias(src); err != nil { return fmt.Errorf("failed to lookup %q: %v", src, err) } - if linkDest, err = netlink.LinkByName(dst); err != nil { + if linkDest, err = utils.LinkByNameOrAlias(dst); err != nil { return fmt.Errorf("failed to lookup %q: %v", dst, err) } diff --git a/clab/vxlan.go b/clab/vxlan.go index 9fef665b6..fe97e64ae 100644 --- a/clab/vxlan.go +++ b/clab/vxlan.go @@ -9,6 +9,7 @@ import ( "net" log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/utils" "github.com/vishvananda/netlink" ) @@ -30,7 +31,7 @@ func AddVxLanInterface(vxlan VxLAN) (err error) { log.Infof("Adding VxLAN link %s to remote address %s via %s with VNI %v", vxlan.Name, vxlan.Remote, vxlan.ParentIf, vxlan.ID) // before creating vxlan interface, check if it doesn't exist already - if vxlanIf, err = netlink.LinkByName(vxlan.Name); err != nil { + if vxlanIf, err = utils.LinkByNameOrAlias(vxlan.Name); err != nil { if _, ok := err.(netlink.LinkNotFoundError); !ok { return fmt.Errorf("failed to check if VxLAN interface %s exists: %v", vxlan.Name, err) } @@ -39,7 +40,7 @@ func AddVxLanInterface(vxlan VxLAN) (err error) { return fmt.Errorf("interface %s already exists", vxlan.Name) } - if parentIf, err = netlink.LinkByName(vxlan.ParentIf); err != nil { + if parentIf, err = utils.LinkByNameOrAlias(vxlan.ParentIf); err != nil { return fmt.Errorf("failed to get VxLAN parent interface %s: %v", vxlan.ParentIf, err) } diff --git a/cmd/destroy.go b/cmd/destroy.go index 86f3a7c53..50e5d1ad5 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -193,6 +193,15 @@ func destroyLab(ctx context.Context, c *clab.CLab) (err error) { maxWorkers = 1 } + // populating the nspath for the nodes + for _, n := range c.Nodes { + nsp, err := n.GetRuntime().GetNSPath(ctx, n.Config().LongName) + if err != nil { + continue + } + n.Config().NSPath = nsp + } + log.Infof("Destroying lab: %s", c.Config.Name) c.DeleteNodes(ctx, maxWorkers, serialNodes) @@ -221,10 +230,5 @@ func destroyLab(ctx context.Context, c *clab.CLab) (err error) { } } - // Remove any dangling veths from host netns or bridges - err = c.VethCleanup(ctx) - if err != nil { - return fmt.Errorf("error during veth cleanup procedure, %w", err) - } return err } diff --git a/cmd/vxlan.go b/cmd/vxlan.go index 281b0785b..6273a9c74 100644 --- a/cmd/vxlan.go +++ b/cmd/vxlan.go @@ -5,13 +5,14 @@ package cmd import ( + "context" "fmt" "net" - "github.com/jsimonetti/rtnetlink/rtnl" 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/utils" "github.com/vishvananda/netlink" ) @@ -57,39 +58,63 @@ var vxlanCreateCmd = &cobra.Command{ Use: "create", Short: "create vxlan interface", RunE: func(cmd *cobra.Command, args []string) error { - if _, err := netlink.LinkByName(cntLink); err != nil { + + ctx := context.Background() + + if _, err := utils.LinkByNameOrAlias(cntLink); err != nil { return fmt.Errorf("failed to lookup link %q: %v", cntLink, err) } // if vxlan device was not set specifically, we will use // the device that is reported by `ip route get $remote` if parentDev == "" { - conn, err := rtnl.Dial(nil) - if err != nil { - return fmt.Errorf("can't establish netlink connection: %s", err) - } - defer conn.Close() - r, err := conn.RouteGet(net.ParseIP(vxlanRemote)) + r, err := utils.GetRouteForIP(net.ParseIP(vxlanRemote)) if err != nil { return fmt.Errorf("failed to find a route to VxLAN remote address %s", vxlanRemote) } + parentDev = r.Interface.Name } - vxlanCfg := clab.VxLAN{ - Name: "vx-" + cntLink, - ID: vxlanID, - ParentIf: parentDev, - Remote: net.ParseIP(vxlanRemote), - MTU: vxlanMTU, + vxlraw := &links.LinkVxlanRaw{ + Remote: vxlanRemote, + VNI: vxlanID, + ParentInterface: parentDev, + LinkCommonParams: links.LinkCommonParams{ + MTU: vxlanMTU, + }, UDPPort: vxlanUDPPort, + LinkType: links.LinkTypeVxlanStitch, + Endpoint: *links.NewEndpointRaw( + "host", + cntLink, + "", + ), + } + + rp := &links.ResolveParams{ + Nodes: map[string]links.Node{ + "host": links.GetHostLinkNode(), + }, + VxlanIfaceNameOverwrite: cntLink, } - if err := clab.AddVxLanInterface(vxlanCfg); err != nil { + link, err := vxlraw.Resolve(rp) + if err != nil { return err } - return clab.BindIfacesWithTC(vxlanCfg.Name, cntLink) + var vxl *links.VxlanStitched + var ok bool + if vxl, ok = link.(*links.VxlanStitched); !ok { + return fmt.Errorf("not a VxlanStitched link") + } + + err = vxl.DeployWithExistingVeth(ctx) + if err != nil { + return err + } + return nil }, } @@ -100,7 +125,7 @@ var vxlanDeleteCmd = &cobra.Command{ var ls []netlink.Link var err error - ls, err = clab.GetLinksByNamePrefix(delPrefix) + ls, err = utils.GetLinksByNamePrefix(delPrefix) if err != nil { return err diff --git a/docs/manual/topo-def-file.md b/docs/manual/topo-def-file.md index 4a282047b..980b79c4a 100644 --- a/docs/manual/topo-def-file.md +++ b/docs/manual/topo-def-file.md @@ -231,7 +231,7 @@ In comparison to the veth type, no bridge or other namespace is required to be r - node: # mandatory interface: # mandatory mac: # optional - host-interface: # mandatory mtu: # optional vars: # optional (used in templating) labels: # optional (used in templating) @@ -239,6 +239,43 @@ In comparison to the veth type, no bridge or other namespace is required to be r The `host-interface` parameter defines the name of the veth interface in the host's network namespace. +###### vxlan +The vxlan type results in a vxlan tunnel interface that is created in the host namespace and subsequently pushed into the nodes network namespace. + +```yaml + links: + - type: vxlan + endpoint: # mandatory + node: # mandatory + interface: # mandatory + mac: # optional + remote: # mandatory + vni: # mandatory + udp-port: # mandatory + mtu: # optional + vars: # optional (used in templating) + labels: # optional (used in templating) +``` + +###### vxlan-stitched +The vxlan-stitched type results in a veth pair linking the host namespace and the nodes namespace and a vxlan tunnel that also terminates in the host namespace. +In addition to these interfaces, tc rules are being provisioned to stitch the vxlan tunnel and the host based veth interface together. + +```yaml + links: + - type: vxlan-stitch + endpoint: # mandatory + node: # mandatory + interface: # mandatory + mac: # optional + remote: # mandatory + vni: # mandatory + udp-port: # mandatory + mtu: # optional + vars: # optional (used in templating) + labels: # optional (used in templating) +``` + #### Kinds Kinds define the behavior and the nature of a node, it says if the node is a specific containerized Network OS, virtualized router or something else. We go into details of kinds in its own [document section](kinds/index.md), so here we will discuss what happens when `kinds` section appears in the topology definition: diff --git a/links/endpoint.go b/links/endpoint.go index e18dc45cd..0d45d7681 100644 --- a/links/endpoint.go +++ b/links/endpoint.go @@ -5,6 +5,8 @@ import ( "net" "github.com/containernetworking/plugins/pkg/ns" + log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/utils" "github.com/vishvananda/netlink" ) @@ -72,8 +74,8 @@ func (e *EndpointGeneric) GetNode() Node { } func (e *EndpointGeneric) Remove() error { - return e.GetNode().ExecFunction(func(_ ns.NetNS) error { - brSideEp, err := netlink.LinkByName(e.GetIfaceName()) + return e.GetNode().ExecFunction(func(n ns.NetNS) error { + brSideEp, err := utils.LinkByNameOrAlias(e.GetIfaceName()) _, notfound := err.(netlink.LinkNotFoundError) switch { @@ -83,7 +85,7 @@ func (e *EndpointGeneric) Remove() error { case err != nil: return err } - + log.Debugf("Removing interface %q from namespace %q", e.GetIfaceName(), e.GetNode().GetShortName()) return netlink.LinkDel(brSideEp) }) } @@ -132,11 +134,15 @@ 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()) + var err error + // long interface names (14+ chars) are aliased in the node's namespace + + _, err = utils.LinkByNameOrAlias(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()) + return fmt.Errorf("interface %s is defined via topology but does already exist: %v", e.String(), err) }) } diff --git a/links/endpoint_bridge.go b/links/endpoint_bridge.go index 0ccf699ff..328849b26 100644 --- a/links/endpoint_bridge.go +++ b/links/endpoint_bridge.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/containernetworking/plugins/pkg/ns" + "github.com/srl-labs/containerlab/utils" "github.com/vishvananda/netlink" ) @@ -12,6 +13,12 @@ type EndpointBridge struct { EndpointGeneric } +func NewEndpointBridge(eg *EndpointGeneric) *EndpointBridge { + return &EndpointBridge{ + EndpointGeneric: *eg, + } +} + func (e *EndpointBridge) Verify(p *VerifyLinkParams) error { var errs []error err := CheckEndpointUniqueness(e) @@ -38,7 +45,7 @@ func (e *EndpointBridge) Verify(p *VerifyLinkParams) error { // 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()) + br, err := utils.LinkByNameOrAlias(n.GetShortName()) _, notfound := err.(netlink.LinkNotFoundError) switch { case notfound: diff --git a/links/endpoint_host.go b/links/endpoint_host.go index 65534ae1b..943ef460e 100644 --- a/links/endpoint_host.go +++ b/links/endpoint_host.go @@ -6,8 +6,14 @@ type EndpointHost struct { EndpointGeneric } +func NewEndpointHost(eg *EndpointGeneric) *EndpointHost { + return &EndpointHost{ + EndpointGeneric: *eg, + } +} + func (e *EndpointHost) Verify(_ *VerifyLinkParams) error { - errs := []error{} + var errs []error err := CheckEndpointUniqueness(e) if err != nil { errs = append(errs, err) diff --git a/links/endpoint_macvlan.go b/links/endpoint_macvlan.go index d3853f87c..997c512dd 100644 --- a/links/endpoint_macvlan.go +++ b/links/endpoint_macvlan.go @@ -4,7 +4,13 @@ type EndpointMacVlan struct { EndpointGeneric } -// Verify verifies the veth based deployment pre-conditions. +func NewEndpointMacVlan(eg *EndpointGeneric) *EndpointMacVlan { + return &EndpointMacVlan{ + EndpointGeneric: *eg, + } +} + +// Verify runs verification to check if the endpoint can be deployed. func (e *EndpointMacVlan) Verify(_ *VerifyLinkParams) error { return CheckEndpointExists(e) } diff --git a/links/endpoint_raw.go b/links/endpoint_raw.go index 6e0fdceb9..fc6eef724 100644 --- a/links/endpoint_raw.go +++ b/links/endpoint_raw.go @@ -59,17 +59,13 @@ func (er *EndpointRaw) Resolve(params *ResolveParams, l Link) (Endpoint, error) switch node.GetLinkEndpointType() { case LinkEndpointTypeBridge: - e = &EndpointBridge{ - EndpointGeneric: *genericEndpoint, - } + e = NewEndpointBridge(genericEndpoint) + case LinkEndpointTypeHost: - e = &EndpointHost{ - EndpointGeneric: *genericEndpoint, - } + e = NewEndpointHost(genericEndpoint) + case LinkEndpointTypeVeth: - e = &EndpointVeth{ - EndpointGeneric: *genericEndpoint, - } + e = NewEndpointVeth(genericEndpoint) } // also add the endpoint to the node diff --git a/links/endpoint_veth.go b/links/endpoint_veth.go index b715966a8..50c89d08f 100644 --- a/links/endpoint_veth.go +++ b/links/endpoint_veth.go @@ -4,6 +4,12 @@ type EndpointVeth struct { EndpointGeneric } +func NewEndpointVeth(eg *EndpointGeneric) *EndpointVeth { + return &EndpointVeth{ + EndpointGeneric: *eg, + } +} + // Verify verifies the veth based deployment pre-conditions. func (e *EndpointVeth) Verify(_ *VerifyLinkParams) error { return CheckEndpointUniqueness(e) diff --git a/links/endpoint_vxlan.go b/links/endpoint_vxlan.go new file mode 100644 index 000000000..a12fe247d --- /dev/null +++ b/links/endpoint_vxlan.go @@ -0,0 +1,34 @@ +package links + +import ( + "fmt" + "net" +) + +type EndpointVxlan struct { + EndpointGeneric + udpPort int + remote net.IP + parentIface string + vni int + randName string +} + +func NewEndpointVxlan(node Node, link Link) *EndpointVxlan { + return &EndpointVxlan{ + randName: genRandomIfName(), + EndpointGeneric: EndpointGeneric{ + Link: link, + Node: node, + }, + } +} + +func (e *EndpointVxlan) String() string { + return fmt.Sprintf("vxlan remote: %q, udp-port: %d, vni: %d", e.remote, e.udpPort, e.vni) +} + +// Verify verifies that the endpoint is valid and can be deployed +func (e *EndpointVxlan) Verify(*VerifyLinkParams) error { + return CheckEndpointUniqueness(e) +} diff --git a/links/generic_link_node.go b/links/generic_link_node.go index 4729a9885..6fe10e688 100644 --- a/links/generic_link_node.go +++ b/links/generic_link_node.go @@ -55,8 +55,18 @@ func (g *GenericLinkNode) GetEndpoints() []Endpoint { return g.endpoints } -func (g *GenericLinkNode) GetState() state.NodeState { +func (*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 } + +func (g *GenericLinkNode) Delete(ctx context.Context) error { + for _, l := range g.links { + err := l.Remove(ctx) + if err != nil { + return err + } + } + return nil +} diff --git a/links/link.go b/links/link.go index a93bb2359..2e39a803e 100644 --- a/links/link.go +++ b/links/link.go @@ -21,13 +21,20 @@ const ( LinkDeploymentStateNotDeployed = iota LinkDeploymentStateDeployed + LinkDeploymentStateRemoved ) // 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"` + MTU int `yaml:"mtu,omitempty"` + Labels map[string]string `yaml:"labels,omitempty"` + Vars map[string]interface{} `yaml:"vars,omitempty"` + DeploymentState LinkDeploymentState +} + +// GetMTU returns the MTU of the link. +func (l *LinkCommonParams) GetMTU() int { + return l.MTU } // LinkDefinition represents a link definition in the topology file. @@ -40,10 +47,12 @@ type LinkDefinition struct { type LinkType string const ( - LinkTypeVEth LinkType = "veth" - LinkTypeMgmtNet LinkType = "mgmt-net" - LinkTypeMacVLan LinkType = "macvlan" - LinkTypeHost LinkType = "host" + LinkTypeVEth LinkType = "veth" + LinkTypeMgmtNet LinkType = "mgmt-net" + LinkTypeMacVLan LinkType = "macvlan" + LinkTypeHost LinkType = "host" + LinkTypeVxlan LinkType = "vxlan" + LinkTypeVxlanStitch LinkType = "vxlan-stitch" // LinkTypeBrief is a link definition where link types // are encoded in the endpoint definition as string and allow users @@ -56,14 +65,25 @@ 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 + + case string(LinkTypeVxlan): + return LinkTypeVxlan, nil + + case string(LinkTypeVxlanStitch): + return LinkTypeVxlanStitch, nil + default: return "", fmt.Errorf("unable to parse %q as LinkType", s) } @@ -73,7 +93,7 @@ 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 { +func (ld *LinkDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error { // skipcq: GO-R1005 // struct to avoid recursion when unmarshalling // used only to unmarshal the type field. var a struct { @@ -119,6 +139,7 @@ func (ld *LinkDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error return err } ld.Link = &l.LinkVEthRaw + case LinkTypeMgmtNet: var l struct { Type string `yaml:"type"` @@ -129,6 +150,7 @@ func (ld *LinkDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error return err } ld.Link = &l.LinkMgmtNetRaw + case LinkTypeHost: var l struct { Type string `yaml:"type"` @@ -139,6 +161,7 @@ func (ld *LinkDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error return err } ld.Link = &l.LinkHostRaw + case LinkTypeMacVLan: var l struct { Type string `yaml:"type"` @@ -149,6 +172,31 @@ func (ld *LinkDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error return err } ld.Link = &l.LinkMacVlanRaw + + case LinkTypeVxlan: + var l struct { + Type string `yaml:"type"` + LinkVxlanRaw `yaml:",inline"` + } + err := unmarshal(&l) + if err != nil { + return err + } + l.LinkVxlanRaw.LinkType = LinkTypeVxlan + ld.Link = &l.LinkVxlanRaw + + case LinkTypeVxlanStitch: + var l struct { + Type string `yaml:"type"` + LinkVxlanRaw `yaml:",inline"` + } + err := unmarshal(&l) + if err != nil { + return err + } + l.LinkVxlanRaw.LinkType = LinkTypeVxlanStitch + ld.Link = &l.LinkVxlanRaw + case LinkTypeBrief: // brief link's endpoint format var l struct { @@ -167,6 +215,7 @@ func (ld *LinkDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error if err != nil { return err } + default: return fmt.Errorf("unknown link type %q", lt) } @@ -218,6 +267,15 @@ func (r *LinkDefinition) MarshalYAML() (interface{}, error) { Type: string(LinkTypeMacVLan), } return x, nil + case LinkTypeVxlan: + x := struct { + Type string `yaml:"type"` + LinkVxlanRaw `yaml:",inline"` + }{ + LinkVxlanRaw: *r.Link.(*LinkVxlanRaw), + Type: string(LinkTypeMacVLan), + } + return x, nil case LinkTypeBrief: return r.Link, nil } @@ -245,6 +303,8 @@ type Link interface { GetType() LinkType // GetEndpoints returns the endpoints of the link. GetEndpoints() []Endpoint + // GetMTU returns the Link MTU. + GetMTU() int } func extractHostNodeInterfaceData(lb *LinkBriefRaw, specialEPIndex int) (host, hostIf, node, nodeIf string) { @@ -263,8 +323,12 @@ func extractHostNodeInterfaceData(lb *LinkBriefRaw, specialEPIndex int) (host, h } func genRandomIfName() string { + return "clab-" + genRandomString(8) +} + +func genRandomString(length int) string { s, _ := uuid.New().MarshalText() // .MarshalText() always return a nil error - return "clab-" + string(s[:8]) + return string(s[:length]) } // Node interface is an interface that is satisfied by all nodes. @@ -287,6 +351,7 @@ type Node interface { GetEndpoints() []Endpoint ExecFunction(func(ns.NetNS) error) error GetState() state.NodeState + Delete(ctx context.Context) error } type LinkEndpointType string @@ -301,26 +366,38 @@ const ( // 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) + // rename the link created with random name if its length is acceptable by linux + if len(endpt.GetIfaceName()) < 16 { + err := netlink.LinkSetName(l, endpt.GetIfaceName()) + if err != nil { + return fmt.Errorf( + "failed to rename link: %v", err) + } + } else { + // else we set the desired long name as alias + // in future we need to set it as an alternative name, + // pending https://github.com/vishvananda/netlink/pull/862 + err := netlink.LinkSetAlias(l, endpt.GetIfaceName()) + if err != nil { + return fmt.Errorf( + "failed to add alias: %v", err) + } } // lets set the MAC address if provided if len(endpt.GetMac()) == 6 { - err = netlink.LinkSetHardwareAddr(l, endpt.GetMac()) + err := netlink.LinkSetHardwareAddr(l, endpt.GetMac()) if err != nil { return err } } // bring the given link up - if err = netlink.LinkSetUp(l); err != nil { + if err := netlink.LinkSetUp(l); err != nil { return fmt.Errorf("failed to set %q up: %v", endpt.GetIfaceName(), err) } + return nil } } @@ -334,6 +411,11 @@ type ResolveParams struct { // list of node shortnames that user // passed as a node filter NodesFilter []string + // for the tools command we need to overwrite the + // veth interface name on the host side. So this can + // be set and will thereby overwrite the general interface + // name generation. + VxlanIfaceNameOverwrite string } type VerifyLinkParams struct { diff --git a/links/link_macvlan.go b/links/link_macvlan.go index 75824b2a8..faea26609 100644 --- a/links/link_macvlan.go +++ b/links/link_macvlan.go @@ -143,7 +143,7 @@ func (*LinkMacVlan) GetType() LinkType { } func (l *LinkMacVlan) GetParentInterfaceMtu() (int, error) { - hostLink, err := netlink.LinkByName(l.HostEndpoint.GetIfaceName()) + hostLink, err := utils.LinkByNameOrAlias(l.HostEndpoint.GetIfaceName()) if err != nil { return 0, err } @@ -152,7 +152,7 @@ func (l *LinkMacVlan) GetParentInterfaceMtu() (int, error) { func (l *LinkMacVlan) Deploy(ctx context.Context) error { // lookup the parent host interface - parentInterface, err := netlink.LinkByName(l.HostEndpoint.GetIfaceName()) + parentInterface, err := utils.LinkByNameOrAlias(l.HostEndpoint.GetIfaceName()) if err != nil { return err } @@ -189,7 +189,7 @@ func (l *LinkMacVlan) Deploy(ctx context.Context) error { } // retrieve the Link by name - mvInterface, err := netlink.LinkByName(l.NodeEndpoint.GetRandIfaceName()) + mvInterface, err := utils.LinkByNameOrAlias(l.NodeEndpoint.GetRandIfaceName()) if err != nil { return fmt.Errorf("failed to lookup %q: %v", l.NodeEndpoint.GetRandIfaceName(), err) } @@ -200,8 +200,15 @@ func (l *LinkMacVlan) Deploy(ctx context.Context) error { return err } -func (*LinkMacVlan) Remove(_ context.Context) error { - // TODO +func (l *LinkMacVlan) Remove(_ context.Context) error { + if l.DeploymentState == LinkDeploymentStateRemoved { + return nil + } + err := l.NodeEndpoint.Remove() + if err != nil { + log.Debug(err) + } + l.DeploymentState = LinkDeploymentStateRemoved return nil } diff --git a/links/link_veth.go b/links/link_veth.go index 6ba217740..003e6fbfe 100644 --- a/links/link_veth.go +++ b/links/link_veth.go @@ -46,10 +46,8 @@ func (r *LinkVEthRaw) Resolve(params *ResolveParams) (Link, error) { } // create LinkVEth struct - l := &LinkVEth{ - LinkCommonParams: r.LinkCommonParams, - Endpoints: make([]Endpoint, 0, 2), - } + l := NewLinkVEth() + l.LinkCommonParams = r.LinkCommonParams // resolve raw endpoints (epr) to endpoints (ep) for _, epr := range r.Endpoints { @@ -95,15 +93,17 @@ type LinkVEth struct { LinkCommonParams Endpoints []Endpoint - deploymentState LinkDeploymentState - deployMutex sync.Mutex + deployMutex sync.Mutex } -func (*LinkVEth) GetType() LinkType { - return LinkTypeVEth +func NewLinkVEth() *LinkVEth { + return &LinkVEth{ + Endpoints: make([]Endpoint, 0, 2), + } } -func (*LinkVEth) Verify() { +func (*LinkVEth) GetType() LinkType { + return LinkTypeVEth } func (l *LinkVEth) Deploy(ctx context.Context) error { @@ -111,7 +111,7 @@ func (l *LinkVEth) Deploy(ctx context.Context) error { // the link once, even if multiple nodes call deploy on the same link. l.deployMutex.Lock() defer l.deployMutex.Unlock() - if l.deploymentState == LinkDeploymentStateDeployed { + if l.DeploymentState == LinkDeploymentStateDeployed { return nil } @@ -141,7 +141,7 @@ func (l *LinkVEth) Deploy(ctx context.Context) error { } // retrieve the netlink.Link for the B / Peer side of the link - linkB, err := netlink.LinkByName(l.Endpoints[1].GetRandIfaceName()) + linkB, err := utils.LinkByNameOrAlias(l.Endpoints[1].GetRandIfaceName()) if err != nil { return err } @@ -167,13 +167,24 @@ func (l *LinkVEth) Deploy(ctx context.Context) error { } } - l.deploymentState = LinkDeploymentStateDeployed + l.DeploymentState = LinkDeploymentStateDeployed return nil } -func (*LinkVEth) Remove(_ context.Context) error { - // TODO +func (l *LinkVEth) Remove(_ context.Context) error { + l.deployMutex.Lock() + defer l.deployMutex.Unlock() + if l.DeploymentState == LinkDeploymentStateRemoved { + return nil + } + for _, ep := range l.GetEndpoints() { + err := ep.Remove() + if err != nil { + log.Debug(err) + } + } + l.DeploymentState = LinkDeploymentStateRemoved return nil } diff --git a/links/link_veth_test.go b/links/link_veth_test.go index 7aaa3ee66..9b6558143 100644 --- a/links/link_veth_test.go +++ b/links/link_veth_test.go @@ -185,13 +185,11 @@ func TestLinkVEthRaw_Resolve(t *testing.T) { 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) + 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) + t.Errorf("LinkVEthRaw.Resolve() EndpointVeth got %s, want %s", e.(*EndpointVeth).Node, tt.want.Endpoints[i].(*EndpointVeth).Node) } } }) @@ -242,3 +240,7 @@ func (*fakeNode) ExecFunction(_ func(ns.NetNS) error) error { func (f *fakeNode) GetState() state.NodeState { return f.State } + +func (*fakeNode) Delete(context.Context) error { + return nil +} diff --git a/links/link_vxlan.go b/links/link_vxlan.go new file mode 100644 index 000000000..4ac85b794 --- /dev/null +++ b/links/link_vxlan.go @@ -0,0 +1,287 @@ +package links + +import ( + "context" + "fmt" + "net" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/utils" + "github.com/vishvananda/netlink" +) + +const ( + // vxlan port is different from the default port number 4789 + // since 4789 may be filtered by the firewalls or clash with other overlay services. + VxLANDefaultPort = 14789 +) + +// LinkVxlanRaw is the raw (string) representation of a vxlan link as defined in the topology file. +type LinkVxlanRaw struct { + LinkCommonParams `yaml:",inline"` + Remote string `yaml:"remote"` + VNI int `yaml:"vni"` + Endpoint EndpointRaw `yaml:"endpoint"` + UDPPort int `yaml:"udp-port,omitempty"` + ParentInterface string `yaml:"parent-interface,omitempty"` + + // we use the same struct for vxlan and vxlan stitch, so we need to differentiate them in the raw format + LinkType LinkType +} + +func (lr *LinkVxlanRaw) Resolve(params *ResolveParams) (Link, error) { + switch lr.LinkType { + case LinkTypeVxlan: + return lr.resolveVxlan(params, false) + + case LinkTypeVxlanStitch: + return lr.resolveStitchedVxlan(params) + + default: + return nil, fmt.Errorf("unexpected LinkType %s for Vxlan based link", lr.LinkType) + } +} + +// resolveStitchedVEthComponent creates the veth link and return it, the endpoint that is +// supposed to be stitched is returned seperately for further processing +func (lr *LinkVxlanRaw) resolveStitchedVEthComponent(params *ResolveParams) (*LinkVEth, Endpoint, error) { + var err error + + lhr := &LinkHostRaw{ + LinkCommonParams: lr.LinkCommonParams, + HostInterface: fmt.Sprintf("ve-%s_%s", lr.Endpoint.Node, lr.Endpoint.Iface), + Endpoint: &EndpointRaw{ + Node: lr.Endpoint.Node, + Iface: lr.Endpoint.Iface, + }, + } + + hl, err := lhr.Resolve(params) + if err != nil { + return nil, nil, err + } + + vethLink := hl.(*LinkVEth) + + // host endpoint is always a 2nd element in the Endpoints slice + return vethLink, vethLink.Endpoints[1], nil +} + +// resolveStitchedVxlan resolves the stitched raw vxlan link. +func (lr *LinkVxlanRaw) resolveStitchedVxlan(params *ResolveParams) (Link, error) { + // prepare the vxlan struct + vxlanLink, err := lr.resolveVxlan(params, true) + if err != nil { + return nil, err + } + + // prepare the veth struct + vethLink, stitchEp, err := lr.resolveStitchedVEthComponent(params) + if err != nil { + return nil, err + } + + // return the stitched vxlan link + stitchedLink := NewVxlanStitched(vxlanLink, vethLink, stitchEp) + + // add stitched link to node + params.Nodes[lr.Endpoint.Node].AddLink(stitchedLink) + + return stitchedLink, nil +} + +func (lr *LinkVxlanRaw) resolveVxlan(params *ResolveParams, stitched bool) (*LinkVxlan, error) { + var err error + link := &LinkVxlan{ + LinkCommonParams: lr.LinkCommonParams, + } + + link.localEndpoint, err = lr.resolveLocalEndpoint(stitched, params, link) + if err != nil { + return nil, err + } + + ip := net.ParseIP(lr.Remote) + // if the returned ip is nil, an error occured. + // we consider, that we maybe have a textual hostname + // e.g. dns name so we try to resolve the string next + if ip == nil { + ips, err := net.LookupIP(lr.Remote) + if err != nil { + return nil, err + } + + // prepare log message + sb := strings.Builder{} + for _, ip := range ips { + sb.WriteString(", ") + sb.WriteString(ip.String()) + } + log.Debugf("looked up hostname %s, received IP addresses [%s]", lr.Remote, sb.String()[2:]) + + // always use the first address + if len(ips) <= 0 { + return nil, fmt.Errorf("unable to resolve %s", lr.Remote) + } + ip = ips[0] + } + + parentIf := lr.ParentInterface + + if parentIf == "" { + r, err := utils.GetRouteForIP(ip) + if err != nil { + return nil, fmt.Errorf("failed to find a route to VxLAN remote address %s", ip.String()) + } + + parentIf = r.Interface.Name + } + + // resolve remote endpoint + link.remoteEndpoint = NewEndpointVxlan(params.Nodes["host"], link) + link.remoteEndpoint.parentIface = parentIf + link.remoteEndpoint.udpPort = lr.UDPPort + if lr.UDPPort == 0 { + link.remoteEndpoint.udpPort = VxLANDefaultPort + } + link.remoteEndpoint.remote = ip + link.remoteEndpoint.vni = lr.VNI + // check if MAC-Addr is set in the raw vxlan link + if lr.Endpoint.MAC == "" { + // if it is not set generate a MAC + link.remoteEndpoint.MAC, err = utils.GenMac(ClabOUI) + if err != nil { + return nil, err + } + } else { + // if a MAC is set, parse and use it + hwaddr, err := net.ParseMAC(lr.Endpoint.MAC) + if err != nil { + return nil, err + } + link.remoteEndpoint.MAC = hwaddr + } + + // add link to local endpoints node + link.localEndpoint.GetNode().AddLink(link) + + return link, nil +} + +func (lr *LinkVxlanRaw) resolveLocalEndpoint(stitched bool, params *ResolveParams, link *LinkVxlan) (Endpoint, error) { + if stitched { + // point the vxlan endpoint to the host system + vxlanRawEp := lr.Endpoint + vxlanRawEp.Iface = fmt.Sprintf("vx-%s_%s", lr.Endpoint.Node, lr.Endpoint.Iface) + + if params.VxlanIfaceNameOverwrite != "" { + vxlanRawEp.Iface = fmt.Sprintf("vx-%s", params.VxlanIfaceNameOverwrite) + } + + // in the stiched vxlan mode we create vxlan interface in the host node namespace + vxlanRawEp.Node = "host" + vxlanRawEp.MAC = "" + + // resolve local Endpoint + return vxlanRawEp.Resolve(params, link) + + } else { + // resolve local Endpoint + return lr.Endpoint.Resolve(params, link) + } +} + +func (*LinkVxlanRaw) GetType() LinkType { + return LinkTypeVxlan +} + +type LinkVxlan struct { + LinkCommonParams + localEndpoint Endpoint + remoteEndpoint *EndpointVxlan +} + +func (l *LinkVxlan) Deploy(ctx context.Context) error { + err := l.deployVxlanInterface() + if err != nil { + return err + } + + // retrieve the Link by name + mvInterface, err := utils.LinkByNameOrAlias(l.localEndpoint.GetRandIfaceName()) + if err != nil { + return fmt.Errorf("failed to lookup %q: %v", l.localEndpoint.GetRandIfaceName(), err) + } + + // add the link to the Node Namespace + err = l.localEndpoint.GetNode().AddLinkToContainer(ctx, mvInterface, SetNameMACAndUpInterface(mvInterface, l.localEndpoint)) + return err +} + +// deployVxlanInterface internal function to create the vxlan interface in the host namespace +func (l *LinkVxlan) deployVxlanInterface() error { + // retrieve the parent interface netlink handle + parentIface, err := utils.LinkByNameOrAlias(l.remoteEndpoint.parentIface) + if err != nil { + return err + } + + // create the Vxlan struct + vxlanconf := netlink.Vxlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: l.localEndpoint.GetRandIfaceName(), + TxQLen: 1000, + HardwareAddr: l.remoteEndpoint.MAC, + }, + VxlanId: l.remoteEndpoint.vni, + VtepDevIndex: parentIface.Attrs().Index, + Group: l.remoteEndpoint.remote, + } + // set the upd port if defined in the input + if l.remoteEndpoint.udpPort != 0 { + vxlanconf.Port = l.remoteEndpoint.udpPort + } + + // define the MTU if defined in the input + if l.MTU != 0 { + vxlanconf.LinkAttrs.MTU = l.MTU + } + + // add the link + err = netlink.LinkAdd(&vxlanconf) + if err != nil { + return err + } + + // fetch the mtu from the actual state for templated config generation + if l.MTU == 0 { + interf, err := utils.LinkByNameOrAlias(l.localEndpoint.GetRandIfaceName()) + if err != nil { + return err + } + l.MTU = interf.Attrs().MTU + } + + return nil +} + +func (l *LinkVxlan) Remove(_ context.Context) error { + if l.DeploymentState == LinkDeploymentStateRemoved { + return nil + } + err := l.localEndpoint.Remove() + if err != nil { + log.Debug(err) + } + l.DeploymentState = LinkDeploymentStateRemoved + return nil +} + +func (l *LinkVxlan) GetEndpoints() []Endpoint { + return []Endpoint{l.localEndpoint, l.remoteEndpoint} +} + +func (*LinkVxlan) GetType() LinkType { + return LinkTypeVxlan +} diff --git a/links/link_vxlan_stitched.go b/links/link_vxlan_stitched.go new file mode 100644 index 000000000..d7ab78364 --- /dev/null +++ b/links/link_vxlan_stitched.go @@ -0,0 +1,168 @@ +package links + +import ( + "context" + "fmt" + "os" + "syscall" + + log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/utils" + "github.com/vishvananda/netlink" +) + +type VxlanStitched struct { + LinkCommonParams + vxlanLink *LinkVxlan + vethLink *LinkVEth + // the veth does not distinguist between endpoints. but we + // need to know which endpoint is the one used for + // stitching therefore we get that endpoint seperately + vethStitchEp Endpoint +} + +// NewVxlanStitched constructs a new VxlanStitched object +func NewVxlanStitched(vxlan *LinkVxlan, veth *LinkVEth, vethStitchEp Endpoint) *VxlanStitched { + // init the VxlanStitched struct + vxlanStitched := &VxlanStitched{ + LinkCommonParams: vxlan.LinkCommonParams, + vxlanLink: vxlan, + vethLink: veth, + vethStitchEp: vethStitchEp, + } + + return vxlanStitched +} + +// DeployWithExistingVeth provisons the stitched vxlan link whilst the +// veth interface does already exist, hence it is not created as part of this +// deployment +func (l *VxlanStitched) DeployWithExistingVeth(ctx context.Context) error { + return l.internalDeploy(ctx, true) +} + +// Deploy provisions the stitched vxlan link with all its underlaying sub-links +func (l *VxlanStitched) Deploy(ctx context.Context) error { + return l.internalDeploy(ctx, false) +} + +func (l *VxlanStitched) internalDeploy(ctx context.Context, skipVethCreation bool) error { + // deploy the vxlan link + err := l.vxlanLink.Deploy(ctx) + if err != nil { + return err + } + + // the veth creation might be skipped if it already exists + if !skipVethCreation { + err = l.vethLink.Deploy(ctx) + if err != nil { + return err + } + } + + // unidirectionally stitch the vxlan endpoint to the veth endpoint + err = stitch(l.vxlanLink.localEndpoint, l.vethStitchEp) + if err != nil { + return err + } + + // unidirectionally stitch the veth endpoint to the vxlan endpoint + err = stitch(l.vethStitchEp, l.vxlanLink.localEndpoint) + if err != nil { + return err + } + + return nil +} + +// Remove deprovisions the stitched vxlan link +func (l *VxlanStitched) Remove(ctx context.Context) error { + // remove the veth link piece + err := l.vethLink.Remove(ctx) + if err != nil { + log.Debug(err) + } + // remove the vxlan link piece + err = l.vxlanLink.Remove(ctx) + if err != nil { + log.Debug(err) + } + // set the links DeploymentState to Removed + l.DeploymentState = LinkDeploymentStateRemoved + return nil +} + +// GetEndpoints returns the endpoints that are part of the link +func (l *VxlanStitched) GetEndpoints() []Endpoint { + return []Endpoint{l.vxlanLink.localEndpoint, l.vxlanLink.remoteEndpoint} +} + +// GetType returns the LinkType enum +func (*VxlanStitched) GetType() LinkType { + return LinkTypeVxlanStitch +} + +// stitch provisions the tc rules to stitch two endpoints together in a unidirectional fashion +// it should take the veth and the vxlan endpoints of the root namespace +func stitch(ep1, ep2 Endpoint) error { + var err error + // collection of netlink links for the given endpoints + netlinkLinks := make([]netlink.Link, 0, 2) + log.Infof("configuring ingress mirroring with tc in the direction of %s -> %s", ep1, ep2) + + // retrieve the respective netlink Links + for _, endpointName := range []string{ep1.GetIfaceName(), ep2.GetIfaceName()} { + var l netlink.Link + if l, err = utils.LinkByNameOrAlias(endpointName); err != nil { + return fmt.Errorf("failed to lookup %q: %v", endpointName, err) + } + netlinkLinks = append(netlinkLinks, l) + } + + // tc qdisc add dev $SRC_IFACE ingress + qdisc := &netlink.Ingress{ + QdiscAttrs: netlink.QdiscAttrs{ + LinkIndex: netlinkLinks[0].Attrs().Index, + Handle: netlink.MakeHandle(0xffff, 0), + Parent: netlink.HANDLE_INGRESS, + }, + } + if err = netlink.QdiscAdd(qdisc); err != nil { + if !os.IsExist(err) { + return err + } + } + + // tc filter add dev $SRC_IFACE parent fffff: + // protocol all + // u32 match u32 0 0 + // action mirred egress mirror dev $DST_IFACE + filter := &netlink.U32{ + FilterAttrs: netlink.FilterAttrs{ + LinkIndex: netlinkLinks[0].Attrs().Index, + Parent: netlink.MakeHandle(0xffff, 0), + Protocol: syscall.ETH_P_ALL, + }, + Sel: &netlink.TcU32Sel{ + Keys: []netlink.TcU32Key{ + { + Mask: 0x0, + Val: 0, + }, + }, + Flags: netlink.TC_U32_TERMINAL, + }, + Actions: []netlink.Action{ + &netlink.MirredAction{ + ActionAttrs: netlink.ActionAttrs{ + Action: netlink.TC_ACT_PIPE, + }, + MirredAction: netlink.TCA_EGRESS_MIRROR, + Ifindex: netlinkLinks[1].Attrs().Index, + }, + }, + } + // finally add the tc filter + return netlink.FilterAdd(filter) +} diff --git a/nodes/bridge/bridge.go b/nodes/bridge/bridge.go index 5a057673b..f75bfa707 100644 --- a/nodes/bridge/bridge.go +++ b/nodes/bridge/bridge.go @@ -136,7 +136,7 @@ func (b *bridge) AddLinkToContainer(ctx context.Context, link netlink.Link, f fu } // get the bridge as netlink.Link - br, err := netlink.LinkByName(b.Cfg.ShortName) + br, err := utils.LinkByNameOrAlias(b.Cfg.ShortName) if err != nil { return err } diff --git a/nodes/default_node.go b/nodes/default_node.go index 4fff183b5..c5858fe01 100644 --- a/nodes/default_node.go +++ b/nodes/default_node.go @@ -143,6 +143,12 @@ func (d *DefaultNode) Deploy(ctx context.Context, _ *DeployParams) error { } func (d *DefaultNode) Delete(ctx context.Context) error { + for _, l := range d.Links { + err := l.Remove(ctx) + if err != nil { + return err + } + } return d.Runtime.DeleteContainer(ctx, d.OverwriteNode.GetContainerName()) } diff --git a/nodes/srl/srl.go b/nodes/srl/srl.go index 45b56200e..f8c4de77e 100644 --- a/nodes/srl/srl.go +++ b/nodes/srl/srl.go @@ -37,6 +37,14 @@ const ( readyTimeout = time.Minute * 2 // max wait time for node to boot retryTimer = time.Second + + // defaultCfgPath is a path to a file with default config that clab adds on top of the factory config. + // Default config is a config that adds some basic configuration to the node, such as tls certs, gnmi/json-rpc, login-banner. + defaultCfgPath = "/tmp/clab-default-config" + // overlayCfgPath is a path to a file with additional config that clab adds on top of the default config. + // Partial config provided via startup-config parameter is an overlay config. + overlayCfgPath = "/tmp/clab-overlay-config" + // additional config that clab adds on top of the factory config. srlConfigCmdsTpl = `set / system tls server-profile clab-profile set / system tls server-profile clab-profile key "{{ .TLSKey }}" @@ -318,6 +326,11 @@ func (s *srl) PostDeploy(ctx context.Context, params *nodes.PostDeployParams) er return err } + // once default and overlay config is added, we can commit the config + if err := s.commitConfig(ctx); err != nil { + return err + } + return s.generateCheckpoint(ctx) } @@ -585,17 +598,10 @@ func (n *srl) addDefaultConfig(ctx context.Context) error { 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 + // if the endpoint has a custom MTU set, use it in the template logic + // otherwise we don't set the mtu as srlinux will use the default max value 9232 + if m := e.GetLink().GetMTU(); m != links.DefaultLinkMTU { + iface.Mtu = m } // add the template interface definition to the template data @@ -612,17 +618,17 @@ func (n *srl) addDefaultConfig(ctx context.Context) error { execCmd := exec.NewExecCmdFromSlice([]string{ "bash", "-c", - fmt.Sprintf("echo '%s' > /tmp/clab-config", buf.String()), + fmt.Sprintf("echo '%s' > %s", buf.String(), defaultCfgPath), }) _, err = n.RunExec(ctx, execCmd) if err != nil { return err } - cmd, err := exec.NewExecCmdFromString(`bash -c "/opt/srlinux/bin/sr_cli -ed < /tmp/clab-config"`) - if err != nil { - return err - } + cmd := exec.NewExecCmdFromSlice([]string{ + "bash", "-c", + fmt.Sprintf("/opt/srlinux/bin/sr_cli -ed < %s", defaultCfgPath), + }) execResult, err := n.RunExec(ctx, cmd) if err != nil { @@ -636,20 +642,51 @@ func (n *srl) addDefaultConfig(ctx context.Context) error { // addOverlayCLIConfig adds CLI formatted config that is read out of a file provided via startup-config directive. func (s *srl) addOverlayCLIConfig(ctx context.Context) error { + if len(s.startupCliCfg) == 0 { + log.Debugf("node %q: startup-config empty, committing existing candidate", s.Config().ShortName) + + return nil + } + cfgStr := string(s.startupCliCfg) log.Debugf("Node %q additional config from startup-config file %s:\n%s", s.Cfg.ShortName, s.Cfg.StartupConfig, cfgStr) cmd := exec.NewExecCmdFromSlice([]string{ "bash", "-c", - fmt.Sprintf("echo '%s' > /tmp/clab-config", cfgStr), + fmt.Sprintf("echo '%s' > %s", cfgStr, overlayCfgPath), }) _, err := s.RunExec(ctx, cmd) if err != nil { return err } - cmd, _ = exec.NewExecCmdFromString(`bash -c "/opt/srlinux/bin/sr_cli -ed --post 'commit save' < tmp/clab-config"`) + cmd = exec.NewExecCmdFromSlice([]string{ + "bash", "-c", + fmt.Sprintf("/opt/srlinux/bin/sr_cli -ed < %s", overlayCfgPath), + }) + execResult, err := s.RunExec(ctx, cmd) + if err != nil { + return err + } + + if len(execResult.GetStdErrString()) != 0 { + return fmt.Errorf("%w:%s", nodes.ErrCommandExecError, execResult.GetStdErrString()) + } + + log.Debugf("node %s. stdout: %s, stderr: %s", s.Cfg.ShortName, execResult.GetStdOutString(), execResult.GetStdErrString()) + + return nil +} + +// commitConfig commits and saves default+overlay config to the startup-config file. +func (s *srl) commitConfig(ctx context.Context) error { + log.Debugf("Node %q: commiting configuration", s.Cfg.ShortName) + + cmd, err := exec.NewExecCmdFromString(`bash -c "/opt/srlinux/bin/sr_cli -ed commit save"`) + if err != nil { + return err + } execResult, err := s.RunExec(ctx, cmd) if err != nil { return err diff --git a/tests/08-vxlan/01-vxlan-s1.config b/tests/08-vxlan/01-vxlan-s1.config new file mode 100644 index 000000000..5366c282d --- /dev/null +++ b/tests/08-vxlan/01-vxlan-s1.config @@ -0,0 +1,6 @@ +set / interface ethernet-1/1 admin-state enable +set / interface ethernet-1/1 subinterface 1 admin-state enable +set / interface ethernet-1/1 subinterface 1 ipv4 +set / interface ethernet-1/1 subinterface 1 ipv4 admin-state enable +set / interface ethernet-1/1 subinterface 1 ipv4 address 192.168.67.1/30 +set / network-instance default interface ethernet-1/1.1 \ No newline at end of file diff --git a/tests/08-vxlan/01-vxlan.clab.yml b/tests/08-vxlan/01-vxlan.clab.yml new file mode 100644 index 000000000..2ab7edb1c --- /dev/null +++ b/tests/08-vxlan/01-vxlan.clab.yml @@ -0,0 +1,35 @@ +name: vxlan + +mgmt: + network: clab-vxlan + bridge: clab-vxlan-br + ipv4-subnet: 172.20.25.0/24 + +topology: + nodes: + srl1: + kind: nokia_srlinux + image: ghcr.io/nokia/srlinux + startup-config: 01-vxlan-s1.config + mgmt-ipv4: 172.20.25.21 + l2: + kind: linux + image: alpine:3 + exec: + - > + ash -c ' + apk add iproute2 && + ip link add name vxlan0 type vxlan id 100 remote 172.20.25.21 dstport 14788 && + ip l set dev vxlan0 up && + ip addr add dev vxlan0 192.168.67.2/30' + mgmt-ipv4: 172.20.25.22 + + links: + - type: vxlan + endpoint: + node: srl1 + interface: e1-1 + mac: 02:00:00:00:00:04 + remote: 172.20.25.22 + vni: 100 + udp-port: 14788 diff --git a/tests/08-vxlan/01-vxlan.robot b/tests/08-vxlan/01-vxlan.robot new file mode 100644 index 000000000..674b1f550 --- /dev/null +++ b/tests/08-vxlan/01-vxlan.robot @@ -0,0 +1,86 @@ +*** Settings *** +Library OperatingSystem +Library String +Resource ../common.robot + +Suite Setup Setup +Suite Teardown Cleanup + + +*** Variables *** +${lab-name} vxlan +${lab-file} 01-vxlan.clab.yml +${runtime} docker +${lab-net} clab-vxlan +${vxlan-br} clab-vxlan-br +${vxlan-br-ip} 172.20.25.1/24 + + +*** Test Cases *** +Deploy ${lab-name} lab + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E ${CLAB_BIN} --runtime ${runtime} deploy -t ${CURDIR}/${lab-file} -d + Log ${output} + Should Be Equal As Integers ${rc} 0 + +Check VxLAN interface parameters in srl node + # the commented out piece is to identify the link ifindex for a clab network + # but since we use a custom network here, we can just use its name, as the link will **not** be in the form of br- + # ... sudo docker inspect -f '{{.Id}}' ${lab-net} | cut -c1-12 | xargs echo br- | tr -d ' ' | xargs ip -j l show | jq -r '.[0].ifindex' + ${rc} ${link_ifindex} = Run And Return Rc And Output + ... ip -j l show ${vxlan-br} | jq -r '.[0].ifindex' + + ${rc} ${output} = Run And Return Rc And Output + ... sudo docker exec clab-${lab-name}-srl1 ip -d l show e1-1 + + Should Contain ${output} vxlan id 100 remote 172.20.25.22 dev if${link_ifindex} srcport 0 0 dstport 14788 + +Check VxLAN connectivity srl-linux + # CI env var is set to true in Github Actions + # and this test won't run there, since it fails for unknown reason + IF '%{CI=false}'=='false' + Wait Until Keyword Succeeds 60 2s Check VxLAN connectivity srl->linux + END + +Check VxLAN connectivity linux-srl + IF '%{CI=false}'=='false' + Wait Until Keyword Succeeds 60 2s Check VxLAN connectivity linux->srl + END + + +*** Keywords *** +Check VxLAN connectivity srl->linux + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E docker exec -it clab-vxlan-srl1 ip netns exec srbase-default ping 192.168.67.2 -c 1 + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} 0% packet loss + +Check VxLAN connectivity linux->srl + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E docker exec clab-vxlan-l2 ping 192.168.67.1 -c 1 + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} 0% packet loss + +Setup + # skipping this test suite for podman for now + Skip If '${runtime}' == 'podman' + # setup vxlan underlay bridge + # we have to setup an underlay management bridge with big enought mtu to support vxlan and srl requirements for link mtu + # we set mtu 9100 (and not the default 9500) because srl can't set vxlan mtu > 9412 and < 1500 + ${rc} ${output} = Run And Return Rc And Output + ... sudo ip link add ${vxlan-br} type bridge || true + ${rc} ${output} = Run And Return Rc And Output + ... sudo ip link set dev ${vxlan-br} up && sudo ip link set dev ${vxlan-br} mtu 9100 && sudo ip addr add ${vxlan-br-ip} dev ${vxlan-br} || true + Log ${output} + Should Be Equal As Integers ${rc} 0 + +Cleanup + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E ${CLAB_BIN} --runtime ${runtime} destroy -t ${CURDIR}/${lab-file} --cleanup + Log ${output} + + ${rc} ${output} = Run And Return Rc And Output + ... sudo ip l del ${vxlan-br} + Log ${output} diff --git a/tests/08-vxlan/02-vxlan-stitch.clab.yml b/tests/08-vxlan/02-vxlan-stitch.clab.yml new file mode 100644 index 000000000..65393ed7e --- /dev/null +++ b/tests/08-vxlan/02-vxlan-stitch.clab.yml @@ -0,0 +1,52 @@ +name: vxlan-stitch + +mgmt: + network: clab-vxlan + bridge: clab-vxlan-br + ipv4-subnet: 172.20.25.0/24 + +topology: + nodes: + srl1: + kind: nokia_srlinux + image: ghcr.io/nokia/srlinux + startup-config: 01-vxlan-s1.config + mgmt-ipv4: 172.20.25.21 + + # this node doesn't participate in the vxlan datapath + # we just put it here to test that long named nodes + # are treated correctly with ip aliases added to link name. + some_very_long_node_name_l1: + kind: linux + image: alpine:3 + exec: + - apk add iproute2 + + l2: + kind: linux + image: alpine:3 + exec: + - > + ash -c ' + apk add iproute2 && + ip link add name vxlan0 type vxlan id 100 remote 172.20.25.21 dstport 14788 && + ip l set dev vxlan0 up && + ip addr add dev vxlan0 192.168.67.2/30' + mgmt-ipv4: 172.20.25.22 + + links: + - type: vxlan-stitch + endpoint: + node: srl1 + interface: e1-1 + mac: 02:00:00:00:00:04 + remote: 172.20.25.22 + vni: 100 + udp-port: 14788 + + - type: vxlan-stitch + endpoint: + node: some_very_long_node_name_l1 + interface: e1-1 + remote: 172.20.25.23 + vni: 101 diff --git a/tests/08-vxlan/02-vxlan-stitch.robot b/tests/08-vxlan/02-vxlan-stitch.robot new file mode 100644 index 000000000..8ce020696 --- /dev/null +++ b/tests/08-vxlan/02-vxlan-stitch.robot @@ -0,0 +1,109 @@ +*** Settings *** +Library OperatingSystem +Library String +Resource ../common.robot + +Suite Setup Setup +Suite Teardown Cleanup + + +*** Variables *** +${lab-name} vxlan +${lab-file} 02-vxlan-stitch.clab.yml +${runtime} docker +${lab-net} clab-vxlan +${vxlan-br} clab-vxlan-br +${vxlan-br-ip} 172.20.25.1/24 + + +*** Test Cases *** +Deploy ${lab-name} lab + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E ${CLAB_BIN} --runtime ${runtime} deploy -t ${CURDIR}/${lab-file} -d + Log ${output} + Should Be Equal As Integers ${rc} 0 + +Check VxLAN interface parameters on the host for srl1 node + ${rc} ${output} = Run And Return Rc And Output + ... sudo ip -d l show vx-srl1_e1-1 + + Should Contain ${output} mtu 9050 + + Should Contain ${output} vxlan id 100 remote 172.20.25.22 dev clab-vxlan-br srcport 0 0 dstport 14788 + +Check veth interface parameters on the host for srl1 node + ${rc} ${output} = Run And Return Rc And Output + ... sudo ip -d l show ve-srl1_e1-1 + + Should Contain ${output} mtu 9500 + + Should Contain ${output} link-netns clab-vxlan-stitch-srl1 + +Check VxLAN interface parameters on the host for very long name node + ${rc} ${output} = Run And Return Rc And Output + ... sudo ip -j link | jq -r '.[] | select(.ifalias == "vx-some_very_long_node_name_l1_e1-1") | .ifname' | xargs ip -d l show + + Should Contain ${output} mtu 9050 + + Should Contain ${output} vxlan id 101 remote 172.20.25.23 dev clab-vxlan-br srcport 0 0 dstport 14789 + +Check veth interface parameters on the host for very long name node + ${rc} ${output} = Run And Return Rc And Output + ... sudo ip -j link | jq -r '.[] | select(.ifalias == "ve-some_very_long_node_name_l1_e1-1") | .ifname' | xargs ip -d l show + + Should Contain ${output} mtu 9500 qdisc noqueue state UP + + # in github actions the output for this link weirdly state the netnsid instead of nsname, thus we check for any of those + Should Contain Any ${output} link-netns clab-vxlan-stitch-some_very_long_node_name_l1 link-netnsid 2 + + Should Contain ${output} alias ve-some_very_long_node_name_l1_e1-1 + +Check VxLAN connectivity srl-linux + # CI env var is set to true in Github Actions + # and this test won't run there, since it fails for unknown reason + IF '%{CI=false}'=='false' + Wait Until Keyword Succeeds 60 2s Check VxLAN connectivity srl->linux + END + +Check VxLAN connectivity linux-srl + IF '%{CI=false}'=='false' + Wait Until Keyword Succeeds 60 2s Check VxLAN connectivity linux->srl + END + + +*** Keywords *** +Check VxLAN connectivity srl->linux + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E docker exec -it clab-vxlan-stitch-srl1 ip netns exec srbase-default ping 192.168.67.2 -c 1 + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} 0% packet loss + +Check VxLAN connectivity linux->srl + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E docker exec clab-vxlan-stitch-l2 ping 192.168.67.1 -c 1 + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} 0% packet loss + +Setup + # skipping this test suite for podman for now + Skip If '${runtime}' == 'podman' + # setup vxlan underlay bridge + # we have to setup an underlay management bridge with big enought mtu to support vxlan and srl requirements for link mtu + # we set mtu 9100 (and not the default 9500) because srl can't set vxlan mtu > 9412 and < 1500 + ${rc} ${output} = Run And Return Rc And Output + ... sudo ip link add ${vxlan-br} type bridge || true + ${rc} ${output} = Run And Return Rc And Output + ... sudo ip link set dev ${vxlan-br} up && sudo ip link set dev ${vxlan-br} mtu 9100 && sudo ip addr add ${vxlan-br-ip} dev ${vxlan-br} || true + Log ${output} + Should Be Equal As Integers ${rc} 0 + +Cleanup + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E ${CLAB_BIN} --runtime ${runtime} destroy -t ${CURDIR}/${lab-file} --cleanup + Log ${output} + + ${rc} ${output} = Run And Return Rc And Output + ... sudo ip l del ${vxlan-br} + Log ${output} diff --git a/tests/rf-run.sh b/tests/rf-run.sh index 135a72a2b..d7171a137 100755 --- a/tests/rf-run.sh +++ b/tests/rf-run.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # Copyright 2020 Nokia # Licensed under the BSD 3-Clause License. # SPDX-License-Identifier: BSD-3-Clause @@ -20,4 +20,19 @@ COV_DIR=tests/coverage # coverage output directory mkdir -p ${COV_DIR} -GOCOVERDIR=${COV_DIR} robot --consolecolors on -r none --variable CLAB_BIN:${CLAB_BIN} --variable runtime:$1 -l ./tests/out/$(basename $2)-$1-log --output ./tests/out/$(basename $2)-$1-out.xml $2 +# parses the dir or file name passed to the rf-run.sh script +# and in case of a directory, it returns the name of the directory +# in case of a file it returns the name of the file's dir catenated with file name without extension +function get_logname() { + path=$1 + filename=$(basename "$path") + if [[ "$filename" == *.* ]]; then + dirname=$(dirname "$path") + basename=$(basename "$path" | cut -d. -f1) + echo "${dirname##*/}-${basename}" + else + echo "${filename}" + fi +} + +GOCOVERDIR=${COV_DIR} robot --consolecolors on -r none --variable CLAB_BIN:${CLAB_BIN} --variable runtime:$1 -l ./tests/out/$(get_logname $2)-$1-log --output ./tests/out/$(basename $2)-$1-out.xml $2 diff --git a/utils/netlink.go b/utils/netlink.go index 9dd58e682..53bcabe46 100644 --- a/utils/netlink.go +++ b/utils/netlink.go @@ -9,14 +9,16 @@ import ( "fmt" "net" "os" + "strings" + "github.com/jsimonetti/rtnetlink/rtnl" log "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" ) // BridgeByName returns a *netlink.Bridge referenced by its name. func BridgeByName(name string) (*netlink.Bridge, error) { - l, err := netlink.LinkByName(name) + l, err := LinkByNameOrAlias(name) if err != nil { return nil, fmt.Errorf("could not lookup %q: %v", name, err) } @@ -42,34 +44,6 @@ func LinkContainerNS(nspath, containerName string) error { return nil } -func CheckBrInUse(brname string) (bool, error) { - InUse := false - l, err := netlink.LinkList() - if err != nil { - return InUse, err - } - mgmtbr, err := netlink.LinkByName(brname) - if err != nil { - return InUse, err - } - mgmtbridx := mgmtbr.Attrs().Index - for _, link := range l { - if link.Attrs().MasterIndex == mgmtbridx { - InUse = true - break - } - } - return InUse, nil -} - -func DeleteLinkByName(name string) error { - l, err := netlink.LinkByName(name) - if err != nil { - return err - } - return netlink.LinkDel(l) -} - // GenMac generates a random MAC address for a given OUI. func GenMac(oui string) (net.HardwareAddr, error) { buf := make([]byte, 3) @@ -92,7 +66,7 @@ func DeleteNetnsSymlink(n string) error { // LinkIPs returns IPv4/IPv6 addresses assigned to a link referred by its name. func LinkIPs(ln string) (v4addrs, v6addrs []netlink.Addr, err error) { - l, err := netlink.LinkByName(ln) + l, err := LinkByNameOrAlias(ln) if err != nil { return nil, nil, fmt.Errorf("failed to lookup link %q: %w", ln, err) } @@ -128,3 +102,52 @@ func FirstLinkIPs(ln string) (v4, v6 string, err error) { return v4, v6, err } + +// GetLinksByNamePrefix returns a list of links whose name matches a prefix. +func GetLinksByNamePrefix(prefix string) ([]netlink.Link, error) { + // filtered list of interfaces + if prefix == "" { + return nil, fmt.Errorf("prefix is not specified") + } + var fls []netlink.Link + + ls, err := netlink.LinkList() + if err != nil { + return nil, err + } + for _, l := range ls { + if strings.HasPrefix(l.Attrs().Name, prefix) { + fls = append(fls, l) + } + } + if len(fls) == 0 { + return nil, fmt.Errorf("no links found by specified prefix %s", prefix) + } + return fls, nil +} + +func LinkByNameOrAlias(name string) (netlink.Link, error) { + var l netlink.Link + var err error + + // long interface names (16+ chars) are aliased by clab + if len(name) > 15 { + l, err = netlink.LinkByAlias(name) + } else { + l, err = netlink.LinkByName(name) + } + + return l, err +} + +func GetRouteForIP(ip net.IP) (*rtnl.Route, error) { + conn, err := rtnl.Dial(nil) + if err != nil { + return nil, fmt.Errorf("can't establish netlink connection: %s", err) + } + defer conn.Close() + + r, err := conn.RouteGet(ip) + + return r, err +}