Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 29 additions & 24 deletions plugins/action/common/prepare_plugins/prep_105_fabric_overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,25 @@
#
# SPDX-License-Identifier: MIT

from ansible_collections.cisco.nac_dc_vxlan.plugins.plugin_utils.helper_functions import restructure_leaf_tor_data
from ansible_collections.cisco.nac_dc_vxlan.plugins.plugin_utils.helper_functions import (
restructure_leaf_tor_data,
resolve_switch_by_identifier,
)


class PreparePlugin:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.keys = []

def _resolve_mgmt_ip(self, identifier, topology_switches):
found = resolve_switch_by_identifier(identifier, topology_switches)
mgmt = found.get('management') or {}
return (
mgmt.get('management_ipv4_address')
or mgmt.get('management_ipv6_address')
)

def prepare(self):
data_model = self.kwargs['results']['model_extended']

Expand All @@ -48,14 +59,8 @@ def prepare(self):
vrf_grp_name_list.append(grp['name'])
for switch in grp['switches']:
data_model['vxlan']['overlay']['vrf_attach_groups_dict'][grp['name']].append(switch)
# If the switch is in the switch list and a hostname is used, replace the hostname with the management IP
for switch in data_model['vxlan']['overlay']['vrf_attach_groups_dict'][grp['name']]:
if any(sw['name'] == switch['hostname'] for sw in switches):
found_switch = next((item for item in switches if item["name"] == switch['hostname']))
if found_switch.get('management').get('management_ipv4_address'):
switch['mgmt_ip_address'] = found_switch['management']['management_ipv4_address']
elif found_switch.get('management').get('management_ipv6_address'):
switch['mgmt_ip_address'] = found_switch['management']['management_ipv6_address']
switch['mgmt_ip_address'] = self._resolve_mgmt_ip(switch['hostname'], switches)

# Remove vrf_attach_group from vrf if the group_name is not defined
for vrf in data_model['vxlan']['overlay']['vrfs']:
Expand All @@ -78,31 +83,31 @@ def prepare(self):

for switch in grp['switches']:
data_model['vxlan']['overlay']['network_attach_groups_dict'][grp['name']].append(switch)
# If the switch is in the switch list and a hostname is used, replace the hostname with the management IP
for switch in data_model['vxlan']['overlay']['network_attach_groups_dict'][grp['name']]:
if any(sw['name'] == switch['hostname'] for sw in switches):
found_switch = next((item for item in switches if item["name"] == switch['hostname']))
if found_switch.get('management').get('management_ipv4_address'):
switch['mgmt_ip_address'] = found_switch['management']['management_ipv4_address']
elif found_switch.get('management').get('management_ipv6_address'):
switch['mgmt_ip_address'] = found_switch['management']['management_ipv6_address']

# Process nested TOR entries and resolve their management IPs
switch['mgmt_ip_address'] = self._resolve_mgmt_ip(switch['hostname'], switches)

if 'tors' in switch and switch['tors']:
for tor in switch['tors']:
tor_hostname = tor.get('hostname')
if tor_hostname and any(sw['name'] == tor_hostname for sw in switches):
found_tor = next((item for item in switches if item["name"] == tor_hostname))
if found_tor.get('management').get('management_ipv4_address'):
tor['mgmt_ip_address'] = found_tor['management']['management_ipv4_address']
elif found_tor.get('management').get('management_ipv6_address'):
tor['mgmt_ip_address'] = found_tor['management']['management_ipv6_address']
tor_id = tor.get('hostname')
if not tor_id:
continue
tor['mgmt_ip_address'] = self._resolve_mgmt_ip(tor_id, switches)

# Remove network_attach_group from net if the group_name is not defined
for net in data_model['vxlan']['overlay']['networks']:
if 'network_attach_group' in net:
if net.get('network_attach_group') not in net_grp_name_list:
del net['network_attach_group']

for net in data_model['vxlan']['overlay']['networks']:
overrides = net.get('switch_attach_overrides')
if not overrides:
continue
for override in overrides:
override_id = override.get('hostname')
if not override_id:
continue
override['mgmt_ip_address'] = self._resolve_mgmt_ip(override_id, switches)

