From b2cc2f29fb6b61df926dcc46333b3e10910bf6b4 Mon Sep 17 00:00:00 2001 From: opcoder0 <110003254+opcoder0@users.noreply.github.com> Date: Tue, 10 Feb 2026 05:44:45 +0000 Subject: [PATCH 01/10] Add IPv6-only management network support for virtual testbed This change introduces the --ipv6-only-mgmt flag for testbed-cli.sh to configure DUTs with IPv6-only management interfaces, eliminating IPv4 from the management network. Key changes: - testbed-cli.sh: Add --ipv6-only-mgmt flag for gen-mg, deploy-mg, test-mg - config_sonic_basedon_testbed.yml: Handle IPv6-only minigraph deployment with proper async handling for management IP transition - minigraph_dpg.j2, minigraph_dpg_asic.j2: Conditionally include IPv4/IPv6 management interfaces based on use_ipv6_mgmt flag - group_vars/*/ipv6.yml: IPv6 service configurations (NTP, DNS, syslog) - setup-ntp-server.sh: Docker-based Chrony NTP server for local time sync - pr_test_template.yml: Add CI jobs for IPv6-only management testing - Documentation updates for setup and troubleshooting The local NTP server (fec0::ffff:afa:2) ensures reliable time synchronization when external IPv6 NTP servers are unreachable. Signed-off-by: opcoder0 <110003254+opcoder0@users.noreply.github.com> --- .azure-pipelines/pr_test_template.yml | 72 +++++ ansible/config_sonic_basedon_testbed.yml | 133 +++++++++- ansible/files/docker-chrony/Dockerfile | 25 ++ ansible/files/docker-chrony/chrony.conf | 47 ++++ ansible/group_vars/lab/ipv6.yml | 52 ++++ ansible/group_vars/sonic/ipv6.yml | 48 ++++ ansible/group_vars/vm_host/ceos.yml | 6 +- ansible/group_vars/vm_host/creds.yml | 6 +- ansible/group_vars/vm_host/ipv6.yml | 46 ++++ ansible/host_vars/STR-ACS-VSERV-01.yml | 5 + ansible/setup-ntp-server.sh | 324 +++++++++++++++++++++++ ansible/templates/minigraph_dpg.j2 | 8 +- ansible/templates/minigraph_dpg_asic.j2 | 8 +- ansible/testbed-cli.sh | 59 ++++- ansible/veos_vtb | 4 +- docs/ipv6-management-setup.md | 270 +++++++++++++++++++ docs/testbed/README.testbed.VsSetup.md | 37 ++- 17 files changed, 1121 insertions(+), 29 deletions(-) create mode 100644 ansible/files/docker-chrony/Dockerfile create mode 100644 ansible/files/docker-chrony/chrony.conf create mode 100644 ansible/group_vars/lab/ipv6.yml create mode 100644 ansible/group_vars/sonic/ipv6.yml create mode 100644 ansible/group_vars/vm_host/ipv6.yml create mode 100755 ansible/setup-ntp-server.sh create mode 100644 docs/ipv6-management-setup.md diff --git a/.azure-pipelines/pr_test_template.yml b/.azure-pipelines/pr_test_template.yml index 4d0ee740ad6..cfac4fac591 100644 --- a/.azure-pipelines/pr_test_template.yml +++ b/.azure-pipelines/pr_test_template.yml @@ -157,6 +157,78 @@ jobs: ${{ each param in parameters.OVERRIDE_PARAMS }}: ${{ param.key }}: ${{ param.value }} + - job: impacted_area_t0_ipv6_mgmt_elastictest + displayName: "impacted-area-kvmtest-t0-ipv6-mgmt by Elastictest" + dependsOn: + - get_impacted_area + condition: contains(dependencies.get_impacted_area.outputs['SetVariableTask.PR_CHECKERS'], 't0_checker') + variables: + TEST_SCRIPTS: $[ dependencies.get_impacted_area.outputs['SetVariableTask.TEST_SCRIPTS'] ] + timeoutInMinutes: ${{ parameters.TIMEOUT_IN_MINUTES_PR_TEST }} + continueOnError: true + pool: ${{ parameters.AGENT_POOL }} + steps: + - ${{ if eq(parameters.CHECKOUT_SONIC_MGMT, true) }}: + - checkout: ${{ parameters.SONIC_MGMT_NAME }} + displayName: "Checkout sonic-mgmt repository" + + - template: impacted_area_testing/calculate-instance-numbers.yml + parameters: + TOPOLOGY: t0 + BUILD_BRANCH: $(BUILD_BRANCH) + + - template: run-test-elastictest-template.yml + parameters: + ${{ each param in parameters.GLOBAL_PARAMS }}: + ${{ param.key }}: ${{ param.value }} + TOPOLOGY: t0 + SCRIPTS: $(SCRIPTS) + MIN_WORKER: $(INSTANCE_NUMBER) + MAX_WORKER: $(INSTANCE_NUMBER) + DEPLOY_MG_EXTRA_PARAMS: "--ipv6-only-mgmt" + KVM_IMAGE_BRANCH: $(BUILD_BRANCH) + MGMT_BRANCH: $(BUILD_BRANCH) + COMMON_EXTRA_PARAMS: "--disable_sai_validation " + STOP_ON_FAILURE: "false" + ${{ each param in parameters.OVERRIDE_PARAMS }}: + ${{ param.key }}: ${{ param.value }} + + - job: impacted_area_t1_lag_ipv6_mgmt_elastictest + displayName: "impacted-area-kvmtest-t1-lag-ipv6-mgmt by Elastictest" + dependsOn: + - get_impacted_area + condition: contains(dependencies.get_impacted_area.outputs['SetVariableTask.PR_CHECKERS'], 't1_checker') + variables: + TEST_SCRIPTS: $[ dependencies.get_impacted_area.outputs['SetVariableTask.TEST_SCRIPTS'] ] + timeoutInMinutes: ${{ parameters.TIMEOUT_IN_MINUTES_PR_TEST }} + continueOnError: true + pool: ${{ parameters.AGENT_POOL }} + steps: + - ${{ if eq(parameters.CHECKOUT_SONIC_MGMT, true) }}: + - checkout: ${{ parameters.SONIC_MGMT_NAME }} + displayName: "Checkout sonic-mgmt repository" + + - template: impacted_area_testing/calculate-instance-numbers.yml + parameters: + TOPOLOGY: t1 + BUILD_BRANCH: $(BUILD_BRANCH) + + - template: run-test-elastictest-template.yml + parameters: + ${{ each param in parameters.GLOBAL_PARAMS }}: + ${{ param.key }}: ${{ param.value }} + TOPOLOGY: t1-lag + SCRIPTS: $(SCRIPTS) + MIN_WORKER: $(INSTANCE_NUMBER) + MAX_WORKER: $(INSTANCE_NUMBER) + DEPLOY_MG_EXTRA_PARAMS: "--ipv6-only-mgmt" + KVM_IMAGE_BRANCH: $(BUILD_BRANCH) + MGMT_BRANCH: $(BUILD_BRANCH) + COMMON_EXTRA_PARAMS: "--disable_sai_validation " + STOP_ON_FAILURE: "false" + ${{ each param in parameters.OVERRIDE_PARAMS }}: + ${{ param.key }}: ${{ param.value }} + - job: impacted_area_dualtor_elastictest displayName: "impacted-area-kvmtest-dualtor by Elastictest" dependsOn: diff --git a/ansible/config_sonic_basedon_testbed.yml b/ansible/config_sonic_basedon_testbed.yml index c469fb9f1ee..4bcc3ed15b1 100644 --- a/ansible/config_sonic_basedon_testbed.yml +++ b/ansible/config_sonic_basedon_testbed.yml @@ -35,6 +35,37 @@ gather_facts: no tasks: + # Auto-detect if DUT is IPv6-only by trying to connect + # This must run BEFORE any task that connects to the DUT + # Save original IPv4 address before potentially switching + - name: Save original IPv4 address from inventory + set_fact: + original_ipv4_address: "{{ ansible_host }}" + when: + - ansible_host is defined + - "':' not in ansible_host" + + - name: Try to connect using IPv4 address (pre-task check) + wait_for: + host: "{{ ansible_host }}" + port: 22 + timeout: 10 + delegate_to: localhost + ignore_errors: true + register: ipv4_reachable_precheck + when: + - "':' not in ansible_host" + + - name: Switch to IPv6 if IPv4 is not reachable and IPv6 is available (pre-task) + set_fact: + ansible_host: "{{ ansible_hostv6 }}" + dut_was_ipv6_only: true + when: + - ipv4_reachable_precheck is defined + - ipv4_reachable_precheck.failed|default(false) + - ansible_hostv6 is defined + - ansible_hostv6 != '' + - block: - name: set default testbed file set_fact: @@ -48,6 +79,58 @@ - fail: msg="The DUT you are trying to run test does not belongs to this testbed" when: inventory_hostname not in testbed_facts['duts'] + - name: Debug connection mode + debug: + msg: "Connecting to {{ inventory_hostname }} via {{ ansible_host }} (was IPv6-only: {{ dut_was_ipv6_only|default(false) }}, original IPv4: {{ original_ipv4_address|default('N/A') }})" + + # IPv6-only management network configuration loading + - name: Load IPv6 management configuration + include_vars: "group_vars/{{ group_names[1] }}/ipv6.yml" + when: + - use_ipv6_mgmt is defined + - use_ipv6_mgmt|bool == true + - group_names|length > 1 + ignore_errors: true + + - name: Override management service configurations for IPv6-only mode + set_fact: + ntp_servers: "{{ ntp_servers_ipv6 | default(ntp_servers) }}" + dns_servers: "{{ dns_servers_ipv6 | default(dns_servers) }}" + syslog_servers: "{{ syslog_servers_ipv6 | default(syslog_servers) }}" + # Note: tacacs_servers will be overridden later to use PTF IPv6 if use_ptf_tacacs_server is true + tacacs_servers: "{{ tacacs_servers_ipv6 | default(tacacs_servers) }}" + snmp_servers: "{{ snmp_servers_ipv6 | default(snmp_servers) }}" + forced_mgmt_routes: "{{ forced_mgmt_routes_ipv6 | default(forced_mgmt_routes) }}" + tacacs_group: "{{ tacacs_group_ipv6 | default(tacacs_group) }}" + tacacs_passkey: "{{ tacacs_passkey_ipv6 | default(tacacs_passkey) }}" + tacacs_enabled_by_default: "{{ tacacs_enabled_by_default_ipv6 | default(tacacs_enabled_by_default) }}" + when: + - use_ipv6_mgmt is defined + - use_ipv6_mgmt|bool == true + + # DO NOT override ansible_host here - we need to use IPv4 to connect initially + # The DUT will be configured with IPv6 management through the minigraph + - name: Set management IP configuration for IPv6-only mode + set_fact: + target_mgmt_ip: "{{ ansible_hostv6 }}" + mgmt_subnet_mask_length: 64 + when: + - use_ipv6_mgmt is defined + - use_ipv6_mgmt|bool == true + - ansible_hostv6 is defined + + - name: Debug IPv6-only management configuration + debug: + msg: | + IPv6-only management mode enabled for {{ inventory_hostname }} + Management IP: {{ ansible_host }} + NTP servers: {{ ntp_servers | join(', ') }} + DNS servers: {{ dns_servers | join(', ') }} + TACACS servers: {{ tacacs_servers | join(', ') }} + when: + - use_ipv6_mgmt is defined + - use_ipv6_mgmt|bool == true + - name: Set default num_asic set_fact: num_asics: 1 @@ -491,9 +574,9 @@ ignore_errors: true when: init_cfg_profile is not defined - - name: Determine if sonic management is IPv6 + - name: Determine if configuring IPv6-only management (based on use_ipv6_mgmt flag, not current ansible_host) set_fact: - sonic_mgmt_ipv6: "{{ ansible_host is defined and ansible_host != '' and ':' in ansible_host }}" + sonic_mgmt_ipv6: "{{ use_ipv6_mgmt is defined and use_ipv6_mgmt|bool == true }}" - name: Update TACACS server address to PTF IP (IPv4) set_fact: @@ -502,23 +585,23 @@ - use_ptf_tacacs_server is defined and use_ptf_tacacs_server|bool == true - not sonic_mgmt_ipv6|bool - - name: Validate IPv6 TACACS configuration for IPv6 sonic management + - name: Validate IPv6 TACACS configuration for IPv6-only management mode fail: - msg: "sonic management is IPv6 for {{ inventory_hostname }} but ptf_ipv6 is not configured in testbed file" + msg: "IPv6-only management mode requested for {{ inventory_hostname }} but ptf_ipv6 is not configured in testbed file" when: - use_ptf_tacacs_server is defined and use_ptf_tacacs_server|bool == true - sonic_mgmt_ipv6|bool - testbed_facts.get('ptf_ipv6', '') == '' - - name: Update TACACS server address to PTF IP (IPv6) for IPv6 sonic management + - name: Update TACACS server address to PTF IP (IPv6) for IPv6-only management mode set_fact: - tacacs_servers: ["{{ testbed_facts['ptf_ipv6'] }}"] + tacacs_servers: ["{{ testbed_facts['ptf_ipv6'] | regex_replace('/.*$', '') }}"] when: - use_ptf_tacacs_server is defined and use_ptf_tacacs_server|bool == true - sonic_mgmt_ipv6|bool - testbed_facts['ptf_ipv6'] is defined and testbed_facts['ptf_ipv6'] != "" - - debug: msg="sonic_mgmt_ipv6 is {{ sonic_mgmt_ipv6|default(false) }}" + - debug: msg="Configuring IPv6-only management mode={{ sonic_mgmt_ipv6|default(false) }}" when: use_ptf_tacacs_server is defined and use_ptf_tacacs_server|bool == true - debug: msg="tacacs_servers {{ tacacs_servers }}" @@ -767,11 +850,32 @@ failed_when: - load_minigraph_result.rc != 0 - "'no such option: --override_config' not in load_minigraph_result.stderr" + # When switching to IPv6-only, the management IP changes and connection may drop + # Use async to prevent SSH connection timeout from failing the task + async: "{{ 300 if (use_ipv6_mgmt|default(false)|bool) else 0 }}" + poll: "{{ 0 if (use_ipv6_mgmt|default(false)|bool) else 15 }}" + when: not (use_ipv6_mgmt|default(false)|bool) + + - name: execute cli "config load_minigraph --override_config -y" to apply new minigraph (IPv6 transition) + become: true + shell: config load_minigraph --override_config -y + register: load_minigraph_result_ipv6 + async: 300 + poll: 0 + ignore_errors: true + when: use_ipv6_mgmt|default(false)|bool + + - name: Pause briefly to allow minigraph load to start (IPv6 transition) + pause: + seconds: 5 + when: use_ipv6_mgmt|default(false)|bool - name: execute cli "config load_minigraph -y" to apply new minigraph become: true shell: config load_minigraph -y - when: "'no such option: --override_config' in load_minigraph_result.stderr" + when: + - not (use_ipv6_mgmt|default(false)|bool) + - "'no such option: --override_config' in load_minigraph_result.stderr" - name: remove DSCP_TO_TC_MAP for {{ hwsku }}. Some platform doesn't support this configuration become: true @@ -779,6 +883,7 @@ when: - hwsku is defined - hwsku == 'cisco-8101-p4-32x100-vs' + - not (use_ipv6_mgmt|default(false)|bool) when: init_cfg_profile is not defined - name: SONiC config reload to pick up config_db.json @@ -790,7 +895,9 @@ become: false local_action: wait_for args: - host: "{{ ansible_host }}" + # When use_ipv6_mgmt is true, wait on IPv6 address + # When use_ipv6_mgmt is false/undefined, wait on IPv4 (original_ipv4_address if we auto-switched to IPv6, otherwise ansible_host) + host: "{{ ansible_hostv6 if (use_ipv6_mgmt|default(false)|bool and ansible_hostv6 is defined and ansible_hostv6 != '') else (original_ipv4_address if original_ipv4_address is defined else ansible_host) }}" port: 22 state: started search_regex: "OpenSSH_[\\w\\.]+ Debian" @@ -798,6 +905,14 @@ timeout: 600 changed_when: false + - name: Switch ansible_host to IPv6 after minigraph load (for subsequent tasks) + set_fact: + ansible_host: "{{ ansible_hostv6 }}" + when: + - use_ipv6_mgmt|default(false)|bool + - ansible_hostv6 is defined + - ansible_hostv6 != '' + - name: Check if chrony exists service: name=chrony register: chrony_service diff --git a/ansible/files/docker-chrony/Dockerfile b/ansible/files/docker-chrony/Dockerfile new file mode 100644 index 00000000000..47fc4b40120 --- /dev/null +++ b/ansible/files/docker-chrony/Dockerfile @@ -0,0 +1,25 @@ +# Chrony NTP Server for SONiC Virtual Testbed +# This container provides NTP time synchronization for DUTs on the management network +FROM alpine:3.19 + +LABEL maintainer="SONiC Community" +LABEL description="Chrony NTP server for SONiC virtual testbed management network" + +# Install chrony +RUN apk add --no-cache chrony tzdata + +# Create chrony directories +RUN mkdir -p /var/run/chrony /var/lib/chrony + +# Copy configuration file +COPY chrony.conf /etc/chrony/chrony.conf + +# Expose NTP port (UDP 123) +EXPOSE 123/udp + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD chronyc tracking || exit 1 + +# Run chrony in foreground +CMD ["chronyd", "-d", "-f", "/etc/chrony/chrony.conf"] diff --git a/ansible/files/docker-chrony/chrony.conf b/ansible/files/docker-chrony/chrony.conf new file mode 100644 index 00000000000..b4cac364fad --- /dev/null +++ b/ansible/files/docker-chrony/chrony.conf @@ -0,0 +1,47 @@ +# Chrony configuration for SONiC Virtual Testbed NTP Server +# This server provides NTP time sync for DUTs on the management network + +# Upstream NTP servers to sync from +# Using pool.ntp.org for reliable time sources +pool pool.ntp.org iburst maxsources 4 +pool time.google.com iburst maxsources 2 + +# Fallback to local clock if external sources are unavailable +# This ensures DUTs can still sync even without internet connectivity +local stratum 10 + +# Allow NTP clients on the management networks +# IPv4 management network (10.250.0.0/24) +allow 10.250.0.0/24 + +# IPv6 management network (fec0::/10 - site-local) +allow fec0::/10 + +# Also allow localhost +allow 127.0.0.0/8 +allow ::1/128 + +# Record the rate at which the system clock gains/loses time +driftfile /var/lib/chrony/drift + +# Enable kernel synchronisation of the real-time clock (RTC) +rtcsync + +# Allow stepping the system clock during the first updates +makestep 1.0 3 + +# Enable logging +log tracking measurements statistics + +# Specify the location of the log files +logdir /var/log/chrony + +# Listen on all interfaces +bindaddress 0.0.0.0 +bindaddress :: + +# Serve time even if not synchronized (for isolated networks) +local stratum 10 orphan + +# Rate limiting for NTP clients +ratelimit interval 1 burst 16 diff --git a/ansible/group_vars/lab/ipv6.yml b/ansible/group_vars/lab/ipv6.yml new file mode 100644 index 00000000000..ef41dc78ed2 --- /dev/null +++ b/ansible/group_vars/lab/ipv6.yml @@ -0,0 +1,52 @@ +--- +# IPv6-only management network configuration for virtual testbed (lab) +# This file contains IPv6 equivalents of management services when using --ipv6-only-mgmt +# +# For virtual testbeds, we use: +# - Unique Local Addresses (ULA) fc00::/7 for local services +# - The PTF container IPv6 address will be used for TACACS (configured in testbed.yaml) +# - Google IPv6 DNS for external DNS resolution + +# IPv6 NTP servers +# Primary: Local NTP server on the testbed host (deployed via setup-ntp-server.sh) +# Fallback: Public IPv6 NTP servers +ntp_servers_ipv6: + - 'fec0::ffff:afa:2' # Local testbed NTP server (primary) + - '2610:20:6f15:15::27' # NIST NTP server (fallback) + +# IPv6 DNS servers +# Using public Google IPv6 DNS servers +dns_servers_ipv6: + - '2001:4860:4860::8888' # Google IPv6 DNS Primary + - '2001:4860:4860::8844' # Google IPv6 DNS Secondary + +# IPv6 syslog servers +# Using a link-local or ULA address that would point to the test server +# In virtual testbed, syslog typically goes to the PTF or test server +syslog_servers_ipv6: + - 'fc00:1::1' # Virtual testbed server IPv6 (ULA) + +# IPv6 TACACS servers +# Note: These will be overridden to use PTF IPv6 address when use_ptf_tacacs_server is enabled +# which is the default behavior. The PTF IPv6 is configured in testbed.yaml +tacacs_servers_ipv6: [] + +# IPv6 SNMP servers +# Using ULA address for local test server +snmp_servers_ipv6: + - 'fc00:1::1' # Virtual testbed server IPv6 (ULA) + +# Keep existing TACACS settings for virtual testbed +tacacs_group_ipv6: 'testlab' +tacacs_passkey_ipv6: testing123 +tacacs_enabled_by_default_ipv6: false + +# IPv6 forced management routes +# Routes needed for IPv6-only management in virtual testbed +# These are typically local routes within the virtual network +forced_mgmt_routes_ipv6: [ + # Local ULA network for PTF and test server + 'fc00:1::/64', + # Default management network for virtual testbed (fc00:2::/64 is commonly used) + 'fc00:2::/64' +] diff --git a/ansible/group_vars/sonic/ipv6.yml b/ansible/group_vars/sonic/ipv6.yml new file mode 100644 index 00000000000..9ccf3ec272c --- /dev/null +++ b/ansible/group_vars/sonic/ipv6.yml @@ -0,0 +1,48 @@ +--- +# IPv6-only management network configuration for sonic group (virtual testbed DUTs) +# This file contains IPv6 equivalents of management services when using --ipv6-only-mgmt +# +# For virtual testbeds, we use: +# - Site-local addresses (fec0::/10) for management network +# - The PTF container IPv6 address will be used for TACACS (configured in testbed.yaml) +# - Google IPv6 DNS for external DNS resolution + +# IPv6 NTP servers +# Primary: Local NTP server on the testbed host (deployed via setup-ntp-server.sh) +# Fallback: Public IPv6 NTP servers +ntp_servers_ipv6: + - 'fec0::ffff:afa:2' # Local testbed NTP server (primary) + - '2610:20:6f15:15::27' # NIST NTP server (fallback) + +# IPv6 DNS servers +# Using public Google IPv6 DNS servers +dns_servers_ipv6: + - '2001:4860:4860::8888' # Google IPv6 DNS Primary + - '2001:4860:4860::8844' # Google IPv6 DNS Secondary + +# IPv6 syslog servers +# In virtual testbed, syslog typically goes to the PTF or test server +syslog_servers_ipv6: + - 'fec0::1' # Virtual testbed server IPv6 (gateway) + +# IPv6 TACACS servers +# Note: These will be overridden to use PTF IPv6 address when use_ptf_tacacs_server is enabled +# which is the default behavior. The PTF IPv6 is configured in testbed.yaml +tacacs_servers_ipv6: [] + +# IPv6 SNMP servers +snmp_servers_ipv6: + - 'fec0::1' # Virtual testbed server IPv6 (gateway) + +# Keep existing TACACS settings for virtual testbed +tacacs_group_ipv6: 'testlab' +tacacs_passkey_ipv6: testing123 +tacacs_enabled_by_default_ipv6: false + +# IPv6 forced management routes +# Routes needed for IPv6-only management in virtual testbed +forced_mgmt_routes_ipv6: + - '2001:4860:4860::/64' # Google IPv6 DNS network + - 'fc00:1::/64' # Local ULA network for PTF backplane + - 'fc00:2::/64' # Management network (ULA) + - 'fec0::/64' # Site-local management network diff --git a/ansible/group_vars/vm_host/ceos.yml b/ansible/group_vars/vm_host/ceos.yml index 75dac55237a..7274e55059c 100644 --- a/ansible/group_vars/vm_host/ceos.yml +++ b/ansible/group_vars/vm_host/ceos.yml @@ -1,6 +1,6 @@ -ceos_image_filename: cEOS64-lab-4.32.5M.tar -ceos_image_orig: ceosimage:4.32.5M -ceos_image: ceosimage:4.32.5M-1 +ceos_image_filename: cEOS64-lab-4.29.7M.tar +ceos_image_orig: ceosimage:4.29.7M +ceos_image: ceosimage:4.29.7M # Please update ceos_image_url to the actual URL of the cEOS image file in your environment. If the cEOS image file # is not available on test server, the cEOS image file will be downloaded from this URL. # The ceos_image_url can be a string as single URL or a list of strings as multiple URLs. If it is a list, the code diff --git a/ansible/group_vars/vm_host/creds.yml b/ansible/group_vars/vm_host/creds.yml index 327da266185..38a23fc955e 100644 --- a/ansible/group_vars/vm_host/creds.yml +++ b/ansible/group_vars/vm_host/creds.yml @@ -5,6 +5,6 @@ ansible_become_password: use_own_value # Use the following username/password variables to login to vm hosts # instead of the default variables (defined above). -vm_host_user: use_own_value -vm_host_password: use_own_value -vm_host_become_password: use_own_value +vm_host_user: sagummaraj +vm_host_password: neo123*() +vm_host_become_password: neo123*() diff --git a/ansible/group_vars/vm_host/ipv6.yml b/ansible/group_vars/vm_host/ipv6.yml new file mode 100644 index 00000000000..61180245962 --- /dev/null +++ b/ansible/group_vars/vm_host/ipv6.yml @@ -0,0 +1,46 @@ +--- +# IPv6-only management network configuration for virtual testbed (vm_host) +# This file contains IPv6 equivalents of management services when using --ipv6-only-mgmt +# +# Virtual testbed IPv6 addressing scheme: +# - Management network: fc00:2::/64 (ULA) +# - PTF backplane: fc0a::/64 (already defined in vm_host/main.yml as ptf_bp_ipv6) +# - DUT management: Uses ansible_hostv6 from inventory + +# IPv6 NTP servers +# Primary: Local NTP server on the testbed host (deployed via setup-ntp-server.sh) +# Fallback: Public IPv6 NTP servers +ntp_servers_ipv6: + - 'fec0::ffff:afa:2' # Local testbed NTP server (primary) + - '2610:20:6f15:15::27' # NIST NTP server (fallback) + +# IPv6 DNS servers +# Using public Google IPv6 DNS servers +dns_servers_ipv6: + - '2001:4860:4860::8888' # Google IPv6 DNS Primary + - '2001:4860:4860::8844' # Google IPv6 DNS Secondary + +# IPv6 syslog servers +# In virtual testbed, syslog typically goes to the PTF or test server +syslog_servers_ipv6: + - 'fc00:1::1' # Virtual testbed server IPv6 (ULA) + +# IPv6 TACACS servers +# Note: These will be overridden to use PTF IPv6 address when use_ptf_tacacs_server is enabled +tacacs_servers_ipv6: [] + +# IPv6 SNMP servers +snmp_servers_ipv6: + - 'fc00:1::1' # Virtual testbed server IPv6 (ULA) + +# TACACS settings for virtual testbed +tacacs_group_ipv6: 'testlab' +tacacs_passkey_ipv6: testing123 +tacacs_enabled_by_default_ipv6: false + +# IPv6 forced management routes +forced_mgmt_routes_ipv6: [ + 'fc00:1::/64', # Local test server network + 'fc00:2::/64', # Management network + 'fc0a::/64' # PTF backplane network +] diff --git a/ansible/host_vars/STR-ACS-VSERV-01.yml b/ansible/host_vars/STR-ACS-VSERV-01.yml index 6ffcc82014b..d65b644feff 100644 --- a/ansible/host_vars/STR-ACS-VSERV-01.yml +++ b/ansible/host_vars/STR-ACS-VSERV-01.yml @@ -4,4 +4,9 @@ mgmt_gw: 10.250.0.1 mgmt_gw_v6: fec0::1 vm_mgmt_gw: 10.250.0.1 +# Local NTP server running on the testbed host (setup via setup-ntp-server.sh) +# These addresses are used by DUTs for time synchronization +local_ntp_ipv4: 10.250.0.2 +local_ntp_ipv6: fec0::ffff:afa:2 + internal_mgmt_port: True diff --git a/ansible/setup-ntp-server.sh b/ansible/setup-ntp-server.sh new file mode 100755 index 00000000000..3c82f8e2029 --- /dev/null +++ b/ansible/setup-ntp-server.sh @@ -0,0 +1,324 @@ +#!/bin/bash +# +# setup-ntp-server.sh - Set up a local NTP server for SONiC virtual testbed +# +# This script deploys a Chrony NTP server in a Docker container connected +# to the management bridge (br1) for providing time synchronization to +# SONiC DUTs, especially in IPv6-only management network scenarios. +# +# Usage: +# ./setup-ntp-server.sh [start|stop|status|restart] +# +# The NTP server will be accessible at: +# IPv4: 10.250.0.2 +# IPv6: fec0::ffff:afa:2 +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOCKER_DIR="${SCRIPT_DIR}/files/docker-chrony" + +# Configuration +CONTAINER_NAME="sonic-mgmt-ntp" +IMAGE_NAME="sonic-mgmt-ntp:latest" +MGMT_BRIDGE="br1" +NTP_IPV4="10.250.0.2" +NTP_IPV4_PREFIX="24" +NTP_IPV6="fec0::ffff:afa:2" +NTP_IPV6_PREFIX="64" +NETWORK_NAME="sonic-mgmt-ntp-net" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +check_prerequisites() { + log_info "Checking prerequisites..." + + # Check if Docker is installed + if ! command -v docker &> /dev/null; then + log_error "Docker is not installed. Please install Docker first." + exit 1 + fi + + # Check if bridge exists + if ! ip link show "${MGMT_BRIDGE}" &> /dev/null; then + log_error "Management bridge '${MGMT_BRIDGE}' does not exist." + log_error "Please run 'add-topo' first to create the testbed infrastructure." + exit 1 + fi + + # Check if Dockerfile exists + if [[ ! -f "${DOCKER_DIR}/Dockerfile" ]]; then + log_error "Dockerfile not found at ${DOCKER_DIR}/Dockerfile" + exit 1 + fi + + log_info "Prerequisites check passed." +} + +build_image() { + log_info "Building NTP server Docker image..." + + docker build -t "${IMAGE_NAME}" "${DOCKER_DIR}" + + log_info "Docker image '${IMAGE_NAME}' built successfully." +} + +create_network() { + log_info "Setting up Docker network connected to ${MGMT_BRIDGE}..." + + # Check if network already exists + if docker network ls --format '{{.Name}}' | grep -q "^${NETWORK_NAME}$"; then + log_info "Network '${NETWORK_NAME}' already exists." + return 0 + fi + + # Create a macvlan network attached to br1 + # This allows the container to have its own IP on the bridge network + docker network create \ + --driver=macvlan \ + --subnet=10.250.0.0/24 \ + --gateway=10.250.0.1 \ + --ipv6 \ + --subnet=fec0::/64 \ + --gateway=fec0::1 \ + -o parent="${MGMT_BRIDGE}" \ + "${NETWORK_NAME}" + + log_info "Network '${NETWORK_NAME}' created successfully." +} + +start_container() { + log_info "Starting NTP server container..." + + # Check if container already exists + if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + log_warn "Container '${CONTAINER_NAME}' is already running." + return 0 + else + log_info "Removing stopped container '${CONTAINER_NAME}'..." + docker rm "${CONTAINER_NAME}" + fi + fi + + # Run the container with both IPv4 and IPv6 addresses + docker run -d \ + --name "${CONTAINER_NAME}" \ + --network "${NETWORK_NAME}" \ + --ip "${NTP_IPV4}" \ + --ip6 "${NTP_IPV6}" \ + --cap-add SYS_TIME \ + --restart unless-stopped \ + "${IMAGE_NAME}" + + log_info "NTP server container started." + log_info " IPv4 address: ${NTP_IPV4}" + log_info " IPv6 address: ${NTP_IPV6}" +} + +stop_container() { + log_info "Stopping NTP server container..." + + if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + docker stop "${CONTAINER_NAME}" + docker rm "${CONTAINER_NAME}" + log_info "Container '${CONTAINER_NAME}' stopped and removed." + else + log_warn "Container '${CONTAINER_NAME}' is not running." + fi +} + +remove_network() { + log_info "Removing Docker network..." + + if docker network ls --format '{{.Name}}' | grep -q "^${NETWORK_NAME}$"; then + docker network rm "${NETWORK_NAME}" 2>/dev/null || true + log_info "Network '${NETWORK_NAME}' removed." + else + log_info "Network '${NETWORK_NAME}' does not exist." + fi +} + +show_status() { + echo "" + echo "=== NTP Server Status ===" + echo "" + + # Check if container exists and is running + if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo -e "Container: ${GREEN}Running${NC}" + echo " Name: ${CONTAINER_NAME}" + echo " IPv4: ${NTP_IPV4}" + echo " IPv6: ${NTP_IPV6}" + echo "" + + # Show chrony status + echo "Chrony Status:" + docker exec "${CONTAINER_NAME}" chronyc tracking 2>/dev/null || echo " Unable to get chrony status" + echo "" + + # Show connected clients + echo "Connected Clients:" + docker exec "${CONTAINER_NAME}" chronyc clients 2>/dev/null || echo " Unable to get client list" + elif docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo -e "Container: ${YELLOW}Stopped${NC}" + else + echo -e "Container: ${RED}Not created${NC}" + fi + + # Check network + echo "" + if docker network ls --format '{{.Name}}' | grep -q "^${NETWORK_NAME}$"; then + echo -e "Network: ${GREEN}Created${NC} (${NETWORK_NAME})" + else + echo -e "Network: ${RED}Not created${NC}" + fi + + # Check if image exists + echo "" + if docker images --format '{{.Repository}}:{{.Tag}}' | grep -q "^${IMAGE_NAME}$"; then + echo -e "Image: ${GREEN}Built${NC} (${IMAGE_NAME})" + else + echo -e "Image: ${RED}Not built${NC}" + fi + + echo "" +} + +test_ntp() { + log_info "Testing NTP server connectivity..." + + # First check if container is running + if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + log_error "NTP server container is not running. Start it with: $0 start" + return 1 + fi + + # Show chrony status from inside the container + echo "" + echo "=== NTP Server Internal Status ===" + docker exec "${CONTAINER_NAME}" chronyc tracking 2>/dev/null || log_warn "Could not get chrony tracking info" + echo "" + + # Test IPv4 connectivity + echo "=== Testing IPv4 Connectivity (${NTP_IPV4}) ===" + if ping -c 1 -W 2 "${NTP_IPV4}" &> /dev/null; then + echo " Ping: successful" + else + log_warn " Ping: failed (macvlan network may not be reachable from host)" + fi + + # Try various NTP test methods + if command -v ntpdate &> /dev/null; then + echo " NTP query (ntpdate):" + ntpdate -q "${NTP_IPV4}" 2>&1 | sed 's/^/ /' || true + elif command -v sntp &> /dev/null; then + echo " NTP query (sntp):" + sntp "${NTP_IPV4}" 2>&1 | sed 's/^/ /' || true + elif command -v ntpq &> /dev/null; then + echo " NTP query (ntpq):" + timeout 5 ntpq -p "${NTP_IPV4}" 2>&1 | sed 's/^/ /' || echo " Query timed out or failed" + else + echo " No NTP client tools found (ntpdate, sntp, ntpq)" + echo " Install with: sudo apt-get install ntpdate" + fi + + # Test IPv6 connectivity + echo "" + echo "=== Testing IPv6 Connectivity (${NTP_IPV6}) ===" + if ping6 -c 1 -W 2 "${NTP_IPV6}" &> /dev/null; then + echo " Ping: successful" + else + log_warn " Ping: failed (macvlan network may not be reachable from host)" + fi + + # Note about macvlan limitation + echo "" + echo "=== Notes ===" + echo " - Macvlan networks are not directly reachable from the host by design" + echo " - The NTP server IS reachable from VMs/containers on the br1 network" + echo " - To test from a DUT: ping6 ${NTP_IPV6} && chronyc sources" +} + +usage() { + echo "Usage: $0 [command]" + echo "" + echo "Commands:" + echo " start Build image and start NTP server container" + echo " stop Stop and remove NTP server container" + echo " restart Restart NTP server container" + echo " status Show NTP server status" + echo " test Test NTP server connectivity" + echo " clean Stop container, remove network and image" + echo " help Show this help message" + echo "" + echo "NTP Server Configuration:" + echo " IPv4: ${NTP_IPV4}/${NTP_IPV4_PREFIX}" + echo " IPv6: ${NTP_IPV6}/${NTP_IPV6_PREFIX}" + echo " Bridge: ${MGMT_BRIDGE}" + echo "" +} + +# Main +case "${1:-}" in + start) + check_prerequisites + build_image + create_network + start_container + echo "" + show_status + echo "" + log_info "NTP server is ready. DUTs can sync time using:" + log_info " IPv4: ${NTP_IPV4}" + log_info " IPv6: ${NTP_IPV6}" + ;; + stop) + stop_container + ;; + restart) + stop_container + start_container + ;; + status) + show_status + ;; + test) + test_ntp + ;; + clean) + stop_container + remove_network + log_info "Removing Docker image..." + docker rmi "${IMAGE_NAME}" 2>/dev/null || true + log_info "Cleanup complete." + ;; + help|--help|-h) + usage + ;; + *) + if [[ -n "${1:-}" ]]; then + log_error "Unknown command: $1" + echo "" + fi + usage + exit 1 + ;; +esac diff --git a/ansible/templates/minigraph_dpg.j2 b/ansible/templates/minigraph_dpg.j2 index 7d9b995bbd1..6c85187b13d 100644 --- a/ansible/templates/minigraph_dpg.j2 +++ b/ansible/templates/minigraph_dpg.j2 @@ -63,16 +63,18 @@ {% endif %} +{% if use_ipv6_mgmt is not defined or not use_ipv6_mgmt|bool %} HostIP eth0 - {{ ansible_host }}/{{ mgmt_subnet_mask_length }} + {{ original_ipv4_address if original_ipv4_address is defined else ansible_host }}/{{ mgmt_subnet_mask_length }} - {{ ansible_host }}/{{ mgmt_subnet_mask_length }} + {{ original_ipv4_address if original_ipv4_address is defined else ansible_host }}/{{ mgmt_subnet_mask_length }} +{% endif %} - V6HostIP + {% if use_ipv6_mgmt is defined and use_ipv6_mgmt|bool %}HostIP{% else %}V6HostIP{% endif %} eth0 {{ ansible_hostv6 if ansible_hostv6 is defined else 'fc00:2::32' }}/{{ mgmt_subnet_v6_mask_length if mgmt_subnet_v6_mask_length is defined else '64' }} diff --git a/ansible/templates/minigraph_dpg_asic.j2 b/ansible/templates/minigraph_dpg_asic.j2 index 9179b9c4bec..3ac6c911206 100644 --- a/ansible/templates/minigraph_dpg_asic.j2 +++ b/ansible/templates/minigraph_dpg_asic.j2 @@ -49,16 +49,18 @@ {% endif %} +{% if use_ipv6_mgmt is not defined or not use_ipv6_mgmt|bool %} HostIP eth0 - {{ ansible_host }}/{{ mgmt_subnet_mask_length }} + {{ original_ipv4_address if original_ipv4_address is defined else ansible_host }}/{{ mgmt_subnet_mask_length }} - {{ ansible_host }}/{{ mgmt_subnet_mask_length }} + {{ original_ipv4_address if original_ipv4_address is defined else ansible_host }}/{{ mgmt_subnet_mask_length }} +{% endif %} - V6HostIP + {% if use_ipv6_mgmt is defined and use_ipv6_mgmt|bool %}HostIP{% else %}V6HostIP{% endif %} eth0 {{ ansible_hostv6 if ansible_hostv6 is defined else 'fc00:2::32' }}/{{ mgmt_subnet_v6_mask_length if mgmt_subnet_v6_mask_length is defined else '64' }} diff --git a/ansible/testbed-cli.sh b/ansible/testbed-cli.sh index c5b1d351548..5617085939d 100755 --- a/ansible/testbed-cli.sh +++ b/ansible/testbed-cli.sh @@ -70,6 +70,8 @@ function usage echo " -e enable_data_plane_acl=true" echo " -e enable_data_plane_acl=false" echo " by default, data acl is enabled" + echo " deploy-mg also supports IPv6-only management network configuration:" + echo " --ipv6-only-mgmt Use IPv6-only management configuration (NTP, DNS, TACACS, etc.)" echo "To config simulated y-cable driver for DUT in specified testbed: $0 config-y-cable 'testbed-name' 'inventory' ~/.password" echo "To create Kubernetes master on a server: $0 -m k8s_ubuntu create-master 'k8s-server-name' ~/.password" echo "To destroy Kubernetes master on a server: $0 -m k8s_ubuntu destroy-master 'k8s-server-name' ~/.password" @@ -613,7 +615,24 @@ function generate_minigraph read_file $testbed_name - ansible-playbook -i "$inventory" config_sonic_basedon_testbed.yml --vault-password-file="$passfile" -l "$duts" -e testbed_name="$testbed_name" -e testbed_file=$tbfile -e vm_file=$vmfile -e local_minigraph=true $@ + # Parse --ipv6-only-mgmt flag + ipv6_mgmt_flag="" + for arg in "$@"; do + if [[ "$arg" == "--ipv6-only-mgmt" ]]; then + ipv6_mgmt_flag="-e use_ipv6_mgmt=true -e use_ptf_tacacs_server=true" + break + fi + done + + # Remove --ipv6-only-mgmt from args if present + filtered_args=() + for arg in "$@"; do + if [[ "$arg" != "--ipv6-only-mgmt" ]]; then + filtered_args+=("$arg") + fi + done + + ansible-playbook -i "$inventory" config_sonic_basedon_testbed.yml --vault-password-file="$passfile" -l "$duts" -e testbed_name="$testbed_name" -e testbed_file=$tbfile -e vm_file=$vmfile -e local_minigraph=true $ipv6_mgmt_flag "${filtered_args[@]}" echo Done } @@ -631,7 +650,24 @@ function deploy_minigraph read_file $testbed_name - ansible-playbook -i "$inventory" config_sonic_basedon_testbed.yml --vault-password-file="$passfile" -l "$duts" -e testbed_name="$testbed_name" -e testbed_file=$tbfile -e vm_file=$vmfile -e deploy=true -e save=true $@ + # Parse --ipv6-only-mgmt flag + ipv6_mgmt_flag="" + for arg in "$@"; do + if [[ "$arg" == "--ipv6-only-mgmt" ]]; then + ipv6_mgmt_flag="-e use_ipv6_mgmt=true -e use_ptf_tacacs_server=true" + break + fi + done + + # Remove --ipv6-only-mgmt from args if present + filtered_args=() + for arg in "$@"; do + if [[ "$arg" != "--ipv6-only-mgmt" ]]; then + filtered_args+=("$arg") + fi + done + + ansible-playbook -i "$inventory" config_sonic_basedon_testbed.yml --vault-password-file="$passfile" -l "$duts" -e testbed_name="$testbed_name" -e testbed_file=$tbfile -e vm_file=$vmfile -e deploy=true -e save=true $ipv6_mgmt_flag "${filtered_args[@]}" echo Done } @@ -649,7 +685,24 @@ function test_minigraph read_file $testbed_name - ansible-playbook -i "$inventory" --diff --connection=local --check config_sonic_basedon_testbed.yml --vault-password-file="$passfile" -l "$duts" -e testbed_name="$testbed_name" -e testbed_file=$tbfile -e vm_file=$vmfile -e local_minigraph=true $@ + # Parse --ipv6-only-mgmt flag + ipv6_mgmt_flag="" + for arg in "$@"; do + if [[ "$arg" == "--ipv6-only-mgmt" ]]; then + ipv6_mgmt_flag="-e use_ipv6_mgmt=true -e use_ptf_tacacs_server=true" + break + fi + done + + # Remove --ipv6-only-mgmt from args if present + filtered_args=() + for arg in "$@"; do + if [[ "$arg" != "--ipv6-only-mgmt" ]]; then + filtered_args+=("$arg") + fi + done + + ansible-playbook -i "$inventory" --diff --connection=local --check config_sonic_basedon_testbed.yml --vault-password-file="$passfile" -l "$duts" -e testbed_name="$testbed_name" -e testbed_file=$tbfile -e vm_file=$vmfile -e local_minigraph=true $ipv6_mgmt_flag "${filtered_args[@]}" echo Done } diff --git a/ansible/veos_vtb b/ansible/veos_vtb index 847cdc9704e..fbd07b0e5a4 100644 --- a/ansible/veos_vtb +++ b/ansible/veos_vtb @@ -373,8 +373,8 @@ vm_host_1: hosts: STR-ACS-VSERV-01: ansible_host: 172.17.0.1 - ansible_user: use_own_value - vm_host_user: use_own_value + ansible_user: sagummaraj + vm_host_user: sagummaraj vms_1: hosts: diff --git a/docs/ipv6-management-setup.md b/docs/ipv6-management-setup.md new file mode 100644 index 00000000000..25473d9c30c --- /dev/null +++ b/docs/ipv6-management-setup.md @@ -0,0 +1,270 @@ +# IPv6-Only Management Network Configuration for Virtual Testbed + +This document explains how to use the `--ipv6-only-mgmt` flag to configure virtual testbeds with IPv6-only management networks. + +## Overview + +The `--ipv6-only-mgmt` flag allows you to configure a virtual DUT (Device Under Test) with IPv6-only management network settings, including: + +- IPv6 management IP address (using `ansible_hostv6` from inventory) +- IPv6 NTP servers +- IPv6 DNS servers +- IPv6 TACACS servers (using PTF container's IPv6 address) +- IPv6 syslog servers +- IPv6 SNMP servers +- IPv6 management routes + +## Usage + +### Basic Usage + +To deploy a virtual testbed with IPv6-only management configuration: + +**Generate Minigraph** + +Generate minigraph for IPv6 only management address is only required for Virtual testbed. + +```bash +./testbed-cli.sh [-t testbed-file] [-m inventory] gen-mg --ipv6-only-mgmt +``` + +**Deploy Minigraph** +```bash +./testbed-cli.sh deploy-mg --ipv6-only-mgmt +``` + +### Examples + +1. **Deploy minigraph with IPv6-only management:** + ```bash + ./testbed-cli.sh deploy-mg vms-sn2700-t0 lab ~/.password --ipv6-only-mgmt + ``` + +2. **Generate and deploy minigraph with IPv6-only management for virtual testbed:** + ```bash + # generate minigraph + ./testbed-cli.sh -t vtestbed.yaml -m veos_vtb gen-mg vms-kvm-t0 veos_vtb ~/.password --ipv6-only-mgmt + # deploy minigraph + ./testbed-cli.sh -t vtestbed.yaml -m veos_vtb deploy-mg vms-kvm-t0 veos_vtb ~/.password --ipv6-only-mgmt + ``` + +3. **Test minigraph with IPv6-only management:** + ```bash + ./testbed-cli.sh -t vtestbed.yaml -m veos_vtb test-mg vms-kvm-t0 veos_vtb ~/.password --ipv6-only-mgmt + ``` + +## Prerequisites + +### 1. Testbed Configuration + +The testbed must have `ptf_ipv6` defined in the testbed.yaml file: + +```yaml +- conf-name: vms-sn2700-t0 + group-name: vms1-3 + topo: t0 + ptf_image_name: docker-ptf + ptf: ptf_vms1-3 + ptf_ip: 10.255.0.180/24 + ptf_ipv6: 2001:db8:1::10/64 # Required for IPv6-only management + server: server_1 + vm_base: VM0100 + dut: + - str-msn2700-01 + inv_name: lab +``` + +### 2. Inventory Configuration + +The device should have `ansible_hostv6` defined in the inventory file: + +```yaml +str-msn2700-01: + ansible_host: 10.250.0.101 + ansible_hostv6: fc00:2::101 + mgmt_subnet_mask_length: 24 +``` + +### 3. IPv6 Configuration Files + +IPv6 service configurations are defined in: +- `group_vars/lab/ipv6.yml` - For lab inventory +- `group_vars/sonic/ipv6.yml` - For lab inventory (depending on the inventory group) +- `group_vars/vm_host/ipv6.yml` - For vm_host inventory + +## Virtual Testbed IPv6 Addressing Scheme + +For virtual testbeds, we use the following IPv6 addressing: + +| Network Purpose | IPv6 Prefix | Notes | +|----------------|-------------|-------| +| Management Network | `fc00:2::/64` | ULA prefix for DUT management | +| PTF Backplane | `fc0a::/64` | Defined in vm_host/main.yml | +| Local Server | `fc00:1::/64` | ULA for syslog, SNMP | +| Google DNS | `2001:4860:4860::/64` | Public DNS servers | + +## Configuration Details + +### IPv6 Service Addresses + +When `--ipv6-only-mgmt` is used, the following services use IPv6: + +| Service | IPv6 Address | Notes | +|---------|-------------|-------| +| NTP | `fec0::ffff:afa:2` | Local testbed NTP server (primary) | +| NTP | `2001:4860:4806::` | Google IPv6 NTP (fallback) | +| TACACS | PTF's `ptf_ipv6` | From testbed.yaml | +| Syslog | `fec0::1` | Testbed gateway | +| SNMP | `fec0::1` | Testbed gateway | + +### Management IP Configuration + +- The DUT's management IP uses `ansible_hostv6` instead of `ansible_host` +- The subnet mask length defaults to `/64` +- IPv6 management routes are configured for proper connectivity + +### TACACS Integration + +When using PTF TACACS servers (default behavior): +- IPv4 mode: Uses testbed's `ptf_ip` +- IPv6 mode: Uses testbed's `ptf_ipv6` (with CIDR notation stripped) + +The TACACS server runs on the PTF container, which must have IPv6 connectivity to the DUT. + +## Setting Up IPv6 on the PTF Container + +Ensure the PTF container has IPv6 configured: + +1. The `ptf_ipv6` address from testbed.yaml or vtestbed.yaml is assigned to the PTF container +2. The TACACS server on PTF must listen on the IPv6 address +3. Verify IPv6 connectivity between DUT and PTF + +## Setting Up Local NTP Server + +For IPv6-only management networks, external NTP servers may not be reachable. A local NTP server running on the testbed host provides reliable time synchronization. + +### Deploying the NTP Server + +The NTP server runs as a Docker container connected to the management bridge (br1): + +```bash +cd ansible/ +./setup-ntp-server.sh start +``` + +This will: +1. Build the Chrony NTP server Docker image +2. Create a macvlan network attached to br1 +3. Start the container with both IPv4 and IPv6 addresses + +### NTP Server Addresses + +| Protocol | Address | Notes | +|----------|---------|-------| +| IPv4 | `10.250.0.2` | For IPv4 management networks | +| IPv6 | `fec0::ffff:afa:2` | For IPv6-only management networks | + +### Managing the NTP Server + +```bash +# Check status +./setup-ntp-server.sh status + +# Stop the server +./setup-ntp-server.sh stop + +# Restart the server +./setup-ntp-server.sh restart + +# Test connectivity +./setup-ntp-server.sh test + +# Full cleanup (remove container, network, and image) +./setup-ntp-server.sh clean +``` + +### Verifying NTP Synchronization + +On the DUT, verify NTP is working: + +```bash +# Check NTP status +show ntp + +# Or using chronyc +chronyc sources +chronyc tracking +``` + +### Configuration Files + +The local NTP server is configured as the primary NTP source in: +- `group_vars/sonic/ipv6.yml` - For DUTs +- `group_vars/lab/ipv6.yml` - For lab devices +- `group_vars/vm_host/ipv6.yml` - For VM hosts +- `host_vars/STR-ACS-VSERV-01.yml` - Server-specific config + +## Troubleshooting + +### Missing ptf_ipv6 + +**Error:** `IPv6-only management mode requested but ptf_ipv6 is not configured in testbed file` + +**Solution:** Add `ptf_ipv6` to your testbed entry in testbed.yaml + +### Missing ansible_hostv6 + +**Issue:** Minigraph uses default IPv6 address + +**Solution:** Add `ansible_hostv6` to the device entry in the inventory file + +### DUT Not Reachable After IPv6 Transition + +**Issue:** Playbook fails waiting for DUT after loading minigraph + +**Solution:** +1. Verify IPv6 routing is configured on the management network +2. Check that the server/PTF can reach the DUT's IPv6 management address +3. Ensure the management switch/bridge supports IPv6 + +### IPv6 Services Not Working + +**Issue:** NTP or other services fail after IPv6 transition + +**Solution:** +1. Verify the IPv6 service addresses are reachable from the DUT +2. Check firewall rules allow IPv6 traffic +3. Ensure forced management routes include necessary IPv6 prefixes + +### NTP Server Not Reachable + +**Issue:** DUT cannot sync time with local NTP server + +**Solution:** +1. Verify the NTP server is running: + ```bash + ./setup-ntp-server.sh status + ``` +2. Check network connectivity from DUT: + ```bash + # On DUT + ping6 fec0::ffff:afa:2 + ``` +3. Verify the macvlan network is attached to br1: + ```bash + docker network inspect sonic-mgmt-ntp-net + ``` +4. Check chrony configuration inside container: + ```bash + docker exec sonic-mgmt-ntp chronyc tracking + ``` + +## Reverting to IPv4 Management + +To switch back to IPv4 management, simply run `deploy-mg` without the `--ipv6-only-mgmt` flag: + +```bash +./testbed-cli.sh deploy-mg vms-sn2700-t0 lab ~/.password +``` + +This will regenerate and deploy a minigraph with IPv4-only management configuration. diff --git a/docs/testbed/README.testbed.VsSetup.md b/docs/testbed/README.testbed.VsSetup.md index 886d1e34dc0..c76fe81cef8 100644 --- a/docs/testbed/README.testbed.VsSetup.md +++ b/docs/testbed/README.testbed.VsSetup.md @@ -74,11 +74,11 @@ mkdir -p ~/veos-vm/images #### Option 2.1: Manually download cEOS image 1. Obtain the cEOS image from [Arista's software download page](https://www.arista.com/en/support/software-download). You can choose later cEOS versions, they do not guarantee to work (the latest 4.35.0F do not). - + **Note:** You may need to register an Arista guest account to access the download resources. Ensure that the cEOS version you download matches the version specified in `ansible/group_vars/vm_host/ceos.yml`. For example, the following steps use `cEOS64-lab-4.29.3M` as a reference. - + 2. Unxz it with `unxz cEOS64-lab-4.29.3M.tar.xz`. 3. Place the image file in the `images` subfolder located within the directory specified by the `root_path` variable in the `ansible/group_vars/vm_host/main.yml` file. @@ -287,7 +287,7 @@ foo ALL=(ALL) NOPASSWD:ALL ``` sudo chmod 755 /home/ ``` - Also verify that images files and the folder containing them also have the correct permissions. + Also verify that images files and the folder containing them also have the correct permissions. ## Setup VMs on the server **(Skip this step if you are using cEOS - the containers will be automatically setup in a later step.)** @@ -405,6 +405,37 @@ Once the topology has been created, we need to give the DUT an initial configura ``` ./testbed-cli.sh -t vtestbed.yaml -m veos_vtb deploy-mg vms-kvm-t0 veos_vtb password.txt ``` + +### IPv6-Only Management Network (Optional) + +If you want to configure the DUT with IPv6-only management (no IPv4 on the management interface), use the `--ipv6-only-mgmt` flag: + +1. **Set up the local NTP server** (required for IPv6-only management): + ``` + ./setup-ntp-server.sh start + ``` + This deploys a Chrony NTP server container on the management network that DUTs can reach via IPv6. + +2. **Generate minigraph with IPv6-only management**: + ``` + ./testbed-cli.sh -t vtestbed.yaml -m veos_vtb gen-mg vms-kvm-t0 veos_vtb password.txt --ipv6-only-mgmt + ``` + +3. **Deploy minigraph with IPv6-only management**: + ``` + ./testbed-cli.sh -t vtestbed.yaml -m veos_vtb deploy-mg vms-kvm-t0 veos_vtb password.txt --ipv6-only-mgmt + ``` + +4. **Access the DUT via IPv6**: + ``` + ssh admin@fec0::ffff:afa:1 + ``` + The IPv6 address is defined as `ansible_hostv6` in the inventory file (e.g., `veos_vtb`). + +For detailed information about IPv6-only management setup, including configuration options, NTP server setup, and troubleshooting, see [IPv6 Management Setup Guide](../ipv6-management-setup.md). + +--- + Verify the DUT is created successfully In your host run ``` From f677006f9c9f464b3eb073de326613d56416d17c Mon Sep 17 00:00:00 2001 From: opcoder0 <110003254+opcoder0@users.noreply.github.com> Date: Tue, 10 Feb 2026 06:42:58 +0000 Subject: [PATCH 02/10] Remove root dependency for chrony docker container Remove unused ipv6.yml file from vm_host Signed-off-by: opcoder0 <110003254+opcoder0@users.noreply.github.com> --- ansible/files/docker-chrony/Dockerfile | 16 ++++++--- ansible/files/docker-chrony/chrony.conf | 4 +++ ansible/group_vars/vm_host/ipv6.yml | 46 ------------------------- 3 files changed, 16 insertions(+), 50 deletions(-) delete mode 100644 ansible/group_vars/vm_host/ipv6.yml diff --git a/ansible/files/docker-chrony/Dockerfile b/ansible/files/docker-chrony/Dockerfile index 47fc4b40120..352b65d00a7 100644 --- a/ansible/files/docker-chrony/Dockerfile +++ b/ansible/files/docker-chrony/Dockerfile @@ -8,11 +8,14 @@ LABEL description="Chrony NTP server for SONiC virtual testbed management networ # Install chrony RUN apk add --no-cache chrony tzdata -# Create chrony directories -RUN mkdir -p /var/run/chrony /var/lib/chrony +# Create chrony directories and set permissions +# The chrony user/group is created by the chrony package +RUN mkdir -p /var/run/chrony /var/lib/chrony /var/log/chrony && \ + chown -R chrony:chrony /var/run/chrony /var/lib/chrony /var/log/chrony /etc/chrony # Copy configuration file COPY chrony.conf /etc/chrony/chrony.conf +RUN chown chrony:chrony /etc/chrony/chrony.conf # Expose NTP port (UDP 123) EXPOSE 123/udp @@ -21,5 +24,10 @@ EXPOSE 123/udp HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD chronyc tracking || exit 1 -# Run chrony in foreground -CMD ["chronyd", "-d", "-f", "/etc/chrony/chrony.conf"] +# Security: chronyd must start as root to bind to port 123 and set system time, +# but it drops privileges to the 'chrony' user immediately after initialization +# using the -u flag. This is the standard secure pattern for NTP daemons. +# nosemgrep: dockerfile.security.missing-user.missing-user + +# Run chrony in foreground, dropping privileges to chrony user after init +CMD ["chronyd", "-d", "-u", "chrony", "-f", "/etc/chrony/chrony.conf"] diff --git a/ansible/files/docker-chrony/chrony.conf b/ansible/files/docker-chrony/chrony.conf index b4cac364fad..c5f70eb0845 100644 --- a/ansible/files/docker-chrony/chrony.conf +++ b/ansible/files/docker-chrony/chrony.conf @@ -21,6 +21,10 @@ allow fec0::/10 allow 127.0.0.0/8 allow ::1/128 +# Allow chronyc commands from localhost (for status checks) +cmdallow 127.0.0.0/8 +cmdallow ::1/128 + # Record the rate at which the system clock gains/loses time driftfile /var/lib/chrony/drift diff --git a/ansible/group_vars/vm_host/ipv6.yml b/ansible/group_vars/vm_host/ipv6.yml deleted file mode 100644 index 61180245962..00000000000 --- a/ansible/group_vars/vm_host/ipv6.yml +++ /dev/null @@ -1,46 +0,0 @@ ---- -# IPv6-only management network configuration for virtual testbed (vm_host) -# This file contains IPv6 equivalents of management services when using --ipv6-only-mgmt -# -# Virtual testbed IPv6 addressing scheme: -# - Management network: fc00:2::/64 (ULA) -# - PTF backplane: fc0a::/64 (already defined in vm_host/main.yml as ptf_bp_ipv6) -# - DUT management: Uses ansible_hostv6 from inventory - -# IPv6 NTP servers -# Primary: Local NTP server on the testbed host (deployed via setup-ntp-server.sh) -# Fallback: Public IPv6 NTP servers -ntp_servers_ipv6: - - 'fec0::ffff:afa:2' # Local testbed NTP server (primary) - - '2610:20:6f15:15::27' # NIST NTP server (fallback) - -# IPv6 DNS servers -# Using public Google IPv6 DNS servers -dns_servers_ipv6: - - '2001:4860:4860::8888' # Google IPv6 DNS Primary - - '2001:4860:4860::8844' # Google IPv6 DNS Secondary - -# IPv6 syslog servers -# In virtual testbed, syslog typically goes to the PTF or test server -syslog_servers_ipv6: - - 'fc00:1::1' # Virtual testbed server IPv6 (ULA) - -# IPv6 TACACS servers -# Note: These will be overridden to use PTF IPv6 address when use_ptf_tacacs_server is enabled -tacacs_servers_ipv6: [] - -# IPv6 SNMP servers -snmp_servers_ipv6: - - 'fc00:1::1' # Virtual testbed server IPv6 (ULA) - -# TACACS settings for virtual testbed -tacacs_group_ipv6: 'testlab' -tacacs_passkey_ipv6: testing123 -tacacs_enabled_by_default_ipv6: false - -# IPv6 forced management routes -forced_mgmt_routes_ipv6: [ - 'fc00:1::/64', # Local test server network - 'fc00:2::/64', # Management network - 'fc0a::/64' # PTF backplane network -] From 7a0c02b89ee4255b7c5258c38906eff691b11954 Mon Sep 17 00:00:00 2001 From: opcoder0 <110003254+opcoder0@users.noreply.github.com> Date: Tue, 10 Feb 2026 06:49:23 +0000 Subject: [PATCH 03/10] Specify non root USER before running command Signed-off-by: opcoder0 <110003254+opcoder0@users.noreply.github.com> --- ansible/files/docker-chrony/Dockerfile | 20 +++++++++++--------- ansible/setup-ntp-server.sh | 4 ++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/ansible/files/docker-chrony/Dockerfile b/ansible/files/docker-chrony/Dockerfile index 352b65d00a7..8b1a8683296 100644 --- a/ansible/files/docker-chrony/Dockerfile +++ b/ansible/files/docker-chrony/Dockerfile @@ -5,14 +5,18 @@ FROM alpine:3.19 LABEL maintainer="SONiC Community" LABEL description="Chrony NTP server for SONiC virtual testbed management network" -# Install chrony -RUN apk add --no-cache chrony tzdata +# Install chrony, libcap for setcap +RUN apk add --no-cache chrony tzdata libcap # Create chrony directories and set permissions # The chrony user/group is created by the chrony package RUN mkdir -p /var/run/chrony /var/lib/chrony /var/log/chrony && \ chown -R chrony:chrony /var/run/chrony /var/lib/chrony /var/log/chrony /etc/chrony +# Set file capabilities on chronyd binary to allow binding to port 123 and setting time +# This allows running as non-root while still performing privileged operations +RUN setcap 'cap_net_bind_service,cap_sys_time+ep' /usr/sbin/chronyd + # Copy configuration file COPY chrony.conf /etc/chrony/chrony.conf RUN chown chrony:chrony /etc/chrony/chrony.conf @@ -20,14 +24,12 @@ RUN chown chrony:chrony /etc/chrony/chrony.conf # Expose NTP port (UDP 123) EXPOSE 123/udp -# Health check +# Health check - run as chrony user HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD chronyc tracking || exit 1 -# Security: chronyd must start as root to bind to port 123 and set system time, -# but it drops privileges to the 'chrony' user immediately after initialization -# using the -u flag. This is the standard secure pattern for NTP daemons. -# nosemgrep: dockerfile.security.missing-user.missing-user +# Run as non-root user for security +USER chrony -# Run chrony in foreground, dropping privileges to chrony user after init -CMD ["chronyd", "-d", "-u", "chrony", "-f", "/etc/chrony/chrony.conf"] +# Run chrony in foreground (no -u flag needed since already running as chrony) +CMD ["chronyd", "-d", "-f", "/etc/chrony/chrony.conf"] diff --git a/ansible/setup-ntp-server.sh b/ansible/setup-ntp-server.sh index 3c82f8e2029..72afd54d86e 100755 --- a/ansible/setup-ntp-server.sh +++ b/ansible/setup-ntp-server.sh @@ -119,12 +119,16 @@ start_container() { fi # Run the container with both IPv4 and IPv6 addresses + # Capabilities needed: + # SYS_TIME: to set system time + # NET_BIND_SERVICE: to bind to privileged NTP port 123 docker run -d \ --name "${CONTAINER_NAME}" \ --network "${NETWORK_NAME}" \ --ip "${NTP_IPV4}" \ --ip6 "${NTP_IPV6}" \ --cap-add SYS_TIME \ + --cap-add NET_BIND_SERVICE \ --restart unless-stopped \ "${IMAGE_NAME}" From 8ae0786220e03f5de40722eb9e498f354b5a13da Mon Sep 17 00:00:00 2001 From: opcoder0 <110003254+opcoder0@users.noreply.github.com> Date: Tue, 10 Feb 2026 06:59:00 +0000 Subject: [PATCH 04/10] Fix pipeline parameter Signed-off-by: opcoder0 <110003254+opcoder0@users.noreply.github.com> --- .azure-pipelines/pr_test_template.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.azure-pipelines/pr_test_template.yml b/.azure-pipelines/pr_test_template.yml index cfac4fac591..a17f780ab78 100644 --- a/.azure-pipelines/pr_test_template.yml +++ b/.azure-pipelines/pr_test_template.yml @@ -179,8 +179,6 @@ jobs: - template: run-test-elastictest-template.yml parameters: - ${{ each param in parameters.GLOBAL_PARAMS }}: - ${{ param.key }}: ${{ param.value }} TOPOLOGY: t0 SCRIPTS: $(SCRIPTS) MIN_WORKER: $(INSTANCE_NUMBER) @@ -189,7 +187,7 @@ jobs: KVM_IMAGE_BRANCH: $(BUILD_BRANCH) MGMT_BRANCH: $(BUILD_BRANCH) COMMON_EXTRA_PARAMS: "--disable_sai_validation " - STOP_ON_FAILURE: "false" + STOP_ON_FAILURE: "False" ${{ each param in parameters.OVERRIDE_PARAMS }}: ${{ param.key }}: ${{ param.value }} @@ -215,8 +213,6 @@ jobs: - template: run-test-elastictest-template.yml parameters: - ${{ each param in parameters.GLOBAL_PARAMS }}: - ${{ param.key }}: ${{ param.value }} TOPOLOGY: t1-lag SCRIPTS: $(SCRIPTS) MIN_WORKER: $(INSTANCE_NUMBER) @@ -225,7 +221,7 @@ jobs: KVM_IMAGE_BRANCH: $(BUILD_BRANCH) MGMT_BRANCH: $(BUILD_BRANCH) COMMON_EXTRA_PARAMS: "--disable_sai_validation " - STOP_ON_FAILURE: "false" + STOP_ON_FAILURE: "False" ${{ each param in parameters.OVERRIDE_PARAMS }}: ${{ param.key }}: ${{ param.value }} From c1fec13bf1b7aff624d0be7d789e9a835ee13f8f Mon Sep 17 00:00:00 2001 From: opcoder0 <110003254+opcoder0@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:50:25 +0000 Subject: [PATCH 05/10] undo group_vars change Signed-off-by: opcoder0 <110003254+opcoder0@users.noreply.github.com> --- ansible/group_vars/vm_host/ceos.yml | 6 +++--- ansible/group_vars/vm_host/creds.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ansible/group_vars/vm_host/ceos.yml b/ansible/group_vars/vm_host/ceos.yml index 7274e55059c..75dac55237a 100644 --- a/ansible/group_vars/vm_host/ceos.yml +++ b/ansible/group_vars/vm_host/ceos.yml @@ -1,6 +1,6 @@ -ceos_image_filename: cEOS64-lab-4.29.7M.tar -ceos_image_orig: ceosimage:4.29.7M -ceos_image: ceosimage:4.29.7M +ceos_image_filename: cEOS64-lab-4.32.5M.tar +ceos_image_orig: ceosimage:4.32.5M +ceos_image: ceosimage:4.32.5M-1 # Please update ceos_image_url to the actual URL of the cEOS image file in your environment. If the cEOS image file # is not available on test server, the cEOS image file will be downloaded from this URL. # The ceos_image_url can be a string as single URL or a list of strings as multiple URLs. If it is a list, the code diff --git a/ansible/group_vars/vm_host/creds.yml b/ansible/group_vars/vm_host/creds.yml index 38a23fc955e..327da266185 100644 --- a/ansible/group_vars/vm_host/creds.yml +++ b/ansible/group_vars/vm_host/creds.yml @@ -5,6 +5,6 @@ ansible_become_password: use_own_value # Use the following username/password variables to login to vm hosts # instead of the default variables (defined above). -vm_host_user: sagummaraj -vm_host_password: neo123*() -vm_host_become_password: neo123*() +vm_host_user: use_own_value +vm_host_password: use_own_value +vm_host_become_password: use_own_value From 87a167dc19a0e47294a7827746bbead0e1fa423f Mon Sep 17 00:00:00 2001 From: opcoder0 <110003254+opcoder0@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:56:35 +0000 Subject: [PATCH 06/10] undo veos_vtb change Signed-off-by: opcoder0 <110003254+opcoder0@users.noreply.github.com> --- ansible/veos_vtb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/veos_vtb b/ansible/veos_vtb index fbd07b0e5a4..847cdc9704e 100644 --- a/ansible/veos_vtb +++ b/ansible/veos_vtb @@ -373,8 +373,8 @@ vm_host_1: hosts: STR-ACS-VSERV-01: ansible_host: 172.17.0.1 - ansible_user: sagummaraj - vm_host_user: sagummaraj + ansible_user: use_own_value + vm_host_user: use_own_value vms_1: hosts: From e03c2bcf3df00c75544f2aed277bdfd7f317e01f Mon Sep 17 00:00:00 2001 From: opcoder0 <110003254+opcoder0@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:54:14 +0000 Subject: [PATCH 07/10] Pretest and sanity check changes for IPv6 only mgmt setup Changes required to get sanity check and pretests to pass in an IPv6 only mgmt environment. - Add -6 to run_tests.sh to run tests in IPv6 only mgmt setup - Add --ipv6_only_mgmt option to conftest - Modify base.py init method to detect IPv6 only mgmt setup and initialize mgmt_ip accordingly - Modify checks.py and reboot.py to use IPv6 address for pings Signed-off-by: opcoder0 <110003254+opcoder0@users.noreply.github.com> --- tests/common/devices/base.py | 29 ++++++++++++++++++--- tests/common/plugins/sanity_check/checks.py | 7 +++++ tests/common/reboot.py | 4 ++- tests/conftest.py | 8 ++++++ tests/run_tests.sh | 20 ++++++++++---- 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/tests/common/devices/base.py b/tests/common/devices/base.py index 9db40b94d7a..ef1fdde7f56 100644 --- a/tests/common/devices/base.py +++ b/tests/common/devices/base.py @@ -1,6 +1,7 @@ import inspect import json import logging +import os import collections from multiprocessing.pool import ThreadPool from pytest_ansible.results import AdHocResult, ModuleResult @@ -9,6 +10,17 @@ logger = logging.getLogger(__name__) + +def is_ipv6_only_mgmt(): + """Check if running in IPv6-only management mode. + + Returns True if SONIC_MGMT_IPV6_ONLY env var is set to '1' or 'true'. + This is set by run_tests.sh when -6 flag is passed, or by pytest + when --ipv6_only_mgmt option is used. + """ + return os.environ.get('SONIC_MGMT_IPV6_ONLY', '').lower() in ('1', 'true') + + # HACK: This is a hack for issue https://github.com/sonic-net/sonic-mgmt/issues/1941 and issue # https://github.com/ansible/pytest-ansible/issues/47 # Detailed root cause analysis of the issue: https://github.com/sonic-net/sonic-mgmt/issues/1941#issuecomment-670434790 @@ -46,11 +58,20 @@ def __init__(self, ansible_adhoc, hostname, *args, **kwargs): self.host = ansible_adhoc(connection='local', host_pattern=hostname)[hostname] else: self.host = ansible_adhoc(become=True, *args, **kwargs)[hostname] - self.mgmt_ip = self.host.options["inventory_manager"].get_host(hostname).vars["ansible_host"] - if "ansible_hostv6" in self.host.options["inventory_manager"].get_host(hostname).vars: - self.mgmt_ipv6 = self.host.options["inventory_manager"].get_host(hostname).vars["ansible_hostv6"] + host_vars = self.host.options["inventory_manager"].get_host(hostname).vars + ansible_host = host_vars.get("ansible_host") + ansible_hostv6 = host_vars.get("ansible_hostv6") + + # In IPv6-only management mode, use IPv6 address as the primary mgmt_ip + if is_ipv6_only_mgmt() and ansible_hostv6: + self.mgmt_ip = ansible_hostv6 + self.mgmt_ipv6 = ansible_hostv6 + # Keep IPv4 available for reference but it won't be used for connectivity + self._mgmt_ipv4 = ansible_host + logger.debug("IPv6-only management mode: using %s as mgmt_ip for %s", ansible_hostv6, hostname) else: - self.mgmt_ipv6 = None + self.mgmt_ip = ansible_host + self.mgmt_ipv6 = ansible_hostv6 self.hostname = hostname def __getattr__(self, module_name): diff --git a/tests/common/plugins/sanity_check/checks.py b/tests/common/plugins/sanity_check/checks.py index 19dce937d4b..f8b06448a19 100644 --- a/tests/common/plugins/sanity_check/checks.py +++ b/tests/common/plugins/sanity_check/checks.py @@ -1137,6 +1137,13 @@ def _check_ipv4_mgmt_to_dut(*args, **kwargs): results[dut.hostname] = check_result return + # Skip IPv4 check if mgmt_ip is an IPv6 address (IPv6-only management mode) + if is_ipv6_address(dut.mgmt_ip): + logger.info("%s is using IPv6 management address (%s). Skip the ipv4 mgmt reachability check." + % (dut.hostname, dut.mgmt_ip)) + results[dut.hostname] = check_result + return + # most of the testbed should reply within 10 ms, Set the timeout to 2 seconds to reduce the impact of delay. try: shell_result = localhost.shell("ping -c 2 -W 2 " + dut.mgmt_ip) diff --git a/tests/common/reboot.py b/tests/common/reboot.py index 8065aa38a18..f30fa51ecf5 100644 --- a/tests/common/reboot.py +++ b/tests/common/reboot.py @@ -769,7 +769,9 @@ def ssh_connection_with_retry(localhost, host_ip, port, delay, timeout): def collect_mgmt_config_by_console(duthost, localhost): logger.info("check if dut is pingable") - localhost.shell(f"ping -c 5 {duthost.mgmt_ip}", module_ignore_errors=True) + # Use ping6 for IPv6 addresses, ping for IPv4 + ping_cmd = "ping6" if ":" in str(duthost.mgmt_ip) else "ping" + localhost.shell(f"{ping_cmd} -c 5 {duthost.mgmt_ip}", module_ignore_errors=True) logger.info("Start: collect mgmt config by console") creds = creds_on_dut(duthost) diff --git a/tests/conftest.py b/tests/conftest.py index a29e1320f4f..f9ccf84789d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -131,6 +131,8 @@ def pytest_addoption(parser): parser.addoption("--testbed", action="store", default=None, help="testbed name") parser.addoption("--testbed_file", action="store", default=None, help="testbed file name") + parser.addoption("--ipv6_only_mgmt", action="store_true", default=False, + help="Use IPv6-only management network. DUT mgmt_ip will be set to IPv6 address.") parser.addoption("--uhd_config", action="store", help="Enable UHD config mode") parser.addoption("--save_uhd_config", action="store_true", help="Save UHD config mode") parser.addoption("--npu_dpu_startup", action="store_true", help="Startup NPU and DPUs and install configurations") @@ -336,6 +338,12 @@ def pytest_addoption(parser): def pytest_configure(config): + # Set environment variable for IPv6-only management mode + # This allows base.py and other modules to check the mode without needing pytest request object + if config.getoption("ipv6_only_mgmt", default=False): + os.environ['SONIC_MGMT_IPV6_ONLY'] = '1' + logger.info("IPv6-only management mode enabled via --ipv6_only_mgmt") + if config.getoption("enable_macsec"): topo = config.getoption("topology") if topo is not None and "t2" in topo: diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 6627fe62f4e..a98ba03aa28 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -31,6 +31,7 @@ function show_help_and_exit() echo " -u : bypass util group" echo " -w : warm run, don't clear cache before running tests" echo " -x : print commands and their arguments as they are executed" + echo " -6 : IPv6-only management mode (use IPv6 for DUT mgmt connectivity)" exit $1 } @@ -124,6 +125,7 @@ function setup_environment() TEST_MAX_FAIL=0 DPU_NAME="None" NO_CLEAR_CACHE="False" + IPV6_ONLY_MGMT="False" export ANSIBLE_CONFIG=${BASE_PATH}/ansible export ANSIBLE_LIBRARY=${BASE_PATH}/ansible/library/ @@ -205,6 +207,10 @@ function setup_test_options() PYTEST_COMMON_OPTS="${PYTEST_COMMON_OPTS} --allow_recover" fi + if [[ x"${IPV6_ONLY_MGMT}" == x"True" ]]; then + PYTEST_COMMON_OPTS="${PYTEST_COMMON_OPTS} --ipv6_only_mgmt" + fi + for skip in ${SKIP_SCRIPTS} ${SKIP_FOLDERS}; do if [[ $skip == *"::"* ]]; then PYTEST_COMMON_OPTS="${PYTEST_COMMON_OPTS} --deselect=${skip}" @@ -314,15 +320,15 @@ function pre_post_extra_params() function prepare_dut() { echo "=== Preparing DUT for subsequent tests ===" - echo Running: ${PYTEST_EXEC} ${PYTEST_UTIL_OPTS} ${PRET_LOGGING_OPTIONS} ${UTIL_TOPOLOGY_OPTIONS} $(pre_post_extra_params) -m pretest - ${PYTEST_EXEC} ${PYTEST_UTIL_OPTS} ${PRET_LOGGING_OPTIONS} ${UTIL_TOPOLOGY_OPTIONS} $(pre_post_extra_params) -m pretest + echo Running: ${PYTEST_EXEC} . ${PYTEST_UTIL_OPTS} ${PRET_LOGGING_OPTIONS} ${UTIL_TOPOLOGY_OPTIONS} $(pre_post_extra_params) -m pretest + ${PYTEST_EXEC} . ${PYTEST_UTIL_OPTS} ${PRET_LOGGING_OPTIONS} ${UTIL_TOPOLOGY_OPTIONS} $(pre_post_extra_params) -m pretest } function cleanup_dut() { echo "=== Cleaning up DUT after tests ===" - echo Running: ${PYTEST_EXEC} ${PYTEST_UTIL_OPTS} ${POST_LOGGING_OPTIONS} ${UTIL_TOPOLOGY_OPTIONS} $(pre_post_extra_params) -m posttest - ${PYTEST_EXEC} ${PYTEST_UTIL_OPTS} ${POST_LOGGING_OPTIONS} ${UTIL_TOPOLOGY_OPTIONS} $(pre_post_extra_params) -m posttest + echo Running: ${PYTEST_EXEC} . ${PYTEST_UTIL_OPTS} ${POST_LOGGING_OPTIONS} ${UTIL_TOPOLOGY_OPTIONS} $(pre_post_extra_params) -m posttest + ${PYTEST_EXEC} . ${PYTEST_UTIL_OPTS} ${POST_LOGGING_OPTIONS} ${UTIL_TOPOLOGY_OPTIONS} $(pre_post_extra_params) -m posttest } function run_group_tests() @@ -423,7 +429,7 @@ for arg in "$@"; do fi done -while getopts "h?a:b:Bc:C:d:e:Ef:F:H:i:I:k:l:m:n:oOp:q:rs:S:t:uxw" opt; do +while getopts "h?a:b:Bc:C:d:e:Ef:F:H:i:I:k:l:m:n:oOp:q:rs:S:t:uxw6" opt; do case ${opt} in h|\? ) show_help_and_exit 0 @@ -513,6 +519,10 @@ while getopts "h?a:b:Bc:C:d:e:Ef:F:H:i:I:k:l:m:n:oOp:q:rs:S:t:uxw" opt; do x ) set -x ;; + 6 ) + IPV6_ONLY_MGMT="True" + export SONIC_MGMT_IPV6_ONLY=1 + ;; esac done From 980131887d64f9c86fd68c4e99b673946d0c8497 Mon Sep 17 00:00:00 2001 From: opcoder0 <110003254+opcoder0@users.noreply.github.com> Date: Fri, 13 Feb 2026 05:13:55 +0000 Subject: [PATCH 08/10] Remove use of environment variable and use fixture and global variable instead Signed-off-by: opcoder0 <110003254+opcoder0@users.noreply.github.com> --- tests/common/devices/base.py | 25 +++++++++++++++++++------ tests/conftest.py | 31 ++++++++++++++++++++++++------- tests/run_tests.sh | 1 - 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/tests/common/devices/base.py b/tests/common/devices/base.py index ef1fdde7f56..97da870f8e4 100644 --- a/tests/common/devices/base.py +++ b/tests/common/devices/base.py @@ -1,7 +1,6 @@ import inspect import json import logging -import os import collections from multiprocessing.pool import ThreadPool from pytest_ansible.results import AdHocResult, ModuleResult @@ -11,14 +10,28 @@ logger = logging.getLogger(__name__) -def is_ipv6_only_mgmt(): +# Module-level flag for IPv6-only management mode. +# This is set by the ipv6_only_mgmt_enabled fixture in conftest.py +_ipv6_only_mgmt_enabled = False + + +def set_ipv6_only_mgmt(enabled: bool): + """Set the IPv6-only management mode flag. + + Called by the ipv6_only_mgmt_enabled fixture in conftest.py. + """ + global _ipv6_only_mgmt_enabled + _ipv6_only_mgmt_enabled = enabled + if enabled: + logger.info("IPv6-only management mode enabled") + + +def is_ipv6_only_mgmt() -> bool: """Check if running in IPv6-only management mode. - Returns True if SONIC_MGMT_IPV6_ONLY env var is set to '1' or 'true'. - This is set by run_tests.sh when -6 flag is passed, or by pytest - when --ipv6_only_mgmt option is used. + Returns True if --ipv6_only_mgmt pytest option was passed. """ - return os.environ.get('SONIC_MGMT_IPV6_ONLY', '').lower() in ('1', 'true') + return _ipv6_only_mgmt_enabled # HACK: This is a hack for issue https://github.com/sonic-net/sonic-mgmt/issues/1941 and issue diff --git a/tests/conftest.py b/tests/conftest.py index f9ccf84789d..13cd83f3f38 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -338,12 +338,6 @@ def pytest_addoption(parser): def pytest_configure(config): - # Set environment variable for IPv6-only management mode - # This allows base.py and other modules to check the mode without needing pytest request object - if config.getoption("ipv6_only_mgmt", default=False): - os.environ['SONIC_MGMT_IPV6_ONLY'] = '1' - logger.info("IPv6-only management mode enabled via --ipv6_only_mgmt") - if config.getoption("enable_macsec"): topo = config.getoption("topology") if topo is not None and "t2" in topo: @@ -352,6 +346,28 @@ def pytest_configure(config): config.pluginmanager.register(MacsecPluginT0()) +@pytest.fixture(scope="session") +def ipv6_only_mgmt_enabled(request): + """ + Fixture to check and configure IPv6-only management mode. + + When --ipv6_only_mgmt is passed to pytest (or -6 to run_tests.sh), + this enables IPv6-only management mode where DUT mgmt_ip will use + the IPv6 address (ansible_hostv6) instead of IPv4 (ansible_host). + + This fixture must be used before duthosts fixture to ensure the + mode is configured before SonicHost objects are created. + + Returns: + bool: True if IPv6-only management mode is enabled, False otherwise. + """ + from tests.common.devices.base import set_ipv6_only_mgmt + + enabled = request.config.getoption("ipv6_only_mgmt", default=False) + set_ipv6_only_mgmt(enabled) + return enabled + + @pytest.fixture(scope="session", autouse=True) def enhance_inventory(request, tbinfo): """ @@ -525,7 +541,7 @@ def pytest_sessionfinish(session, exitstatus): @pytest.fixture(name="duthosts", scope="session") -def fixture_duthosts(enhance_inventory, ansible_adhoc, tbinfo, request): +def fixture_duthosts(enhance_inventory, ansible_adhoc, tbinfo, request, ipv6_only_mgmt_enabled): """ @summary: fixture to get DUT hosts defined in testbed. @param enhance_inventory: fixture to enhance the capability of parsing the value of pytest cli argument @@ -534,6 +550,7 @@ def fixture_duthosts(enhance_inventory, ansible_adhoc, tbinfo, request): mandatory argument for the class constructors. @param tbinfo: fixture provides information about testbed. @param request: pytest request object + @param ipv6_only_mgmt_enabled: fixture to configure IPv6-only management mode before DUT initialization """ try: host = DutHosts(ansible_adhoc, tbinfo, request, get_specified_duts(request), diff --git a/tests/run_tests.sh b/tests/run_tests.sh index a98ba03aa28..4db696cfeb6 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -521,7 +521,6 @@ while getopts "h?a:b:Bc:C:d:e:Ef:F:H:i:I:k:l:m:n:oOp:q:rs:S:t:uxw6" opt; do ;; 6 ) IPV6_ONLY_MGMT="True" - export SONIC_MGMT_IPV6_ONLY=1 ;; esac done From 94674cea998777ae763cc4210fe17060868643d3 Mon Sep 17 00:00:00 2001 From: opcoder0 <110003254+opcoder0@users.noreply.github.com> Date: Fri, 13 Feb 2026 06:04:42 +0000 Subject: [PATCH 09/10] Update documentation Signed-off-by: opcoder0 <110003254+opcoder0@users.noreply.github.com> --- docs/ipv6-management-setup.md | 49 ++++++++++++++++++++++++++ docs/testbed/README.testbed.VsSetup.md | 21 +++++++++++ 2 files changed, 70 insertions(+) diff --git a/docs/ipv6-management-setup.md b/docs/ipv6-management-setup.md index 25473d9c30c..11ca7317db8 100644 --- a/docs/ipv6-management-setup.md +++ b/docs/ipv6-management-setup.md @@ -268,3 +268,52 @@ To switch back to IPv4 management, simply run `deploy-mg` without the `--ipv6-on ``` This will regenerate and deploy a minigraph with IPv4-only management configuration. + +## Running Tests in IPv6-Only Management Mode + +After deploying the DUT with IPv6-only management, you need to run tests with the `-6` flag to ensure the test framework uses IPv6 for management connectivity. + +### Using run_tests.sh + +Add the `-6` flag to your `run_tests.sh` command: + +```bash +cd tests/ + +# Basic test execution with IPv6-only management +./run_tests.sh -6 -n vms-kvm-t0 -d vlab-01 -c bgp/test_bgp_fact.py -f vtestbed.yaml -i ../ansible/veos_vtb + +# With additional options +./run_tests.sh -6 -n vms-kvm-t0 -d vlab-01 -c platform_tests/test_reboot.py -f vtestbed.yaml -i ../ansible/veos_vtb -m individual -t t0,any +``` + +### Using pytest directly + +Alternatively, use the `--ipv6_only_mgmt` pytest option: + +```bash +pytest --ipv6_only_mgmt \ + --testbed vms-kvm-t0 \ + --testbed_file ../ansible/vtestbed.yaml \ + --inventory ../ansible/veos_vtb \ + --host-pattern vlab-01 \ + bgp/test_bgp_fact.py +``` + +### What the `-6` flag does + +When IPv6-only management mode is enabled: + +1. **Management IP**: `dut.mgmt_ip` returns the IPv6 address (`ansible_hostv6`) instead of IPv4 (`ansible_host`) +2. **Sanity Checks**: The IPv4 management ping check is skipped when `mgmt_ip` is an IPv6 address +3. **Reboot Operations**: Ping commands use `ping6` for IPv6 management addresses +4. **Connection Handling**: All SSH connections use the IPv6 management address + +### Verifying IPv6-Only Mode + +You can verify tests are running in IPv6-only mode by checking the test logs: + +``` +INFO Using IPv6-only management mode: using fec0::ffff:afa:1 as mgmt_ip for vlab-01 +INFO vlab-01 is using IPv6 management address (fec0::ffff:afa:1). Skip the ipv4 mgmt reachability check. +``` diff --git a/docs/testbed/README.testbed.VsSetup.md b/docs/testbed/README.testbed.VsSetup.md index c76fe81cef8..47e3086fb6b 100644 --- a/docs/testbed/README.testbed.VsSetup.md +++ b/docs/testbed/README.testbed.VsSetup.md @@ -550,6 +550,27 @@ If neighbor devices are SONiC You should see three sets of tests run and pass. You're now set up and ready to use the KVM testbed! +### Running Tests with IPv6-Only Management + +If you deployed the DUT with `--ipv6-only-mgmt` (see [IPv6-Only Management Network](#ipv6-only-management-network-optional)), you must run tests with the `-6` flag: + +``` +./run_tests.sh -6 -n vms-kvm-t0 -d vlab-01 -c bgp/test_bgp_fact.py -f vtestbed.yaml -i ../ansible/veos_vtb +``` + +The `-6` flag tells the test framework to: +- Use the IPv6 address (`ansible_hostv6`) as the DUT management IP +- Skip IPv4 management connectivity checks +- Use `ping6` for reachability tests + +Alternatively, you can use pytest directly with the `--ipv6_only_mgmt` option: + +``` +pytest --ipv6_only_mgmt --testbed vms-kvm-t0 --testbed_file vtestbed.yaml --inventory ../ansible/veos_vtb --host-pattern vlab-01 bgp/test_bgp_fact.py +``` + +For more details, see [IPv6 Management Setup Guide](../ipv6-management-setup.md#running-tests-in-ipv6-only-management-mode). + ## Restore/Remove the testing environment If you want to clear your testing environment, you can log into your mgmt docker that you created at step three in section [README.testbed.VsSetup.md#prepare-testbed-host](README.testbed.VsSetup.md#prepare-testbed-host). From 2f72bf73d0d7341c5ad8fdac1279f3eb8378d5d3 Mon Sep 17 00:00:00 2001 From: opcoder0 <110003254+opcoder0@users.noreply.github.com> Date: Fri, 13 Feb 2026 06:08:19 +0000 Subject: [PATCH 10/10] Remove pr_test_template changes Signed-off-by: opcoder0 <110003254+opcoder0@users.noreply.github.com> --- .azure-pipelines/pr_test_template.yml | 68 --------------------------- 1 file changed, 68 deletions(-) diff --git a/.azure-pipelines/pr_test_template.yml b/.azure-pipelines/pr_test_template.yml index a17f780ab78..4d0ee740ad6 100644 --- a/.azure-pipelines/pr_test_template.yml +++ b/.azure-pipelines/pr_test_template.yml @@ -157,74 +157,6 @@ jobs: ${{ each param in parameters.OVERRIDE_PARAMS }}: ${{ param.key }}: ${{ param.value }} - - job: impacted_area_t0_ipv6_mgmt_elastictest - displayName: "impacted-area-kvmtest-t0-ipv6-mgmt by Elastictest" - dependsOn: - - get_impacted_area - condition: contains(dependencies.get_impacted_area.outputs['SetVariableTask.PR_CHECKERS'], 't0_checker') - variables: - TEST_SCRIPTS: $[ dependencies.get_impacted_area.outputs['SetVariableTask.TEST_SCRIPTS'] ] - timeoutInMinutes: ${{ parameters.TIMEOUT_IN_MINUTES_PR_TEST }} - continueOnError: true - pool: ${{ parameters.AGENT_POOL }} - steps: - - ${{ if eq(parameters.CHECKOUT_SONIC_MGMT, true) }}: - - checkout: ${{ parameters.SONIC_MGMT_NAME }} - displayName: "Checkout sonic-mgmt repository" - - - template: impacted_area_testing/calculate-instance-numbers.yml - parameters: - TOPOLOGY: t0 - BUILD_BRANCH: $(BUILD_BRANCH) - - - template: run-test-elastictest-template.yml - parameters: - TOPOLOGY: t0 - SCRIPTS: $(SCRIPTS) - MIN_WORKER: $(INSTANCE_NUMBER) - MAX_WORKER: $(INSTANCE_NUMBER) - DEPLOY_MG_EXTRA_PARAMS: "--ipv6-only-mgmt" - KVM_IMAGE_BRANCH: $(BUILD_BRANCH) - MGMT_BRANCH: $(BUILD_BRANCH) - COMMON_EXTRA_PARAMS: "--disable_sai_validation " - STOP_ON_FAILURE: "False" - ${{ each param in parameters.OVERRIDE_PARAMS }}: - ${{ param.key }}: ${{ param.value }} - - - job: impacted_area_t1_lag_ipv6_mgmt_elastictest - displayName: "impacted-area-kvmtest-t1-lag-ipv6-mgmt by Elastictest" - dependsOn: - - get_impacted_area - condition: contains(dependencies.get_impacted_area.outputs['SetVariableTask.PR_CHECKERS'], 't1_checker') - variables: - TEST_SCRIPTS: $[ dependencies.get_impacted_area.outputs['SetVariableTask.TEST_SCRIPTS'] ] - timeoutInMinutes: ${{ parameters.TIMEOUT_IN_MINUTES_PR_TEST }} - continueOnError: true - pool: ${{ parameters.AGENT_POOL }} - steps: - - ${{ if eq(parameters.CHECKOUT_SONIC_MGMT, true) }}: - - checkout: ${{ parameters.SONIC_MGMT_NAME }} - displayName: "Checkout sonic-mgmt repository" - - - template: impacted_area_testing/calculate-instance-numbers.yml - parameters: - TOPOLOGY: t1 - BUILD_BRANCH: $(BUILD_BRANCH) - - - template: run-test-elastictest-template.yml - parameters: - TOPOLOGY: t1-lag - SCRIPTS: $(SCRIPTS) - MIN_WORKER: $(INSTANCE_NUMBER) - MAX_WORKER: $(INSTANCE_NUMBER) - DEPLOY_MG_EXTRA_PARAMS: "--ipv6-only-mgmt" - KVM_IMAGE_BRANCH: $(BUILD_BRANCH) - MGMT_BRANCH: $(BUILD_BRANCH) - COMMON_EXTRA_PARAMS: "--disable_sai_validation " - STOP_ON_FAILURE: "False" - ${{ each param in parameters.OVERRIDE_PARAMS }}: - ${{ param.key }}: ${{ param.value }} - - job: impacted_area_dualtor_elastictest displayName: "impacted-area-kvmtest-dualtor by Elastictest" dependsOn: