From 01e15759ebb53bfb53b86badca54588f68a59c6c Mon Sep 17 00:00:00 2001
From: Matt Tarkington <mtarking@cisco.com>
Date: Fri, 8 Nov 2024 09:31:53 -0500
Subject: [PATCH] Remove Unmanaged Policy (#202)

* start of remove policy

* more work on policy remove

* fix local lint errors

* fix lint errors

* revert update hostname plugin

* refactor remove policy

* fix path

* remove path

* clean up task vernacular & add remove policy tag in global tags

* add comments to action plugin

* fix lint error

* fix lint error
---
 README.md                                     |   1 +
 .../prepare_plugins/prep_001_list_defaults.py |   2 +-
 plugins/action/dtc/unmanaged_policy.py        | 198 ++++++++++++++++++
 .../dtc/update_switch_hostname_policy.py      |   8 +-
 plugins/action/helper_functions.py            |  63 ------
 plugins/plugin_utils/.gitkeep                 |   0
 plugins/plugin_utils/helper_functions.py      | 141 +++++++++++++
 roles/common_global/vars/main.yml             |   9 +-
 roles/dtc/common/templates/ndfc_policy.j2     |   5 +-
 .../tasks/verify_ndfc_authorization.yml       |  23 +-
 .../tasks/verify_ndfc_connectivity.yml        |  21 ++
 roles/dtc/remove/tasks/interfaces.yml         |   8 +-
 roles/dtc/remove/tasks/links.yml              |   4 +-
 roles/dtc/remove/tasks/networks.yml           |   3 +-
 roles/dtc/remove/tasks/policy.yml             |  58 +++++
 roles/dtc/remove/tasks/sub_main.yml           |  14 +-
 roles/dtc/remove/tasks/switches.yml           |   4 +-
 roles/dtc/remove/tasks/vpc_peers.yml          |   4 +-
 roles/dtc/remove/tasks/vrfs.yml               |   3 +-
 tests/sanity/ignore-2.14.txt                  |   2 +-
 tests/sanity/ignore-2.15.txt                  |   2 +-
 tests/sanity/ignore-2.16.txt                  |   2 +-
 22 files changed, 483 insertions(+), 92 deletions(-)
 create mode 100644 plugins/action/dtc/unmanaged_policy.py
 delete mode 100644 plugins/action/helper_functions.py
 create mode 100644 plugins/plugin_utils/.gitkeep
 create mode 100644 plugins/plugin_utils/helper_functions.py
 create mode 100644 roles/dtc/remove/tasks/policy.yml

diff --git a/README.md b/README.md
index 18955b1e..0e7197d0 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,7 @@ The following control variables are available in this collection.
 | `inventory_delete_mode` | Remove inventory state as part of the remove role | `false` |
 | `link_vpc_delete_mode` | Remove vpc link state as part of the remove role | `false` |
 | `vpc_delete_mode` | Remove vpc pair state as part of the remove role | `false` |
+| `policy_delete_mode` | Remove policy state as part of the remove role | `false` |
 
 These variables are described in more detail in different sections of this document.
 
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 c3fb522d..75a5f776 100644
--- a/plugins/action/common/prepare_plugins/prep_001_list_defaults.py
+++ b/plugins/action/common/prepare_plugins/prep_001_list_defaults.py
@@ -20,7 +20,7 @@
 # SPDX-License-Identifier: MIT
 
 
-from ...helper_functions import data_model_key_check
+from ....plugin_utils.helper_functions import data_model_key_check
 
 
 def update_nested_dict(nested_dict, keys, new_value):
diff --git a/plugins/action/dtc/unmanaged_policy.py b/plugins/action/dtc/unmanaged_policy.py
new file mode 100644
index 00000000..3e972c26
--- /dev/null
+++ b/plugins/action/dtc/unmanaged_policy.py
@@ -0,0 +1,198 @@
+# 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
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+from ...plugin_utils.helper_functions import ndfc_get_nac_switch_policy_using_desc
+
+
+class ActionModule(ActionBase):
+
+    def run(self, tmp=None, task_vars=None):
+        results = super(ActionModule, self).run(tmp, task_vars)
+        results['changed'] = False
+
+        # List of switch serial numbes obtained directly from NDFC
+        ndfc_sw_serial_numbers = self._task.args["switch_serial_numbers"]
+        # Data from data model
+        model_data = self._task.args["model_data"]
+
+        # Switches list from data model
+        dm_topology_switches = model_data["vxlan"]["topology"]["switches"]
+
+        # Policy, Poilcy Groups, and Switches Policy Group lists from data model
+        dm_policy_policies = model_data["vxlan"]["policy"]["policies"]
+        dm_policy_groups = model_data["vxlan"]["policy"]["groups"]
+        dm_policy_switches = model_data["vxlan"]["policy"]["switches"]
+
+        # Build list of VRF Lites from data model if entries exist
+        # Used to exclude matching on VRF Lites as part of the unmanaged policies
+        vrf_lites = []
+        if model_data["vxlan"].get("overlay_extensions", None):
+            if model_data["vxlan"]["overlay_extensions"].get("vrf_lites", None):
+                dm_vrf_lites = model_data["vxlan"]["overlay_extensions"]["vrf_lites"]
+                for dm_vrf_lite in dm_vrf_lites:
+                    for dm_vrf_lite_switch in dm_vrf_lite["switches"]:
+                        unique_name = f"nac_{dm_vrf_lite['name']}_{dm_vrf_lite_switch['name']}"
+                        vrf_lites.append(unique_name)
+
+        # Set defaults for management IP addresses, current switch policies, and unmanaged policies
+        dm_management_ipv4_address = ""
+        dm_management_ipv6_address = ""
+        # For each switch current_sw_policies will be used to store a list of policies currently associated to the switch
+        current_sw_policies = []
+        # For each switch that has unmanaged policies, the switch IP address and the list of unmanaged policies will be stored
+        # This default dict is the start of what is required for the NDFC policy module
+        unmanaged_policies = [
+            {
+                "switch": []
+            }
+        ]
+
+        # Loop over each serial number obtained from NDFC
+        for ndfc_sw_serial_number in ndfc_sw_serial_numbers:
+            # Check if the serial number from NDFC matches any serial number for a switch in the data model
+            # If found, grab the specific switch entry from the data model
+            # Also if a match, set the IP mgmt information for the current switch found
+            if any(switch["serial_number"] == ndfc_sw_serial_number for switch in dm_topology_switches):
+                dm_switch_found = next(
+                    (dm_topology_switch for dm_topology_switch in dm_topology_switches if dm_topology_switch["serial_number"] == ndfc_sw_serial_number)
+                )
+                dm_management_ipv4_address = dm_switch_found["management"].get("management_ipv4_address", None)
+                dm_management_ipv6_address = dm_switch_found["management"].get("management_ipv6_address", None)
+
+            # Check if the name matching either the IPv4 or IPv6 mgmt address is found in the policy switches data model
+            # If found, grab the specific entry from the policy switches data model and store
+            # This stores the current switches policy group list
+            if any(
+                (switch["name"] == dm_management_ipv4_address for switch in dm_policy_switches) or
+                (switch["name"] == dm_management_ipv6_address for switch in dm_policy_switches)
+            ):
+                dm_policy_switch = next(
+                    (
+                        dm_policy_switch for dm_policy_switch in dm_policy_switches
+                        if dm_policy_switch["name"] == dm_management_ipv4_address or dm_policy_switch["name"] == dm_management_ipv6_address
+                    )
+                )
+
+                # Loop over each policy group associated to the current switch in the data model
+                for dm_sw_policy_group in dm_policy_switch["groups"]:
+                    # Check if the policy group name associated to the switch is found in the policy groups data model
+                    # If found, store a list of current policies that are part of that policy group in the data model
+                    # In the process of storing, reformat the policy description name to prepend "nac_" and replace white spaces with underscores
+                    if any(dm_policy_group["name"] == dm_sw_policy_group for dm_policy_group in dm_policy_groups):
+                        current_sw_policies = next(
+                            (
+                                ["nac_" + policy["name"].replace(" ", "_") for policy in dm_policy_group["policies"]]
+                                for dm_policy_group in dm_policy_groups if dm_policy_group["name"] == dm_sw_policy_group
+                            )
+                        )
+
+                # Query NDFC for the current switch's serial number to get back any policy that exists for that switch
+                # with the description prepended with "nac_"
+                ndfc_policies_with_nac_desc = ndfc_get_nac_switch_policy_using_desc(self, task_vars, tmp, ndfc_sw_serial_number)
+
+                # Currently, check two things to determine an unmanaged policy:
+                # Check no matching policy in the data model against the policy returned from NDFC for the current switch
+                # This check uses the prepended "nac_"
+                # Additionally, as of now, check no matching policy is from the VRF Lite policy of the data model
+                if any(
+                    ((ndfc_policy_with_desc["description"] not in current_sw_policies) and (ndfc_policy_with_desc["description"] not in vrf_lites))
+                    for ndfc_policy_with_desc in ndfc_policies_with_nac_desc
+                ):
+                    # If found, do the following:
+                    # Update Ansible result status
+                    # Add the switch to unmanaged_policies payload
+                    # Get the last index of where the switch was added
+                    # Build specific unmanaged policy entry
+                    # Add unmanaged policy entry to last switch added to list
+
+                    # Update Ansible for a configuration change
+                    results['changed'] = True
+
+                    # Update unmanaged_policies with the IP address of the switch that now has unmanaged policy
+                    # The NDFC policy module can take a list of various dictionaries with the switch key previously being pre-stored
+                    # Given this, each update the switch element with a new switch entry is the zeroth reference location always in unmanaged_policies
+                    # Example:
+                    # [
+                    #     {
+                    #         "switch": [
+                    #             {
+                    #                 "ip": <mgmt_ip_address>,
+                    #             }
+                    #         ]
+                    #     }
+                    # ]
+                    unmanaged_policies[0]["switch"].append(
+                        {
+                            "ip": dm_management_ipv4_address if dm_management_ipv4_address else dm_management_ipv6_address
+                        }
+                    )
+
+                    # Grab the last index of a switch added
+                    last_idx = len(unmanaged_policies[0]["switch"]) - 1
+
+                    # Since initially found there is indeed an unmananged policy, build a list of unmanaged policy
+                    _unmanaged_policies = [
+                        {
+                            "name": ndfc_policy_with_desc["policyId"],
+                            "description": ndfc_policy_with_desc["description"]
+                        }
+                        for ndfc_policy_with_desc in ndfc_policies_with_nac_desc
+                        if ((ndfc_policy_with_desc["description"] not in current_sw_policies) and (ndfc_policy_with_desc["description"] not in vrf_lites))
+                    ]
+
+                    # Update the dictionary entry for the last switch with the expected policies key the NDFC policy module expects
+                    unmanaged_policies[0]["switch"][last_idx].update(
+                        {
+                            "policies": _unmanaged_policies
+                        }
+                    )
+
+                    # Example of unmanaged policy payload:
+                    # [
+                    #     {
+                    #         "switch": [
+                    #             {
+                    #                 "ip": '<ip_address>',
+                    #                 "policies": [
+                    #                     {
+                    #                         "name": <Policy ID>,
+                    #                         "description": "nac_<description>"
+                    #                     },
+                    #                     {
+                    #                         "name": <Policy ID>,
+                    #                         "description": "nac_<description>"
+                    #                     },
+                    #                 ]
+                    #             },
+                    #         ]
+                    #     }
+                    # ]
+
+        # Store the unmanaged policy payload for return and usage in the NDFC policy module to delete from NDFC
+        results['unmanaged_policies'] = unmanaged_policies
+
+        return results
diff --git a/plugins/action/dtc/update_switch_hostname_policy.py b/plugins/action/dtc/update_switch_hostname_policy.py
index 450c2b23..f52891b2 100644
--- a/plugins/action/dtc/update_switch_hostname_policy.py
+++ b/plugins/action/dtc/update_switch_hostname_policy.py
@@ -25,7 +25,7 @@
 __metaclass__ = type
 
 from ansible.plugins.action import ActionBase
-from ..helper_functions import ndfc_get_switch_policy
+from ...plugin_utils.helper_functions import ndfc_get_switch_policy_using_template
 
 
 class ActionModule(ActionBase):
@@ -41,12 +41,12 @@ def run(self, tmp=None, task_vars=None):
         policy_update = {}
 
         for switch_serial_number in switch_serial_numbers:
-            policy_match = ndfc_get_switch_policy(
+            policy_match = ndfc_get_switch_policy_using_template(
                 self=self,
                 task_vars=task_vars,
                 tmp=tmp,
-                template_name=template_name,
-                switch_serial_number=switch_serial_number
+                switch_serial_number=switch_serial_number,
+                template_name=template_name
             )
 
             switch_match = next((item for item in model_data["vxlan"]["topology"]["switches"] if item["serial_number"] == switch_serial_number))
diff --git a/plugins/action/helper_functions.py b/plugins/action/helper_functions.py
deleted file mode 100644
index 104b9fc7..00000000
--- a/plugins/action/helper_functions.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# 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
-
-# This is an example file for help functions that can be called by
-# our various action plugins for common routines.
-#
-# For example in prepare_serice_model.py we can do the following:
-#  from ..helper_functions import do_something
-
-def data_model_key_check(tested_object, keys):
-    dm_key_dict = {'keys_found': [], 'keys_not_found': [], 'keys_data': [], 'keys_no_data': []}
-    for key in keys:
-        if tested_object and key in tested_object:
-            dm_key_dict['keys_found'].append(key)
-            tested_object = tested_object[key]
-            if tested_object:
-                dm_key_dict['keys_data'].append(key)
-            else:
-                dm_key_dict['keys_no_data'].append(key)
-        else:
-            dm_key_dict['keys_not_found'].append(key)
-    return dm_key_dict
-
-
-def ndfc_get_switch_policy(self, task_vars, tmp, template_name, switch_serial_number):
-    policy_data = self._execute_module(
-        module_name="cisco.dcnm.dcnm_rest",
-        module_args={
-            "method": "GET",
-            "path": f"/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/policies/switches/{switch_serial_number}/SWITCH/SWITCH"
-        },
-        task_vars=task_vars,
-        tmp=tmp
-    )
-
-    try:
-        policy_match = next(
-            (item for item in policy_data["response"]["DATA"] if item["templateName"] == template_name and item['serialNumber'] == switch_serial_number)
-        )
-    except StopIteration:
-        err_msg = f"Policy for template {template_name} and switch {switch_serial_number} not found!"
-        err_msg += f" Please ensure switch with serial number {switch_serial_number} is part of the fabric."
-        raise Exception(err_msg)
-
-    return policy_match
diff --git a/plugins/plugin_utils/.gitkeep b/plugins/plugin_utils/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/plugins/plugin_utils/helper_functions.py b/plugins/plugin_utils/helper_functions.py
new file mode 100644
index 00000000..8c8a3df3
--- /dev/null
+++ b/plugins/plugin_utils/helper_functions.py
@@ -0,0 +1,141 @@
+# 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
+
+# This is an example file for help functions that can be called by
+# our various action plugins for common routines.
+#
+# For example in prepare_serice_model.py we can do the following:
+#  from ..helper_functions import do_something
+
+
+def data_model_key_check(tested_object, keys):
+    """
+    Check if key(s) are found and exist in the data model.
+
+    :Parameters:
+        :tested_object (dict): Data model to check for keys.
+        :keys (list): List of keys to check in the data model.
+
+    :Returns:
+        :dm_key_dict (dict): Dictionary of lists for keys found, not found, and corresponding data or empty data.
+
+    :Raises:
+        N/A
+    """
+    dm_key_dict = {'keys_found': [], 'keys_not_found': [], 'keys_data': [], 'keys_no_data': []}
+    for key in keys:
+        if tested_object and key in tested_object:
+            dm_key_dict['keys_found'].append(key)
+            tested_object = tested_object[key]
+            if tested_object:
+                dm_key_dict['keys_data'].append(key)
+            else:
+                dm_key_dict['keys_no_data'].append(key)
+        else:
+            dm_key_dict['keys_not_found'].append(key)
+
+    return dm_key_dict
+
+
+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.
+
+    :Parameters:
+        :self: Ansible action plugin instance object.
+        :task_vars (dict): Ansible task vars.
+        :tmp (None, optional): Ansible tmp object. Defaults to None via Action Plugin.
+        :switch_serial_number (str): The serial number of the managed switch for which the NDFC policy is to be retrieved.
+
+    :Returns:
+        :policy_data: The NDFC policy data for the given switch.
+
+    :Raises:
+        N/A
+    """
+    policy_data = self._execute_module(
+        module_name="cisco.dcnm.dcnm_rest",
+        module_args={
+            "method": "GET",
+            "path": f"/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/policies/switches/{switch_serial_number}/SWITCH/SWITCH"
+        },
+        task_vars=task_vars,
+        tmp=tmp
+    )
+
+    return policy_data
+
+
+def ndfc_get_switch_policy_using_template(self, task_vars, tmp, switch_serial_number, template_name):
+    """
+    Get NDFC policy for a given managed switch by the switch's serial number and a specified NDFC template name.
+
+    :Parameters:
+        :self: Ansible action plugin instance object.
+        :task_vars (dict): Ansible task vars.
+        :tmp (None, optional): Ansible tmp object. Defaults to None via Action Plugin.
+        :switch_serial_number (str): The serial number of the managed switch for which the NDFC policy is to be retrieved.
+        :template_name (str): The name of the NDFC template for which the policy is to be retrieved.
+
+    :Returns:
+        :policy_match: The NDFC policy data for the given switch and matching template.
+
+    :Raises:
+        :Exception: If the policy for the given switch and template is not found.
+    """
+    policy_data = ndfc_get_switch_policy(self, task_vars, tmp, switch_serial_number)
+
+    try:
+        policy_match = next(
+            (item for item in policy_data["response"]["DATA"] if item["templateName"] == template_name and item['serialNumber'] == switch_serial_number)
+        )
+    except StopIteration:
+        err_msg = f"Policy for template {template_name} and switch {switch_serial_number} not found!"
+        err_msg += f" Please ensure switch with serial number {switch_serial_number} is part of the fabric."
+        raise Exception(err_msg)
+
+    return policy_match
+
+
+def ndfc_get_nac_switch_policy_using_desc(self, task_vars, tmp, switch_serial_number):
+    """
+    Get NDFC policy for a given managed switch by the switch's serial number and the prepanded string nac.
+
+    :Parameters:
+        :self: Ansible action plugin instance object.
+        :task_vars (dict): Ansible task vars.
+        :tmp (None, optional): Ansible tmp object. Defaults to None via Action Plugin.
+        :switch_serial_number (str): The serial number of the managed switch for which the NDFC policy is to be retrieved.
+
+    :Returns:
+        :policy_match: The NDFC policy data for the given switch and matching template.
+
+    :Raises:
+        N/A
+    """
+    policy_data = ndfc_get_switch_policy(self, task_vars, tmp, switch_serial_number)
+
+    policy_match = [
+        item for item in policy_data["response"]["DATA"]
+        if item.get("description", None) and "nac_" in item.get("description", None) and item["source"] == ""
+    ]
+
+    return policy_match
diff --git a/roles/common_global/vars/main.yml b/roles/common_global/vars/main.yml
index 7821ee4d..3bdc7daf 100644
--- a/roles/common_global/vars/main.yml
+++ b/roles/common_global/vars/main.yml
@@ -39,6 +39,7 @@ nac_tags:
     - rr_manage_vpc_peers
     - rr_manage_links
     - rr_manage_switches
+    - rr_manage_policy
     # -------------------------
     - role_validate
     - role_create
@@ -62,6 +63,7 @@ nac_tags:
     - rr_manage_vpc_peers
     - rr_manage_links
     - rr_manage_switches
+    - rr_manage_policy
   # We need the ability to pass tags to the common role but we don't need the following
   #   - validate, cc_verify
   common_role:
@@ -80,6 +82,7 @@ nac_tags:
     - rr_manage_vpc_peers
     - rr_manage_links
     - rr_manage_switches
+    - rr_manage_policy
   # We need the ability to pass tags to the validate role but we don't need the following
   #   - cc_verify
   validate_role:
@@ -99,6 +102,7 @@ nac_tags:
     - rr_manage_vpc_peers
     - rr_manage_links
     - rr_manage_switches
+    - rr_manage_policy
   # All Create Tags
   create:
     - cr_manage_fabric
@@ -127,6 +131,7 @@ nac_tags:
     - rr_manage_vpc_peers
     - rr_manage_links
     - rr_manage_switches
+    - rr_manage_policy
   remove_interfaces:
     - rr_manage_interfaces
   remove_networks:
@@ -139,7 +144,7 @@ nac_tags:
     - rr_manage_links
   remove_switches:
     - rr_manage_switches
+  remove_policy:
+    - rr_manage_policy
   deploy:
     - role_deploy
-
-  
\ No newline at end of file
diff --git a/roles/dtc/common/templates/ndfc_policy.j2 b/roles/dtc/common/templates/ndfc_policy.j2
index 4f539725..6701cc09 100644
--- a/roles/dtc/common/templates/ndfc_policy.j2
+++ b/roles/dtc/common/templates/ndfc_policy.j2
@@ -2,6 +2,7 @@
 # This NDFC policy and switch attachments config data structure is auto-generated
 # DO NOT EDIT MANUALLY
 #
+{% if MD_Extended.vxlan.policy.policies | default([]) | length > 0 %}
 - switch:
 {% for switch in MD_Extended.vxlan.policy.switches %}
     - ip: {{ switch.name }}
@@ -12,8 +13,9 @@
 {% 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 %}
+{% set policy_name = policy_match.name | ansible.builtin.regex_replace('\\s+', '_') %}
         - create_additional_policy: False
-          description: {{ policy_match.name }}
+          description: {{ 'nac_' + policy_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 %}
@@ -42,3 +44,4 @@
 {% endfor %}
 {% endfor %}
 {% endfor %}
+{% endif %}
diff --git a/roles/dtc/connectivity_check/tasks/verify_ndfc_authorization.yml b/roles/dtc/connectivity_check/tasks/verify_ndfc_authorization.yml
index 6455bf6f..91c8868b 100644
--- a/roles/dtc/connectivity_check/tasks/verify_ndfc_authorization.yml
+++ b/roles/dtc/connectivity_check/tasks/verify_ndfc_authorization.yml
@@ -1,3 +1,24 @@
+# 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: Verify Authorization to NDFC
@@ -18,7 +39,7 @@
         validate_certs: false
         timeout: 30
       register: response
-      # no_log: true
+      no_log: true
       delegate_to: localhost
 
   rescue:
diff --git a/roles/dtc/connectivity_check/tasks/verify_ndfc_connectivity.yml b/roles/dtc/connectivity_check/tasks/verify_ndfc_connectivity.yml
index 647a448b..9fc51d92 100644
--- a/roles/dtc/connectivity_check/tasks/verify_ndfc_connectivity.yml
+++ b/roles/dtc/connectivity_check/tasks/verify_ndfc_connectivity.yml
@@ -1,3 +1,24 @@
+# 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: Verify Connection to NDFC {{ ansible_host }} on Port {{ ansible_httpapi_port | default(443) }}
diff --git a/roles/dtc/remove/tasks/interfaces.yml b/roles/dtc/remove/tasks/interfaces.yml
index 96e995bf..ab08d570 100644
--- a/roles/dtc/remove/tasks/interfaces.yml
+++ b/roles/dtc/remove/tasks/interfaces.yml
@@ -20,13 +20,13 @@
 # SPDX-License-Identifier: MIT
 ---
 
-- ansible.builtin.debug: msg="Removing all Unmanaged Fabric Interfaces. This could take several minutes..."
+- ansible.builtin.debug: msg="Removing Unmanaged Fabric Interfaces. This could take several minutes..."
   when:
     - switch_list.response.DATA | length > 0
     - (interface_delete_mode is defined) and (interface_delete_mode is true|bool)
 
-
-- cisco.dcnm.dcnm_interface:
+- name: Remove Unmanaged Fabric Interfaces
+  cisco.dcnm.dcnm_interface:
     fabric: "{{ MD.vxlan.global.name }}"
     state: overridden
     config: "{{ interface_all }}"
@@ -65,4 +65,4 @@
       - "-------------------------------------------------------------------------------------------------------------------"
       - "+ SKIPPING Remove Unmanaged Fabric Interfaces task because interface_delete_mode flag is set to False  +"
       - "-------------------------------------------------------------------------------------------------------------------"
-  when: not ((interface_delete_mode is defined) and (interface_delete_mode is true|bool))
\ No newline at end of file
+  when: not ((interface_delete_mode is defined) and (interface_delete_mode is true|bool))
diff --git a/roles/dtc/remove/tasks/links.yml b/roles/dtc/remove/tasks/links.yml
index 39677bac..d547771a 100644
--- a/roles/dtc/remove/tasks/links.yml
+++ b/roles/dtc/remove/tasks/links.yml
@@ -20,12 +20,12 @@
 # SPDX-License-Identifier: MIT
 ---
 
-- ansible.builtin.debug: msg="Removing all Unmanaged links. This could take several minutes..."
+- ansible.builtin.debug: msg="Removing Unmanaged Links. This could take several minutes..."
   when:
     - switch_list.response.DATA | length > 0
     - (link_vpc_delete_mode is defined) and (link_vpc_delete_mode is true|bool)
 
-- name: Remove Intra Fabric Links for vpc peering
+- name: Remove Intra Fabric Links for vPC Peering
   cisco.dcnm.dcnm_links:
     state: replaced
     src_fabric: "{{ MD_Extended.vxlan.global.name }}"
diff --git a/roles/dtc/remove/tasks/networks.yml b/roles/dtc/remove/tasks/networks.yml
index 3ae54cdd..82487a74 100644
--- a/roles/dtc/remove/tasks/networks.yml
+++ b/roles/dtc/remove/tasks/networks.yml
@@ -25,7 +25,8 @@
     - switch_list.response.DATA | length > 0
     - (network_delete_mode is defined) and (network_delete_mode is true|bool)
 
-- cisco.dcnm.dcnm_network:
+- name: Remove Unmanaged Fabric Networks
+  cisco.dcnm.dcnm_network:
     fabric: "{{ MD.vxlan.global.name }}"
     state: overridden
     config: "{{ net_config }}"
diff --git a/roles/dtc/remove/tasks/policy.yml b/roles/dtc/remove/tasks/policy.yml
new file mode 100644
index 00000000..d203a5ad
--- /dev/null
+++ b/roles/dtc/remove/tasks/policy.yml
@@ -0,0 +1,58 @@
+# 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
+---
+
+- block:
+  - ansible.builtin.debug: msg="Removing Unmanaged Fabric Policy From Switches. This could take several minutes..."
+
+  - name: Create List of Switch Serial Numbers from NDFC Switch List
+    ansible.builtin.set_fact:
+      switch_serial_numbers: "{{ switch_list.response.DATA | map(attribute='serialNumber') | list }}"
+    delegate_to: localhost
+
+  - name: Build Unmanaged Fabric Policy Payload
+    cisco.nac_dc_vxlan.dtc.unmanaged_policy:
+      switch_serial_numbers: "{{ switch_serial_numbers }}"
+      model_data: "{{ MD_Extended }}"
+    register: unmanaged_policy_config
+    # do not delegate_to: localhost as this action plugin uses Python to execute cisco.dcnm.dcnm_rest
+
+  - name: Remove Unmanaged NDFC Fabric Policy
+    cisco.dcnm.dcnm_policy:
+      fabric: "{{ MD.vxlan.global.name }}"
+      use_desc_as_key: true
+      config: "{{ unmanaged_policy_config.unmanaged_policies }}"
+      deploy: true
+      state: deleted
+    when: unmanaged_policy_config.unmanaged_policies | length > 0
+    vars:
+        ansible_command_timeout: 3000
+        ansible_connect_timeout: 3000
+  when:
+    - switch_list.response.DATA | length > 0
+    - (policy_delete_mode is defined) and (policy_delete_mode is true|bool)
+
+- ansible.builtin.debug:
+    msg:
+      - "--------------------------------------------------------------------------------------------------------"
+      - "+ SKIPPING Remove Unmanaged Policy from Switches task because policy_delete_mode flag is set to False   +"
+      - "--------------------------------------------------------------------------------------------------------"
+  when: not ((policy_delete_mode is defined) and (policy_delete_mode is true|bool))
diff --git a/roles/dtc/remove/tasks/sub_main.yml b/roles/dtc/remove/tasks/sub_main.yml
index 9a8f0e3c..d4f6daf8 100644
--- a/roles/dtc/remove/tasks/sub_main.yml
+++ b/roles/dtc/remove/tasks/sub_main.yml
@@ -32,15 +32,19 @@
 - ansible.builtin.debug: msg="Configuring NXOS Devices using NDFC (Direct to Controller)"
   tags: "{{ nac_tags.remove }}"
 
-- ansible.builtin.debug: msg="Query NDFC for List of Fabric Switches"
-  tags: "{{ nac_tags.remove }}"
-
-- cisco.dcnm.dcnm_rest:
+- name: Get List of Fabric Switches from NDFC
+  cisco.dcnm.dcnm_rest:
     method: GET
     path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ MD.vxlan.global.name }}/inventory/switchesByFabric"
   register: switch_list
   tags: "{{ nac_tags.remove }}"
 
+- name: Remove Fabric Policy
+  ansible.builtin.import_tasks: policy.yml
+  tags: "{{ nac_tags.remove_policy }}"
+  when:
+    - changes_detected_policy
+
 - name: Remove Fabric Interfaces
   ansible.builtin.import_tasks: interfaces.yml
   tags: "{{ nac_tags.remove_interfaces }}"
@@ -75,4 +79,4 @@
   ansible.builtin.import_tasks: switches.yml
   tags: "{{ nac_tags.remove_switches }}"
   when:
-    - changes_detected_inventory
\ No newline at end of file
+    - changes_detected_inventory
diff --git a/roles/dtc/remove/tasks/switches.yml b/roles/dtc/remove/tasks/switches.yml
index d7e70044..a3ab2de8 100644
--- a/roles/dtc/remove/tasks/switches.yml
+++ b/roles/dtc/remove/tasks/switches.yml
@@ -24,7 +24,7 @@
   when:
     - (inventory_delete_mode is defined) and (inventory_delete_mode is true|bool)
 
-- name: Remove NDFC Fabric Devices {{ MD.vxlan.global.name }}
+- name: Remove Unmanaged NDFC Fabric Devices
   cisco.dcnm.dcnm_inventory:
     fabric: "{{ MD.vxlan.global.name }}"
     config: "{{ updated_inv_config['updated_inv_list'] }}"
@@ -42,4 +42,4 @@
       - "----------------------------------------------------------------------------------------------------------"
       - "+ SKIPPING Remove NDFC Fabric Devices task because inventory_delete_mode flag is set to False +"
       - "----------------------------------------------------------------------------------------------------------"
-  when: not ((inventory_delete_mode is defined) and (inventory_delete_mode is true|bool))
\ No newline at end of file
+  when: not ((inventory_delete_mode is defined) and (inventory_delete_mode is true|bool))
diff --git a/roles/dtc/remove/tasks/vpc_peers.yml b/roles/dtc/remove/tasks/vpc_peers.yml
index 1a426282..9c400a64 100644
--- a/roles/dtc/remove/tasks/vpc_peers.yml
+++ b/roles/dtc/remove/tasks/vpc_peers.yml
@@ -20,12 +20,12 @@
 # SPDX-License-Identifier: MIT
 ---
 
-- ansible.builtin.debug: msg="Removing all Unmanaged vPC Peering. This could take several minutes..."
+- ansible.builtin.debug: msg="Removing Unmanaged vPC Peering. This could take several minutes..."
   when:
     - switch_list.response.DATA | length > 0
     - (vpc_delete_mode is defined) and (vpc_delete_mode is true|bool)
 
-- name: Remove vPC Peering
+- name: Remove Unmanaged vPC Peering
   cisco.dcnm.dcnm_vpc_pair:
     src_fabric: "{{ MD.vxlan.global.name }}"
     deploy: true
diff --git a/roles/dtc/remove/tasks/vrfs.yml b/roles/dtc/remove/tasks/vrfs.yml
index 83aa51b3..87e39d7e 100644
--- a/roles/dtc/remove/tasks/vrfs.yml
+++ b/roles/dtc/remove/tasks/vrfs.yml
@@ -25,7 +25,8 @@
     - switch_list.response.DATA | length > 0
     - (vrf_delete_mode is defined) and (vrf_delete_mode is true|bool)
 
-- cisco.dcnm.dcnm_vrf:
+- name: Remove Unmanaged Fabric VRFs
+  cisco.dcnm.dcnm_vrf:
     fabric: "{{ MD.vxlan.global.name }}"
     state: overridden
     config: "{{ vrf_config }}"
diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt
index 18784758..956af7b1 100644
--- a/tests/sanity/ignore-2.14.txt
+++ b/tests/sanity/ignore-2.14.txt
@@ -17,10 +17,10 @@ plugins/action/dtc/vpc_pair_check.py action-plugin-docs # action plugin has no m
 plugins/action/dtc/verify_tags.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtc/diff_model_changes.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtc/update_switch_hostname_policy.py action-plugin-docs # action plugin has no matching module to provide documentation
+plugins/action/dtc/unmanaged_policy.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtc/get_poap_data.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtd/prepare_service_model.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/test/inventory.py action-plugin-docs # action plugin has no matching module to provide documentation
-plugins/action/helper_functions.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/common/nac_dc_validate.py import-3.10!skip
 plugins/action/test/inventory.py import-3.10!skip
 plugins/action/common/run_map.py import-3.10!skip
diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt
index 18784758..956af7b1 100644
--- a/tests/sanity/ignore-2.15.txt
+++ b/tests/sanity/ignore-2.15.txt
@@ -17,10 +17,10 @@ plugins/action/dtc/vpc_pair_check.py action-plugin-docs # action plugin has no m
 plugins/action/dtc/verify_tags.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtc/diff_model_changes.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtc/update_switch_hostname_policy.py action-plugin-docs # action plugin has no matching module to provide documentation
+plugins/action/dtc/unmanaged_policy.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtc/get_poap_data.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtd/prepare_service_model.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/test/inventory.py action-plugin-docs # action plugin has no matching module to provide documentation
-plugins/action/helper_functions.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/common/nac_dc_validate.py import-3.10!skip
 plugins/action/test/inventory.py import-3.10!skip
 plugins/action/common/run_map.py import-3.10!skip
diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt
index 18784758..956af7b1 100644
--- a/tests/sanity/ignore-2.16.txt
+++ b/tests/sanity/ignore-2.16.txt
@@ -17,10 +17,10 @@ plugins/action/dtc/vpc_pair_check.py action-plugin-docs # action plugin has no m
 plugins/action/dtc/verify_tags.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtc/diff_model_changes.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtc/update_switch_hostname_policy.py action-plugin-docs # action plugin has no matching module to provide documentation
+plugins/action/dtc/unmanaged_policy.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtc/get_poap_data.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/dtd/prepare_service_model.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/test/inventory.py action-plugin-docs # action plugin has no matching module to provide documentation
-plugins/action/helper_functions.py action-plugin-docs # action plugin has no matching module to provide documentation
 plugins/action/common/nac_dc_validate.py import-3.10!skip
 plugins/action/test/inventory.py import-3.10!skip
 plugins/action/common/run_map.py import-3.10!skip