diff --git a/plugins/action/common/prepare_plugins/prep_001_list_defaults.py b/plugins/action/common/prepare_plugins/prep_001_list_defaults.py index 39e40fcc..c3fb522d 100644 --- a/plugins/action/common/prepare_plugins/prep_001_list_defaults.py +++ b/plugins/action/common/prepare_plugins/prep_001_list_defaults.py @@ -172,5 +172,33 @@ def prepare(self): list_index += 1 + # -------------------------------------------------------------------- + # Fabric Policy List Defaults + # -------------------------------------------------------------------- + + # Check vxlan.policy list elements + parent_keys = ['vxlan', 'policy'] + dm_check = data_model_key_check(self.model_data, parent_keys) + if 'policy' in dm_check['keys_not_found'] or 'policy' in dm_check['keys_no_data']: + self.model_data['vxlan']['policy'] = {} + self.model_data['vxlan']['policy'].update({'policies': []}) + self.model_data['vxlan']['policy'].update({'groups': []}) + self.model_data['vxlan']['policy'].update({'switches': []}) + + parent_keys = ['vxlan', 'policy', 'policies'] + dm_check = data_model_key_check(self.model_data, parent_keys) + if 'policies' in dm_check['keys_not_found'] or 'policies' in dm_check['keys_no_data']: + self.model_data['vxlan']['policy']['policies'] = [] + + parent_keys = ['vxlan', 'policy', 'groups'] + dm_check = data_model_key_check(self.model_data, parent_keys) + if 'groups' in dm_check['keys_not_found'] or 'groups' in dm_check['keys_no_data']: + self.model_data['vxlan']['policy']['groups'] = [] + + parent_keys = ['vxlan', 'policy', 'switches'] + dm_check = data_model_key_check(self.model_data, parent_keys) + if 'switches' in dm_check['keys_not_found'] or 'switches' in dm_check['keys_no_data']: + self.model_data['vxlan']['policy']['switches'] = [] + self.kwargs['results']['model_extended'] = self.model_data return self.kwargs['results'] diff --git a/plugins/action/common/prepare_plugins/prep_107_policy.py b/plugins/action/common/prepare_plugins/prep_107_policy.py new file mode 100644 index 00000000..a594bb34 --- /dev/null +++ b/plugins/action/common/prepare_plugins/prep_107_policy.py @@ -0,0 +1,43 @@ +# Copyright (c) 2024 Cisco Systems, Inc. and its affiliates +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# SPDX-License-Identifier: MIT + + +class PreparePlugin: + def __init__(self, **kwargs): + self.kwargs = kwargs + self.keys = [] + + def prepare(self): + model_data = self.kwargs['results']['model_extended'] + + # Ensure that vrf_lite's switches are mapping to their respective + # management IP address from topology switches + topology_switches = model_data['vxlan']['topology']['switches'] + for switch in model_data['vxlan']['policy']['switches']: + if any(sw['name'] == switch['name'] for sw in topology_switches): + found_switch = next((item for item in topology_switches if item["name"] == switch['name'])) + if found_switch.get('management').get('management_ipv4_address'): + switch['name'] = found_switch['management']['management_ipv4_address'] + elif found_switch.get('management').get('management_ipv6_address'): + switch['name'] = found_switch['management']['management_ipv6_address'] + + self.kwargs['results']['model_extended'] = model_data + return self.kwargs['results'] diff --git a/roles/common_global/vars/main.yml b/roles/common_global/vars/main.yml index 825d9955..de1f17a2 100644 --- a/roles/common_global/vars/main.yml +++ b/roles/common_global/vars/main.yml @@ -29,6 +29,7 @@ nac_tags: - cr_manage_vpc_peers - cr_manage_interfaces - cr_manage_vrfs_networks + - cr_manage_policy # ------------------------- - rr_manage_interfaces - rr_manage_networks @@ -48,6 +49,7 @@ nac_tags: - cr_manage_vpc_peers - cr_manage_interfaces - cr_manage_vrfs_networks + - cr_manage_policy create_fabric: - cr_manage_fabric create_switches: @@ -58,6 +60,8 @@ nac_tags: - cr_manage_interfaces create_vrfs_networks: - cr_manage_vrfs_networks + create_policy: + - cr_manage_policy # All Remove Tags remove: - rr_manage_interfaces diff --git a/roles/dtc/common/tasks/ndfc_policy.yml b/roles/dtc/common/tasks/ndfc_policy.yml new file mode 100644 index 00000000..66d01169 --- /dev/null +++ b/roles/dtc/common/tasks/ndfc_policy.yml @@ -0,0 +1,78 @@ +# Copyright (c) 2024 Cisco Systems, Inc. and its affiliates +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# SPDX-License-Identifier: MIT + +--- + +- name: Initialize changes_detected Var + ansible.builtin.set_fact: + changes_detected_policy: false + delegate_to: localhost + +- name: Set file_name Var + ansible.builtin.set_fact: + file_name: "{{ MD.vxlan.global.name }}_ndfc_policy.yml" + delegate_to: localhost + +- name: Stat Previous File If It Exists + ansible.builtin.stat: + path: "{{ role_path }}/files/{{ file_name }}" + register: data_file_previous + delegate_to: localhost + # TODO: Add capability to overridde path variable above for CI/CD pipeline + +- name: Backup Previous Data File If It Exists + ansible.builtin.copy: + src: "{{ role_path }}/files/{{ file_name }}" + dest: "{{ role_path }}/files/{{ file_name }}.old" + when: data_file_previous.stat.exists + +- name: Delete Previous Data File If It Exists + ansible.builtin.file: + state: absent + path: "{{ role_path }}/files/{{ file_name }}" + delegate_to: localhost + when: data_file_previous.stat.exists + +- name: Build Policy List From Template + ansible.builtin.template: + src: ndfc_policy.j2 + dest: "{{ role_path }}/files/{{ file_name }}" + delegate_to: localhost + +- ansible.builtin.set_fact: + policy_config: "{{ lookup('file', file_name) | from_yaml }}" + when: (MD_Extended.vxlan.policy.policies | default([])) | length > 0 + delegate_to: localhost + +- name: Diff Previous and Current Data Files + cisco.nac_dc_vxlan.dtc.diff_model_changes: + file_name_previous: "{{ role_path }}/files/{{ file_name }}.old" + file_name_current: "{{ role_path }}/files/{{ file_name }}" + register: file_diff_result + delegate_to: localhost + +- name: Set File Change Flag Based on File Diff Result + ansible.builtin.set_fact: + changes_detected_policy: true + delegate_to: localhost + when: + - file_diff_result.file_data_changed + - check_roles['save_previous'] diff --git a/roles/dtc/common/tasks/ndfc_vrfs.yml b/roles/dtc/common/tasks/ndfc_vrfs.yml index aa558ff6..da83cfce 100644 --- a/roles/dtc/common/tasks/ndfc_vrfs.yml +++ b/roles/dtc/common/tasks/ndfc_vrfs.yml @@ -80,4 +80,4 @@ delegate_to: localhost when: - file_diff_result.file_data_changed - - check_roles['save_previous'] \ No newline at end of file + - check_roles['save_previous'] diff --git a/roles/dtc/common/tasks/sub_main.yml b/roles/dtc/common/tasks/sub_main.yml index 9513e710..41331d3d 100644 --- a/roles/dtc/common/tasks/sub_main.yml +++ b/roles/dtc/common/tasks/sub_main.yml @@ -145,6 +145,13 @@ - name: Build Fabric interface All List From Template ansible.builtin.include_tasks: ndfc_interface_all.yml +# -------------------------------------------------------------------- +# Build Fabric Policy List From Template +# -------------------------------------------------------------------- + +- name: Build Fabric Policy List From Template + ansible.builtin.include_tasks: ndfc_policy.yml + - name: Run Diff Flags ansible.builtin.debug: msg: @@ -168,7 +175,8 @@ - "+ ----- All Interfaces -----" - "+ VRFs Changes Detected - [ {{ changes_detected_vrfs }} ]" - "+ Networks Changes Detected - [ {{ changes_detected_networks }} ]" + - "+ Policy Changes Detected - [ {{ changes_detected_policy }} ]" - "+ ----- Run Map -----" - "+ Run Map Diff Run - [ {{ run_map_read_result.diff_run }} ]" - - " + Force Run Flag - [ {{ force_run_all }} ]" + - "+ Force Run Flag - [ {{ force_run_all }} ]" - "----------------------------------------------------------------" diff --git a/roles/dtc/common/templates/ndfc_policy.j2 b/roles/dtc/common/templates/ndfc_policy.j2 new file mode 100644 index 00000000..b7f49e71 --- /dev/null +++ b/roles/dtc/common/templates/ndfc_policy.j2 @@ -0,0 +1,41 @@ +--- +# This NDFC policy and switch attachments config data structure is auto-generated +# DO NOT EDIT MANUALLY +# +- switch: +{% for switch in MD_Extended.vxlan.policy.switches %} + - ip: {{ switch.name }} + policies: +{% for group_entry in switch.groups %} +{% set query = "[?(@.name==`" ~ group_entry ~ "`)]" %} +{% set policy_group_match = MD_Extended.vxlan.policy.groups | community.general.json_query(query) | first %} +{% for policy in policy_group_match.policies %} +{% set query = "[?(@.name==`" ~ policy.name ~ "`)]" %} +{% set policy_match = MD_Extended.vxlan.policy.policies | community.general.json_query(query) | first %} + - create_additional_policy: False + description: {{ policy_match.name }} +{% if (policy_match.template_name is defined and policy_match.template_name) or (policy_match.filename is defined and policy_match.filename and (".yaml" in policy_match.filename or ".yml" in policy_match.filename)) %} + name: {{ policy_match.template_name | default(defaults.vxlan.policy.template_name) }} +{% elif policy_match.filename is defined and policy_match.filename and ".cfg" in policy_match.filename %} + name: {{ defaults.vxlan.policy.template_name }} +{% endif %} + policy_vars: +{% if policy_match.template_vars is defined and policy_match.template_vars %} +{% for key, value in policy_match.template_vars.items() %} +{% if key == 'CONF' %} + {{ key }}: |- + {{ value | indent(14) }} +{% else %} + {{ key }}: {{ value | to_nice_yaml(indent=2) | indent(10) | trim }} +{% endif %} +{% endfor %} +{% elif policy_match.filename is defined and policy_match.filename and (".yaml" in policy_match.filename or ".yml" in policy_match.filename) %} + {{ lookup('ansible.builtin.file', policy_match.filename) | indent(12) }} +{% elif policy_match.filename is defined and policy_match.filename and ".cfg" in policy_match.filename %} + CONF: |- + {{ lookup('ansible.builtin.file', policy_match.filename) | indent(14) | trim }} +{% endif %} + priority: {{ policy.priority | default(policy_group_match.priority) | default(defaults.vxlan.policy.priority) }} +{% endfor %} +{% endfor %} +{% endfor %} diff --git a/roles/dtc/create/tasks/main.yml b/roles/dtc/create/tasks/main.yml index b9b60087..f2f9ff15 100644 --- a/roles/dtc/create/tasks/main.yml +++ b/roles/dtc/create/tasks/main.yml @@ -23,7 +23,7 @@ - name: Import Role Tasks ansible.builtin.import_tasks: sub_main.yml - when: changes_detected_fabric or changes_detected_inventory or changes_detected_vpc_peering or changes_detected_interfaces or changes_detected_link_vpc_peering or changes_detected_vrfs or changes_detected_networks + when: changes_detected_fabric or changes_detected_inventory or changes_detected_vpc_peering or changes_detected_interfaces or changes_detected_link_vpc_peering or changes_detected_vrfs or changes_detected_networks or changes_detected_policy - name: Mark Stage Role Create Completed cisco.nac_dc_vxlan.common.run_map: diff --git a/roles/dtc/create/tasks/policies.yml b/roles/dtc/create/tasks/policies.yml new file mode 100644 index 00000000..c052c15a --- /dev/null +++ b/roles/dtc/create/tasks/policies.yml @@ -0,0 +1,41 @@ +# Copyright (c) 2024 Cisco Systems, Inc. and its affiliates +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# SPDX-License-Identifier: MIT + +--- + +- name: Manage Policies Entry Point + ansible.builtin.debug: + msg: + - "----------------------------------------------------------------" + - "+ Manage Policies Fabric {{ MD.vxlan.global.name }}" + - "----------------------------------------------------------------" + +# -------------------------------------------------------------------- +# Manage VRF Configuration on NDFC +# -------------------------------------------------------------------- +- name: Manage NDFC Fabric Policies + cisco.dcnm.dcnm_policy: + fabric: "{{ MD.vxlan.global.name }}" + use_desc_as_key: true + config: "{{ policy_config }}" + deploy: false + state: merged + register: manage_policies_result diff --git a/roles/dtc/create/tasks/sub_main.yml b/roles/dtc/create/tasks/sub_main.yml index 237e572f..cb1ec174 100644 --- a/roles/dtc/create/tasks/sub_main.yml +++ b/roles/dtc/create/tasks/sub_main.yml @@ -65,4 +65,11 @@ when: - (MD.vxlan.overlay_services is defined) and (MD_Extended.vxlan.topology.switches | length > 0) - changes_detected_vrfs or changes_detected_networks - tags: "{{ nac_tags.create_vrfs_networks }}" \ No newline at end of file + tags: "{{ nac_tags.create_vrfs_networks }}" + +- name: Manage NDFC Fabric Policies + ansible.builtin.import_tasks: policies.yml + when: + - (MD_Extended.vxlan.policy is defined) and (MD_Extended.vxlan.policy.policies | length > 0) + - changes_detected_policy + tags: "{{ nac_tags.create_policy }}" diff --git a/roles/dtc/deploy/tasks/main.yml b/roles/dtc/deploy/tasks/main.yml index 4dce3eb0..e2d2d2be 100644 --- a/roles/dtc/deploy/tasks/main.yml +++ b/roles/dtc/deploy/tasks/main.yml @@ -24,7 +24,7 @@ - name: Import Role Tasks ansible.builtin.import_tasks: sub_main.yml tags: "{{ nac_tags.create }}" # Tags defined in roles/common_global/vars/main.yml - when: changes_detected_fabric or changes_detected_inventory or changes_detected_vpc_peering or changes_detected_interfaces or changes_detected_link_vpc_peering or changes_detected_vrfs or changes_detected_networks + when: changes_detected_fabric or changes_detected_inventory or changes_detected_vpc_peering or changes_detected_interfaces or changes_detected_link_vpc_peering or changes_detected_vrfs or changes_detected_networks or changes_detected_policy - name: Mark Stage Role Deploy Completed cisco.nac_dc_vxlan.common.run_map: diff --git a/roles/render/files/.keep b/roles/render/files/.gitkeep similarity index 100% rename from roles/render/files/.keep rename to roles/render/files/.gitkeep diff --git a/roles/render/tasks/main.yml b/roles/render/tasks/main.yml index 239ce1fd..f66551ed 100644 --- a/roles/render/tasks/main.yml +++ b/roles/render/tasks/main.yml @@ -20,4 +20,4 @@ # SPDX-License-Identifier: MIT --- -# tasks file for render +# tasks file for render \ No newline at end of file diff --git a/roles/validate/defaults/main.yml b/roles/validate/defaults/main.yml index cf548f72..2dc1c202 100644 --- a/roles/validate/defaults/main.yml +++ b/roles/validate/defaults/main.yml @@ -253,3 +253,6 @@ defaults: disable_connected_check: false remove_private_as: false remove_private_as_all: false + policy: + template_name: switch_freeform + priority: 500 diff --git a/roles/validate/files/rules/required_rules/501_policy_cross_reference.py b/roles/validate/files/rules/required_rules/501_policy_cross_reference.py new file mode 100644 index 00000000..2a2619d9 --- /dev/null +++ b/roles/validate/files/rules/required_rules/501_policy_cross_reference.py @@ -0,0 +1,80 @@ +class Rule: + id = "501" + description = "Verify Policy Cross Reference Between Policies, Groups, and Switches" + severity = "HIGH" + + @classmethod + def match(cls, inventory): + policies = [] + groups = [] + topology_switches = [] + results = [] + + if inventory.get("vxlan", None): + if inventory["vxlan"].get("policy", None): + if inventory["vxlan"].get("policy").get("policies", None): + policies = inventory["vxlan"]["policy"]["policies"] + for policy in policies: + filename = policy.get("filename", None) + + if ((filename and policy.get("template_name", None) and policy.get("template_vars", None)) or + (filename and policy.get("template_vars", None))): + results.append( + "Policy definition filenames with .config or .cfg default template_name to NDFC freeform whereas " + "filenames with .yaml or .yml requires template_name to be defined per NDFC standards and " + "template_name and template_vars must not be defined together per NDFC standards." + ) + break + + if (filename and filename.endswith(('.config', '.cfg', '.yaml', '.yml'))) and policy.get("template_vars", None): + results.append( + f"Policy filename {filename} is of .config, .cfg, .yaml, or .yml extension. The template_vars parameter must not be defined." + ) + break + + if (filename and filename.endswith(('.yaml', '.yml'))) and not policy.get("template_name", None): + results.append( + f"Policy filename {filename} is of .yaml, or .yml extension. The template_name parameter must be defined." + ) + break + + if inventory["vxlan"].get("policy").get("groups", None): + groups = inventory["vxlan"]["policy"]["groups"] + for group in groups: + group_policies = group.get("policies", []) + for group_policy in group_policies: + if not any(policy['name'] == group_policy['name'] for policy in policies): + results.append( + f"Policy name {group_policy['name']} is defined in a group and must be defined in the policies section." + ) + break + + if inventory["vxlan"].get("policy").get("switches", None): + switches = inventory["vxlan"]["policy"]["switches"] + + if inventory.get("vxlan"): + if inventory["vxlan"].get("topology"): + if inventory.get("vxlan").get("topology").get("switches"): + topology_switches = inventory.get("vxlan").get("topology").get("switches") + for switch in switches: + if not ((any(topology_switch['name'] == switch['name'] for topology_switch in topology_switches)) or + (any(topology_switch['management'].get('management_ipv4_address', None) == switch['name'] + for topology_switch in topology_switches)) or + (any(topology_switch['management'].get('management_ipv6_address', None) == switch['name'] + for topology_switch in topology_switches))): + results.append( + f"Switch name {switch['name']} is defined and must be defined in the topology switches section." + ) + break + + switch_groups = switch.get("groups", []) + for switch_group in switch_groups: + for group in groups: + if not (any(group['name'] == switch_group for group in groups)): + results.append( + f"Policy group name {switch_group} is defined for switch {switch['name']} and " + "must be defined in the policy groups section." + ) + break + + return results diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index 8b06862c..02f27b41 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -6,6 +6,7 @@ plugins/action/common/prepare_plugins/prep_103_topology_switches.py action-plugi plugins/action/common/prepare_plugins/prep_104_fabric_overlay_services.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/prepare_plugins/prep_105_topology_interfaces.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/prepare_plugins/prep_106_topology_vpc_interfaces.py action-plugin-docs # action plugin has no matching module to provide documentation +plugins/action/common/prepare_plugins/prep_107_policy.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/prepare_service_model.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/get_credentials.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/run_map.py action-plugin-docs # action plugin has no matching module to provide documentation diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index 8b06862c..02f27b41 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -6,6 +6,7 @@ plugins/action/common/prepare_plugins/prep_103_topology_switches.py action-plugi plugins/action/common/prepare_plugins/prep_104_fabric_overlay_services.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/prepare_plugins/prep_105_topology_interfaces.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/prepare_plugins/prep_106_topology_vpc_interfaces.py action-plugin-docs # action plugin has no matching module to provide documentation +plugins/action/common/prepare_plugins/prep_107_policy.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/prepare_service_model.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/get_credentials.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/run_map.py action-plugin-docs # action plugin has no matching module to provide documentation diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index 8b06862c..02f27b41 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -6,6 +6,7 @@ plugins/action/common/prepare_plugins/prep_103_topology_switches.py action-plugi plugins/action/common/prepare_plugins/prep_104_fabric_overlay_services.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/prepare_plugins/prep_105_topology_interfaces.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/prepare_plugins/prep_106_topology_vpc_interfaces.py action-plugin-docs # action plugin has no matching module to provide documentation +plugins/action/common/prepare_plugins/prep_107_policy.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/prepare_service_model.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/get_credentials.py action-plugin-docs # action plugin has no matching module to provide documentation plugins/action/common/run_map.py action-plugin-docs # action plugin has no matching module to provide documentation