diff --git a/tests/conftest.py b/tests/conftest.py index 7ef61ae8ee8..f5aab2b788b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,7 +31,8 @@ 'tests.common.plugins.sanity_check', 'tests.common.plugins.custom_markers', 'tests.common.plugins.test_completeness', - 'tests.common.plugins.log_section_start') + 'tests.common.plugins.log_section_start', + 'tests.vxlan') class TestbedInfo(object): diff --git a/tests/vxlan/__init__.py b/tests/vxlan/__init__.py new file mode 100644 index 00000000000..b14a55aa66d --- /dev/null +++ b/tests/vxlan/__init__.py @@ -0,0 +1,127 @@ +import pytest +import yaml + +from os import path +from vnet_utils import combine_dicts, safe_open_template + +from vnet_constants import * + +def pytest_addoption(parser): + """ + Adds pytest options that are used by VxLAN tests + """ + + parser.addoption( + "--num_vnet", + action="store", + default=None, + type=int, + help="number of VNETs for VNET VxLAN test" + ) + + parser.addoption( + "--num_routes", + action="store", + default=None, + type=int, + help="number of routes for VNET VxLAN test" + ) + + parser.addoption( + "--num_endpoints", + action="store", + default=None, + type=int, + help="number of endpoints for VNET VxLAN" + ) + + parser.addoption( + "--ipv6_vxlan_test", + action="store_true", + help="Use IPV6 for VxLAN test" + ) + + parser.addoption( + "--skip_cleanup", + action="store_true", + help="Do not cleanup after VNET VxLAN test" + ) + + parser.addoption( + "--skip_apply_config", + action="store_true", + help="Apply new configurations on DUT" + ) + +@pytest.fixture(scope="module") +def scaled_vnet_params(request): + """ + Fixture to get CLI parameters for scaled vnet testing + + Args: + request: Pytest fixture containing parsed CLI parameters + + Returns: + A dictionary holding each scaled vnet parameter with the parameter name as the key + * num_vnet + * num_routes + * num_endpoints + """ + + params = {} + params[NUM_VNET_KEY] = request.config.option.num_vnet + params[NUM_ROUTES_KEY] = request.config.option.num_routes + params[NUM_ENDPOINTS_KEY] = request.config.option.num_endpoints + return params + +@pytest.fixture(scope="module") +def vnet_test_params(request): + """ + Fixture to get CLI parameters for vnet testing + + Args: + request: Pytest fixture containing parsed CLI parameters + + Returns: + A dictionary holding each parameter with the parameter name as the key + * ipv6_vxlan_test - whether to include ipv6 functionality in testing + * cleanup - whether to remove test data/configs after test is finished + * apply_new_config - whether to apply new configurations that were pushed to the DUT + """ + + params = {} + params[IPV6_VXLAN_TEST_KEY] = request.config.option.ipv6_vxlan_test + params[CLEANUP_KEY] = not request.config.option.skip_cleanup + params[APPLY_NEW_CONFIG_KEY] = not request.config.option.skip_apply_config + return params + +@pytest.fixture(scope="module") +def minigraph_facts(duthost): + """ + Fixture to get minigraph facts + + Args: + duthost: DUT host object + + Returns: + Dictionary containing minigraph information + """ + + return duthost.minigraph_facts(host=duthost.hostname)["ansible_facts"] + +@pytest.fixture(scope="module") +def vnet_config(minigraph_facts, vnet_test_params, scaled_vnet_params): + """ + Fixture to generate vnet configuration from templates/vnet_config.j2 + + Args: + minigraph_facts: minigraph information/facts + vnet_test_params: Dictionary holding vnet test parameters + scaled_vnet_params: Dictionary holding scaled vnet testing parameters + + Returns: + A dictionary containing the generated vnet configuration information + """ + + combined_args = combine_dicts(minigraph_facts, vnet_test_params, scaled_vnet_params) + return yaml.safe_load(safe_open_template(path.join(TEMPLATE_DIR, "vnet_config.j2")).render(combined_args)) diff --git a/tests/vxlan/templates/vnet_config.j2 b/tests/vxlan/templates/vnet_config.j2 new file mode 100644 index 00000000000..c1b2636e1c2 --- /dev/null +++ b/tests/vxlan/templates/vnet_config.j2 @@ -0,0 +1,278 @@ +#jinja2: trim_blocks: True,lstrip_blocks: True +{# + For normal functional test, no need to pass the below configurations. + num_vnet - Default 8. + num_routes - (including peer & local routes) + value 24 - if ipv6_vxlan_test == true + value 12 - if ipv6_vxlan_test == false + num_endpoints - Default 8 + + For Scale test, below configurations are to be passed. + num_vnet - Mandatory for scale test. + num_routes - Optional. Default 16000 for scale test. + num_endpoints - Optional. Default 4000 for scale test. + Ex: ansible playbook extra vars: -e "num_vnet=51 num_routes=550 num_endpoints=200" + + Ethernet IP - 10.10.10.1/24 + Vlan IP - A.B.10.1/24; starts 30.1.10.1/24 (A,B derived from vlan id starting 3001) + Route Pfx - A.B.C.D/32; starts 100.1.1.1/32 + (A,B derived from 100+vnet_id; C,D derived from number of routes per vnet) + Route endpoint - A.B.C.D; starts from 200.1.1.1 (C,D derived from number of endpoints) + +#} +{% set vnet_v6_base = 4 %} +{% set vlan_id_base = 3001 %} + +{# A single vnet batch contains 8 Vnets #} +{% set num_vnet_batch = 8 %} +num_vnet_batch: {{ num_vnet_batch }} + +{# A single vnet batch has total 18 routes and 8 endpoints including peers if ipv6_vxlan_test == true. + There will be only 9 routes and 4 endpoints if ipv6_vxlan_test == false #} +{% if ipv6_vxlan_test == true %} + {% set num_routes_batch = 18 %} + {% set num_endpoints_batch = 8 %} +{% else %} + {% set num_routes_batch = 9 %} + {% set num_endpoints_batch = 4 %} +{% endif %} + +{# Normal testing - 8 Vnets #} +{% if num_vnet is undefined or not num_vnet %} + {% set num_vnet = 8 %} + {% set num_routes_iterations = 1 %} + {% set num_endpoints = 4000 %} +{% endif %} +{% set num_vnet = num_vnet|int %} + +{# Convert num_vnet to a divisible of 8 since one batch has 8 vnets #} +{% set num_vnet = ((num_vnet//num_vnet_batch)|int)*num_vnet_batch %} + +{% if num_vnet <= 8 %} + {% set num_vnet = 8 %} + {% set num_routes_iterations = 1 %} + {% set num_endpoints = 4000 %} +{% else %} + {# Scale testing - Determine the number of routes per each Vnet batch (8 Vnets) #} + {% if num_routes is undefined or not num_routes %} + {% set num_routes = 16000 %} + {% endif %} + {% if num_endpoints is undefined or not num_endpoints %} + {% set num_endpoints = 4000 %} + {% endif %} + {% set num_routes = num_routes|int %} + {% set num_endpoints = num_endpoints|int %} + {% set num_routes_iterations = ((num_routes/num_routes_batch)/(num_vnet/num_vnet_batch))|round|int %} + {% if num_routes_iterations == 0 %} {% set num_routes_iterations = 1 %} {% endif %} +{% endif %} +{% set topo_vlan = minigraph_vlans.keys()[0] %} + +{# Max RIFs support currently is 128 #} +{% if num_vnet > 128 %} + {% set max_rif = 128 %} +{% else %} + {% set max_rif = num_vnet %} +{% endif %} + +{# Vlan Configurations + Vlan IP - A.B.10.1/24; starts 30.1.10.1/24 (A,B derived from vlan id) +#} +vlan_intf_list: +{% set ports = minigraph_vlans[topo_vlan].members[1:] %} +{% for vlan in range (vlan_id_base, vlan_id_base + num_vnet) %} + {% set vlan_str = (vlan-1)|string %} + {% set ip_a, ip_b = vlan_str[:2]|int, (vlan_str[2:]|int)+1 %} + - vlan_id: '{{ (vlan|int) }}' + ifname: 'Vlan{{ vlan }}' + ip: '{{ ip_a }}.{{ ip_b }}.10.1/24' + port: '{{ ports[loop.index0%(ports|length)] }}' +{% endfor %} + +{# Interface Configuration #} +intf_list: +{% set index = 10 %} + - ifname: {{ minigraph_vlans[topo_vlan].members[0] }} + ip: '{{ index }}.{{ index }}.10.1/24' + +{# Vnet Configurations #} +vnet_id_list: +{% for vnet in range (1, 1 + num_vnet) %} + - Vnet{{ vnet }} +{% endfor %} + +{% if ipv6_vxlan_test == true %} +vnet_v6_base: {{ vnet_v6_base }} +{% endif %} + +{# Vnet - Peer Configurations #} +vnet_peer_list: +{% for vnet_batch in range (1, 1 + max_rif)|batch(4) %} + - Vnet{{ vnet_batch[2] }}: Vnet{{ vnet_batch[3] }} + - Vnet{{ vnet_batch[3] }}: Vnet{{ vnet_batch[2] }} +{% endfor %} +{% if num_vnet > max_rif %} + {% set peering_vnets = num_vnet - max_rif %} + {% for vnet_id in range (max_rif + 1, num_vnet + 1) %} + {% set peer_vnet = (loop.index0 % max_rif) + 1 %} + - Vnet{{ vnet_id }}: Vnet{{ peer_vnet }} + - Vnet{{ peer_vnet }}: Vnet{{ vnet_id }} + {% endfor %} +{% endif %} + +{# Vnet - Interface Configurations #} +vnet_intf_list: +{% for vlan in range (vlan_id_base, vlan_id_base + max_rif) %} + - ifname: Vlan{{ vlan }} + vnet: Vnet{{ loop.index }} +{% endfor %} + +{# Vnet - Neighbor Configurations #} +vnet_nbr_list: +{% for vlan_batch in range (vlan_id_base, vlan_id_base + num_vnet)|batch(4) %} + {% for vlan in vlan_batch %} + {% set vlan_str = (vlan-1)|string %} + {% set ip_a, ip_b = vlan_str[:2]|int, (vlan_str[2:]|int)+1 %} + - ifname: Vlan{{ vlan }} + ip: '{{ ip_a }}.{{ ip_b }}.10.101' + {% if (loop.index0 == 0) or (loop.index0 == 1) %} + - ifname: Vlan{{ vlan }} + ip: '{{ ip_a }}.{{ ip_b }}.10.102' + {% endif %} + {% endfor %} +{% endfor %} + - ifname: {{ minigraph_vlans[topo_vlan].members[0] }} + ip: '10.10.10.102' + +{# Vnet - Local Routes #} +vnet_local_routes: +{% for vlan_batch in range (vlan_id_base, vlan_id_base + max_rif)|batch(4) %} + - Vnet{{ vlan_batch[0]-vlan_id_base+1 }}_route_list: + {% set vlan_str = (vlan_batch[0]-1)|string %} + {% set ip_a, ip_b = vlan_str[:2]|int, (vlan_str[2:]|int)+1 %} + - pfx: '50.1.1.0/24' + nh: '{{ ip_a }}.{{ ip_b }}.10.101' + ifname: 'Vlan{{ vlan_batch[0] }}' + - pfx: '50.2.2.0/24' + nh: '{{ ip_a }}.{{ ip_b }}.10.102' + ifname: 'Vlan{{ vlan_batch[0] }}' + - Vnet{{ vlan_batch[1]-vlan_id_base+1 }}_route_list: + {% set vlan_str = (vlan_batch[1]-1)|string %} + {% set ip_a, ip_b = vlan_str[:2]|int, (vlan_str[2:]|int)+1 %} + - pfx: '60.1.1.0/24' + nh: '{{ ip_a }}.{{ ip_b }}.10.101' + ifname: 'Vlan{{ vlan_batch[1] }}' +{% endfor %} + +{# Vnet - Routes + Route Pfx - A.B.C.D/32; starts 100.1.1.1/32 + (A,B derived from 100+vnet_id; C,D derived from number of routes per vnet) + Route endpoint - A.B.C.D; starts from 200.1.1.1 (C,D derived from number of endpoints) +#} +{% set endpoints_iters = (num_endpoints//(num_routes_iterations*num_endpoints_batch))|int %} +{% if (num_endpoints%(num_routes_iterations*num_endpoints_batch))|int > 0 %} + {% set endpoints_iters = endpoints_iters + 1 %} +{% endif %} + +vnet_route_list: +{% for vnet_batch in range (1, 1 + num_vnet)|batch(8) %} + {% set temp = (vnet_batch[0]-1)|int %} + {% set ip_r_a, ip_r_b = ((temp//100)|int)+100, ((temp%100)|int)+1 %} + {% set endpoints_batch_idx = (loop.index0%endpoints_iters)*num_routes_iterations*num_endpoints_batch %} + {% set temp_routes_iterations = num_routes_iterations %} + {% if ( (endpoints_batch_idx+(num_routes_iterations*num_endpoints_batch)) > num_endpoints ) %} + {% set temp_routes_iterations = ((num_endpoints-endpoints_batch_idx)//num_endpoints_batch)|int %} + {% endif %} + + - Vnet{{ vnet_batch[0] }}_route_list: + {% for idx in range (0, num_routes_iterations*2, 2) %} + {% set idx_a, idx_b = ((idx//100)|int)+1, ((idx%100)|int)+1 %} + {% set e_idx = endpoints_batch_idx+(idx%(temp_routes_iterations*2)) %} + {% set e_idx_a, e_idx_b = ((e_idx//100)|int)+1, ((e_idx%100)|int)+1 %} + + - pfx: '{{ ip_r_a }}.{{ ip_r_b }}.{{ idx_a }}.{{ idx_b }}/32' + end: '200.1.{{ e_idx_a }}.{{ e_idx_b }}' + - pfx: '{{ ip_r_a }}.{{ ip_r_b }}.{{ idx_a }}.{{ idx_b+1 }}/32' + end: '200.1.{{ e_idx_a }}.{{ e_idx_b+1 }}' + mac: '00:00:00:00:01:02' + {% endfor %} + - Vnet{{ vnet_batch[1] }}_route_list: + {% for idx in range (num_routes_iterations) %} + {% set idx_a, idx_b = ((idx//100)|int)+1, (idx%100)|int+1 %} + {% set e_idx = endpoints_batch_idx+(idx%temp_routes_iterations) %} + {% set e_idx_a, e_idx_b = ((e_idx//100)|int)+1, ((e_idx%100)|int)+1 %} + + - pfx: '{{ ip_r_a }}.{{ ip_r_b+1 }}.{{ idx_a }}.{{ idx_b }}/32' + end: '200.2.{{ e_idx_a }}.{{ e_idx_b }}' + mac: '00:00:00:00:02:05' + {% endfor %} + - Vnet{{ vnet_batch[2] }}_route_list: + {% for idx in range (0, num_routes_iterations*2, 2) %} + {% set idx_a, idx_b = ((idx//100)|int)+1, ((idx%100)|int)+1 %} + {% set e_idx = endpoints_batch_idx+(idx%(temp_routes_iterations*2)) %} + {% set e_idx_a, e_idx_b = ((e_idx//100)|int)+1, ((e_idx%100)|int)+1 %} + + - pfx: '{{ ip_r_a }}.{{ ip_r_b }}.{{ idx_a }}.{{ idx_b }}/32' + end: '200.1.{{ e_idx_a }}.{{ e_idx_b }}' + vni: '12345' + - pfx: '{{ ip_r_a }}.{{ ip_r_b }}.{{ idx_a }}.{{ idx_b+1 }}/32' + end: '200.1.{{ e_idx_a }}.{{ e_idx_b+1 }}' + mac: '00:00:00:00:01:02' + {% endfor %} + - Vnet{{ vnet_batch[3] }}_route_list: + {% for idx in range (num_routes_iterations) %} + {% set idx_a, idx_b = ((idx//100)|int)+1, (idx%100)|int+1 %} + {% set e_idx = endpoints_batch_idx+(idx%temp_routes_iterations) %} + {% set e_idx_a, e_idx_b = ((e_idx//100)|int)+1, ((e_idx%100)|int)+1 %} + + - pfx: '{{ ip_r_a }}.{{ ip_r_b+3 }}.{{ idx_a }}.{{ idx_b }}/32' + end: '200.4.{{ e_idx_a }}.{{ e_idx_b }}' + mac: '00:00:00:00:02:05' + {% endfor %} +{% if ipv6_vxlan_test == true %} + - Vnet{{ vnet_batch[4] }}_route_list: + {% for idx in range (0, num_routes_iterations*2, 2) %} + {% set idx_a, idx_b = ((idx//100)|int)+1, ((idx%100)|int)+1 %} + {% set e_idx = endpoints_batch_idx+(idx%(temp_routes_iterations*2)) %} + {% set e_idx_a, e_idx_b = ((e_idx//100)|int)+1, ((e_idx%100)|int)+1 %} + + - pfx: '{{ ip_r_a }}.{{ ip_r_b }}.{{ idx_a }}.{{ idx_b }}/32' + end: 'FC00:1::{{ e_idx_a }}:{{ e_idx_b }}' + - pfx: '{{ ip_r_a }}.{{ ip_r_b }}.{{ idx_a }}.{{ idx_b+1 }}/32' + end: 'FC00:1::{{ e_idx_a }}:{{ e_idx_b+1 }}' + mac: '00:00:00:00:01:02' + {% endfor %} + - Vnet{{ vnet_batch[5] }}_route_list: + {% for idx in range (num_routes_iterations) %} + {% set idx_a, idx_b = ((idx//100)|int)+1, (idx%100)|int+1 %} + {% set e_idx = endpoints_batch_idx+(idx%temp_routes_iterations) %} + {% set e_idx_a, e_idx_b = ((e_idx//100)|int)+1, ((e_idx%100)|int)+1 %} + + - pfx: '{{ ip_r_a }}.{{ ip_r_b+1 }}.{{ idx_a }}.{{ idx_b }}/32' + end: 'FC00:2::{{ e_idx_a }}:{{ e_idx_b }}' + mac: '00:00:00:00:02:05' + {% endfor %} + - Vnet{{ vnet_batch[6] }}_route_list: + {% for idx in range (0, num_routes_iterations*2, 2) %} + {% set idx_a, idx_b = ((idx//100)|int)+1, ((idx%100)|int)+1 %} + {% set e_idx = endpoints_batch_idx+(idx%(temp_routes_iterations*2)) %} + {% set e_idx_a, e_idx_b = ((e_idx//100)|int)+1, ((e_idx%100)|int)+1 %} + + - pfx: '{{ ip_r_a }}.{{ ip_r_b }}.{{ idx_a }}.{{ idx_b }}/32' + end: 'FC00:1::{{ e_idx_a }}:{{ e_idx_b }}' + vni: '12345' + - pfx: '{{ ip_r_a }}.{{ ip_r_b }}.{{ idx_a }}.{{ idx_b+1 }}/32' + end: 'FC00:1::{{ e_idx_a }}:{{ e_idx_b+1 }}' + mac: '00:00:00:00:01:02' + {% endfor %} + - Vnet{{ vnet_batch[7] }}_route_list: + {% for idx in range (num_routes_iterations) %} + {% set idx_a, idx_b = ((idx//100)|int)+1, (idx%100)|int+1 %} + {% set e_idx = endpoints_batch_idx+(idx%temp_routes_iterations) %} + {% set e_idx_a, e_idx_b = ((e_idx//100)|int)+1, ((e_idx%100)|int)+1 %} + + - pfx: '{{ ip_r_a }}.{{ ip_r_b+3 }}.{{ idx_a }}.{{ idx_b }}/32' + end: 'FC00:4::{{ e_idx_a }}:{{ e_idx_b }}' + mac: '00:00:00:00:02:05' + {% endfor %} +{% endif %} +{% endfor %} diff --git a/tests/vxlan/templates/vnet_interface.j2 b/tests/vxlan/templates/vnet_interface.j2 new file mode 100644 index 00000000000..e8d2c869456 --- /dev/null +++ b/tests/vxlan/templates/vnet_interface.j2 @@ -0,0 +1,12 @@ +{ + "VLAN_INTERFACE": { +{%- for vlan_intf in vlan_intf_list %} + "Vlan{{ vlan_intf.vlan_id }}|{{ vlan_intf.ip }}": {}{{ "," if not loop.last else "" }} +{%- endfor %} + }, + "INTERFACE": { +{%- for intf in intf_list %} + "{{ intf.ifname }}|{{ intf.ip }}": {}{{ "," if not loop.last else "" }} +{%- endfor %} + } +} diff --git a/tests/vxlan/templates/vnet_nbr.j2 b/tests/vxlan/templates/vnet_nbr.j2 new file mode 100644 index 00000000000..9793bc646ec --- /dev/null +++ b/tests/vxlan/templates/vnet_nbr.j2 @@ -0,0 +1,9 @@ +{ + "NEIGH": { +{%- for nbr in vnet_nbr_list %} + "{{ nbr.ifname }}|{{ nbr.ip }}": { + "family": "IPv4" + }{{ "," if not loop.last else "" }} +{%- endfor %} + } +} diff --git a/tests/vxlan/templates/vnet_routes.j2 b/tests/vxlan/templates/vnet_routes.j2 new file mode 100644 index 00000000000..ce855183435 --- /dev/null +++ b/tests/vxlan/templates/vnet_routes.j2 @@ -0,0 +1,102 @@ +[ +{%- for vnet in vnet_id_list %} +{%- for routes in vnet_route_list %} +{%- set route_list = vnet + '_route_list' %} +{%- if routes.keys()[0] == route_list %} +{%- for key,entries in routes.items() %} +{%- for route in entries %} + { + "VNET_ROUTE_TUNNEL_TABLE:{{ vnet }}:{{ route.pfx }}" : { +{%- if route.vni is defined %} + "vni": "{{ route.vni }}", +{%- endif %} +{%- if route.mac is defined %} + "mac_address": "{{ route.mac }}", +{%- endif %} + "endpoint": "{{ route.end }}" + }, + "OP": "{{ op }}" + }, +{%- for peers in vnet_peer_list %} +{%- for key,peer in peers.items() %} +{%- if key == vnet %} + { + "VNET_ROUTE_TUNNEL_TABLE:{{ peer }}:{{ route.pfx }}" : { +{%- if route.vni is defined %} + "vni": "{{ route.vni }}", +{%- endif %} +{%- if route.mac is defined %} + "mac_address": "{{ route.mac }}", +{%- endif %} + "endpoint": "{{ route.end }}" + }, + "OP": "{{ op }}" + }, +{%- endif %} +{%- endfor %} +{%- endfor %} +{%- endfor %} +{%- endfor %} +{%- endif %} +{%- endfor %} +{%- endfor %} + +{%- for vnet in vnet_id_list %} +{%- for routes in vnet_local_routes %} +{%- set route_list = vnet + '_route_list' %} +{%- if routes.keys()[0] == route_list %} +{%- for key,entries in routes.items() %} +{%- for route in entries %} + { + "VNET_ROUTE_TABLE:{{ vnet }}:{{ route.pfx }}" : { + "nexthop": "{{ route.nh }}", + "ifname": "{{ route.ifname }}" + }, + "OP": "{{ op }}" + }, +{%- endfor %} +{%- endfor %} +{%- endif %} +{%- endfor %} +{%- endfor %} + +{%- for intf in intf_list %} +{%- set outloop = loop %} +{%- for vnet_intf in vnet_intf_list %} +{%- if vnet_intf.ifname == intf.ifname %} + { + "VNET_ROUTE_TABLE:{{ vnet_intf.vnet }}:10.10.10.0/24": { + "ifname": "{{ intf.ifname }}" + }, + "OP": "{{ op }}" + }, +{%- endif %} +{%- endfor %} +{%- endfor %} +{%- for vlan_intf in vlan_intf_list %} +{%- set outloop = loop %} +{%- for vnet_intf in vnet_intf_list %} +{%- if vnet_intf.ifname == vlan_intf.ifname %} +{%- set vlan_intf_ip = vlan_intf.ip[:-5] %} +{%- for peers in vnet_peer_list %} +{%- for key,peer in peers.items() %} +{%- if key == vnet_intf.vnet %} + { + "VNET_ROUTE_TABLE:{{ peer }}:{{ vlan_intf_ip }}.0/24": { + "ifname": "Vlan{{ vlan_intf.vlan_id }}" + }, + "OP": "{{ op }}" + }, +{%- endif %} +{%- endfor %} +{%- endfor %} + { + "VNET_ROUTE_TABLE:{{ vnet_intf.vnet }}:{{ vlan_intf_ip }}.0/24": { + "ifname": "Vlan{{ vlan_intf.vlan_id }}" + }, + "OP": "{{ op }}" + }{{ "," if not outloop.last else "" }} +{%- endif %} +{%- endfor %} +{%- endfor %} +] diff --git a/tests/vxlan/templates/vnet_vxlan.j2 b/tests/vxlan/templates/vnet_vxlan.j2 new file mode 100644 index 00000000000..443f1c8827d --- /dev/null +++ b/tests/vxlan/templates/vnet_vxlan.j2 @@ -0,0 +1,54 @@ +{ + "VXLAN_TUNNEL": { +{%- if ipv6_vxlan_test == true %} + "tunnel_v6": { + "src_ip": "{{ minigraph_lo_interfaces[1]['addr'] }}" + }, +{%- endif %} + "tunnel_v4": { + "src_ip": "{{ minigraph_lo_interfaces[0]['addr'] }}" + } + }, + + "VNET": { +{%- for vnet in vnet_id_list %} + "{{ vnet }}": { +{%- if (ipv6_vxlan_test == false) or ((loop.index0%num_vnet_batch) < vnet_v6_base) %} + "vxlan_tunnel": "tunnel_v4", +{%- else %} + "vxlan_tunnel": "tunnel_v6", +{%- endif %} + "vni": "{{ vnet | replace("Vnet", "") |int + 10000}}", + "peer_list": "" + }{{ "," if not loop.last else "" }} +{%- endfor %} + }, + + "VLAN": { +{%- for vlan_intf in vlan_intf_list %} + "Vlan{{ vlan_intf.vlan_id }}": { + "vlanid": {{ vlan_intf.vlan_id }} + }{{ "," if not loop.last else "" }} +{%- endfor %} + }, + + "VLAN_INTERFACE": { +{%- for vlan_intf in vlan_intf_list %} +{%- for vnet_intf in vnet_intf_list %} +{%- if vnet_intf.ifname == vlan_intf.ifname %} + "Vlan{{ vlan_intf.vlan_id }}": { + "vnet_name": "{{ vnet_intf.vnet }}" + }{{ "," if not loop.last else "" }} +{%- endif %} +{%- endfor %} +{%- endfor %} + }, + + "VLAN_MEMBER": { +{%- for vlan_intf in vlan_intf_list %} + "Vlan{{ vlan_intf.vlan_id }}|{{ vlan_intf.port }}": { + "tagging_mode": "tagged" + }{{ "," if not loop.last else "" }} +{%- endfor %} + } +} diff --git a/tests/vxlan/test_vnet_vxlan.py b/tests/vxlan/test_vnet_vxlan.py new file mode 100644 index 00000000000..157f5521b99 --- /dev/null +++ b/tests/vxlan/test_vnet_vxlan.py @@ -0,0 +1,154 @@ +import json +import logging +import pytest + +from datetime import datetime +from tests.ptf_runner import ptf_runner +from vnet_constants import CLEANUP_KEY +from vnet_utils import generate_dut_config_files, safe_open_template, \ + apply_dut_config_files, cleanup_dut_vnets, cleanup_vxlan_tunnels, cleanup_vnet_routes + +from tests.common.fixtures.ptfhost_utils import remove_ip_addresses, change_mac_addresses, \ + copy_arp_responder_py, copy_ptftests_directory + +logger = logging.getLogger(__name__) + +pytestmark = [ + pytest.mark.topology("t0") +] + +def prepare_ptf(ptfhost, mg_facts, dut_facts, vnet_config): + """ + Prepares the PTF container for testing + + Generates and copies PTF required config files to the PTF host + + Args: + ptfhost: PTF host object + mg_facts: Minigraph facts + dut_facts: DUT host facts + vnet_config: Configuration file generated from templates/vnet_config.j2 + """ + + logger.info("Preparing PTF host") + + arp_responder_conf = safe_open_template("templates/arp_responder.conf.j2") \ + .render(arp_responder_args="--conf /tmp/vnet_arpresponder.conf") + + ptfhost.copy(content=arp_responder_conf, dest="/etc/supervisor/conf.d/arp_responder.conf") + + ptfhost.shell("supervisorctl reread") + ptfhost.shell("supervisorctl update") + + logger.debug("VNet config is: " + str(vnet_config)) + vnet_json = { + "minigraph_port_indices": mg_facts["minigraph_port_indices"], + "minigraph_portchannel_interfaces": mg_facts["minigraph_portchannel_interfaces"], + "minigraph_portchannels": mg_facts["minigraph_portchannels"], + "minigraph_lo_interfaces": mg_facts["minigraph_lo_interfaces"], + "minigraph_vlans": mg_facts["minigraph_vlans"], + "minigraph_vlan_interfaces": mg_facts["minigraph_vlan_interfaces"], + "dut_mac": dut_facts["ansible_Ethernet0"]["macaddress"], + "vnet_interfaces": vnet_config["vnet_intf_list"], + "vnet_routes": vnet_config["vnet_route_list"], + "vnet_local_routes": vnet_config["vnet_local_routes"], + "vnet_neighbors": vnet_config["vnet_nbr_list"], + "vnet_peers": vnet_config["vnet_peer_list"] + } + ptfhost.copy(content=json.dumps(vnet_json, indent=2), dest="/tmp/vnet.json") + +@pytest.fixture(scope="module") +def setup(duthost, ptfhost, minigraph_facts, vnet_config, vnet_test_params): + """ + Prepares DUT and PTF hosts for testing + + Args: + duthost: DUT host object + ptfhost: PTF host object + minigraph_facts: Minigraph facts + vnet_config: Configuration file generated from templates/vnet_config.j2 + vnet_test_params: Dictionary holding vnet test parameters + """ + + dut_facts = duthost.setup(gather_subset="!all,!any,network", filter="ansible_Ethernet*")["ansible_facts"] + + prepare_ptf(ptfhost, minigraph_facts, dut_facts, vnet_config) + + generate_dut_config_files(duthost, minigraph_facts, vnet_test_params, vnet_config) + + return minigraph_facts + +@pytest.fixture(params=["Disabled", "Enabled", "Cleanup"]) +def vxlan_status(setup, request, duthost, vnet_test_params, vnet_config): + """ + Paramterized fixture that tests the Disabled, Enabled, and Cleanup configs for VxLAN + + Args: + setup: Pytest fixture that provides access to minigraph facts + request: Contains the parameter (Disabled, Enabled, or Cleanup) for the current test iteration + duthost: DUT host object + + Returns: + A tuple containing the VxLAN status (True or False), and the test scenario (one of the pytest parameters) + """ + + vxlan_enabled = False + if request.param == "Disabled": + vxlan_enabled = False + elif request.param == "Enabled": + mg_facts = setup + + duthost.shell("sonic-clear fdb all") + + attached_vlan = mg_facts["minigraph_vlan_interfaces"][0]['attachto'] + member_to_remove = mg_facts["minigraph_vlans"][attached_vlan]['members'][0] + duthost.shell("redis-cli -n 4 del \"VLAN_MEMBER|{}|{}\"".format(attached_vlan, member_to_remove)) + + apply_dut_config_files(duthost, vnet_test_params) + + vxlan_enabled = True + elif request.param == "Cleanup" and vnet_test_params[CLEANUP_KEY]: + vxlan_enabled = True + cleanup_vnet_routes(duthost, vnet_config) + cleanup_dut_vnets(duthost, setup, vnet_config) + cleanup_vxlan_tunnels(duthost, vnet_test_params) + return vxlan_enabled, request.param + + +def test_vnet_vxlan(setup, vxlan_status, duthost, ptfhost, vnet_test_params, creds): + """ + Test case for VNET VxLAN + + Args: + setup: Pytest fixture that sets up PTF and DUT hosts + vxlan_status: Parameterized pytest fixture used to test different VxLAN configurations + duthost: DUT host object + ptfhost: PTF host object + vnet_test_params: Dictionary containing vnet test parameters + """ + + vxlan_enabled, scenario = vxlan_status + + logger.info("vxlan_enabled={}, scenario={}".format(vxlan_enabled, scenario)) + + log_file = "/tmp/vnet-vxlan.Vxlan.{}.{}.log".format(scenario, datetime.now().strftime('%Y-%m-%d-%H:%M:%S')) + ptf_params = {"vxlan_enabled": vxlan_enabled, + "config_file": '/tmp/vnet.json', + "sonic_admin_user": creds.get('sonicadmin_user'), + "sonic_admin_password": creds.get('sonicadmin_password'), + "dut_host": duthost.host.options['inventory_manager'].get_host(duthost.hostname).vars['ansible_host']} + if scenario == "Cleanup": + ptf_params["routes_removed"] = True + + if scenario == "Cleanup" and not vnet_test_params[CLEANUP_KEY]: + logger.info("Skipping cleanup") + pytest.skip("Skip cleanup specified") + + logger.debug("Starting PTF runner") + ptf_runner(ptfhost, + "ptftests", + "vnet_vxlan.VNET", + platform_dir="ptftests", + params=ptf_params, + qlen=1000, + log_file=log_file) diff --git a/tests/vxlan/vnet_constants.py b/tests/vxlan/vnet_constants.py new file mode 100644 index 00000000000..7ed47d4302a --- /dev/null +++ b/tests/vxlan/vnet_constants.py @@ -0,0 +1,19 @@ +__all__ = ["CLEANUP_KEY", "IPV6_VXLAN_TEST_KEY", "APPLY_NEW_CONFIG_KEY", "NUM_VNET_KEY", "NUM_ROUTES_KEY", "NUM_ENDPOINTS_KEY", + "DUT_VNET_SWITCH_JSON", "DUT_VNET_CONF_JSON", "DUT_VNET_ROUTE_JSON", "DUT_VNET_INTF_JSON", "DUT_VNET_NBR_JSON", + "TEMPLATE_DIR"] + +VXLAN_PORT = "13330" +VXLAN_MAC = "00:aa:bb:cc:78:9a" +DUT_VNET_SWITCH_JSON = "/tmp/vnet.switch.json" +DUT_VNET_CONF_JSON = "/tmp/vnet.conf.json" +DUT_VNET_ROUTE_JSON = "/tmp/vnet.route.json" +DUT_VNET_INTF_JSON = "/tmp/vnet.intf.json" +DUT_VNET_NBR_JSON = "/tmp/vnet.nbr.json" +TEMPLATE_DIR = "vxlan/templates" + +CLEANUP_KEY = "cleanup" +IPV6_VXLAN_TEST_KEY = "ipv6_vxlan_test" +APPLY_NEW_CONFIG_KEY = "apply_new_config" +NUM_VNET_KEY = "num_vnet" +NUM_ROUTES_KEY = "num_routes" +NUM_ENDPOINTS_KEY = "num_endpoints" diff --git a/tests/vxlan/vnet_utils.py b/tests/vxlan/vnet_utils.py new file mode 100644 index 00000000000..ea23bda6173 --- /dev/null +++ b/tests/vxlan/vnet_utils.py @@ -0,0 +1,169 @@ +import json +import logging + +from jinja2 import Template +from os import path +from time import sleep +from vnet_constants import * +from vnet_constants import VXLAN_PORT, VXLAN_MAC + +logger = logging.getLogger(__name__) + +def safe_open_template(template_path): + """ + Safely loads Jinja2 template from given path + + Note: + All Jinja2 templates should be accessed with this method to ensure proper garbage disposal + + Args: + template_path: String containing the location of the template file to be opened + + Returns: + A Jinja2 Template object read from the provided file + """ + + with open(template_path) as template_file: + return Template(template_file.read()) + +def combine_dicts(*args): + """ + Combines multiple Python dictionaries into a single dictionary + + Used primarily to pass arguments contained in multiple dictionaries to the `render()` method for Jinja2 templates + + Args: + *args: The dictionaries to be combined + + Returns: + A single Python dictionary containing the key/value pairs of all the input dictionaries + """ + + combined_args = {} + + for arg in args: + combined_args.update(arg) + + return combined_args + +def render_template_to_host(template_name, host, dest_file, *template_args, **template_kwargs): + """ + Renders a template with the given arguments and copies it to the host + + Args: + template_name: A template inside the "templates" folder (without the preceding "templates/") + host: The host device to copy the rendered template to (either a PTF or DUT host object) + dest_file: The location on the host to copy the rendered template to + *template_args: Any arguments to be passed to j2 during rendering + **template_kwargs: Any keyword arguments to be passed to j2 during rendering + """ + + combined_args = combine_dicts(*template_args) + + rendered = safe_open_template(path.join(TEMPLATE_DIR, template_name)).render(combined_args, **template_kwargs) + + host.copy(content=rendered, dest=dest_file) + +def generate_dut_config_files(duthost, mg_facts, vnet_test_params, vnet_config): + """ + Generate VNET and VXLAN config files and copy them to DUT + + Note: + Does not actually apply any of these new configs + + Args + duthost: DUT host object + mg_facts: Minigraph facts + vnet_test_params: Dictionary holding vnet test parameters + vnet_config: Configuration generated from templates/vnet_config.j2 + """ + + logger.info("Generating config files and copying to DUT") + + vnet_switch_config = [{ + "SWITCH_TABLE:switch": { + "vxlan_port": VXLAN_PORT, + "vxlan_router_mac": VXLAN_MAC + }, + "OP": "SET" + }] + + duthost.copy(content=json.dumps(vnet_switch_config, indent=4), dest=DUT_VNET_SWITCH_JSON) + + render_template_to_host("vnet_vxlan.j2", duthost, DUT_VNET_CONF_JSON, vnet_config, mg_facts, vnet_test_params) + render_template_to_host("vnet_interface.j2", duthost, DUT_VNET_INTF_JSON, vnet_config) + render_template_to_host("vnet_nbr.j2", duthost, DUT_VNET_NBR_JSON, vnet_config) + render_template_to_host("vnet_routes.j2", duthost, DUT_VNET_ROUTE_JSON, vnet_config, op="SET") + +def apply_dut_config_files(duthost, vnet_test_params): + """ + Applies config files that are stored on the given DUT + + Args: + duthost: DUT host object + """ + if vnet_test_params[APPLY_NEW_CONFIG_KEY]: + logger.info("Applying config files on DUT") + + config_files = [DUT_VNET_INTF_JSON, DUT_VNET_NBR_JSON, DUT_VNET_CONF_JSON] + for config in config_files: + duthost.shell("sonic-cfggen -j {} --write-to-db".format(config)) + sleep(3) + + duthost.shell("docker cp {} swss:/vnet.route.json".format(DUT_VNET_ROUTE_JSON)) + duthost.shell("docker cp {} swss:/vnet.switch.json".format(DUT_VNET_SWITCH_JSON)) + duthost.shell("docker exec swss sh -c \"swssconfig /vnet.switch.json\"") + duthost.shell("docker exec swss sh -c \"swssconfig /vnet.route.json\"") + sleep(3) + else: + logger.info("Skip applying config files on DUT") + +def cleanup_dut_vnets(duthost, mg_facts, vnet_config): + """ + Removes all VNET information from DUT + + Args: + duthost: DUT host object + mg_facts: Minigraph facts + vnet_config: Configuration generated from templates/vnet_config.j2 + """ + logger.info("Removing VNET information from DUT") + + for intf in vnet_config['vlan_intf_list']: + duthost.shell("redis-cli -n 4 del \"VLAN_INTERFACE|{}|{}\"".format(intf['ifname'], intf['ip'])) + + for intf in vnet_config['vlan_intf_list']: + duthost.shell("redis-cli -n 4 del \"VLAN_INTERFACE|{}\"".format(intf['ifname'])) + + for vnet in vnet_config['vnet_id_list']: + duthost.shell("redis-cli -n 4 del \"VNET|{}\"".format(vnet)) + +def cleanup_vxlan_tunnels(duthost, vnet_test_params): + """ + Removes all VxLAN tunnels from DUT + + Args: + duthost: DUT host object + vnet_test_params: Dictionary holding vnet test parameters + """ + logger.info("Removing VxLAN tunnel from DUT") + tunnels = ["tunnel_v4"] + if vnet_test_params[IPV6_VXLAN_TEST_KEY]: + tunnels.append("tunnel_v6") + + for tunnel in tunnels: + duthost.shell("redis-cli -n 4 del \"VXLAN_TUNNEL|{}\"".format(tunnel)) + +def cleanup_vnet_routes(duthost, vnet_config): + """ + Generates, pushes, and applies VNET route config to clear routes set during test + + Args: + duthost: DUT host object + vnet_config: VNET configuration generated from templates/vnet_config.j2 + """ + + render_template_to_host("vnet_routes.j2", duthost, DUT_VNET_ROUTE_JSON, vnet_config, op="DEL") + duthost.shell("docker cp {} swss:/vnet.route.json".format(DUT_VNET_ROUTE_JSON)) + duthost.shell("docker exec swss sh -c \"swssconfig /vnet.route.json\"") + sleep(3)