Skip to content

Commit a4e3600

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

File tree

1 file changed

+199
-65
lines changed

1 file changed

+199
-65
lines changed

test/extended/openstack/egressip.go

Lines changed: 199 additions & 65 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

@@ -36,7 +38,6 @@ var _ = g.Describe("[sig-installer][Suite:openshift/openstack][egressip] An egre
3638
var networkClient *gophercloud.ServiceClient
3739
var clientSet *kubernetes.Clientset
3840
var err error
39-
var infraID string
4041
var workerNodeList *corev1.NodeList
4142
var cloudNetworkClientset cloudnetwork.Interface
4243
oc := exutil.NewCLI("openstack")
@@ -60,10 +61,6 @@ var _ = g.Describe("[sig-installer][Suite:openshift/openstack][egressip] An egre
6061
e2eskipper.Skipf("Test not applicable for proxy setup")
6162
}
6263

63-
infrastructure, err := oc.AdminConfigClient().ConfigV1().Infrastructures().Get(ctx, "cluster", metav1.GetOptions{})
64-
o.Expect(err).NotTo(o.HaveOccurred())
65-
infraID = infrastructure.Status.InfrastructureName
66-
6764
g.By("Getting the worker node list")
6865
workerNodeList, err = clientSet.CoreV1().Nodes().List(ctx, metav1.ListOptions{
6966
LabelSelector: "node-role.kubernetes.io/worker",
@@ -102,13 +99,15 @@ var _ = g.Describe("[sig-installer][Suite:openshift/openstack][egressip] An egre
10299
defer node.RemoveLabelOffNode(clientSet, primaryWorker.Name, egressAssignableLabelKey)
103100

104101
g.By(fmt.Sprintf("Getting the EgressIP network from the '%s' annotation", egressIPConfigAnnotationKey))
105-
egressIPNetCidrStr, err := getEgressIPNetwork(primaryWorker)
102+
egressIPNetCidrStr, egressPortId, err := getEgressNetworkInfo(primaryWorker, "ipv4")
106103
o.Expect(err).NotTo(o.HaveOccurred())
107104
o.Expect(egressIPNetCidrStr).NotTo(o.BeEmpty(), "Could not get the EgressIP network from the '%s' annotation", egressIPConfigAnnotationKey)
108105
e2e.Logf("Found the EgressIP network: %s", egressIPNetCidrStr)
106+
o.Expect(egressPortId).NotTo(o.BeEmpty(), "Could not get the Egress openstack portId from the '%s' annotation", egressIPConfigAnnotationKey)
107+
e2e.Logf("Found the Egress PortID: %s", egressPortId)
109108

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

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()
126+
g.By("Create egressIP resource in openshift")
127+
egressIPname := "egress-ip"
128+
err = createEgressIpResource(oc, egressIPname, egressIPAddrStr, "app: egress")
150129
o.Expect(err).NotTo(o.HaveOccurred())
151130
defer oc.AsAdmin().Run("delete").Args("egressip", egressIPname).Execute()
152131

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

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

233287
// getNotInUseEgressIP returns a not in use IP address from the EgressIP network CIDR
@@ -248,8 +302,8 @@ func getNotInUseEgressIP(client *gophercloud.ServiceClient, egressCidr string, e
248302
return freeIP, nil
249303
}
250304

251-
// getEgressIPNetwork returns the IP address from the node egress-ipconfig annotation
252-
func getEgressIPNetwork(node corev1.Node) (string, error) {
305+
// getEgressNetworkInfo returns the IP address CIDR and openstack portId from the node egress-ipconfig annotation
306+
func getEgressNetworkInfo(node corev1.Node, ipVersion string) (string, string, error) {
253307
type ifAddr struct {
254308
IPv4 string `json:"ipv4,omitempty"`
255309
IPv6 string `json:"ipv6,omitempty"`
@@ -268,40 +322,39 @@ func getEgressIPNetwork(node corev1.Node) (string, error) {
268322
annotation, ok := node.Annotations[egressIPConfigAnnotationKey]
269323
if !ok {
270324
e2e.Logf("Annotation '%s' not found in '%s' node", egressIPConfigAnnotationKey, node.Name)
271-
return "", nil
325+
return "", "", nil
272326
}
273327
e2e.Logf("Found '%s' annotation in '%s': %s", egressIPConfigAnnotationKey, node.Name, annotation)
274328
var nodeEgressIPConfigs []*NodeEgressIPConfiguration
275329
err := json.Unmarshal([]byte(annotation), &nodeEgressIPConfigs)
276330
if err != nil {
277-
return "", err
331+
return "", "", err
278332
}
279-
egressIPNetStr := nodeEgressIPConfigs[0].IFAddr.IPv4
280-
if egressIPNetStr == "" {
281-
e2e.Logf("Empty ifaddr.ipv4 in the '%s' annotation", egressIPConfigAnnotationKey)
282-
return "", nil
333+
if ipVersion == "ipv4" {
334+
egressIPNetStr := nodeEgressIPConfigs[0].IFAddr.IPv4
335+
if egressIPNetStr == "" {
336+
e2e.Logf("Empty ifaddr.ipv4 in the '%s' annotation", egressIPConfigAnnotationKey)
337+
return "", "", nil
338+
}
339+
return egressIPNetStr, nodeEgressIPConfigs[0].Interface, nil
340+
} else if ipVersion == "ipv6" {
341+
egressIPNetStr := nodeEgressIPConfigs[0].IFAddr.IPv6
342+
if egressIPNetStr == "" {
343+
e2e.Logf("Empty ifaddr.ipv6 in the '%s' annotation", egressIPConfigAnnotationKey)
344+
return "", "", nil
345+
}
346+
return egressIPNetStr, nodeEgressIPConfigs[0].Interface, nil
283347
}
284-
return egressIPNetStr, nil
348+
return "", "", fmt.Errorf("ipVersion %s is not supported, only ipv4 and ipv6", ipVersion)
285349
}
286350

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)
351+
// getNetworkIdFromPortId returns the Openstack network ID for a given Openstack port
352+
func getNetworkIdFromPortId(client *gophercloud.ServiceClient, portId string) (string, error) {
353+
port, err := ports.Get(client, portId).Extract()
298354
if err != nil {
299-
return "", fmt.Errorf("failed to extract subnets")
355+
return "", fmt.Errorf("failed to get port %s: %w", portId, err)
300356
}
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
357+
return port.NetworkID, nil
305358
}
306359

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

444497
// Returns the list of IPs present on the openstack allowed_address_pairs attribute in the node main port
445498
func getAllowedIPsFromNode(client *gophercloud.ServiceClient, node corev1.Node, machineNetwork string) ([]string, error) {
499+
var nodeOpenStackPort *ports.Port
500+
var err error
446501

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

0 commit comments

Comments
 (0)