Skip to content

Commit d7657e5

Browse files
committed
Adapt egressIP tests to ipv6primary and ipv4primary dualstack cluster
1 parent 6f962b8 commit d7657e5

File tree

1 file changed

+199
-60
lines changed

1 file changed

+199
-60
lines changed

test/extended/openstack/egressip.go

Lines changed: 199 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import (
55
"encoding/json"
66
"fmt"
77
"net"
8+
"net/netip"
89
"os"
10+
"strings"
911

1012
"github.com/gophercloud/gophercloud"
1113
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
1214
"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
13-
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
1415
g "github.com/onsi/ginkgo/v2"
1516
o "github.com/onsi/gomega"
17+
configv1 "github.com/openshift/api/config/v1"
1618
cloudnetwork "github.com/openshift/client-go/cloudnetwork/clientset/versioned"
1719
exutil "github.com/openshift/origin/test/extended/util"
1820

@@ -102,13 +104,15 @@ var _ = g.Describe("[sig-installer][Suite:openshift/openstack][egressip] An egre
102104
defer node.RemoveLabelOffNode(clientSet, primaryWorker.Name, egressAssignableLabelKey)
103105

104106
g.By(fmt.Sprintf("Getting the EgressIP network from the '%s' annotation", egressIPConfigAnnotationKey))
105-
egressIPNetCidrStr, err := getEgressIPNetwork(primaryWorker)
107+
egressIPNetCidrStr, egressPortId, err := getEgressNetworkInfo(primaryWorker, "ipv4")
106108
o.Expect(err).NotTo(o.HaveOccurred())
107109
o.Expect(egressIPNetCidrStr).NotTo(o.BeEmpty(), "Could not get the EgressIP network from the '%s' annotation", egressIPConfigAnnotationKey)
108110
e2e.Logf("Found the EgressIP network: %s", egressIPNetCidrStr)
111+
o.Expect(egressPortId).NotTo(o.BeEmpty(), "Could not get the Egress openstack portId from the '%s' annotation", egressIPConfigAnnotationKey)
112+
e2e.Logf("Found the Egress PortID: %s", egressPortId)
109113

110-
g.By("Obtaining a not in use IP address from the EgressIP network (machineNetwork cidr)")
111-
machineNetworkID, err := getNetworkIdFromSubnetCidr(networkClient, egressIPNetCidrStr, infraID)
114+
g.By("Finding the openstack network ID from the egressPortId defined in openshift node annotation")
115+
machineNetworkID, err := getNetworkIdFromPortId(networkClient, egressPortId)
112116
o.Expect(err).NotTo(o.HaveOccurred())
113117
o.Expect(machineNetworkID).NotTo(o.BeEmpty(), "Could not get the EgressIP network ID in openstack for '%s' subnet CIDR", egressIPNetCidrStr)
114118
e2e.Logf("Found the EgressIP network ID '%s' in Openstack for the EgressIP CIDR '%s'", machineNetworkID, egressIPNetCidrStr)
@@ -124,29 +128,9 @@ var _ = g.Describe("[sig-installer][Suite:openshift/openstack][egressip] An egre
124128
e2e.Logf("Created '%s' temporary directory", egressIPTempDir)
125129
defer os.RemoveAll(egressIPTempDir)
126130

127-
g.By("Creating an EgressIP yaml file")
128-
var egressIPname = "egress-ip"
129-
var egressIPYamlFileName = "egressip.yaml"
130-
var egressIPYamlFilePath = egressIPTempDir + "/" + egressIPYamlFileName
131-
var egressIPYamlTemplate = `apiVersion: k8s.ovn.org/v1
132-
kind: EgressIP
133-
metadata:
134-
name: %s
135-
spec:
136-
egressIPs:
137-
- %s
138-
namespaceSelector:
139-
matchLabels:
140-
%s`
141-
142-
egressIPYaml := fmt.Sprintf(egressIPYamlTemplate, egressIPname, egressIPAddrStr, "app: egress")
143-
e2e.Logf("egressIPYaml: %s", egressIPYaml)
144-
145-
err = os.WriteFile(egressIPYamlFilePath, []byte(egressIPYaml), 0644)
146-
o.Expect(err).NotTo(o.HaveOccurred())
147-
148-
g.By(fmt.Sprintf("Creating an EgressIP object from '%s'", egressIPYamlFilePath))
149-
err = oc.AsAdmin().Run("create").Args("-f", egressIPYamlFilePath).Execute()
131+
g.By("Create egressIP resource in openshift")
132+
egressIPname := "egress-ip"
133+
err = createEgressIpResource(oc, egressIPname, egressIPAddrStr, "app: egress")
150134
o.Expect(err).NotTo(o.HaveOccurred())
151135
defer oc.AsAdmin().Run("delete").Args("egressip", egressIPname).Execute()
152136

@@ -228,6 +212,81 @@ spec:
228212
e2e.Logf("Found the expected Fixed IP (%s) for the FIP '%s'", egressIPAddrStr, fip.FloatingIP)
229213

230214
})
215+
216+
// https://issues.redhat.com/browse/OCPBUGS-27222
217+
g.It("with IPv6 format should be created on dualstack or ssipv6 cluster with OVN-Kubernetes NetworkType", func() {
218+
219+
networks, err := oc.AdminConfigClient().ConfigV1().Networks().Get(ctx, "cluster", metav1.GetOptions{})
220+
o.Expect(err).NotTo(o.HaveOccurred())
221+
dualstack, err := isDualStackCluster(networks.Status.ClusterNetwork)
222+
o.Expect(err).NotTo(o.HaveOccurred())
223+
ssipv6, err := isSingleStackIpv6Cluster(ctx, oc)
224+
o.Expect(err).NotTo(o.HaveOccurred())
225+
226+
if !(dualstack || ssipv6) {
227+
e2eskipper.Skipf("Test only applicable for dualstack or SingleStack IPv6 clusters")
228+
}
229+
230+
g.By("Getting the network type")
231+
networkType, err := getNetworkType(ctx, oc)
232+
o.Expect(err).NotTo(o.HaveOccurred())
233+
if networkType != NetworkTypeOVNKubernetes {
234+
e2eskipper.Skipf("Test not applicable for '%s' NetworkType (only valid for '%s')", networkType, NetworkTypeOVNKubernetes)
235+
}
236+
237+
e2e.Logf("Getting the worker for the EgressIP")
238+
worker := workerNodeList.Items[0]
239+
240+
g.By(fmt.Sprintf("Getting the EgressIP network from the '%s' annotation", egressIPConfigAnnotationKey))
241+
egressIPNetCidrStr, egressPortId, err := getEgressNetworkInfo(worker, "ipv6")
242+
o.Expect(err).NotTo(o.HaveOccurred())
243+
o.Expect(egressIPNetCidrStr).NotTo(o.BeEmpty(), "Could not get the EgressIP network from the '%s' annotation", egressIPConfigAnnotationKey)
244+
e2e.Logf("Found the EgressIP network: %s", egressIPNetCidrStr)
245+
o.Expect(egressPortId).NotTo(o.BeEmpty(), "Could not get the Egress openstack portId from the '%s' annotation", egressPortId)
246+
e2e.Logf("Found the Egress PortID: %s", egressPortId)
247+
248+
g.By("Finding the openstack network ID from the egressPortId defined in openshift node annotation")
249+
machineNetworkID, err := getNetworkIdFromPortId(networkClient, egressPortId)
250+
o.Expect(err).NotTo(o.HaveOccurred())
251+
o.Expect(machineNetworkID).NotTo(o.BeEmpty(), "Could not get the EgressIP network ID in openstack for '%s' subnet CIDR", egressIPNetCidrStr)
252+
e2e.Logf("Found the EgressIP network ID '%s' in Openstack for the EgressIP CIDR '%s'", machineNetworkID, egressIPNetCidrStr)
253+
254+
// Label the worker node as EgressIP assignable node
255+
g.By(fmt.Sprintf("Labeling the primary node '%s' with '%s'", worker.Name, egressAssignableLabelKey))
256+
node.AddOrUpdateLabelOnNode(clientSet, worker.Name, egressAssignableLabelKey, "dummy")
257+
defer node.RemoveLabelOffNode(clientSet, worker.Name, egressAssignableLabelKey)
258+
259+
g.By("Looking for a free IP in the subnet to use for the egressIP object in openshift")
260+
egressIPAddrStr, err := getNotInUseEgressIP(networkClient, egressIPNetCidrStr, machineNetworkID)
261+
o.Expect(err).NotTo(o.HaveOccurred())
262+
o.Expect(egressIPAddrStr).NotTo(o.BeEmpty(), "Couldn't find a free IP address in '%s' network in Openstack", egressIPNetCidrStr)
263+
o.Expect(isIPv6(egressIPAddrStr)).To(o.BeTrue(), "egressIP should be IPv6 but it's %q", egressIPAddrStr)
264+
e2e.Logf("Found '%s' free IP address in the EgressIP network in Openstack", egressIPAddrStr)
265+
266+
g.By("Create egressIP resource in openshift")
267+
egressIPname := "egress-ipv6"
268+
err = createEgressIpResource(oc, egressIPname, egressIPAddrStr, "app: egress")
269+
o.Expect(err).NotTo(o.HaveOccurred())
270+
defer oc.AsAdmin().Run("delete").Args("egressip", egressIPname).Execute()
271+
272+
g.By("Waiting until CloudPrivateIPConfig is created and assigned to the primary worker node")
273+
cloudNetworkClientset, err = cloudnetwork.NewForConfig(oc.AdminConfig())
274+
o.Expect(err).NotTo(o.HaveOccurred())
275+
egressIPAddr, err := netip.ParseAddr(egressIPAddrStr)
276+
o.Expect(err).NotTo(o.HaveOccurred())
277+
waitOk, err := waitCloudPrivateIPConfigAssignedNode(ctx, cloudNetworkClientset, strings.ReplaceAll(egressIPAddr.StringExpanded(), ":", "."), worker.Name)
278+
o.Expect(err).NotTo(o.HaveOccurred())
279+
o.Expect(waitOk).To(o.BeTrue(), "Not found the expected assigned node '%s' in '%s' CloudPrivateIPConfig", worker.Name, egressIPAddrStr)
280+
e2e.Logf("Found the expected assigned node '%s' in '%s' CloudPrivateIPConfig", worker.Name, egressIPAddrStr)
281+
282+
g.By("Checking that the port exists from openstack perspective")
283+
egressNetInUseIPs, err := getInUseIPs(networkClient, machineNetworkID)
284+
o.Expect(err).NotTo(o.HaveOccurred())
285+
o.Expect(egressNetInUseIPs).To(o.ContainElement(egressIPAddrStr))
286+
287+
g.By("Checking that the allowed_addresses_pairs are properly updated for the worker in the Openstack")
288+
checkAllowedAddressesPairs(networkClient, worker, corev1.Node{}, egressIPAddrStr, machineNetworkID)
289+
})
231290
})
232291

