diff --git a/test/e2e/localnet-underlay.go b/test/e2e/localnet-underlay.go index d09600cbd70..251469b09ce 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", vlanName) + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to delete vlan interface %s: %v", vlanName, err) + } + return nil +} diff --git a/test/e2e/multihoming.go b/test/e2e/multihoming.go index 15fc2be76d1..dda951a0a3a 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()) @@ -695,8 +697,7 @@ var _ = Describe("Multi Homing", 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(cmdWebServer.Start()).NotTo(HaveOccurred(), "failed to create web server, port might be busy") }) BeforeEach(func() { @@ -715,7 +716,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 +900,85 @@ var _ = Describe("Multi Homing", func() { ) }) }) + + Context("with a trunked configuration", func() { + const vlanID = 20 + BeforeEach(func() { + nodes = ovsPods(cs) + Expect(nodes).NotTo(BeEmpty()) + + // we are setting up the bridge in trunked mode by not + // specifying a particular VLAN ID on the network conf + netConfig = newNetworkAttachmentConfig( + networkAttachmentConfigParams{ + name: secondaryNetworkName, + namespace: f.Namespace.Name, + topology: "localnet", + cidr: secondaryLocalnetNetworkCIDR, + excludeCIDRs: []string{underlayServiceIP + "/32"}, + }) + + By("setting up the localnet underlay with a trunked configuration") + Expect(setupUnderlay(nodes, secondaryInterfaceName, netConfig)).To(Succeed(), "configuring the OVS bridge") + + By(fmt.Sprintf("creating a VLAN interface on top of the bridge connecting the cluster nodes with IP: %s", underlayIP)) + 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", + ) + + By("starting a service, connected to the underlay") + cmdWebServer = exec.Command("python3", "-m", "http.server", "--bind", underlayServiceIP, strconv.Itoa(port)) + cmdWebServer.Stderr = os.Stderr + Expect(cmdWebServer.Start()).NotTo(HaveOccurred(), "failed to create web server, port might be busy") + }) + + 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(f.Namespace.Name).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) + + By(fmt.Sprintf("asserting the *client* pod can contact the underlay service with IP %q on the separate vlan", underlayIP)) + Expect(connectToServer(clientPodConfig, underlayServiceIP, servicePort)).To(Succeed()) + }) + }) }) Context("multi-network policies", func() { @@ -1666,3 +1746,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)