self.kwargs['results']['model_extended'] = data_model
return self.kwargs['results']
104 changes: 77 additions & 27 deletions plugins/action/dtc/prepare_msite_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from ansible_collections.cisco.nac_dc_vxlan.plugins.plugin_utils.helper_functions import ndfc_get_fabric_attributes
from ansible_collections.cisco.nac_dc_vxlan.plugins.plugin_utils.helper_functions import ndfc_get_fabric_switches
from ansible_collections.cisco.nac_dc_vxlan.plugins.plugin_utils.helper_functions import restructure_leaf_tor_data
from ansible_collections.cisco.nac_dc_vxlan.plugins.plugin_utils.helper_functions import resolve_child_fabric_switch
from ansible_collections.cisco.nac_dc_vxlan.plugins.filter.version_compare import version_compare
import re

Expand Down Expand Up @@ -277,23 +278,18 @@ def run(self, tmp=None, task_vars=None):
data_model['vxlan']['multisite']['overlay']['vrf_attach_groups_dict'][grp['name']].append(switch)
# If the switch is in the switch list and a hostname is used, replace the hostname with the management IP
for switch in data_model['vxlan']['multisite']['overlay']['vrf_attach_groups_dict'][grp['name']]:
# FQDN-tolerant match; preprovision switches (hostname=None) are skipped
for child_fabric in child_fabrics_data.keys():
for sw in child_fabrics_data[child_fabric]['switches']:
# When switch is in preprovision, sw['hostname'] is None.
if sw.get('hostname') is not None:
# Compare switches with regex to catch hostname when ip domain-name is configured
# Check both directions: data model name vs NDFC name and vice versa
fwd_pattern = f"^{re.escape(switch['hostname'])}$|^{re.escape(switch['hostname'])}\\..*$"
rev_pattern = f"^{re.escape(sw['hostname'])}$|^{re.escape(sw['hostname'])}\\..*$"
if re.search(fwd_pattern, sw['hostname']) or re.search(rev_pattern, switch['hostname']):
switch['mgmt_ip_address'] = sw['mgmt_ip_address']
resolved = resolve_child_fabric_switch(switch['hostname'], child_fabrics_data[child_fabric]['switches'])
if resolved is not None:
switch['mgmt_ip_address'] = resolved['mgmt_ip_address']
break

if 'mgmt_ip_address' not in switch:
results['failed'] = True
results['msg'] = (
f"Unable to resolve management IP for switch '{switch['hostname']}' "
f"in VRF attach group '{grp['name']}'. "
f"Verify the hostname matches a discovered switch in a child fabric of '{parent_fabric}'."
f"vrf attach group {grp['name']} hostname {switch['hostname']} "
f"does not match any switch name in child fabrics of '{parent_fabric}'."
)
return results

Expand Down Expand Up @@ -327,32 +323,41 @@ def run(self, tmp=None, task_vars=None):
data_model['vxlan']['multisite']['overlay']['network_attach_groups_dict'][grp['name']].append(switch)
# If the switch is in the switch list and a hostname is used, replace the hostname with the management IP
for switch in data_model['vxlan']['multisite']['overlay']['network_attach_groups_dict'][grp['name']]:
# FQDN-tolerant match; preprovision switches (hostname=None) are skipped
for child_fabric in child_fabrics_data.keys():
for sw in child_fabrics_data[child_fabric]['switches']:
if sw.get('hostname') is not None:
# Check both directions: data model name vs NDFC name and vice versa
fwd_pattern = f"^{re.escape(switch['hostname'])}$|^{re.escape(switch['hostname'])}\\..*$"
rev_pattern = f"^{re.escape(sw['hostname'])}$|^{re.escape(sw['hostname'])}\\..*$"
if re.search(fwd_pattern, sw['hostname']) or re.search(rev_pattern, switch['hostname']):
switch['mgmt_ip_address'] = sw['mgmt_ip_address']

# Process nested TOR entries and resolve their management IPs
resolved = resolve_child_fabric_switch(switch['hostname'], child_fabrics_data[child_fabric]['switches'])
if resolved is not None:
switch['mgmt_ip_address'] = resolved['mgmt_ip_address']

