-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Porting Duplicate MAC Test to Origin #31478
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ import ( | |||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||
| "io/ioutil" | ||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||
| "regexp" | ||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -567,6 +568,287 @@ var _ = g.Describe("[sig-network][Feature:EgressIP][apigroup:operator.openshift. | |||||||||||||||||||||||||||||||||||
| }) // end testing to external targets | ||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| var _ = g.Describe("[sig-network][Feature:EgressIP][apigroup:operator.openshift.io] EgressIP duplicate MAC prevention", func() { | ||||||||||||||||||||||||||||||||||||
| oc := exutil.NewCLIWithPodSecurityLevel("egressip-mac", admissionapi.LevelPrivileged) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||||||||||||||
| egressIPObjectName = "egressip-mac-test" | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||||||||||||||
| clientset kubernetes.Interface | ||||||||||||||||||||||||||||||||||||
| tmpDirEgressIP string | ||||||||||||||||||||||||||||||||||||
| workerNodesOrdered []corev1.Node | ||||||||||||||||||||||||||||||||||||
| workerNodesOrderedNames []string | ||||||||||||||||||||||||||||||||||||
| hasIPv4 bool | ||||||||||||||||||||||||||||||||||||
| hasIPv6 bool | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.BeforeEach(func() { | ||||||||||||||||||||||||||||||||||||
| g.By("Verifying that this cluster uses OVN-Kubernetes") | ||||||||||||||||||||||||||||||||||||
| if networkPluginName() != OVNKubernetesPluginName { | ||||||||||||||||||||||||||||||||||||
| skipper.Skipf("This cluster does not use OVN Kubernetes") | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("Checking platform type - this test requires L2 network adjacency") | ||||||||||||||||||||||||||||||||||||
| infra, err := oc.AdminConfigClient().ConfigV1().Infrastructures().Get(context.Background(), "cluster", metav1.GetOptions{}) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
| cloudType := infra.Spec.PlatformSpec.Type | ||||||||||||||||||||||||||||||||||||
| cloudPlatforms := []configv1.PlatformType{ | ||||||||||||||||||||||||||||||||||||
| configv1.AWSPlatformType, | ||||||||||||||||||||||||||||||||||||
| configv1.GCPPlatformType, | ||||||||||||||||||||||||||||||||||||
| configv1.AzurePlatformType, | ||||||||||||||||||||||||||||||||||||
| configv1.OpenStackPlatformType, | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| for _, cp := range cloudPlatforms { | ||||||||||||||||||||||||||||||||||||
| if cloudType == cp { | ||||||||||||||||||||||||||||||||||||
| skipper.Skipf("This test requires L2 network adjacency (baremetal); cloud platform %s is not supported", cloudType) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("Creating a temp directory") | ||||||||||||||||||||||||||||||||||||
| tmpDirEgressIP, err = ioutil.TempDir("", "egressip-mac-e2e") | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("Getting the kubernetes clientset") | ||||||||||||||||||||||||||||||||||||
| clientset = oc.KubeFramework().ClientSet | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("Getting all worker nodes in alphabetical order") | ||||||||||||||||||||||||||||||||||||
| workerNodesOrdered, err = getWorkerNodesOrdered(clientset) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
| workerNodesOrderedNames = nil | ||||||||||||||||||||||||||||||||||||
| for _, n := range workerNodesOrdered { | ||||||||||||||||||||||||||||||||||||
| workerNodesOrderedNames = append(workerNodesOrderedNames, n.Name) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| if len(workerNodesOrdered) < 3 { | ||||||||||||||||||||||||||||||||||||
| skipper.Skipf("This test requires minimum 3 worker nodes, found %d", len(workerNodesOrdered)) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("Determining the IP address families") | ||||||||||||||||||||||||||||||||||||
| hasIPv4, hasIPv6, err = GetIPAddressFamily(oc) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.AfterEach(func() { | ||||||||||||||||||||||||||||||||||||
| g.By("Deleting the EgressIP object if it exists") | ||||||||||||||||||||||||||||||||||||
| egressIPYamlPath := tmpDirEgressIP + "/" + egressIPYaml | ||||||||||||||||||||||||||||||||||||
| if _, err := os.Stat(egressIPYamlPath); err == nil { | ||||||||||||||||||||||||||||||||||||
| _, _ = runOcWithRetry(oc.AsAdmin(), "delete", "-f", egressIPYamlPath) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("Removing the egress-assignable labels from all worker nodes") | ||||||||||||||||||||||||||||||||||||
| for _, nodeName := range workerNodesOrderedNames { | ||||||||||||||||||||||||||||||||||||
| _, _ = runOcWithRetry(oc.AsAdmin(), "label", "node", nodeName, "k8s.ovn.org/egress-assignable-") | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("Removing the temp directory") | ||||||||||||||||||||||||||||||||||||
| os.RemoveAll(tmpDirEgressIP) | ||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.It("should prevent duplicate MAC responses when egress node is rebooted [Serial]", func() { | ||||||||||||||||||||||||||||||||||||
| // Node assignment: | ||||||||||||||||||||||||||||||||||||
| // workerNodesOrderedNames[0] = probe node (runs arping, NOT egress-assignable) | ||||||||||||||||||||||||||||||||||||
| // workerNodesOrderedNames[1] = egress node 1 (initial EgressIP holder) | ||||||||||||||||||||||||||||||||||||
| // workerNodesOrderedNames[2] = egress node 2 (failover target) | ||||||||||||||||||||||||||||||||||||
| probeNodeName := workerNodesOrderedNames[0] | ||||||||||||||||||||||||||||||||||||
| egressNode1Name := workerNodesOrderedNames[1] | ||||||||||||||||||||||||||||||||||||
| egressNode2Name := workerNodesOrderedNames[2] | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| isIPv6 := hasIPv6 && !hasIPv4 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("1. Labeling egress node 1 as egress-assignable") | ||||||||||||||||||||||||||||||||||||
| _, err := runOcWithRetry(oc.AsAdmin(), "label", "node", egressNode1Name, "k8s.ovn.org/egress-assignable=") | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("2. Allocating an EgressIP from egress node 1") | ||||||||||||||||||||||||||||||||||||
| nodeEgressIPMap, err := findNodeEgressIPsBaremetal(oc, clientset, []string{egressNode1Name}) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
| egressIPStr := nodeEgressIPMap[egressNode1Name][0] | ||||||||||||||||||||||||||||||||||||
| framework.Logf("Allocated EgressIP: %s for node %s", egressIPStr, egressNode1Name) | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+664
to
+667
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Assert that an EgressIP was allocated before indexing. Line 666 indexes 🛡️ Proposed change nodeEgressIPMap, err := findNodeEgressIPsBaremetal(oc, clientset, []string{egressNode1Name})
o.Expect(err).NotTo(o.HaveOccurred())
+ o.Expect(nodeEgressIPMap).To(o.HaveKey(egressNode1Name))
+ o.Expect(nodeEgressIPMap[egressNode1Name]).NotTo(o.BeEmpty(),
+ fmt.Sprintf("no free EgressIP found for node %s", egressNode1Name))
egressIPStr := nodeEgressIPMap[egressNode1Name][0]📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("3. Creating and applying the EgressIP object") | ||||||||||||||||||||||||||||||||||||
| egressIPYamlPath := tmpDirEgressIP + "/" + egressIPYaml | ||||||||||||||||||||||||||||||||||||
| egressIPSet := map[string]string{egressIPStr: egressNode1Name} | ||||||||||||||||||||||||||||||||||||
| createEgressIPObject(oc, egressIPYamlPath, egressIPObjectName, oc.Namespace(), "", egressIPSet) | ||||||||||||||||||||||||||||||||||||
| _, err = runOcWithRetry(oc.AsAdmin(), "create", "-f", egressIPYamlPath) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("4. Verifying EgressIP is assigned to egress node 1") | ||||||||||||||||||||||||||||||||||||
| var hasIP bool | ||||||||||||||||||||||||||||||||||||
| var assignedNode string | ||||||||||||||||||||||||||||||||||||
| o.Eventually(func() bool { | ||||||||||||||||||||||||||||||||||||
| hasIP, assignedNode, err = egressIPStatusHasIP(oc, egressIPObjectName, egressIPStr) | ||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||
| framework.Logf("Error checking EgressIP status: %v", err) | ||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| return hasIP && assignedNode == egressNode1Name | ||||||||||||||||||||||||||||||||||||
| }, 60*time.Second, 5*time.Second).Should(o.BeTrue(), | ||||||||||||||||||||||||||||||||||||
| fmt.Sprintf("EgressIP %s should be assigned to node %s", egressIPStr, egressNode1Name)) | ||||||||||||||||||||||||||||||||||||
| framework.Logf("EgressIP %s assigned to node: %s", egressIPStr, assignedNode) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("5. Labeling egress node 2 as egress-assignable for failover") | ||||||||||||||||||||||||||||||||||||
| _, err = runOcWithRetry(oc.AsAdmin(), "label", "node", egressNode2Name, "k8s.ovn.org/egress-assignable=") | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("6. Getting br-ex physical interface name") | ||||||||||||||||||||||||||||||||||||
| physInterface, err := findBridgePhysicalInterface(oc, egressNode1Name, "br-ex") | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
| framework.Logf("Using physical interface: %s", physInterface) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("7. Getting MAC addresses of egress node 1 and egress node 2") | ||||||||||||||||||||||||||||||||||||
| mac1, err := getNodeInterfaceMAC(oc, egressNode1Name, physInterface) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
| mac2, err := getNodeInterfaceMAC(oc, egressNode2Name, physInterface) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
| framework.Logf("Egress node 1 (%s) MAC: %s", egressNode1Name, mac1) | ||||||||||||||||||||||||||||||||||||
| framework.Logf("Egress node 2 (%s) MAC: %s", egressNode2Name, mac2) | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+694
to
+705
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Resolve the physical interface name per node. Line 695 discovers the Call 🐛 Proposed change- g.By("6. Getting br-ex physical interface name")
- physInterface, err := findBridgePhysicalInterface(oc, egressNode1Name, "br-ex")
- o.Expect(err).NotTo(o.HaveOccurred())
- framework.Logf("Using physical interface: %s", physInterface)
-
- g.By("7. Getting MAC addresses of egress node 1 and egress node 2")
- mac1, err := getNodeInterfaceMAC(oc, egressNode1Name, physInterface)
- o.Expect(err).NotTo(o.HaveOccurred())
- mac2, err := getNodeInterfaceMAC(oc, egressNode2Name, physInterface)
- o.Expect(err).NotTo(o.HaveOccurred())
+ g.By("6. Getting br-ex physical interface name on each node")
+ iface1, err := findBridgePhysicalInterface(oc, egressNode1Name, "br-ex")
+ o.Expect(err).NotTo(o.HaveOccurred())
+ iface2, err := findBridgePhysicalInterface(oc, egressNode2Name, "br-ex")
+ o.Expect(err).NotTo(o.HaveOccurred())
+ probeInterface, err := findBridgePhysicalInterface(oc, probeNodeName, "br-ex")
+ o.Expect(err).NotTo(o.HaveOccurred())
+
+ g.By("7. Getting MAC addresses of egress node 1 and egress node 2")
+ mac1, err := getNodeInterfaceMAC(oc, egressNode1Name, iface1)
+ o.Expect(err).NotTo(o.HaveOccurred())
+ mac2, err := getNodeInterfaceMAC(oc, egressNode2Name, iface2)
+ o.Expect(err).NotTo(o.HaveOccurred())Then pass 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("8. Verifying EgressIP resolves to egress node 1 MAC before migration") | ||||||||||||||||||||||||||||||||||||
| probePodInfo, err := ovnkubePod(oc, probeNodeName) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| var discoveryCmd string | ||||||||||||||||||||||||||||||||||||
| var macRegex *regexp.Regexp | ||||||||||||||||||||||||||||||||||||
| if isIPv6 { | ||||||||||||||||||||||||||||||||||||
| discoveryCmd = fmt.Sprintf("ndisc6 -1 -w 1000 %s %s 2>&1", egressIPStr, physInterface) | ||||||||||||||||||||||||||||||||||||
| macRegex = regexp.MustCompile(`Target link-layer address:\s+([0-9a-fA-F]{1,2}:[0-9a-fA-F]{1,2}:[0-9a-fA-F]{1,2}:[0-9a-fA-F]{1,2}:[0-9a-fA-F]{1,2}:[0-9a-fA-F]{1,2})`) | ||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||
| discoveryCmd = fmt.Sprintf("arping -c 1 -I %s %s 2>&1", physInterface, egressIPStr) | ||||||||||||||||||||||||||||||||||||
| macRegex = regexp.MustCompile(`\[([0-9a-fA-F:]+)\]`) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| output, err := adminExecInPod(oc, "openshift-ovn-kubernetes", probePodInfo.podName, probePodInfo.containerName, discoveryCmd) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred(), "network discovery should succeed before migration") | ||||||||||||||||||||||||||||||||||||
| matches := macRegex.FindStringSubmatch(output) | ||||||||||||||||||||||||||||||||||||
| o.Expect(matches).To(o.HaveLen(2), fmt.Sprintf("should extract MAC from discovery output: %s", output)) | ||||||||||||||||||||||||||||||||||||
| macBeforeMigration := strings.ToLower(strings.TrimSpace(matches[1])) | ||||||||||||||||||||||||||||||||||||
| framework.Logf("MAC before migration: %s, expected node 1 MAC: %s", macBeforeMigration, mac1) | ||||||||||||||||||||||||||||||||||||
| o.Expect(macBeforeMigration).To(o.Equal(mac1), "EgressIP should resolve to egress node 1 MAC before migration") | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+711
to
+726
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Determine which IP family findNodeEgressIPsBaremetal returns on dual-stack clusters.
ast-grep run --pattern 'func findNodeEgressIPsBaremetal($$$) { $$$ }' --lang go test/extended/networking/egressip_helpers.goRepository: openshift/origin Length of output: 154 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)egressip(_helpers)?\.go$|test/extended/(include\.go)$' || true
echo "== egressip.go outline =="
ast-grep outline test/extended/networking/egressip.go --match 'egress' --view expanded || true
echo "== helpers outline =="
if [ -f test/extended/networking/egressip_helpers.go ]; then
ast-grep outline test/extended/networking/egressip_helpers.go --view expanded || true
fi
echo "== relevant searches =="
rg -n "findNodeEgressIPsBaremetal|isIPv6|egressIPStr|hasIPv|baremetal|dual-stack|dual stack|dualstack" test/extended/networking/egressip.go test/extended/networking/egressip_helpers.go 2>/dev/null || trueRepository: openshift/origin Length of output: 11727 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== test context 620-810 =="
sed -n '620,810p' test/extended/networking/egressip.go
echo "== findNodeEgressIPs 882-940 =="
sed -n '882,940p' test/extended/networking/egressip_helpers.go
echo "== baremetal allocator 1834-1940 =="
sed -n '1834,1945p' test/extended/networking/egressip_helpers.go
echo "== checkForDuplicateMACOnNode 1767-1815 =="
sed -n '1767,1815p' test/extended/networking/egressip_helpers.go
echo "== behavioral probe: current dual-stack selector and address-family selection =="
python3 - <<'PY'
IP_STR = "fd12:3456:7890:abcd::1"
for is_ipv6 in [False, True]:
import subprocess
cmd = "arping -c 1 -I eth0 fd12:3456:7890:abcd::1 2>&1" if not is_ipv6 else "ndisc6 -1 -w 1000 fd12:3456:7890:abcd::1 eth0 2>&1"
res = subprocess.run(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
print(is_ipv6, res.returncode, res.stdout.splitlines()[0] if res.stdout else "")
PYRepository: openshift/origin Length of output: 16004 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== getFirstFreeIPs 982-1052 =="
sed -n '982,1052p' test/extended/networking/egressip_helpers.go
echo "== getNodeEgressIPConfiguration 1053-1070 =="
sed -n '1053,1070p' test/extended/networking/egressip_helpers.go
echo "== GetIPAddressFamily occurrences =="
rg -n "func GetIPAddressFamily|GetIPAddressFamily\\(" -S .
echo "== deterministic family selection behavior from source comments =="
python3 - <<'PY'
from pathlib import Path
p = Path('test/extended/networking/egressip.go')
text = p.read_text()
for key in ['hasIPv4, hasIPv6, err = GetIPAddressFamily(oc)', 'getFirstFreeIPs']:
print(key, text.find(key))
PYRepository: openshift/origin Length of output: 4921 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== util GetIPAddressFamily 550-630 =="
sed -n '550,630p' test/extended/networking/util.go
echo "== check duplicate family source around line 1486 util.go =="
sed -n '1470,1505p' test/extended/networking/util.go
echo "== getTargetProtocolHostPort 1630-1670 =="
sed -n '1630,1670p' test/extended/networking/egressip_helpers.goRepository: openshift/origin Length of output: 3969 🌐 Web query:
💡 Result: In OVN-Kubernetes, the cloud.network.openshift.io/egress-ipconfig annotation is used to provide metadata about a node's networking capabilities—specifically interface information, subnets (ifaddr), and IP address capacity—in public cloud environments (AWS, Azure, Google Cloud) [1][2][3][4]. This annotation allows the EgressIP controller to understand how many egress IPs can be assigned to a node and on which interface [1][3][4]. Regarding IPv4/IPv6 dual-stack and EgressIP allocation: 1. Dual-Stack Configuration: For clusters configured for dual-stack networking, a single EgressIP object can support both IPv4 and IPv6 addresses [1][2][3][5]. Red Hat does not support using two separate EgressIP objects to achieve dual-stack functionality (e.g., one for IPv4 and one for IPv6) [1][2][3][6]. 2. Egress IP Allocation: Egress IPs are assigned to nodes that have been labeled with k8s.ovn.org/egress-assignable="" [7][8][9][5]. The OVN-Kubernetes controller tracks these assignments [10][11]. While the term findNodeEgressIPsBaremetal is not a standard documented API or command, the assignment process involves the EgressIP controller (running in ovnkube-cluster-manager) periodically checking for reachable nodes with the required label to host the assigned egress IPs [8]. If a node is no longer reachable, the controller revalidates and moves the egress IPs to another usable node [8]. 3. Platform Support: While the cloud.network.openshift.io/egress-ipconfig annotation is specific to public cloud environments, the EgressIP feature is also supported on bare-metal clusters [9]. On bare-metal, the requirement to label nodes with k8s.ovn.org/egress-assignable="" remains the primary mechanism for directing where egress traffic is handled [7][9]. Citations:
Select the discovery command from the allocated EgressIP family.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("9. Getting ovnkube-node pod name on egress node 1") | ||||||||||||||||||||||||||||||||||||
| egressNode1PodInfo, err := ovnkubePod(oc, egressNode1Name) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
| framework.Logf("Found ovnkube-node pod: %s on node %s", egressNode1PodInfo.podName, egressNode1Name) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("10. Starting goroutine to monitor nftables chain creation during pod deletion") | ||||||||||||||||||||||||||||||||||||
| nftChainFound := make(chan bool, 1) | ||||||||||||||||||||||||||||||||||||
| stopChecking := make(chan bool, 1) | ||||||||||||||||||||||||||||||||||||
| goroutineReady := make(chan bool, 1) | ||||||||||||||||||||||||||||||||||||
| nftChainCheckCmd := "nft list chains 2>/dev/null | grep -q egressip-drop && echo FOUND || echo NOTFOUND" | ||||||||||||||||||||||||||||||||||||
| go func() { | ||||||||||||||||||||||||||||||||||||
| defer close(nftChainFound) | ||||||||||||||||||||||||||||||||||||
| goroutineReady <- true | ||||||||||||||||||||||||||||||||||||
| ticker := time.NewTicker(200 * time.Millisecond) | ||||||||||||||||||||||||||||||||||||
| defer ticker.Stop() | ||||||||||||||||||||||||||||||||||||
| for { | ||||||||||||||||||||||||||||||||||||
| select { | ||||||||||||||||||||||||||||||||||||
| case <-stopChecking: | ||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||
| case <-ticker.C: | ||||||||||||||||||||||||||||||||||||
| // Use oc debug to run on the node directly since the ovnkube-node pod may be terminating | ||||||||||||||||||||||||||||||||||||
| result, debugErr := oc.AsAdmin().Run("debug").Args( | ||||||||||||||||||||||||||||||||||||
| "node/"+egressNode1Name, | ||||||||||||||||||||||||||||||||||||
| "--", | ||||||||||||||||||||||||||||||||||||
| "chroot", "/host", | ||||||||||||||||||||||||||||||||||||
| "/bin/bash", "-c", | ||||||||||||||||||||||||||||||||||||
| nftChainCheckCmd, | ||||||||||||||||||||||||||||||||||||
| ).Output() | ||||||||||||||||||||||||||||||||||||
| if debugErr == nil && strings.Contains(result, "FOUND") { | ||||||||||||||||||||||||||||||||||||
| nftChainFound <- true | ||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||||||||||||||
| <-goroutineReady | ||||||||||||||||||||||||||||||||||||
| framework.Logf("Nftables chain monitoring goroutine started") | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+733
to
+764
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Inspect exutil.CLI for mutable state that makes concurrent use unsafe.
fd -t f 'cli.go' test/extended/util | xargs -r rg -nP -C3 '(func \(c \*CLI\) (AsAdmin|Run|Args|WithoutNamespace)|type CLI struct)'Repository: openshift/origin Length of output: 154 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate egressip.go and util files =="
fd -t f 'egressip.go|cli.go' test | sed -n '1,120p'
echo
echo "== locate CLI type/AsAdmin/Run/Args definitions =="
rg -n "type CLI struct|func \\(c \\*CLI\\) (AsAdmin|Run|Args|WithoutNamespace|SetNamespace)" test -SRepository: openshift/origin Length of output: 720 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== read egressip.go around goroutine and later oc/clientset usage =="
sed -n '700,810p' test/extended/networking/egressip.go
echo
echo "== inspect relevant CLI implementation files =="
for f in $(rg -l "type CLI struct|func \\(c \\*CLI\\) Run" test -S); do
echo "--- $f ---"
sed -n '1,220p' "$f"
doneRepository: openshift/origin Length of output: 13052 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== runOcc/Output/AsAdmin/SetNamespace implementation =="
sed -n '240,320p' test/extended/util/client.go
sed -n '860,1025p' test/extended/util/client.go
echo
echo "== occurrences of runOcWithRetry/Output usages in surrounding egressip.go =="
rg -n "runOcWithRetry|oc\\.Output\\(|oc\\.SetNamespace|\\.Output\\(" test/extended/networking/egressip.go test/extended/util/client.goRepository: openshift/origin Length of output: 9021 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== client.go outputs/runOcc implementation =="
sed -n '970,1055p' test/extended/util/client.go
echo
echo "== helper definitions in egressip.go =="
sed -n '150,205p' test/extended/networking/egressip.go
echo
echo "== read-only structural check: goroutine call chain and later runOcWithRetry/Output calls =="
python3 - <<'PY'
from pathlib import Path
p = Path('test/extended/networking/egressip.go')
s = p.read_text()
lines = s.splitlines()
calls = []
for i,l in enumerate(lines,1):
if 'go func()' in l or 'oc.AsAdmin().Run("debug")' in l or 'oc.AsAdmin()' in l:
calls.append((i,l.strip()))
print('\n'.join(f'{i}:{line}' for i,line in calls))
PYRepository: openshift/origin Length of output: 6750 Do not share one CLI instance between the monitor goroutine and the main test path. The goroutine and later test helpers both derive commands from 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("11. Deleting ovnkube-node pod on egress node 1 to trigger nftables rules and EgressIP migration") | ||||||||||||||||||||||||||||||||||||
| framework.Logf("Deleting ovnkube-node pod %s to trigger EgressIP migration", egressNode1PodInfo.podName) | ||||||||||||||||||||||||||||||||||||
| err = clientset.CoreV1().Pods("openshift-ovn-kubernetes").Delete(context.TODO(), egressNode1PodInfo.podName, metav1.DeleteOptions{}) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("12. Verifying nftables chain egressip-drop exists on egress node 1 during shutdown") | ||||||||||||||||||||||||||||||||||||
| select { | ||||||||||||||||||||||||||||||||||||
| case found := <-nftChainFound: | ||||||||||||||||||||||||||||||||||||
| o.Expect(found).To(o.BeTrue(), "nftables chain egressip-drop should be found on egress node 1") | ||||||||||||||||||||||||||||||||||||
| framework.Logf("Nftables chain egressip-drop verified on node %s", egressNode1Name) | ||||||||||||||||||||||||||||||||||||
| case <-time.After(60 * time.Second): | ||||||||||||||||||||||||||||||||||||
| close(stopChecking) | ||||||||||||||||||||||||||||||||||||
| framework.Failf("Timed out waiting for nftables chain egressip-drop on node %s", egressNode1Name) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| close(stopChecking) | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+771
to
+780
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Avoid the double close of The timeout branch closes Close the channel once with a 🐛 Proposed change <-goroutineReady
+ defer close(stopChecking)
framework.Logf("Nftables chain monitoring goroutine started")
@@
case <-time.After(60 * time.Second):
- close(stopChecking)
framework.Failf("Timed out waiting for nftables chain egressip-drop on node %s", egressNode1Name)
}
- close(stopChecking)🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("13. Waiting for EgressIP to migrate to egress node 2") | ||||||||||||||||||||||||||||||||||||
| o.Eventually(func() bool { | ||||||||||||||||||||||||||||||||||||
| hasIP, assignedNode, err = egressIPStatusHasIP(oc, egressIPObjectName, egressIPStr) | ||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||
| framework.Logf("Error checking EgressIP status: %v", err) | ||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| if hasIP && assignedNode == egressNode2Name { | ||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| framework.Logf("EgressIP %s still on node %s, waiting for migration to %s", egressIPStr, assignedNode, egressNode2Name) | ||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||
| }, 120*time.Second, 5*time.Second).Should(o.BeTrue(), | ||||||||||||||||||||||||||||||||||||
| fmt.Sprintf("EgressIP %s should migrate to node %s", egressIPStr, egressNode2Name)) | ||||||||||||||||||||||||||||||||||||
| framework.Logf("EgressIP successfully migrated to node %s", egressNode2Name) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("14. Checking for duplicate MAC responses after migration") | ||||||||||||||||||||||||||||||||||||
| err = checkForDuplicateMACOnNode( | ||||||||||||||||||||||||||||||||||||
| oc, | ||||||||||||||||||||||||||||||||||||
| probeNodeName, | ||||||||||||||||||||||||||||||||||||
| physInterface, | ||||||||||||||||||||||||||||||||||||
| egressIPStr, | ||||||||||||||||||||||||||||||||||||
| mac1, | ||||||||||||||||||||||||||||||||||||
| mac2, | ||||||||||||||||||||||||||||||||||||
| isIPv6, | ||||||||||||||||||||||||||||||||||||
| 20, | ||||||||||||||||||||||||||||||||||||
| 500*time.Millisecond, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred(), "duplicate MAC detection check failed") | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("15. Waiting for ovnkube-node pod to restart on egress node 1") | ||||||||||||||||||||||||||||||||||||
| o.Eventually(func() bool { | ||||||||||||||||||||||||||||||||||||
| pods, listErr := clientset.CoreV1().Pods("openshift-ovn-kubernetes").List(context.TODO(), metav1.ListOptions{ | ||||||||||||||||||||||||||||||||||||
| FieldSelector: fmt.Sprintf("spec.nodeName=%s", egressNode1Name), | ||||||||||||||||||||||||||||||||||||
| LabelSelector: "app=ovnkube-node", | ||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
| if listErr != nil { | ||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| for _, p := range pods.Items { | ||||||||||||||||||||||||||||||||||||
| if p.Status.Phase == corev1.PodRunning && p.DeletionTimestamp == nil { | ||||||||||||||||||||||||||||||||||||
| for _, c := range p.Status.ContainerStatuses { | ||||||||||||||||||||||||||||||||||||
| if c.Ready { | ||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||
| }, 120*time.Second, 5*time.Second).Should(o.BeTrue(), | ||||||||||||||||||||||||||||||||||||
| "ovnkube-node pod should restart on egress node 1") | ||||||||||||||||||||||||||||||||||||
| framework.Logf("ovnkube-node pod restarted on node %s", egressNode1Name) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| g.By("16. Verifying nftables cleanup on egress node 1 after pod restart") | ||||||||||||||||||||||||||||||||||||
| newPodInfo, err := ovnkubePod(oc, egressNode1Name) | ||||||||||||||||||||||||||||||||||||
| o.Expect(err).NotTo(o.HaveOccurred()) | ||||||||||||||||||||||||||||||||||||
| nftCheckCmd := "nft list table netdev ovn-kubernetes-egressip 2>&1" | ||||||||||||||||||||||||||||||||||||
| nftOutput, nftErr := adminExecInPod(oc, "openshift-ovn-kubernetes", newPodInfo.podName, newPodInfo.containerName, nftCheckCmd) | ||||||||||||||||||||||||||||||||||||
| if nftErr == nil { | ||||||||||||||||||||||||||||||||||||
| o.Expect(nftOutput).To(o.Or( | ||||||||||||||||||||||||||||||||||||
| o.ContainSubstring("No such file or directory"), | ||||||||||||||||||||||||||||||||||||
| o.ContainSubstring("No such file"), | ||||||||||||||||||||||||||||||||||||
| ), "nftables egress IP table should be deleted after cleanup") | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| framework.Logf("Nftables table cleaned up on node %s", egressNode1Name) | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+838
to
+846
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win The nftables cleanup check passes when the command fails.
Assert on the output in both cases, and treat an unexpected error as a failure. The two 🐛 Proposed change- nftCheckCmd := "nft list table netdev ovn-kubernetes-egressip 2>&1"
- nftOutput, nftErr := adminExecInPod(oc, "openshift-ovn-kubernetes", newPodInfo.podName, newPodInfo.containerName, nftCheckCmd)
- if nftErr == nil {
- o.Expect(nftOutput).To(o.Or(
- o.ContainSubstring("No such file or directory"),
- o.ContainSubstring("No such file"),
- ), "nftables egress IP table should be deleted after cleanup")
- }
+ // `nft list table` exits non-zero when the table is absent, so ignore the exit
+ // status and assert on the combined output instead.
+ nftCheckCmd := "nft list table netdev ovn-kubernetes-egressip 2>&1 || true"
+ nftOutput, nftErr := adminExecInPod(oc, "openshift-ovn-kubernetes", newPodInfo.podName, newPodInfo.containerName, nftCheckCmd)
+ o.Expect(nftErr).NotTo(o.HaveOccurred())
+ o.Expect(nftOutput).To(o.ContainSubstring("No such file"),
+ "nftables egress IP table should be deleted after cleanup")📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| framework.Logf("Test passed: EgressIP migrated cleanly without duplicate MAC responses") | ||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||
| // Functions to reduce code duplication below - those could also go into egressip_helpers.go, but they feel more appropriate here as they call | ||||||||||||||||||||||||||||||||||||
| // the various testing framework matchers such as o.Expect, etc. These functions also have no return value. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: openshift/origin
Length of output: 5204
🏁 Script executed:
Repository: openshift/origin
Length of output: 245
🏁 Script executed:
Repository: openshift/origin
Length of output: 21107
Read the platform type from
Status.PlatformStatus, notSpec.PlatformSpec.Spec.PlatformSpec.Typecan remain empty on installed clusters, so cloud platforms may not match the skip list and the test can run where L2 adjacency is unavailable. Useinfra.Status.PlatformStatus.Type, with a nilPlatformStatusguard.🤖 Prompt for AI Agents