diff --git a/test/e2e/localnet-underlay.go b/test/e2e/localnet-underlay.go index d09600cbd70..d18d52857c3 100644 --- a/test/e2e/localnet-underlay.go +++ b/test/e2e/localnet-underlay.go @@ -3,6 +3,8 @@ package e2e import ( "context" "fmt" + "os" + "os/exec" "strings" v1 "k8s.io/api/core/v1" @@ -161,3 +163,38 @@ func bridgeMapping(physnet, ovsBridge string) BridgeMapping { ovsBridge: ovsBridge, } } + +func createVLANInterface(deviceName string, vlanID string, ipAddress *string) error { + vlanName := fmt.Sprintf("%s.%s", deviceName, vlanID) + cmd := exec.Command("sudo", "ip", "link", "add", "link", deviceName, "name", vlanName, "type", "vlan", "id", vlanID) + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to create vlan interface %s: %v", vlanName, err) + } + + cmd = exec.Command("sudo", "ip", "link", "set", "dev", vlanName, "up") + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to enable vlan interface %s: %v", vlanName, err) + } + + if ipAddress != nil { + cmd = exec.Command("sudo", "ip", "addr", "add", *ipAddress, "dev", vlanName) + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to define the vlan interface %q IP Address %s: %v", vlanName, *ipAddress, err) + } + } + return nil +} + +func deleteVLANInterface(deviceName string, vlanID string) error { + vlanName := fmt.Sprintf("%s.%s", deviceName, vlanID) + cmd := exec.Command("sudo", "ip", "link", "del", deviceName) + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to create vlan interface %s: %v", vlanName, err) + } + return nil +} diff --git a/test/e2e/multihoming.go b/test/e2e/multihoming.go index 15fc2be76d1..8984785aaec 100644 --- a/test/e2e/multihoming.go +++ b/test/e2e/multihoming.go @@ -3,6 +3,7 @@ package e2e import ( "context" "fmt" + "net" "os" "os/exec" "strconv" @@ -656,6 +657,7 @@ var _ = Describe("Multi Homing", func() { var underlayBridgeName string var cmdWebServer *exec.Cmd + underlayIP := underlayServiceIP + "/24" Context("with a service running on the underlay", func() { BeforeEach(func() { netConfig = newNetworkAttachmentConfig( @@ -685,7 +687,7 @@ var _ = Describe("Multi Homing", func() { underlayBridgeName, err = findInterfaceByIP(gatewayIP) Expect(err).NotTo(HaveOccurred()) - cmd := exec.Command("sudo", "ip", "addr", "add", underlayServiceIP+"/24", "dev", underlayBridgeName) + cmd := exec.Command("sudo", "ip", "addr", "add", underlayIP, "dev", underlayBridgeName) cmd.Stderr = os.Stderr err = cmd.Run() Expect(err).NotTo(HaveOccurred()) @@ -693,10 +695,7 @@ var _ = Describe("Multi Homing", func() { BeforeEach(func() { By("starting a service, connected to the underlay") - cmdWebServer = exec.Command("python3", "-m", "http.server", "--bind", underlayServiceIP, strconv.Itoa(servicePort)) - cmdWebServer.Stderr = os.Stderr - err := cmdWebServer.Start() - Expect(err).NotTo(HaveOccurred(), "failed to create web server, port might be busy") + Expect(startServer(underlayServiceIP, servicePort)).NotTo(HaveOccurred(), "failed to create web server, port might be busy") }) BeforeEach(func() { @@ -715,7 +714,7 @@ var _ = Describe("Multi Homing", func() { }) AfterEach(func() { - cmd := exec.Command("sudo", "ip", "addr", "del", underlayServiceIP+"/24", "dev", underlayBridgeName) + cmd := exec.Command("sudo", "ip", "addr", "del", underlayIP, "dev", underlayBridgeName) cmd.Stderr = os.Stderr err := cmd.Run() Expect(err).NotTo(HaveOccurred()) @@ -899,6 +898,90 @@ var _ = Describe("Multi Homing", func() { ) }) }) + + Context("with a trunked configuration", func() { + const vlanID = 20 + BeforeEach(func() { + nodes := ovsPods(cs) + Expect(nodes).NotTo(BeEmpty()) + + Expect( + setupUnderlayForTrunkedConfig( + f.Namespace.Name, + secondaryLocalnetNetworkCIDR, + secondaryInterfaceName, + underlayServiceIP, + ), + ).To(Succeed()) + + By("creating a VLAN interface with an IP on top of the bridge connecting the cluster nodes") + cli, err := client.NewClientWithOpts(client.FromEnv) + Expect(err).NotTo(HaveOccurred()) + + gatewayIP, err := getNetworkGateway(cli, dockerNetworkName) + Expect(err).NotTo(HaveOccurred()) + + underlayBridgeName, err = findInterfaceByIP(gatewayIP) + Expect(err).NotTo(HaveOccurred()) + + Expect(createVLANInterface(underlayBridgeName, strconv.Itoa(vlanID), &underlayIP)).To( + Succeed(), + "create a VLAN interface on the bridge interconnecting the cluster nodes", + ) + }) + + BeforeEach(func() { + By("starting a service, connected to the underlay") + Expect(startServer(underlayServiceIP, servicePort)).NotTo(HaveOccurred(), "failed to create web server, port might be busy") + }) + + BeforeEach(func() { + _, err := nadClient.NetworkAttachmentDefinitions(netConfig.namespace).Create( + context.Background(), + generateNAD(netConfig), + metav1.CreateOptions{}, + ) + Expect(err).NotTo(HaveOccurred(), "create the attachment configuration") + }) + + AfterEach(func() { + Expect(cmdWebServer.Process.Kill()).NotTo(HaveOccurred(), "kill the python webserver") + Expect(deleteVLANInterface(underlayBridgeName, strconv.Itoa(vlanID))).NotTo(HaveOccurred(), "remove the underlay physical configuration") + Expect(teardownUnderlay(nodes)).To(Succeed(), "tear down the localnet underlay") + }) + + It("the same localnet network mapping can be shared on a separate VLAN by using the localnet alias attribute", func() { + const otherNetworkName = "different-network" + vlan20NetConfig := newNetworkAttachmentConfig( + networkAttachmentConfigParams{ + name: otherNetworkName, + localnetPortNameAlias: netConfig.networkName, + namespace: f.Namespace.Name, + vlanID: vlanID, + topology: "localnet", + cidr: secondaryLocalnetNetworkCIDR, + excludeCIDRs: []string{underlayServiceIP + "/32"}, + }) + + By("creating the attachment configuration for a separate VLAN") + _, err := nadClient.NetworkAttachmentDefinitions(netConfig.namespace).Create( + context.Background(), + generateNAD(vlan20NetConfig), + metav1.CreateOptions{}, + ) + Expect(err).NotTo(HaveOccurred()) + + clientPodConfig := podConfiguration{ + name: clientPodName, + namespace: f.Namespace.Name, + attachments: []nadapi.NetworkSelectionElement{{Name: otherNetworkName}}, + } + kickstartPod(cs, clientPodConfig) + time.Sleep(10 * time.Minute) + By("asserting the *client* pod can contact the underlay service on the separate vlan") + Expect(connectToServer(clientPodConfig, underlayServiceIP, servicePort)).To(Succeed()) + }) + }) }) Context("multi-network policies", func() { @@ -1599,6 +1682,30 @@ var _ = Describe("Multi Homing", func() { }) }) +func setupUnderlayForTrunkedConfig(namespace string, subnet string, underlayServiceIP string, secondaryInterfaceName string, ovsNodes ...v1.Pod) error { + netConfig := newNetworkAttachmentConfig( + // if we omit the vlan tag, we'll have a trunked configuration in the ovs bridge + networkAttachmentConfigParams{ + name: secondaryNetworkName, + namespace: namespace, + topology: "localnet", + cidr: subnet, + excludeCIDRs: []string{underlayServiceIP + "/32"}, + }) + + By("setting up the localnet underlay") + if err := setupUnderlay(ovsNodes, secondaryInterfaceName, netConfig); err != nil { + return err + } + return nil +} + +func startServer(ip string, port int) error { + cmdWebServer := exec.Command("python3", "-m", "http.server", "--bind", ip, strconv.Itoa(port)) + cmdWebServer.Stderr = os.Stderr + return cmdWebServer.Start() +} + func kickstartPod(cs clientset.Interface, configuration podConfiguration) *v1.Pod { By(fmt.Sprintf("instantiating the %q pod", fmt.Sprintf("%s/%s", configuration.namespace, configuration.name))) createdPod, err := cs.CoreV1().Pods(configuration.namespace).Create( @@ -1666,3 +1773,15 @@ func createMultiNetworkPolicy(mnpClient mnpclient.K8sCniCncfIoV1beta1Interface, ) return err } + +func MustParseIPNet(cidrStr string) *net.IPNet { + ip, ipNet, err := net.ParseCIDR(cidrStr) + if err != nil { + panic(fmt.Sprintf("Could not parse %q as a CIDR: %v", cidrStr, err)) + } + + if !ipNet.IP.Equal(ip) { + ipNet.IP = ip + } + return ipNet +} diff --git a/test/e2e/multihoming_utils.go b/test/e2e/multihoming_utils.go index 874fcc4eb43..a07fb3b9e90 100644 --- a/test/e2e/multihoming_utils.go +++ b/test/e2e/multihoming_utils.go @@ -52,15 +52,16 @@ func getNetCIDRSubnet(netCIDR string) (string, error) { } type networkAttachmentConfigParams struct { - cidr string - excludeCIDRs []string - namespace string - name string - topology string - networkName string - vlanID int - allowPersistentIPs bool - role string + cidr string + excludeCIDRs []string + namespace string + name string + topology string + networkName string + vlanID int + allowPersistentIPs bool + role string + localnetPortNameAlias string } type networkAttachmentConfig struct { @@ -96,6 +97,7 @@ func generateNAD(config networkAttachmentConfig) *nadapi.NetworkAttachmentDefini "netAttachDefName": %q, "vlanID": %d, "allowPersistentIPs": %t, + "localnetPortAlias": %q, "role": %q } `, @@ -106,6 +108,7 @@ func generateNAD(config networkAttachmentConfig) *nadapi.NetworkAttachmentDefini namespacedName(config.namespace, config.name), config.vlanID, config.allowPersistentIPs, + config.localnetPortNameAlias, config.role, ) return generateNetAttachDef(config.namespace, config.name, nadSpec)