if 'tors' in switch and switch['tors']:
for tor in switch['tors']:
tor_hostname = tor.get('hostname')
if tor_hostname and any(sw['name'] == tor_hostname for sw in child_fabrics_data[child_fabric]['switches']):
found_tor = next((item for item in child_fabrics_data[child_fabric]['switches'] if item["name"] == tor_hostname))
tor['mgmt_ip_address'] = found_tor['mgmt_ip_address']
if not tor_hostname:
continue
# FQDN-tolerant match; preprovision switches (hostname=None) are skipped
resolved_tor = resolve_child_fabric_switch(tor_hostname, child_fabrics_data[child_fabric]['switches'])
if resolved_tor is not None:
tor['mgmt_ip_address'] = resolved_tor['mgmt_ip_address']

if 'mgmt_ip_address' not in switch:
results['failed'] = True
results['msg'] = (
f"Unable to resolve management IP for switch '{switch['hostname']}' "
f"in network attach group '{grp['name']}'. "
f"Verify the hostname matches a discovered switch in a child fabric of '{parent_fabric}'."
f"network attach group {grp['name']} hostname {switch['hostname']} "
f"does not match any switch name in child fabrics of '{parent_fabric}'."
)
return results

if 'tors' in switch and switch['tors']:
for tor in switch['tors']:
if tor.get('hostname') and 'mgmt_ip_address' not in tor:
results['failed'] = True
results['msg'] = (
f"network attach group {grp['name']} tor {tor['hostname']} "
f"under leaf {switch['hostname']} does not match any switch name "
f"in child fabrics of '{parent_fabric}'."
)
return results

# Append switch to a flat list of switches for cross comparison later when we query the
# MSD fabric information. We need to stop execution if the list returned by the MSD query
# does not include one of these switches.
Expand All @@ -364,5 +369,50 @@ def run(self, tmp=None, task_vars=None):
if net.get('network_attach_group') not in net_grp_name_list:
del net['network_attach_group']

for net in data_model['vxlan']['multisite']['overlay']['networks']:
overrides = net.get('switch_attach_overrides')
if not overrides:
continue
net_name = net.get('name')
net_attach_group = net.get('network_attach_group')
group_hostnames = set()
if net_attach_group:
group_hostnames = {
s.get('hostname')
for s in data_model['vxlan']['multisite']['overlay']['network_attach_groups_dict'].get(net_attach_group, [])
if s.get('hostname')
}

for override in overrides:
hostname = override.get('hostname')
if not hostname:
continue

resolved = None
# FQDN-tolerant match; preprovision switches (hostname=None) are skipped
for child_fabric in child_fabrics_data.keys():
resolved = resolve_child_fabric_switch(hostname, child_fabrics_data[child_fabric]['switches'])
if resolved is not None:
override['mgmt_ip_address'] = resolved['mgmt_ip_address']
break

if resolved is None:
results['failed'] = True
results['msg'] = (
f"Network '{net_name}' switch_attach_overrides identifier "
f"'{hostname}' does not match any switch name in child "
f"fabrics of '{parent_fabric}'."
)
return results

if net_attach_group and hostname not in group_hostnames:
results['failed'] = True
results['msg'] = (
f"Network '{net_name}' switch_attach_overrides identifier "
f"'{hostname}' could not be resolved to a switch attached "
f"through network_attach_group '{net_attach_group}'."
)
return results

results['overlay_attach_groups'] = data_model['vxlan']['multisite']['overlay']
return results
31 changes: 31 additions & 0 deletions plugins/plugin_utils/helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,37 @@ def hostname_to_ip_mapping(data_model):
return data_model


def resolve_switch_by_identifier(identifier, topology_switches):
"""Return the topology switch dict whose name matches identifier, or None."""
if not identifier or not topology_switches:
return None
for sw in topology_switches:
if sw.get('name') == identifier:
return sw
return None


