@@ -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
445503func 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