233292
// getNotInUseEgressIP returns a not in use IP address from the EgressIP network CIDR
@@ -248,8 +307,8 @@ func getNotInUseEgressIP(client *gophercloud.ServiceClient, egressCidr string, e
248307
return freeIP, nil
249308
}
250309

251-
// getEgressIPNetwork returns the IP address from the node egress-ipconfig annotation
252-
func getEgressIPNetwork(node corev1.Node) (string, error) {
310+
// getEgressNetworkInfo returns the IP address CIDR and openstack portId from the node egress-ipconfig annotation
311+
func getEgressNetworkInfo(node corev1.Node, ipVersion string) (string, string, error) {
253312
type ifAddr struct {
254313
IPv4 string `json:"ipv4,omitempty"`
255314
IPv6 string `json:"ipv6,omitempty"`
@@ -268,40 +327,39 @@ func getEgressIPNetwork(node corev1.Node) (string, error) {
268327
annotation, ok := node.Annotations[egressIPConfigAnnotationKey]
269328
if !ok {
270329
e2e.Logf("Annotation '%s' not found in '%s' node", egressIPConfigAnnotationKey, node.Name)
271-
return "", nil
330+
return "", "", nil
272331
}
273332
e2e.Logf("Found '%s' annotation in '%s': %s", egressIPConfigAnnotationKey, node.Name, annotation)
274333
var nodeEgressIPConfigs []*NodeEgressIPConfiguration
275334
err := json.Unmarshal([]byte(annotation), &nodeEgressIPConfigs)
276335
if err != nil {
277-
return "", err
336+
return "", "", err
278337
}
279-
egressIPNetStr := nodeEgressIPConfigs[0].IFAddr.IPv4
280-
if egressIPNetStr == "" {
281-
e2e.Logf("Empty ifaddr.ipv4 in the '%s' annotation", egressIPConfigAnnotationKey)
282-
return "", nil
338+
if ipVersion == "ipv4" {
339+
egressIPNetStr := nodeEgressIPConfigs[0].IFAddr.IPv4
340+
if egressIPNetStr == "" {
341+
e2e.Logf("Empty ifaddr.ipv4 in the '%s' annotation", egressIPConfigAnnotationKey)
342+
return "", "", nil
343+
}
344+
return egressIPNetStr, nodeEgressIPConfigs[0].Interface, nil
345+
} else if ipVersion == "ipv6" {
346+
egressIPNetStr := nodeEgressIPConfigs[0].IFAddr.IPv6
347+
if egressIPNetStr == "" {
348+
e2e.Logf("Empty ifaddr.ipv6 in the '%s' annotation", egressIPConfigAnnotationKey)
349+
return "", "", nil
350+
}
351+
return egressIPNetStr, nodeEgressIPConfigs[0].Interface, nil
283352
}
284-
return egressIPNetStr, nil
353+
return "", "", fmt.Errorf("ipVersion %s is not supported, only ipv4 and ipv6", ipVersion)
285354
}
286355

287-
// getNetworkIdFromSubnetCidr returns the Openstack network ID for a given Openstack subnet CIDR
288-
func getNetworkIdFromSubnetCidr(client *gophercloud.ServiceClient, subnetCidr string, infraID string) (string, error) {
289-
listOpts := subnets.ListOpts{
290-
CIDR: subnetCidr,
291-
Tags: "openshiftClusterID=" + infraID,
292-
}
293-
allPages, err := subnets.List(client, listOpts).AllPages()
294-
if err != nil {
295-
return "", fmt.Errorf("failed to get subnets")
296-
}
297-
allSubnets, err := subnets.ExtractSubnets(allPages)
356+
// getNetworkIdFromPortId returns the Openstack network ID for a given Openstack port
357+
func getNetworkIdFromPortId(client *gophercloud.ServiceClient, portId string) (string, error) {
358+
port, err := ports.Get(client, portId).Extract()
298359
if err != nil {
299-
return "", fmt.Errorf("failed to extract subnets")
360+
return "", fmt.Errorf("failed to get port %s: %w", portId, err)
300361
}
301-
if len(allSubnets) != 1 {
302-
return "", fmt.Errorf("unexpected number of subnets found with '%s' CIDR: %d subnets", subnetCidr, len(allSubnets))
303-
}
304-
return allSubnets[0].NetworkID, nil
362+
return port.NetworkID, nil
305363
}
306364

307365
// getInUseIPs returns the in use IPs in a given network ID in Openstack
@@ -443,17 +501,37 @@ func waitCloudPrivateIPConfigAssignedNode(ctx context.Context, cloudNetClientset
443501

444502
// Returns the list of IPs present on the openstack allowed_address_pairs attribute in the node main port
445503
func getAllowedIPsFromNode(client *gophercloud.ServiceClient, node corev1.Node, machineNetwork string) ([]string, error) {
504+
var nodeOpenStackPort *ports.Port
505+
var err error
446506

447-
result := []string{}
448-
ip := node.GetAnnotations()["alpha.kubernetes.io/provided-node-ip"]
449-
nodePorts, err := getPortsByIP(client, ip, machineNetwork)
450-
if err != nil {
451-
return nil, err
507+
// In a dualstack environment, the node can have both ipv4 and ipv6 addresses. We are looking for the port associated
508+
// to the machine network. We iterate over all the node's internal IPs until we find the one that has a port in the
509+
// machine network.
510+
for _, addr := range node.Status.Addresses {
511+
if addr.Type == corev1.NodeInternalIP {
512+
var nodePorts []ports.Port
513+
nodePorts, err = getPortsByIP(client, addr.Address, machineNetwork)
514+
if err != nil {
515+
// We can safely ignore the error because we are iterating over all the node's internal IPs, and we only
516+
// need to find one that has a port in the machine network.
517+
e2e.Logf("Can't find a port for IP %s in network %s, skipping.", addr.Address, machineNetwork)
518+
continue
519+
}
520+
// We expect to find only one port for the node's internal IP in the machine network.
521+
if len(nodePorts) == 1 {
522+
nodeOpenStackPort = &nodePorts[0]
523+
e2e.Logf("Found port %s for IP %s in network %s", nodeOpenStackPort.ID, addr.Address, machineNetwork)
524+
break
525+
}
526+
}
452527
}
453-
if len(nodePorts) != 1 {
454-
return nil, fmt.Errorf("unexpected number of openstack ports for IP %s", ip)
528+
529+
if nodeOpenStackPort == nil {
530+
return nil, fmt.Errorf("failed to find the node's port in the machine network %s", machineNetwork)
455531
}
456-
for _, addressPair := range nodePorts[0].AllowedAddressPairs {
532+
533+
result := []string{}
534+
for _, addressPair := range nodeOpenStackPort.AllowedAddressPairs {
457535
result = append(result, addressPair.IPAddress)
458536
}
459537
return result, nil
@@ -488,3 +566,64 @@ func checkAllowedAddressesPairs(client *gophercloud.ServiceClient, nodeHoldingEg
488566
}, "10s", "1s").Should(o.BeTrue(), "Timed out checking allowed address pairs for node %s", nodeNotHoldingEgressIp.Name)
489567
e2e.Logf("egressIp %s correctly not included on the node allowed-address-pairs for %s", egressIp, nodeNotHoldingEgressIp.Name)
490568
}
569+
570+
func createEgressIpResource(oc *exutil.CLI, egressIPname string, egressIPAddrStr string, labels string) error {
571+
572+
g.By("Creating a temp directory")
573+
egressIPTempDir, err := os.MkdirTemp("", "egressip-e2e")
574+
if err != nil {
575+
return err
576+
}
577+
e2e.Logf("Created '%s' temporary directory", egressIPTempDir)
578+
defer os.RemoveAll(egressIPTempDir)
579+
580+
g.By("Creating an EgressIP yaml file")
581+
var egressIPYamlFileName = "egressip.yaml"
582+
var egressIPYamlFilePath = egressIPTempDir + "/" + egressIPYamlFileName
583+
var egressIPYamlTemplate = `apiVersion: k8s.ovn.org/v1
584+
kind: EgressIP
585+
metadata:
586+
name: %s
587+
spec:
588+
egressIPs:
589+
- %s
590+
namespaceSelector:
591+
matchLabels:
592+
%s`
593+
594+
egressIPYaml := fmt.Sprintf(egressIPYamlTemplate, egressIPname, egressIPAddrStr, labels)
595+
e2e.Logf("egressIPYaml: %s", egressIPYaml)
596+
597+
err = os.WriteFile(egressIPYamlFilePath, []byte(egressIPYaml), 0644)
598+
if err != nil {
599+
return err
600+
}
601+
602+
g.By(fmt.Sprintf("Creating an EgressIP object from '%s'", egressIPYamlFilePath))
603+
err = oc.AsAdmin().Run("create").Args("-f", egressIPYamlFilePath).Execute()
604+
if err != nil {
605+
return err
606+
}
607+
return nil
608+
}
609+
610+
// isDualStackCluster returns true if the cluster is dual stack
611+
func isDualStackCluster(clusterNetwork []configv1.ClusterNetworkEntry) (bool, error) {
612+
return len(clusterNetwork) > 1, nil
613+
}
614+
615+
// isSingleStackIpv6Cluster returns true if the cluster is single stack IPv6
616+
func isSingleStackIpv6Cluster(ctx context.Context, oc *exutil.CLI) (bool, error) {
617+
networks, err := oc.AdminConfigClient().ConfigV1().Networks().Get(ctx, "cluster", metav1.GetOptions{})
618+
if err != nil {
619+
return false, err
620+
}
621+
if len(networks.Status.ClusterNetwork) == 1 && net.ParseIP(networks.Status.ClusterNetwork[0].CIDR).To4() == nil {
622+
return true, nil
623+
}
624+
return false, nil
625+
}
626+
627+
func isIPv6(ip string) bool {
628+
return net.ParseIP(ip) != nil && net.ParseIP(ip).To4() == nil
629+
}

0 commit comments

Comments
 (0)