def resolve_child_fabric_switch(identifier, child_fabric_switches):
"""Return the child fabric switch dict whose hostname matches identifier, or None.

FQDN-tolerant: also matches when either side is the bare form of the
other (e.g. data model uses 'leaf1' but NDFC returns 'leaf1.example.com').
Preprovisioned switches (hostname == None) are skipped.
"""
if not identifier or not child_fabric_switches:
return None
import re
for sw in child_fabric_switches:
sw_hostname = sw.get('hostname')
if sw_hostname is None:
continue
fwd = f"^{re.escape(identifier)}$|^{re.escape(identifier)}\\..*$"
rev = f"^{re.escape(sw_hostname)}$|^{re.escape(sw_hostname)}\\..*$"
if re.search(fwd, sw_hostname) or re.search(rev, identifier):
return sw
return None


def ndfc_get_switch_policy(self, task_vars, tmp, switch_serial_number):
"""
Get NDFC policy for a given managed switch by the switch's serial number.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{# Auto-generated NDFC DC VXLAN EVPN VRFs config data structure for fabric {{ vxlan.fabric.name }} #}
{# Auto-generated NDFC DC VXLAN EVPN Networks config data structure for fabric {{ vxlan.fabric.name }} #}
{% set networks = [] %}
{% if data_model_extended.vxlan.overlay.networks is defined and data_model_extended.vxlan.overlay.networks %}
{% set networks = data_model_extended.vxlan.overlay.networks %}
Expand Down Expand Up @@ -68,6 +68,22 @@
{% endif %}
{% for attach in network_attach_groups_dict[net['network_attach_group']] %}
- ip_address: {{ attach['mgmt_ip_address'] }}
{% if net['switch_attach_overrides'] is defined %}
{% for switch_override in net['switch_attach_overrides'] %}
{% if switch_override['mgmt_ip_address'] == attach['mgmt_ip_address'] %}
{% if switch_override['vlan_id'] is defined and switch_override['vlan_id'] != net['vlan_id'] %}
vlan_id: {{ switch_override['vlan_id'] }}
{% endif %}
{% if switch_override['freeform_config'] is defined %}
freeform_config: |-
{{ switch_override['freeform_config'] | cisco.nac_dc_vxlan.dedent | indent(8, true) }}
{% endif %}
{% if ndfc_version | cisco.nac_dc_vxlan.version_compare('12.4.1', '>=') %}
svi_enabled: {{ switch_override['svi_enabled'] | default(defaults.vxlan.overlay.networks.svi_enabled) }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% if attach['ports'] is defined %}
ports: {{ attach['ports'] }}
{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
{% else %}
{% set networks = [] %}
{% endif %}
{% set net_overrides = {} %}
{% for _n in runtime_mcfg_data_model.overlay_attach_groups.networks | default([], true) %}
{% set _ = net_overrides.update({_n['name']: _n.get('switch_attach_overrides') or []}) %}
{% endfor %}
{% for net in networks %}
- net_name: {{ net['name'] }}
{# ------------------------------------------------------ #}
Expand Down Expand Up @@ -74,6 +78,20 @@
{% endif %}
{% for attach in network_attach_groups_dict[net['network_attach_group']] %}
- ip_address: {{ attach['mgmt_ip_address'] }}
{% for switch_override in net_overrides.get(net['name'], []) %}
{% if switch_override['mgmt_ip_address'] == attach['mgmt_ip_address'] %}
{% if switch_override['vlan_id'] is defined and switch_override['vlan_id'] != net['vlan_id'] %}
vlan_id: {{ switch_override['vlan_id'] }}
{% endif %}
{% if switch_override['freeform_config'] is defined %}
freeform_config: |-
{{ switch_override['freeform_config'] | cisco.nac_dc_vxlan.dedent | indent(8, true) }}
{% endif %}
{% if ndfc_version | cisco.nac_dc_vxlan.version_compare('12.4.1', '>=') %}
svi_enabled: {{ switch_override['svi_enabled'] | default(defaults.vxlan.overlay.networks.svi_enabled) }}
{% endif %}
{% endif %}
{% endfor %}
{% if attach['ports'] is defined %}
ports: {{ attach['ports'] }}
{% endif %}
Expand Down
Loading
Loading