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