#!/usr/bin/python
#
# Copyright (c) 2021-2023 Cisco and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function

__metaclass__ = type
__author__ = "Mallik Mudigonda"

DOCUMENTATION = """
---
module: dcnm_service_policy
short_description: DCNM ansible module for managing service policies.
version_added: "1.2.0"
description:
    - DCNM ansible module for creating, deleting, querying and modifying service policies
author: Mallik Mudigonda (@mmudigon)
options:
  fabric:
    description:
      - 'name of the target fabric for service policy operations'
    type: str
    required: true
  service_fabric:
    description:
      - 'name of the external fabric attached to the service node for service policy operations'
    type: str
    required: true
  state:
    description:
      - the required state of the configuration after module completion.
    type: str
    required: false
    choices:
      - merged
      - replaced
      - overridden
      - deleted
      - query
    default: merged
  attach:
    description:
      - a flag specifying if the given service policy is to be attached to the specified service node
    type: bool
    required: false
    default: true
  deploy:
    description:
      - a flag specifying if a service policy is to be deployed on the switches
    type: bool
    required: false
    default: true
  config:
    description:
      - a list of dictionaries containing service policy and switch information
    type: list
    elements: dict
    suboptions:
      name:
        description:
          - a unique name which identifies the service policy
        type: str
        required: true
      src_vrf:
        description:
          - name of the source vrf for this service policy
        type: str
        required: true
      dest_vrf:
        description:
          - name of the destination vrf for this service policy
        type: str
        required: true
      src_network:
        description:
          - name of the source network for this service policy
        type: str
        required: true
      dest_network:
        description:
          - name of the destination network for this service policy
        type: str
        required: true
      next_hop:
        description:
          - next hop ip address to be used in source to network direction
          - This must exactly match the next hop IP configured for the route
            peering associated with this policy
        type: str
        required: false
        default: ''
      reverse_next_hop:
        description:
          - reverse next hop ip address to be used in network to source direction
          - This must exactly match the reverse next hop IP configured for the route
            peering associated with this policy
        type: str
        required: false
        default: ''
      policy:
        description:
          - details of the policy (ACL) to be applied
        type: dict
        suboptions:
          proto:
            description:
              - protocol to be matched to apply this ACL
            type: str
            required: true
            choices:
              - ip
              - icmp
              - tcp
              - udp
          src_port:
            description:
              - source port number to be matched to apply this ACL
            type: str
            required: true
            choices:  [any, Min 1, Max 65535]
          dest_port:
            description:
              - destination port number to be matched to apply this ACL
            type: str
            required: true
            choices:  [any, Min 1, Max 65535]
          action:
            description:
              - action to apply for traffic matching the service profile
            type: str
            required: false
            choices:  [permit, deny]
            default: permit
          next_hop_option:
            description:
              - option to specify how to redirect traffic
            type: str
            required: false
            choices:  [none, drop-on-fail, drop]
            default: none
          acl_name:
            description:
              - Name of the ACL in the forward direction
            type: str
            required: false
            default: will be auto-generated by DCNM
          rev_acl_name:
            description:
              - Name of the ACL in the reverse direction
            type: str
            required: false
            default: will be auto-generated by DCNM
          route_map_num:
            description:
              - route map match number
              - Minimum Value (1), Maximum Value (65535)
              - Default value is auto-generated by DCNM
            type: int
            required: false
          rev_route_map_num:
            description:
              - route map match number for reverse direction
              - Minimum Value (1), Maximum Value (65535)
              - Default value is auto-generated by DCNM
            type: int
            required: false
"""

EXAMPLES = """
# L4-L7 Service Insertion:
#
# Cisco DCNM has the ability to insert Layer 4-Layer 7 (L4-L7) service devices in a data center fabric, and also enables selectively
# redirecting traffic to these service devices. You can add a service node, create route peering between the service node and the
# service leaf switch, and then selectively redirect traffic to these service nodes. Ansible collections support 3 modules viz.
# Service Node, Service Route Peering and Service Policy to enable this.
#
# Service Node:
#
# You have to create an external fabric and specify that a service node resides in that external fabric during service node creation.
# Service policies are created on the service node to determine the actions to be applied to the traffic
#
# Route Peerings:
#
# Multiple Service Route Peerings can be created under service node. Each Route Peering creates required service networks that is used to
# carry traffic towards the service node.
#
# Service Policy:
#
# Each route peering can have multiple service policies. Service policies can only be created for networks created through route peerings.
# The service policies define the actions to be taken for matching traffic.
#
# Dependency Tree:
#
# Service Node
# |
# |---- Route Peering 1
# |     |
# .     |---- Service Policy 1
# .     |
# .     .
# .     .
# .     .
# .     |---- Service Policy N
# .
# |---- Route Peering N
#       |
#       |---- Service Policy 1
#       |
#       .
#       .
#       .
#       |---- Service Policy N
#
#
# This module supports the following states:

# Merged:
#   Service Policies defined in the playbook will be merged into the target fabric.
#     - If the Service Policies does not exist it will be added.
#     - If the Service Policies exists but properties managed by the playbook are different
#       they will be updated if possible.
#     - Service Policies that are not specified in the playbook will be untouched.
#
# Replaced:
#   Service Policies defined in the playbook will be replaced in the target fabric.
#     - If the Service Policies does not exist it will be added.
#     - If the Service Policies exists but properties managed by the playbook are different
#       they will be updated if possible.
#     - Properties that can be managed by the module but are not specified
#       in the playbook will be deleted or defaulted if possible.
#     - Service Policies that are not specified in the playbook will be untouched.
#
# Overridden:
#   Service Policies defined in the playbook will be overridden in the target fabric.
#     - If the Service Policies does not exist it will be added.
#     - If the Service Policies exists but properties managed by the playbook are different
#       they will be updated if possible.
#     - Properties that can be managed by the module but are not specified
#       in the playbook will be deleted or defaulted if possible.
#     - Service Policies that are not specified in the playbook will be deleted.
#
# Deleted:
#   Service Policies defined in the playbook will be deleted.
#
# Query:
#   Returns the current DCNM state for the Service Policies listed in the playbook.

# CREATING SERVICE POLICIES
# =========================

- name: Create service policy including all optional objects
  cisco.dcnm.dcnm_service_policy:
    fabric: test_fabric
    service_fabric: external
    attach: true
    deploy: true
    state: merged
    config:
      - name: service_policy_1
        node_name: it-sn-1
        rp_name: it-fw-rp1
        src_vrf: vrf_11
        dest_vrf: vrf_11
        src_network: net_11
        dest_network: net_12
        next_hop: 192.161.1.100
        reverse_next_hop: 192.161.2.100
        reverse: true
        policy:
          proto: tcp
          src_port: any
          dest_port: 22
          action: permit
          next_hop_option:  none
          acl_name: fwd_acl_10
          rev_acl_name: rev_acl_10
          route_map_num: 101
          rev_route_map_num: 102

# DELETE SERVICE POLICIES
# =======================

# 1. With Policy Name and Node name
#
# Deletes the specific service policy specified from the given node

- name: Delete service policies with policy name and node name
  cisco.dcnm.dcnm_service_policy:
    fabric: test_fabric
    service_fabric: external
    state: deleted
    config:
      - name: service_policy_1
        node_name: it-sn-1

      - name: service_policy_2
        node_name: it-sn-2

      - name: service_policy_3
        node_name: it-sn-2

      - name: service_policy_4
        node_name: it-sn-2

      - name: service_policy_5
        node_name: it-sn-2

# 2. With Node name alone
#
# Deletes all service policies from the specified nodes

- name: Delete service policies with Node names
  cisco.dcnm.dcnm_service_policy:
    fabric: test_fabric
    service_fabric: external
    state: deleted
    config:
      - node_name: it-sn-1
      - node_name: it-sn-2

# 3. With Node name and RP name
#
# Deletes all service policies under the specified route peering and node

- name: Delete service policies with Node name and RP name
  cisco.dcnm.dcnm_service_policy:
    fabric: test_fabric
    service_fabric: external
    state: deleted
    config:
      - node_name: it-sn-1
        rp_name: it-fw-rp1

      - node_name: it-sn-2
        rp_name: it-fw-rp2

# 4. Without config
#
# Deletes all service policies on the given fabric and attached fabric

- name: Delete service policies without config
  cisco.dcnm.dcnm_service_policy:
    fabric: test_fabric
    service_fabric: external
    state: deleted

# OVERRIDE SERVICE POLICIES
# =========================

# When this playbook is executed, service policy service_policy_1 will be created or replaced and all
# other service policies in test_fabric and external will be deleted

- name: Override all existing service policies with a new one
  cisco.dcnm.dcnm_service_policy:
    fabric: test_fabric
    service_fabric: external
    attach: true
    deploy: true
    state: overridden
    config:
      - name: service_policy_1
        node_name: it-sn-1
        rp_name: it-fw-rp1
        src_vrf: vrf_11
        dest_vrf: vrf_11
        src_network: net_11
        dest_network: net_12
        next_hop: 192.161.1.100
        reverse_next_hop: 192.161.2.100
        policy:
          proto: icmp
          src_port: 555
          dest_port: 22
          action: permit
          next_hop_option:  none
          acl_name: fwd_acl_555
          rev_acl_name: rev_acl_555
          route_map_num: 555
          rev_route_map_num: 556

# REPLACE SERVICE POLICIES
# ========================

- name: Replace service policy_1 with the one specified below
  cisco.dcnm.dcnm_service_policy:
    fabric: test_fabric
    service_fabric: external
    attach: true
    deploy: true
    state: replaced
    config:
      - name: service_policy_1
        node_name: it-sn-1
        rp_name: it-fw-rp1
        src_vrf: vrf_11
        dest_vrf: vrf_11
        src_network: net_11
        dest_network: net_12
        next_hop: 192.161.1.100
        reverse_next_hop: 192.161.2.100
        policy:
          proto: udp
          src_port: 501
          dest_port: 502
          action: deny
          next_hop_option: drop_on_fail

# QUERY SERVICE POLICIES
# ======================

- name: Query service policies based on service node and policy name
  cisco.dcnm.dcnm_service_policy:
    fabric: test_fabric
    service_fabric: external
    state: query
    config:
      - name: service_policy_1
        node_name: it-sn-1

- name: Query service policies based on service node
  cisco.dcnm.dcnm_service_policy:
  fabric: test_fabric
  service_fabric: external
  state: query
  config:
    - node_name: it-sn-1

"""

import time
import json
import copy

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import (
    dcnm_send,
    validate_list_of_dicts,
    dcnm_reset_connection,
    dcnm_version_supported,
)

from datetime import datetime


# Service Policy Class object which includes all the required methods and data to configure and maintain service policy objects
class DcnmServicePolicy:
    dcnm_sp_paths = {
        11: {
            "GET_SNODE_WITH_NAME": "/appcenter/Cisco/elasticservice/elasticservice-api/fabrics/{}/service-nodes/{}",
            "GET_SP_INFO_WITH_SN": "/appcenter/Cisco/elasticservice/elasticservice-api/fabrics/{}/service-nodes/{}/policies/{}",
            "GET_SERVICE_NODES": "/appcenter/Cisco/elasticservice/elasticservice-api/?attached-fabric={}",
            "GET_SP_INFO": "/appcenter/Cisco/elasticservice/elasticservice-api/fabrics/{}/service-nodes/{}/policies/{}/{}",
            "GET_DEPLOY_STATUS": "/appcenter/Cisco/elasticservice/elasticservice-api/fabrics/{}/service-nodes/{}/policies/{}",
            "CREATE_SP": "/appcenter/Cisco/elasticservice/elasticservice-api/fabrics/{}/service-nodes/{}/policies",
            "UPDATE_SP": "/appcenter/Cisco/elasticservice/elasticservice-api/fabrics/{}/service-nodes/{}/policies/{}/{}",
            "SP_DETACH_SUFFIX": "/attachments?policy-names=",
            "SP_DELETE": "/appcenter/Cisco/elasticservice/elasticservice-api/fabrics/{}/service-nodes/{}/policies/{}/{}",
            "SP_DEPLOY_SUFFIX": "/deployments",
            "SP_CFG_SAVE_DEPLOY": "/rest/control/fabrics/{}/config-deploy",
            "SP_PREFIX": "/appcenter/Cisco/elasticservice/elasticservice-api/fabrics/{}/service-nodes/{}/policies/{}",
        },
        12: {
            "GET_SNODE_WITH_NAME": "/appcenter/cisco/ndfc/api/v1/elastic-service/fabrics/{}/service-nodes/{}",
            "GET_SP_INFO_WITH_SN": "/appcenter/cisco/ndfc/api/v1/elastic-service/fabrics/{}/service-nodes/{}/policies/{}",
            "GET_SERVICE_NODES": "/appcenter/cisco/ndfc/api/v1/elastic-service/service-nodes?attached-fabric={}",
            "GET_SP_INFO": "/appcenter/cisco/ndfc/api/v1/elastic-service/fabrics/{}/service-nodes/{}/policies/{}/{}",
            "GET_DEPLOY_STATUS": "/appcenter/cisco/ndfc/api/v1/elastic-service/fabrics/{}/service-nodes/{}/policies/{}",
            "CREATE_SP": "/appcenter/cisco/ndfc/api/v1/elastic-service/fabrics/{}/service-nodes/{}/policies",
            "UPDATE_SP": "/appcenter/cisco/ndfc/api/v1/elastic-service/fabrics/{}/service-nodes/{}/policies/{}/{}",
            "SP_DETACH_SUFFIX": "/attachments?policy-names=",
            "SP_DELETE": "/appcenter/cisco/ndfc/api/v1/elastic-service/fabrics/{}/service-nodes/{}/policies/{}/{}",
            "SP_DEPLOY_SUFFIX": "/deployments",
            "SP_CFG_SAVE_DEPLOY": "/rest/control/fabrics/{}/config-deploy",
            "SP_PREFIX": "/appcenter/cisco/ndfc/api/v1/elastic-service/fabrics/{}/service-nodes/{}/policies/{}",
        },
    }

    def __init__(self, module):
        self.module = module
        self.params = module.params
        self.fabric = module.params["fabric"]
        self.config = copy.deepcopy(module.params.get("config"))
        self.sp_info = []
        self.want = []
        self.have = []
        self.have_all = {}
        self.diff_create = []
        self.diff_modify = []
        self.diff_delete = []
        self.diff_deploy = []
        self.fd = None
        self.changed_dict = [
            {
                "merged": [],
                "modified": [],
                "deleted": [],
                "deploy": [],
                "query": [],
                "debugs": [],
            }
        ]
        self.dcnm_version = dcnm_version_supported(self.module)

        self.paths = self.dcnm_sp_paths[self.dcnm_version]
        self.result = dict(changed=False, diff=[], response=[])

    def log_msg(self, msg):

        if self.fd is None:
            self.fd = open("sp.log", "a+")
        if self.fd is not None:
            self.fd.write(msg)
            self.fd.write("\n")
            self.fd.flush()

    def dcnm_sp_validate_and_build_sp_info(self, cfg, sp_spec, sp_policy_spec):

        """
        Routine to validate the playbook input and fill up default values for objects not included. It takes specific profiles
        to validate the input against. In this case we validate the playbook against sp_spec which inlcudes common information
        sp_policy_spec which inlcudes policy specific information.  This routine updates self.sp_info with validated playbook
        information by defaulting values not included

        Parameters:
            cfg (dict): The config from playbook
            sp_spec (dict): Service Policy common spec
            sp_policy_spec (dict): Service policy related policy information

        Returns:
            None
        """

        sp_info, invalid_params = validate_list_of_dicts(cfg, sp_spec)
        if invalid_params:
            if cfg[0].get("name", " ") != " ":
                mesg = "Invalid parameters in playbook: {0}".format(
                    "while processing Service Policy -  "
                    + cfg[0]["name"]
                    + ", "
                    + "\n".join(invalid_params)
                )
            else:
                mesg = "Invalid parameters in playbook: {0}".format(
                    "while processing Service Policy -  Unknown, "
                    + "\n".join(invalid_params)
                )
            self.module.fail_json(msg=mesg)

        self.sp_info.extend(sp_info)

        pol_list = []
        for item in sp_info:

            pol_list.append(item["policy"])
            # Validate policy information included in the Service policy
            policy_info, invalid_params = validate_list_of_dicts(
                pol_list, sp_policy_spec
            )
            if invalid_params:
                mesg = "Invalid parameters in playbook: {0}".format(
                    "while processing policy under Service Policy - "
                    + cfg[0]["name"]
                    + ", "
                    + "\n".join(invalid_params)
                )
                self.module.fail_json(msg=mesg)
            pol_list.remove(item["policy"])
            item["policy"] = policy_info

    def dcnm_sp_validate_input(self):

        """
        Routine to validate playbook input based on the state. Since each state has a different i
        config structure, this routine handles the validation based on the given state

        Parameters:
            None

        Returns:
            None
        """

        if None is self.config:
            return

        cfg = []
        for item in self.config:

            citem = copy.deepcopy(item)

            cfg.append(citem)

            if self.module.params["state"] == "deleted":
                if item.get("name") or item.get("node_name") or item.get("rp_name"):
                    # config for delete state is different. So validate deleted state differently
                    self.dcnm_sp_validate_delete_state_input(cfg)
            elif self.module.params["state"] == "query":
                # config for query state is different. So validate query state differently
                self.dcnm_sp_validate_query_state_input(cfg)
            # For 'overridden' state, we can have full config for a policy or just service node name alone.
            # In the formar case go down to 'else' block to validate the full config
            elif (self.module.params["state"] == "overridden") and (
                item.get("name", None) is None
            ):
                pass
            else:
                self.dcnm_sp_validate_sp_input(cfg)
            cfg.remove(citem)

    def dcnm_sp_validate_sp_input(self, cfg):

        """
        Routine to validate the playbook input. This routine updates self.sp_info
        with validated playbook information by defaulting values not included

        Parameters:
            cfg (dict): The config from playbook

        Returns:
            None
        """

        sp_spec = dict(
            name=dict(required=True, type="str"),
            node_name=dict(required=True, type="str"),
            rp_name=dict(required=True, type="str"),
            src_vrf=dict(required=True, type="str"),
            dest_vrf=dict(required=True, type="str"),
            src_network=dict(required=True, type="str"),
            dest_network=dict(required=True, type="str"),
            next_hop=dict(required=True, type="ipv4"),
            reverse_next_hop=dict(type="ipv4", default=""),
            reverse=dict(required=False, type=bool, default=True),
            policy=dict(required=True, type="dict"),
        )

        sp_policy_spec = dict(
            proto=dict(required=True, type="str", choices=["ip", "icmp", "tcp", "udp"]),
            src_port=dict(required=True, type="str"),
            dest_port=dict(required=True, type="str"),
            action=dict(
                required=False,
                typr="str",
                default="permit",
                choices=["permit", "deny"],
            ),
            next_hop_option=dict(
                required=False,
                type="str",
                default="none",
                choices=["none", "drop_on_fail", "drop"],
            ),
            acl_name=dict(required=False, type="str", default=""),
            rev_acl_name=dict(required=False, type="str", default=""),
            route_map_num=dict(required=False, type="int", default=0),
            rev_route_map_num=dict(required=False, type="int", default=0),
        )

        self.dcnm_sp_validate_and_build_sp_info(cfg, sp_spec, sp_policy_spec)

    def dcnm_sp_validate_delete_state_input(self, cfg):

        """
        Playbook input will be different for differnt states. This routine validates the delete state
        input. This routine updates self.sp_info with validated playbook information related to delete
        state.

        Parameters:
            cfg (dict): The config from playbook

        Returns:
            None
        """

        sp_delete_spec = dict(
            node_name=dict(required=True, type="str"),
            rp_name=dict(required=False, type="str", default=""),
            name=dict(required=False, type="str", default=""),
        )

        sp_info, invalid_params = validate_list_of_dicts(cfg, sp_delete_spec)
        if invalid_params:
            if cfg[0].get("name", " ") != " ":
                mesg = "Invalid parameters in playbook: {0}".format(
                    "while processing Service Policy -  "
                    + cfg[0]["name"]
                    + ", "
                    + "".join(invalid_params)
                )
            else:
                mesg = "Invalid parameters in playbook: {0}".format(
                    "while processing Service Policy -  Unknown, "
                    + "".join(invalid_params)
                )
            self.module.fail_json(msg=mesg)

        self.sp_info.extend(sp_info)

    def dcnm_sp_validate_query_state_input(self, cfg):

        """
        Playbook input will be different for differnt states. This routine validates the query state
        input. This routine updates self.sp_info with validated playbook information related to query
        state.

        Parameters:
            cfg (dict): The config from playbook

        Returns:
           None
        """

        sp_query_spec = dict(
            node_name=dict(required=True, type="str"),
            name=dict(required=False, type="str", default="None"),
        )

        sp_info, invalid_params = validate_list_of_dicts(cfg, sp_query_spec)
        if invalid_params:
            mesg = "Invalid parameters in playbook: {0}".format(
                "while processing Service Policy -  "
                + cfg[0]["name"]
                + ", "
                + "\n".join(invalid_params)
            )
            self.module.fail_json(msg=mesg)

        self.sp_info.extend(sp_info)

    def dcnm_sp_get_service_node_type(self, sp):

        path = self.paths["GET_SNODE_WITH_NAME"].format(
            self.module.params["service_fabric"], sp["node_name"]
        )

        retries = 0
        while retries <= 30:

            retries += 1
            resp = dcnm_send(self.module, "GET", path)

            if resp and (resp["RETURN_CODE"] == 200) and resp["DATA"]:
                return resp["DATA"].get("type", None)
            else:
                self.dcnm_sp_check_for_errors_in_resp(resp)
                time.sleep(10)
                continue
        self.changed_dict[0]["debugs"].append({"GET_SNODE_TYPE": resp})

    def dcnm_sp_get_sp_payload(self, sp):

        """
        This routine builds the complete service policy payload based on the information in self.want

        Parameters:
            sp (dict): Service Policy information

        Returns:
            sp_payload (dict): SP payload information populated with appropriate data from playbook config
        """

        node_type = self.dcnm_sp_get_service_node_type(sp)

        sp_payload = {}

        # Populate the common information

        sp_payload["serviceNodeName"] = sp["node_name"]
        sp_payload["attachedFabricName"] = self.module.params["fabric"]
        sp_payload["serviceNodeType"] = node_type
        sp_payload["fabricName"] = self.module.params["service_fabric"]
        sp_payload["enabled"] = str(self.attach).lower()

        sp_payload["policyName"] = sp["name"]
        sp_payload["peeringName"] = sp["rp_name"]
        sp_payload["sourceVrfName"] = sp["src_vrf"]
        sp_payload["destinationVrfName"] = sp["dest_vrf"]
        sp_payload["sourceNetwork"] = sp["src_network"]
        sp_payload["destinationNetwork"] = sp["dest_network"]
        sp_payload["reverseEnabled"] = sp["reverse"]
        sp_payload["policyTemplateName"] = "service_pbr"
        sp_payload["nextHopIp"] = sp["next_hop"]
        if sp["reverse"]:
            sp_payload["reverseNextHopIp"] = sp["reverse_next_hop"]

        # Populate the policy information
        sp_payload["nvPairs"] = {}

        sp_payload["nvPairs"]["PROTOCOL"] = sp["policy"][0]["proto"]
        sp_payload["nvPairs"]["SRC_PORT"] = sp["policy"][0]["src_port"]
        sp_payload["nvPairs"]["DEST_PORT"] = sp["policy"][0]["dest_port"]
        sp_payload["nvPairs"]["NEXT_HOP_IP"] = sp["next_hop"]
        sp_payload["nvPairs"]["REVERSE"] = sp["reverse"]
        if sp["reverse"]:
            sp_payload["nvPairs"]["REVERSE_NEXT_HOP_IP"] = sp["reverse_next_hop"]
        sp_payload["nvPairs"]["FWD_DIRECTION"] = "true"
        sp_payload["nvPairs"]["ROUTE_MAP_ACTION"] = sp["policy"][0]["action"]
        sp_payload["nvPairs"]["NEXT_HOP_OPTION"] = sp["policy"][0][
            "next_hop_option"
        ].replace("_", "-")

        if sp["policy"][0]["acl_name"] != "":
            sp_payload["nvPairs"]["ACL_NAME"] = sp["policy"][0]["acl_name"]
        if sp["policy"][0]["rev_acl_name"] != "":
            if sp["reverse"]:
                sp_payload["nvPairs"]["ACL_NAME_REVERSE"] = sp["policy"][0][
                    "rev_acl_name"
                ]
        if sp["policy"][0]["route_map_num"] != 0:
            sp_payload["nvPairs"]["ROUTE_MAP_MATCH_SEQUENCE_NUMBER"] = str(
                sp["policy"][0]["route_map_num"]
            )
        if sp["policy"][0]["rev_route_map_num"] != 0:
            if sp["reverse"]:
                sp_payload["nvPairs"]["ROUTE_MAP_MATCH_SEQUENCE_NUMBER_REVERSE"] = str(
                    sp["policy"][0]["rev_route_map_num"]
                )

        return sp_payload

    def dcnm_sp_update_policy_information(self, want, have, cfg):

        if cfg["policy"].get("action", None) is None:
            want["nvPairs"]["ROUTE_MAP_ACTION"] = have["nvPairs"]["ROUTE_MAP_ACTION"]
        if cfg["policy"].get("next_hop_option", None) is None:
            want["nvPairs"]["NEXT_HOP_OPTION"] = have["nvPairs"]["NEXT_HOP_OPTION"]

    def dcnm_sp_update_want(self):

        """
        Routine to compare want and have and make approriate changes to want. This routine checks the existing
        information with the config from playbook and populates the payloads in self.want apropriately.
        This routine updates self.want with final paylload information after comparing self.want and self.have and
        the playbook information.

        Parameters:
            None

        Returns:
            None
        """

        # only for 'merged' state we need to update the objects that are not included in playbook with
        # values from self.have.

        if self.module.params["state"] != "merged":
            return

        if self.want == []:
            return

        for sp in self.want:

            # Get the matching have to copy values if required
            match_have = [
                have
                for have in self.have
                if (
                    (sp["policyName"] == have["policyName"])
                    and (sp["peeringName"] == have["peeringName"])
                    and (sp["fabricName"] == have["fabricName"])
                    and (sp["serviceNodeName"] == have["serviceNodeName"])
                    and (sp["attachedFabricName"] == have["attachedFabricName"])
                )
            ]
            if match_have == []:
                continue

            # Get the SP from self.config to check if a particular object is included or not
            match_cfg = [
                cfg
                for cfg in self.config
                if (
                    (sp["policyName"] == cfg["name"])
                    and (sp["peeringName"] == cfg["rp_name"])
                    and (sp["fabricName"] == self.module.params["service_fabric"])
                    and (sp["serviceNodeName"] == cfg["node_name"])
                    and (sp["attachedFabricName"] == self.module.params["fabric"])
                )
            ]
            if match_cfg == []:
                continue

            self.dcnm_sp_update_policy_information(sp, match_have[0], match_cfg[0])

    def dcnm_sp_get_want(self):

        """
        This routine updates self.want with the payload information based on the playbook configuration.

        Parameters:
            None

        Returns:
            None
        """

        if None is self.config:
            return

        if not self.sp_info:
            return

        # self.sp_info is a list of directories each having config related to a particular srp
        for sp_elem in self.sp_info:

            # If service policy name is not given, then that means we are handling the case of Playbook
            # including just the service node name and/or RP name. In such a casse we don't have to worry
            # about filling want
            if sp_elem.get("name", "") == "":
                continue

            sp_payload = self.dcnm_sp_get_sp_payload(sp_elem)
            if sp_payload not in self.want:
                self.want.append(sp_payload)

    def dcnm_sp_get_sp_info_with_service_node(self, node_name):

        """
        Routine to get all service policies based on the Service Node information included in the playbook.

        Parameters:
            node_name (string): service node name to fetch the service policy information from

        Returns:
            resp["DATA"] (dict): All service policies present on the specified service node
        """

        path = self.paths["GET_SP_INFO_WITH_SN"].format(
            self.module.params["service_fabric"],
            node_name,
            self.module.params["fabric"],
        )

        retries = 0
        while retries < 30:
            retries += 1
            resp = dcnm_send(self.module, "GET", path)

            if resp and resp["RETURN_CODE"] != 200:
                self.dcnm_sp_check_for_errors_in_resp(resp)
                time.sleep(10)
                continue

            break

        if resp and (resp["RETURN_CODE"] == 200) and resp["DATA"]:
            resp["RETRIES"] = retries
            return resp["DATA"]
        else:
            self.changed_dict[0]["debugs"].append({"GET_SP_WITH_SNODE": resp})
            return []

    def dcnm_sp_get_service_nodes_from_dcnm(self):

        """
        Routine to get list of all service nodes from DCNM.

        Parameters:
            None

        Returns:
            resp["DATA"] (dict): All service nodes on the specified fabric
        """

        path = self.paths["GET_SERVICE_NODES"].format(self.module.params["fabric"])

        retries = 0
        while retries < 30:
            retries += 1
            resp = dcnm_send(self.module, "GET", path)

            if resp and resp["RETURN_CODE"] != 200:
                self.dcnm_sp_check_for_errors_in_resp(resp)
                time.sleep(10)
                continue

            break

        if resp and (resp["RETURN_CODE"] == 200) and resp["DATA"]:
            resp["RETRIES"] = retries
            return resp["DATA"]
        else:
            self.changed_dict[0]["debugs"].append({"GET_SNODES": resp})
            return []

    def dcnm_sp_get_sp_info_from_dcnm(self, sp, sp_type):

        """
        Routine to get existing Service Policy information from DCNM which matches the given SRP.

        Parameters:
            sp  (dict): Service policy information
            sp_type (string): String indicating whether the 'srp' passed is in 'PLAYBOOK' format
                            or 'PAYLOAD' format
        Returns:
            resp["DATA"] (dict): SRP informatikon obtained from the DCNM server if it exists
            [] otherwise
        """

        if sp_type == "PAYLOAD":
            path = self.paths["GET_SP_INFO"].format(
                sp["fabricName"],
                sp["serviceNodeName"],
                sp["attachedFabricName"],
                sp["policyName"],
            )
        else:
            path = self.paths["GET_SP_INFO"].format(
                self.module.params["service_fabric"],
                sp["node_name"],
                self.module.params["fabric"],
                sp["name"],
            )

        resource_not_found = False
        retries = 0
        while retries < 30:
            retries += 1
            resp = dcnm_send(self.module, "GET", path)

            if resp and resp["RETURN_CODE"] != 200:
                if resp.get("DATA", None) is not None:
                    # Check if the error is "ResourceNotFound". In that case we can return without
                    # retrying.
                    if isinstance(resp["DATA"], dict):
                        if resp["DATA"].get("error", None) is not None:
                            if (
                                isinstance(resp["DATA"]["error"], dict)
                                and resp["DATA"]["error"].get("code")
                                == "ResourceNotFound"
                            ):
                                resource_not_found = True
                                break
                self.dcnm_sp_check_for_errors_in_resp(resp)
                time.sleep(10)
                continue

            break

        if resp and (resp["RETURN_CODE"] == 200) and resp["DATA"]:
            resp["RETRIES"] = retries
            return resp["DATA"]
        else:
            if resource_not_found is False:
                self.changed_dict[0]["debugs"].append({"GET_SP_INFO": resp})
            return []

    def dcnm_sp_get_have(self):

        """
        Routine to get exisitng service policy information from DCNM that matches information in self.want.
        This routine updates self.have with all the service policies that match the given playbook configuration

        Parameters:
            None

        Returns:
            None
        """

        if self.want == []:
            return

        for sp in self.want:
            have = self.dcnm_sp_get_sp_info_from_dcnm(sp, "PAYLOAD")
            if (have != []) and (have not in self.have):
                self.have.append(have)

    def dcnm_sp_get_sp_deployment_status(self, sp, refresh):

        """
        Routine to get the attachment/deployment information for a given service policies. This information
        is used to implement idempotent operations. Change is deployment state will be treated as a change
        in service policies during merge and replace operations.

        Parameters:
            sp (dict): Service policy information
            refresh (bool): A flag indicating if the required SP information is to be obtained from DCNM

        Returns:
            deployed (bool): a flag indicating is the given SRP is deployed
        """

        resp = None
        key = (
            sp["fabricName"]
            + "-"
            + sp["serviceNodeName"]
            + "-"
            + sp["attachedFabricName"]
        )
        if refresh or self.have_all.get(key) is None:

            self.have_all[key] = []
            # Get all policies and filter out the specific policy to check for attachment details.
            path = self.paths["GET_DEPLOY_STATUS"].format(
                sp["fabricName"], sp["serviceNodeName"], sp["attachedFabricName"]
            )

            retries = 0
            while retries < 30:
                retries += 1
                resp = dcnm_send(self.module, "GET", path, "")

                if resp and resp["RETURN_CODE"] != 200:
                    self.dcnm_sp_check_for_errors_in_resp(resp)
                    time.sleep(10)
                    continue

                if resp["RETURN_CODE"] == 200 and resp.get("DATA") == []:
                    time.sleep(10)
                    continue

                break

            if resp and (resp["RETURN_CODE"] == 200) and resp["DATA"]:
                self.have_all[key].extend(resp["DATA"])
            else:
                self.changed_dict[0]["debugs"].append({"GET_SP_ATT_STATUS": resp})
                resp["CHANGED"] = self.changed_dict[0]
                self.module.fail_json(msg=resp)

        # Filter out the required policy
        match_pol = [
            pol
            for pol in self.have_all[key]
            if (
                (sp["peeringName"] == pol["peeringName"])
                and (sp["fabricName"] == pol["fabricName"])
                and (sp["serviceNodeName"] == pol["serviceNodeName"])
                and (sp["attachedFabricName"] == pol["attachedFabricName"])
                and (sp["policyName"] == pol["policyName"])
            )
        ]

        deployed = False
        retry = False
        if match_pol:
            att_status = {
                "GET_SP_ATT_STATUS": match_pol[0]["status"],
                "PolicyName": match_pol[0]["policyName"],
            }
            if att_status not in self.changed_dict[0]["debugs"]:
                self.changed_dict[0]["debugs"].append(att_status)

            if (match_pol[0]["status"].lower() == "na") or (
                match_pol[0]["status"].lower() == "pending"
            ):
                return resp, False, False, match_pol[0]["status"].lower()
            elif match_pol[0]["status"].lower() == "success":
                pol_info = {}
                pol_info = self.dcnm_sp_combine_policies(sp, pol_info)

                for path in pol_info:
                    self.dcnm_sp_deploy_sp(path, pol_info[path])
                return resp, True, False, "success"
            elif match_pol[0]["status"].lower() == "out-of-sync":
                pol_info = {}
                pol_info = self.dcnm_sp_combine_policies(sp, pol_info)

                for path in pol_info:
                    self.dcnm_sp_deploy_sp(path, pol_info[path])
                return resp, True, False, "out-of-sync"
            elif match_pol[0]["status"].lower() == "in progress":
                return resp, True, True, match_pol[0]["status"].lower()
            elif match_pol[0]["status"].lower() == "failure":
                return resp, True, False, match_pol[0]["status"].lower()
            else:
                return resp, False, True, match_pol[0]["status"].lower()
        else:
            self.changed_dict[0]["debugs"].append(
                {
                    "GET_SP_ATT_STATUS": "No Matching Policy",
                    "PolicyName": sp["policyName"],
                }
            )
            return resp, False, False, None

        return (resp, retry, deployed, None)

    def dcnm_sp_compare_policy_info(self, want, have):

        """
        Routine to compare service policies from self.want and self.have and return an appropriate
        returen code.

        Parameters:
            want (dict): The SP payload information populated from playbook
            have (dict): The SP payload information that already exists

        Returns:
            DCNM_SRP_MATCH (string): if 'want' and 'have' match exactly
            DCNM_SRP_NO_MATCH (string): if given 'want and 'have differ
            mismatch_reasons (list): a list identifying objects that differed
        """

        mismatch_reasons = []

        if want["enabled"] != str(have["enabled"]).lower():
            mismatch_reasons.append("DCNM_SP_ENABLED_MISMATCH")
        if want["sourceVrfName"] != have["sourceVrfName"]:
            mismatch_reasons.append("DCNM_SP_SRC_VRF_MISMATCH")
        if want["destinationVrfName"] != have["destinationVrfName"]:
            mismatch_reasons.append("DCNM_SP_DST_VRF_MISMATCH")
        if want["sourceNetwork"] != have["sourceNetwork"]:
            mismatch_reasons.append("DCNM_SP_SRC_NETWORK_MISMATCH")
        if want["destinationNetwork"] != have["destinationNetwork"]:
            mismatch_reasons.append("DCNM_SP_DST_NETWORK_MISMATCH")
        if str(want["reverseEnabled"]).lower() != str(have["reverseEnabled"]).lower():
            mismatch_reasons.append("DCNM_SP_REV_ENABLED_MISMATCH")
        if want["policyTemplateName"] != have["policyTemplateName"]:
            mismatch_reasons.append("DCNM_SP_POL_TEMPLATE_MISMATCH")
        if want["nextHopIp"] != have["nextHopIp"]:
            mismatch_reasons.append("DCNM_SP_NHOP_IP_MISMATCH")

        # If reverseEnabled is False, the fields related to the reverse direction will not be
        # present. Check that before comparing
        rnhip = want.get("reverseNextHopIp")
        if rnhip and rnhip != have.get("reverseNextHopIp"):
            mismatch_reasons.append("DCNM_SP_REV_NHOP_IP_MISMATCH")

        for key in want["nvPairs"].keys():
            # key "REVERSE" will be 'true' or 'false' in 'have' where as it will be a boolean True or False
            # in 'want'. Convert the value in 'have' to bool before comparing

            if key == "REVERSE":

                if str(have["nvPairs"]["REVERSE"]).lower() == "true":
                    have["nvPairs"]["REVERSE"] = True
                if str(have["nvPairs"]["REVERSE"]).lower() == "false":
                    have["nvPairs"]["REVERSE"] = False
            if want["nvPairs"][key] != have["nvPairs"].get(key, None):
                mismatch_reasons.append("DCNM_SP_" + key + "_MISMATCH")

        if mismatch_reasons == []:
            return "DCNM_SRP_MATCH", mismatch_reasons
        else:
            return "DCNM_SRP_NO_MATCH", mismatch_reasons

    def dcnm_sp_compare_service_policies(self, sp):

        """
        Routine to compare service policies from self.want and self.have. Used during merge and replace.

        Parameters:
            sp (dict): The SP payload information

        Returns:
            DCNM_SRP_ADD_NEW (string): if the given SP does not exist
            DCNM_SRP_DONT_ADD (string): if given SP already exist and is exactly the same
            DCNM_SRP_MERGE (string): if given SP already exists but not exactly the same
        """

        found = False

        if self.have == []:
            return ("DCNM_SRP_ADD_NEW", None, [])

        match_have = [
            have
            for have in self.have
            if (
                (sp["peeringName"] == have["peeringName"])
                and (sp["fabricName"] == have["fabricName"])
                and (sp["serviceNodeName"] == have["serviceNodeName"])
                and (sp["attachedFabricName"] == have["attachedFabricName"])
                and (sp["policyName"] == have["policyName"])
            )
        ]
        for have in match_have:
            found = True

            # A matching SP found. Check if it exactly matches with what is being requested for
            rc, reasons = self.dcnm_sp_compare_policy_info(sp, have)

            if rc == "DCNM_SRP_MATCH":
                return ("DCNM_SRP_DONT_ADD", have, [])

        if found is True:
            # Found a matching service policy, but some of the objects don't match.
            # Go ahead and merge the objects into the existing srp
            return ("DCNM_SRP_MERGE", have, reasons)
        else:
            return ("DCNM_SRP_ADD_NEW", None, [])

    def dcnm_sp_get_diff_merge(self):

        """
        Routine to get a list of payload information, self.diff_create/self.diff_modify to create new or modify
        existing policies. This routine updates self.diff_merge/self.diff_modify	with service policy  payloads
        that are to created or modified.

        Parameters:
            None

        Returns:
            None
        """

        if not self.want:
            return

        for sp in self.want:

            rc, have, reasons = self.dcnm_sp_compare_service_policies(sp)

            if rc == "DCNM_SRP_ADD_NEW":
                # A sp does not exists, create a new one.
                if sp not in self.diff_create:
                    self.changed_dict[0]["merged"].append(sp)
                    self.diff_create.append(sp)
            elif rc == "DCNM_SRP_MERGE":
                # A sp exists and it needs to be updated
                self.changed_dict[0]["modified"].append(sp)
                self.changed_dict[0]["debugs"].append(
                    {"Policy": sp["policyName"], "REASONS": reasons}
                )
                self.diff_modify.append(sp)

            # Check the 'deploy' flag and decide if this sp is to be deployed
            if have is None:
                # A new service policy. If attach and deploy are set, attach and deploy
                if self.deploy is True:
                    ditem = {}
                    ditem["serviceNodeName"] = sp["serviceNodeName"]
                    ditem["attachedFabricName"] = sp["attachedFabricName"]
                    ditem["fabricName"] = sp["fabricName"]
                    ditem["peeringName"] = sp["peeringName"]
                    ditem["policyName"] = sp["policyName"]
                    self.diff_deploy.append(ditem)
            else:
                retries = 0
                refresh = False
                while retries < 30:
                    retries += 1
                    (
                        resp,
                        retry,
                        deployed,
                        status,
                    ) = self.dcnm_sp_get_sp_deployment_status(have, refresh)

                    # Sometimes due to timing a policy may return "Out-of-Sync" status. Retry a few times to make sure
                    # that it is really out of sync.
                    if retry:
                        time.sleep(10)
                        refresh = True
                    else:
                        break

                if self.deploy is True:
                    # We deploy when self.deploy is True and:
                    #   1. there are no changes due to this request(rc is DCNM_SRP_DONT_ADD), but the SRP is not deployed
                    #   2. there are changes due to this request (rc is DCNM_SRP_MERGE)
                    if ((rc == "DCNM_SRP_DONT_ADD") and (deployed is False)) or (
                        rc == "DCNM_SRP_MERGE"
                    ):
                        ditem = {}
                        ditem["serviceNodeName"] = sp["serviceNodeName"]
                        ditem["attachedFabricName"] = sp["attachedFabricName"]
                        ditem["fabricName"] = sp["fabricName"]
                        ditem["peeringName"] = sp["peeringName"]
                        ditem["policyName"] = sp["policyName"]
                        self.diff_deploy.append(ditem)

        if self.diff_deploy != []:
            self.changed_dict[0]["deploy"].extend(self.diff_deploy)

    def dcnm_sp_get_diff_deleted(self):

        """
        Routine to get a list of payload information that will be used to delete service policies.
        This routine updates self.diff_delete	with payloads that are used to delete service policies
        from the server.

        Parameters:
            None

        Returns:
            None
        """

        processed_nodes = []
        if self.sp_info == []:
            # In this case we need to get all service nodes and all service policies from those nodes
            # and delete them
            serv_nodes = self.dcnm_sp_get_service_nodes_from_dcnm()
            for snode in serv_nodes:
                if snode["name"] in processed_nodes:
                    continue
                sps = self.dcnm_sp_get_sp_info_with_service_node(snode["name"])
                self.diff_delete.extend(sps)
                self.changed_dict[0]["deleted"].extend(sps)
                processed_nodes.append(snode["name"])
        else:
            serv_nodes = [
                {
                    "node_name": d["node_name"],
                    "rp_name": d.get("rp_name", ""),
                    "name": d.get("name", ""),
                }
                for d in self.sp_info
            ]
            for snode in serv_nodes:
                match_sps = []
                # If policy name is given, get the specific policy
                if snode.get("name") != "":
                    sps = self.dcnm_sp_get_sp_info_from_dcnm(snode, "PLAYBOOK")
                    if sps != [] and sps not in self.diff_delete:
                        match_sps = sps
                else:
                    if snode["node_name"] in processed_nodes:
                        continue
                    # Policy name is not given. Get all policies based on Service Node. If RP
                    # name is given then filter the list based on that
                    sps = self.dcnm_sp_get_sp_info_with_service_node(snode["node_name"])
                    # From all the policies filter out the ones that have a matching RP
                    if snode.get("rp_name") != "":
                        match_sps = [
                            sp for sp in sps if (sp["peeringName"] == snode["rp_name"])
                        ]
                    else:
                        match_sps = sps

                    processed_nodes.append(snode["node_name"])
                if match_sps != []:
                    if isinstance(match_sps, list):
                        self.diff_delete.extend(match_sps)
                        self.changed_dict[0]["deleted"].extend(match_sps)
                    else:
                        self.diff_delete.append(match_sps)
                        self.changed_dict[0]["deleted"].append(match_sps)

    def dcnm_sp_get_diff_query(self):

        """
        Routine to get service policy information based on the playbook configuration.
        This routine updates self.result with SPs requested for in the playbook if they exist on
        the DCNM server.

        Parameters:
            None

        Returns:
            None
        """

        for sp in self.sp_info:

            # Query may or may not include the policy name. Get the SP info based on whether
            # a policy name is included or not. If a policy name is not included, then get all
            # policies from the service-node. Otherwise get the specific policy that is requested

            if sp["name"] != "None":
                # policy name included
                resp = self.dcnm_sp_get_sp_info_from_dcnm(sp, "PLAYBOOK")

                if resp != []:
                    self.result["response"].append(resp)
            else:
                # policy name is not included
                resp = self.dcnm_sp_get_sp_info_with_service_node(sp["node_name"])

                if resp != []:
                    self.result["response"].extend(resp)
            self.changed_dict[0]["query"].append(sp)

    def dcnm_sp_get_diff_overridden(self):

        """
        Routine to build payload information for overridden state. This routine will build delete list,
        merge list, replace list etc. based on what is required and what is already existing on the DCNM server.
        This routine updates self.diff_merge that contains all service policy that are to be created afresh and
        self.diff_dlete that contains all service policy that are to be deleted.

        Parameters:
            None

        Returns:
            None
        """

        # In this case we need to get all service nodes and all service policy from those nodes and delete them
        serv_nodes = self.dcnm_sp_get_service_nodes_from_dcnm()

        sp_list = []
        # From each of the service nodes get the list of all service policy.
        for snode in serv_nodes:
            sps = self.dcnm_sp_get_sp_info_with_service_node(snode["name"])
            sp_list.extend(sps)

        # Before we add a service policy to self.diff_delete, make sure a matching service policy
        # is not included in the current self.want. If it is, then we need not delete the same. We can just update
        # the same

        delete_sps = []

        for sp in sp_list:
            match_want = [
                want
                for want in self.want
                if (
                    (sp["policyName"] == want["policyName"])
                    and (sp["peeringName"] == want["peeringName"])
                    and (sp["fabricName"] == want["fabricName"])
                    and (sp["serviceNodeName"] == want["serviceNodeName"])
                    and (sp["attachedFabricName"] == want["attachedFabricName"])
                )
            ]
            if match_want == []:
                # There is no matching RP in want. The SRP can be deleted
                delete_sps.append(sp)

        self.diff_delete.extend(delete_sps)
        self.changed_dict[0]["deleted"].extend(delete_sps)

        # Now go and handle SPs in self.want
        self.dcnm_sp_get_diff_merge()

    def dcnm_sp_create_sp(self, sp, command):

        """
        Routine to send create payload to DCNM.

        Parameters:
            sp  (dict): Service Policy information
            command (string): REST API command, either POST or PUT

        Returns:
            resp (dict): Response from DCNM server
        """

        if command == "POST":
            path = self.paths["CREATE_SP"].format(
                sp["fabricName"], sp["serviceNodeName"]
            )
        else:
            path = self.paths["UPDATE_SP"].format(
                sp["fabricName"],
                sp["serviceNodeName"],
                sp["attachedFabricName"],
                sp["policyName"],
            )

        json_payload = json.dumps(sp)

        resp = dcnm_send(self.module, command, path, json_payload)
        return resp

    def dcnm_sp_detach_sp(self, fixed_path, policy_list):

        """
        Routine to detach SP from service node.

        Parameters:
            fixed_path: Path identifying the policy
            policy_list (dict): Service Poilicy information to be deployed

        Returns:
            resp (dict): Response from DCNM server
        """

        resp = None

        path = fixed_path + self.paths["SP_DETACH_SUFFIX"]
        path = path + ",".join(policy_list)

        resp = dcnm_send(self.module, "DELETE", path, "")
        return resp

    def dcnm_sp_delete_sp(self, sp):

        """
        Routine to delete an SP from service node.

        Parameters:
            srp (dict): Service Policy information that is to be deleted

        Returns:
            resp (dict): Response from DCNM server
        """

        path = self.paths["SP_DELETE"].format(
            sp["fabricName"],
            sp["serviceNodeName"],
            sp["attachedFabricName"],
            sp["policyName"],
        )
        # Delete the service policy

        json_payload = json.dumps(sp)

        resp = dcnm_send(self.module, "DELETE", path, json_payload)
        return resp

    def dcnm_sp_deploy_sp(self, fixed_path, policy_list):

        """
        Routine to deploy SP on the service node.

        Parameters:
            fixed_path: Path identifying the peering
            policy_list (dict): Service Policy information to be deployed

        Returns:
            resp (dict): Response from DCNM server
        """

        path = fixed_path + self.paths["SP_DEPLOY_SUFFIX"]
        json_payload = json.dumps(policy_list)

        resp = dcnm_send(self.module, "POST", path, json_payload)
        return resp

    def dcnm_sp_config_save_and_deploy(self):

        """
        Routine to save and deploy configuration for the entire box.

        Parameters:
            None

        Returns:
            resp (dict): Response from DCNM server
        """

        path = self.paths["SP_CFG_SAVE_DEPLOY"].format(self.module.params["fabric"])

        resp = dcnm_send(self.module, "POST", path, "")
        return resp

    def dcnm_sp_check_deployment_status(self, sp_list, final_state):

        """
        Routine to check deployment status of SPs on the service node. This routine is used for CREATE
        MODIFY and DELETE deploy status check. The 'final_state' argument will be based on what status
        to check for.

        Parameters:
            sp_list (dict): Service Policies information to be checked for deployment status
            final_state (string): A flag indicating what the required final state is

        Returns:
            None
        """

        # Scan each SP for its deployment status. If all are deployed, then we are done. Otherwise
        # the create or modify operation is a failure
        for sp in sp_list:

            retries = 0
            att_state = "Unknown"
            while retries < 50:
                retries += 1
                (
                    resp,
                    retry,
                    deployed,
                    att_state,
                ) = self.dcnm_sp_get_sp_deployment_status(sp, True)

                if att_state == final_state:
                    break
                # Sometimes the deploy state will remain in "success" state after detach and deploy. Go ahead and delete
                if final_state == "na":
                    if att_state.lower() == "success":
                        if (retries % 10) == 0:
                            att_state = final_state
                            break
                if att_state == "out-of-sync":
                    self.dcnm_sp_config_save_and_deploy()
                time.sleep(30)
            # After all retries, if the SP did not move to 'final_state' it is an error
            if att_state != final_state:
                self.module.fail_json(
                    msg={
                        "CHANGED": self.changed_dict[0],
                        "FAILURE REASON": "SP "
                        + sp["policyName"]
                        + " did not reach 'In-Sync' State",
                        "Attach State": att_state,
                    }
                )

    def dcnm_sp_combine_policies(self, sp, pol_info):

        """
        This routine will combine policies based on fabric, external fabric, service node so that deploy
        can be performed for all the policies together. If not, when multiple policies are configured under
        a single RP, a separate deploy operation may result in errors.

        Parameters:
            sp(dict): Service Policy information to be combined
            pol_info(dict): Dict containing the current list of combined policies

        Returns:
            pol_info(dict): A dict containing a list of combined policies including the current one
        """

        path = self.paths["SP_PREFIX"].format(
            sp["fabricName"], sp["serviceNodeName"], sp["attachedFabricName"]
        )

        if pol_info.get(path) is None:
            pol_info[path] = {"policyNames": []}
        pol_info.get(path)["policyNames"].append(sp["policyName"])

        return pol_info

    def dcnm_sp_check_for_errors_in_resp(self, resp):

        """
        Routine to check for "unauthorized" errors in which case the conncetion must be reset by logging out and
        logging in again.

        Parameters:
            resp (dict): Response which has to be checked for "unauthorized error"

        Returns:
            rc (string): unauthorized_error, if error code results in a connection reset
                         other_error, otherwise
        """

        rc = "other_error"
        if resp.get("DATA"):
            if isinstance(resp["DATA"], dict) and resp["DATA"].get("error"):
                if (
                    isinstance(resp["DATA"]["error"], dict)
                    and resp["DATA"]["error"].get("code") == "UserUnauthorized"
                ):
                    # We have seen "unauthorized error" from DCNM even though the token has been allocated and the connection timeout
                    # has not happened. As per suggestions from L4-L7 services team we will reset token by logging out and logging in
                    # again so that a new token is obtained
                    dcnm_reset_connection(self.module)
                    rc = "unauthorized_error"
                if (
                    isinstance(resp["DATA"]["error"], dict)
                    and resp["DATA"]["error"].get("code") == "InvalidRequest"
                ):
                    if (
                        "not allowed" not in resp["DATA"]["error"].get("detail", "")
                    ) and ("Deployment" not in resp["DATA"]["error"].get("detail", "")):
                        # For the case of "InvalidRequest", check if it is because of deployment operation. If not, we should
                        # reset the connection because the token may have expired in the middle of transaction.
                        dcnm_reset_connection(self.module)
                        rc = "unauthorized_error"
                if (
                    isinstance(resp["DATA"]["error"], dict)
                    and resp["DATA"]["error"].get("code") == "InvalidFabric"
                ):
                    dcnm_reset_connection(self.module)
                    rc = "unauthorized_error"
        return rc

    def dcnm_sp_send_message_to_dcnm(self):

        """
        Routine to push payloads to DCNM server. This routine implements reqquired error checks and retry mechanisms to handle
        transient errors. This routine checks self.diff_create, self.diff_modify, self.diff_delete and self.diff_deploy lists
        and push appropriate requests to DCNM.

        Parameters:
            None

        Returns:
            None
        """

        resp = None
        create_flag = False
        modify_flag = False
        delete_flag = False
        deploy_flag = False

        for srp in self.diff_create:
            retries = 0
            command = "POST"
            while retries < 30:
                retries += 1
                resp = self.dcnm_sp_create_sp(srp, command)
                if resp.get("RETURN_CODE") == 200:
                    create_flag = True
                    break
                # We sometimes see "UserUnauthorized" errors while transacting with DCNM server. Suggested remedy is to
                # logout and login again. We will do the logout from here and expect the login to happen again after this
                # from the connection module
                self.dcnm_sp_check_for_errors_in_resp(resp)

                # There may be a temporary issue on the server. so we should try again. In case
                # of create or modify, the policy may have been created/updated, but the error may
                # be due to the attach. So check if the policy is created and if attach flag is set.
                # If so then try attaching the policy and do not try to recreate
                get_resp = self.dcnm_sp_get_sp_info_from_dcnm(srp, "PAYLOAD")
                if get_resp != []:
                    # Since the policy is already created, use PUT to update the policy again with
                    # the same payload
                    command = "PUT"
                time.sleep(10)
                continue
            resp["RETRIES"] = retries
            self.result["response"].append(resp)
            if resp and resp.get("RETURN_CODE") != 200:
                resp["CHANGED"] = self.changed_dict[0]
                self.module.fail_json(msg=resp)

        for srp in self.diff_modify:
            retries = 0
            while retries < 30:
                retries += 1
                resp = self.dcnm_sp_create_sp(srp, "PUT")
                if resp.get("RETURN_CODE") == 200:
                    modify_flag = True
                    break
                # We sometimes see "UserUnauthorized" errors while transacting with DCNM server. Suggested remedy is to
                # logout and login again. We will do the logout from here and expect the login to happen again after this
                # from the connection module
                self.dcnm_sp_check_for_errors_in_resp(resp)
                time.sleep(10)
                continue
            resp["RETRIES"] = retries
            self.result["response"].append(resp)
            if resp and resp.get("RETURN_CODE") != 200:
                resp["CHANGED"] = self.changed_dict[0]
                self.module.fail_json(msg=resp)

        if self.diff_delete:
            delete_pol_info = {}
            for sp in self.diff_delete:
                delete_pol_info = self.dcnm_sp_combine_policies(sp, delete_pol_info)
        else:
            delete_pol_info = {}

        for path in delete_pol_info:
            detach_failed = False
            retries = 0
            while retries < 30:
                retries += 1
                detach_failed = False
                resp = self.dcnm_sp_detach_sp(
                    path, delete_pol_info[path]["policyNames"]
                )
                if (resp is not None) and (resp.get("RETURN_CODE") == 200):
                    # We may see a '200' return code,  but the transaction may have failed. Check for
                    # the status
                    if resp.get("DATA"):
                        for d in resp["DATA"]:
                            if d.get("status") == "Success":
                                delete_flag = True
                            else:
                                detach_failed = True
                        if detach_failed is False:
                            break
                # We sometimes see "UserUnauthorized" errors while transacting with DCNM server. Suggested remedy is to
                # logout and login again. We will do the logout from here and expect the login to happen again after this
                # from the connection module
                self.dcnm_sp_check_for_errors_in_resp(resp)
                time.sleep(10)
                continue
            if resp is not None:
                resp["RETRIES"] = retries
                self.result["response"].append(resp)
            if (resp and resp.get("RETURN_CODE") != 200) or detach_failed:
                resp["CHANGED"] = self.changed_dict[0]
                self.module.fail_json(msg=resp)

        # For delete case we have done a detach. do a deploy before actual delete
        for path in delete_pol_info:
            del_deploy_failed = False
            retries = 0
            while retries < 30:
                retries += 1
                del_deploy_failed = False
                resp = self.dcnm_sp_deploy_sp(path, delete_pol_info[path])

                if (resp is not None) and (resp.get("RETURN_CODE") == 200):
                    # We may see a '200' return code,  but the transaction may have failed. Check for
                    # the status
                    if resp.get("DATA"):
                        for d in resp["DATA"]:
                            if d.get("status") == "Success":
                                delete_flag = True
                            else:
                                del_deploy_failed = True
                        if del_deploy_failed is False:
                            time.sleep(10)
                            break
                # We sometimes see "UserUnauthorized" errors while transacting with DCNM server. Suggested remedy is to
                # logout and login again. We will do the logout from here and expect the login to happen again after this
                # from the connection module
                self.dcnm_sp_check_for_errors_in_resp(resp)
                time.sleep(10)
                continue
            resp["RETRIES"] = retries
            self.result["response"].append(resp)
            if (resp and resp.get("RETURN_CODE") != 200) or del_deploy_failed:
                resp["CHANGED"] = self.changed_dict[0]
                self.module.fail_json(msg=resp)

        if delete_flag:
            self.dcnm_sp_check_deployment_status(self.diff_delete, "na")

        # All policies are detached and deployed. Now go ahead and delete the same from the server
        for sp in self.diff_delete:
            retries = 0
            while retries < 30:
                retries += 1
                resp = self.dcnm_sp_delete_sp(sp)

                if (resp is not None) and (resp.get("RETURN_CODE") == 200):
                    delete_flag = True
                    break
                # We sometimes see "UserUnauthorized" errors while transacting with DCNM server. Suggested remedy is to
                # logout and login again. We will do the logout from here and expect the login to happen again after this
                # from the connection module
                self.dcnm_sp_check_for_errors_in_resp(resp)
                time.sleep(10)
                continue
            resp["RETRIES"] = retries
            self.result["response"].append(resp)
            if resp and resp.get("RETURN_CODE") != 200:
                resp["CHANGED"] = self.changed_dict[0]
                self.module.fail_json(msg=resp)

        if self.diff_deploy:
            deploy_pol_info = {}
            for sp in self.diff_deploy:
                deploy_pol_info = self.dcnm_sp_combine_policies(sp, deploy_pol_info)
        else:
            deploy_pol_info = {}

        for path in deploy_pol_info:
            deploy_failed = False
            retries = 0
            while retries < 30:
                retries += 1
                resp = self.dcnm_sp_deploy_sp(path, deploy_pol_info[path])
                if resp.get("RETURN_CODE") == 200:
                    # We may see a '200' return code,  but the transaction may have failed. Check for
                    # the status
                    if resp.get("DATA"):
                        for d in resp["DATA"]:
                            if d.get("status") == "Success":
                                deploy_flag = True
                            else:
                                deploy_failed = True
                        if deploy_failed is False:
                            time.sleep(10)
                            break
                # We sometimes see "UserUnauthorized" errors while transacting with DCNM server. Suggested remedy is to
                # logout and login again. We will do the logout from here and expect the login to happen again after this
                # from the connection module
                self.dcnm_sp_check_for_errors_in_resp(resp)
                time.sleep(10)
                continue
            resp["RETRIES"] = retries
            self.result["response"].append(resp)
            if (resp and resp.get("RETURN_CODE") != 200) or deploy_failed:
                resp["CHANGED"] = self.changed_dict[0]
                self.module.fail_json(msg=resp)

        if deploy_flag:
            # Ensure all the route peerings are properly deployed before returning.
            self.dcnm_sp_check_deployment_status(self.diff_deploy, "in-sync")

        self.result["changed"] = (
            create_flag or modify_flag or delete_flag or deploy_flag
        )


def main():

    """main entry point for module execution"""
    element_spec = dict(
        fabric=dict(required=True, type="str"),
        service_fabric=dict(required=True, type="str"),
        config=dict(required=False, type="list", elements="dict"),
        state=dict(
            type="str",
            default="merged",
            choices=["merged", "deleted", "replaced", "query", "overridden"],
        ),
        deploy=dict(required=False, type="bool", default=True),
        attach=dict(required=False, type="bool", default=True),
    )

    module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True)

    dcnm_sp = DcnmServicePolicy(module)

    dcnm_sp.result["StartTime"] = datetime.now().strftime("%H:%M:%S")

    dcnm_sp.deploy = module.params["deploy"]

    if (module.params["attach"] is None) or (
        str(module.params["attach"]).lower() == "true"
    ):
        dcnm_sp.attach = True
    else:
        dcnm_sp.attach = False

    state = module.params["state"]

    if not dcnm_sp.config:
        if state == "merged" or state == "replaced" or state == "query":
            module.fail_json(
                msg="'config' element is mandatory for state '{0}', given = '{1}'".format(
                    state, dcnm_sp.config
                )
            )

    dcnm_sp.dcnm_sp_validate_input()

    if (module.params["state"] != "query") and (module.params["state"] != "deleted"):

        dcnm_sp.dcnm_sp_get_want()
        dcnm_sp.dcnm_sp_get_have()

        # self.want would have defaulted all optional objects not included in playbook. But the way
        # these objects are handled is different between 'merged' and 'replaced' states. For 'merged'
        # state, objects not included in the playbook must be left as they are and for state 'replaced'
        # they must be purged or defaulted.
        dcnm_sp.dcnm_sp_update_want()

    if (module.params["state"] == "merged") or (module.params["state"] == "replaced"):
        dcnm_sp.dcnm_sp_get_diff_merge()

    if module.params["state"] == "deleted":
        dcnm_sp.dcnm_sp_get_diff_deleted()

    if module.params["state"] == "query":
        dcnm_sp.dcnm_sp_get_diff_query()

    if module.params["state"] == "overridden":
        dcnm_sp.dcnm_sp_get_diff_overridden()

    dcnm_sp.result["diff"] = dcnm_sp.changed_dict

    if dcnm_sp.diff_create or dcnm_sp.diff_modify or dcnm_sp.diff_delete:
        dcnm_sp.result["changed"] = True

    if module.check_mode:
        dcnm_sp.result["changed"] = False
        dcnm_sp.result["EndTime"] = datetime.now().strftime("%H:%M:%S")
        module.exit_json(**dcnm_sp.result)

    dcnm_sp.dcnm_sp_send_message_to_dcnm()

    module.exit_json(**dcnm_sp.result)


if __name__ == "__main__":
    main()
