From 4fd98981ac42d0aef94129f61810d83b072456cd Mon Sep 17 00:00:00 2001 From: tusharjadhav3302 Date: Tue, 23 Jun 2026 13:56:49 +0530 Subject: [PATCH 1/3] Add IPv6 secondary interface support (prepare + CNO configuration) OSPRH-6486 Co-authored-by: Cursor --- .../cleanup/tasks/cleanup_ipv6_secondary.yml | 36 ++++ .../stages/roles/cleanup/tasks/main.yml | 4 + .../files/worker-machineconfig-dhcp.yaml | 13 ++ .../procedures/configure_ipv6_secondary.yml | 176 ++++++++++++++++++ .../procedures/verify_ipv6_connectivity.yml | 55 ++++++ .../verify_ipv6_external_reachability.yml | 41 ++++ .../day2ops/templates/ipv6-deployment.yml.j2 | 41 ++++ .../day2ops/templates/network-macvlan.yml.j2 | 14 ++ .../templates/install-config-ipi.yaml.j2 | 3 + .../tasks/create_ipv6_secondary_networks.yml | 53 ++++++ .../stages/roles/prepare/tasks/main.yml | 6 +- configs/global.yml | 18 ++ .../osp_verification_ipv6_secondary.yaml | 58 ++++++ 13 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 collection/stages/roles/cleanup/tasks/cleanup_ipv6_secondary.yml create mode 100644 collection/stages/roles/day2ops/files/worker-machineconfig-dhcp.yaml create mode 100644 collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml create mode 100644 collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml create mode 100644 collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_external_reachability.yml create mode 100644 collection/stages/roles/day2ops/templates/ipv6-deployment.yml.j2 create mode 100644 collection/stages/roles/day2ops/templates/network-macvlan.yml.j2 create mode 100644 collection/stages/roles/prepare/tasks/create_ipv6_secondary_networks.yml create mode 100644 jobs_definitions/osp_verification_ipv6_secondary.yaml diff --git a/collection/stages/roles/cleanup/tasks/cleanup_ipv6_secondary.yml b/collection/stages/roles/cleanup/tasks/cleanup_ipv6_secondary.yml new file mode 100644 index 00000000..99da6493 --- /dev/null +++ b/collection/stages/roles/cleanup/tasks/cleanup_ipv6_secondary.yml @@ -0,0 +1,36 @@ +--- +- name: Check if IPv6 secondary resources exist (via resources file) + ansible.builtin.include_vars: + file: "{{ resources_file }}" + name: registered_resources + ignore_errors: true # noqa: ignore-errors + register: resources_load + +- name: Cleanup IPv6 secondary OpenStack resources + when: + - resources_load is succeeded + - registered_resources.ipv6_secondary_router_name is defined + block: + - name: Detach IPv6 secondary subnets from router + ansible.builtin.shell: > + openstack router remove subnet {{ registered_resources.ipv6_secondary_router_name }} {{ item }} + loop: "{{ registered_resources.ipv6_secondary_subnet_ids | default([]) }}" + environment: + OS_CLOUD: "{{ user_cloud }}" + changed_when: true + ignore_errors: true # noqa: ignore-errors + + - name: Delete IPv6 secondary router + openstack.cloud.router: + cloud: "{{ user_cloud }}" + state: absent + name: "{{ registered_resources.ipv6_secondary_router_name }}" + ignore_errors: true # noqa: ignore-errors + + - name: Delete IPv6 secondary networks + openstack.cloud.network: + cloud: "{{ user_cloud }}" + state: absent + name: "{{ item.net_name }}" + loop: "{{ ipv6_secondary_networks.networks }}" + ignore_errors: true # noqa: ignore-errors diff --git a/collection/stages/roles/cleanup/tasks/main.yml b/collection/stages/roles/cleanup/tasks/main.yml index 60dfb2c6..6a21a643 100644 --- a/collection/stages/roles/cleanup/tasks/main.yml +++ b/collection/stages/roles/cleanup/tasks/main.yml @@ -68,6 +68,10 @@ - name: Discover the OpenShift installation type ansible.builtin.include_tasks: detect_ocp_installation.yml + - name: Cleanup IPv6 secondary network resources if they exist + ansible.builtin.include_tasks: cleanup_ipv6_secondary.yml + when: ocp_deployment_topology.secondary_ip_protocol | default('') == 'ipv6' + - name: Destroy the OpenShift cluster if it exists ansible.builtin.include_tasks: destroy_openshift_cluster.yml when: existing_ocp_installation_type != '' diff --git a/collection/stages/roles/day2ops/files/worker-machineconfig-dhcp.yaml b/collection/stages/roles/day2ops/files/worker-machineconfig-dhcp.yaml new file mode 100644 index 00000000..571ccf1f --- /dev/null +++ b/collection/stages/roles/day2ops/files/worker-machineconfig-dhcp.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: machineconfiguration.openshift.io/v1 +kind: MachineConfig +metadata: + labels: + machineconfiguration.openshift.io/role: worker + name: 05-worker-kernelarg-dhcp +spec: + config: + ignition: + version: 3.2.0 + kernelArguments: + - ip=dhcp,dhcp6 diff --git a/collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml b/collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml new file mode 100644 index 00000000..ad92f696 --- /dev/null +++ b/collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml @@ -0,0 +1,176 @@ +--- +- name: Apply DHCP MachineConfig to workers + kubernetes.core.k8s: + kubeconfig: "{{ kubeconfig }}" + state: present + definition: "{{ lookup('file', '../files/worker-machineconfig-dhcp.yaml') | from_yaml }}" + +- name: Wait for the MCP to finish the cluster updates + ansible.builtin.include_role: + name: tools_cluster_checks + tasks_from: wait_mcp_updated.yml + vars: + wait_retries: 60 + wait_delay: 60 + +- name: Active wait until all the ClusterOperators are ready + ansible.builtin.include_role: + name: tools_cluster_checks + tasks_from: wait_until_cluster_operators_ready.yml + +- name: Wait until OCP cluster is healthy + ansible.builtin.include_role: + name: tools_cluster_checks + tasks_from: wait_until_cluster_is_healthy.yml + +- name: Get OCP worker nodes + kubernetes.core.k8s_info: + kubeconfig: "{{ kubeconfig }}" + api_version: v1 + kind: Node + label_selectors: + - node-role.kubernetes.io/worker + register: workers + +- name: Store the OCP worker node names + ansible.builtin.set_fact: + ocp_workers: "{{ workers | json_query('resources[*].metadata.name') | sort }}" + num_workers: "{{ workers.resources | length }}" + +- name: Discover IPv6 interfaces on first worker + ansible.builtin.shell: | + set -o pipefail && \ + oc adm node-logs {{ ocp_workers[0] }} | \ + grep '{{ item.cidr | regex_replace('::/64$', '') }}' | \ + grep dev | \ + sed 's/.*{{ item.cidr | regex_replace('::/64$', '') }}::.* dev \(\S\+\).*/\1/g' | \ + tail -1 + environment: + KUBECONFIG: "{{ kubeconfig }}" + loop: "{{ ipv6_secondary_networks.networks }}" + register: ipv6_interfaces_results + changed_when: false + retries: 10 + delay: 30 + until: ipv6_interfaces_results.stdout != "" + +- name: Store IPv6 interface names + ansible.builtin.set_fact: + ipv6_interfaces: "{{ ipv6_interfaces_results.results | map(attribute='stdout') | list }}" + +- name: Display discovered IPv6 interfaces + ansible.builtin.debug: + var: ipv6_interfaces + +- name: Template CNO macvlan patch + ansible.builtin.template: + src: network-macvlan.yml.j2 + dest: "/tmp/network-macvlan.yml" + mode: u=rw,g=rw,o=r + +- name: Create OCP projects for IPv6 network testing + kubernetes.core.k8s: + kubeconfig: "{{ kubeconfig }}" + api_version: project.openshift.io/v1 + kind: Project + name: "{{ item }}" + state: present + loop: "{{ ipv6_secondary_networks.projects }}" + +- name: Patch CNO with macvlan additionalNetworks + ansible.builtin.shell: | + set -o pipefail && \ + oc patch network.operator cluster --patch "$(cat /tmp/network-macvlan.yml)" --type merge + environment: + KUBECONFIG: "{{ kubeconfig }}" + changed_when: true + +- name: Verify NetworkAttachmentDefinition CRs exist + kubernetes.core.k8s_info: + kubeconfig: "{{ kubeconfig }}" + api_version: k8s.cni.cncf.io/v1 + kind: NetworkAttachmentDefinition + register: network_attachments + until: + - network_attachments is defined + - network_attachments is not failed + - network_attachments | json_query('resources[*].metadata.name') | list | sort == ipv6_secondary_networks.projects | sort + retries: 10 + delay: 30 + +- name: Template IPv6 test deployments + ansible.builtin.template: + src: ipv6-deployment.yml.j2 + dest: "/tmp/ipv6-deployment-{{ item }}.yml" + mode: u=rw,g=rw,o=r + vars: + ipv6_project: "{{ item }}" + loop: "{{ ipv6_secondary_networks.projects }}" + +- name: Deploy hello-openshift pods with IPv6 network annotation + kubernetes.core.k8s: + kubeconfig: "{{ kubeconfig }}" + state: present + src: "/tmp/ipv6-deployment-{{ item }}.yml" + wait: true + wait_timeout: 300 + namespace: "{{ item }}" + loop: "{{ ipv6_secondary_networks.projects }}" + +- name: Wait for pods to be ready in each IPv6 namespace + kubernetes.core.k8s_info: + kubeconfig: "{{ kubeconfig }}" + kind: Pod + namespace: "{{ item }}" + label_selectors: + - app=hello-openshift + field_selectors: + - status.phase=Running + register: running_pods + until: running_pods.resources | length == num_workers | int + retries: 20 + delay: 15 + loop: "{{ ipv6_secondary_networks.projects }}" + +- name: Get IPv6 network IDs for port security operations + openstack.cloud.networks_info: + cloud: "{{ user_cloud }}" + register: all_openstack_networks + +- name: Build list of IPv6 secondary network IDs + ansible.builtin.set_fact: + ipv6_net_ids: "{{ all_openstack_networks.networks | selectattr('name', 'in', ipv6_secondary_networks.networks | map(attribute='net_name') | list) | map(attribute='id') | list }}" + +- name: Get worker ports on IPv6 secondary networks + openstack.cloud.port_info: + cloud: "{{ user_cloud }}" + filters: + device_owner: compute:nova + register: all_compute_ports + +- name: Filter worker ports on IPv6 networks + ansible.builtin.set_fact: + ipv6_worker_ports: "{{ all_compute_ports.ports | selectattr('network_id', 'in', ipv6_net_ids) | list }}" + +- name: Disable port security on worker IPv6 ports + openstack.cloud.port: + cloud: "{{ user_cloud }}" + state: present + name: "{{ item.id }}" + port_security_enabled: false + no_security_groups: true + loop: "{{ ipv6_worker_ports }}" + loop_control: + label: "{{ item.id }}" + +- name: Verify pod-to-pod IPv6 connectivity on each network + ansible.builtin.include_tasks: procedures/verify_ipv6_connectivity.yml + loop: "{{ ipv6_secondary_networks.projects }}" + loop_control: + loop_var: ipv6_namespace + +- name: Verify external IPv6 reachability from worker nodes + ansible.builtin.include_tasks: procedures/verify_ipv6_external_reachability.yml + loop: "{{ ipv6_secondary_networks.projects }}" + loop_control: + loop_var: ipv6_namespace diff --git a/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml b/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml new file mode 100644 index 00000000..c9b46f9c --- /dev/null +++ b/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml @@ -0,0 +1,55 @@ +--- +- name: Get pods in namespace {{ ipv6_namespace }} + kubernetes.core.k8s_info: + kubeconfig: "{{ kubeconfig }}" + kind: Pod + namespace: "{{ ipv6_namespace }}" + label_selectors: + - app=hello-openshift + field_selectors: + - status.phase=Running + register: ipv6_pods + +- name: Store pod names for {{ ipv6_namespace }} + ansible.builtin.set_fact: + ipv6_pod_names: "{{ ipv6_pods.resources | map(attribute='metadata.name') | list }}" + +- name: Get pod IPv6 addresses on net1 interface + ansible.builtin.shell: | + set -o pipefail && \ + oc exec {{ item }} -n {{ ipv6_namespace }} -- ip -6 addr show dev net1 scope global | \ + awk '/inet6/{print $2}' | cut -f1 -d'/' + environment: + KUBECONFIG: "{{ kubeconfig }}" + loop: "{{ ipv6_pod_names }}" + register: pod_ipv6_results + changed_when: false + retries: 20 + delay: 30 + until: pod_ipv6_results.stdout != "" + +- name: Build pod name to IPv6 address mapping + ansible.builtin.set_fact: + ipv6_pod_ips: "{{ ipv6_pod_ips | default({}) | combine({item.item: item.stdout}) }}" + loop: "{{ pod_ipv6_results.results }}" + loop_control: + label: "{{ item.item }}" + +- name: Display pod IPv6 addresses for {{ ipv6_namespace }} + ansible.builtin.debug: + var: ipv6_pod_ips + +- name: Check pod-to-pod IPv6 connectivity in {{ ipv6_namespace }} + ansible.builtin.shell: | + oc exec {{ item[0] }} -n {{ ipv6_namespace }} -- /bin/curl -s --connect-timeout 10 http://[{{ item[1] }}]:8080 + environment: + KUBECONFIG: "{{ kubeconfig }}" + with_nested: + - "{{ ipv6_pod_ips.keys() | list }}" + - "{{ ipv6_pod_ips.values() | list }}" + when: ipv6_pod_ips[item[0]] != item[1] + register: connectivity_check + changed_when: false + retries: 5 + delay: 10 + until: connectivity_check.stdout is search('Hello OpenShift!') diff --git a/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_external_reachability.yml b/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_external_reachability.yml new file mode 100644 index 00000000..ba0df67a --- /dev/null +++ b/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_external_reachability.yml @@ -0,0 +1,41 @@ +--- +- name: Get pods in namespace {{ ipv6_namespace }} for external reachability test + kubernetes.core.k8s_info: + kubeconfig: "{{ kubeconfig }}" + kind: Pod + namespace: "{{ ipv6_namespace }}" + label_selectors: + - app=hello-openshift + field_selectors: + - status.phase=Running + register: ipv6_ext_pods + +- name: Store pod names for external test + ansible.builtin.set_fact: + ipv6_ext_pod_names: "{{ ipv6_ext_pods.resources | map(attribute='metadata.name') | list }}" + +- name: Get pod IPv6 addresses for external test + ansible.builtin.shell: | + set -o pipefail && \ + oc exec {{ item }} -n {{ ipv6_namespace }} -- ip -6 addr show dev net1 scope global | \ + awk '/inet6/{print $2}' | cut -f1 -d'/' + environment: + KUBECONFIG: "{{ kubeconfig }}" + loop: "{{ ipv6_ext_pod_names }}" + register: ext_pod_ipv6_results + changed_when: false + +- name: Verify external IPv6 reachability from worker node to pods in {{ ipv6_namespace }} + ansible.builtin.shell: | + oc debug node/{{ ocp_workers[0] }} -- chroot /host curl -s --connect-timeout 10 http://[{{ item.stdout }}]:8080 + environment: + KUBECONFIG: "{{ kubeconfig }}" + loop: "{{ ext_pod_ipv6_results.results }}" + loop_control: + label: "{{ item.item }}" + when: item.stdout != "" + register: external_check + changed_when: false + retries: 5 + delay: 10 + until: external_check.stdout is search('Hello OpenShift!') diff --git a/collection/stages/roles/day2ops/templates/ipv6-deployment.yml.j2 b/collection/stages/roles/day2ops/templates/ipv6-deployment.yml.j2 new file mode 100644 index 00000000..b11781bc --- /dev/null +++ b/collection/stages/roles/day2ops/templates/ipv6-deployment.yml.j2 @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello-openshift + namespace: {{ ipv6_project }} +spec: + replicas: {{ num_workers }} + selector: + matchLabels: + app: hello-openshift + template: + metadata: + labels: + app: hello-openshift + annotations: + k8s.v1.cni.cncf.io/networks: "{{ ipv6_project }}" + spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app + operator: In + values: + - hello-openshift + topologyKey: kubernetes.io/hostname + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: hello-openshift + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + image: quay.io/openshift/origin-hello-openshift + ports: + - containerPort: 8080 diff --git a/collection/stages/roles/day2ops/templates/network-macvlan.yml.j2 b/collection/stages/roles/day2ops/templates/network-macvlan.yml.j2 new file mode 100644 index 00000000..4c8d6f4d --- /dev/null +++ b/collection/stages/roles/day2ops/templates/network-macvlan.yml.j2 @@ -0,0 +1,14 @@ +spec: + additionalNetworks: +{% for network in ipv6_secondary_networks.networks %} + - name: {{ network.net_name }} + namespace: {{ network.net_name }} + rawCNIConfig: |- + { + "cniVersion": "0.3.1", + "name": "{{ network.net_name }}", + "type": "macvlan", + "master": "{{ ipv6_interfaces[loop.index0] }}" + } + type: Raw +{% endfor %} diff --git a/collection/stages/roles/install/templates/install-config-ipi.yaml.j2 b/collection/stages/roles/install/templates/install-config-ipi.yaml.j2 index 79c8c68b..81ee14a6 100644 --- a/collection/stages/roles/install/templates/install-config-ipi.yaml.j2 +++ b/collection/stages/roles/install/templates/install-config-ipi.yaml.j2 @@ -17,6 +17,9 @@ compute: openstack: type: "{{ installcfg_worker_flavor }}" serverGroupPolicy: "{{ installcfg_worker_servergrouppolicy }}" + {%- if ocp_deployment_topology.secondary_ip_protocol == 'ipv6' +%} + additionalNetworkIDs: {{ ipv6_secondary_network_ids | to_json }} + {%- endif +%} replicas: {{ installcfg_worker_replicas }} metadata: name: "{{ installcfg_cluster_name }}" diff --git a/collection/stages/roles/prepare/tasks/create_ipv6_secondary_networks.yml b/collection/stages/roles/prepare/tasks/create_ipv6_secondary_networks.yml new file mode 100644 index 00000000..a9787e3e --- /dev/null +++ b/collection/stages/roles/prepare/tasks/create_ipv6_secondary_networks.yml @@ -0,0 +1,53 @@ +--- +- name: Create IPv6 secondary networks for additional worker interfaces + openstack.cloud.network: + cloud: "{{ user_cloud }}" + state: present + name: "{{ item.net_name }}" + loop: "{{ ipv6_secondary_networks.networks }}" + register: ipv6_secondary_networks_created + +- name: Store IPv6 secondary network IDs + ansible.builtin.set_fact: + ipv6_secondary_network_ids: "{{ ipv6_secondary_networks_created.results | map(attribute='network.id') | list }}" + +- name: Create IPv6 secondary subnets + openstack.cloud.subnet: + cloud: "{{ user_cloud }}" + state: present + name: "{{ item.subnet_name }}" + network_name: "{{ item.net_name }}" + ip_version: 6 + cidr: "{{ item.cidr }}" + ipv6_ra_mode: "{{ item.ipv6_ra_mode }}" + ipv6_address_mode: "{{ item.ipv6_address_mode }}" + loop: "{{ ipv6_secondary_networks.networks }}" + register: ipv6_secondary_subnets_created + +- name: Store IPv6 secondary subnet IDs + ansible.builtin.set_fact: + ipv6_secondary_subnet_ids: "{{ ipv6_secondary_subnets_created.results | map(attribute='subnet.id') | list }}" + +- name: Create router for IPv6 secondary networks + openstack.cloud.router: + cloud: "{{ user_cloud }}" + state: present + name: "{{ ipv6_secondary_networks.router_name }}" + register: ipv6_secondary_router + +- name: Attach IPv6 secondary subnets to router + ansible.builtin.shell: > + openstack router add subnet {{ ipv6_secondary_networks.router_name }} {{ item }} + loop: "{{ ipv6_secondary_subnet_ids }}" + environment: + OS_CLOUD: "{{ user_cloud }}" + changed_when: true + +- name: Register IPv6 secondary network resources for cleanup + ansible.builtin.include_role: + name: shiftstack.tools.tools_register_resources_file + vars: + input: + ipv6_secondary_network_ids: "{{ ipv6_secondary_network_ids }}" + ipv6_secondary_subnet_ids: "{{ ipv6_secondary_subnet_ids }}" + ipv6_secondary_router_name: "{{ ipv6_secondary_networks.router_name }}" diff --git a/collection/stages/roles/prepare/tasks/main.yml b/collection/stages/roles/prepare/tasks/main.yml index afedf7f2..745c99f2 100644 --- a/collection/stages/roles/prepare/tasks/main.yml +++ b/collection/stages/roles/prepare/tasks/main.yml @@ -2,7 +2,7 @@ - name: Validate openshift topology network configuration vars: primary_ip_protocol: "{{ ocp_deployment_topology.primary_ip_protocol | default('') }}" - network_secondary: "{{ ocp_deployment_topology.network_secondary | default('') }}" + network_secondary: "{{ ocp_deployment_topology.secondary_ip_protocol | default('') }}" ansible.builtin.assert: that: - primary_ip_protocol in ['ipv4', 'ipv6'] @@ -44,6 +44,10 @@ - name: Prepare setup for accessing the OCP Cluster ansible.builtin.include_tasks: external_access.yml +- name: Create IPv6 secondary interface networks + ansible.builtin.include_tasks: create_ipv6_secondary_networks.yml + when: ocp_deployment_topology.secondary_ip_protocol == 'ipv6' + - name: Deploy and configure an installer host VM and update the inventory ansible.builtin.include_role: name: tools_installer_host diff --git a/configs/global.yml b/configs/global.yml index 819c6f2f..fde8871b 100644 --- a/configs/global.yml +++ b/configs/global.yml @@ -167,3 +167,21 @@ upi_playbooks_logs_dir: "{{ upi_playbooks_dir }}/playbooks_logs" tests: default_go_version_target: '1.22.5' + +# IPv6 Secondary Interface Networks (used with secondary_ip_protocol: ipv6) +ipv6_secondary_networks: + router_name: 'ipv6-secondary-router' + networks: + - net_name: slaac + subnet_name: subnet-slaac + cidr: '2001:db8:2222:5555::/64' + ipv6_ra_mode: 'slaac' + ipv6_address_mode: 'slaac' + - net_name: dhcpstateless + subnet_name: subnet-dhcpstateless + cidr: '2001:db8:2222:6666::/64' + ipv6_ra_mode: 'dhcpv6-stateless' + ipv6_address_mode: 'dhcpv6-stateless' + projects: + - slaac + - dhcpstateless diff --git a/jobs_definitions/osp_verification_ipv6_secondary.yaml b/jobs_definitions/osp_verification_ipv6_secondary.yaml new file mode 100644 index 00000000..f66e2061 --- /dev/null +++ b/jobs_definitions/osp_verification_ipv6_secondary.yaml @@ -0,0 +1,58 @@ +--- +# +# * OSP VERIFICATION - IPv6 SECONDARY INTERFACE * +# +# - Validates IPv6 secondary interface support on OCP workers. +# - Uses latest stable OCP version (same as osp_verification.yaml). +# - Deploys OCP with primary IPv4 + secondary IPv6 networks attached to workers. +# - Runs configure_ipv6_secondary day2ops procedure: DHCP MachineConfig, +# macvlan CNO configuration, and pod-to-pod IPv6 connectivity verification. +# - Prepared to run on top of RHOSO 18.0 OSASINFRA Validated Architecture. +# +# Jira: https://redhat.atlassian.net/browse/OSPRH-6486 +# + +openshift_release: 4-stable +openshift_build_name: "" +installation_type: ipi +stages: + - cleanup + - prepare + - install + - post + - verification + - day2ops + +day2ops_procedures: + - configure_ipv6_secondary + +ocp_deployment_topology: + network_type: OVNKubernetes + primary_ip_protocol: ipv4 + secondary_ip_protocol: "ipv6" + ipv4: + ip_version: 4 + tenant_subnet: + cidr: 10.196.0.0/16 + cluster_network: + cidr: 10.128.0.0/14 + host_prefix: 23 + service_network: + - 172.30.0.0/16 + flavors: + master: + name: "master" + ram: 16384 + vcpus: 4 + disk: 30 + worker: + name: "worker" + ram: 8192 + vcpus: 2 + disk: 30 + replicas: + master: 3 + worker: 2 + servergroups: + master: "anti-affinity" + worker: "anti-affinity" From 19cb90676d3faa78c52adc39544a93b3edb43c33 Mon Sep 17 00:00:00 2001 From: tusharjadhav3302 Date: Mon, 29 Jun 2026 20:50:29 +0530 Subject: [PATCH 2/3] Enhancements according to best practices Co-authored-by: Cursor --- .../procedures/configure_ipv6_secondary.yml | 18 ++++++++++++++++++ .../procedures/verify_ipv6_connectivity.yml | 10 ++++++++++ .../verify_ipv6_external_reachability.yml | 3 ++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml b/collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml index ad92f696..d9d437a4 100644 --- a/collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml +++ b/collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml @@ -58,6 +58,15 @@ ansible.builtin.set_fact: ipv6_interfaces: "{{ ipv6_interfaces_results.results | map(attribute='stdout') | list }}" +- name: Validate discovered IPv6 interfaces are not empty + ansible.builtin.assert: + that: + - ipv6_interfaces | length == ipv6_secondary_networks.networks | length + - ipv6_interfaces | select('equalto', '') | list | length == 0 + fail_msg: | + Failed to discover IPv6 interfaces on worker {{ ocp_workers[0] }}. + Expected {{ ipv6_secondary_networks.networks | length }} interfaces, got: {{ ipv6_interfaces }} + - name: Display discovered IPv6 interfaces ansible.builtin.debug: var: ipv6_interfaces @@ -174,3 +183,12 @@ loop: "{{ ipv6_secondary_networks.projects }}" loop_control: loop_var: ipv6_namespace + +- name: Cleanup temporary manifest files + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: "{{ ['/tmp/network-macvlan.yml'] + ipv6_secondary_networks.projects | map('regex_replace', '^(.*)$', '/tmp/ipv6-deployment-\\1.yml') | list }}" + loop_control: + label: "{{ item }}" + ignore_errors: true # noqa: ignore-errors diff --git a/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml b/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml index c9b46f9c..6fade28b 100644 --- a/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml +++ b/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml @@ -39,8 +39,18 @@ ansible.builtin.debug: var: ipv6_pod_ips +- name: Validate all pods have IPv6 addresses + ansible.builtin.assert: + that: + - ipv6_pod_ips | length > 0 + - ipv6_pod_ips.values() | select('equalto', '') | list | length == 0 + fail_msg: | + Not all pods in {{ ipv6_namespace }} received IPv6 addresses. + Pod IPs: {{ ipv6_pod_ips }} + - name: Check pod-to-pod IPv6 connectivity in {{ ipv6_namespace }} ansible.builtin.shell: | + set -o pipefail && \ oc exec {{ item[0] }} -n {{ ipv6_namespace }} -- /bin/curl -s --connect-timeout 10 http://[{{ item[1] }}]:8080 environment: KUBECONFIG: "{{ kubeconfig }}" diff --git a/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_external_reachability.yml b/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_external_reachability.yml index ba0df67a..60062fef 100644 --- a/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_external_reachability.yml +++ b/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_external_reachability.yml @@ -27,7 +27,8 @@ - name: Verify external IPv6 reachability from worker node to pods in {{ ipv6_namespace }} ansible.builtin.shell: | - oc debug node/{{ ocp_workers[0] }} -- chroot /host curl -s --connect-timeout 10 http://[{{ item.stdout }}]:8080 + set -o pipefail && \ + timeout 120 oc debug node/{{ ocp_workers[0] }} -- chroot /host curl -s --connect-timeout 10 http://[{{ item.stdout }}]:8080 environment: KUBECONFIG: "{{ kubeconfig }}" loop: "{{ ext_pod_ipv6_results.results }}" From b809c6783e308935cae228a41398bba7c774662f Mon Sep 17 00:00:00 2001 From: tusharjadhav3302 Date: Wed, 8 Jul 2026 13:00:22 +0530 Subject: [PATCH 3/3] Address PR review comments from imatza-rh Co-authored-by: Cursor --- .../cleanup/tasks/cleanup_ipv6_secondary.yml | 11 ++--- .../procedures/configure_ipv6_secondary.yml | 42 ++++++++++++------- .../procedures/verify_ipv6_connectivity.yml | 4 ++ .../ipi_install_config.yml | 6 +++ .../templates/install-config-ipi.yaml.j2 | 2 +- 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/collection/stages/roles/cleanup/tasks/cleanup_ipv6_secondary.yml b/collection/stages/roles/cleanup/tasks/cleanup_ipv6_secondary.yml index 99da6493..841bdfe6 100644 --- a/collection/stages/roles/cleanup/tasks/cleanup_ipv6_secondary.yml +++ b/collection/stages/roles/cleanup/tasks/cleanup_ipv6_secondary.yml @@ -28,9 +28,10 @@ ignore_errors: true # noqa: ignore-errors - name: Delete IPv6 secondary networks - openstack.cloud.network: - cloud: "{{ user_cloud }}" - state: absent - name: "{{ item.net_name }}" - loop: "{{ ipv6_secondary_networks.networks }}" + ansible.builtin.shell: > + openstack network delete {{ item }} + loop: "{{ registered_resources.ipv6_secondary_network_ids | default([]) }}" + environment: + OS_CLOUD: "{{ user_cloud }}" + changed_when: true ignore_errors: true # noqa: ignore-errors diff --git a/collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml b/collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml index d9d437a4..a037c375 100644 --- a/collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml +++ b/collection/stages/roles/day2ops/tasks/procedures/configure_ipv6_secondary.yml @@ -37,26 +37,38 @@ ocp_workers: "{{ workers | json_query('resources[*].metadata.name') | sort }}" num_workers: "{{ workers.resources | length }}" -- name: Discover IPv6 interfaces on first worker +- name: Discover IPv6 interfaces on all workers via live state ansible.builtin.shell: | set -o pipefail && \ - oc adm node-logs {{ ocp_workers[0] }} | \ - grep '{{ item.cidr | regex_replace('::/64$', '') }}' | \ - grep dev | \ - sed 's/.*{{ item.cidr | regex_replace('::/64$', '') }}::.* dev \(\S\+\).*/\1/g' | \ - tail -1 + timeout 120 oc debug node/{{ item.0 }} -- chroot /host \ + ip -6 addr show scope global | \ + grep -B1 '{{ item.1.cidr | regex_replace('::/64$', '') }}' | \ + awk -F': ' '/^[0-9]/{print $2}' | head -1 environment: KUBECONFIG: "{{ kubeconfig }}" - loop: "{{ ipv6_secondary_networks.networks }}" - register: ipv6_interfaces_results + with_nested: + - "{{ ocp_workers }}" + - "{{ ipv6_secondary_networks.networks }}" + register: all_worker_interfaces changed_when: false retries: 10 delay: 30 - until: ipv6_interfaces_results.stdout != "" + until: all_worker_interfaces.stdout != "" -- name: Store IPv6 interface names +- name: Validate consistent NIC naming across all workers + ansible.builtin.assert: + that: + - per_network_interfaces | unique | length == 1 + fail_msg: | + NIC naming inconsistency for {{ item.net_name }}: {{ per_network_interfaces }} + Workers have different interface names for the same IPv6 network. + vars: + per_network_interfaces: "{{ all_worker_interfaces.results | selectattr('item.1.net_name', 'equalto', item.net_name) | map(attribute='stdout') | list }}" + loop: "{{ ipv6_secondary_networks.networks }}" + +- name: Store IPv6 interface names (validated consistent across workers) ansible.builtin.set_fact: - ipv6_interfaces: "{{ ipv6_interfaces_results.results | map(attribute='stdout') | list }}" + ipv6_interfaces: "{{ all_worker_interfaces.results | selectattr('item.0', 'equalto', ocp_workers[0]) | map(attribute='stdout') | list }}" - name: Validate discovered IPv6 interfaces are not empty ansible.builtin.assert: @@ -64,7 +76,7 @@ - ipv6_interfaces | length == ipv6_secondary_networks.networks | length - ipv6_interfaces | select('equalto', '') | list | length == 0 fail_msg: | - Failed to discover IPv6 interfaces on worker {{ ocp_workers[0] }}. + Failed to discover IPv6 interfaces on workers. Expected {{ ipv6_secondary_networks.networks | length }} interfaces, got: {{ ipv6_interfaces }} - name: Display discovered IPv6 interfaces @@ -94,18 +106,20 @@ KUBECONFIG: "{{ kubeconfig }}" changed_when: true -- name: Verify NetworkAttachmentDefinition CRs exist +- name: Verify NetworkAttachmentDefinition CR exists in {{ item }} kubernetes.core.k8s_info: kubeconfig: "{{ kubeconfig }}" api_version: k8s.cni.cncf.io/v1 kind: NetworkAttachmentDefinition + namespace: "{{ item }}" register: network_attachments until: - network_attachments is defined - network_attachments is not failed - - network_attachments | json_query('resources[*].metadata.name') | list | sort == ipv6_secondary_networks.projects | sort + - network_attachments.resources | length > 0 retries: 10 delay: 30 + loop: "{{ ipv6_secondary_networks.projects }}" - name: Template IPv6 test deployments ansible.builtin.template: diff --git a/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml b/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml index 6fade28b..280e8fe5 100644 --- a/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml +++ b/collection/stages/roles/day2ops/tasks/procedures/verify_ipv6_connectivity.yml @@ -1,4 +1,8 @@ --- +- name: Reset pod IP mapping for {{ ipv6_namespace }} + ansible.builtin.set_fact: + ipv6_pod_ips: {} + - name: Get pods in namespace {{ ipv6_namespace }} kubernetes.core.k8s_info: kubeconfig: "{{ kubeconfig }}" diff --git a/collection/stages/roles/install/tasks/install_config_generation/ipi_install_config.yml b/collection/stages/roles/install/tasks/install_config_generation/ipi_install_config.yml index 0eb66b3f..4a854f6b 100644 --- a/collection/stages/roles/install/tasks/install_config_generation/ipi_install_config.yml +++ b/collection/stages/roles/install/tasks/install_config_generation/ipi_install_config.yml @@ -22,6 +22,12 @@ changed_when: false register: _cacert_content +- name: Load registered resources for IPv6 secondary network IDs + ansible.builtin.include_vars: + file: "{{ resources_file }}" + name: registered_resources + when: ocp_deployment_topology.secondary_ip_protocol == 'ipv6' + - name: Generate install-config.yaml from install-config-ipi.yaml.j2 template ansible.builtin.template: src: install-config-ipi.yaml.j2 diff --git a/collection/stages/roles/install/templates/install-config-ipi.yaml.j2 b/collection/stages/roles/install/templates/install-config-ipi.yaml.j2 index 81ee14a6..fa62c376 100644 --- a/collection/stages/roles/install/templates/install-config-ipi.yaml.j2 +++ b/collection/stages/roles/install/templates/install-config-ipi.yaml.j2 @@ -18,7 +18,7 @@ compute: type: "{{ installcfg_worker_flavor }}" serverGroupPolicy: "{{ installcfg_worker_servergrouppolicy }}" {%- if ocp_deployment_topology.secondary_ip_protocol == 'ipv6' +%} - additionalNetworkIDs: {{ ipv6_secondary_network_ids | to_json }} + additionalNetworkIDs: {{ registered_resources.ipv6_secondary_network_ids | to_json }} {%- endif +%} replicas: {{ installcfg_worker_replicas }} metadata: