diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 87f39e76f..f86ddb558 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,15 +21,15 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ansible: [2.15.12, 2.16.7] + ansible: [2.15.13, 2.16.14, 2.17.12, 2.18.6] steps: - name: Check out code uses: actions/checkout@v2 - - name: Set up Python "3.10" + - name: Set up Python "3.11" uses: actions/setup-python@v1 with: - python-version: "3.10" + python-version: "3.11" - name: Install ansible-base (v${{ matrix.ansible }}) run: pip install https://github.com/ansible/ansible/archive/v${{ matrix.ansible }}.tar.gz --disable-pip-version-check @@ -52,11 +52,17 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ansible: [2.15.12, 2.16.7] + ansible: [2.15.13, 2.16.14, 2.17.12, 2.18.6] python: ["3.9", "3.10", "3.11"] exclude: - - ansible: 2.16.7 + - ansible: 2.16.14 python: "3.9" + - ansible: 2.17.12 + python: "3.9" + - ansible: 2.18.6 + python: "3.9" + - ansible: 2.18.6 + python: "3.10" steps: - name: Set up Python (v${{ matrix.python }}) uses: actions/setup-python@v1 @@ -87,15 +93,21 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ansible: [2.15.12, 2.16.7] + ansible: [2.15.13, 2.16.14, 2.17.12, 2.18.6] steps: - - name: Set up Python "3.10" + - name: Set up Python "3.11" uses: actions/setup-python@v1 with: - python-version: "3.10" + python-version: "3.11" - name: Install ansible-base (v${{ matrix.ansible }}) run: pip install https://github.com/ansible/ansible/archive/v${{ matrix.ansible }}.tar.gz --disable-pip-version-check + + - name: Install Pydantic (v2) + run: pip install pydantic==2.11.4 + + - name: Install DeepDiff (v8.5.0) + run: pip install deepdiff==8.5.0 - name: Install coverage (v7.3.4) run: pip install coverage==7.3.4 diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/fabrics/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/fabrics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/fabrics/fabrics.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/fabrics/fabrics.py new file mode 100644 index 000000000..3557186a2 --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/fabrics/fabrics.py @@ -0,0 +1,106 @@ +# Copyright (c) 2024 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ..top_down import TopDown + + +class Fabrics(TopDown): + """ + ## api.v1.lan-fabric.rest.top-down.fabrics.Fabrics() + + ### Description + Common methods and properties for top-down.fabrics.Fabrics() subclasses. + + ### Path + - ``/api/v1/lan-fabric/rest/top_down/fabrics`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fabrics = f"{self.top_down}/fabrics" + msg = f"ENTERED api.v1.lan_fabric.rest.top_down.fabrics.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Set the fabric_name property. + """ + self.properties["fabric_name"] = None + self.properties["ticket_id"] = None + + @property + def fabric_name(self): + """ + - getter: Return the fabric_name. + - setter: Set the fabric_name. + - setter: Raise ``ValueError`` if fabric_name is not valid. + """ + return self.properties["fabric_name"] + + @fabric_name.setter + def fabric_name(self, value): + method_name = inspect.stack()[0][3] + try: + self.conversion.validate_fabric_name(value) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) from error + self.properties["fabric_name"] = value + + @property + def path_fabric_name(self): + """ + - Endpoint path property, including fabric_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.fabrics}/{self.fabric_name}" + + @property + def ticket_id(self): + """ + - getter: Return the ticket_id. + - setter: Set the ticket_id. + - setter: Raise ``ValueError`` if ticket_id is not a string. + - Default: None + - Note: ticket_id is optional unless Change Control is enabled. + """ + return self.properties["ticket_id"] + + @ticket_id.setter + def ticket_id(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected string for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["ticket_id"] = value diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/fabrics/vrfs/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/fabrics/vrfs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/fabrics/vrfs/vrfs.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/fabrics/vrfs/vrfs.py new file mode 100644 index 000000000..268989968 --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/fabrics/vrfs/vrfs.py @@ -0,0 +1,219 @@ +# Copyright (c) 2024 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ..fabrics import Fabrics +from .........common.enums.http_requests import RequestVerb + + +class Vrfs(Fabrics): + """ + ## api.v1.lan-fabric.rest.top-down.fabrics.Vrfs() + + ### Description + Common methods and properties for top-down.fabrics.Vrfs() subclasses. + + ### Path + - ``/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fabrics = f"{self.top_down}/fabrics" + msg = f"ENTERED api.v1.lan_fabric.rest.top_down.fabrics.vrfs.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Set the fabric_name property. + """ + self.properties["fabric_name"] = None + self.properties["ticket_id"] = None + + @property + def fabric_name(self): + """ + - getter: Return the fabric_name. + - setter: Set the fabric_name. + - setter: Raise ``ValueError`` if fabric_name is not valid. + """ + return self.properties["fabric_name"] + + @fabric_name.setter + def fabric_name(self, value): + method_name = inspect.stack()[0][3] + try: + self.conversion.validate_fabric_name(value) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) from error + self.properties["fabric_name"] = value + + @property + def path_fabric_name(self): + """ + - Endpoint path property, including fabric_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.fabrics}/{self.fabric_name}" + + @property + def ticket_id(self): + """ + - getter: Return the ticket_id. + - setter: Set the ticket_id. + - setter: Raise ``ValueError`` if ticket_id is not a string. + - Default: None + - Note: ticket_id is optional unless Change Control is enabled. + """ + return self.properties["ticket_id"] + + @ticket_id.setter + def ticket_id(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected string for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["ticket_id"] = value + + +class EpVrfGet(Fabrics): + """ + ## V1 API - Vrfs().EpVrfGet() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/rest/top-down/fabrics/{fabric_name}/vrfs`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpVrfGet() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.top_down.fabrics.vrfs." + msg += f"Vrfs.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = RequestVerb.GET + + @property + def path(self): + """ + - Endpoint for VRF GET request. + - Raise ``ValueError`` if fabric_name is not set. + """ + return f"{self.path_fabric_name}/vrfs" + + +class EpVrfPost(Fabrics): + """ + ## V1 API - Vrfs().EpVrfPost() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/rest/top-down/fabrics/{fabric_name}/vrfs`` + + ### Verb + - POST + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpVrfPost() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.top_down.fabrics.vrfs." + msg += f"Vrfs.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = RequestVerb.POST + + @property + def path(self): + """ + - Endpoint for VRF POST request. + - Raise ``ValueError`` if fabric_name is not set. + """ + return f"{self.path_fabric_name}/vrfs" diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/top_down.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/top_down.py new file mode 100644 index 000000000..7db861f1b --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/top_down/top_down.py @@ -0,0 +1,48 @@ +# Copyright (c) 2024 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ..rest import Rest + + +class TopDown(Rest): + """ + ## api.v1.lan_fabric.rest.top_down.TopDown() + + ### Description + Common methods and properties for TopDown() subclasses. + + ### Path + - ``/api/v1/lan-fabric/rest/top-down`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.top_down = f"{self.rest}/top-down" + msg = f"ENTERED api.v1.lan_fabric.rest.top_down.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + Populate properties specific to this class and its subclasses. + """ diff --git a/plugins/module_utils/common/enums/__init__.py b/plugins/module_utils/common/enums/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/enums/ansible.py b/plugins/module_utils/common/enums/ansible.py new file mode 100644 index 000000000..2a388a301 --- /dev/null +++ b/plugins/module_utils/common/enums/ansible.py @@ -0,0 +1,78 @@ +""" +Values used by Ansible +""" + +from enum import Enum + + +class AnsibleStates(Enum): + """ + # Summary + + Ansible states used by the DCNM Ansible Collection + + ## Values + + ### deleted + + Remove the resource, if it exists. NDFC uses DELETE HTTP verb for this. + + If the resource does not exist, no action is taken and the Ansible + result is updated to indicate that no changes were made (i.e. + `changed` = False). + + ### merged + + Merge the resource. NDFC uses POST HTTP verb for this. + + With merged state, a resource is created if it does not exist, + or is updated if it does exist. + + For idempotency, each resource, if it exists, is updated only if + its current properties differ from the properties specified in + the Ansible task. + + If no resources have been created or updated, the Ansible + result is updated to indicate that no changes were made (i.e. + `changed` = False). + + ### overridden + + Override the resource. NDFC uses DELETE and POST HTTP verbs for this. + + With overridden state, all resources that are not specified in the + Ansible task are removed, and the specified resources are created or + updated as specified in the Ansible task. + + For idempotency, each resource is modified only if its current + properties differ from the properties specified in the Ansible + task. + + If no resources have been removed, created or updated, the Ansible + result is updated to indicate that no changes were made (i.e. + `changed` = False). + + ### query + + Query the resource. NDFC uses GET HTTP verb for this. + + If the resource exists, its representation is returned to the caller. + If the resource does not exist, an empty list is returned. A + 200 response is returned in both cases. + + The Ansible result in this case will always have `changed` set to False. + + ### replaced + + Replace the resource if it exists and its properties differ from + the properties specified in the Ansible task. NDFC uses DELETE and + POST HTTP verbs for this. Resources not specified in the + Ansible task are not removed or modified. + + """ + + DELETED = "deleted" + MERGED = "merged" + OVERRIDDEN = "overridden" + QUERY = "query" + REPLACED = "replaced" diff --git a/plugins/module_utils/common/enums/bgp.py b/plugins/module_utils/common/enums/bgp.py new file mode 100644 index 000000000..0a7c1bd14 --- /dev/null +++ b/plugins/module_utils/common/enums/bgp.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/enums/bgp.py +""" +Enumerations for BGP parameters. +""" +from enum import Enum + + +class BgpPasswordEncrypt(Enum): + """ + Enumeration for BGP password encryption types. + + - MDS = 3 + - TYPE7 = 7 + - NONE = -1 + """ + MD5 = 3 + TYPE7 = 7 + NONE = -1 + + @classmethod + def choices(cls): + """ + Returns a list of all the encryption types. + """ + return [cls.NONE, cls.MD5, cls.TYPE7] diff --git a/plugins/module_utils/common/enums/http_requests.py b/plugins/module_utils/common/enums/http_requests.py new file mode 100644 index 000000000..e01b3dff5 --- /dev/null +++ b/plugins/module_utils/common/enums/http_requests.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/enums/http_requests.py +""" +Enumerations related to HTTP requests +""" +from enum import Enum + + +class RequestVerb(Enum): + """ + # Summary + + HTTP request verbs used in this collection. + + ## Values + + - `DELETE` + - `GET` + - `POST` + - `PUT` + + """ + + DELETE = "DELETE" + GET = "GET" + POST = "POST" + PUT = "PUT" diff --git a/plugins/module_utils/common/models/__init__.py b/plugins/module_utils/common/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/models/ipv4_cidr_host.py b/plugins/module_utils/common/models/ipv4_cidr_host.py new file mode 100644 index 000000000..17f0f03d2 --- /dev/null +++ b/plugins/module_utils/common/models/ipv4_cidr_host.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/models/ipv4_cidr_host.py +""" +Validate CIDR-format IPv4 host address. +""" +from pydantic import BaseModel, Field, field_validator + +from ..validators.ipv4_cidr_host import validate_ipv4_cidr_host + + +class IPv4CidrHostModel(BaseModel): + """ + # Summary + + Model to validate a CIDR-format IPv4 host address. + + ## Raises + + - ValueError: If the input is not a valid CIDR-format IPv4 host address. + + ## Example usage + ```python + try: + ipv4_cidr_host_address = IPv4CidrHostModel(ipv4_cidr_host="192.168.1.1/24") + except ValueError as err: + # Handle the error + ``` + + """ + + ipv4_cidr_host: str = Field( + ..., + description="CIDR-format IPv4 host address, e.g. 10.1.1.1/24", + ) + + @field_validator("ipv4_cidr_host") + @classmethod + def validate(cls, value: str) -> str: + """ + Validate that the input is a valid CIDR-format IPv4 host address + and that it is NOT a network address. + + Note: Broadcast addresses are accepted as valid. + """ + # Validate the address part + try: + result = validate_ipv4_cidr_host(value) + except ValueError as error: + msg = f"Invalid CIDR-format IPv4 host address: {value}. " + msg += f"detail: {error}" + raise ValueError(msg) from error + + if result is True: + # Valid CIDR-format IPv4 host address + return value + msg = f"Invalid CIDR-format IPv4 host address: {value}. " + msg += "Are the host bits all zero?" + raise ValueError(msg) diff --git a/plugins/module_utils/common/models/ipv4_host.py b/plugins/module_utils/common/models/ipv4_host.py new file mode 100644 index 000000000..4a25792d0 --- /dev/null +++ b/plugins/module_utils/common/models/ipv4_host.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/models/ipv4_host.py +""" +Validate IPv4 host address. +""" +from pydantic import BaseModel, Field, field_validator + +from ..validators.ipv4_host import validate_ipv4_host + + +class IPv4HostModel(BaseModel): + """ + # Summary + + Model to validate an IPv4 host address without prefix. + + ## Raises + + - ValueError: If the input is not a valid IPv4 host address. + + ## Example usage + + ```python + try: + ipv4_host_address = IPv4HostModel(ipv4_host="10.33.0.1") + except ValueError as err: + # Handle the error + ``` + + """ + + ipv4_host: str = Field( + ..., + description="IPv4 address without prefix e.g. 10.1.1.1", + ) + + @field_validator("ipv4_host") + @classmethod + def validate(cls, value: str) -> str: + """ + Validate that the input is a valid IPv4 host address + + Note: Broadcast addresses are accepted as valid. + """ + # Validate the address part + try: + result = validate_ipv4_host(value) + except ValueError as error: + msg = f"Invalid IPv4 host address: {value}. " + msg += f"detail: {error}" + raise ValueError(msg) from error + + if result is True: + # Valid IPv4 host address + return value + msg = f"Invalid IPv4 host address: {value}." + raise ValueError(msg) diff --git a/plugins/module_utils/common/models/ipv4_multicast_group_address.py b/plugins/module_utils/common/models/ipv4_multicast_group_address.py new file mode 100644 index 000000000..b4c7b2ef4 --- /dev/null +++ b/plugins/module_utils/common/models/ipv4_multicast_group_address.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/models/ipv4_host.py +""" +Validate IPv4 host address. +""" +from pydantic import BaseModel, Field, field_validator + +from ..validators.ipv4_multicast_group_address import validate_ipv4_multicast_group_address + + +class IPv4MulticastGroupModel(BaseModel): + """ + # Summary + + Model to validate an IPv4 multicast group address without prefix. + + ## Raises + + - ValueError: If the input is not a valid IPv4 multicast group address. + + ## Example usage + + ```python + try: + ipv4_multicast_group_address = IPv4MulticastGroupModel(ipv4_multicast_group="224.1.1.1") + except ValueError as err: + # Handle the error + ``` + + """ + + ipv4_multicast_group: str = Field( + ..., + description="IPv4 multicast group address without prefix e.g. 224.1.1.1", + ) + + @field_validator("ipv4_multicast_group") + @classmethod + def validate(cls, value: str) -> str: + """ + Validate that the input is a valid IPv4 multicast group address + """ + # Validate the address part + try: + result = validate_ipv4_multicast_group_address(value) + except ValueError as error: + msg = f"Invalid IPv4 multicast group address: {value}. " + msg += f"detail: {error}" + raise ValueError(msg) from error + + if result is True: + # Valid IPv4 multicast group address + return value + msg = f"Invalid IPv4 multicast group address: {value}." + raise ValueError(msg) diff --git a/plugins/module_utils/common/models/ipv6_cidr_host.py b/plugins/module_utils/common/models/ipv6_cidr_host.py new file mode 100644 index 000000000..9f58215f4 --- /dev/null +++ b/plugins/module_utils/common/models/ipv6_cidr_host.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/models/ipv6_cidr_host.py +""" +Validate CIDR-format IPv6 host address. +""" +from pydantic import BaseModel, Field, field_validator + +from ..validators.ipv6_cidr_host import validate_ipv6_cidr_host + + +class IPv6CidrHostModel(BaseModel): + """ + # Summary + + Model to validate a CIDR-format IPv6 host address. + + ## Raises + + - ValueError: If the input is not a valid CIDR-format IPv6 host address. + + ## Example usage + ```python + try: + ipv6_cidr_host_address = IPv6CidrHostModel(ipv6_cidr_host="2001:db8::1/64") + except ValueError as err: + # Handle the error + ``` + + """ + + ipv6_cidr_host: str = Field( + ..., + description="CIDR-format IPv6 host address, e.g. 2001:db8::1/64", + ) + + @field_validator("ipv6_cidr_host") + @classmethod + def validate(cls, value: str) -> str: + """ + Validate that the input is a valid IPv6 CIDR-format host address + and that it is NOT a network address. + + Note: Broadcast addresses are accepted as valid. + """ + # Validate the address part + try: + result = validate_ipv6_cidr_host(value) + except ValueError as error: + msg = f"Invalid CIDR-format IPv6 host address: {value}. " + msg += f"detail: {error}" + raise ValueError(msg) from error + + if result is True: + # Valid CIDR-format IPv6 host address + return value + msg = f"Invalid CIDR-format IPv6 host address: {value}. " + msg += "Are the host bits all zero?" + raise ValueError(msg) diff --git a/plugins/module_utils/common/models/ipv6_host.py b/plugins/module_utils/common/models/ipv6_host.py new file mode 100644 index 000000000..aae3dfc4b --- /dev/null +++ b/plugins/module_utils/common/models/ipv6_host.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/models/ipv6_host.py +""" +Validate IPv6 host address. +""" +from pydantic import BaseModel, Field, field_validator + +from ..validators.ipv6_host import validate_ipv6_host + + +class IPv6HostModel(BaseModel): + """ + # Summary + + Model to validate an IPv6 host address without prefix. + + ## Raises + + - ValueError: If the input is not a valid IPv6 host address. + + ## Example usage + + ```python + try: + ipv6_host_address = IPv6HostModel(ipv6_host="2001::1") + log.debug(f"Valid: {ipv6_host_address}") + except ValueError as err: + # Handle the error + ``` + + """ + + ipv6_host: str = Field( + ..., + description="IPv6 address without prefix e.g. 2001::1", + ) + + @field_validator("ipv6_host") + @classmethod + def validate(cls, value: str) -> str: + """ + Validate that the input is a valid IPv6 host address + + Note: Broadcast addresses are accepted as valid. + """ + # Validate the address part + try: + result = validate_ipv6_host(value) + except ValueError as error: + msg = f"Invalid IPv6 host address: {value}. " + msg += f"detail: {error}" + raise ValueError(msg) from error + + if result is True: + # Valid IPv6 host address + return value + msg = f"Invalid IPv6 host address: {value}." + raise ValueError(msg) diff --git a/plugins/module_utils/common/validators/__init__.py b/plugins/module_utils/common/validators/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/validators/ipv4_cidr_host.py b/plugins/module_utils/common/validators/ipv4_cidr_host.py new file mode 100644 index 000000000..39b5d0edd --- /dev/null +++ b/plugins/module_utils/common/validators/ipv4_cidr_host.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/validators/ipv4_cidr_host.py +""" +Validate CIDR-format IPv4 host address +""" +import ipaddress + + +def validate_ipv4_cidr_host(value: str) -> bool: + """ + # Summary + + - Return True if value is an IPv4 CIDR-format host address. + - Return False otherwise. + + Where: value is a string representation of CIDR-format IPv4 address. + + ## Raises + + None + + ## Examples + + - value: "10.10.10.1/24" -> True + - value: "10.10.10.81/28" -> True + - value: "10.10.10.80/28" -> False (is a network) + - value: 1 -> False (is not a string) + """ + try: + address, prefixlen = value.split("/") + except (AttributeError, ValueError): + return False + + if int(prefixlen) == 32: + # A /32 prefix length is always a host address for our purposes, + # but the IPv4Interface module treats it as a network_address, + # as shown below. + # + # >>> ipaddress.IPv4Interface("10.1.1.1/32").network.network_address + # IPv4Address('10.1.1.1') + # >>> + return True + + try: + network = ipaddress.IPv4Interface(value).network.network_address + except ipaddress.AddressValueError: + return False + + if address != str(network): + return True + return False diff --git a/plugins/module_utils/common/validators/ipv4_host.py b/plugins/module_utils/common/validators/ipv4_host.py new file mode 100644 index 000000000..7aaf0d93f --- /dev/null +++ b/plugins/module_utils/common/validators/ipv4_host.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/validators/ipv4_host.py +""" +Validate IPv4 host address without a prefix +""" +from ipaddress import AddressValueError, IPv4Address + + +def validate_ipv4_host(value: str) -> bool: + """ + # Summary + + - Return True if value is an IPv4 host address without a prefix. + - Return False otherwise. + + Where: value is a string representation an IPv4 address without a prefix. + + ## Raises + + None + + ## Examples + + - value: "10.10.10.1" -> True + - value: "10.10.10.81/28" -> False + - value: "10.10.10.0" -> True + - value: 1 -> False (is not a string) + """ + prefixlen: str = "" + try: + __, prefixlen = value.split("/") + except (AttributeError, ValueError): + pass + + if isinstance(value, int): + # value is an int and IPv4Address accepts int as a valid address. + # We don't want to acceps int, so reject it here. + return False + + try: + addr = IPv4Address(value) # pylint: disable=unused-variable + except AddressValueError: + return False + + return True diff --git a/plugins/module_utils/common/validators/ipv4_multicast_group_address.py b/plugins/module_utils/common/validators/ipv4_multicast_group_address.py new file mode 100644 index 000000000..43fb69cfc --- /dev/null +++ b/plugins/module_utils/common/validators/ipv4_multicast_group_address.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/validators/ipv4_host.py +""" +Validate IPv4 host address without a prefix +""" +from ipaddress import AddressValueError, IPv4Address + + +def validate_ipv4_multicast_group_address(value: str) -> bool: + """ + # Summary + + - Return True if value is an IPv4 multicast group address without prefix. + - Return False otherwise. + + Where: value is a string representation an IPv4 multicast group address without prefix. + + ## Raises + + None + + ## Examples + + - value: "224.10.10.1" -> True + - value: "224.10.10.1/24" -> False (contains prefix) + - value: "10.10.10.81/28" -> False + - value: "10.10.10.0" -> False + - value: 1 -> False (is not a string) + """ + prefixlen: str = "" # pylint: disable=unused-variable + try: + __, prefixlen = value.split("/") + except (AttributeError, ValueError): + pass + + if isinstance(value, int): + # value is an int and IPv4Address accepts int as a valid address. + # We don't want to acceps int, so reject it here. + return False + + try: + addr = IPv4Address(value) + except AddressValueError: + return False + if not addr.is_multicast: + return False + + return True diff --git a/plugins/module_utils/common/validators/ipv6_cidr_host.py b/plugins/module_utils/common/validators/ipv6_cidr_host.py new file mode 100644 index 000000000..ae2b998d3 --- /dev/null +++ b/plugins/module_utils/common/validators/ipv6_cidr_host.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/validators/ipv6_cidr_host.py +""" +Validate CIDR-format IPv6 host address +""" +import ipaddress + + +def validate_ipv6_cidr_host(value: str) -> bool: + """ + # Summary + + - Return True if value is an IPv6 CIDR-format host address. + - Return False otherwise. + + Where: value is a string representation of CIDR-format IPv6 address. + + ## Raises + + None + + ## Examples + + - value: "2001::1/128" -> True + - value: "2001:20:20:20::1/64" -> True + - value: "2001:20:20:20::/64" -> False (is a network) + - value: 1 -> False (is not a string) + """ + try: + address, prefixlen = value.split("/") + except (AttributeError, ValueError): + return False + + if int(prefixlen) == 128: + # A /128 prefix length is always a host address for our purposes, + # but the IPv4Interface module treats it as a network_address, + # as shown below. + # + # >>> ipaddress.IPv6Interface("2001:20:20:20::1/128").network.network_address + # IPv6Address('2001:20:20:20::1') + # >>> + return True + + try: + network = ipaddress.IPv6Interface(value).network.network_address + except ipaddress.AddressValueError: + return False + + if address != str(network): + return True + return False diff --git a/plugins/module_utils/common/validators/ipv6_host.py b/plugins/module_utils/common/validators/ipv6_host.py new file mode 100644 index 000000000..19c1141d3 --- /dev/null +++ b/plugins/module_utils/common/validators/ipv6_host.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/validators/ipv6_host.py +""" +Validate IPv6 host address without a prefix +""" +from ipaddress import AddressValueError, IPv6Address + + +def validate_ipv6_host(value: str) -> bool: + """ + # Summary + + - Return True if value is an IPv6 host address without a prefix. + - Return False otherwise. + + Where: value is a string representation an IPv6 address without a prefix. + + ## Raises + + None + + ## Examples + + - value: "2001::1" -> True + - value: "2001:20:20:20::1" -> True + - value: "2001:20:20:20::/64" -> False (has a prefix) + - value: "10.10.10.0" -> False (is not an IPv6 address) + - value: 1 -> False (is not an IPv6 address) + """ + prefixlen: str = "" + try: + __, prefixlen = value.split("/") + except (AttributeError, ValueError): + if prefixlen != "": + # prefixlen is not empty + return False + + if isinstance(value, int): + # value is an int and IPv6Address accepts int as a valid address. + # We don't want to acceps int, so reject it here. + return False + + try: + addr = IPv6Address(value) # pylint: disable=unused-variable + except AddressValueError: + return False + + return True diff --git a/plugins/module_utils/vrf/__init__.py b/plugins/module_utils/vrf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/vrf/dcnm_vrf_v11.py b/plugins/module_utils/vrf/dcnm_vrf_v11.py new file mode 100644 index 000000000..b98bdf932 --- /dev/null +++ b/plugins/module_utils/vrf/dcnm_vrf_v11.py @@ -0,0 +1,3538 @@ +# -*- coding: utf-8 -*- +# mypy: disable-error-code="import-untyped" +# +# Copyright (c) 2020-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. +# pylint: disable=wrong-import-position +from __future__ import absolute_import, division, print_function + +# pylint: disable=invalid-name +__metaclass__ = type +__author__ = "Shrishail Kariyappanavar, Karthik Babu Harichandra Babu, Praveen Ramoorthy, Allen Robel" +# pylint: enable=invalid-name +import ast +import copy +import inspect +import json +import logging +import re +import time +import traceback +from dataclasses import asdict, dataclass +from typing import Any, Final, Optional, Union + +from ansible.module_utils.basic import AnsibleModule + +HAS_FIRST_PARTY_IMPORTS: set[bool] = set() +HAS_THIRD_PARTY_IMPORTS: set[bool] = set() + +FIRST_PARTY_IMPORT_ERROR: Optional[ImportError] +FIRST_PARTY_FAILED_IMPORT: set[str] = set() +THIRD_PARTY_IMPORT_ERROR: Optional[str] +THIRD_PARTY_FAILED_IMPORT: set[str] = set() + +try: + import pydantic + + HAS_THIRD_PARTY_IMPORTS.add(True) + THIRD_PARTY_IMPORT_ERROR = None +except ImportError: + HAS_THIRD_PARTY_IMPORTS.add(False) + THIRD_PARTY_FAILED_IMPORT.add("pydantic") + THIRD_PARTY_IMPORT_ERROR = traceback.format_exc() + +from ...module_utils.common.enums.http_requests import RequestVerb +from ...module_utils.network.dcnm.dcnm import ( + dcnm_get_ip_addr_info, + dcnm_get_url, + dcnm_send, + get_fabric_details, + get_fabric_inventory_details, + get_ip_sn_dict, + get_sn_fabric_dict, +) + +try: + from ...module_utils.vrf.vrf_controller_to_playbook_v11 import VrfControllerToPlaybookV11Model + + HAS_FIRST_PARTY_IMPORTS.add(True) +except ImportError as import_error: + FIRST_PARTY_IMPORT_ERROR = import_error + HAS_FIRST_PARTY_IMPORTS.add(False) + FIRST_PARTY_FAILED_IMPORT.add("VrfControllerToPlaybookV11Model") + +try: + from .model_playbook_vrf_v11 import VrfPlaybookModelV11 + + HAS_FIRST_PARTY_IMPORTS.add(True) +except ImportError as import_error: + FIRST_PARTY_IMPORT_ERROR = import_error + HAS_FIRST_PARTY_IMPORTS.add(False) + FIRST_PARTY_FAILED_IMPORT.add("VrfPlaybookModelV11") + +dcnm_vrf_paths: dict = { + "GET_VRF": "/rest/top-down/fabrics/{}/vrfs", + "GET_VRF_ATTACH": "/rest/top-down/fabrics/{}/vrfs/attachments?vrf-names={}", + "GET_VRF_SWITCH": "/rest/top-down/fabrics/{}/vrfs/switches?vrf-names={}&serial-numbers={}", + "GET_VRF_ID": "/rest/managed-pool/fabrics/{}/partitions/ids", + "GET_VLAN": "/rest/resource-manager/vlan/{}?vlanUsageType=TOP_DOWN_VRF_VLAN", +} + + +@dataclass +class SendToControllerArgs: + """ + # Summary + + Arguments for DcnmVrf.send_to_controller() + + ## params + + - `action`: The action to perform (create, update, delete, etc.) + - `verb`: The HTTP verb to use (GET, POST, PUT, DELETE) + - `path`: The endpoint path for the request + - `payload`: The payload to send with the request (None for no payload) + - `log_response`: If True, log the response in the result, else + do not include the response in the result + - `is_rollback`: If True, attempt to rollback on failure + + """ + + action: str + verb: RequestVerb + path: str + payload: Optional[Union[dict, list]] + log_response: bool = True + is_rollback: bool = False + + dict = asdict + + +class DcnmVrf11: + """ + # Summary + + dcnm_vrf module implementation for DCNM Version 11 + """ + + def __init__(self, module: AnsibleModule): + self.class_name: str = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + self.module: AnsibleModule = module + self.params: dict[str, Any] = module.params + + try: + self.state: str = self.params["state"] + except KeyError: + msg = f"{self.class_name}.__init__(): " + msg += "'state' parameter is missing from params." + module.fail_json(msg=msg) + + try: + self.fabric: str = module.params["fabric"] + except KeyError: + msg = f"{self.class_name}.__init__(): " + msg += "fabric missing from params." + module.fail_json(msg=msg) + + msg = f"self.state: {self.state}, " + msg += "self.params: " + msg += f"{json.dumps(self.params, indent=4, sort_keys=True)}" + self.log.debug(msg) + + self.config: Optional[list[dict]] = copy.deepcopy(module.params.get("config")) + + msg = f"self.state: {self.state}, " + msg += "self.config: " + msg += f"{json.dumps(self.config, indent=4, sort_keys=True)}" + self.log.debug(msg) + + # Setting self.conf_changed to class scope since, after refactoring, + # it is initialized and updated in one refactored method + # (diff_merge_create) and accessed in another refactored method + # (diff_merge_attach) which reset it to {} at the top of the method + # (which undid the update in diff_merge_create). + # TODO: Revisit this in Phase 2 refactoring. + self.conf_changed: dict = {} + self.check_mode: bool = False + self.have_create: list[dict] = [] + self.want_create: list[dict] = [] + self.diff_create: list = [] + self.diff_create_update: list = [] + # self.diff_create_quick holds all the create payloads which are + # missing a vrfId. These payloads are sent to DCNM out of band + # (in the get_diff_merge()). We lose diffs for these without this + # variable. The content stored here will be helpful for cases like + # "check_mode" and to print diffs[] in the output of each task. + self.diff_create_quick: list = [] + self.have_attach: list = [] + self.want_attach: list = [] + self.diff_attach: list = [] + self.validated: list = [] + # diff_detach contains all attachments of a vrf being deleted, + # especially for state: OVERRIDDEN + # The diff_detach and delete operations have to happen before + # create+attach+deploy for vrfs being created. This is to address + # cases where VLAN from a vrf which is being deleted is used for + # another vrf. Without this additional logic, the create+attach+deploy + # go out first and complain the VLAN is already in use. + self.diff_detach: list = [] + self.have_deploy: dict = {} + self.want_deploy: dict = {} + self.diff_deploy: dict = {} + self.diff_undeploy: dict = {} + self.diff_delete: dict = {} + self.diff_input_format: list = [] + self.query: list = [] + + self.inventory_data: dict = get_fabric_inventory_details(self.module, self.fabric) + + msg = "self.inventory_data: " + msg += f"{json.dumps(self.inventory_data, indent=4, sort_keys=True)}" + self.log.debug(msg) + + self.ip_sn: dict = {} + self.hn_sn: dict = {} + self.ip_sn, self.hn_sn = get_ip_sn_dict(self.inventory_data) + self.sn_ip: dict = {value: key for (key, value) in self.ip_sn.items()} + self.fabric_data: dict = get_fabric_details(self.module, self.fabric) + + msg = "self.fabric_data: " + msg += f"{json.dumps(self.fabric_data, indent=4, sort_keys=True)}" + self.log.debug(msg) + + try: + self.fabric_type: str = self.fabric_data["fabricType"] + except KeyError: + msg = f"{self.class_name}.__init__(): " + msg += "'fabricType' parameter is missing from self.fabric_data." + self.module.fail_json(msg=msg) + + try: + self.sn_fab: dict = get_sn_fabric_dict(self.inventory_data) + except ValueError as error: + msg += f"{self.class_name}.__init__(): {error}" + module.fail_json(msg=msg) + + self.paths: dict = dcnm_vrf_paths + + self.result: dict[str, Any] = {"changed": False, "diff": [], "response": []} + + self.failed_to_rollback: bool = False + self.wait_time_for_delete_loop: Final[int] = 5 # in seconds + + self.vrf_lite_properties: Final[list[str]] = [ + "DOT1Q_ID", + "IF_NAME", + "IP_MASK", + "IPV6_MASK", + "IPV6_NEIGHBOR", + "NEIGHBOR_IP", + "PEER_VRF_NAME", + ] + + # Controller responses + self.response: dict = {} + self.log.debug("DONE") + + @staticmethod + def get_list_of_lists(lst: list, size: int) -> list[list]: + """ + # Summary + + Given a list of items (lst) and a chunk size (size), return a + list of lists, where each list is size items in length. + + ## Raises + + - ValueError if: + - lst is not a list. + - size is not an integer + + ## Example + + print(get_lists_of_lists([1,2,3,4,5,6,7], 3) + + # -> [[1, 2, 3], [4, 5, 6], [7]] + """ + if not isinstance(lst, list): + msg = "lst must be a list(). " + msg += f"Got {type(lst)}." + raise ValueError(msg) + if not isinstance(size, int): + msg = "size must be an integer. " + msg += f"Got {type(size)}." + raise ValueError(msg) + return [lst[x : x + size] for x in range(0, len(lst), size)] + + @staticmethod + def find_dict_in_list_by_key_value(search: Optional[list[dict[Any, Any]]], key: str, value: str) -> dict[Any, Any]: + """ + # Summary + + Find a dictionary in a list of dictionaries. + + + ## Raises + + None + + ## Parameters + + - search: A list of dict, or None + - key: The key to lookup in each dict + - value: The desired matching value for key + + ## Returns + + Either the first matching dict or an empty dict + + ## Usage + + ```python + content = [{"foo": "bar"}, {"foo": "baz"}] + + match = find_dict_in_list_by_key_value(search=content, key="foo", value="baz") + print(f"{match}") + # -> {"foo": "baz"} + + match = find_dict_in_list_by_key_value(search=content, key="foo", value="bingo") + print(f"{match}") + # -> {} + + match = find_dict_in_list_by_key_value(search=None, key="foo", value="bingo") + print(f"{match}") + # -> {} + ``` + """ + if search is None: + return {} + for item in search: + match = item.get(key) + if match == value: + return item + return {} + + # pylint: disable=inconsistent-return-statements + def to_bool(self, key: Any, dict_with_key: dict[Any, Any]) -> bool: + """ + # Summary + + Given a dictionary and key, access dictionary[key] and + try to convert the value therein to a boolean. + + - If the value is a boolean, return a like boolean. + - If the value is a boolean-like string (e.g. "false" + "True", etc), return the value converted to boolean. + + ## Raises + + - Call fail_json() if the value is not convertable to boolean. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + value = dict_with_key.get(key) + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += f"key: {key}, " + msg += f"value: {value}" + self.log.debug(msg) + + result: bool = False + if value in ["false", "False", False]: + result = False + elif value in ["true", "True", True]: + result = True + else: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}: " + msg += f"key: {key}, " + msg += f"value ({str(value)}), " + msg += f"with type {type(value)} " + msg += "is not convertable to boolean" + self.module.fail_json(msg=msg) + return result + + # pylint: enable=inconsistent-return-statements + @staticmethod + def compare_properties(dict1: dict[Any, Any], dict2: dict[Any, Any], property_list: list) -> bool: + """ + Given two dictionaries and a list of keys: + + - Return True if all property values match. + - Return False otherwise + """ + for prop in property_list: + if dict1.get(prop) != dict2.get(prop): + return False + return True + + def diff_for_attach_deploy(self, want_a: list[dict], have_a: list[dict], replace=False) -> tuple[list, bool]: + """ + # Summary + + Return attach_list, deploy_vrf + + Where: + + - attach list is a list of attachment differences + - deploy_vrf is a boolean + + ## Raises + + None + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += f"replace == {replace}" + self.log.debug(msg) + + attach_list: list = [] + deploy_vrf: bool = False + + if not want_a: + return attach_list, deploy_vrf + + for want in want_a: + found: bool = False + interface_match: bool = False + if not have_a: + continue + for have in have_a: + if want.get("serialNumber") != have.get("serialNumber"): + continue + # handle instanceValues first + want.update({"freeformConfig": have.get("freeformConfig", "")}) # copy freeformConfig from have as module is not managing it + want_inst_values: dict = {} + have_inst_values: dict = {} + if want.get("instanceValues") is not None and have.get("instanceValues") is not None: + want_inst_values = ast.literal_eval(want["instanceValues"]) + have_inst_values = ast.literal_eval(have["instanceValues"]) + + # update unsupported parameters using have + # Only need ipv4 or ipv6. Don't require both, but both can be supplied (as per the GUI) + if "loopbackId" in have_inst_values: + want_inst_values.update({"loopbackId": have_inst_values["loopbackId"]}) + if "loopbackIpAddress" in have_inst_values: + want_inst_values.update({"loopbackIpAddress": have_inst_values["loopbackIpAddress"]}) + if "loopbackIpV6Address" in have_inst_values: + want_inst_values.update({"loopbackIpV6Address": have_inst_values["loopbackIpV6Address"]}) + + want.update({"instanceValues": json.dumps(want_inst_values)}) + if want.get("extensionValues", "") != "" and have.get("extensionValues", "") != "": + + want_ext_values = want["extensionValues"] + have_ext_values = have["extensionValues"] + + want_ext_values_dict: dict = ast.literal_eval(want_ext_values) + have_ext_values_dict: dict = ast.literal_eval(have_ext_values) + + want_e: dict = ast.literal_eval(want_ext_values_dict["VRF_LITE_CONN"]) + have_e: dict = ast.literal_eval(have_ext_values_dict["VRF_LITE_CONN"]) + + if replace and (len(want_e["VRF_LITE_CONN"]) != len(have_e["VRF_LITE_CONN"])): + # In case of replace/override if the length of want and have lite attach of a switch + # is not same then we have to push the want to NDFC. No further check is required for + # this switch + break + + wlite: dict + hlite: dict + for wlite in want_e["VRF_LITE_CONN"]: + for hlite in have_e["VRF_LITE_CONN"]: + found = False + interface_match = False + if wlite["IF_NAME"] != hlite["IF_NAME"]: + continue + found = True + interface_match = True + if not self.compare_properties(wlite, hlite, self.vrf_lite_properties): + found = False + break + + if found: + break + + if interface_match and not found: + break + + if interface_match and not found: + break + + elif want["extensionValues"] != "" and have["extensionValues"] == "": + found = False + elif want["extensionValues"] == "" and have["extensionValues"] != "": + if replace: + found = False + else: + found = True + else: + found = True + + want_is_deploy = self.to_bool("is_deploy", want) + have_is_deploy = self.to_bool("is_deploy", have) + + msg = "want_is_deploy: " + msg += f"type {type(want_is_deploy)}, " + msg += f"value {want_is_deploy}" + self.log.debug(msg) + + msg = "have_is_deploy: " + msg += f"type {type(have_is_deploy)}, " + msg += f"value {have_is_deploy}" + self.log.debug(msg) + + want_is_attached = self.to_bool("isAttached", want) + have_is_attached = self.to_bool("isAttached", have) + + msg = "want_is_attached: " + msg += f"type {type(want_is_attached)}, " + msg += f"value {want_is_attached}" + self.log.debug(msg) + + msg = "have_is_attached: " + msg += f"type {type(have_is_attached)}, " + msg += f"value {have_is_attached}" + self.log.debug(msg) + + if have_is_attached != want_is_attached: + + if "isAttached" in want: + del want["isAttached"] + + want["deployment"] = True + attach_list.append(want) + if want_is_deploy is True: + if "isAttached" in want: + del want["isAttached"] + deploy_vrf = True + continue + + want_deployment = self.to_bool("deployment", want) + have_deployment = self.to_bool("deployment", have) + + msg = "want_deployment: " + msg += f"type {type(want_deployment)}, " + msg += f"value {want_deployment}" + self.log.debug(msg) + + msg = "have_deployment: " + msg += f"type {type(have_deployment)}, " + msg += f"value {have_deployment}" + self.log.debug(msg) + + if (want_deployment != have_deployment) or (want_is_deploy != have_is_deploy): + if want_is_deploy is True: + deploy_vrf = True + + try: + if self.dict_values_differ(dict1=want_inst_values, dict2=have_inst_values): + found = False + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}: {error}" + self.module.fail_json(msg=msg) + + if found: + break + + if not found: + msg = "isAttached: " + msg += f"{str(want.get('isAttached'))}, " + msg += "is_deploy: " + msg += f"{str(want.get('is_deploy'))}" + self.log.debug(msg) + + if self.to_bool("isAttached", want): + del want["isAttached"] + want["deployment"] = True + attach_list.append(want) + if self.to_bool("is_deploy", want): + deploy_vrf = True + + msg = "Returning " + msg += f"deploy_vrf: {deploy_vrf}, " + msg += "attach_list: " + msg += f"{json.dumps(attach_list, indent=4, sort_keys=True)}" + self.log.debug(msg) + return attach_list, deploy_vrf + + def update_attach_params_extension_values(self, attach: dict) -> dict: + """ + # Summary + + Given an attachment object (see example below): + + - Return a populated extension_values dictionary + if the attachment object's vrf_lite parameter is + not null. + - Return an empty dictionary if the attachment object's + vrf_lite parameter is null. + + ## Raises + + Calls fail_json() if the vrf_lite parameter is not null + and the role of the switch in the attachment object is not + one of the various border roles. + + ## Example attach object + + - extensionValues content removed for brevity + - instanceValues content removed for brevity + + ```json + { + "deployment": true, + "export_evpn_rt": "", + "extensionValues": "{}", + "fabric": "f1", + "freeformConfig": "", + "import_evpn_rt": "", + "instanceValues": "{}", + "isAttached": true, + "is_deploy": true, + "serialNumber": "FOX2109PGCS", + "vlan": 500, + "vrfName": "ansible-vrf-int1", + "vrf_lite": [ + { + "dot1q": 2, + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.2/30", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv4": "10.33.0.1", + "neighbor_ipv6": "2010::10:34:0:3", + "peer_vrf": "ansible-vrf-int1" + } + ] + } + ``` + + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + if not attach["vrf_lite"]: + msg = "Early return. No vrf_lite extensions to process." + self.log.debug(msg) + return {} + + extension_values: dict = {} + extension_values["VRF_LITE_CONN"] = [] + ms_con: dict = {} + ms_con["MULTISITE_CONN"] = [] + extension_values["MULTISITE_CONN"] = json.dumps(ms_con) + + # Before applying the vrf_lite config, verify that the + # switch role begins with border + + role: str = self.inventory_data[attach["ip_address"]].get("switchRole") + + if not re.search(r"\bborder\b", role.lower()): + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "VRF LITE attachments are appropriate only for switches " + msg += "with Border roles e.g. Border Gateway, Border Spine, etc. " + msg += "The playbook and/or controller settings for switch " + msg += f"{attach['ip_address']} with role {role} need review." + self.module.fail_json(msg=msg) + + item: dict + for item in attach["vrf_lite"]: + + # If the playbook contains vrf lite parameters + # update the extension values. + vrf_lite_conn: dict = {} + for param in self.vrf_lite_properties: + vrf_lite_conn[param] = "" + + if item["interface"]: + vrf_lite_conn["IF_NAME"] = item["interface"] + if item["dot1q"]: + vrf_lite_conn["DOT1Q_ID"] = str(item["dot1q"]) + if item["ipv4_addr"]: + vrf_lite_conn["IP_MASK"] = item["ipv4_addr"] + if item["neighbor_ipv4"]: + vrf_lite_conn["NEIGHBOR_IP"] = item["neighbor_ipv4"] + if item["ipv6_addr"]: + vrf_lite_conn["IPV6_MASK"] = item["ipv6_addr"] + if item["neighbor_ipv6"]: + vrf_lite_conn["IPV6_NEIGHBOR"] = item["neighbor_ipv6"] + if item["peer_vrf"]: + vrf_lite_conn["PEER_VRF_NAME"] = item["peer_vrf"] + + vrf_lite_conn["VRF_LITE_JYTHON_TEMPLATE"] = "Ext_VRF_Lite_Jython" + + msg = "vrf_lite_conn: " + msg += f"{json.dumps(vrf_lite_conn, indent=4, sort_keys=True)}" + self.log.debug(msg) + + vrf_lite_connections: dict = {} + vrf_lite_connections["VRF_LITE_CONN"] = [] + vrf_lite_connections["VRF_LITE_CONN"].append(copy.deepcopy(vrf_lite_conn)) + + if extension_values["VRF_LITE_CONN"]: + extension_values["VRF_LITE_CONN"]["VRF_LITE_CONN"].extend(vrf_lite_connections["VRF_LITE_CONN"]) + else: + extension_values["VRF_LITE_CONN"] = copy.deepcopy(vrf_lite_connections) + + extension_values["VRF_LITE_CONN"] = json.dumps(extension_values["VRF_LITE_CONN"]) + + msg = "Returning extension_values: " + msg += f"{json.dumps(extension_values, indent=4, sort_keys=True)}" + self.log.debug(msg) + + return copy.deepcopy(extension_values) + + def update_attach_params(self, attach: dict, vrf_name: str, deploy: bool, vlan_id: int) -> dict: + """ + # Summary + + Turn an attachment object (attach) into a payload for the controller. + + ## Raises + + Calls fail_json() if: + + - The switch in the attachment object is a spine + - If the vrf_lite object is not null, and the switch is not + a border switch + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}." + self.log.debug(msg) + + if not attach: + msg = "Early return. No attachments to process." + self.log.debug(msg) + return {} + + # dcnm_get_ip_addr_info converts serial_numbers, + # hostnames, etc, to ip addresses. + attach["ip_address"] = dcnm_get_ip_addr_info(self.module, attach["ip_address"], None, None) + + serial = self.ip_to_serial_number(attach["ip_address"]) + + msg = "ip_address: " + msg += f"{attach['ip_address']}, " + msg += "serial: " + msg += f"{serial}, " + msg += "attach: " + msg += f"{json.dumps(attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not serial: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"Fabric {self.fabric} does not contain switch " + msg += f"{attach['ip_address']}" + self.module.fail_json(msg=msg) + + role = self.inventory_data[attach["ip_address"]].get("switchRole") + + if role.lower() in ("spine", "super spine"): + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "VRF attachments are not appropriate for " + msg += "switches with Spine or Super Spine roles. " + msg += "The playbook and/or controller settings for switch " + msg += f"{attach['ip_address']} with role {role} need review." + self.module.fail_json(msg=msg) + + extension_values = self.update_attach_params_extension_values(attach) + if extension_values: + attach.update({"extensionValues": json.dumps(extension_values).replace(" ", "")}) + else: + attach.update({"extensionValues": ""}) + + attach.update({"fabric": self.fabric}) + attach.update({"vrfName": vrf_name}) + attach.update({"vlan": vlan_id}) + # This flag is not to be confused for deploy of attachment. + # "deployment" should be set to True for attaching an attachment + # and set to False for detaching an attachment + attach.update({"deployment": True}) + attach.update({"isAttached": True}) + attach.update({"serialNumber": serial}) + attach.update({"is_deploy": deploy}) + + # freeformConfig, loopbackId, loopbackIpAddress, and + # loopbackIpV6Address will be copied from have + attach.update({"freeformConfig": ""}) + inst_values = { + "loopbackId": "", + "loopbackIpAddress": "", + "loopbackIpV6Address": "", + } + attach.update({"instanceValues": json.dumps(inst_values).replace(" ", "")}) + + if "deploy" in attach: + del attach["deploy"] + if "ip_address" in attach: + del attach["ip_address"] + + msg = "Returning attach: " + msg += f"{json.dumps(attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + return copy.deepcopy(attach) + + def dict_values_differ(self, dict1: dict, dict2: dict, skip_keys=None) -> bool: + """ + # Summary + + Given two dictionaries and, optionally, a list of keys to skip: + + - Return True if the values for any (non-skipped) keys differs. + - Return False otherwise + + ## Raises + + - ValueError if dict1 or dict2 is not a dictionary + - ValueError if skip_keys is not a list + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + if skip_keys is None: + skip_keys = [] + + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + if not isinstance(skip_keys, list): + msg += "skip_keys must be a list. " + msg += f"Got {type(skip_keys)}." + raise ValueError(msg) + if not isinstance(dict1, dict): + msg += "dict1 must be a dict. " + msg += f"Got {type(dict1)}." + raise ValueError(msg) + if not isinstance(dict2, dict): + msg += "dict2 must be a dict. " + msg += f"Got {type(dict2)}." + raise ValueError(msg) + + for key in dict1.keys(): + if key in skip_keys: + continue + dict1_value = str(dict1.get(key)).lower() + dict2_value = str(dict2.get(key)).lower() + # Treat None and "" as equal + if dict1_value in (None, "none", ""): + dict1_value = "none" + if dict2_value in (None, "none", ""): + dict2_value = "none" + if dict1_value != dict2_value: + msg = f"Values differ: key {key} " + msg += f"dict1_value {dict1_value}, type {type(dict1_value)} != " + msg += f"dict2_value {dict2_value}, type {type(dict2_value)}. " + msg += "returning True" + self.log.debug(msg) + return True + msg = "All dict values are equal. Returning False." + self.log.debug(msg) + return False + + def diff_for_create(self, want, have) -> tuple[dict, bool]: + """ + # Summary + + Given a want and have object, return a tuple of + (create, configuration_changed) where: + - create is a dictionary of parameters to send to the + controller + - configuration_changed is a boolean indicating if + the configuration has changed + - If the configuration has not changed, return an empty + dictionary for create and False for configuration_changed + - If the configuration has changed, return a dictionary + of parameters to send to the controller and True for + configuration_changed + - If the configuration has changed, but the vrfId is + None, return an empty dictionary for create and True + for configuration_changed + + ## Raises + + - Calls fail_json if the vrfId is not None and the vrfId + in the want object is not equal to the vrfId in the + have object. + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + configuration_changed = False + if not have: + return {}, configuration_changed + + create = {} + + json_to_dict_want = json.loads(want["vrfTemplateConfig"]) + json_to_dict_have = json.loads(have["vrfTemplateConfig"]) + + # vlan_id_want drives the conditional below, so we cannot + # remove it here (as we did with the other params that are + # compared in the call to self.dict_values_differ()) + vlan_id_want = str(json_to_dict_want.get("vrfVlanId", "")) + + skip_keys = [] + if vlan_id_want == "0": + skip_keys = ["vrfVlanId"] + try: + templates_differ = self.dict_values_differ(dict1=json_to_dict_want, dict2=json_to_dict_have, skip_keys=skip_keys) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"templates_differ: {error}" + self.module.fail_json(msg=msg) + + msg = f"templates_differ: {templates_differ}, " + msg += f"vlan_id_want: {vlan_id_want}" + self.log.debug(msg) + + if want.get("vrfId") is not None and have.get("vrfId") != want.get("vrfId"): + msg = f"{self.class_name}.{method_name}: " + msg += f"vrf_id for vrf {want['vrfName']} cannot be updated to " + msg += "a different value" + self.module.fail_json(msg=msg) + + elif templates_differ: + configuration_changed = True + if want.get("vrfId") is None: + # The vrf updates with missing vrfId will have to use existing + # vrfId from the instance of the same vrf on DCNM. + want["vrfId"] = have["vrfId"] + create = want + + else: + pass + + msg = f"returning configuration_changed: {configuration_changed}, " + msg += f"create: {create}" + self.log.debug(msg) + + return create, configuration_changed + + def update_create_params(self, vrf: dict, vlan_id: str = "") -> dict: + """ + # Summary + + Given a vrf dictionary from a playbook, return a VRF payload suitable + for sending to the controller. + + Translate playbook keys into keys expected by the controller. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + if not vrf: + return vrf + + v_template = vrf.get("vrf_template", "Default_VRF_Universal") + ve_template = vrf.get("vrf_extension_template", "Default_VRF_Extension_Universal") + src = None + s_v_template = vrf.get("service_vrf_template", None) + + vrf_upd = { + "fabric": self.fabric, + "vrfName": vrf["vrf_name"], + "vrfTemplate": v_template, + "vrfExtensionTemplate": ve_template, + "vrfId": vrf.get("vrf_id", None), # vrf_id will be auto generated in get_diff_merge() + "serviceVrfTemplate": s_v_template, + "source": src, + } + template_conf = { + "vrfSegmentId": vrf.get("vrf_id", None), + "vrfName": vrf["vrf_name"], + "vrfVlanId": vlan_id, + "vrfVlanName": vrf.get("vrf_vlan_name", ""), + "vrfIntfDescription": vrf.get("vrf_intf_desc", ""), + "vrfDescription": vrf.get("vrf_description", ""), + "mtu": vrf.get("vrf_int_mtu", ""), + "tag": vrf.get("loopback_route_tag", ""), + "vrfRouteMap": vrf.get("redist_direct_rmap", ""), + "maxBgpPaths": vrf.get("max_bgp_paths", ""), + "maxIbgpPaths": vrf.get("max_ibgp_paths", ""), + "ipv6LinkLocalFlag": vrf.get("ipv6_linklocal_enable", True), + "trmEnabled": vrf.get("trm_enable", False), + "isRPExternal": vrf.get("rp_external", False), + "rpAddress": vrf.get("rp_address", ""), + "loopbackNumber": vrf.get("rp_loopback_id", ""), + "L3VniMcastGroup": vrf.get("underlay_mcast_ip", ""), + "multicastGroup": vrf.get("overlay_mcast_group", ""), + "trmBGWMSiteEnabled": vrf.get("trm_bgw_msite", False), + "advertiseHostRouteFlag": vrf.get("adv_host_routes", False), + "advertiseDefaultRouteFlag": vrf.get("adv_default_routes", True), + "configureStaticDefaultRouteFlag": vrf.get("static_default_route", True), + "bgpPassword": vrf.get("bgp_password", ""), + "bgpPasswordKeyType": vrf.get("bgp_passwd_encrypt", ""), + } + + vrf_upd.update({"vrfTemplateConfig": json.dumps(template_conf)}) + + return vrf_upd + + def get_vrf_objects(self) -> dict: + """ + # Summary + + Retrieve all VRF objects from the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + path = self.paths["GET_VRF"].format(self.fabric) + + vrf_objects = dcnm_send(self.module, "GET", path) + + missing_fabric, not_ok = self.handle_response(vrf_objects, "query_dcnm") + + if missing_fabric or not_ok: + msg0 = f"caller: {caller}. " + msg1 = f"{msg0} Fabric {self.fabric} not present on the controller" + msg2 = f"{msg0} Unable to find vrfs under fabric: {self.fabric}" + self.module.fail_json(msg=msg1 if missing_fabric else msg2) + + return copy.deepcopy(vrf_objects) + + def get_vrf_lite_objects(self, attach) -> dict: + """ + # Summary + + Retrieve the IP/Interface that is connected to the switch with serial_number + + attach must contain at least the following keys: + + - fabric: The fabric to search + - serialNumber: The serial_number of the switch + - vrfName: The vrf to search + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}" + self.log.debug(msg) + + msg = f"attach: {json.dumps(attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + verb = "GET" + path = self.paths["GET_VRF_SWITCH"].format(attach["fabric"], attach["vrfName"], attach["serialNumber"]) + msg = f"verb: {verb}, path: {path}" + self.log.debug(msg) + lite_objects = dcnm_send(self.module, verb, path) + + msg = f"Returning lite_objects: {json.dumps(lite_objects, indent=4, sort_keys=True)}" + self.log.debug(msg) + + return copy.deepcopy(lite_objects) + + def get_have(self) -> None: + """ + # Summary + + Retrieve all VRF objects and attachment objects from the + controller. Update the following with this information: + + - self.have_create + - self.have_attach + - self.have_deploy + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + have_create: list[dict] = [] + have_deploy: dict = {} + + vrf_objects = self.get_vrf_objects() + + if not vrf_objects.get("DATA"): + return + + vrf: dict = {} + curr_vrfs: set = set() + for vrf in vrf_objects["DATA"]: + if vrf.get("vrfName"): + curr_vrfs.add(vrf["vrfName"]) + + get_vrf_attach_response: dict = dcnm_get_url( + module=self.module, + fabric=self.fabric, + path=self.paths["GET_VRF_ATTACH"], + items=",".join(curr_vrfs), + module_name="vrfs", + ) + + if not get_vrf_attach_response.get("DATA"): + return + + for vrf in vrf_objects["DATA"]: + json_to_dict: dict = json.loads(vrf["vrfTemplateConfig"]) + t_conf: dict = { + "vrfSegmentId": vrf["vrfId"], + "vrfName": vrf["vrfName"], + "vrfVlanId": json_to_dict.get("vrfVlanId", 0), + "vrfVlanName": json_to_dict.get("vrfVlanName", ""), + "vrfIntfDescription": json_to_dict.get("vrfIntfDescription", ""), + "vrfDescription": json_to_dict.get("vrfDescription", ""), + "mtu": json_to_dict.get("mtu", 9216), + "tag": json_to_dict.get("tag", 12345), + "vrfRouteMap": json_to_dict.get("vrfRouteMap", ""), + "maxBgpPaths": json_to_dict.get("maxBgpPaths", 1), + "maxIbgpPaths": json_to_dict.get("maxIbgpPaths", 2), + "ipv6LinkLocalFlag": json_to_dict.get("ipv6LinkLocalFlag", True), + "trmEnabled": json_to_dict.get("trmEnabled", False), + "isRPExternal": json_to_dict.get("isRPExternal", False), + "rpAddress": json_to_dict.get("rpAddress", ""), + "loopbackNumber": json_to_dict.get("loopbackNumber", ""), + "L3VniMcastGroup": json_to_dict.get("L3VniMcastGroup", ""), + "multicastGroup": json_to_dict.get("multicastGroup", ""), + "trmBGWMSiteEnabled": json_to_dict.get("trmBGWMSiteEnabled", False), + "advertiseHostRouteFlag": json_to_dict.get("advertiseHostRouteFlag", False), + "advertiseDefaultRouteFlag": json_to_dict.get("advertiseDefaultRouteFlag", True), + "configureStaticDefaultRouteFlag": json_to_dict.get("configureStaticDefaultRouteFlag", True), + "bgpPassword": json_to_dict.get("bgpPassword", ""), + "bgpPasswordKeyType": json_to_dict.get("bgpPasswordKeyType", 3), + } + + vrf.update({"vrfTemplateConfig": json.dumps(t_conf)}) + del vrf["vrfStatus"] + have_create.append(vrf) + + vrfs_to_update: set[str] = set() + + vrf_attach: dict = {} + for vrf_attach in get_vrf_attach_response["DATA"]: + if not vrf_attach.get("lanAttachList"): + continue + attach_list: list[dict] = vrf_attach["lanAttachList"] + vrf_to_deploy: str = "" + for attach in attach_list: + attach_state = not attach["lanAttachState"] == "NA" + deploy = attach["isLanAttached"] + deployed: bool = False + if deploy and (attach["lanAttachState"] == "OUT-OF-SYNC" or attach["lanAttachState"] == "PENDING"): + deployed = False + else: + deployed = True + + if deployed: + vrf_to_deploy = attach["vrfName"] + + switch_serial_number: str = attach["switchSerialNo"] + vlan = attach["vlanId"] + inst_values = attach.get("instanceValues", None) + + # The deletes and updates below are done to update the incoming + # dictionary format to align with the outgoing payload requirements. + # Ex: 'vlanId' in the attach section of the incoming payload needs to + # be changed to 'vlan' on the attach section of outgoing payload. + + del attach["vlanId"] + del attach["switchSerialNo"] + del attach["switchName"] + del attach["switchRole"] + del attach["ipAddress"] + del attach["lanAttachState"] + del attach["isLanAttached"] + del attach["vrfId"] + del attach["fabricName"] + + attach.update({"fabric": self.fabric}) + attach.update({"vlan": vlan}) + attach.update({"serialNumber": switch_serial_number}) + attach.update({"deployment": deploy}) + attach.update({"extensionValues": ""}) + attach.update({"instanceValues": inst_values}) + attach.update({"isAttached": attach_state}) + attach.update({"is_deploy": deployed}) + + # Get the VRF LITE extension template and update it + # with the attach['extensionvalues'] + + lite_objects = self.get_vrf_lite_objects(attach) + + if not lite_objects.get("DATA"): + msg = "Early return. lite_objects missing DATA" + self.log.debug(msg) + return + + msg = f"lite_objects: {json.dumps(lite_objects, indent=4, sort_keys=True)}" + self.log.debug(msg) + + sdl: dict = {} + epv: dict = {} + extension_values_dict: dict = {} + ms_con: dict = {} + for sdl in lite_objects["DATA"]: + for epv in sdl["switchDetailsList"]: + if not epv.get("extensionValues"): + attach.update({"freeformConfig": ""}) + continue + ext_values = ast.literal_eval(epv["extensionValues"]) + if ext_values.get("VRF_LITE_CONN") is None: + continue + ext_values = ast.literal_eval(ext_values["VRF_LITE_CONN"]) + extension_values: dict = {} + extension_values["VRF_LITE_CONN"] = [] + + for extension_values_dict in ext_values.get("VRF_LITE_CONN"): + ev_dict = copy.deepcopy(extension_values_dict) + ev_dict.update({"AUTO_VRF_LITE_FLAG": "false"}) + ev_dict.update({"VRF_LITE_JYTHON_TEMPLATE": "Ext_VRF_Lite_Jython"}) + + if extension_values["VRF_LITE_CONN"]: + extension_values["VRF_LITE_CONN"]["VRF_LITE_CONN"].extend([ev_dict]) + else: + extension_values["VRF_LITE_CONN"] = {"VRF_LITE_CONN": [ev_dict]} + + extension_values["VRF_LITE_CONN"] = json.dumps(extension_values["VRF_LITE_CONN"]) + + ms_con["MULTISITE_CONN"] = [] + extension_values["MULTISITE_CONN"] = json.dumps(ms_con) + e_values = json.dumps(extension_values).replace(" ", "") + + attach.update({"extensionValues": e_values}) + + ff_config: str = epv.get("freeformConfig", "") + attach.update({"freeformConfig": ff_config}) + + if vrf_to_deploy: + vrfs_to_update.add(vrf_to_deploy) + + have_attach = get_vrf_attach_response["DATA"] + + if vrfs_to_update: + have_deploy.update({"vrfNames": ",".join(vrfs_to_update)}) + + self.have_create = copy.deepcopy(have_create) + self.have_attach = copy.deepcopy(have_attach) + self.have_deploy = copy.deepcopy(have_deploy) + + msg = "self.have_create: " + msg += f"{json.dumps(self.have_create, indent=4)}" + self.log.debug(msg) + + # json.dumps() here breaks unit tests since self.have_attach is + # a MagicMock and not JSON serializable. + msg = "self.have_attach: " + msg += f"{self.have_attach}" + self.log.debug(msg) + + msg = "self.have_deploy: " + msg += f"{json.dumps(self.have_deploy, indent=4)}" + self.log.debug(msg) + + def get_want(self) -> None: + """ + # Summary + + Parse the playbook config and populate the following. + + - self.want_create : list of dictionaries + - self.want_attach : list of dictionaries + - self.want_deploy : dictionary + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + want_create: list[dict[str, Any]] = [] + want_attach: list[dict[str, Any]] = [] + want_deploy: dict[str, Any] = {} + + msg = "self.config " + msg += f"{json.dumps(self.config, indent=4)}" + self.log.debug(msg) + + all_vrfs: set = set() + + msg = "self.validated: " + msg += f"{json.dumps(self.validated, indent=4, sort_keys=True)}" + self.log.debug(msg) + + vrf: dict[str, Any] + for vrf in self.validated: + try: + vrf_name: str = vrf["vrf_name"] + except KeyError: + msg = f"{self.class_name}.{method_name}: " + msg += f"vrf missing mandatory key vrf_name: {vrf}" + self.module.fail_json(msg=msg) + + all_vrfs.add(vrf_name) + vrf_attach: dict[Any, Any] = {} + vrfs: list[dict[Any, Any]] = [] + + vrf_deploy: bool = vrf.get("deploy", True) + + vlan_id: int = 0 + if vrf.get("vlan_id"): + vlan_id = vrf["vlan_id"] + + want_create.append(self.update_create_params(vrf=vrf, vlan_id=str(vlan_id))) + + if not vrf.get("attach"): + msg = f"No attachments for vrf {vrf_name}. Skipping." + self.log.debug(msg) + continue + for attach in vrf["attach"]: + deploy = vrf_deploy + vrfs.append(self.update_attach_params(attach, vrf_name, deploy, vlan_id)) + + if vrfs: + vrf_attach.update({"vrfName": vrf_name}) + vrf_attach.update({"lanAttachList": vrfs}) + want_attach.append(vrf_attach) + + if len(all_vrfs) != 0: + vrf_names = ",".join(all_vrfs) + want_deploy.update({"vrfNames": vrf_names}) + + self.want_create = copy.deepcopy(want_create) + self.want_attach = copy.deepcopy(want_attach) + self.want_deploy = copy.deepcopy(want_deploy) + + msg = "self.want_create: " + msg += f"{json.dumps(self.want_create, indent=4)}" + self.log.debug(msg) + + msg = "self.want_attach: " + msg += f"{json.dumps(self.want_attach, indent=4)}" + self.log.debug(msg) + + msg = "self.want_deploy: " + msg += f"{json.dumps(self.want_deploy, indent=4)}" + self.log.debug(msg) + + def get_diff_delete(self) -> None: + """ + # Summary + + Using self.have_create, and self.have_attach, update + the following: + + - diff_detach: a list of attachment objects to detach + - diff_undeploy: a dictionary of vrf names to undeploy + - diff_delete: a dictionary of vrf names to delete + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + def get_items_to_detach(attach_list: list[dict]) -> list[dict]: + """ + # Summary + + Given a list of attachment objects, return a list of + attachment objects that are to be detached. + + This is done by checking for the presence of the + "isAttached" key in the attachment object and + checking if the value is True. + + If the "isAttached" key is present and True, it + indicates that the attachment is attached to a + VRF and needs to be detached. In this case, + remove the "isAttached" key and set the + "deployment" key to False. + + The modified attachment object is added to the + detach_list. + + Finally, return the detach_list. + """ + detach_list = [] + for item in attach_list: + if "isAttached" in item: + if item["isAttached"]: + del item["isAttached"] + item.update({"deployment": False}) + detach_list.append(item) + return detach_list + + diff_detach: list[dict] = [] + diff_undeploy: dict = {} + diff_delete: dict = {} + + all_vrfs = set() + + if self.config: + want_c: dict = {} + have_a: dict = {} + for want_c in self.want_create: + + if self.find_dict_in_list_by_key_value(search=self.have_create, key="vrfName", value=want_c["vrfName"]) == {}: + continue + + diff_delete.update({want_c["vrfName"]: "DEPLOYED"}) + + have_a = self.find_dict_in_list_by_key_value(search=self.have_attach, key="vrfName", value=want_c["vrfName"]) + + if not have_a: + continue + + detach_items = get_items_to_detach(have_a["lanAttachList"]) + if detach_items: + have_a.update({"lanAttachList": detach_items}) + diff_detach.append(have_a) + all_vrfs.add(have_a["vrfName"]) + if len(all_vrfs) != 0: + diff_undeploy.update({"vrfNames": ",".join(all_vrfs)}) + + else: + + for have_a in self.have_attach: + detach_items = get_items_to_detach(have_a["lanAttachList"]) + if detach_items: + have_a.update({"lanAttachList": detach_items}) + diff_detach.append(have_a) + all_vrfs.add(have_a.get("vrfName")) + + diff_delete.update({have_a["vrfName"]: "DEPLOYED"}) + if len(all_vrfs) != 0: + diff_undeploy.update({"vrfNames": ",".join(all_vrfs)}) + + self.diff_detach = copy.deepcopy(diff_detach) + self.diff_undeploy = copy.deepcopy(diff_undeploy) + self.diff_delete = copy.deepcopy(diff_delete) + + msg = "self.diff_detach: " + msg += f"{json.dumps(self.diff_detach, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_undeploy: " + msg += f"{json.dumps(self.diff_undeploy, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_delete: " + msg += f"{json.dumps(self.diff_delete, indent=4)}" + self.log.debug(msg) + + def get_diff_override(self): + """ + # Summary + + For override state, we delete existing attachments and vrfs + (self.have_attach) that are not in the want list. + + Using self.have_attach and self.want_create, update + the following: + + - diff_detach: a list of attachment objects to detach + - diff_undeploy: a dictionary of vrf names to undeploy + - diff_delete: a dictionary keyed on vrf name indicating + the deployment status of the vrf e.g. "DEPLOYED" + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + all_vrfs = set() + diff_delete = {} + + self.get_diff_replace() + + diff_detach = copy.deepcopy(self.diff_detach) + diff_undeploy = copy.deepcopy(self.diff_undeploy) + + for have_a in self.have_attach: + found = self.find_dict_in_list_by_key_value(search=self.want_create, key="vrfName", value=have_a["vrfName"]) + + detach_list = [] + if not found: + for item in have_a.get("lanAttachList"): + if "isAttached" not in item: + continue + if item["isAttached"]: + del item["isAttached"] + item.update({"deployment": False}) + detach_list.append(item) + + if detach_list: + have_a.update({"lanAttachList": detach_list}) + diff_detach.append(have_a) + all_vrfs.add(have_a["vrfName"]) + + diff_delete.update({have_a["vrfName"]: "DEPLOYED"}) + + if len(all_vrfs) != 0: + diff_undeploy.update({"vrfNames": ",".join(all_vrfs)}) + + self.diff_delete = copy.deepcopy(diff_delete) + self.diff_detach = copy.deepcopy(diff_detach) + self.diff_undeploy = copy.deepcopy(diff_undeploy) + + msg = "self.diff_delete: " + msg += f"{json.dumps(self.diff_delete, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_detach: " + msg += f"{json.dumps(self.diff_detach, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_undeploy: " + msg += f"{json.dumps(self.diff_undeploy, indent=4)}" + self.log.debug(msg) + + def get_diff_replace(self) -> None: + """ + # Summary + + For replace state, update the attachment objects in self.have_attach + that are not in the want list. + + - diff_attach: a list of attachment objects to attach + - diff_deploy: a dictionary of vrf names to deploy + - diff_delete: a dictionary of vrf names to delete + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + all_vrfs: set = set() + + self.get_diff_merge(replace=True) + # Don't use copy.deepcopy() here. It breaks unit tests. + # Need to think this through, but for now, just use the + # original self.diff_attach and self.diff_deploy. + diff_attach = self.diff_attach + diff_deploy = self.diff_deploy + + replace_vrf_list: list + have_in_want: bool + have_a: dict + want_a: dict + attach_match: bool + for have_a in self.have_attach: + replace_vrf_list = [] + have_in_want = False + for want_a in self.want_attach: + if have_a.get("vrfName") != want_a.get("vrfName"): + continue + have_in_want = True + + try: + have_lan_attach_list: list = have_a["lanAttachList"] + except KeyError: + msg = f"{self.class_name}.{inspect.stack()[0][3]}: " + msg += "lanAttachList key missing from in have_a" + self.module.fail_json(msg=msg) + + have_lan_attach: dict + for have_lan_attach in have_lan_attach_list: + if "isAttached" in have_lan_attach: + if not have_lan_attach.get("isAttached"): + continue + + attach_match = False + try: + want_lan_attach_list = want_a["lanAttachList"] + except KeyError: + msg = f"{self.class_name}.{inspect.stack()[0][3]}: " + msg += "lanAttachList key missing from in want_a" + self.module.fail_json(msg=msg) + + want_lan_attach: dict + for want_lan_attach in want_lan_attach_list: + if have_lan_attach.get("serialNumber") != want_lan_attach.get("serialNumber"): + continue + # Have is already in diff, no need to continue looking for it. + attach_match = True + break + if not attach_match: + if "isAttached" in have_lan_attach: + del have_lan_attach["isAttached"] + have_lan_attach.update({"deployment": False}) + replace_vrf_list.append(have_lan_attach) + break + + if not have_in_want: + found = self.find_dict_in_list_by_key_value(search=self.want_create, key="vrfName", value=have_a["vrfName"]) + + if found: + atch_h = have_a["lanAttachList"] + for a_h in atch_h: + if "isAttached" not in a_h: + continue + if not a_h["isAttached"]: + continue + del a_h["isAttached"] + a_h.update({"deployment": False}) + replace_vrf_list.append(a_h) + + if replace_vrf_list: + in_diff = False + for d_attach in self.diff_attach: + if have_a["vrfName"] != d_attach["vrfName"]: + continue + in_diff = True + d_attach["lanAttachList"].extend(replace_vrf_list) + break + + if not in_diff: + r_vrf_dict = { + "vrfName": have_a["vrfName"], + "lanAttachList": replace_vrf_list, + } + diff_attach.append(r_vrf_dict) + all_vrfs.add(have_a["vrfName"]) + + if len(all_vrfs) == 0: + self.diff_attach = copy.deepcopy(diff_attach) + self.diff_deploy = copy.deepcopy(diff_deploy) + return + + vrf: str + for vrf in self.diff_deploy.get("vrfNames", "").split(","): + all_vrfs.add(vrf) + diff_deploy.update({"vrfNames": ",".join(all_vrfs)}) + + self.diff_attach = copy.deepcopy(diff_attach) + self.diff_deploy = copy.deepcopy(diff_deploy) + + msg = "self.diff_attach: " + msg += f"{json.dumps(self.diff_attach, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_deploy: " + msg += f"{json.dumps(self.diff_deploy, indent=4)}" + self.log.debug(msg) + + def get_next_vrf_id(self, fabric: str) -> int: + """ + # Summary + + Return the next available vrf_id for fabric. + + ## Raises + + Calls fail_json() if: + - Controller version is unsupported + - Unable to retrieve next available vrf_id for fabric + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + attempt = 0 + vrf_id: int = -1 + while attempt < 10: + attempt += 1 + path = self.paths["GET_VRF_ID"].format(fabric) + vrf_id_obj = dcnm_send(self.module, "POST", path) + + missing_fabric, not_ok = self.handle_response(vrf_id_obj, "query_dcnm") + + if missing_fabric or not_ok: + # arobel: TODO: Not covered by UT + msg0 = f"{self.class_name}.{method_name}: " + msg1 = f"{msg0} Fabric {fabric} not present on the controller" + msg2 = f"{msg0} Unable to generate vrfId under fabric {fabric}" + self.module.fail_json(msg=msg1 if missing_fabric else msg2) + + if not vrf_id_obj["DATA"]: + continue + + vrf_id = vrf_id_obj["DATA"].get("partitionSegmentId") + + if vrf_id == -1: + msg = f"{self.class_name}.{method_name}: " + msg += "Unable to retrieve vrf_id " + msg += f"for fabric {fabric}" + self.module.fail_json(msg) + return int(str(vrf_id)) + + def diff_merge_create(self, replace=False) -> None: + """ + # Summary + + Populates the following lists + + - self.diff_create + - self.diff_create_update + - self.diff_create_quick + + TODO: arobel: replace parameter is not used. See Note 1 below. + + Notes + 1. The replace parameter is not used in this method and should be removed. + This was used prior to refactoring this method, and diff_merge_attach, + from an earlier method. diff_merge_attach() does still use + the replace parameter. + + In order to remove this, we have to update 35 unit tests, so we'll + do this as part of a future PR. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + self.conf_changed = {} + + diff_create: list = [] + diff_create_update: list = [] + diff_create_quick: list = [] + + want_c: dict = {} + for want_c in self.want_create: + vrf_found: bool = False + have_c: dict = {} + for have_c in self.have_create: + if want_c["vrfName"] != have_c["vrfName"]: + continue + vrf_found = True + msg = "Calling diff_for_create with: " + msg += f"want_c: {json.dumps(want_c, indent=4, sort_keys=True)}, " + msg += f"have_c: {json.dumps(have_c, indent=4, sort_keys=True)}" + self.log.debug(msg) + + diff, changed = self.diff_for_create(want_c, have_c) + + msg = "diff_for_create() returned with: " + msg += f"changed {changed}, " + msg += f"diff {json.dumps(diff, indent=4, sort_keys=True)}, " + self.log.debug(msg) + + msg = f"Updating self.conf_changed[{want_c['vrfName']}] " + msg += f"with {changed}" + self.log.debug(msg) + self.conf_changed.update({want_c["vrfName"]: changed}) + + if diff: + msg = "Appending diff_create_update with " + msg += f"{json.dumps(diff, indent=4, sort_keys=True)}" + self.log.debug(msg) + diff_create_update.append(diff) + break + + if vrf_found: + continue + vrf_id = want_c.get("vrfId", None) + if vrf_id is not None: + diff_create.append(want_c) + else: + # vrfId is not provided by user. + # Fetch the next available vrfId and use it here. + vrf_id = self.get_next_vrf_id(self.fabric) + + want_c.update({"vrfId": vrf_id}) + json_to_dict = json.loads(want_c["vrfTemplateConfig"]) + template_conf = { + "vrfSegmentId": vrf_id, + "vrfName": want_c["vrfName"], + "vrfVlanId": json_to_dict.get("vrfVlanId"), + "vrfVlanName": json_to_dict.get("vrfVlanName"), + "vrfIntfDescription": json_to_dict.get("vrfIntfDescription"), + "vrfDescription": json_to_dict.get("vrfDescription"), + "mtu": json_to_dict.get("mtu"), + "tag": json_to_dict.get("tag"), + "vrfRouteMap": json_to_dict.get("vrfRouteMap"), + "maxBgpPaths": json_to_dict.get("maxBgpPaths"), + "maxIbgpPaths": json_to_dict.get("maxIbgpPaths"), + "ipv6LinkLocalFlag": json_to_dict.get("ipv6LinkLocalFlag"), + "trmEnabled": json_to_dict.get("trmEnabled"), + "isRPExternal": json_to_dict.get("isRPExternal"), + "rpAddress": json_to_dict.get("rpAddress"), + "loopbackNumber": json_to_dict.get("loopbackNumber"), + "L3VniMcastGroup": json_to_dict.get("L3VniMcastGroup"), + "multicastGroup": json_to_dict.get("multicastGroup"), + "trmBGWMSiteEnabled": json_to_dict.get("trmBGWMSiteEnabled"), + "advertiseHostRouteFlag": json_to_dict.get("advertiseHostRouteFlag"), + "advertiseDefaultRouteFlag": json_to_dict.get("advertiseDefaultRouteFlag"), + "configureStaticDefaultRouteFlag": json_to_dict.get("configureStaticDefaultRouteFlag"), + "bgpPassword": json_to_dict.get("bgpPassword"), + "bgpPasswordKeyType": json_to_dict.get("bgpPasswordKeyType"), + } + + want_c.update({"vrfTemplateConfig": json.dumps(template_conf)}) + + create_path = self.paths["GET_VRF"].format(self.fabric) + + diff_create_quick.append(want_c) + + if self.module.check_mode: + continue + + # arobel: TODO: Not covered by UT + resp = dcnm_send(self.module, "POST", create_path, json.dumps(want_c)) + self.result["response"].append(resp) + + fail, self.result["changed"] = self.handle_response(resp, "create") + + if fail: + self.failure(resp) + + self.diff_create = copy.deepcopy(diff_create) + self.diff_create_update = copy.deepcopy(diff_create_update) + self.diff_create_quick = copy.deepcopy(diff_create_quick) + + msg = "self.diff_create: " + msg += f"{json.dumps(self.diff_create, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_create_quick: " + msg += f"{json.dumps(self.diff_create_quick, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_create_update: " + msg += f"{json.dumps(self.diff_create_update, indent=4)}" + self.log.debug(msg) + + def diff_merge_attach(self, replace=False) -> None: + """ + # Summary + + Populates the following + + - self.diff_attach + - self.diff_deploy + + ## params + + - replace: Passed unaltered to self.diff_for_attach_deploy() + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += f"replace == {replace}" + self.log.debug(msg) + + if not self.want_attach: + self.diff_attach = [] + self.diff_deploy = {} + msg = "Early return. No attachments to process." + self.log.debug(msg) + return + + diff_attach: list = [] + diff_deploy: dict = {} + all_vrfs: set = set() + + for want_a in self.want_attach: + # Check user intent for this VRF and don't add it to the all_vrfs + # set if the user has not requested a deploy. + want_config = self.find_dict_in_list_by_key_value(search=self.config, key="vrf_name", value=want_a["vrfName"]) + vrf_to_deploy: str = "" + attach_found = False + for have_a in self.have_attach: + if want_a["vrfName"] != have_a["vrfName"]: + continue + attach_found = True + diff, deploy_vrf_bool = self.diff_for_attach_deploy( + want_a=want_a["lanAttachList"], + have_a=have_a["lanAttachList"], + replace=replace, + ) + if diff: + base = want_a.copy() + del base["lanAttachList"] + base.update({"lanAttachList": diff}) + + diff_attach.append(base) + if (want_config["deploy"] is True) and (deploy_vrf_bool is True): + vrf_to_deploy = want_a["vrfName"] + else: + if want_config["deploy"] is True and (deploy_vrf_bool or self.conf_changed.get(want_a["vrfName"], False)): + vrf_to_deploy = want_a["vrfName"] + + msg = f"attach_found: {attach_found}" + self.log.debug(msg) + + if not attach_found and want_a.get("lanAttachList"): + attach_list = [] + for attach in want_a["lanAttachList"]: + if attach.get("isAttached"): + del attach["isAttached"] + if attach.get("is_deploy") is True: + vrf_to_deploy = want_a["vrfName"] + attach["deployment"] = True + attach_list.append(copy.deepcopy(attach)) + if attach_list: + base = want_a.copy() + del base["lanAttachList"] + base.update({"lanAttachList": attach_list}) + diff_attach.append(base) + + if vrf_to_deploy: + all_vrfs.add(vrf_to_deploy) + + if len(all_vrfs) != 0: + diff_deploy.update({"vrfNames": ",".join(all_vrfs)}) + + self.diff_attach = copy.deepcopy(diff_attach) + self.diff_deploy = copy.deepcopy(diff_deploy) + + msg = "self.diff_attach: " + msg += f"{json.dumps(self.diff_attach, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_deploy: " + msg += f"{json.dumps(self.diff_deploy, indent=4)}" + self.log.debug(msg) + + def get_diff_merge(self, replace=False): + """ + # Summary + + Call the following methods + + - diff_merge_create() + - diff_merge_attach() + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += f"replace == {replace}" + self.log.debug(msg) + + # Special cases: + # 1. Auto generate vrfId if its not mentioned by user: + # - In this case, query the controller for a vrfId and + # use it in the payload. + # - Any such vrf create requests need to be pushed individually + # (not bulk operation). + + self.diff_merge_create(replace) + self.diff_merge_attach(replace) + + def format_diff(self) -> None: + """ + # Summary + + Populate self.diff_input_format, which represents the + difference to the controller configuration after the playbook + has run, from the information in the following lists: + + - self.diff_create + - self.diff_create_quick + - self.diff_create_update + - self.diff_attach + - self.diff_detach + - self.diff_deploy + - self.diff_undeploy + + self.diff_input_format is formatted using keys a user + would use in a playbook. The keys in the above lists + are those used by the controller API. + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + diff: list = [] + + diff_create: list = copy.deepcopy(self.diff_create) + diff_create_quick: list = copy.deepcopy(self.diff_create_quick) + diff_create_update: list = copy.deepcopy(self.diff_create_update) + diff_attach: list = copy.deepcopy(self.diff_attach) + diff_detach: list = copy.deepcopy(self.diff_detach) + diff_deploy: list = self.diff_deploy["vrfNames"].split(",") if self.diff_deploy else [] + diff_undeploy: list = self.diff_undeploy["vrfNames"].split(",") if self.diff_undeploy else [] + + msg = "INPUT: diff_create: " + msg += f"{json.dumps(diff_create, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "INPUT: diff_create_quick: " + msg += f"{json.dumps(diff_create_quick, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "INPUT: diff_create_update: " + msg += f"{json.dumps(diff_create_update, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "INPUT: diff_attach: " + msg += f"{json.dumps(diff_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "INPUT: diff_detach: " + msg += f"{json.dumps(diff_detach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "INPUT: diff_deploy: " + msg += f"{json.dumps(diff_deploy, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "INPUT: diff_undeploy: " + msg += f"{json.dumps(diff_undeploy, indent=4, sort_keys=True)}" + self.log.debug(msg) + + diff_create.extend(diff_create_quick) + diff_create.extend(diff_create_update) + diff_attach.extend(diff_detach) + diff_deploy.extend(diff_undeploy) + + for want_d in diff_create: + + msg = "want_d: " + msg += f"{json.dumps(want_d, indent=4, sort_keys=True)}" + self.log.debug(msg) + + found_a = self.find_dict_in_list_by_key_value(search=diff_attach, key="vrfName", value=want_d["vrfName"]) + + msg = "found_a: " + msg += f"{json.dumps(found_a, indent=4, sort_keys=True)}" + self.log.debug(msg) + + found_c = copy.deepcopy(want_d) + + msg = "found_c: PRE_UPDATE_v11: " + msg += f"{json.dumps(found_c, indent=4, sort_keys=True)}" + self.log.debug(msg) + + src = found_c["source"] + found_c.update({"vrf_name": found_c["vrfName"]}) + found_c.update({"vrf_id": found_c["vrfId"]}) + found_c.update({"vrf_template": found_c["vrfTemplate"]}) + found_c.update({"vrf_extension_template": found_c["vrfExtensionTemplate"]}) + del found_c["source"] + found_c.update({"source": src}) + found_c.update({"service_vrf_template": found_c["serviceVrfTemplate"]}) + found_c.update({"attach": []}) + + json_to_dict = json.loads(found_c["vrfTemplateConfig"]) + try: + vrf_controller_to_playbook = VrfControllerToPlaybookV11Model(**json_to_dict) + except pydantic.ValidationError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"Validation error: {error}" + self.module.fail_json(msg=msg) + found_c.update(vrf_controller_to_playbook.model_dump(by_alias=False)) + + msg = f"found_c: POST_UPDATE_v11: {json.dumps(found_c, indent=4, sort_keys=True)}" + self.log.debug(msg) + + del found_c["fabric"] + del found_c["vrfName"] + del found_c["vrfId"] + del found_c["vrfTemplate"] + del found_c["vrfExtensionTemplate"] + del found_c["serviceVrfTemplate"] + del found_c["vrfTemplateConfig"] + + msg = "found_c: POST_UPDATE_FINAL: " + msg += f"{json.dumps(found_c, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if diff_deploy and found_c["vrf_name"] in diff_deploy: + diff_deploy.remove(found_c["vrf_name"]) + if not found_a: + msg = "not found_a. Appending found_c to diff." + self.log.debug(msg) + diff.append(found_c) + continue + + attach = found_a["lanAttachList"] + + for a_w in attach: + attach_d = {} + + for key, value in self.ip_sn.items(): + if value != a_w["serialNumber"]: + continue + attach_d.update({"ip_address": key}) + break + attach_d.update({"vlan_id": a_w["vlan"]}) + attach_d.update({"deploy": a_w["deployment"]}) + found_c["attach"].append(attach_d) + + msg = "Appending found_c to diff." + self.log.debug(msg) + + diff.append(found_c) + + diff_attach.remove(found_a) + + for vrf in diff_attach: + new_attach_dict = {} + new_attach_list = [] + attach = vrf["lanAttachList"] + + for a_w in attach: + attach_d = {} + for key, value in self.ip_sn.items(): + if value == a_w["serialNumber"]: + attach_d.update({"ip_address": key}) + break + attach_d.update({"vlan_id": a_w["vlan"]}) + attach_d.update({"deploy": a_w["deployment"]}) + new_attach_list.append(copy.deepcopy(attach_d)) + + if new_attach_list: + if diff_deploy and vrf["vrfName"] in diff_deploy: + diff_deploy.remove(vrf["vrfName"]) + new_attach_dict.update({"attach": new_attach_list}) + new_attach_dict.update({"vrf_name": vrf["vrfName"]}) + diff.append(copy.deepcopy(new_attach_dict)) + + for vrf in diff_deploy: + new_deploy_dict = {"vrf_name": vrf} + diff.append(copy.deepcopy(new_deploy_dict)) + + self.diff_input_format = copy.deepcopy(diff) + + msg = "self.diff_input_format: " + msg += f"{json.dumps(self.diff_input_format, indent=4, sort_keys=True)}" + self.log.debug(msg) + + def get_diff_query(self) -> None: + """ + # Summary + + Query the DCNM for the current state of the VRFs in the fabric. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + path_get_vrf_attach: str + + path_get_vrf: str = self.paths["GET_VRF"].format(self.fabric) + vrf_objects = dcnm_send(self.module, "GET", path_get_vrf) + + missing_fabric, not_ok = self.handle_response(vrf_objects, "query_dcnm") + + if vrf_objects.get("ERROR") == "Not Found" and vrf_objects.get("RETURN_CODE") == 404: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"Fabric {self.fabric} does not exist on the controller" + self.module.fail_json(msg=msg) + + if missing_fabric or not_ok: + # arobel: TODO: Not covered by UT + msg0 = f"{self.class_name}.{method_name}:" + msg0 += f"caller: {caller}. " + msg1 = f"{msg0} Fabric {self.fabric} not present on the controller" + msg2 = f"{msg0} Unable to find VRFs under fabric: {self.fabric}" + self.module.fail_json(msg=msg1 if missing_fabric else msg2) + + if not vrf_objects["DATA"]: + return + + query: list + vrf: dict + get_vrf_attach_response: dict + if self.config: + query = [] + for want_c in self.want_create: + # Query the VRF + for vrf in vrf_objects["DATA"]: + + if want_c["vrfName"] != vrf["vrfName"]: + continue + + item: dict = {"parent": {}, "attach": []} + item["parent"] = vrf + + # Query the Attachment for the found VRF + path_get_vrf_attach = self.paths["GET_VRF_ATTACH"].format(self.fabric, vrf["vrfName"]) + + get_vrf_attach_response = dcnm_send(self.module, "GET", path_get_vrf_attach) + + missing_fabric, not_ok = self.handle_response(get_vrf_attach_response, "query_dcnm") + + if missing_fabric or not_ok: + # arobel: TODO: Not covered by UT + msg0 = f"{self.class_name}.{method_name}:" + msg0 += f"caller: {caller}. " + msg1 = f"{msg0} Fabric {self.fabric} not present on the controller" + msg2 = f"{msg0} Unable to find attachments for " + msg2 += f"vrfs: {vrf['vrfName']} under " + msg2 += f"fabric: {self.fabric}" + self.module.fail_json(msg=msg1 if missing_fabric else msg2) + + if not get_vrf_attach_response.get("DATA", []): + return + + for vrf_attach in get_vrf_attach_response["DATA"]: + if want_c["vrfName"] != vrf_attach["vrfName"]: + continue + if not vrf_attach.get("lanAttachList"): + continue + attach_list = vrf_attach["lanAttachList"] + + for attach in attach_list: + # copy attach and update it with the keys that + # get_vrf_lite_objects() expects. + attach_copy = copy.deepcopy(attach) + attach_copy.update({"fabric": self.fabric}) + attach_copy.update({"serialNumber": attach["switchSerialNo"]}) + lite_objects = self.get_vrf_lite_objects(attach_copy) + if not lite_objects.get("DATA"): + return + data = lite_objects.get("DATA") + if data is not None: + item["attach"].append(data[0]) + query.append(item) + + else: + query = [] + # Query the VRF + for vrf in vrf_objects["DATA"]: + item = {"parent": {}, "attach": []} + item["parent"] = vrf + + # Query the Attachment for the found VRF + path_get_vrf_attach = self.paths["GET_VRF_ATTACH"].format(self.fabric, vrf["vrfName"]) + + get_vrf_attach_response = dcnm_send(self.module, "GET", path_get_vrf_attach) + + missing_fabric, not_ok = self.handle_response(vrf_objects, "query_dcnm") + + if missing_fabric or not_ok: + msg0 = f"caller: {caller}. " + msg1 = f"{msg0} Fabric {self.fabric} not present on DCNM" + msg2 = f"{msg0} Unable to find attachments for " + msg2 += f"vrfs: {vrf['vrfName']} under fabric: {self.fabric}" + + self.module.fail_json(msg=msg1 if missing_fabric else msg2) + # TODO: add a _pylint_: disable=inconsistent-return + # at the top and remove this return + return + + if not get_vrf_attach_response["DATA"]: + return + + for vrf_attach in get_vrf_attach_response["DATA"]: + if not vrf_attach.get("lanAttachList"): + continue + attach_list = vrf_attach["lanAttachList"] + + for attach in attach_list: + # copy attach and update it with the keys that + # get_vrf_lite_objects() expects. + attach_copy = copy.deepcopy(attach) + attach_copy.update({"fabric": self.fabric}) + attach_copy.update({"serialNumber": attach["switchSerialNo"]}) + lite_objects = self.get_vrf_lite_objects(attach_copy) + + lite_objects_data: list = lite_objects.get("DATA", []) + if not lite_objects_data: + return + if not isinstance(lite_objects_data, list): + msg = "lite_objects_data is not a list." + self.module.fail_json(msg=msg) + item["attach"].append(lite_objects_data[0]) + query.append(item) + + self.query = copy.deepcopy(query) + + def push_diff_create_update(self, is_rollback=False) -> None: + """ + # Summary + + Send diff_create_update to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + action: str = "create" + path: str = self.paths["GET_VRF"].format(self.fabric) + + if self.diff_create_update: + for payload in self.diff_create_update: + update_path: str = f"{path}/{payload['vrfName']}" + + args = SendToControllerArgs( + action=action, + path=update_path, + verb=RequestVerb.PUT, + payload=payload, + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def push_diff_detach(self, is_rollback=False) -> None: + """ + # Summary + + Send diff_detach to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += "self.diff_detach: " + msg += f"{json.dumps(self.diff_detach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.diff_detach: + msg = "Early return. self.diff_detach is empty." + self.log.debug(msg) + return + + # For multisite fabric, update the fabric name to the child fabric + # containing the switches + if self.fabric_type == "MFD": + for elem in self.diff_detach: + for node in elem["lanAttachList"]: + node["fabric"] = self.sn_fab[node["serialNumber"]] + + for diff_attach in self.diff_detach: + for vrf_attach in diff_attach["lanAttachList"]: + if "is_deploy" in vrf_attach.keys(): + del vrf_attach["is_deploy"] + + action: str = "attach" + path: str = self.paths["GET_VRF"].format(self.fabric) + detach_path: str = path + "/attachments" + + args = SendToControllerArgs( + action=action, + path=detach_path, + verb=RequestVerb.POST, + payload=self.diff_detach, + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def push_diff_undeploy(self, is_rollback=False): + """ + # Summary + + Send diff_undeploy to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += "self.diff_undeploy: " + msg += f"{json.dumps(self.diff_undeploy, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.diff_undeploy: + msg = "Early return. self.diff_undeploy is empty." + self.log.debug(msg) + return + + action = "deploy" + path = self.paths["GET_VRF"].format(self.fabric) + deploy_path = path + "/deployments" + + args = SendToControllerArgs( + action=action, + path=deploy_path, + verb=RequestVerb.POST, + payload=self.diff_undeploy, + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def push_diff_delete(self, is_rollback=False) -> None: + """ + # Summary + + Send diff_delete to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += "self.diff_delete: " + msg += f"{json.dumps(self.diff_delete, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.diff_delete: + msg = "Early return. self.diff_delete is None." + self.log.debug(msg) + return + + self.wait_for_vrf_del_ready() + + del_failure: set = set() + path: str = self.paths["GET_VRF"].format(self.fabric) + for vrf, state in self.diff_delete.items(): + if state == "OUT-OF-SYNC": + del_failure.add(vrf) + continue + args = SendToControllerArgs( + action="delete", + path=f"{path}/{vrf}", + verb=RequestVerb.DELETE, + payload=self.diff_delete, + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + if len(del_failure) > 0: + msg = f"{self.class_name}.push_diff_delete: " + msg += f"Deletion of vrfs {','.join(del_failure)} has failed" + self.result["response"].append(msg) + self.module.fail_json(msg=self.result) + + def push_diff_create(self, is_rollback=False) -> None: + """ + # Summary + + Send diff_create to the controller + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += "self.diff_create: " + msg += f"{json.dumps(self.diff_create, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.diff_create: + msg = "Early return. self.diff_create is empty." + self.log.debug(msg) + return + + for vrf in self.diff_create: + json_to_dict = json.loads(vrf["vrfTemplateConfig"]) + vlan_id = json_to_dict.get("vrfVlanId", "0") + vrf_name = json_to_dict.get("vrfName") + + if vlan_id == 0: + vlan_path = self.paths["GET_VLAN"].format(self.fabric) + vlan_data = dcnm_send(self.module, "GET", vlan_path) + + # TODO: arobel: Not in UT + if vlan_data["RETURN_CODE"] != 200: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}, " + msg += f"vrf_name: {vrf_name}. " + msg += f"Failure getting autogenerated vlan_id {vlan_data}" + self.module.fail_json(msg=msg) + + vlan_id = vlan_data["DATA"] + + t_conf = { + "vrfSegmentId": vrf["vrfId"], + "vrfName": json_to_dict.get("vrfName", ""), + "vrfVlanId": vlan_id, + "vrfVlanName": json_to_dict.get("vrfVlanName"), + "vrfIntfDescription": json_to_dict.get("vrfIntfDescription"), + "vrfDescription": json_to_dict.get("vrfDescription"), + "mtu": json_to_dict.get("mtu"), + "tag": json_to_dict.get("tag"), + "vrfRouteMap": json_to_dict.get("vrfRouteMap"), + "maxBgpPaths": json_to_dict.get("maxBgpPaths"), + "maxIbgpPaths": json_to_dict.get("maxIbgpPaths"), + "ipv6LinkLocalFlag": json_to_dict.get("ipv6LinkLocalFlag"), + "trmEnabled": json_to_dict.get("trmEnabled"), + "isRPExternal": json_to_dict.get("isRPExternal"), + "rpAddress": json_to_dict.get("rpAddress"), + "loopbackNumber": json_to_dict.get("loopbackNumber"), + "L3VniMcastGroup": json_to_dict.get("L3VniMcastGroup"), + "multicastGroup": json_to_dict.get("multicastGroup"), + "trmBGWMSiteEnabled": json_to_dict.get("trmBGWMSiteEnabled"), + "advertiseHostRouteFlag": json_to_dict.get("advertiseHostRouteFlag"), + "advertiseDefaultRouteFlag": json_to_dict.get("advertiseDefaultRouteFlag"), + "configureStaticDefaultRouteFlag": json_to_dict.get("configureStaticDefaultRouteFlag"), + "bgpPassword": json_to_dict.get("bgpPassword"), + "bgpPasswordKeyType": json_to_dict.get("bgpPasswordKeyType"), + } + + vrf.update({"vrfTemplateConfig": json.dumps(t_conf)}) + + msg = "Sending vrf create request." + self.log.debug(msg) + + args = SendToControllerArgs( + action="create", + path=self.paths["GET_VRF"].format(self.fabric), + verb=RequestVerb.POST, + payload=copy.deepcopy(vrf), + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def is_border_switch(self, serial_number) -> bool: + """ + # Summary + + Given a switch serial_number: + + - Return True if the switch is a border switch + - Return False otherwise + """ + is_border = False + for ip_address, serial in self.ip_sn.items(): + if serial != serial_number: + continue + role = self.inventory_data[ip_address].get("switchRole") + re_result = re.search(r"\bborder\b", role.lower()) + if re_result: + is_border = True + return is_border + + def get_extension_values_from_lite_objects(self, lite: list[dict]) -> list: + """ + # Summary + + Given a list of lite objects, return: + + - A list containing the extensionValues, if any, from these + lite objects. + - An empty list, if the lite objects have no extensionValues + + ## Raises + + None + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + extension_values_list: list[dict] = [] + for item in lite: + if str(item.get("extensionType")) != "VRF_LITE": + continue + extension_values = item["extensionValues"] + extension_values = ast.literal_eval(extension_values) + extension_values_list.append(extension_values) + + msg = "Returning extension_values_list: " + msg += f"{json.dumps(extension_values_list, indent=4, sort_keys=True)}." + self.log.debug(msg) + + return extension_values_list + + def update_vrf_attach_vrf_lite_extensions(self, vrf_attach, lite) -> dict: + """ + # Summary + + ## params + - vrf_attach + A vrf_attach object containing a vrf_lite extension + to update + - lite: A list of current vrf_lite extension objects from + the switch + + ## Description + + 1. Merge the values from the vrf_attach object into a matching + vrf_lite extension object (if any) from the switch. + 2, Update the vrf_attach object with the merged result. + 3. Return the updated vrf_attach object. + + If no matching vrf_lite extension object is found on the switch, + return the unmodified vrf_attach object. + + "matching" in this case means: + + 1. The extensionType of the switch's extension object is VRF_LITE + 2. The IF_NAME in the extensionValues of the extension object + matches the interface in vrf_attach.vrf_lite. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += "vrf_attach: " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + serial_number = vrf_attach.get("serialNumber") + + msg = f"serial_number: {serial_number}" + self.log.debug(msg) + + if vrf_attach.get("vrf_lite") is None: + if "vrf_lite" in vrf_attach: + del vrf_attach["vrf_lite"] + vrf_attach["extensionValues"] = "" + msg = f"serial_number: {serial_number}, " + msg += "vrf_attach does not contain a vrf_lite configuration. " + msg += "Returning it with empty extensionValues. " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + return copy.deepcopy(vrf_attach) + + msg = f"serial_number: {serial_number}, " + msg += "Received lite: " + msg += f"{json.dumps(lite, indent=4, sort_keys=True)}" + self.log.debug(msg) + + ext_values = self.get_extension_values_from_lite_objects(lite) + if ext_values is None: + ip_address = self.serial_number_to_ip(serial_number) + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "No VRF LITE capable interfaces found on " + msg += "this switch. " + msg += f"ip: {ip_address}, " + msg += f"serial_number: {serial_number}" + self.log.debug(msg) + self.module.fail_json(msg=msg) + + matches: dict = {} + # user_vrf_lite_interfaces and switch_vrf_lite_interfaces + # are used in fail_json message when no matching interfaces + # are found on the switch + user_vrf_lite_interfaces = [] + switch_vrf_lite_interfaces = [] + for item in vrf_attach.get("vrf_lite"): + item_interface = item.get("interface") + user_vrf_lite_interfaces.append(item_interface) + for ext_value in ext_values: + ext_value_interface = ext_value.get("IF_NAME") + switch_vrf_lite_interfaces.append(ext_value_interface) + msg = f"item_interface: {item_interface}, " + msg += f"ext_value_interface: {ext_value_interface}" + self.log.debug(msg) + if item_interface != ext_value_interface: + continue + msg = "Found item: " + msg += f"item[interface] {item_interface}, == " + msg += f"ext_values[IF_NAME] {ext_value_interface}, " + msg += f"{json.dumps(item)}" + self.log.debug(msg) + matches[item_interface] = {} + matches[item_interface]["user"] = item + matches[item_interface]["switch"] = ext_value + if not matches: + # No matches. fail_json here to avoid the following 500 error + # "Provided interface doesn't have extensions" + ip_address = self.serial_number_to_ip(serial_number) + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "No matching interfaces with vrf_lite extensions " + msg += f"found on switch {ip_address} ({serial_number}). " + msg += "playbook vrf_lite_interfaces: " + msg += f"{','.join(sorted(user_vrf_lite_interfaces))}. " + msg += "switch vrf_lite_interfaces: " + msg += f"{','.join(sorted(switch_vrf_lite_interfaces))}." + self.log.debug(msg) + self.module.fail_json(msg) + + msg = "Matching extension object(s) found on the switch. " + msg += "Proceeding to convert playbook vrf_lite configuration " + msg += "to payload format. " + msg += f"matches: {json.dumps(matches, indent=4, sort_keys=True)}" + self.log.debug(msg) + + extension_values: dict = {} + extension_values["VRF_LITE_CONN"] = [] + extension_values["MULTISITE_CONN"] = [] + + for interface, item in matches.items(): + msg = f"interface: {interface}: " + msg += "item: " + msg += f"{json.dumps(item, indent=4, sort_keys=True)}" + self.log.debug(msg) + + nbr_dict = {} + nbr_dict["IF_NAME"] = item["user"]["interface"] + + if item["user"]["dot1q"]: + nbr_dict["DOT1Q_ID"] = str(item["user"]["dot1q"]) + else: + nbr_dict["DOT1Q_ID"] = str(item["switch"]["DOT1Q_ID"]) + + if item["user"]["ipv4_addr"]: + nbr_dict["IP_MASK"] = item["user"]["ipv4_addr"] + else: + nbr_dict["IP_MASK"] = item["switch"]["IP_MASK"] + + if item["user"]["neighbor_ipv4"]: + nbr_dict["NEIGHBOR_IP"] = item["user"]["neighbor_ipv4"] + else: + nbr_dict["NEIGHBOR_IP"] = item["switch"]["NEIGHBOR_IP"] + + nbr_dict["NEIGHBOR_ASN"] = item["switch"]["NEIGHBOR_ASN"] + + if item["user"]["ipv6_addr"]: + nbr_dict["IPV6_MASK"] = item["user"]["ipv6_addr"] + else: + nbr_dict["IPV6_MASK"] = item["switch"]["IPV6_MASK"] + + if item["user"]["neighbor_ipv6"]: + nbr_dict["IPV6_NEIGHBOR"] = item["user"]["neighbor_ipv6"] + else: + nbr_dict["IPV6_NEIGHBOR"] = item["switch"]["IPV6_NEIGHBOR"] + + nbr_dict["AUTO_VRF_LITE_FLAG"] = item["switch"]["AUTO_VRF_LITE_FLAG"] + + if item["user"]["peer_vrf"]: + nbr_dict["PEER_VRF_NAME"] = item["user"]["peer_vrf"] + else: + nbr_dict["PEER_VRF_NAME"] = item["switch"]["PEER_VRF_NAME"] + + nbr_dict["VRF_LITE_JYTHON_TEMPLATE"] = "Ext_VRF_Lite_Jython" + vrflite_con: dict = {} + vrflite_con["VRF_LITE_CONN"] = [] + vrflite_con["VRF_LITE_CONN"].append(copy.deepcopy(nbr_dict)) + if extension_values["VRF_LITE_CONN"]: + extension_values["VRF_LITE_CONN"]["VRF_LITE_CONN"].extend(vrflite_con["VRF_LITE_CONN"]) + else: + extension_values["VRF_LITE_CONN"] = vrflite_con + + ms_con: dict = {} + ms_con["MULTISITE_CONN"] = [] + extension_values["MULTISITE_CONN"] = json.dumps(ms_con) + + extension_values["VRF_LITE_CONN"] = json.dumps(extension_values["VRF_LITE_CONN"]) + vrf_attach["extensionValues"] = json.dumps(extension_values).replace(" ", "") + if vrf_attach.get("vrf_lite") is not None: + del vrf_attach["vrf_lite"] + + msg = "Returning modified vrf_attach: " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + return copy.deepcopy(vrf_attach) + + def ip_to_serial_number(self, ip_address): + """ + Given a switch ip_address, return the switch serial number. + + If ip_address is not found, return None. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}" + self.log.debug(msg) + + return self.ip_sn.get(ip_address) + + def serial_number_to_ip(self, serial_number): + """ + Given a switch serial_number, return the switch ip address. + + If serial_number is not found, return None. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}, " + msg += f"serial_number: {serial_number}. " + msg += f"Returning ip: {self.sn_ip.get(serial_number)}." + self.log.debug(msg) + + return self.sn_ip.get(serial_number) + + def send_to_controller(self, args: SendToControllerArgs) -> None: + """ + # Summary + + Send a request to the controller. + + Update self.response with the response from the controller. + + ## params + + args: instance of SendToControllerArgs containing the following + - `action`: The action to perform (create, update, delete, etc.) + - `verb`: The HTTP verb to use (GET, POST, PUT, DELETE) + - `path`: The URL path to send the request to + - `payload`: The payload to send with the request (None for no payload) + - `log_response`: If True, log the response in the result, else + do not include the response in the result + - `is_rollback`: If True, attempt to rollback on failure + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}" + self.log.debug(msg) + + msg = "TX controller: " + msg += f"action: {args.action}, " + msg += f"verb: {args.verb.value}, " + msg += f"path: {args.path}, " + msg += f"log_response: {args.log_response}, " + msg += "type(payload): " + msg += f"{type(args.payload)}, " + msg += "payload: " + msg += f"{json.dumps(args.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if args.payload is not None: + response = dcnm_send(self.module, args.verb.value, args.path, json.dumps(args.payload)) + else: + response = dcnm_send(self.module, args.verb.value, args.path) + + self.response = copy.deepcopy(response) + + msg = "RX controller: " + msg += f"verb: {args.verb.value}, " + msg += f"path: {args.path}, " + msg += "response: " + msg += f"{json.dumps(response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "Calling self.handle_response. " + msg += "self.result[changed]): " + msg += f"{self.result['changed']}" + self.log.debug(msg) + + if args.log_response is True: + self.result["response"].append(response) + + fail, self.result["changed"] = self.handle_response(response, args.action) + + msg = f"caller: {caller}, " + msg += "Calling self.handle_response. DONE" + msg += f"{self.result['changed']}" + self.log.debug(msg) + + if fail: + if args.is_rollback: + self.failed_to_rollback = True + return + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}, " + msg += "Calling self.failure." + self.log.debug(msg) + self.failure(response) + + def update_vrf_attach_fabric_name(self, vrf_attach: dict) -> dict: + """ + # Summary + + For multisite fabrics, replace `vrf_attach.fabric` with the name of + the child fabric returned by `self.sn_fab[vrf_attach.serialNumber]` + + ## params + + - `vrf_attach` + + A `vrf_attach` dictionary containing the following keys: + + - `fabric` : fabric name + - `serialNumber` : switch serial number + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + msg = "Received vrf_attach: " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if self.fabric_type != "MFD": + msg = "Early return. " + msg += f"FABRIC_TYPE {self.fabric_type} is not MFD. " + msg += "Returning unmodified vrf_attach." + self.log.debug(msg) + return copy.deepcopy(vrf_attach) + + parent_fabric_name = vrf_attach.get("fabric") + + msg = f"fabric_type: {self.fabric_type}, " + msg += "replacing parent_fabric_name " + msg += f"({parent_fabric_name}) " + msg += "with child fabric name." + self.log.debug(msg) + + serial_number = vrf_attach.get("serialNumber") + + if serial_number is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "Unable to parse serial_number from vrf_attach. " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + self.module.fail_json(msg) + + child_fabric_name = self.sn_fab[serial_number] + + if child_fabric_name is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "Unable to determine child fabric name for serial_number " + msg += f"{serial_number}." + self.log.debug(msg) + self.module.fail_json(msg) + + msg = f"serial_number: {serial_number}, " + msg += f"child fabric name: {child_fabric_name}. " + self.log.debug(msg) + + vrf_attach["fabric"] = child_fabric_name + + msg += "Updated vrf_attach: " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + return copy.deepcopy(vrf_attach) + + def push_diff_attach(self, is_rollback=False) -> None: + """ + # Summary + + Send diff_attach to the controller + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = f"caller {caller}, " + msg += "ENTERED. " + self.log.debug(msg) + + msg = "self.diff_attach PRE: " + msg += f"{json.dumps(self.diff_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.diff_attach: + msg = "Early return. self.diff_attach is empty. " + msg += f"{json.dumps(self.diff_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + return + + new_diff_attach_list: list = [] + for diff_attach in self.diff_attach: + msg = "diff_attach: " + msg += f"{json.dumps(diff_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + new_lan_attach_list = [] + for vrf_attach in diff_attach["lanAttachList"]: + vrf_attach.update(vlan=0) + + serial_number = vrf_attach.get("serialNumber") + ip_address = self.serial_number_to_ip(serial_number) + msg = f"ip_address {ip_address} ({serial_number}), " + msg += "vrf_attach: " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + vrf_attach = self.update_vrf_attach_fabric_name(vrf_attach) + + if "is_deploy" in vrf_attach: + del vrf_attach["is_deploy"] + # if vrf_lite is null, delete it. + if not vrf_attach.get("vrf_lite"): + if "vrf_lite" in vrf_attach: + msg = "vrf_lite exists, but is null. Delete it." + self.log.debug(msg) + del vrf_attach["vrf_lite"] + new_lan_attach_list.append(vrf_attach) + msg = f"ip_address {ip_address} ({serial_number}), " + msg += "deleted null vrf_lite in vrf_attach and " + msg += "skipping VRF Lite processing. " + msg += "updated vrf_attach: " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + continue + + # VRF Lite processing + + msg = f"ip_address {ip_address} ({serial_number}), " + msg += "vrf_attach.get(vrf_lite): " + msg += f"{json.dumps(vrf_attach.get('vrf_lite'), indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.is_border_switch(serial_number): + # arobel TODO: Not covered by UT + msg = f"{self.class_name}.{method_name}: " + msg += f"caller {caller}. " + msg += "VRF LITE cannot be attached to " + msg += "non-border switch. " + msg += f"ip: {ip_address}, " + msg += f"serial number: {serial_number}" + self.module.fail_json(msg=msg) + + lite_objects = self.get_vrf_lite_objects(vrf_attach) + + msg = f"ip_address {ip_address} ({serial_number}), " + msg += "lite_objects: " + msg += f"{json.dumps(lite_objects, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not lite_objects.get("DATA"): + msg = f"ip_address {ip_address} ({serial_number}), " + msg += "Early return, no lite objects." + self.log.debug(msg) + return + + lite = lite_objects["DATA"][0]["switchDetailsList"][0]["extensionPrototypeValues"] + msg = f"ip_address {ip_address} ({serial_number}), " + msg += "lite: " + msg += f"{json.dumps(lite, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"ip_address {ip_address} ({serial_number}), " + msg += "old vrf_attach: " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + vrf_attach = self.update_vrf_attach_vrf_lite_extensions(vrf_attach, lite) + msg = f"ip_address {ip_address} ({serial_number}), " + msg += "new vrf_attach: " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + new_lan_attach_list.append(vrf_attach) + + msg = "Updating diff_attach[lanAttachList] with: " + msg += f"{json.dumps(new_lan_attach_list, indent=4, sort_keys=True)}" + self.log.debug(msg) + + diff_attach["lanAttachList"] = copy.deepcopy(new_lan_attach_list) + new_diff_attach_list.append(copy.deepcopy(diff_attach)) + + msg = "new_diff_attach_list: " + msg += f"{json.dumps(new_diff_attach_list, indent=4, sort_keys=True)}" + self.log.debug(msg) + + args = SendToControllerArgs( + action="attach", + path=f"{self.paths['GET_VRF'].format(self.fabric)}/attachments", + verb=RequestVerb.POST, + payload=new_diff_attach_list, + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def push_diff_deploy(self, is_rollback=False): + """ + # Summary + + Send diff_deploy to the controller + """ + caller = inspect.stack()[1][3] + + msg = f"caller: {caller}. " + msg += "ENTERED." + self.log.debug(msg) + + if not self.diff_deploy: + msg = "Early return. self.diff_deploy is empty." + self.log.debug(msg) + return + + args = SendToControllerArgs( + action="deploy", + path=f"{self.paths['GET_VRF'].format(self.fabric)}/deployments", + verb=RequestVerb.POST, + payload=self.diff_deploy, + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def release_resources_by_id(self, id_list=None) -> None: + """ + # Summary + + Given a list of resource IDs, send a request to the controller + to release them. + + ## params + + - id_list: A list of resource IDs to release. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = f"caller: {caller}. " + msg += "ENTERED." + self.log.debug(msg) + + if id_list is None: + id_list = [] + + if not isinstance(id_list, list): + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "id_list must be a list of resource IDs. " + msg += f"Got: {id_list}." + self.module.fail_json(msg) + + try: + id_list = [int(x) for x in id_list] + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "id_list must be a list of resource IDs. " + msg += "Where each id is convertable to integer." + msg += f"Got: {id_list}. " + msg += f"Error detail: {error}" + self.module.fail_json(msg) + + # The controller can release only around 500-600 IDs per + # request (not sure of the exact number). We break up + # requests into smaller lists here. In practice, we'll + # likely ever only have one resulting list. + id_list_of_lists = self.get_list_of_lists([str(x) for x in id_list], 512) + + for item in id_list_of_lists: + msg = "Releasing resource IDs: " + msg += f"{','.join(item)}" + self.log.debug(msg) + + path: str = "/appcenter/cisco/ndfc/api/v1/lan-fabric" + path += "/rest/resource-manager/resources" + path += f"?id={','.join(item)}" + args = SendToControllerArgs( + action="deploy", + path=path, + verb=RequestVerb.DELETE, + payload=None, + log_response=False, + is_rollback=False, + ) + self.send_to_controller(args) + + def release_orphaned_resources(self, vrf: str, is_rollback=False) -> None: + """ + # Summary + + Release orphaned resources. + + ## Description + + After a VRF delete operation, resources such as the TOP_DOWN_VRF_VLAN + resource below, can be orphaned from their VRFs. Below, notice that + resourcePool.vrfName is null. This method releases resources if + the following are true for the resources: + + - allocatedFlag is False + - entityName == vrf + - fabricName == self.fabric + + ```json + [ + { + "id": 36368, + "resourcePool": { + "id": 0, + "poolName": "TOP_DOWN_VRF_VLAN", + "fabricName": "f1", + "vrfName": null, + "poolType": "ID_POOL", + "dynamicSubnetRange": null, + "targetSubnet": 0, + "overlapAllowed": false, + "hierarchicalKey": "f1" + }, + "entityType": "Device", + "entityName": "VRF_1", + "allocatedIp": "201", + "allocatedOn": 1734040978066, + "allocatedFlag": false, + "allocatedScopeValue": "FDO211218GC", + "ipAddress": "172.22.150.103", + "switchName": "cvd-1312-leaf", + "hierarchicalKey": "0" + } + ] + ``` + """ + self.log.debug("ENTERED") + + path = "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/" + path += f"resource-manager/fabric/{self.fabric}/" + path += "pools/TOP_DOWN_VRF_VLAN" + + args = SendToControllerArgs( + action="query", + path=path, + verb=RequestVerb.GET, + payload=None, + log_response=False, + is_rollback=False, + ) + self.send_to_controller(args) + resp = copy.deepcopy(self.response) + + fail, self.result["changed"] = self.handle_response(resp, "deploy") + if fail: + if is_rollback: + self.failed_to_rollback = True + return + self.failure(resp) + + delete_ids: list = [] + for item in resp["DATA"]: + if "entityName" not in item: + continue + if item["entityName"] != vrf: + continue + if item.get("allocatedFlag") is not False: + continue + if item.get("id") is None: + continue + + msg = f"item {json.dumps(item, indent=4, sort_keys=True)}" + self.log.debug(msg) + + delete_ids.append(item["id"]) + + self.release_resources_by_id(delete_ids) + + def push_to_remote(self, is_rollback=False) -> None: + """ + # Summary + + Send all diffs to the controller + """ + caller = inspect.stack()[1][3] + msg = "ENTERED. " + msg += f"caller: {caller}." + self.log.debug(msg) + + self.push_diff_create_update(is_rollback=is_rollback) + + # The detach and un-deploy operations are executed before the + # create,attach and deploy to address cases where a VLAN for vrf + # attachment being deleted is re-used on a new vrf attachment being + # created. This is needed specially for state: overridden + + self.push_diff_detach(is_rollback=is_rollback) + self.push_diff_undeploy(is_rollback=is_rollback) + + msg = "Calling self.push_diff_delete" + self.log.debug(msg) + + self.push_diff_delete(is_rollback=is_rollback) + for vrf_name in self.diff_delete: + self.release_orphaned_resources(vrf=vrf_name, is_rollback=is_rollback) + + self.push_diff_create(is_rollback=is_rollback) + self.push_diff_attach(is_rollback=is_rollback) + self.push_diff_deploy(is_rollback=is_rollback) + + def wait_for_vrf_del_ready(self, vrf_name: str = "not_supplied") -> None: + """ + # Summary + + Wait for VRFs to be ready for deletion. + + ## Raises + + Calls fail_json if VRF has associated network attachments. + """ + caller = inspect.stack()[1][3] + msg = "ENTERED. " + msg += f"caller: {caller}, " + msg += f"vrf_name: {vrf_name}" + self.log.debug(msg) + + for vrf in self.diff_delete: + ok_to_delete: bool = False + path: str = self.paths["GET_VRF_ATTACH"].format(self.fabric, vrf) + + while not ok_to_delete: + args = SendToControllerArgs( + action="query", + path=path, + verb=RequestVerb.GET, + payload=None, + log_response=False, + is_rollback=False, + ) + self.send_to_controller(args) + + resp = copy.deepcopy(self.response) + ok_to_delete = True + if resp.get("DATA") is None: + time.sleep(self.wait_time_for_delete_loop) + continue + + attach_list: list = resp["DATA"][0]["lanAttachList"] + msg = f"ok_to_delete: {ok_to_delete}, " + msg += f"attach_list: {json.dumps(attach_list, indent=4)}" + self.log.debug(msg) + + attach: dict = {} + for attach in attach_list: + if attach["lanAttachState"] == "OUT-OF-SYNC" or attach["lanAttachState"] == "FAILED": + self.diff_delete.update({vrf: "OUT-OF-SYNC"}) + break + if attach["lanAttachState"] == "DEPLOYED" and attach["isLanAttached"] is True: + vrf_name = attach.get("vrfName", "unknown") + fabric_name: str = attach.get("fabricName", "unknown") + switch_ip: str = attach.get("ipAddress", "unknown") + switch_name: str = attach.get("switchName", "unknown") + vlan_id: str = attach.get("vlanId", "unknown") + msg = f"Network attachments associated with vrf {vrf_name} " + msg += "must be removed (e.g. using the dcnm_network module) " + msg += "prior to deleting the vrf. " + msg += f"Details: fabric_name: {fabric_name}, " + msg += f"vrf_name: {vrf_name}. " + msg += "Network attachments found on " + msg += f"switch_ip: {switch_ip}, " + msg += f"switch_name: {switch_name}, " + msg += f"vlan_id: {vlan_id}" + self.module.fail_json(msg=msg) + if attach["lanAttachState"] != "NA": + time.sleep(self.wait_time_for_delete_loop) + self.diff_delete.update({vrf: "DEPLOYED"}) + ok_to_delete = False + break + self.diff_delete.update({vrf: "NA"}) + + def validate_input(self) -> None: + """Parse the playbook values, validate to param specs.""" + self.log.debug("ENTERED") + + if self.state == "deleted": + self.validate_input_deleted_state() + elif self.state == "merged": + self.validate_input_merged_state() + elif self.state == "overridden": + self.validate_input_overridden_state() + elif self.state == "query": + self.validate_input_query_state() + elif self.state in ("replaced"): + self.validate_input_replaced_state() + + def validate_vrf_config(self) -> None: + """ + # Summary + + Validate self.config against VrfPlaybookModelV11 and update + self.validated with the validated config. + + ## Raises + + - Calls fail_json() if the input is invalid + + """ + if self.config is None: + return + for vrf_config in self.config: + try: + self.log.debug("Calling VrfPlaybookModelV11") + config = VrfPlaybookModelV11(**vrf_config) + msg = f"config.model_dump_json(): {config.model_dump_json()}" + self.log.debug(msg) + self.log.debug("Calling VrfPlaybookModelV11 DONE") + except pydantic.ValidationError as error: + self.module.fail_json(msg=error) + + self.validated.append(config.model_dump()) + + msg = f"self.validated: {json.dumps(self.validated, indent=4, sort_keys=True)}" + self.log.debug(msg) + + def validate_input_deleted_state(self) -> None: + """ + # Summary + + Validate the input for deleted state. + """ + if self.state != "deleted": + return + if not self.config: + return + self.validate_vrf_config() + + def validate_input_merged_state(self) -> None: + """ + # Summary + + Validate the input for merged state. + """ + if self.state != "merged": + return + + if self.config is None: + self.config = [] + + method_name = inspect.stack()[0][3] + if len(self.config) == 0: + msg = f"{self.class_name}.{method_name}: " + msg += "config element is mandatory for merged state" + self.module.fail_json(msg=msg) + + self.validate_vrf_config() + + def validate_input_overridden_state(self) -> None: + """ + # Summary + + Validate the input for overridden state. + """ + if self.state != "overridden": + return + if not self.config: + return + self.validate_vrf_config() + + def validate_input_query_state(self) -> None: + """ + # Summary + + Validate the input for query state. + """ + if self.state != "query": + return + if not self.config: + return + self.validate_vrf_config() + + def validate_input_replaced_state(self) -> None: + """ + # Summary + + Validate the input for replaced state. + """ + if self.state != "replaced": + return + if not self.config: + return + self.validate_vrf_config() + + def handle_response(self, res, action): + """ + # Summary + + Handle the response from the controller. + """ + self.log.debug("ENTERED") + + fail = False + changed = True + + if action == "query_dcnm": + # These if blocks handle responses to the query APIs. + # Basically all GET operations. + if res.get("ERROR") == "Not Found" and res["RETURN_CODE"] == 404: + return True, False + if res["RETURN_CODE"] != 200 or res["MESSAGE"] != "OK": + return False, True + return False, False + + # Responses to all other operations POST and PUT are handled here. + if res.get("MESSAGE") != "OK" or res["RETURN_CODE"] != 200: + fail = True + changed = False + return fail, changed + if res.get("ERROR"): + fail = True + changed = False + if action == "attach" and "is in use already" in str(res.values()): + fail = True + changed = False + if action == "deploy" and "No switches PENDING for deployment" in str(res.values()): + changed = False + + return fail, changed + + def failure(self, resp): + """ + # Summary + + Handle failures. + """ + # Do not Rollback for Multi-site fabrics + if self.fabric_type == "MFD": + self.failed_to_rollback = True + self.module.fail_json(msg=resp) + return + + # Implementing a per task rollback logic here so that we rollback + # to the have state whenever there is a failure in any of the APIs. + # The idea would be to run overridden state with want=have and have=dcnm_state + self.want_create = self.have_create + self.want_attach = self.have_attach + self.want_deploy = self.have_deploy + + self.have_create = [] + self.have_attach = [] + self.have_deploy = {} + self.get_have() + self.get_diff_override() + + self.push_to_remote(True) + + if self.failed_to_rollback: + msg1 = "FAILED - Attempted rollback of the task has failed, " + msg1 += "may need manual intervention" + else: + msg1 = "SUCCESS - Attempted rollback of the task has succeeded" + + res = copy.deepcopy(resp) + res.update({"ROLLBACK_RESULT": msg1}) + + if not resp.get("DATA"): + data = copy.deepcopy(resp.get("DATA")) + if data.get("stackTrace"): + data.update({"stackTrace": "Stack trace is hidden, use '-vvvvv' to print it"}) + res.update({"DATA": data}) + + # pylint: disable=protected-access + if self.module._verbosity >= 5: + self.module.fail_json(msg=res) + # pylint: enable=protected-access + + self.module.fail_json(msg=res) diff --git a/plugins/module_utils/vrf/dcnm_vrf_v12.py b/plugins/module_utils/vrf/dcnm_vrf_v12.py new file mode 100644 index 000000000..8768e274e --- /dev/null +++ b/plugins/module_utils/vrf/dcnm_vrf_v12.py @@ -0,0 +1,4322 @@ +# -*- coding: utf-8 -*- +# mypy: disable-error-code="import-untyped" +# +# Copyright (c) 2020-2025 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. +# pylint: disable=wrong-import-position +from __future__ import absolute_import, division, print_function + +# pylint: disable=invalid-name +__metaclass__ = type +__author__ = "Shrishail Kariyappanavar, Karthik Babu Harichandra Babu, Praveen Ramoorthy, Allen Robel" +# pylint: enable=invalid-name +""" +""" +import copy +import inspect +import json +import logging +import re +import time +from dataclasses import asdict, dataclass +from typing import Any, Final, Optional, Union + +from ansible.module_utils.basic import AnsibleModule +from pydantic import ValidationError + +from ...module_utils.common.api.v1.lan_fabric.rest.top_down.fabrics.vrfs.vrfs import EpVrfGet, EpVrfPost +from ...module_utils.common.enums.http_requests import RequestVerb +from ...module_utils.network.dcnm.dcnm import dcnm_get_ip_addr_info, dcnm_send, get_fabric_details, get_fabric_inventory_details, get_sn_fabric_dict +from .inventory_ipv4_to_serial_number import InventoryIpv4ToSerialNumber +from .inventory_ipv4_to_switch_role import InventoryIpv4ToSwitchRole +from .inventory_serial_number_to_ipv4 import InventorySerialNumberToIpv4 +from .inventory_serial_number_to_switch_role import InventorySerialNumberToSwitchRole +from .model_controller_response_fabrics_easy_fabric_get import ControllerResponseFabricsEasyFabricGet +from .model_controller_response_generic_v12 import ControllerResponseGenericV12 +from .model_controller_response_get_fabrics_vrfinfo import ControllerResponseGetFabricsVrfinfoV12 +from .model_controller_response_get_int import ControllerResponseGetIntV12 +from .model_controller_response_vrfs_attachments_v12 import ControllerResponseVrfsAttachmentsDataItem, ControllerResponseVrfsAttachmentsV12 +from .model_controller_response_vrfs_deployments_v12 import ControllerResponseVrfsDeploymentsV12 +from .model_controller_response_vrfs_switches_v12 import ControllerResponseVrfsSwitchesDataItem, ControllerResponseVrfsSwitchesV12 +from .model_controller_response_vrfs_v12 import ControllerResponseVrfsV12, VrfObjectV12 +from .model_have_attach_post_mutate_v12 import HaveAttachPostMutate, HaveLanAttachItem +from .model_payload_vrfs_attachments import PayloadVrfsAttachmentsLanAttachListItem +from .model_payload_vrfs_deployments import PayloadVrfsDeployments +from .model_playbook_vrf_v12 import PlaybookVrfAttachModel, PlaybookVrfModelV12 +from .model_vrf_detach_payload_v12 import LanDetachListItemV12, VrfDetachPayloadV12 +from .transmute_diff_attach_to_payload import DiffAttachToControllerPayload +from .vrf_controller_payload_v12 import VrfPayloadV12 +from .vrf_controller_to_playbook_v12 import VrfControllerToPlaybookV12Model +from .vrf_template_config_v12 import VrfTemplateConfigV12 +from .vrf_utils import get_endpoint_with_long_query_string + +dcnm_vrf_paths: dict = { + "GET_VRF_ATTACH": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{}/vrfs/attachments?vrf-names={}", + "GET_VRF_SWITCH": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{}/vrfs/switches?vrf-names={}&serial-numbers={}", + "GET_VRF_ID": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{}/vrfinfo", + "GET_VLAN": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/resource-manager/vlan/{}?vlanUsageType=TOP_DOWN_VRF_VLAN", +} + + +@dataclass +class SendToControllerArgs: + """ + # Summary + + Arguments for DcnmVrf.send_to_controller() + + ## params + + - `action`: The action to perform (create, update, delete, etc.) + - `verb`: The HTTP verb to use (GET, POST, PUT, DELETE) + - `path`: The endpoint path for the request + - `payload`: The payload to send with the request (None for no payload) + - `log_response`: If True, log the response in the result, else + do not include the response in the result + - `is_rollback`: If True, attempt to rollback on failure + - `response_model`: Optional[Any] = None + + """ + + action: str + verb: RequestVerb + path: str + payload: Optional[Union[dict, list]] + log_response: bool = True + is_rollback: bool = False + response_model: Optional[Any] = None + + dict = asdict + + +class NdfcVrf12: + """ + # Summary + + dcnm_vrf module implementation for NDFC version 12 + """ + + def __init__(self, module: AnsibleModule): + self.class_name: str = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + # Temporary hack to determine of usage of Pydantic models is enabled. + # If True, model-based methods are used. + # If False, legacy methods are used. + # Do not set this to True here. It's set/unset strategically + # as needed and will be removed once all methods are modified + # to use Pydantic models. + self.model_enabled: bool = False + + self.module: AnsibleModule = module + self.params: dict[str, Any] = module.params + + try: + self.state: str = self.params["state"] + except KeyError: + msg = f"{self.class_name}.__init__(): " + msg += "'state' parameter is missing from params." + module.fail_json(msg=msg) + + try: + self.fabric: str = module.params["fabric"] + except KeyError: + msg = f"{self.class_name}.__init__(): " + msg += "fabric missing from params." + module.fail_json(msg=msg) + + msg = f"self.state: {self.state}, " + msg += "self.params: " + msg += f"{json.dumps(self.params, indent=4, sort_keys=True)}" + self.log.debug(msg) + + self.config: Optional[list[dict]] = copy.deepcopy(module.params.get("config")) + + msg = f"self.state: {self.state}, " + msg += "self.config: " + msg += f"{json.dumps(self.config, indent=4, sort_keys=True)}" + self.log.debug(msg) + + # Setting self.conf_changed to class scope since, after refactoring, + # it is initialized and updated in one refactored method + # (diff_merge_create) and accessed in another refactored method + # (diff_merge_attach) which reset it to {} at the top of the method + # (which undid the update in diff_merge_create). + # TODO: Revisit this in Phase 2 refactoring. + self.conf_changed: dict = {} + self.check_mode: bool = False + self.have_create: list[dict] = [] + self.want_create: list[dict] = [] + # Will eventually replace self.want_create with self.want_create_payload_models + self.want_create_payload_models: list[VrfPayloadV12] = [] + self.diff_create: list = [] + self.diff_create_update: list = [] + # self.diff_create_quick holds all the create payloads which are + # missing a vrfId. These payloads are sent to DCNM out of band + # (in the get_diff_merge()). We lose diffs for these without this + # variable. The content stored here will be helpful for cases like + # "check_mode" and to print diffs[] in the output of each task. + self.diff_create_quick: list = [] + # self.have_attach is accessed only in self.failure() and is populated in populate_have_attach_models() + # It will eventually be removed. + self.have_attach: list = [] + self.have_attach_models: list[HaveAttachPostMutate] = [] + self.want_attach: list = [] + self.want_attach_vrf_lite: dict = {} + self.diff_attach: list = [] + self.validated_playbook_config: list = [] + self.validated_playbook_config_models: list[PlaybookVrfModelV12] = [] + # diff_detach contains all attachments of a vrf being deleted, + # especially for state: OVERRIDDEN + # The diff_detach and delete operations have to happen before + # create+attach+deploy for vrfs being created. This is to address + # cases where VLAN from a vrf which is being deleted is used for + # another vrf. Without this additional logic, the create+attach+deploy + # go out first and complain the VLAN is already in use. + self.diff_detach: list = [] + self.have_deploy: dict = {} + self.have_deploy_model: PayloadVrfsDeployments = None + self.want_deploy: dict = {} + self.want_deploy_model: PayloadVrfsDeployments = None + # A playbook configuration model representing what was changed + self.diff_deploy: dict = {} + self.diff_undeploy: dict = {} + self.diff_delete: dict = {} + self.diff_input_format: list = [] + self.query: list = [] + + self.inventory_data: dict = get_fabric_inventory_details(self.module, self.fabric) + self.ipv4_to_serial_number = InventoryIpv4ToSerialNumber() + self.ipv4_to_switch_role = InventoryIpv4ToSwitchRole() + self.serial_number_to_ipv4 = InventorySerialNumberToIpv4() + self.serial_number_to_switch_role = InventorySerialNumberToSwitchRole() + + self.ipv4_to_serial_number.fabric_inventory = self.inventory_data + self.ipv4_to_switch_role.fabric_inventory = self.inventory_data + self.serial_number_to_ipv4.fabric_inventory = self.inventory_data + self.serial_number_to_switch_role.fabric_inventory = self.inventory_data + + msg = "self.inventory_data: " + msg += f"{json.dumps(self.inventory_data, indent=4, sort_keys=True)}" + self.log.debug(msg) + + self.fabric_data: dict = get_fabric_details(self.module, self.fabric) + msg = "self.fabric_data: " + msg += f"{json.dumps(self.fabric_data, indent=4, sort_keys=True)}" + self.log.debug(msg) + + # self.fabric_data_model: ControllerResponseFabricsEasyFabricGet = ControllerResponseFabricsEasyFabricGet(**self.fabric_data) + # msg = "ZZZ: self.fabric_data_model: " + # msg += f"{json.dumps(self.fabric_data_model.model_dump(), indent=4, sort_keys=True)}" + # self.log.debug(msg) + + try: + self.fabric_type: str = self.fabric_data["fabricType"] + except KeyError: + msg = f"{self.class_name}.__init__(): " + msg += "'fabricType' parameter is missing from self.fabric_data." + self.module.fail_json(msg=msg) + + try: + self.sn_fab: dict = get_sn_fabric_dict(self.inventory_data) + except ValueError as error: + msg += f"{self.class_name}.__init__(): {error}" + module.fail_json(msg=msg) + + self.paths: dict = dcnm_vrf_paths + + self.result: dict[str, Any] = {"changed": False, "diff": [], "response": []} + + self.failed_to_rollback: bool = False + self.wait_time_for_delete_loop: Final[int] = 5 # in seconds + + self.vrf_lite_properties: Final[list[str]] = [ + "DOT1Q_ID", + "IF_NAME", + "IP_MASK", + "IPV6_MASK", + "IPV6_NEIGHBOR", + "NEIGHBOR_IP", + "PEER_VRF_NAME", + ] + + # Controller responses + self.response: dict = {} + self.log.debug("DONE") + + def set_model_enabled_true(self) -> None: + """ + Temporary method to set self.model_enabled to True. + + Will be removed once all methods are refactored to use Pydantic models. + """ + caller = inspect.stack()[1][3] + msg = f"{caller} Setting self.model_enabled to True." + self.log.debug(msg) + self.model_enabled = True + + def set_model_enabled_false(self) -> None: + """ + Temporary method to set self.model_enabled to False. + + Will be removed once all methods are refactored to use Pydantic models. + """ + caller = inspect.stack()[1][3] + msg = f"{caller} Setting self.model_enabled to False." + self.log.debug(msg) + self.model_enabled = False + + def log_list_of_models(self, model_list: list, by_alias: bool = False) -> None: + """ + # Summary + + Log a list of Pydantic models. + """ + caller = inspect.stack()[1][3] + for index, model in enumerate(model_list): + msg = f"caller: {caller}: by_alias={by_alias}, index {index}. " + msg += f"{json.dumps(model.model_dump(by_alias=by_alias), indent=4, sort_keys=True)}" + self.log.debug(msg) + + @staticmethod + def get_list_of_lists(lst: list, size: int) -> list[list]: + """ + # Summary + + Given a list of items (lst) and a chunk size (size), return a + list of lists, where each list is size items in length. + + ## Raises + + - ValueError if: + - lst is not a list. + - size is not an integer + + ## Example + + print(get_lists_of_lists([1,2,3,4,5,6,7], 3) + + # -> [[1, 2, 3], [4, 5, 6], [7]] + """ + if not isinstance(lst, list): + msg = "lst must be a list(). " + msg += f"Got {type(lst)}." + raise ValueError(msg) + if not isinstance(size, int): + msg = "size must be an integer. " + msg += f"Got {type(size)}." + raise ValueError(msg) + return [lst[x : x + size] for x in range(0, len(lst), size)] + + @staticmethod + def find_dict_in_list_by_key_value(search: Optional[list[dict[Any, Any]]], key: str, value: str) -> dict[Any, Any]: + """ + # Summary + + Find a dictionary in a list of dictionaries. + + + ## Raises + + None + + ## Parameters + + - search: A list of dict, or None + - key: The key to lookup in each dict + - value: The desired matching value for key + + ## Returns + + Either the first matching dict or an empty dict + + ## Usage + + ```python + content = [{"foo": "bar"}, {"foo": "baz"}] + + match = find_dict_in_list_by_key_value(search=content, key="foo", value="baz") + print(f"{match}") + # -> {"foo": "baz"} + + match = find_dict_in_list_by_key_value(search=content, key="foo", value="bingo") + print(f"{match}") + # -> {} + + match = find_dict_in_list_by_key_value(search=None, key="foo", value="bingo") + print(f"{match}") + # -> {} + ``` + """ + if search is None: + return {} + for item in search: + match = item.get(key) + if match == value: + return item + return {} + + def find_model_in_list_by_key_value(self, search: Optional[list], key: str, value: str) -> Any: + """ + # Summary + + Find a model in a list of models and return the matching model. + + + ## Raises + + None + + ## Parameters + + - search: A list of models, or None + - key: The key to lookup in each model + - value: The desired matching value for key + + ## Raises + + - None + + ## Returns + + Either the first matching model or None + """ + if search is None: + return None + msg = "ENTERED. " + msg += f"key: {key}, value: {value}. model_list: length {len(search)}." + self.log.debug(msg) + self.log_list_of_models(search, by_alias=False) + + for item in search: + try: + match = getattr(item, key) + except AttributeError: + return None + if match == value: + return item + return None + + # pylint: disable=inconsistent-return-statements + def to_bool(self, key: Any, dict_with_key: dict[Any, Any]) -> bool: + """ + # Summary + + Given a dictionary and key, access dictionary[key] and + try to convert the value therein to a boolean. + + - If the value is a boolean, return a like boolean. + - If the value is a boolean-like string (e.g. "false" + "True", etc), return the value converted to boolean. + + ## Raises + + - ValueError if the value is not convertable to boolean. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + value = dict_with_key.get(key) + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += f"key: {key}, " + msg += f"value: {value}" + self.log.debug(msg) + + result: bool = False + if value in ["false", "False", False]: + result = False + elif value in ["true", "True", True]: + result = True + else: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}: " + msg += f"key: {key}, " + msg += f"value ({str(value)}), " + msg += f"with type {type(value)} " + msg += "is not convertable to boolean." + self.log.debug(msg) + raise ValueError(msg) + return result + + # pylint: enable=inconsistent-return-statements + @staticmethod + def property_values_match(dict1: dict[Any, Any], dict2: dict[Any, Any], property_list: list) -> bool: + """ + Given two dictionaries and a list of keys: + + - Return True if all property values match. + - Return False otherwise + """ + for prop in property_list: + if dict1.get(prop) != dict2.get(prop): + return False + return True + + def get_next_fabric_vlan_id(self, fabric: str) -> int: + """ + # Summary + + Return the next available vlan_id for fabric. + + ## Raises + + - ValueError if: + - RESPONSE_CODE is not 200 + - Unable to retrieve next available vlan_id for fabric + + ## Notes + + - TODO: This method is not covered by unit tests. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + vlan_path = self.paths["GET_VLAN"].format(fabric) + args = SendToControllerArgs( + action="query", + path=vlan_path, + verb=RequestVerb.GET, + payload=None, + log_response=False, + is_rollback=False, + response_model=ControllerResponseGetIntV12, + ) + + self.send_to_controller(args) + try: + response = ControllerResponseGetIntV12(**self.response) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. Error parsing response: {error}. " + msg += f"Response: {json.dumps(self.response, indent=4, sort_keys=True)}" + raise ValueError(msg) from error + + if response.RETURN_CODE != 200: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}, " + msg += f"Failure retrieving autogenerated vlan_id for fabric {fabric}." + raise ValueError(msg) + + vlan_id = response.DATA + + msg = f"Returning vlan_id: {vlan_id} for fabric {fabric}" + self.log.debug(msg) + return vlan_id + + def get_next_fabric_vrf_id(self, fabric: str) -> int: + """ + # Summary + + Return the next available vrf_id for fabric. + + ## Raises + + - ValueError if: + - RESPONSE_CODE is not 200 + - Unable to retrieve next available vrf_id for fabric + + ## Notes + + - TODO: This method is not covered by unit tests. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + args = SendToControllerArgs( + action="query", + path=self.paths["GET_VRF_ID"].format(fabric), + verb=RequestVerb.GET, + payload=None, + log_response=False, + is_rollback=False, + response_model=ControllerResponseGetFabricsVrfinfoV12, + ) + + self.send_to_controller(args) + try: + response = ControllerResponseGetFabricsVrfinfoV12(**self.response) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. Error parsing response: {error}. " + msg += f"Response: {json.dumps(self.response, indent=4, sort_keys=True)}" + raise ValueError(msg) from error + + if response.RETURN_CODE != 200: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}, " + msg += f"Failure retrieving autogenerated vrf_id for fabric {fabric}." + raise ValueError(msg) + + vrf_id = response.DATA.l3_vni # pylint: disable=no-member + + msg = f"Returning vrf_id: {vrf_id} for fabric {fabric}" + self.log.debug(msg) + return vrf_id + + def diff_for_attach_deploy(self, want_attach_list: list[dict], have_lan_attach_list_models: list[HaveLanAttachItem], replace=False) -> tuple[list, bool]: + """ + Return attach_list, deploy_vrf + + Where: + - attach_list is a list of attachment differences + - deploy_vrf is a boolean + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = f"replace == {replace}" + self.log.debug(msg) + + attach_list = [] + deploy_vrf = False + + if not want_attach_list: + return attach_list, deploy_vrf + + for want_attach in want_attach_list: + if not have_lan_attach_list_models: + # No have_lan_attach, so always attach + if self.to_bool("isAttached", want_attach): + want_attach = self._prepare_attach_for_deploy(want_attach) + attach_list.append(want_attach) + if self.to_bool("is_deploy", want_attach): + deploy_vrf = True + continue + + found = False + for have_lan_attach_model in have_lan_attach_list_models: + if want_attach.get("serialNumber") != have_lan_attach_model.serial_number: + continue + + # Copy freeformConfig from have since the playbook doesn't + # currently support it. + want_attach.update({"freeformConfig": have_lan_attach_model.freeform_config}) + + # Copy unsupported instanceValues keys from have to want_attach + want_inst_values, have_inst_values = {}, {} + if want_attach.get("instanceValues") and have_lan_attach_model.instance_values: + want_inst_values = json.loads(want_attach["instanceValues"]) + have_inst_values = json.loads(have_lan_attach_model.instance_values) + # These keys are not currently supported in the playbook, + # so copy them from have to want. + for key in ["loopbackId", "loopbackIpAddress", "loopbackIpV6Address"]: + if key in have_inst_values: + want_inst_values[key] = have_inst_values[key] + want_attach["instanceValues"] = json.dumps(want_inst_values) + + # Compare extensionValues + want_extension_values = want_attach.get("extensionValues") + have_extension_values = have_lan_attach_model.extension_values + if want_extension_values and have_extension_values: + if not self._extension_values_match(want_extension_values, have_extension_values, replace): + continue + elif want_extension_values and not have_extension_values: + continue + elif not want_extension_values and have_extension_values: + if not replace: + found = True + continue + + # Compare deployment/attachment status + if not self._deployment_status_match(want_attach, have_lan_attach_model): + msg = "self._deployment_status_match() returned False." + self.log.debug(msg) + want_attach = self._prepare_attach_for_deploy(want_attach) + attach_list.append(want_attach) + if self.to_bool("is_deploy", want_attach): + deploy_vrf = True + found = True + break + + # Continue if instanceValues differ + if self.dict_values_differ(dict1=want_inst_values, dict2=have_inst_values): + continue + + found = True + break + + if not found: + if self.to_bool("isAttached", want_attach): + want_attach = self._prepare_attach_for_deploy(want_attach) + attach_list.append(want_attach) + if self.to_bool("is_deploy", want_attach): + deploy_vrf = True + + msg = f"Caller {caller}: Returning deploy_vrf: " + msg += f"{deploy_vrf}, " + msg += "attach_list: " + msg += f"{json.dumps(attach_list, indent=4, sort_keys=True)}" + self.log.debug(msg) + return attach_list, deploy_vrf + + def _prepare_attach_for_deploy(self, want: dict) -> dict: + """ + # Summary + + Prepare an attachment dictionary for deployment. + + - Removes the "isAttached" key if present. + - Sets the "deployment" key to True. + + ## Parameters + + - want: dict + The attachment dictionary to update. + + ## Returns + + - dict: The updated attachment dictionary. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if "isAttached" in want: + del want["isAttached"] + want["deployment"] = True + return want + + def _extension_values_match(self, want_extension_values: str, have_extension_values: str, replace: bool) -> bool: + """ + # Summary + + Compare the extensionValues of two attachment dictionaries to determine if they match. + + - Convert want and have from JSON strings to dictionaries. + - Parses and compares the VRF_LITE_CONN lists in both want and have. + - If replace is True, also checks that the lengths of the VRF_LITE_CONN lists are equal. + - Compares each interface (IF_NAME) and their properties. + + ## Parameters + + - want_extension_values: str + - The desired extensionValues, as a JSON string. + - have_extension_values: str + - The extensionValues on the controller, as a JSON string. + - replace: bool + - True if this is a replace/override operation. + - False otherwise. + + ## Returns + + - bool: True if the extension values match, False otherwise. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + want_extension_values = json.loads(want_extension_values) + have_extension_values = json.loads(have_extension_values) + + want_vrf_lite_conn = json.loads(want_extension_values["VRF_LITE_CONN"]) + have_vrf_lite_conn = json.loads(have_extension_values["VRF_LITE_CONN"]) + if replace and (len(want_vrf_lite_conn["VRF_LITE_CONN"]) != len(have_vrf_lite_conn["VRF_LITE_CONN"])): + return False + for want_vrf_lite in want_vrf_lite_conn["VRF_LITE_CONN"]: + for have_vrf_lite in have_vrf_lite_conn["VRF_LITE_CONN"]: + if want_vrf_lite["IF_NAME"] == have_vrf_lite["IF_NAME"]: + if self.property_values_match(want_vrf_lite, have_vrf_lite, self.vrf_lite_properties): + return True + return False + + def _deployment_status_match(self, want: dict, have_lan_attach_model: HaveLanAttachItem) -> bool: + """ + # Summary + + Compare the deployment and attachment status between two attachment dictionaries. + + - Checks if "isAttached", "deployment", and "is_deploy" keys are equal in both dictionaries. + + ## Parameters + + - want: dict + The desired attachment dictionary. + - have: dict + The current attachment dictionary from the controller. + + ## Returns + + - bool: True if all status flags match, False otherwise. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + # TODO: Remove this conversion once this method is updated to use lan_attach_list_model directly. + have = have_lan_attach_model.model_dump(by_alias=True) + + msg = f"type(want): {type(want)}, type(have): {type(have)}" + self.log.debug(msg) + msg = f"want: {json.dumps(want, indent=4, sort_keys=True)}" + self.log.debug(msg) + msg = f"have: {json.dumps(have, indent=4, sort_keys=True)}" + self.log.debug(msg) + try: + want_is_deploy = self.to_bool("is_deploy", want) + have_is_deploy = self.to_bool("is_deploy", have) + want_is_attached = self.to_bool("isAttached", want) + have_is_attached = self.to_bool("isAttached", have) + want_deployment = self.to_bool("deployment", want) + have_deployment = self.to_bool("deployment", have) + return want_is_attached == have_is_attached and want_deployment == have_deployment and want_is_deploy == have_is_deploy + except ValueError as error: + msg += f"caller: {caller}. " + msg += f"{error}. " + msg += "Returning False." + self.log.debug(msg) + return False + + def update_attach_params_extension_values(self, playbook_vrf_attach_model: PlaybookVrfAttachModel) -> dict: + """ + # Summary + + Given PlaybookVrfAttachModel return a dictionary of extension values that can be used in a payload: + + - Return a populated extension_values dictionary if the attachment object's vrf_lite parameter is is not null. + - Return an empty dictionary if the attachment object's vrf_lite parameter is null. + + ## Raises + + Calls fail_json() if the vrf_lite parameter is not null and the role of the switch in the playbook + attachment object is not one of the various border roles. + + ## Example PlaybookVrfAttachModel contents + + ```json + { + "deploy": true, + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "ip_address": "10.10.10.227", + "vrf_lite": [ + { + "dot1q": 2, + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv4": "10.33.0.1", + "neighbor_ipv6": "2010::10:34:0:3", + "peer_vrf": "test_vrf_1" + } + ] + } + ``` + ## Returns + + extension_values: a dictionary containing two keys whose values are JSON strings + + - "VRF_LITE_CONN": a list of dictionaries containing vrf_lite connection parameters + - Each dictionary contains the following + - "DOT1Q_ID": the dot1q ID as a string + - "IF_NAME": the interface name + - "IP_MASK": the IPv4 address and mask + - "IPV6_MASK": the IPv6 address and mask + - "IPV6_NEIGHBOR": the IPv6 neighbor address + - "NEIGHBOR_IP": the IPv4 neighbor address + - "PEER_VRF_NAME": the peer VRF name + - "VRF_LITE_JYTHON_TEMPLATE": the Jython template name for VRF Lite + - "MULTISITE_CONN": a JSON string containing an empty MULTISITE_CONN dictionary + + ```json + { + "MULTISITE_CONN": "{\"MULTISITE_CONN\": []}", + "VRF_LITE_CONN": "{\"VRF_LITE_CONN\": [{\"DOT1Q_ID\": \"2\", etc...}]}" + } + ``` + + ## TODO + + - We need to return a model instead of a dictionary with JSON strings. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if not playbook_vrf_attach_model.vrf_lite: + msg = "Early return. No vrf_lite extensions to process in playbook." + self.log.debug(msg) + return {} + + msg = "playbook_vrf_attach_model: " + msg += f"{json.dumps(playbook_vrf_attach_model.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + # Before applying the vrf_lite config, verify that the switch role begins with border + ip_address = playbook_vrf_attach_model.ip_address + switch_role = self.ipv4_to_switch_role.convert(ip_address) + if not re.search(r"\bborder\b", switch_role.lower()): + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "VRF LITE attachments are appropriate only for switches " + msg += "with Border roles e.g. Border Gateway, Border Spine, etc. " + msg += "The playbook and/or controller settings for switch " + msg += f"{ip_address} with role {switch_role} need review." + self.module.fail_json(msg=msg) + + msg = f"playbook_vrf_attach_model.vrf_lite: length: {len(playbook_vrf_attach_model.vrf_lite)}" + self.log.debug(msg) + self.log_list_of_models(playbook_vrf_attach_model.vrf_lite) + + extension_values: dict = {} + extension_values["VRF_LITE_CONN"] = [] + ms_con: dict = {} + ms_con["MULTISITE_CONN"] = [] + extension_values["MULTISITE_CONN"] = json.dumps(ms_con) + + for playbook_vrf_lite_model in playbook_vrf_attach_model.vrf_lite: + # If the playbook contains vrf lite parameters update the extension values. + vrf_lite_conn: dict = {} + for param in self.vrf_lite_properties: + vrf_lite_conn[param] = "" + + vrf_lite_conn["IF_NAME"] = playbook_vrf_lite_model.interface + vrf_lite_conn["DOT1Q_ID"] = playbook_vrf_lite_model.dot1q + vrf_lite_conn["IP_MASK"] = playbook_vrf_lite_model.ipv4_addr + vrf_lite_conn["NEIGHBOR_IP"] = playbook_vrf_lite_model.neighbor_ipv4 + vrf_lite_conn["IPV6_MASK"] = playbook_vrf_lite_model.ipv6_addr + vrf_lite_conn["IPV6_NEIGHBOR"] = playbook_vrf_lite_model.neighbor_ipv6 + vrf_lite_conn["PEER_VRF_NAME"] = playbook_vrf_lite_model.peer_vrf + vrf_lite_conn["VRF_LITE_JYTHON_TEMPLATE"] = "Ext_VRF_Lite_Jython" + + msg = "vrf_lite_conn: " + msg += f"{json.dumps(vrf_lite_conn, indent=4, sort_keys=True)}" + self.log.debug(msg) + + vrf_lite_connections: dict = {} + vrf_lite_connections["VRF_LITE_CONN"] = [] + vrf_lite_connections["VRF_LITE_CONN"].append(copy.deepcopy(vrf_lite_conn)) + + if extension_values["VRF_LITE_CONN"]: + extension_values["VRF_LITE_CONN"]["VRF_LITE_CONN"].extend(vrf_lite_connections["VRF_LITE_CONN"]) + else: + extension_values["VRF_LITE_CONN"] = copy.deepcopy(vrf_lite_connections) + + extension_values["VRF_LITE_CONN"] = json.dumps(extension_values["VRF_LITE_CONN"]) + + msg = "Returning extension_values: " + msg += f"{json.dumps(extension_values, indent=4, sort_keys=True)}" + self.log.debug(msg) + + return copy.deepcopy(extension_values) + + def transmute_attach_params_to_payload(self, vrf_attach_model: PlaybookVrfAttachModel, vrf_name: str, deploy: bool, vlan_id: int) -> dict: + """ + # Summary + + Turn playbook vrf attachment config (PlaybookVrfAttachModel) into an attachment payload for the controller. + + ## Raises + + Calls fail_json() if: + + - The switch in the attachment object is a spine + - If the vrf_lite object is not null, and the switch is not + a border switch + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + # TODO: Remove this once the method is refactored to use Pydantic models. + attach = vrf_attach_model.model_dump() + if not vrf_attach_model: + msg = "Early return. No attachments to process." + self.log.debug(msg) + return {} + + # dcnm_get_ip_addr_info converts serial_numbers, hostnames, etc, to ip addresses. + ip_address = dcnm_get_ip_addr_info(self.module, vrf_attach_model.ip_address, None, None) + serial_number = self.ipv4_to_serial_number.convert(vrf_attach_model.ip_address) + + vrf_attach_model.ip_address = ip_address + + msg = f"ip_address: {ip_address}, " + msg += f"serial_number: {serial_number}, " + msg += "vrf_attach_model: " + msg += f"{json.dumps(vrf_attach_model.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not serial_number: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"Fabric {self.fabric} does not contain switch " + msg += f"{ip_address} ({serial_number})." + self.module.fail_json(msg=msg) + + role = self.inventory_data[vrf_attach_model.ip_address].get("switchRole") + + msg = f"ZZZ: role: {role}, " + self.log.debug(msg) + + if role.lower() in ("spine", "super spine"): + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "VRF attachments are not appropriate for " + msg += "switches with Spine or Super Spine roles. " + msg += "The playbook and/or controller settings for switch " + msg += f"{ip_address} with role {role} need review." + self.module.fail_json(msg=msg) + + extension_values = self.update_attach_params_extension_values(playbook_vrf_attach_model=vrf_attach_model) + if extension_values: + attach.update({"extensionValues": json.dumps(extension_values).replace(" ", "")}) + else: + attach.update({"extensionValues": ""}) + + attach.update({"fabric": self.fabric}) + attach.update({"vrfName": vrf_name}) + attach.update({"vlan": vlan_id}) + # This flag is not to be confused for deploy of attachment. + # "deployment" should be set to True for attaching an attachment + # and set to False for detaching an attachment + attach.update({"deployment": True}) + attach.update({"isAttached": True}) + attach.update({"serialNumber": serial_number}) + attach.update({"is_deploy": deploy}) + + # freeformConfig, loopbackId, loopbackIpAddress, and + # loopbackIpV6Address will be copied from have + attach.update({"freeformConfig": ""}) + inst_values = { + "loopbackId": "", + "loopbackIpAddress": "", + "loopbackIpV6Address": "", + } + inst_values.update( + { + "switchRouteTargetImportEvpn": attach.get("import_evpn_rt"), + "switchRouteTargetExportEvpn": attach.get("export_evpn_rt"), + } + ) + attach.update({"instanceValues": json.dumps(inst_values).replace(" ", "")}) + + attach.pop("deploy", None) + attach.pop("ip_address", None) + + msg = "Returning attach: " + msg += f"{json.dumps(attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + return copy.deepcopy(attach) + + def dict_values_differ(self, dict1: dict, dict2: dict, skip_keys=None) -> bool: + """ + # Summary + + Given two dictionaries and, optionally, a list of keys to skip: + + - Return True if the values for any (non-skipped) keys differs. + - Return False otherwise + + ## Raises + + - ValueError if dict1 or dict2 is not a dictionary + - ValueError if skip_keys is not a list + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if skip_keys is None: + skip_keys = [] + + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + if not isinstance(skip_keys, list): + msg += "skip_keys must be a list. " + msg += f"Got {type(skip_keys)}." + raise ValueError(msg) + if not isinstance(dict1, dict): + msg += "dict1 must be a dict. " + msg += f"Got {type(dict1)}." + raise ValueError(msg) + if not isinstance(dict2, dict): + msg += "dict2 must be a dict. " + msg += f"Got {type(dict2)}." + raise ValueError(msg) + + for key in dict1.keys(): + if key in skip_keys: + continue + dict1_value = str(dict1.get(key)).lower() + dict2_value = str(dict2.get(key)).lower() + # Treat None and "" as equal + if dict1_value in (None, "none", ""): + dict1_value = "none" + if dict2_value in (None, "none", ""): + dict2_value = "none" + if dict1_value != dict2_value: + msg = f"Values differ: key {key} " + msg += f"dict1_value {dict1_value}, type {type(dict1_value)} != " + msg += f"dict2_value {dict2_value}, type {type(dict2_value)}. " + msg += "returning True" + self.log.debug(msg) + return True + msg = "All dict values are equal. Returning False." + self.log.debug(msg) + return False + + def diff_for_create(self, want, have) -> tuple[dict, bool]: + """ + # Summary + + Given a want and have object, return a tuple of + (create, configuration_changed) where: + - create is a dictionary of parameters to send to the + controller + - configuration_changed is a boolean indicating if + the configuration has changed + - If the configuration has not changed, return an empty + dictionary for create and False for configuration_changed + - If the configuration has changed, return a dictionary + of parameters to send to the controller and True for + configuration_changed + - If the configuration has changed, but the vrfId is + None, return an empty dictionary for create and True + for configuration_changed + + ## Raises + + - Calls fail_json if the vrfId is not None and the vrfId + in the want object is not equal to the vrfId in the + have object. + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + configuration_changed = False + if not have: + return {}, configuration_changed + + create = {} + + json_to_dict_want = json.loads(want["vrfTemplateConfig"]) + json_to_dict_have = json.loads(have["vrfTemplateConfig"]) + + # vlan_id_want drives the conditional below, so we cannot + # remove it here (as we did with the other params that are + # compared in the call to self.dict_values_differ()) + vlan_id_want = str(json_to_dict_want.get("vrfVlanId", "")) + + skip_keys = [] + if vlan_id_want == "0": + skip_keys = ["vrfVlanId"] + try: + templates_differ = self.dict_values_differ(dict1=json_to_dict_want, dict2=json_to_dict_have, skip_keys=skip_keys) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"templates_differ: {error}" + self.module.fail_json(msg=msg) + + msg = f"templates_differ: {templates_differ}, " + msg += f"vlan_id_want: {vlan_id_want}" + self.log.debug(msg) + + if want.get("vrfId") is not None and have.get("vrfId") != want.get("vrfId"): + msg = f"{self.class_name}.{method_name}: " + msg += f"vrf_id for vrf {want['vrfName']} cannot be updated to " + msg += "a different value" + self.module.fail_json(msg=msg) + + if templates_differ: + configuration_changed = True + if want.get("vrfId") is None: + # The vrf updates with missing vrfId will have to use existing + # vrfId from the instance of the same vrf on DCNM. + want["vrfId"] = have["vrfId"] + create = want + + msg = f"returning configuration_changed: {configuration_changed}, " + msg += f"create: {create}" + self.log.debug(msg) + + return create, configuration_changed + + def transmute_playbook_model_to_vrf_create_payload_model(self, vrf_playbook_model: PlaybookVrfModelV12) -> VrfPayloadV12: + """ + # Summary + + Given an instance of PlaybookVrfModelV12, return an instance of VrfPayloadV12 + suitable for sending to the controller. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if not vrf_playbook_model: + return vrf_playbook_model + + msg = "vrf_playbook_model (PlaybookVrfModelV12): " + msg += f"{json.dumps(vrf_playbook_model.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + # Transmute PlaybookVrfModelV12 into a vrf_template_config dictionary + validated_template_config = VrfTemplateConfigV12.model_validate(vrf_playbook_model.model_dump()) + vrf_template_config_dict = validated_template_config.model_dump_json(by_alias=True) + + # Tramsmute PlaybookVrfModelV12 into VrfPayloadV12 + vrf_payload_v12 = VrfPayloadV12( + fabric=self.fabric, + service_vrf_template=vrf_playbook_model.service_vrf_template or "", + source=None, + vrf_id=vrf_playbook_model.vrf_id or 0, + vrf_name=vrf_playbook_model.vrf_name, + vrf_extension_template=vrf_playbook_model.vrf_extension_template or "Default_VRF_Extension_Universal", + vrf_template=vrf_playbook_model.vrf_template or "Default_VRF_Universal", + vrf_template_config=vrf_template_config_dict, + ) + + msg = "Returning VrfPayloadV12 instance: " + msg += f"{json.dumps(vrf_payload_v12.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + return vrf_payload_v12 + + def update_create_params(self, vrf: dict) -> dict: + """ + # Summary + + Given a vrf dictionary from a playbook, return a VRF payload suitable + for sending to the controller. + + Translate playbook keys into keys expected by the controller. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if not vrf: + return vrf + + msg = "type(vrf): " + msg += f"{type(vrf)}) " + msg += f"vrf: {json.dumps(vrf, indent=4, sort_keys=True)}" + self.log.debug(msg) + + validated_template_config = VrfTemplateConfigV12.model_validate(vrf) + template = validated_template_config.model_dump_json(by_alias=True) + + vrf_upd = { + "fabric": self.fabric, + "vrfName": vrf["vrf_name"], + "vrfTemplate": vrf.get("vrf_template", "Default_VRF_Universal"), + "vrfExtensionTemplate": vrf.get("vrf_extension_template", "Default_VRF_Extension_Universal"), + "vrfId": vrf.get("vrf_id", None), # vrf_id will be auto generated in get_diff_merge() + "serviceVrfTemplate": vrf.get("service_vrf_template", ""), + "source": None, + "vrfTemplateConfig": template, + } + + msg = f"Returning vrf_upd: {json.dumps(vrf_upd, indent=4, sort_keys=True)}" + self.log.debug(msg) + return vrf_upd + + def get_controller_vrf_object_models(self) -> list[VrfObjectV12]: + """ + # Summary + + Retrieve all VRF objects from the controller + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + endpoint = EpVrfGet() + endpoint.fabric_name = self.fabric + + controller_response = dcnm_send(self.module, endpoint.verb.value, endpoint.path) + + if controller_response is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{caller}: Unable to retrieve endpoint. " + msg += f"verb {endpoint.verb.value} path {endpoint.path}" + raise ValueError(msg) + + if isinstance(controller_response, Union[dict, list]): # Avoid json.dumps(MagicMock) during unit tests + msg = "controller_response: " + msg += f"{json.dumps(controller_response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + validated_response = ControllerResponseVrfsV12(**controller_response) + + msg = "validated_response (ControllerResponseVrfsV12): " + msg += f"{json.dumps(validated_response.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + missing_fabric, not_ok = self.handle_response(validated_response, "query") + + if missing_fabric or not_ok: + msg0 = f"caller: {caller}. " + msg1 = f"{msg0} Fabric {self.fabric} not present on the controller" + msg2 = f"{msg0} Unable to find vrfs under fabric: {self.fabric}" + self.module.fail_json(msg=msg1 if missing_fabric else msg2) + + return validated_response.DATA + + def get_list_of_vrfs_switches_data_item_model(self, attach: dict) -> list[ControllerResponseVrfsSwitchesDataItem]: + """ + # Summary + + Retrieve the IP/Interface that is connected to the switch with serial_number + + attach must contain at least the following keys: + + - fabric: The fabric to search + - serialNumber: The serial_number of the switch + - vrfName: The vrf to search + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = f"attach: {json.dumps(attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + verb = "GET" + path = self.paths["GET_VRF_SWITCH"].format(attach["fabric"], attach["vrfName"], attach["serialNumber"]) + msg = f"verb: {verb}, path: {path}" + self.log.debug(msg) + controller_response = dcnm_send(self.module, verb, path) + + if controller_response is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{caller}: Unable to retrieve lite_objects." + raise ValueError(msg) + + msg = "controller_response: " + msg += f"{json.dumps(controller_response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + try: + validated_response = ControllerResponseVrfsSwitchesV12(**controller_response) + except ValidationError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{caller}: Unable to parse response: {error}" + raise ValueError(msg) from error + + msg = "validated_response (ControllerResponseVrfsSwitchesV12): " + msg += f"{json.dumps(validated_response.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"Returning list of VrfSwitchesDataItem. length {len(validated_response.DATA)}." + self.log.debug(msg) + self.log_list_of_models(validated_response.DATA) + + return validated_response.DATA + + def get_list_of_vrfs_switches_data_item_model_new( + self, lan_attach_item: PayloadVrfsAttachmentsLanAttachListItem + ) -> list[ControllerResponseVrfsSwitchesDataItem]: + """ + # Summary + + Will replace get_list_of_vrfs_switches_data_item_model() in the future. + Retrieve the IP/Interface that is connected to the switch with serial_number + + PayloadVrfsAttachmentsLanAttachListItem must contain at least the following fields: + + - fabric: The fabric to search + - serial_number: The serial_number of the switch + - vrf_name: The vrf to search + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = f"lan_attach_item: {json.dumps(lan_attach_item.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + + verb = "GET" + path = self.paths["GET_VRF_SWITCH"].format(lan_attach_item.fabric, lan_attach_item.vrf_name, lan_attach_item.serial_number) + msg = f"verb: {verb}, path: {path}" + self.log.debug(msg) + controller_response = dcnm_send(self.module, verb, path) + + if controller_response is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{caller}: Unable to retrieve lite_objects." + raise ValueError(msg) + + msg = "controller_response: " + msg += f"{json.dumps(controller_response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + try: + validated_response = ControllerResponseVrfsSwitchesV12(**controller_response) + except ValidationError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{caller}: Unable to parse response: {error}" + raise ValueError(msg) from error + + msg = "validated_response (ControllerResponseVrfsSwitchesV12): " + msg += f"{json.dumps(validated_response.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"Returning list of VrfSwitchesDataItem. length {len(validated_response.DATA)}." + self.log.debug(msg) + self.log_list_of_models(validated_response.DATA) + + return validated_response.DATA + + def populate_have_create(self, vrf_object_models: list[VrfObjectV12]) -> None: + """ + # Summary + + Given a list of VrfObjectV12 models, populate self.have_create, + which is a list of VRF dictionaries used later to generate payloads + to send to the controller (e.g. diff_create, diff_create_update). + + - Remove vrfStatus + - Convert vrfTemplateConfig to a JSON string + + ## Raises + + None + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + have_create = [] + for vrf in vrf_object_models: + vrf_template_config = self.update_vrf_template_config_from_vrf_model(vrf) + vrf_dict = vrf.model_dump(by_alias=True) + vrf_dict["vrfTemplateConfig"] = vrf_template_config.model_dump_json(by_alias=True) + vrf_dict.pop("vrfStatus", None) + have_create.append(vrf_dict) + + self.have_create = copy.deepcopy(have_create) + msg = "self.have_create: " + msg += f"{json.dumps(self.have_create, indent=4, sort_keys=True)}" + self.log.debug(msg) + + def populate_have_deploy(self, get_vrf_attach_response: dict) -> dict: + """ + Return have_deploy, which is a dict representation of VRFs currently deployed on the controller. + + Use get_vrf_attach_response dict to populate have_deploy. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + vrfs_to_update: set[str] = set() + + for vrf_attach in get_vrf_attach_response.get("DATA", []): + if not vrf_attach.get("lanAttachList"): + continue + attach_list = vrf_attach["lanAttachList"] + for attach in attach_list: + deploy = attach.get("isLanAttached") + deployed = not (deploy and attach.get("lanAttachState") in ("OUT-OF-SYNC", "PENDING")) + if deployed: + vrf_to_deploy = attach.get("vrfName") + if vrf_to_deploy: + vrfs_to_update.add(vrf_to_deploy) + + have_deploy = {} + have_deploy["vrfNames"] = ",".join(vrfs_to_update) + + msg = "Returning have_deploy: " + msg += f"{json.dumps(have_deploy, indent=4)}" + self.log.debug(msg) + + return copy.deepcopy(have_deploy) + + def populate_have_deploy_model(self, vrf_attach_responses: list[ControllerResponseVrfsAttachmentsDataItem]) -> PayloadVrfsDeployments: + """ + Return PayloadVrfsDeployments, which is a model representation of VRFs currently deployed on the controller. + + Uses vrf_attach_responses (list[ControllerResponseVrfsAttachmentsDataItem]) to populate PayloadVrfsDeployments. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + vrfs_to_update: set[str] = set() + + for vrf_attach_model in vrf_attach_responses: + if not vrf_attach_model.lan_attach_list: + continue + attach_list_models = vrf_attach_model.lan_attach_list + for lan_attach_model in attach_list_models: + deploy = lan_attach_model.is_lan_attached + deployed = not (deploy and lan_attach_model.lan_attach_state in ("OUT-OF-SYNC", "PENDING")) + if deployed: + vrf_to_deploy = lan_attach_model.vrf_name + if vrf_to_deploy: + vrfs_to_update.add(vrf_to_deploy) + + have_deploy_model = PayloadVrfsDeployments(vrf_names=vrfs_to_update) + + msg = "Returning have_deploy_model: " + msg += f"{json.dumps(have_deploy_model.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + return have_deploy_model + + def populate_have_attach_models(self, vrf_attach_models: list[ControllerResponseVrfsAttachmentsDataItem]) -> None: + """ + Populate the following using vrf_attach_models (list[ControllerResponseVrfsAttachmentsDataItem]): + + - self.have_attach + - self.have_attach_models + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = f"vrf_attach_models.PRE_UPDATE: length: {len(vrf_attach_models)}." + self.log.debug(msg) + self.log_list_of_models(vrf_attach_models) + + updated_vrf_attach_models: list[HaveAttachPostMutate] = [] + for vrf_attach_model in vrf_attach_models: + if not vrf_attach_model.lan_attach_list: + continue + new_attach_list: list[HaveLanAttachItem] = [] + for lan_attach_item in vrf_attach_model.lan_attach_list: + msg = "lan_attach_item: " + msg += f"{json.dumps(lan_attach_item.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + # Mutate attachment + new_attach_dict = { + "deployment": lan_attach_item.is_lan_attached, + "extensionValues": "", + "fabricName": self.fabric, + "instanceValues": lan_attach_item.instance_values, + "isAttached": lan_attach_item.lan_attach_state != "NA", + "is_deploy": not (lan_attach_item.is_lan_attached and lan_attach_item.lan_attach_state in ("OUT-OF-SYNC", "PENDING")), + "serialNumber": lan_attach_item.switch_serial_no, + "vlanId": lan_attach_item.vlan_id, + "vrfName": lan_attach_item.vrf_name, + } + + new_lan_attach_item = HaveLanAttachItem(**new_attach_dict) + msg = "new_lan_attach_item: " + msg += f"{json.dumps(new_lan_attach_item.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + + new_attach = self._update_vrf_lite_extension_model(new_lan_attach_item) + + msg = "new_attach: " + msg += f"{json.dumps(new_attach.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + + new_attach_list.append(new_attach) + + msg = f"new_attach_list: length: {len(new_attach_list)}." + self.log.debug(msg) + self.log_list_of_models(new_attach_list) + + new_attach_dict = { + "lanAttachList": new_attach_list, + "vrfName": vrf_attach_model.vrf_name, + } + new_vrf_attach_model = HaveAttachPostMutate(**new_attach_dict) + new_vrf_attach_model.lan_attach_list = new_attach_list + updated_vrf_attach_models.append(new_vrf_attach_model) + + msg = f"updated_vrf_attach_models: length: {len(updated_vrf_attach_models)}." + self.log.debug(msg) + self.log_list_of_models(updated_vrf_attach_models) + + updated_vrf_attach_models_dicts = [model.model_dump(by_alias=True) for model in updated_vrf_attach_models] + + self.have_attach = copy.deepcopy(updated_vrf_attach_models_dicts) + self.have_attach_models = updated_vrf_attach_models + msg = f"self.have_attach_models.POST_UPDATE: length: {len(self.have_attach_models)}." + self.log.debug(msg) + self.log_list_of_models(self.have_attach_models) + + def _update_vrf_lite_extension_model(self, attach: HaveLanAttachItem) -> HaveLanAttachItem: + """ + # Summary + + - Return updated attach model with VRF Lite extension values if present. + - Update freeformConfig, if present, else set to an empty string. + + ## Raises + + - None + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = "attach: " + msg += f"{json.dumps(attach.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + + params = { + "fabric": attach.fabric, + "serialNumber": attach.serial_number, + "vrfName": attach.vrf_name, + } + lite_objects = self.get_list_of_vrfs_switches_data_item_model(params) + if not lite_objects: + msg = "No vrf_lite_objects found. Update freeformConfig and return." + self.log.debug(msg) + attach.freeform_config = "" + return attach + + msg = f"lite_objects: length {len(lite_objects)}." + self.log.debug(msg) + self.log_list_of_models(lite_objects) + + for sdl in lite_objects: + for epv in sdl.switch_details_list: + if not epv.extension_values: + attach.freeform_config = "" + continue + ext_values = epv.extension_values + # if ext_values.vrf_lite_conn is None: + if ext_values.vrf_lite_conn.vrf_lite_conn[0].auto_vrf_lite_flag == "NA": + # The default value assigned in the model is "NA". If we see this value + # we know that the switch did not contibute to the model values. + continue + ext_values = ext_values.vrf_lite_conn + extension_values = {"VRF_LITE_CONN": {"VRF_LITE_CONN": []}} + for vrf_lite_conn_model in ext_values.vrf_lite_conn: + ev_dict = copy.deepcopy(vrf_lite_conn_model.model_dump(by_alias=True)) + ev_dict.update({"AUTO_VRF_LITE_FLAG": vrf_lite_conn_model.auto_vrf_lite_flag or "false"}) + ev_dict.update({"VRF_LITE_JYTHON_TEMPLATE": "Ext_VRF_Lite_Jython"}) + extension_values["VRF_LITE_CONN"]["VRF_LITE_CONN"].append(ev_dict) + extension_values["VRF_LITE_CONN"] = json.dumps(extension_values["VRF_LITE_CONN"]) + ms_con = {"MULTISITE_CONN": []} + extension_values["MULTISITE_CONN"] = json.dumps(ms_con) + attach.extension_values = json.dumps(extension_values).replace(" ", "") + attach.freeform_config = epv.freeform_config or "" + return attach + + def get_have(self) -> None: + """ + # Summary + + Retrieve all VRF objects and attachment objects from the + controller. Update the following with this information: + + - self.have_create, see populate_have_create() + - self.have_attach_models, see populate_have_attach_models() + - self.have_deploy, see populate_have_deploy() + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + validated_vrf_object_models = self.get_controller_vrf_object_models() + + msg = f"validated_vrf_object_models: length {len(validated_vrf_object_models)}." + self.log.debug(msg) + self.log_list_of_models(validated_vrf_object_models) + + if not validated_vrf_object_models: + return + + self.populate_have_create(validated_vrf_object_models) + + current_vrfs_set = {vrf.vrfName for vrf in validated_vrf_object_models} + controller_response = get_endpoint_with_long_query_string( + module=self.module, + fabric_name=self.fabric, + path=self.paths["GET_VRF_ATTACH"], + query_string_items=",".join(current_vrfs_set), + caller=f"{self.class_name}.{method_name}", + ) + + if controller_response is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}: unable to set controller_response." + raise ValueError(msg) + + msg = "controller_response: " + msg += f"{json.dumps(controller_response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + validated_controller_response = ControllerResponseVrfsAttachmentsV12(**controller_response) + + msg = "validated_controller_response: " + msg += f"{json.dumps(validated_controller_response.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not validated_controller_response.DATA: + return + + self.have_deploy = self.populate_have_deploy(controller_response) + self.have_deploy_model = self.populate_have_deploy_model(validated_controller_response.DATA) + msg = "self.have_deploy_model (by_alias=True): " + msg += f"{json.dumps(self.have_deploy_model.model_dump(by_alias=True), indent=4, sort_keys=True)}" + self.log.debug(msg) + + self.populate_have_attach_models(validated_controller_response.DATA) + + def get_want_attach(self) -> None: + """ + Populate self.want_attach from self.validated_playbook_config. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + want_attach: list[dict[str, Any]] = [] + + for validated_playbook_config_model in self.validated_playbook_config_models: + vrf_name: str = validated_playbook_config_model.vrf_name + vrf_attach: dict[Any, Any] = {} + vrf_attach_payloads: list[dict[Any, Any]] = [] + + vrf_deploy: bool = validated_playbook_config_model.deploy or False + vlan_id: int = validated_playbook_config_model.vlan_id or 0 + + if not validated_playbook_config_model.attach: + msg = f"No attachments for vrf {vrf_name}. Skipping." + self.log.debug(msg) + continue + for vrf_attach_model in validated_playbook_config_model.attach: + deploy = vrf_deploy + vrf_attach_payloads.append(self.transmute_attach_params_to_payload(vrf_attach_model, vrf_name, deploy, vlan_id)) + + if vrf_attach_payloads: + vrf_attach.update({"vrfName": vrf_name}) + vrf_attach.update({"lanAttachList": vrf_attach_payloads}) + want_attach.append(vrf_attach) + + self.want_attach = copy.deepcopy(want_attach) + msg = "self.want_attach: " + msg += f"{json.dumps(self.want_attach, indent=4)}" + self.log.debug(msg) + + self.build_want_attach_vrf_lite() + + def build_want_attach_vrf_lite(self) -> None: + """ + From self.validated_playbook_config_models, build a dictionary, keyed on switch serial_number, + containing a list of VrfLiteModel. + + ## Example structure + + ```json + { + "XYZKSJHSMK4": [ + VrfLiteModel( + dot1q=21, + interface="Ethernet1/1", + ipv4_addr="10.33.0.11/30", + ipv6_addr="2010::10:34:0:1/64", + neighbor_ipv4="10.33.0.12", + neighbor_ipv6="2010::10:34:0:1", + peer_vrf="test_vrf_1" + ) + ] + } + ``` + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if not self.validated_playbook_config_models: + msg = "Early return. No validated VRFs found." + self.log.debug(msg) + return + vrf_config_models_with_attachments = [model for model in self.validated_playbook_config_models if model.attach] + if not vrf_config_models_with_attachments: + msg = "Early return. No playbook configs containing VRF attachments found." + self.log.debug(msg) + return + + for model in vrf_config_models_with_attachments: + for attachment in model.attach: + if not attachment.vrf_lite: + msg = f"switch {attachment.ip_address} VRF attachment does not contain vrf_lite. Skipping." + self.log.debug(msg) + continue + ip_address = attachment.ip_address + self.want_attach_vrf_lite.update({self.ipv4_to_serial_number.convert(ip_address): attachment.vrf_lite}) + + msg = f"self.want_attach_vrf_lite: length: {len(self.want_attach_vrf_lite)}." + self.log.debug(msg) + for serial_number, vrf_lite_list in self.want_attach_vrf_lite.items(): + msg = f"serial_number {serial_number}: -> {json.dumps([model.model_dump(by_alias=True) for model in vrf_lite_list], indent=4, sort_keys=True)}" + self.log.debug(msg) + + def populate_want_create_payload_models(self) -> None: + """ + Populate self.want_create_payload_models from self.validated_playbook_config_models. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + want_create_payload_models: list[VrfPayloadV12] = [] + + for playbook_config_model in self.validated_playbook_config_models: + want_create_payload_models.append(self.transmute_playbook_model_to_vrf_create_payload_model(playbook_config_model)) + + self.want_create_payload_models = want_create_payload_models + msg = f"self.want_create_payload_models: length: {len(self.want_create_payload_models)}." + self.log.debug(msg) + self.log_list_of_models(self.want_create_payload_models) + + def get_want_create(self) -> None: + """ + Populate self.want_create from self.validated_playbook_config. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + want_create: list[dict[str, Any]] = [] + + for vrf in self.validated_playbook_config: + want_create.append(self.update_create_params(vrf=vrf)) + + self.want_create = copy.deepcopy(want_create) + msg = "self.want_create: " + msg += f"{json.dumps(self.want_create, indent=4)}" + self.log.debug(msg) + + def get_want_deploy(self) -> None: + """ + Populate self.want_deploy from self.validated_playbook_config. + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + want_deploy: dict[str, Any] = {} + all_vrfs: set = set() + + for vrf in self.validated_playbook_config: + try: + vrf_name: str = vrf["vrf_name"] + except KeyError: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"vrf missing mandatory key vrf_name: {vrf}" + self.module.fail_json(msg=msg) + all_vrfs.add(vrf_name) + + if len(all_vrfs) != 0: + vrf_names = ",".join(all_vrfs) + want_deploy.update({"vrfNames": vrf_names}) + + self.want_deploy = copy.deepcopy(want_deploy) + msg = "self.want_deploy: " + msg += f"{json.dumps(self.want_deploy, indent=4)}" + self.log.debug(msg) + + def get_want(self) -> None: + """ + Parse the playbook config and populate: + - self.want_attach, see get_want_attach() + - self.want_create, see get_want_create() (to be replaced by self.want_create_payload_models) + - self.want_create_payload_models, see populate_want_create_payload_models() + - self.want_deploy, see get_want_deploy() + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + # We're populating both self.want_create and self.want_create_payload_models + # so that we can gradually replace self.want_create, one method at + # a time. + self.get_want_create() + self.populate_want_create_payload_models() + self.get_want_attach() + self.get_want_deploy() + + def get_items_to_detach(self, attach_list: list[dict]) -> list[dict]: + """ + # Summary + + Given a list of attachment objects, return a list of + attachment objects that are to be detached. + + This is done by checking for the presence of the + "isAttached" key in the attachment object and + checking if the value is True. + + If the "isAttached" key is present and True, it + indicates that the attachment is attached to a + VRF and needs to be detached. In this case, + remove the "isAttached" key and set the + "deployment" key to False. + + The modified attachment object is added to the + detach_list. + + Finally, return the detach_list. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + detach_list = [] + for item in attach_list: + if "isAttached" not in item: + continue + if item["isAttached"]: + del item["isAttached"] + item.update({"deployment": False}) + detach_list.append(item) + return detach_list + + def get_items_to_detach_model(self, attach_list: list[HaveLanAttachItem]) -> Union[VrfDetachPayloadV12, None]: + """ + # Summary + + Given a list of HaveLanAttachItem objects, return a list of + VrfDetachPayloadV12 models, or None if no items are to be detached. + + This is done by checking if the isAttached field in each + HaveLanAttachItem is True. + + If HaveLanAttachItem.isAttached field is True, it indicates that the + attachment is attached to a VRF and needs to be detached. In this case, + mutate the HaveLanAttachItem to a LanDetachListItemV12 which will: + + - Remove the isAttached field + - Set the deployment field to False + + The LanDetachListItemV12 is added to VrfDetachPayloadV12.lan_attach_list. + + ## Raises + + - fail_json if the vrf_name is not found in lan_detach_items + - fail_json if multiple different vrf_names are found in lan_detach_items + + ## Returns + + - A VrfDetachPayloadV12 model containing the list of LanDetachListItemV12 objects. + - None, if no items are to be detached. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + lan_detach_items: list[LanDetachListItemV12] = [] + + msg = f"attach_list: length {len(attach_list)}." + self.log.debug(msg) + self.log_list_of_models(attach_list) + + for have_lan_attach_item in attach_list: + if not have_lan_attach_item.is_attached: + continue + msg = "have_lan_attach_item: " + msg += f"{json.dumps(have_lan_attach_item.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "Mutating HaveLanAttachItem to LanDetachListItemV12." + self.log.debug(msg) + lan_detach_item = LanDetachListItemV12( + deployment=False, + extensionValues=have_lan_attach_item.extension_values, + fabric=have_lan_attach_item.fabric, + freeformConfig=have_lan_attach_item.freeform_config, + instanceValues=have_lan_attach_item.instance_values, + is_deploy=have_lan_attach_item.is_deploy, + serialNumber=have_lan_attach_item.serial_number, + vlanId=have_lan_attach_item.vlan, + vrfName=have_lan_attach_item.vrf_name, + ) + msg = "Mutating HaveLanAttachItem to LanDetachListItemV12. DONE." + self.log.debug(msg) + + vrf_name = have_lan_attach_item.vrf_name + lan_detach_items.append(lan_detach_item) + + if not lan_detach_items: + msg = "No items to detach found in attach_list. Returning None." + self.log.debug(msg) + return None + + msg = "Creating VrfDetachPayloadV12 model." + self.log.debug(msg) + + vrf_name = lan_detach_items[0].vrf_name if lan_detach_items else "" + if not vrf_name: + msg = "vrf_name not found in lan_detach_items. Cannot create VrfDetachPayloadV12 model." + self.module.fail_json(msg=msg) + if len(set(item.vrf_name for item in lan_detach_items)) > 1: + msg = "Multiple VRF names found in lan_detach_items. Cannot create VrfDetachPayloadV12 model." + self.module.fail_json(msg=msg) + + msg = f"lan_detach_items for VrfDetachPayloadV12: length {len(lan_detach_items)}." + self.log.debug(msg) + self.log_list_of_models(lan_detach_items) + + detach_list_model = VrfDetachPayloadV12( + lanAttachList=lan_detach_items, + vrfName=vrf_name, + ) + + msg = "Creating VrfDetachPayloadV12 model. DONE." + self.log.debug(msg) + + msg = f"Returning detach_list_model: length(lan_attach_list): {len(detach_list_model.lan_attach_list)}." + self.log.debug(msg) + msg = f"{json.dumps(detach_list_model.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + return detach_list_model + + # TODO: rename to populate_diff_delete_model after testing + def get_diff_delete(self) -> None: + """ + # Summary + + Called from modules/dcnm_vrf.py + + Using self.have_create, and self.have_attach_models, update the following: + + - diff_detach: a list of attachment objects to detach + - diff_undeploy: a dictionary of vrf names to undeploy + - diff_delete: a dictionary of vrf names to delete + """ + caller = inspect.stack()[1][3] + + self.set_model_enabled_true() + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if self.config: + self._get_diff_delete_with_config_model() + else: + self._get_diff_delete_without_config_model() + + msg = f"self.diff_detach: length: {len(self.diff_detach)}." + self.log.debug(msg) + self.log_list_of_models(self.diff_detach, by_alias=False) + + msg = "self.diff_undeploy: " + msg += f"{json.dumps(self.diff_undeploy, indent=4)}" + self.log.debug(msg) + msg = "self.diff_delete: " + msg += f"{json.dumps(self.diff_delete, indent=4)}" + self.log.debug(msg) + + self.set_model_enabled_false() + + def _get_diff_delete_with_config_model(self) -> None: + """ + Handle diff_delete logic when self.config is not empty. + + In this case, we detach, undeploy, and delete the VRFs + specified in self.config. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + diff_detach: list[VrfDetachPayloadV12] = [] + diff_undeploy: dict = {} + diff_delete: dict = {} + all_vrfs = set() + + msg = "self.have_attach_models: " + self.log.debug(msg) + self.log_list_of_models(self.have_attach_models, by_alias=True) + + for want_create_payload_model in self.want_create_payload_models: + if self.find_dict_in_list_by_key_value(search=self.have_create, key="vrfName", value=want_create_payload_model.vrf_name) == {}: + continue + + diff_delete.update({want_create_payload_model.vrf_name: "DEPLOYED"}) + + have_attach_model: HaveAttachPostMutate = self.find_model_in_list_by_key_value( + search=self.have_attach_models, key="vrf_name", value=want_create_payload_model.vrf_name + ) + if not have_attach_model: + msg = f"have_attach_model not found for vrfName: {want_create_payload_model.vrf_name}. " + msg += "Continuing." + self.log.debug(msg) + continue + + msg = "have_attach_model: " + msg += f"{json.dumps(have_attach_model.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + + detach_list_model: VrfDetachPayloadV12 = self.get_items_to_detach_model(have_attach_model.lan_attach_list) + if not detach_list_model: + msg = "detach_list_model is None. continuing." + self.log.debug(msg) + continue + msg = f"detach_list_model: length(lan_attach_list): {len(detach_list_model.lan_attach_list)}." + self.log.debug(msg) + msg = f"{json.dumps(detach_list_model.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + if detach_list_model.lan_attach_list: + diff_detach.append(detach_list_model) + all_vrfs.add(detach_list_model.vrf_name) + if len(all_vrfs) != 0: + diff_undeploy.update({"vrfNames": ",".join(all_vrfs)}) + + self.diff_detach = diff_detach + self.diff_undeploy = copy.deepcopy(diff_undeploy) + self.diff_delete = copy.deepcopy(diff_delete) + + def _get_diff_delete_without_config_model(self) -> None: + """ + Handle diff_delete logic when self.config is empty or None. + + In this case, we detach, undeploy, and delete all VRFs. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + diff_detach: list[VrfDetachPayloadV12] = [] + diff_undeploy: dict = {} + diff_delete: dict = {} + all_vrfs = set() + + msg = "self.have_attach_models: " + self.log.debug(msg) + self.log_list_of_models(self.have_attach_models, by_alias=True) + + have_attach_model: HaveAttachPostMutate + for have_attach_model in self.have_attach_models: + msg = f"type(have_attach_model): {type(have_attach_model)}" + self.log.debug(msg) + diff_delete.update({have_attach_model.vrf_name: "DEPLOYED"}) + detach_list_model = self.get_items_to_detach_model(have_attach_model.lan_attach_list) + if not detach_list_model: + msg = "detach_list_model is None. continuing." + self.log.debug(msg) + continue + msg = f"detach_list_model: length(lan_attach_list): {len(detach_list_model.lan_attach_list)}." + self.log.debug(msg) + msg = f"{json.dumps(detach_list_model.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + if detach_list_model.lan_attach_list: + diff_detach.append(detach_list_model) + all_vrfs.add(detach_list_model.vrf_name) + + if len(all_vrfs) != 0: + diff_undeploy.update({"vrfNames": ",".join(all_vrfs)}) + + self.diff_detach = diff_detach + self.diff_undeploy = copy.deepcopy(diff_undeploy) + self.diff_delete = copy.deepcopy(diff_delete) + + def get_diff_override(self) -> None: + """ + # Summary + + For override state, we delete existing attachments and vrfs (self.have_attach_models) that are not in self.want_create_payload_models. + + Using self.have_attach_models and self.want_create_payload_models, update the following: + + - diff_detach: a list of attachment objects to detach (see append_to_diff_detach) + - diff_undeploy: a dictionary with single key "vrfNames" and value of a comma-separated list of vrf_names to undeploy + - diff_delete: a dictionary keyed on vrf_name with value set to "DEPLOYED". These VRFs will be deleted. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + self.get_diff_replace() + all_vrfs = set() + + for have_attach_model in self.have_attach_models: + found_in_want = self.find_model_in_list_by_key_value(search=self.want_create_payload_models, key="vrf_name", value=have_attach_model.vrf_name) + + if found_in_want: + continue + # VRF exists on the controller but is not in the want list. Detach and delete it. + vrf_detach_payload = self.get_items_to_detach_model(have_attach_model.lan_attach_list) + if vrf_detach_payload: + self.diff_detach.append(vrf_detach_payload) + all_vrfs.add(vrf_detach_payload.vrf_name) + self.diff_delete.update({vrf_detach_payload.vrf_name: "DEPLOYED"}) + + if len(all_vrfs) != 0: + self.diff_undeploy.update({"vrfNames": ",".join(all_vrfs)}) + + msg = "self.diff_delete: " + msg += f"{json.dumps(self.diff_delete, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_detach: " + self.log.debug(msg) + self.log_list_of_models(self.diff_detach, by_alias=False) + + msg = "self.diff_undeploy: " + msg += f"{json.dumps(self.diff_undeploy, indent=4)}" + self.log.debug(msg) + + def get_diff_replace(self) -> None: + """ + # Summary + + For replace state, update the following: + + - self.diff_attach: a list of attachment objects to attach + - self.diff_deploy: a dictionary of vrf names to deploy + + By comparing the current state of attachments (self.have_attach_models) + with the desired state (self.want_attach) and determining which attachments + need to be replaced or removed. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + all_vrfs: set = set() + self.get_diff_merge(replace=True) + + msg = f"self.have_attach_models: length: {len(self.have_attach_models)}." + self.log.debug(msg) + self.log_list_of_models(self.have_attach_models, by_alias=True) + for have_attach_model in self.have_attach_models: + replace_lan_attach_list = [] + + # Find want_attach whose vrfName matches have_attach + want_attach = next((w for w in self.want_attach if w.get("vrfName") == have_attach_model.vrf_name), None) + + if want_attach: # matches have_attach + have_lan_attach_list = have_attach_model.lan_attach_list + want_lan_attach_list = want_attach.get("lanAttachList", []) + + for have_lan_attach_model in have_lan_attach_list: + if have_lan_attach_model.is_attached is False: + continue + # Check if this have_lan_attach_model exists in want_lan_attach_list by serialNumber + if not any(have_lan_attach_model.serial_number == want_lan_attach.get("serialNumber") for want_lan_attach in want_lan_attach_list): + have_lan_attach_model.deployment = False + # Need to convert model to dict to remove isAttached key. TODO: revisit + have_lan_attach_dict = have_lan_attach_model.model_dump(by_alias=True) + have_lan_attach_dict.pop("isAttached", None) # Remove isAttached key + replace_lan_attach_list.append(have_lan_attach_dict) + else: # have_attach is not in want_attach + have_attach_in_want_create = self.find_model_in_list_by_key_value( + search=self.want_create_payload_models, key="vrf_name", value=have_attach_model.vrf_name + ) + if not have_attach_in_want_create: + continue + # If have_attach is not in want_attach but is in want_create, detach all attached + for have_lan_attach_model in have_attach_model.lan_attach_list: + if not have_lan_attach_model.is_attached: + continue + have_lan_attach_model.deployment = False + # Need to convert model to dict to remove isAttached key. TODO: revisit + have_lan_attach_dict = have_lan_attach_model.model_dump(by_alias=True) + have_lan_attach_dict.pop("isAttached", None) # Remove isAttached key + replace_lan_attach_list.append(have_lan_attach_dict) + + if not replace_lan_attach_list: + continue + # Find or create the diff_attach entry for this VRF + diff_attach = next((d for d in self.diff_attach if d.get("vrfName") == have_attach_model.vrf_name), None) + if diff_attach: + diff_attach["lanAttachList"].extend(replace_lan_attach_list) + else: + attachment = { + "vrfName": have_attach_model.vrf_name, + "lanAttachList": replace_lan_attach_list, + } + self.diff_attach.append(attachment) + all_vrfs.add(have_attach_model.vrf_name) + + if not all_vrfs: + msg = "Early return. No VRF attachments required modification for replaced state." + self.log.debug(msg) + return + + all_vrfs.update({vrf for vrf in self.want_deploy.get("vrfNames", "").split(",") if vrf}) + self.diff_deploy.update({"vrfNames": ",".join(all_vrfs)}) + + msg = "self.diff_attach: " + msg += f"{json.dumps(self.diff_attach, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_deploy: " + msg += f"{json.dumps(self.diff_deploy, indent=4)}" + self.log.debug(msg) + + def diff_merge_create(self, replace=False) -> None: + """ + # Summary + + Populates the following lists + + - self.diff_create + - self.diff_create_update + - self.diff_create_quick + + TODO: arobel: replace parameter is not used. See Note 1 below. + + Notes + 1. The replace parameter is not used in this method and should be removed. + This was used prior to refactoring this method, and diff_merge_attach, + from an earlier method. diff_merge_attach() does still use + the replace parameter. + + In order to remove this, we have to update 35 unit tests, so we'll + do this as part of a future PR. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + self.conf_changed = {} + + diff_create: list = [] + diff_create_update: list = [] + diff_create_quick: list = [] + + want_c: dict = {} + for want_c in self.want_create: + vrf_found: bool = False + have_c: dict = {} + for have_c in self.have_create: + if want_c["vrfName"] != have_c["vrfName"]: + continue + vrf_found = True + msg = "Calling diff_for_create with: " + msg += f"want_c: {json.dumps(want_c, indent=4, sort_keys=True)}, " + msg += f"have_c: {json.dumps(have_c, indent=4, sort_keys=True)}" + self.log.debug(msg) + + diff, changed = self.diff_for_create(want_c, have_c) + + msg = "diff_for_create() returned with: " + msg += f"changed {changed}, " + msg += f"diff {json.dumps(diff, indent=4, sort_keys=True)}, " + self.log.debug(msg) + + msg = f"Updating self.conf_changed[{want_c['vrfName']}] " + msg += f"with {changed}" + self.log.debug(msg) + self.conf_changed.update({want_c["vrfName"]: changed}) + + if diff: + msg = "Appending diff_create_update with " + msg += f"{json.dumps(diff, indent=4, sort_keys=True)}" + self.log.debug(msg) + diff_create_update.append(diff) + break + + if vrf_found: + continue + vrf_id = want_c.get("vrfId", None) + if vrf_id is not None: + diff_create.append(want_c) + else: + # Special case: + # 1. Auto generate vrfId since it is not provided in the playbook task: + # - In this case, query the controller for a vrfId and + # use it in the payload. + # - This vrf create request needs to be pushed individually + # i.e. not as a bulk operation. + # TODO: arobel: review this with Mike to understand why this + # couldn't be moved to a method called by push_to_remote(). + vrf_id = self.get_next_fabric_vrf_id(self.fabric) + + want_c.update({"vrfId": vrf_id}) + + want_c.update({"vrfTemplateConfig": self.update_vrf_template_config(want_c)}) + want_c["vrfTemplateConfig"]["vrfSegmentId"] = vrf_id + + diff_create_quick.append(want_c) + + if self.module.check_mode: + continue + + # arobel: TODO: Not covered by UT + endpoint = EpVrfPost() + endpoint.fabric_name = self.fabric + + args = SendToControllerArgs( + action="attach", + path=endpoint.path, + verb=endpoint.verb, + payload=json.dumps(want_c), + log_response=True, + is_rollback=True, + ) + self.send_to_controller(args) + + self.diff_create = copy.deepcopy(diff_create) + self.diff_create_update = copy.deepcopy(diff_create_update) + self.diff_create_quick = copy.deepcopy(diff_create_quick) + + msg = "self.diff_create: " + msg += f"{json.dumps(self.diff_create, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_create_quick: " + msg += f"{json.dumps(self.diff_create_quick, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_create_update: " + msg += f"{json.dumps(self.diff_create_update, indent=4)}" + self.log.debug(msg) + + def diff_merge_attach(self, replace=False) -> None: + """ + # Summary + + Populates the following + + - self.diff_attach + - self.diff_deploy + + By comparing the current state of attachments (self.have_attach_models) + with the desired state (self.want_attach) and determining which attachments + need to be updated. + + ## params + + - replace: Passed unaltered to self.diff_for_attach_deploy() + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = f"replace == {replace}." + self.log.debug(msg) + + if not self.want_attach: + self.diff_attach = [] + self.diff_deploy = {} + msg = "Early return. No attachments to process." + self.log.debug(msg) + return + + diff_attach: list = [] + diff_deploy: dict = {} + all_vrfs: set = set() + + msg = "self.want_attach: " + msg += f"type: {type(self.want_attach)}" + self.log.debug(msg) + msg = f"value: {json.dumps(self.want_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.have_attach_models: " + self.log.debug(msg) + self.log_list_of_models(self.have_attach_models, by_alias=True) + + for want_attach in self.want_attach: + msg = f"type(want_attach): {type(want_attach)}, " + msg += f"want_attach: {json.dumps(want_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + # Check user intent for this VRF and don't add it to the all_vrfs + # set if the user has not requested a deploy. + want_config_model: PlaybookVrfModelV12 = self.find_model_in_list_by_key_value( + search=self.validated_playbook_config_models, key="vrf_name", value=want_attach["vrfName"] + ) + want_config_deploy = want_config_model.deploy if want_config_model else False + vrf_to_deploy: str = "" + attach_found = False + for have_attach_model in self.have_attach_models: + if want_attach.get("vrfName") != have_attach_model.vrf_name: + continue + attach_found = True + diff, deploy_vrf_bool = self.diff_for_attach_deploy( + want_attach_list=want_attach["lanAttachList"], + have_lan_attach_list_models=have_attach_model.lan_attach_list, + replace=replace, + ) + msg = "diff_for_attach_deploy() returned with: " + msg += f"deploy_vrf_bool {deploy_vrf_bool}, " + msg += f"diff {json.dumps(diff, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if diff: + base = copy.deepcopy(want_attach) + base["lanAttachList"] = diff + + diff_attach.append(base) + if (want_config_deploy is True) and (deploy_vrf_bool is True): + vrf_to_deploy = want_attach.get("vrfName") + else: + if want_config_deploy is True and (deploy_vrf_bool or self.conf_changed.get(want_attach.get("vrfName"), False)): + vrf_to_deploy = want_attach.get("vrfName") + + msg = f"attach_found: {attach_found}" + self.log.debug(msg) + + if not attach_found and want_attach.get("lanAttachList"): + attach_list = [] + for lan_attach in want_attach["lanAttachList"]: + if lan_attach.get("isAttached"): + del lan_attach["isAttached"] + if lan_attach.get("is_deploy") is True: + vrf_to_deploy = want_attach["vrfName"] + lan_attach["deployment"] = True + attach_list.append(copy.deepcopy(lan_attach)) + if attach_list: + base = copy.deepcopy(want_attach) + base["lanAttachList"] = attach_list + diff_attach.append(base) + + if vrf_to_deploy: + all_vrfs.add(vrf_to_deploy) + + if len(all_vrfs) != 0: + diff_deploy.update({"vrfNames": ",".join(all_vrfs)}) + + self.diff_attach = copy.deepcopy(diff_attach) + self.diff_deploy = copy.deepcopy(diff_deploy) + + msg = "self.diff_attach: " + msg += f"{json.dumps(self.diff_attach, indent=4)}" + self.log.debug(msg) + + msg = "self.diff_deploy: " + msg += f"{json.dumps(self.diff_deploy, indent=4)}" + self.log.debug(msg) + + def get_diff_merge(self, replace=False): + """ + # Summary + + Call the following methods + + - diff_merge_create() + - diff_merge_attach() + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = f"replace == {replace}" + self.log.debug(msg) + + self.diff_merge_create(replace) + self.diff_merge_attach(replace) + + def format_diff_attach(self, diff_attach: list[dict], diff_deploy: list[str]) -> list[dict]: + """ + Populate the diff list with remaining attachment entries. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if len(diff_attach) > 0: + msg = f"type(diff_attach[0]): {type(diff_attach[0])}, length {len(diff_attach)}" + self.log.debug(msg) + msg = "diff_attach: " + msg += f"{json.dumps(diff_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if len(diff_deploy) > 0: + msg = f"type(diff_deploy[0]): {type(diff_deploy[0])}, length {len(diff_deploy)}" + self.log.debug(msg) + msg = "diff_deploy: " + msg += f"{json.dumps(diff_deploy, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not diff_attach: + msg = "No diff_attach entries to process. Returning empty list." + self.log.debug(msg) + return [] + diff = [] + for vrf in diff_attach: + # TODO: arobel: using models, we get a KeyError for lan_attach[vlan], so we try lan_attach[vlanId] too. + # TODO: arobel: remove this once we've fixed the model to dump what is expected here. + new_attach_list = [ + { + "ip_address": self.serial_number_to_ipv4.convert(lan_attach.get("serialNumber")), + "vlan_id": lan_attach.get("vlan") or lan_attach.get("vlanId"), + "deploy": lan_attach["deployment"], + } + for lan_attach in vrf["lanAttachList"] + ] + msg = "ZZZ: new_attach_list: " + msg += f"{json.dumps(new_attach_list, indent=4, sort_keys=True)}" + self.log.debug(msg) + msg = f"ZZZ: diff_deploy: {diff_deploy}" + self.log.debug(msg) + if new_attach_list: + if diff_deploy and vrf["vrfName"] in diff_deploy: + diff_deploy.remove(vrf["vrfName"]) + new_attach_dict = { + "attach": new_attach_list, + "vrf_name": vrf["vrfName"], + } + diff.append(new_attach_dict) + + msg = "returning diff (diff_attach): " + msg += f"{json.dumps(diff, indent=4, sort_keys=True)}" + self.log.debug(msg) + return diff + + def format_diff_create(self, diff_create: list, diff_attach: list, diff_deploy: list) -> list: + """ + # Summary + + Populate the diff list with VRF create/update entries. + + ## Raises + + - fail_json if vrfTemplateConfig fails validation + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + diff = [] + for want_d in diff_create: + found_attach = self.find_dict_in_list_by_key_value(search=diff_attach, key="vrfName", value=want_d["vrfName"]) + found_create = copy.deepcopy(want_d) + + found_create.update( + { + "attach": [], + "service_vrf_template": found_create["serviceVrfTemplate"], + "vrf_extension_template": found_create["vrfExtensionTemplate"], + "vrf_id": found_create["vrfId"], + "vrf_name": found_create["vrfName"], + "vrf_template": found_create["vrfTemplate"], + } + ) + + vrf_template_config = json.loads(found_create["vrfTemplateConfig"]) + try: + vrf_controller_to_playbook = VrfControllerToPlaybookV12Model(**vrf_template_config) + found_create.update(vrf_controller_to_playbook.model_dump(by_alias=False)) + except ValidationError as error: + msg = f"{self.class_name}.format_diff_create: Validation error: {error}" + self.module.fail_json(msg=msg) + + for key in ["fabric", "serviceVrfTemplate", "vrfExtensionTemplate", "vrfId", "vrfName", "vrfTemplate", "vrfTemplateConfig"]: + found_create.pop(key, None) + + if diff_deploy and found_create["vrf_name"] in diff_deploy: + diff_deploy.remove(found_create["vrf_name"]) + if not found_attach: + diff.append(found_create) + continue + + # TODO: arobel: using models, we get a KeyError for lan_attach[vlan], so we try lan_attach[vlanId] too. + # TODO: arobel: remove this once we've fixed the model to dump what is expected here. + found_create["attach"] = [ + { + "ip_address": self.serial_number_to_ipv4.convert(lan_attach.get("serialNumber")), + "vlan_id": lan_attach.get("vlan") or lan_attach.get("vlanId"), + "deploy": lan_attach["deployment"], + } + for lan_attach in found_attach["lanAttachList"] + ] + diff.append(found_create) + diff_attach.remove(found_attach) + msg = "Returning diff (diff_create): " + msg += f"{json.dumps(diff, indent=4, sort_keys=True)}" + self.log.debug(msg) + return diff + + def format_diff_deploy(self, diff_deploy: list[str]) -> list: + """ + # Summary + + Populate the diff list with deploy/undeploy entries. + + ## Raises + + - None + + ## Notes + + - Unit tests all return [] for diff_deploy. Look into add a test case that returns a non-empty list. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + diff = [] + for vrf in diff_deploy: + new_deploy_dict = {"vrf_name": vrf} + diff.append(copy.deepcopy(new_deploy_dict)) + + msg = "Returning diff (diff_deploy): " + msg += f"{json.dumps(diff, indent=4, sort_keys=True)}" + self.log.debug(msg) + return diff + + def format_diff(self) -> None: + """ + # Summary + + Called from modules/dcnm_vrf.py + + Populate self.diff_input_format, which represents the + difference to the controller configuration after the playbook + has run, from the information in the following lists: + + - self.diff_create + - self.diff_create_quick + - self.diff_create_update + - self.diff_attach + - self.diff_detach + - self.diff_deploy + - self.diff_undeploy + + self.diff_input_format is formatted using keys a user + would use in a playbook. The keys in the above lists + are those used by the controller API. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + diff_create = copy.deepcopy(self.diff_create) + diff_create_quick = copy.deepcopy(self.diff_create_quick) + diff_create_update = copy.deepcopy(self.diff_create_update) + + diff_attach = copy.deepcopy(self.diff_attach) + if len(diff_attach) > 0: + msg = f"type(diff_attach[0]): {type(diff_attach[0])} length {len(diff_attach)}" + else: + msg = f"type(diff_attach): {type(diff_attach)}, length {len(diff_attach)}, " + self.log.debug(msg) + msg = f"{json.dumps(diff_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if len(self.diff_detach) > 0: + msg = f"type(self.diff_detach[0]): {type(self.diff_detach[0])}, length {len(self.diff_detach)}." + else: + msg = f"type(self.diff_detach): {type(self.diff_detach)}, length {len(self.diff_detach)}." + self.log.debug(msg) + self.log_list_of_models(self.diff_detach, by_alias=False) + + diff_deploy = self.diff_deploy["vrfNames"].split(",") if self.diff_deploy else [] + diff_undeploy = self.diff_undeploy["vrfNames"].split(",") if self.diff_undeploy else [] + + diff_create.extend(diff_create_quick) + diff_create.extend(diff_create_update) + diff_attach.extend([model.model_dump(by_alias=True) for model in self.diff_detach]) + diff_deploy.extend(diff_undeploy) + + diff = [] + diff.extend(self.format_diff_create(diff_create, diff_attach, diff_deploy)) + diff.extend(self.format_diff_attach(diff_attach, diff_deploy)) + diff.extend(self.format_diff_deploy(diff_deploy)) + + self.diff_input_format = copy.deepcopy(diff) + msg = "self.diff_input_format: " + msg += f"{json.dumps(self.diff_input_format, indent=4, sort_keys=True)}" + self.log.debug(msg) + + def push_diff_create_update(self, is_rollback=False) -> None: + """ + # Summary + + Send diff_create_update to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = "self.diff_create_update: " + msg += f"{json.dumps(self.diff_create_update, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.diff_create_update: + msg = "Early return. self.diff_create_update is empty." + self.log.debug(msg) + return + + action: str = "create" + endpoint = EpVrfPost() + endpoint.fabric_name = self.fabric + + for payload in self.diff_create_update: + args = SendToControllerArgs( + action=action, + path=f"{endpoint.path}/{payload['vrfName']}", + verb=RequestVerb.PUT, + payload=json.dumps(payload), + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def push_diff_detach(self, is_rollback=False) -> None: + """ + # Summary + + Send diff_detach to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + self.model_enabled = True + if self.model_enabled: + self.push_diff_detach_model(is_rollback) + self.model_enabled = False + return + + msg = "self.diff_detach: " + msg += f"{json.dumps(self.diff_detach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.diff_detach: + msg = "Early return. self.diff_detach is empty." + self.log.debug(msg) + return + + # Replace fabricName key (if present) with fabric key + for diff_attach in self.diff_detach: + for lan_attach_item in diff_attach["lanAttachList"]: + if "fabricName" in lan_attach_item: + lan_attach_item["fabric"] = lan_attach_item.pop("fabricName", None) + if lan_attach_item.get("fabric") is None: + msg = "lan_attach_item.fabric is None. " + msg += f"Setting it to self.fabric ({self.fabric})." + self.log.debug(msg) + lan_attach_item["fabric"] = self.fabric + + # For multisite fabric, update the fabric name to the child fabric + # containing the switches + if self.fabric_type == "MFD": + for elem in self.diff_detach: + for node in elem["lanAttachList"]: + node["fabric"] = self.sn_fab[node["serialNumber"]] + + for diff_attach in self.diff_detach: + for vrf_attach in diff_attach["lanAttachList"]: + if "is_deploy" in vrf_attach.keys(): + del vrf_attach["is_deploy"] + + msg = "self.diff_detach after processing: " + msg += f"{json.dumps(self.diff_detach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + action: str = "attach" + endpoint = EpVrfPost() + endpoint.fabric_name = self.fabric + + args = SendToControllerArgs( + action=action, + path=f"{endpoint.path}/attachments", + verb=endpoint.verb, + payload=json.dumps(self.diff_detach), + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def push_diff_detach_model(self, is_rollback=False) -> None: + """ + # Summary + + Send diff_detach to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = "self.diff_detach: " + self.log.debug(msg) + self.log_list_of_models(self.diff_detach, by_alias=False) + + if not self.diff_detach: + msg = "Early return. self.diff_detach is empty." + self.log.debug(msg) + return + + # For multisite fabric, update the fabric name to the child fabric + # containing the switches + if self.fabric_type == "MFD": + for model in self.diff_detach: + for lan_attach_item in model.lan_attach_list: + lan_attach_item.fabric = self.sn_fab[lan_attach_item.serial_number] + + for diff_attach_model in self.diff_detach: + for lan_attach_item in diff_attach_model.lan_attach_list: + try: + del lan_attach_item.is_deploy + except AttributeError: + # If the model does not have is_deploy, skip the deletion + msg = "is_deploy not found in lan_attach_item. " + msg += "Continuing without deleting is_deploy." + self.log.debug(msg) + + action: str = "attach" + endpoint = EpVrfPost() + endpoint.fabric_name = self.fabric + + payload = [model.model_dump(by_alias=True, exclude_none=True, exclude_unset=True) for model in self.diff_detach] + + args = SendToControllerArgs( + action=action, + path=f"{endpoint.path}/attachments", + verb=endpoint.verb, + payload=json.dumps(payload), + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def push_diff_undeploy(self, is_rollback=False): + """ + # Summary + + Send diff_undeploy to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = "self.diff_undeploy: " + msg += f"{json.dumps(self.diff_undeploy, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.diff_undeploy: + msg = "Early return. self.diff_undeploy is empty." + self.log.debug(msg) + return + + action = "deploy" + endpoint = EpVrfPost() + endpoint.fabric_name = self.fabric + args = SendToControllerArgs( + action=action, + path=f"{endpoint.path}/deployments", + verb=endpoint.verb, + payload=json.dumps(self.diff_undeploy), + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def push_diff_delete(self, is_rollback=False) -> None: + """ + # Summary + + Send diff_delete to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = "self.diff_delete: " + msg += f"{json.dumps(self.diff_delete, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.diff_delete: + msg = "Early return. self.diff_delete is None." + self.log.debug(msg) + return + + self.wait_for_vrf_del_ready() + + del_failure: set = set() + endpoint = EpVrfGet() + endpoint.fabric_name = self.fabric + for vrf, state in self.diff_delete.items(): + if state == "OUT-OF-SYNC": + del_failure.add(vrf) + continue + args = SendToControllerArgs( + action="delete", + path=f"{endpoint.path}/{vrf}", + verb=RequestVerb.DELETE, + payload=json.dumps(self.diff_delete), + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + if len(del_failure) > 0: + msg = f"{self.class_name}.push_diff_delete: " + msg += f"Deletion of vrfs {','.join(del_failure)} has failed" + self.result["response"].append(msg) + self.module.fail_json(msg=self.result) + + def get_controller_vrf_attachment_models(self, vrf_name: str) -> list[ControllerResponseVrfsAttachmentsDataItem]: + """ + ## Summary + + Given a vrf_name, query the controller for the attachment list + for that vrf and return a list of ControllerResponseVrfsAttachmentsDataItem + models. + + ## Raises + + - ValueError: If the response from the controller is None. + - ValueError: If the response from the controller is not valid. + - fail_json: If the fabric does not exist on the controller. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + path_get_vrf_attach = self.paths["GET_VRF_ATTACH"].format(self.fabric, vrf_name) + controller_response = dcnm_send(self.module, "GET", path_get_vrf_attach) + + msg = f"path_get_vrf_attach: {path_get_vrf_attach}" + self.log.debug(msg) + msg = "controller_response: " + msg += f"{json.dumps(controller_response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if controller_response is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{caller}: Unable to retrieve endpoint. " + msg += f"verb GET, path {path_get_vrf_attach}" + raise ValueError(msg) + + validated_response = ControllerResponseVrfsAttachmentsV12(**controller_response) + msg = "validated_response (ControllerResponseVrfsAttachmentsV12): " + msg += f"{json.dumps(validated_response.model_dump(by_alias=True), indent=4, sort_keys=True)}" + self.log.debug(msg) + + generic_response = ControllerResponseGenericV12(**controller_response) + missing_fabric, not_ok = self.handle_response(generic_response, "query") + + if missing_fabric or not_ok: + msg0 = f"caller: {caller}. " + msg1 = f"{msg0} Fabric {self.fabric} not present on DCNM" + msg2 = f"{msg0} Unable to find attachments for " + msg2 += f"vrf {vrf_name} under fabric {self.fabric}" + self.module.fail_json(msg=msg1 if missing_fabric else msg2) + return validated_response.DATA + + def get_diff_query_for_vrfs_in_want(self, vrf_object_models: list[VrfObjectV12]) -> list[dict]: + """ + Query the controller for the current state of the VRFs in the fabric + that are present in self.want_create_payload_models. + + ## Raises + + - ValueError: If any controller response is not valid. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + query: list[dict] = [] + + if not self.want_create_payload_models: + msg = "Early return. No VRFs in self.want_create_payload_models to process." + self.log.debug(msg) + return query + + if not vrf_object_models: + msg = f"Early return. No VRFs exist in fabric {self.fabric}." + self.log.debug(msg) + return query + + # Lookup controller VRFs by name, used in for loop below. + vrf_object_model_lookup = {model.vrfName: model for model in vrf_object_models} + for want_create_payload_model in self.want_create_payload_models: + vrf_model = vrf_object_model_lookup.get(want_create_payload_model.vrf_name) + if not vrf_model: + continue + + query_item = {"parent": vrf_model.model_dump(by_alias=True), "attach": []} + vrf_attachment_models = self.get_controller_vrf_attachment_models(vrf_model.vrfName) + + msg = f"caller: {caller}. vrf_attachment_models: length {len(vrf_attachment_models)}." + self.log.debug(msg) + self.log_list_of_models(vrf_attachment_models) + + for vrf_attachment_model in vrf_attachment_models: + if want_create_payload_model.vrf_name != vrf_attachment_model.vrf_name or not vrf_attachment_model.lan_attach_list: + continue + + for lan_attach_model in vrf_attachment_model.lan_attach_list: + params = { + "fabric": self.fabric, + "serialNumber": lan_attach_model.switch_serial_no, + "vrfName": lan_attach_model.vrf_name, + } + + lite_objects = self.get_list_of_vrfs_switches_data_item_model(params) + + msg = f"Caller {caller}. Called get_list_of_vrfs_switches_data_item_model with params: " + msg += f"{json.dumps(params, indent=4, sort_keys=True)}" + self.log.debug(msg) + msg = f"Caller {caller}. lite_objects: length: {len(lite_objects)}." + self.log.debug(msg) + self.log_list_of_models(lite_objects) + + if lite_objects: + query_item["attach"].append(lite_objects[0].model_dump(by_alias=True)) + query.append(query_item) + + msg = f"Caller {caller}. Returning query: " + msg += f"{json.dumps(query, indent=4, sort_keys=True)}" + self.log.debug(msg) + return copy.deepcopy(query) + + def get_diff_query_for_all_controller_vrfs(self, vrf_object_models: list[VrfObjectV12]) -> list[dict]: + """ + Query the controller for the current state of all VRFs in the fabric. + + ## Raises + + - ValueError: If the response from the controller is not valid. + - fail_json: If lite_objects_data is not a list. + + ## Returns + + A list of dictionaries with the following structure: + + [ + { + "parent": VrfObjectV12 + "attach": [ + { + "ip_address": str, + "vlan_id": int, + "deploy": bool + } + ] + } + ] + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + query: list[dict] = [] + + if not vrf_object_models: + msg = f"Early return. No VRFs exist in fabric {self.fabric}." + self.log.debug(msg) + return query + + for vrf in vrf_object_models: + + item = {"parent": vrf.model_dump(by_alias=True), "attach": []} + + vrf_attachment_models = self.get_controller_vrf_attachment_models(vrf.vrfName) + + msg = f"caller: {caller}. vrf_attachment_models: length {len(vrf_attachment_models)}." + self.log.debug(msg) + self.log_list_of_models(vrf_attachment_models) + + for vrf_attach in vrf_attachment_models: + if not vrf_attach.lan_attach_list: + continue + lan_attach_models = vrf_attach.lan_attach_list + msg = f"lan_attach_models: length: {len(lan_attach_models)}" + self.log.debug(msg) + self.log_list_of_models(lan_attach_models) + + for lan_attach_model in lan_attach_models: + params = { + "fabric": self.fabric, + "serialNumber": lan_attach_model.switch_serial_no, + "vrfName": lan_attach_model.vrf_name, + } + msg = f"Calling get_list_of_vrfs_switches_data_item_model with: {params}" + self.log.debug(msg) + + lite_objects = self.get_list_of_vrfs_switches_data_item_model(params) + + msg = f"Caller {caller}. lite_objects: length: {len(lite_objects)}." + self.log.debug(msg) + self.log_list_of_models(lite_objects) + + if not lite_objects: + continue + item["attach"].append(lite_objects[0].model_dump(by_alias=True)) + query.append(item) + + msg = f"Returning query: {query}" + self.log.debug(msg) + return query + + def get_diff_query(self) -> None: + """ + Query the controller for the current state of the VRFs in the fabric. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + vrf_object_models = self.get_controller_vrf_object_models() + + msg = f"vrf_object_models: length {len(vrf_object_models)}." + self.log.debug(msg) + self.log_list_of_models(vrf_object_models) + + if not vrf_object_models: + return + + if self.config: + query = self.get_diff_query_for_vrfs_in_want(vrf_object_models) + else: + query = self.get_diff_query_for_all_controller_vrfs(vrf_object_models) + + self.query = copy.deepcopy(query) + msg = f"self.query: {json.dumps(self.query, indent=4, sort_keys=True)}" + self.log.debug(msg) + + def update_vrf_template_config_from_vrf_model(self, vrf_model: VrfObjectV12) -> VrfTemplateConfigV12: + """ + # Summary + + Update the following fields in VrfObjectV12.VrfTemplateConfigV12 and + return the updated VrfTemplateConfigV12 model instance. + + - vrfVlanId + - Updated from VrfObjectModelV12.vlan_id + - if 0, get the next available vlan_id from the controller + - else, use the vlan_id in vrfTemplateConfig + - vrfSegmentId + - Updated from VrfObjectModelV12.vrf_id + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + # Don't modify the caller's copy + vrf_model = copy.deepcopy(vrf_model) + + vrf_segment_id = vrf_model.vrfId + vlan_id = vrf_model.vrfTemplateConfig.vlan_id + + if vlan_id == 0: + vlan_id = self.get_next_fabric_vlan_id(self.fabric) + msg = "vlan_id was 0. " + msg += f"Using next available controller-generated vlan_id: {vlan_id}" + self.log.debug(msg) + + vrf_model.vrfTemplateConfig.vlan_id = vlan_id + vrf_model.vrfTemplateConfig.vrf_id = vrf_segment_id + return vrf_model.vrfTemplateConfig + + def update_vrf_template_config(self, vrf: dict) -> dict: + """ + TODO: Legacy method. Remove when all callers are updated to use update_vrf_template_config_from_vrf_model. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + vrf_template_config = json.loads(vrf["vrfTemplateConfig"]) + vlan_id = vrf_template_config.get("vrfVlanId", 0) + + if vlan_id == 0: + vlan_id = self.get_next_fabric_vlan_id(self.fabric) + msg = "vlan_id was 0. " + msg += f"Using next available controller-generated vlan_id: {vlan_id}" + self.log.debug(msg) + + vrf_template_config.update({"vrfVlanId": vlan_id}) + vrf_template_config.update({"vrfSegmentId": vrf.get("vrfId")}) + + msg = f"Returning vrf_template_config: {json.dumps(vrf_template_config, indent=4, sort_keys=True)}" + self.log.debug(msg) + return json.dumps(vrf_template_config) + + def vrf_model_to_payload(self, vrf_model: VrfObjectV12) -> dict: + """ + # Summary + + Convert a VrfObjectV12 model to a VrfPayloadV12 model and return + as a dictionary suitable for sending to the controller. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = f"vrf_model: {json.dumps(vrf_model.model_dump(by_alias=True), indent=4, sort_keys=True)}" + self.log.debug(msg) + + vrf_payload = VrfPayloadV12(**vrf_model.model_dump(exclude_unset=True, by_alias=True)) + + return vrf_payload.model_dump_json(exclude_unset=True, by_alias=True) + + def push_diff_create(self, is_rollback=False) -> None: + """ + # Summary + + Update the VRFs in self.diff_create and send them to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = "self.diff_create: " + msg += f"{json.dumps(self.diff_create, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.diff_create: + msg = "Early return. self.diff_create is empty." + self.log.debug(msg) + return + + for vrf in self.diff_create: + vrf_model = VrfObjectV12(**vrf) + vrf_model.vrfTemplateConfig = self.update_vrf_template_config_from_vrf_model(vrf_model) + + msg = "Sending vrf create request." + self.log.debug(msg) + + endpoint = EpVrfPost() + endpoint.fabric_name = self.fabric + args = SendToControllerArgs( + action="create", + path=endpoint.path, + verb=endpoint.verb, + payload=self.vrf_model_to_payload(vrf_model), + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def is_border_switch(self, serial_number) -> bool: + """ + # Summary + + Given a switch serial_number: + + - Return True if the switch is a border switch + - Return False otherwise + """ + switch_role = self.serial_number_to_switch_role.convert(serial_number) + return re.search(r"\bborder\b", switch_role.lower()) + + def send_to_controller(self, args: SendToControllerArgs) -> None: + """ + # Summary + + Send a request to the controller. + + Update self.response with the response from the controller. + + ## Raises + + - ValueError: If the response from the controller is None. + + ## params + + args: instance of SendToControllerArgs containing the following + - `action`: The action to perform (create, update, delete, etc.) + - `verb`: The HTTP verb to use (GET, POST, PUT, DELETE) + - `path`: The URL path to send the request to + - `payload`: The payload to send with the request (None for no payload) + - `log_response`: If True, log the response in the result, else + do not include the response in the result + - `is_rollback`: If True, attempt to rollback on failure + - `response_model`: The model to use to validate the response (optional, default=ControllerResponseGenericV12) + + ## Notes + + 1. send_to_controller sends the payload, if provided, as-is. Hence, + it is the caller's responsibility to ensure payload integrity. + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = "TX controller: " + self.log.debug(msg) + msg = f"action: {args.action}, " + self.log.debug(msg) + msg = f"verb: {args.verb.value}, " + msg += f"path: {args.path}, " + msg += f"log_response: {args.log_response}, " + msg += "type(payload): " + msg += f"{type(args.payload)}, " + self.log.debug(msg) + msg = "payload: " + if args.payload is None: + msg += f"{args.payload}" + else: + msg += f"{json.dumps(json.loads(args.payload), indent=4, sort_keys=True)}" + self.log.debug(msg) + + if args.payload is not None: + controller_response = dcnm_send(self.module, args.verb.value, args.path, args.payload) + else: + controller_response = dcnm_send(self.module, args.verb.value, args.path) + + if controller_response is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "Unable to retrieve endpoint. " + msg += f"verb {args.verb.value}, path {args.path}" + raise ValueError(msg) + + self.response = copy.deepcopy(controller_response) + + msg = "RX controller:" + self.log.debug(msg) + msg = f"verb: {args.verb.value}, " + msg += f"path: {args.path}" + self.log.debug(msg) + + msg = "controller_response: " + msg += f"{json.dumps(controller_response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "Calling self.handle_response. " + msg += "self.result[changed]): " + msg += f"{self.result['changed']}" + self.log.debug(msg) + + if args.log_response is True: + self.result["response"].append(controller_response) + + if args.response_model is None: + response_model = ControllerResponseGenericV12 + else: + response_model = args.response_model + + try: + validated_response = response_model(**controller_response) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"Unable to validate controller_response using model {response_model.__name__}. " + msg += f"controller_response: {json.dumps(controller_response, indent=4, sort_keys=True)}" + self.log.debug(msg) + self.module.fail_json(msg=msg, error=str(error)) + + msg = f"validated_response: ({response_model.__name__}), " + msg += f"{json.dumps(validated_response.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + fail, self.result["changed"] = self.handle_response(validated_response, args.action) + + msg = f"caller: {caller}, " + msg += "RESULT self.handle_response: " + msg = f"fail: {fail}, changed: {self.result['changed']}" + self.log.debug(msg) + + if fail: + if args.is_rollback: + self.failed_to_rollback = True + return + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}, " + msg += "Calling self.failure." + self.log.debug(msg) + self.failure(controller_response) + + def get_vrf_attach_fabric_name(self, vrf_attach: PayloadVrfsAttachmentsLanAttachListItem) -> str: + """ + # Summary + + For multisite fabrics, return the name of the child fabric returned by + `self.sn_fab[vrf_attach.serialNumber]` + + ## params + + - `vrf_attach` + + A PayloadVrfsAttachmentsLanAttachListItem model. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = "Received vrf_attach: " + msg += f"{json.dumps(vrf_attach.model_dump(by_alias=True), indent=4, sort_keys=True)}" + self.log.debug(msg) + + if self.fabric_type != "MFD": + msg = f"FABRIC_TYPE {self.fabric_type} is not MFD. " + msg += f"Returning unmodified fabric name {vrf_attach.fabric}." + self.log.debug(msg) + return vrf_attach.fabric + + msg = f"self.fabric: {self.fabric}, " + msg += f"fabric_type: {self.fabric_type}, " + msg += f"vrf_attach.fabric: {vrf_attach.fabric}." + self.log.debug(msg) + + serial_number = vrf_attach.serial_number + + if serial_number is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "Unable to parse vrf_attach.serial_number. " + msg += f"{json.dumps(vrf_attach.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + self.module.fail_json(msg) + + child_fabric_name = self.sn_fab[serial_number] + + if child_fabric_name is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "Unable to determine child_fabric_name for serial_number " + msg += f"{serial_number}." + self.log.debug(msg) + self.module.fail_json(msg) + + msg = f"serial_number: {serial_number}. " + msg += f"Returning child_fabric_name: {child_fabric_name}. " + self.log.debug(msg) + + return child_fabric_name + + def push_diff_attach_model(self, is_rollback=False) -> None: + """ + # Summary + + Send diff_attach to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if not self.diff_attach: + msg = "Early return. self.diff_attach is empty. " + msg += f"{json.dumps(self.diff_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + return + + try: + instance = DiffAttachToControllerPayload() + instance.ansible_module = self.module + instance.diff_attach = copy.deepcopy(self.diff_attach) + instance.fabric_inventory = self.inventory_data + # TODO: remove once we use fabricTechnology in DiffAttachToControllerPayload + instance.fabric_type = self.fabric_type + instance.playbook_models = self.validated_playbook_config_models + instance.sender = dcnm_send + instance.commit() + payload = instance.payload + except ValueError as error: + self.module.fail_json(error) + + endpoint = EpVrfPost() + endpoint.fabric_name = self.fabric + args = SendToControllerArgs( + action="attach", + path=f"{endpoint.path}/attachments", + verb=endpoint.verb, + payload=json.dumps(payload) if payload else payload, + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def push_diff_deploy(self, is_rollback=False): + """ + # Summary + + Send diff_deploy to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if not self.diff_deploy: + msg = "Early return. self.diff_deploy is empty." + self.log.debug(msg) + return + + endpoint = EpVrfPost() + endpoint.fabric_name = self.fabric + args = SendToControllerArgs( + action="deploy", + path=f"{endpoint.path}/deployments", + verb=endpoint.verb, + payload=json.dumps(self.diff_deploy), + log_response=True, + is_rollback=is_rollback, + ) + self.send_to_controller(args) + + def release_resources_by_id(self, id_list=None) -> None: + """ + # Summary + + Given a list of resource IDs, send a request to the controller + to release them. + + ## params + + - id_list: A list of resource IDs to release. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if id_list is None: + id_list = [] + + if not isinstance(id_list, list): + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "id_list must be a list of resource IDs. " + msg += f"Got: {id_list}." + self.module.fail_json(msg) + + try: + id_list = [int(x) for x in id_list] + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "id_list must be a list of resource IDs. " + msg += "Where each id is convertable to integer." + msg += f"Got: {id_list}. " + msg += f"Error detail: {error}" + self.module.fail_json(msg) + + # The controller can release only around 500-600 IDs per + # request (not sure of the exact number). We break up + # requests into smaller lists here. In practice, we'll + # likely ever only have one resulting list. + id_list_of_lists = self.get_list_of_lists([str(x) for x in id_list], 512) + + for item in id_list_of_lists: + msg = "Releasing resource IDs: " + msg += f"{','.join(item)}" + self.log.debug(msg) + + path: str = "/appcenter/cisco/ndfc/api/v1/lan-fabric" + path += "/rest/resource-manager/resources" + path += f"?id={','.join(item)}" + args = SendToControllerArgs( + action="deploy", + path=path, + verb=RequestVerb.DELETE, + payload=None, + log_response=False, + is_rollback=False, + ) + self.send_to_controller(args) + + def release_orphaned_resources(self, vrf: str, is_rollback=False) -> None: + """ + # Summary + + Release orphaned resources. + + ## Description + + After a VRF delete operation, resources such as the TOP_DOWN_VRF_VLAN + resource below, can be orphaned from their VRFs. Below, notice that + resourcePool.vrfName is null. This method releases resources if + the following are true for the resources: + + - allocatedFlag is False + - entityName == vrf + - fabricName == self.fabric + + ```json + [ + { + "id": 36368, + "resourcePool": { + "id": 0, + "poolName": "TOP_DOWN_VRF_VLAN", + "fabricName": "f1", + "vrfName": null, + "poolType": "ID_POOL", + "dynamicSubnetRange": null, + "targetSubnet": 0, + "overlapAllowed": false, + "hierarchicalKey": "f1" + }, + "entityType": "Device", + "entityName": "VRF_1", + "allocatedIp": "201", + "allocatedOn": 1734040978066, + "allocatedFlag": false, + "allocatedScopeValue": "FDO211218GC", + "ipAddress": "172.22.150.103", + "switchName": "cvd-1312-leaf", + "hierarchicalKey": "0" + } + ] + ``` + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + path = "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/" + path += f"resource-manager/fabric/{self.fabric}/" + path += "pools/TOP_DOWN_VRF_VLAN" + + args = SendToControllerArgs( + action="release_resources", + path=path, + verb=RequestVerb.GET, + payload=None, + log_response=False, + is_rollback=False, + ) + self.send_to_controller(args) + resp = copy.deepcopy(self.response) + + generic_response = ControllerResponseGenericV12(**resp) + + fail, self.result["changed"] = self.handle_response(generic_response, action="release_resources") + + if fail: + if is_rollback: + self.failed_to_rollback = True + return + self.failure(resp) + + delete_ids: list = [] + for item in resp["DATA"]: + if "entityName" not in item: + continue + if item["entityName"] != vrf: + continue + if item.get("allocatedFlag") is not False: + continue + if item.get("id") is None: + continue + + msg = f"item {json.dumps(item, indent=4, sort_keys=True)}" + self.log.debug(msg) + + delete_ids.append(item["id"]) + + self.release_resources_by_id(delete_ids) + + def push_to_remote(self, is_rollback=False) -> None: + """ + # Summary + + Send all diffs to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if self.model_enabled: + self.push_to_remote_model(is_rollback=is_rollback) + return + + self.push_diff_create_update(is_rollback=is_rollback) + + # The detach and un-deploy operations are executed before the + # create,attach and deploy to address cases where a VLAN for vrf + # attachment being deleted is re-used on a new vrf attachment being + # created. This is needed specially for state: overridden + + self.push_diff_detach(is_rollback=is_rollback) + self.push_diff_undeploy(is_rollback=is_rollback) + + msg = "Calling self.push_diff_delete" + self.log.debug(msg) + + self.push_diff_delete(is_rollback=is_rollback) + for vrf_name in self.diff_delete: + self.release_orphaned_resources(vrf=vrf_name, is_rollback=is_rollback) + + self.push_diff_create(is_rollback=is_rollback) + self.push_diff_attach_model(is_rollback=is_rollback) + self.push_diff_deploy(is_rollback=is_rollback) + + def push_to_remote_model(self, is_rollback=False) -> None: + """ + # Summary + + Send all diffs to the controller + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + self.push_diff_create_update(is_rollback=is_rollback) + + # The detach and un-deploy operations are executed before the + # create,attach and deploy to address cases where a VLAN for vrf + # attachment being deleted is re-used on a new vrf attachment being + # created. This is needed specially for state: overridden + + self.push_diff_detach(is_rollback=is_rollback) + self.push_diff_undeploy(is_rollback=is_rollback) + + msg = "Calling self.push_diff_delete" + self.log.debug(msg) + + self.push_diff_delete(is_rollback=is_rollback) + for vrf_name in self.diff_delete: + self.release_orphaned_resources(vrf=vrf_name, is_rollback=is_rollback) + + self.push_diff_create(is_rollback=is_rollback) + self.push_diff_attach_model(is_rollback=is_rollback) + self.push_diff_deploy(is_rollback=is_rollback) + + def wait_for_vrf_del_ready(self, vrf_name: str = "not_supplied") -> None: + """ + # Summary + + Wait for VRFs to be ready for deletion. + + ## Raises + + Calls fail_json if VRF has associated network attachments. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + msg = f"vrf_name: {vrf_name}" + self.log.debug(msg) + + msg = "self.diff_delete: " + msg += f"{json.dumps(self.diff_delete, indent=4, sort_keys=True)}" + self.log.debug(msg) + + for vrf in self.diff_delete: + ok_to_delete: bool = False + path: str = self.paths["GET_VRF_ATTACH"].format(self.fabric, vrf) + + while not ok_to_delete: + args = SendToControllerArgs( + action="query", + path=path, + verb=RequestVerb.GET, + payload=None, + log_response=False, + is_rollback=False, + ) + self.send_to_controller(args) + + response = copy.deepcopy(self.response) + ok_to_delete = True + if response.get("DATA") is None: + time.sleep(self.wait_time_for_delete_loop) + continue + + msg = "response: " + msg += f"{json.dumps(response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + attach_list: list = response["DATA"][0]["lanAttachList"] + msg = f"ok_to_delete: {ok_to_delete}, " + msg += f"attach_list: {json.dumps(attach_list, indent=4)}" + self.log.debug(msg) + + attach: dict = {} + for attach in attach_list: + if attach["lanAttachState"] == "OUT-OF-SYNC" or attach["lanAttachState"] == "FAILED": + self.diff_delete.update({vrf: "OUT-OF-SYNC"}) + break + if attach["lanAttachState"] == "DEPLOYED" and attach["isLanAttached"] is True: + vrf_name = attach.get("vrfName", "unknown") + fabric_name: str = attach.get("fabricName", "unknown") + switch_ip: str = attach.get("ipAddress", "unknown") + switch_name: str = attach.get("switchName", "unknown") + vlan_id: str = attach.get("vlanId", "unknown") + msg = f"Network attachments associated with vrf {vrf_name} " + msg += "must be removed (e.g. using the dcnm_network module) " + msg += "prior to deleting the vrf. " + msg += f"Details: fabric_name: {fabric_name}, " + msg += f"vrf_name: {vrf_name}. " + msg += "Network attachments found on " + msg += f"switch_ip: {switch_ip}, " + msg += f"switch_name: {switch_name}, " + msg += f"vlan_id: {vlan_id}" + self.module.fail_json(msg=msg) + if attach["lanAttachState"] != "NA": + time.sleep(self.wait_time_for_delete_loop) + self.diff_delete.update({vrf: "DEPLOYED"}) + ok_to_delete = False + break + self.diff_delete.update({vrf: "NA"}) + + def validate_input(self) -> None: + """Parse the playbook values, validate to param specs.""" + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if self.state == "deleted": + self.validate_playbook_config_deleted_state() + elif self.state == "merged": + self.validate_playbook_config_merged_state() + elif self.state == "overridden": + self.validate_playbook_config_overridden_state() + elif self.state == "query": + self.validate_playbook_config_query_state() + elif self.state in ("replaced"): + self.validate_playbook_config_replaced_state() + + def validate_playbook_config(self) -> None: + """ + # Summary + + Validate self.config against PlaybookVrfModelV12 and update + self.validated_playbook_config with the validated config. + + ## Raises + + - Calls fail_json() if the playbook configuration could not be validated + + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if self.config is None: + return + for vrf_config in self.config: + try: + msg = "Validating playbook configuration." + self.log.debug(msg) + validated_playbook_config = PlaybookVrfModelV12(**vrf_config) + msg = "validated_playbook_config: " + msg += f"{json.dumps(validated_playbook_config.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + except ValidationError as error: + msg = f"Failed to validate playbook configuration. Error detail: {error}" + self.module.fail_json(msg=msg) + + self.validated_playbook_config.append(validated_playbook_config.model_dump()) + + msg = "self.validated_playbook_config: " + msg += f"{json.dumps(self.validated_playbook_config, indent=4, sort_keys=True)}" + self.log.debug(msg) + + def validate_playbook_config_model(self) -> None: + """ + # Summary + + Validate self.config against PlaybookVrfModelV12 and updates + self.validated_playbook_config_models with the validated config. + + ## Raises + + - Calls fail_json() if the playbook configuration could not be validated + + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + if not self.config: + msg = "Early return. self.config is empty." + self.log.debug(msg) + return + + for config in self.config: + try: + msg = "Validating playbook configuration." + self.log.debug(msg) + validated_playbook_config = PlaybookVrfModelV12(**config) + except ValidationError as error: + # We need to pass the unaltered ValidationError + # directly to the fail_json method for unit tests to pass. + self.module.fail_json(msg=error) + self.validated_playbook_config_models.append(validated_playbook_config) + + msg = "self.validated_playbook_config_models: " + self.log.debug(msg) + self.log_list_of_models(self.validated_playbook_config_models) + + def validate_playbook_config_deleted_state(self) -> None: + """ + # Summary + + Validate the input for deleted state. + """ + if self.state != "deleted": + return + if not self.config: + return + self.validate_playbook_config_model() + self.validate_playbook_config() + + def validate_playbook_config_merged_state(self) -> None: + """ + # Summary + + Validate the input for merged state. + """ + if self.state != "merged": + return + + if self.config is None: + self.config = [] + + method_name = inspect.stack()[0][3] + if len(self.config) == 0: + msg = f"{self.class_name}.{method_name}: " + msg += "config element is mandatory for merged state" + self.module.fail_json(msg=msg) + + self.validate_playbook_config_model() + self.validate_playbook_config() + + def validate_playbook_config_overridden_state(self) -> None: + """ + # Summary + + Validate the input for overridden state. + """ + if self.state != "overridden": + return + if not self.config: + return + self.validate_playbook_config_model() + self.validate_playbook_config() + + def validate_playbook_config_query_state(self) -> None: + """ + # Summary + + Validate the input for query state. + """ + if self.state != "query": + return + if not self.config: + return + self.validate_playbook_config_model() + self.validate_playbook_config() + + def validate_playbook_config_replaced_state(self) -> None: + """ + # Summary + + Validate the input for replaced state. + """ + if self.state != "replaced": + return + if not self.config: + return + self.validate_playbook_config_model() + self.validate_playbook_config() + + def handle_response_deploy(self, controller_response: ControllerResponseGenericV12) -> tuple: + """ + # Summary + + Handle the response from the controller for deploy operations. + + ## params + + - res: The response from the controller. + + ## Returns + + - fail: True if the response indicates a failure, else False + - changed: True if the response indicates a change, else False + + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + changed: bool = True + fail: bool = False + try: + response = ControllerResponseVrfsDeploymentsV12(**controller_response.model_dump()) + except ValueError as error: + msg = "Unable to parse response. " + msg += f"Error detail: {error}" + self.module.fail_json(msg=msg) + + msg = "ControllerResponseVrfsDeploymentsV12: " + msg += f"{json.dumps(response.model_dump(by_alias=True), indent=4, sort_keys=True)}" + self.log.debug(msg) + + if response.DATA == "No switches PENDING for deployment": + changed = False + if response.ERROR != "" or response.RETURN_CODE != 200 or response.MESSAGE != "OK": + fail = True + return fail, changed + + def handle_response(self, response_model: ControllerResponseGenericV12, action: str = "not_supplied") -> tuple: + """ + # Summary + + Handle the response from the controller. + + ## params + + - res: The response from the controller. + - action: The action that was performed. Current actions that are + passed to this method (some of which are not specifically handled) + are: + + - attach + - create (not specifically handled) + - deploy + - query + - release_resources (not specifically handled) + + ## Returns + + - fail: True if the response indicates a failure, else False + - changed: True if the response indicates a change, else False + + ## Example return + + - (True, False) # Indicates a failure, no change + - (False, True). # Indicates success, change + - (False, False) # Indicates success, no change + - (True, True) # Indicates a failure, change + + ## Raises + + - Calls fail_json() if the response is invalid + - Calls fail_json() if the response is not in the expected format + + """ + caller = inspect.stack()[1][3] + msg = f"ENTERED. caller {caller}, action {action}, self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + try: + msg = f"response_model: {json.dumps(response_model.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + except TypeError: + msg = f"response_model: {response_model.model_dump()}" + self.log.debug(msg) + + fail = False + changed = True + + if action == "deploy": + return self.handle_response_deploy(response_model) + + if action == "query": + # These if blocks handle responses to the query APIs. + # Basically all GET operations. + if response_model.ERROR == "Not Found" and response_model.RETURN_CODE == 404: + return True, False + if response_model.RETURN_CODE != 200 or response_model.MESSAGE != "OK": + return False, True + return False, False + + # Responses to all other operations POST and PUT are handled here. + if response_model.MESSAGE != "OK" or response_model.RETURN_CODE != 200: + fail = True + changed = False + return fail, changed + if response_model.ERROR != "": + fail = True + changed = False + if action == "attach" and "is in use already" in str(response_model.DATA): + fail = True + changed = False + + return fail, changed + + def failure(self, resp): + """ + # Summary + + Handle failures. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. self.model_enabled: {self.model_enabled}." + self.log.debug(msg) + + # Do not Rollback for Multi-site fabrics + if self.fabric_type == "MFD": + self.failed_to_rollback = True + self.module.fail_json(msg=resp) + return + + # Implementing a per task rollback logic here so that we rollback + # to the have state whenever there is a failure in any of the APIs. + # The idea would be to run overridden state with want=have and have=dcnm_state + self.want_create = self.have_create + self.want_attach = self.have_attach + self.want_deploy = self.have_deploy + + self.have_create = [] + self.have_attach = [] + self.have_deploy = {} + self.get_have() + self.get_diff_override() + + self.push_to_remote(is_rollback=True) + + if self.failed_to_rollback: + msg1 = "FAILED - Attempted rollback of the task has failed, " + msg1 += "may need manual intervention" + else: + msg1 = "SUCCESS - Attempted rollback of the task has succeeded" + + res = copy.deepcopy(resp) + res.update({"ROLLBACK_RESULT": msg1}) + + if not resp.get("DATA"): + data = copy.deepcopy(resp.get("DATA")) + if data.get("stackTrace"): + data.update({"stackTrace": "Stack trace is hidden, use '-vvvvv' to print it"}) + res.update({"DATA": data}) + + # pylint: disable=protected-access + if self.module._verbosity >= 5: + self.module.fail_json(msg=res) + # pylint: enable=protected-access + + self.module.fail_json(msg=res) diff --git a/plugins/module_utils/vrf/inventory_ipv4_to_serial_number.py b/plugins/module_utils/vrf/inventory_ipv4_to_serial_number.py new file mode 100644 index 000000000..d72178787 --- /dev/null +++ b/plugins/module_utils/vrf/inventory_ipv4_to_serial_number.py @@ -0,0 +1,110 @@ +import inspect +import logging + + +class InventoryIpv4ToSerialNumber: + """ + Given a fabric_inventory, convert a switch ipv4_address to a switch serial_number. + + ## Usage + + ```python + from plugins.module_utils.vrf.inventory_ipv4_to_serial_number import InventoryIpv4ToSerialNumber + fabric_inventory = { + "10.1.1.1": { + "serialNumber": "ABC123456", + # other switch details... + }, + } + + instance = InventoryIpv4ToSerialNumber() + instance.fabric_inventory = fabric_inventory + try: + serial_number_1 = instance.convert("10.1.1.1") + serial_number_2 = instance.convert("10.1.1.2") + # etc... + except ValueError as error: + print(f"Error: {error}") + ``` + """ + + def __init__(self): + """ + # Summary + + - Set class_name + - Initialize the logger + - Initialize class attributes + + # Raises + + - None + """ + self.class_name = self.__class__.__name__ + self._setup_logger() + self.fabric_inventory: dict = {} + + def _setup_logger(self) -> None: + """Initialize the logger.""" + self.log: logging.Logger = logging.getLogger(f"dcnm.{self.class_name}") + + def _validate_fabric_inventory(self) -> None: + """ + # Summary + + Validate that fabric_inventory is set and not empty. + + # Raises + + - ValueError: If fabric_inventory is not set or is empty. + """ + if not self.fabric_inventory: + msg = f"{self.class_name}: fabric_inventory is not set or is empty." + raise ValueError(msg) + + def convert(self, ipv4_address: str) -> str: + """ + # Summary + + Given a switch ipv4_address, return the switch serial_number. + + # Raises + + - ValueError if: + - instance.fabric_inventory is not set before calling this method. + - ipv4_address is not found in fabric_inventory. + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}" + self.log.debug(msg) + + self._validate_fabric_inventory() + data = self.fabric_inventory.get(ipv4_address, None) + if not data: + msg = f"{self.class_name}.{method_name}: caller {caller}. " + msg += f"ipv4_address {ipv4_address} not found in fabric_inventory." + raise ValueError(msg) + + serial_number = data.get("serialNumber", None) + if not serial_number: + msg = f"{self.class_name}.{method_name}: caller {caller}. " + msg += f"ipv4_address {ipv4_address} is missing serial_number in fabric_inventory." + raise ValueError(msg) + return serial_number + + @property + def fabric_inventory(self) -> dict: + """ + Return the fabric_inventory, which maps ipv4_address to switch_data. + """ + return self._fabric_inventory + + @fabric_inventory.setter + def fabric_inventory(self, value: dict): + """ + Set the fabric_inventory, which maps ipv4_address to switch_data. + """ + self._fabric_inventory = value diff --git a/plugins/module_utils/vrf/inventory_ipv4_to_switch_role.py b/plugins/module_utils/vrf/inventory_ipv4_to_switch_role.py new file mode 100644 index 000000000..6153f13d8 --- /dev/null +++ b/plugins/module_utils/vrf/inventory_ipv4_to_switch_role.py @@ -0,0 +1,110 @@ +import inspect +import logging + + +class InventoryIpv4ToSwitchRole: + """ + Given a fabric_inventory, convert a switch ipv4_address to switch's role (switchRole). + + ## Usage + + ```python + from plugins.module_utils.vrf.inventory_ipv4_to_serial_number import InventoryIpv4ToSwitchRole + fabric_inventory = { + "10.1.1.1": { + "switchRole": "leaf", + # other switch details... + }, + } + + instance = InventoryIpv4ToSwitchRole() + instance.fabric_inventory = fabric_inventory + try: + switch_role_1 = instance.convert("10.1.1.1") + switch_role_2 = instance.convert("10.1.1.2") + # etc... + except ValueError as error: + print(f"Error: {error}") + ``` + """ + + def __init__(self): + """ + # Summary + + - Set class_name + - Initialize the logger + - Initialize class attributes + + # Raises + + - None + """ + self.class_name = self.__class__.__name__ + self._setup_logger() + self.fabric_inventory: dict = {} + + def _setup_logger(self) -> None: + """Initialize the logger.""" + self.log: logging.Logger = logging.getLogger(f"dcnm.{self.class_name}") + + def _validate_fabric_inventory(self) -> None: + """ + # Summary + + Validate that fabric_inventory is set and not empty. + + # Raises + + - ValueError: If fabric_inventory is not set or is empty. + """ + if not self.fabric_inventory: + msg = f"{self.class_name}: fabric_inventory is not set or is empty." + raise ValueError(msg) + + def convert(self, ipv4_address: str) -> str: + """ + # Summary + + Given a switch ipv4_address, return the switch_role, e.g. leaf, spine. + + # Raises + + - ValueError if: + - instance.fabric_inventory is not set before calling this method. + - ipv4_address is not found in fabric_inventory. + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}" + self.log.debug(msg) + + self._validate_fabric_inventory() + data = self.fabric_inventory.get(ipv4_address, None) + if not data: + msg = f"{self.class_name}.{method_name}: caller {caller}. " + msg += f"ipv4_address {ipv4_address} not found in fabric_inventory." + raise ValueError(msg) + + switch_role = data.get("switchRole", None) + if not switch_role: + msg = f"{self.class_name}.{method_name}: caller {caller}. " + msg += f"ipv4_address {ipv4_address} is missing switch_role (switchRole) in fabric_inventory." + raise ValueError(msg) + return switch_role + + @property + def fabric_inventory(self) -> dict: + """ + Return the fabric_inventory, which maps ipv4_address to switch_data. + """ + return self._fabric_inventory + + @fabric_inventory.setter + def fabric_inventory(self, value: dict): + """ + Set the fabric_inventory, which maps ipv4_address to switch_data. + """ + self._fabric_inventory = value diff --git a/plugins/module_utils/vrf/inventory_serial_number_to_fabric_name.py b/plugins/module_utils/vrf/inventory_serial_number_to_fabric_name.py new file mode 100644 index 000000000..c1b50afae --- /dev/null +++ b/plugins/module_utils/vrf/inventory_serial_number_to_fabric_name.py @@ -0,0 +1,109 @@ +import inspect +import logging + + +class InventorySerialNumberToFabricName: + """ + Given a fabric_inventory, convert a switch serial_number to the hosting fabric_name of the switch. + + ## Usage + + ```python + from plugins.module_utils.vrf.inventory_ipv4_to_serial_number import InventorySerialNumberToFabricName + fabric_inventory = { + "10.1.1.1": { + "serialNumber": "ABC123456", + "fabricName": "MyFabric", + # other switch details... + }, + } + + instance = InventorySerialNumberToFabricName() + instance.fabric_inventory = fabric_inventory + try: + fabric_name_1 = instance.convert("ABC123456") + fabric_name_2 = instance.convert("CDE123456") + # etc... + except ValueError as error: + print(f"Error: {error}") + ``` + """ + + def __init__(self): + """ + # Summary + + - Set class_name + - Initialize the logger + - Initialize class attributes + + # Raises + + - None + """ + self.class_name = self.__class__.__name__ + self._setup_logger() + self.fabric_inventory: dict = {} + + def _setup_logger(self) -> None: + """Initialize the logger.""" + self.log: logging.Logger = logging.getLogger(f"dcnm.{self.class_name}") + + def _validate_fabric_inventory(self) -> None: + """ + # Summary + + Validate that fabric_inventory is set and not empty. + + # Raises + + - ValueError: If fabric_inventory is not set or is empty. + """ + if not self.fabric_inventory: + msg = f"{self.class_name}: fabric_inventory is not set or is empty." + raise ValueError(msg) + + def convert(self, serial_number: str) -> str: + """ + # Summary + + Given a switch serial_number, return the fabric_name of the fabric in which the switch resides. + + # Raises + + - ValueError if: + - instance.fabric_inventory is not set before calling this method. + - serial_number is not found in fabric_inventory. + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}" + self.log.debug(msg) + + self._validate_fabric_inventory() + for data in self.fabric_inventory.values(): + if data.get("serialNumber") != serial_number: + continue + fabric_name = data.get("fabricName") + + if not fabric_name: + msg = f"{self.class_name}.{method_name}: caller {caller}. " + msg += f"serial_number {serial_number} not found, or has no associated fabric." + raise ValueError(msg) + return fabric_name + + @property + def fabric_inventory(self) -> dict: + """ + Return the fabric_inventory, which maps ipv4_address to switch_data. + """ + return self._fabric_inventory + + @fabric_inventory.setter + def fabric_inventory(self, value: dict): + """ + Set the fabric_inventory, which maps ipv4_address to switch_data. + """ + self._fabric_inventory = value diff --git a/plugins/module_utils/vrf/inventory_serial_number_to_fabric_type.py b/plugins/module_utils/vrf/inventory_serial_number_to_fabric_type.py new file mode 100644 index 000000000..cab9f1389 --- /dev/null +++ b/plugins/module_utils/vrf/inventory_serial_number_to_fabric_type.py @@ -0,0 +1,110 @@ +import inspect +import logging + + +class InventorySerialNumberToFabricType: + """ + Given a fabric_inventory, convert a switch serial_number to the hosting fabric's fabricTechnology (fabric type). + + ## Usage + + ```python + from plugins.module_utils.vrf.inventory_ipv4_to_serial_number import InventorySerialNumberToFabricType + fabric_inventory = { + "10.1.1.1": { + "serialNumber": "ABC123456", + "fabricTechnology": "VXLANFabric", + # other switch details... + }, + } + + instance = InventorySerialNumberToFabricType() + instance.fabric_inventory = fabric_inventory + try: + fabric_type_1 = instance.convert("ABC123456") + fabric_type_2 = instance.convert("CDE123456") + # etc... + except ValueError as error: + print(f"Error: {error}") + ``` + """ + + def __init__(self): + """ + # Summary + + - Set class_name + - Initialize the logger + - Initialize class attributes + + # Raises + + - None + """ + self.class_name = self.__class__.__name__ + self._setup_logger() + self.fabric_inventory: dict = {} + + def _setup_logger(self) -> None: + """Initialize the logger.""" + self.log: logging.Logger = logging.getLogger(f"dcnm.{self.class_name}") + + def _validate_fabric_inventory(self) -> None: + """ + # Summary + + Validate that fabric_inventory is set and not empty. + + # Raises + + - ValueError: If fabric_inventory is not set or is empty. + """ + if not self.fabric_inventory: + msg = f"{self.class_name}: fabric_inventory is not set or is empty." + raise ValueError(msg) + + def convert(self, serial_number: str) -> str: + """ + # Summary + + Given a switch serial_number, return the fabric_type of the + fabric in which the switch resides. + + # Raises + + - ValueError if: + - instance.fabric_inventory is not set before calling this method. + - serial_number is not found in fabric_inventory. + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}" + self.log.debug(msg) + + self._validate_fabric_inventory() + for data in self.fabric_inventory.values(): + if data.get("serialNumber") != serial_number: + continue + fabric_type = data.get("fabricTechnology") + + if not fabric_type: + msg = f"{self.class_name}.{method_name}: caller {caller}. " + msg += f"serial_number {serial_number} not found, or has no associated fabric_type." + raise ValueError(msg) + return fabric_type + + @property + def fabric_inventory(self) -> dict: + """ + Return the fabric_inventory, which maps ipv4_address to switch_data. + """ + return self._fabric_inventory + + @fabric_inventory.setter + def fabric_inventory(self, value: dict): + """ + Set the fabric_inventory, which maps ipv4_address to switch_data. + """ + self._fabric_inventory = value diff --git a/plugins/module_utils/vrf/inventory_serial_number_to_ipv4.py b/plugins/module_utils/vrf/inventory_serial_number_to_ipv4.py new file mode 100644 index 000000000..38409fdc4 --- /dev/null +++ b/plugins/module_utils/vrf/inventory_serial_number_to_ipv4.py @@ -0,0 +1,105 @@ +import inspect +import logging + + +class InventorySerialNumberToIpv4: + """ + Given a fabric_inventory, convert a switch serial_number to an ipv4_address. + + ## Usage + + ```python + from plugins.module_utils.vrf.inventory_ipv4_to_serial_number import InventorySerialNumberToIpv4 + fabric_inventory = { + "10.1.1.1": { + "serialNumber": "ABC123456", + # other switch details... + }, + } + + instance = InventorySerialNumberToIpv4() + instance.fabric_inventory = fabric_inventory + try: + ipv4_address_1 = instance.convert("ABC123456") + ipv4_address_2 = instance.convert("CDE123456") + # etc... + except ValueError as error: + print(f"Error: {error}") + ``` + """ + + def __init__(self): + """ + # Summary + + - Set class_name + - Initialize the logger + - Initialize class attributes + + # Raises + + - None + """ + self.class_name = self.__class__.__name__ + self._setup_logger() + self.fabric_inventory: dict = {} + + def _setup_logger(self) -> None: + """Initialize the logger.""" + self.log: logging.Logger = logging.getLogger(f"dcnm.{self.class_name}") + + def _validate_fabric_inventory(self) -> None: + """ + # Summary + + Validate that fabric_inventory is set and not empty. + + # Raises + + - ValueError: If fabric_inventory is not set or is empty. + """ + if not self.fabric_inventory: + msg = f"{self.class_name}: fabric_inventory is not set or is empty." + raise ValueError(msg) + + def convert(self, serial_number: str) -> str: + """ + # Summary + + Given a switch serial_number, return the switch ipv4_address. + + # Raises + + - ValueError if: + - instance.fabric_inventory is not set before calling this method. + - serial_number is not found in fabric_inventory. + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}" + self.log.debug(msg) + + self._validate_fabric_inventory() + for ipv4_address, data in self.fabric_inventory.items(): + if data.get("serialNumber") == serial_number: + return ipv4_address + + msg = f"{self.class_name}.{method_name}: caller {caller}. " + msg += f"serial_number {serial_number} not found in fabric_inventory." + raise ValueError(msg) + + @property + def fabric_inventory(self) -> dict: + """ + Return the fabric_inventory, which maps ipv4_address to switch_data. + """ + return self._fabric_inventory + + @fabric_inventory.setter + def fabric_inventory(self, value: dict): + """ + Set the fabric_inventory, which maps ipv4_address to switch_data. + """ + self._fabric_inventory = value diff --git a/plugins/module_utils/vrf/inventory_serial_number_to_switch_role.py b/plugins/module_utils/vrf/inventory_serial_number_to_switch_role.py new file mode 100644 index 000000000..1fc9ff10a --- /dev/null +++ b/plugins/module_utils/vrf/inventory_serial_number_to_switch_role.py @@ -0,0 +1,110 @@ +import inspect +import logging + + +class InventorySerialNumberToSwitchRole: + """ + Given a fabric_inventory, convert a switch serial_number to the switch_role (switchRole) of the switch. + + ## Usage + + ```python + from plugins.module_utils.vrf.inventory_ipv4_to_serial_number import InventorySerialNumberToSwitchRole + fabric_inventory = { + "10.1.1.1": { + "serialNumber": "ABC123456", + "switchRole": "leaf", + # other switch details... + }, + } + + instance = InventorySerialNumberToSwitchRole() + instance.fabric_inventory = fabric_inventory + try: + switch_role_1 = instance.convert("ABC123456") + switch_role_2 = instance.convert("CDE123456") + # etc... + except ValueError as error: + print(f"Error: {error}") + ``` + """ + + def __init__(self): + """ + # Summary + + - Set class_name + - Initialize the logger + - Initialize class attributes + + # Raises + + - None + """ + self.class_name = self.__class__.__name__ + self._setup_logger() + self.fabric_inventory: dict = {} + + def _setup_logger(self) -> None: + """Initialize the logger.""" + self.log: logging.Logger = logging.getLogger(f"dcnm.{self.class_name}") + + def _validate_fabric_inventory(self) -> None: + """ + # Summary + + Validate that fabric_inventory is set and not empty. + + # Raises + + - ValueError: If fabric_inventory is not set or is empty. + """ + if not self.fabric_inventory: + msg = f"{self.class_name}: fabric_inventory is not set or is empty." + raise ValueError(msg) + + def convert(self, serial_number: str) -> str: + """ + # Summary + + Given a switch serial_number, return the switch_role of the switch. + + # Raises + + - ValueError if: + - instance.fabric_inventory is not set before calling this method. + - serial_number is not found in fabric_inventory. + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}" + self.log.debug(msg) + + self._validate_fabric_inventory() + switch_role = None + for data in self.fabric_inventory.values(): + if data.get("serialNumber") != serial_number: + continue + switch_role = data.get("switchRole") + + if not switch_role: + msg = f"{self.class_name}.{method_name}: caller {caller}. " + msg += f"serial_number {serial_number} not found, or has no associated switch_role." + raise ValueError(msg) + return switch_role + + @property + def fabric_inventory(self) -> dict: + """ + Return the fabric_inventory, which maps ipv4_address to switch_data. + """ + return self._fabric_inventory + + @fabric_inventory.setter + def fabric_inventory(self, value: dict): + """ + Set the fabric_inventory, which maps ipv4_address to switch_data. + """ + self._fabric_inventory = value diff --git a/plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py b/plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py new file mode 100644 index 000000000..cb1cbd8fe --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/vrf/vrf_playbook_model.py +# Copyright (c) 2020-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. +# pylint: disable=wrong-import-position +""" +Validation model for controller response to the following endpoint when the fabric type is Easy_Fabric (VXLAN_EVPN): + +- Verb: GET +- Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{fabricName} +""" +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class ControllerResponseFabricsEasyFabricGetNvPairs(BaseModel): + """ + # Summary + + Model representing the nvPairs configuration in the controller response for the following endpoint + when the fabric type is Easy_Fabric (VXLAN_EVPN): + + - Verb: GET + - Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{fabricName} + + # Raises + + ValueError if validation fails + """ + + AAA_REMOTE_IP_ENABLED: Optional[str] = Field(None, description="AAA remote IP enabled") + AAA_SERVER_CONF: Optional[str] = Field(None, description="AAA server configuration") + ACTIVE_MIGRATION: Optional[str] = Field(None, description="Active migration") + ADVERTISE_PIP_BGP: Optional[str] = Field(None, description="Advertise PIP BGP") + ADVERTISE_PIP_ON_BORDER: Optional[str] = Field(None, description="Advertise PIP on border") + AGENT_INTF: Optional[str] = Field(None, description="Agent interface") + AGG_ACC_VPC_PO_ID_RANGE: Optional[str] = Field(None, description="Aggregate access VPC PO ID range") + AI_ML_QOS_POLICY: Optional[str] = Field(None, description="AI/ML QoS policy") + ALLOW_L3VNI_NO_VLAN: Optional[str] = Field(None, description="Allow L3VNI no VLAN") + ALLOW_L3VNI_NO_VLAN_PREV: Optional[str] = Field(None, description="Allow L3VNI no VLAN previous") + ALLOW_NXC: Optional[str] = Field(None, description="Allow NXC") + ALLOW_NXC_PREV: Optional[str] = Field(None, description="Allow NXC previous") + ANYCAST_BGW_ADVERTISE_PIP: Optional[str] = Field(None, description="Anycast BGW advertise PIP") + ANYCAST_GW_MAC: Optional[str] = Field(None, description="Anycast GW MAC") + ANYCAST_LB_ID: Optional[str] = Field(None, description="Anycast LB ID") + ANYCAST_RP_IP_RANGE: Optional[str] = Field(None, description="Anycast RP IP range") + ANYCAST_RP_IP_RANGE_INTERNAL: Optional[str] = Field(None, description="Anycast RP IP range internal") + AUTO_SYMMETRIC_DEFAULT_VRF: Optional[str] = Field(None, description="Auto symmetric default VRF") + AUTO_SYMMETRIC_VRF_LITE: Optional[str] = Field(None, description="Auto symmetric VRF lite") + AUTO_UNIQUE_VRF_LITE_IP_PREFIX: Optional[str] = Field(None, description="Auto unique VRF lite IP prefix") + AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV: Optional[str] = Field(None, description="Auto unique VRF lite IP prefix previous") + AUTO_VRFLITE_IFC_DEFAULT_VRF: Optional[str] = Field(None, description="Auto VRF lite interface default VRF") + BANNER: Optional[str] = Field(None, description="Banner") + BFD_AUTH_ENABLE: Optional[str] = Field(None, description="BFD authentication enable") + BFD_AUTH_KEY: Optional[str] = Field(None, description="BFD authentication key") + BFD_AUTH_KEY_ID: Optional[str] = Field(None, description="BFD authentication key ID") + BFD_ENABLE: Optional[str] = Field(None, description="BFD enable") + BFD_ENABLE_PREV: Optional[str] = Field(None, description="BFD enable previous") + BFD_IBGP_ENABLE: Optional[str] = Field(None, description="BFD iBGP enable") + BFD_ISIS_ENABLE: Optional[str] = Field(None, description="BFD ISIS enable") + BFD_OSPF_ENABLE: Optional[str] = Field(None, description="BFD OSPF enable") + BFD_PIM_ENABLE: Optional[str] = Field(None, description="BFD PIM enable") + BGP_AS: str = Field(..., min_length=1, description="BGP AS") + BGP_AS_PREV: Optional[str] = Field(None, description="BGP AS previous") + BGP_AUTH_ENABLE: Optional[str] = Field(None, description="BGP authentication enable") + BGP_AUTH_KEY: Optional[str] = Field(None, description="BGP authentication key") + BGP_AUTH_KEY_TYPE: Optional[str] = Field(None, description="BGP authentication key type") + BGP_LB_ID: Optional[str] = Field(None, description="BGP LB ID") + BOOTSTRAP_CONF: Optional[str] = Field(None, description="Bootstrap configuration") + BOOTSTRAP_ENABLE: Optional[str] = Field(None, description="Bootstrap enable") + BOOTSTRAP_ENABLE_PREV: Optional[str] = Field(None, description="Bootstrap enable previous") + BOOTSTRAP_MULTISUBNET: Optional[str] = Field(None, description="Bootstrap multi-subnet") + BOOTSTRAP_MULTISUBNET_INTERNAL: Optional[str] = Field(None, description="Bootstrap multi-subnet internal") + BRFIELD_DEBUG_FLAG: Optional[str] = Field(None, description="BR field debug flag") + BROWNFIELD_NETWORK_NAME_FORMAT: Optional[str] = Field(None, description="Brownfield network name format") + BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS: Optional[str] = Field(None, description="Brownfield skip overlay network attachments") + CDP_ENABLE: Optional[str] = Field(None, description="CDP enable") + COPP_POLICY: Optional[str] = Field(None, description="COPP policy") + DCI_MACSEC_ALGORITHM: Optional[str] = Field(None, description="DCI MACsec algorithm") + DCI_MACSEC_CIPHER_SUITE: Optional[str] = Field(None, description="DCI MACsec cipher suite") + DCI_MACSEC_FALLBACK_ALGORITHM: Optional[str] = Field(None, description="DCI MACsec fallback algorithm") + DCI_MACSEC_FALLBACK_KEY_STRING: Optional[str] = Field(None, description="DCI MACsec fallback key string") + DCI_MACSEC_KEY_STRING: Optional[str] = Field(None, description="DCI MACsec key string") + DCI_SUBNET_RANGE: Optional[str] = Field(None, description="DCI subnet range") + DCI_SUBNET_TARGET_MASK: Optional[str] = Field(None, description="DCI subnet target mask") + DEAFULT_QUEUING_POLICY_CLOUDSCALE: Optional[str] = Field(None, description="Default queuing policy CloudScale") + DEAFULT_QUEUING_POLICY_OTHER: Optional[str] = Field(None, description="Default queuing policy other") + DEAFULT_QUEUING_POLICY_R_SERIES: Optional[str] = Field(None, description="Default queuing policy R-series") + DEFAULT_VRF_REDIS_BGP_RMAP: Optional[str] = Field(None, description="Default VRF Redis BGP route map") + DEPLOYMENT_FREEZE: Optional[str] = Field(None, description="Deployment freeze") + DHCP_ENABLE: Optional[str] = Field(None, description="DHCP enable") + DHCP_END: Optional[str] = Field(None, description="DHCP end") + DHCP_END_INTERNAL: Optional[str] = Field(None, description="DHCP end internal") + DHCP_IPV6_ENABLE: Optional[str] = Field(None, description="DHCP IPv6 enable") + DHCP_IPV6_ENABLE_INTERNAL: Optional[str] = Field(None, description="DHCP IPv6 enable internal") + DHCP_START: Optional[str] = Field(None, description="DHCP start") + DHCP_START_INTERNAL: Optional[str] = Field(None, description="DHCP start internal") + DNS_SERVER_IP_LIST: Optional[str] = Field(None, description="DNS server IP list") + DNS_SERVER_VRF: Optional[str] = Field(None, description="DNS server VRF") + DOMAIN_NAME_INTERNAL: Optional[str] = Field(None, description="Domain name internal") + ENABLE_AAA: Optional[str] = Field(None, description="Enable AAA") + ENABLE_AGENT: Optional[str] = Field(None, description="Enable agent") + ENABLE_AGG_ACC_ID_RANGE: Optional[str] = Field(None, description="Enable aggregate access ID range") + ENABLE_AI_ML_QOS_POLICY: Optional[str] = Field(None, description="Enable AI/ML QoS policy") + ENABLE_AI_ML_QOS_POLICY_FLAP: Optional[str] = Field(None, description="Enable AI/ML QoS policy flap") + ENABLE_DCI_MACSEC: Optional[str] = Field(None, description="Enable DCI MACsec") + ENABLE_DCI_MACSEC_PREV: Optional[str] = Field(None, description="Enable DCI MACsec previous") + ENABLE_DEFAULT_QUEUING_POLICY: Optional[str] = Field(None, description="Enable default queuing policy") + ENABLE_EVPN: Optional[str] = Field(None, description="Enable EVPN") + ENABLE_FABRIC_VPC_DOMAIN_ID: Optional[str] = Field(None, description="Enable fabric VPC domain ID") + ENABLE_FABRIC_VPC_DOMAIN_ID_PREV: Optional[str] = Field(None, description="Enable fabric VPC domain ID previous") + ENABLE_L3VNI_NO_VLAN: Optional[str] = Field(None, description="Enable L3VNI no VLAN") + ENABLE_MACSEC: Optional[str] = Field(None, description="Enable MACsec") + ENABLE_MACSEC_PREV: Optional[str] = Field(None, description="Enable MACsec previous") + ENABLE_NETFLOW: Optional[str] = Field(None, description="Enable NetFlow") + ENABLE_NETFLOW_PREV: Optional[str] = Field(None, description="Enable NetFlow previous") + ENABLE_NGOAM: Optional[str] = Field(None, description="Enable NGOAM") + ENABLE_NXAPI: Optional[str] = Field(None, description="Enable NXAPI") + ENABLE_NXAPI_HTTP: Optional[str] = Field(None, description="Enable NXAPI HTTP") + ENABLE_PBR: Optional[str] = Field(None, description="Enable PBR") + ENABLE_PVLAN: Optional[str] = Field(None, description="Enable PVLAN") + ENABLE_PVLAN_PREV: Optional[str] = Field(None, description="Enable PVLAN previous") + ENABLE_QKD: Optional[str] = Field(None, description="Enable QKD") + ENABLE_RT_INTF_STATS: Optional[str] = Field(None, description="Enable RT interface stats") + ENABLE_SGT: Optional[str] = Field(None, description="Enable SGT") + ENABLE_SGT_PREV: Optional[str] = Field(None, description="Enable SGT previous") + ENABLE_TENANT_DHCP: Optional[str] = Field(None, description="Enable tenant DHCP") + ENABLE_TRM: Optional[str] = Field(None, description="Enable TRM") + ENABLE_TRMv6: Optional[str] = Field(None, description="Enable TRMv6") + ENABLE_VPC_PEER_LINK_NATIVE_VLAN: Optional[str] = Field(None, description="Enable VPC peer link native VLAN") + ENABLE_VRI_ID_REALLOC: Optional[str] = Field(None, description="Enable VRI ID reallocation") + EXTRA_CONF_INTRA_LINKS: Optional[str] = Field(None, description="Extra config intra links") + EXTRA_CONF_LEAF: Optional[str] = Field(None, description="Extra config leaf") + EXTRA_CONF_SPINE: Optional[str] = Field(None, description="Extra config spine") + EXTRA_CONF_TOR: Optional[str] = Field(None, description="Extra config ToR") + EXT_FABRIC_TYPE: Optional[str] = Field(None, description="External fabric type") + FABRIC_INTERFACE_TYPE: Optional[str] = Field(None, description="Fabric interface type") + FABRIC_MTU: Optional[str] = Field(None, description="Fabric MTU") + FABRIC_MTU_PREV: Optional[str] = Field(None, description="Fabric MTU previous") + FABRIC_NAME: str = Field(..., description="Fabric name") + FABRIC_TYPE: Optional[str] = Field(None, description="Fabric type") + FABRIC_VPC_DOMAIN_ID: Optional[str] = Field(None, description="Fabric VPC domain ID") + FABRIC_VPC_DOMAIN_ID_PREV: Optional[str] = Field(None, description="Fabric VPC domain ID previous") + FABRIC_VPC_QOS: Optional[str] = Field(None, description="Fabric VPC QoS") + FABRIC_VPC_QOS_POLICY_NAME: Optional[str] = Field(None, description="Fabric VPC QoS policy name") + FEATURE_PTP: Optional[str] = Field(None, description="Feature PTP") + FEATURE_PTP_INTERNAL: Optional[str] = Field(None, description="Feature PTP internal") + FF: Optional[str] = Field(None, description="FF") + GRFIELD_DEBUG_FLAG: Optional[str] = Field(None, description="GR field debug flag") + HD_TIME: Optional[str] = Field(None, description="HD time") + HOST_INTF_ADMIN_STATE: Optional[str] = Field(None, description="Host interface admin state") + IBGP_PEER_TEMPLATE: Optional[str] = Field(None, description="iBGP peer template") + IBGP_PEER_TEMPLATE_LEAF: Optional[str] = Field(None, description="iBGP peer template leaf") + IGNORE_CERT: Optional[str] = Field(None, description="Ignore certificate") + INBAND_DHCP_SERVERS: Optional[str] = Field(None, description="Inband DHCP servers") + INBAND_MGMT: Optional[str] = Field(None, description="Inband management") + INBAND_MGMT_PREV: Optional[str] = Field(None, description="Inband management previous") + INTF_STAT_LOAD_INTERVAL: Optional[str] = Field(None, description="Interface stat load interval") + IPv6_ANYCAST_RP_IP_RANGE: Optional[str] = Field(None, description="IPv6 anycast RP IP range") + IPv6_ANYCAST_RP_IP_RANGE_INTERNAL: Optional[str] = Field(None, description="IPv6 anycast RP IP range internal") + IPv6_MULTICAST_GROUP_SUBNET: Optional[str] = Field(None, description="IPv6 multicast group subnet") + ISIS_AREA_NUM: Optional[str] = Field(None, description="ISIS area number") + ISIS_AREA_NUM_PREV: Optional[str] = Field(None, description="ISIS area number previous") + ISIS_AUTH_ENABLE: Optional[str] = Field(None, description="ISIS authentication enable") + ISIS_AUTH_KEY: Optional[str] = Field(None, description="ISIS authentication key") + ISIS_AUTH_KEYCHAIN_KEY_ID: Optional[str] = Field(None, description="ISIS authentication keychain key ID") + ISIS_AUTH_KEYCHAIN_NAME: Optional[str] = Field(None, description="ISIS authentication keychain name") + ISIS_LEVEL: Optional[str] = Field(None, description="ISIS level") + ISIS_OVERLOAD_ELAPSE_TIME: Optional[str] = Field(None, description="ISIS overload elapse time") + ISIS_OVERLOAD_ENABLE: Optional[str] = Field(None, description="ISIS overload enable") + ISIS_P2P_ENABLE: Optional[str] = Field(None, description="ISIS P2P enable") + KME_SERVER_IP: Optional[str] = Field(None, description="KME server IP") + KME_SERVER_PORT: Optional[str] = Field(None, description="KME server port") + L2_HOST_INTF_MTU: Optional[str] = Field(None, description="L2 host interface MTU") + L2_HOST_INTF_MTU_PREV: Optional[str] = Field(None, description="L2 host interface MTU previous") + L2_SEGMENT_ID_RANGE: Optional[str] = Field(None, description="L2 segment ID range") + L3VNI_IPv6_MCAST_GROUP: Optional[str] = Field(None, description="L3VNI IPv6 multicast group") + L3VNI_MCAST_GROUP: Optional[str] = Field(None, description="L3VNI multicast group") + L3_PARTITION_ID_RANGE: Optional[str] = Field(None, description="L3 partition ID range") + LINK_STATE_ROUTING: Optional[str] = Field(None, description="Link state routing") + LINK_STATE_ROUTING_TAG: Optional[str] = Field(None, description="Link state routing tag") + LINK_STATE_ROUTING_TAG_PREV: Optional[str] = Field(None, description="Link state routing tag previous") + LOOPBACK0_IPV6_RANGE: Optional[str] = Field(None, description="Loopback0 IPv6 range") + LOOPBACK0_IP_RANGE: Optional[str] = Field(None, description="Loopback0 IP range") + LOOPBACK1_IPV6_RANGE: Optional[str] = Field(None, description="Loopback1 IPv6 range") + LOOPBACK1_IP_RANGE: Optional[str] = Field(None, description="Loopback1 IP range") + MACSEC_ALGORITHM: Optional[str] = Field(None, description="MACsec algorithm") + MACSEC_CIPHER_SUITE: Optional[str] = Field(None, description="MACsec cipher suite") + MACSEC_FALLBACK_ALGORITHM: Optional[str] = Field(None, description="MACsec fallback algorithm") + MACSEC_FALLBACK_KEY_STRING: Optional[str] = Field(None, description="MACsec fallback key string") + MACSEC_KEY_STRING: Optional[str] = Field(None, description="MACsec key string") + MACSEC_REPORT_TIMER: Optional[str] = Field(None, description="MACsec report timer") + MGMT_GW: Optional[str] = Field(None, description="Management gateway") + MGMT_GW_INTERNAL: Optional[str] = Field(None, description="Management gateway internal") + MGMT_PREFIX: Optional[str] = Field(None, description="Management prefix") + MGMT_PREFIX_INTERNAL: Optional[str] = Field(None, description="Management prefix internal") + MGMT_V6PREFIX: Optional[str] = Field(None, description="Management V6 prefix") + MGMT_V6PREFIX_INTERNAL: Optional[str] = Field(None, description="Management V6 prefix internal") + MPLS_HANDOFF: Optional[str] = Field(None, description="MPLS handoff") + MPLS_ISIS_AREA_NUM: Optional[str] = Field(None, description="MPLS ISIS area number") + MPLS_ISIS_AREA_NUM_PREV: Optional[str] = Field(None, description="MPLS ISIS area number previous") + MPLS_LB_ID: Optional[str] = Field(None, description="MPLS LB ID") + MPLS_LOOPBACK_IP_RANGE: Optional[str] = Field(None, description="MPLS loopback IP range") + MSO_CONNECTIVITY_DEPLOYED: Optional[str] = Field(None, description="MSO connectivity deployed") + MSO_CONTROLER_ID: Optional[str] = Field(None, description="MSO controller ID") + MSO_SITE_GROUP_NAME: Optional[str] = Field(None, description="MSO site group name") + MSO_SITE_ID: Optional[str] = Field(None, description="MSO site ID") + MST_INSTANCE_RANGE: Optional[str] = Field(None, description="MST instance range") + MULTICAST_GROUP_SUBNET: Optional[str] = Field(None, description="Multicast group subnet") + MVPN_VRI_ID_RANGE: Optional[str] = Field(None, description="MVPN VRI ID range") + NETFLOW_EXPORTER_LIST: Optional[str] = Field(None, description="NetFlow exporter list") + NETFLOW_MONITOR_LIST: Optional[str] = Field(None, description="NetFlow monitor list") + NETFLOW_RECORD_LIST: Optional[str] = Field(None, description="NetFlow record list") + NETWORK_VLAN_RANGE: Optional[str] = Field(None, description="Network VLAN range") + NTP_SERVER_IP_LIST: Optional[str] = Field(None, description="NTP server IP list") + NTP_SERVER_VRF: Optional[str] = Field(None, description="NTP server VRF") + NVE_LB_ID: Optional[str] = Field(None, description="NVE LB ID") + NXAPI_HTTPS_PORT: Optional[str] = Field(None, description="NXAPI HTTPS port") + NXAPI_HTTP_PORT: Optional[str] = Field(None, description="NXAPI HTTP port") + NXC_DEST_VRF: Optional[str] = Field(None, description="NXC destination VRF") + NXC_PROXY_PORT: Optional[str] = Field(None, description="NXC proxy port") + NXC_PROXY_SERVER: Optional[str] = Field(None, description="NXC proxy server") + NXC_SRC_INTF: Optional[str] = Field(None, description="NXC source interface") + OBJECT_TRACKING_NUMBER_RANGE: Optional[str] = Field(None, description="Object tracking number range") + OSPF_AREA_ID: Optional[str] = Field(None, description="OSPF area ID") + OSPF_AUTH_ENABLE: Optional[str] = Field(None, description="OSPF authentication enable") + OSPF_AUTH_KEY: Optional[str] = Field(None, description="OSPF authentication key") + OSPF_AUTH_KEY_ID: Optional[str] = Field(None, description="OSPF authentication key ID") + OVERLAY_MODE: Optional[str] = Field(None, description="Overlay mode") + OVERLAY_MODE_PREV: Optional[str] = Field(None, description="Overlay mode previous") + OVERWRITE_GLOBAL_NXC: Optional[str] = Field(None, description="Overwrite global NXC") + PER_VRF_LOOPBACK_AUTO_PROVISION: Optional[str] = Field(None, description="Per VRF loopback auto provision") + PER_VRF_LOOPBACK_AUTO_PROVISION_PREV: Optional[str] = Field(None, description="Per VRF loopback auto provision previous") + PER_VRF_LOOPBACK_AUTO_PROVISION_V6: Optional[str] = Field(None, description="Per VRF loopback auto provision V6") + PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV: Optional[str] = Field(None, description="Per VRF loopback auto provision V6 previous") + PER_VRF_LOOPBACK_IP_RANGE: Optional[str] = Field(None, description="Per VRF loopback IP range") + PER_VRF_LOOPBACK_IP_RANGE_V6: Optional[str] = Field(None, description="Per VRF loopback IP range V6") + PFC_WATCH_INT: Optional[str] = Field(None, description="PFC watch interval") + PFC_WATCH_INT_PREV: Optional[str] = Field(None, description="PFC watch interval previous") + PHANTOM_RP_LB_ID1: Optional[str] = Field(None, description="Phantom RP LB ID1") + PHANTOM_RP_LB_ID2: Optional[str] = Field(None, description="Phantom RP LB ID2") + PHANTOM_RP_LB_ID3: Optional[str] = Field(None, description="Phantom RP LB ID3") + PHANTOM_RP_LB_ID4: Optional[str] = Field(None, description="Phantom RP LB ID4") + PIM_HELLO_AUTH_ENABLE: Optional[str] = Field(None, description="PIM hello authentication enable") + PIM_HELLO_AUTH_KEY: Optional[str] = Field(None, description="PIM hello authentication key") + PM_ENABLE: Optional[str] = Field(None, description="PM enable") + PM_ENABLE_PREV: Optional[str] = Field(None, description="PM enable previous") + PNP_ENABLE_INTERNAL: Optional[str] = Field(None, description="PNP enable internal") + POWER_REDUNDANCY_MODE: Optional[str] = Field(None, description="Power redundancy mode") + PREMSO_PARENT_FABRIC: Optional[str] = Field(None, description="Pre-MSO parent fabric") + PTP_DOMAIN_ID: Optional[str] = Field(None, description="PTP domain ID") + PTP_LB_ID: Optional[str] = Field(None, description="PTP LB ID") + PTP_VLAN_ID: Optional[str] = Field(None, description="PTP VLAN ID") + QKD_PROFILE_NAME: Optional[str] = Field(None, description="QKD profile name") + QKD_PROFILE_NAME_PREV: Optional[str] = Field(None, description="QKD profile name previous") + REPLICATION_MODE: Optional[str] = Field(None, description="Replication mode") + ROUTER_ID_RANGE: Optional[str] = Field(None, description="Router ID range") + ROUTE_MAP_SEQUENCE_NUMBER_RANGE: Optional[str] = Field(None, description="Route map sequence number range") + RP_COUNT: Optional[str] = Field(None, description="RP count") + RP_LB_ID: Optional[str] = Field(None, description="RP LB ID") + RP_MODE: Optional[str] = Field(None, description="RP mode") + RR_COUNT: Optional[str] = Field(None, description="RR count") + SEED_SWITCH_CORE_INTERFACES: Optional[str] = Field(None, description="Seed switch core interfaces") + SERVICE_NETWORK_VLAN_RANGE: Optional[str] = Field(None, description="Service network VLAN range") + SGT_ID_RANGE: Optional[str] = Field(None, description="SGT ID range") + SGT_NAME_PREFIX: Optional[str] = Field(None, description="SGT name prefix") + SGT_OPER_STATUS: Optional[str] = Field(None, description="SGT operational status") + SGT_PREPROVISION: Optional[str] = Field(None, description="SGT pre-provision") + SGT_PREPROVISION_PREV: Optional[str] = Field(None, description="SGT pre-provision previous") + SGT_PREPROV_RECALC_STATUS: Optional[str] = Field(None, description="SGT pre-provision recalc status") + SGT_RECALC_STATUS: Optional[str] = Field(None, description="SGT recalc status") + SITE_ID: Optional[str] = Field(None, description="Site ID") + SITE_ID_POLICY_ID: Optional[str] = Field(None, description="Site ID policy ID") + SLA_ID_RANGE: Optional[str] = Field(None, description="SLA ID range") + SNMP_SERVER_HOST_TRAP: Optional[str] = Field(None, description="SNMP server host trap") + SPINE_COUNT: Optional[str] = Field(None, description="Spine count") + SPINE_SWITCH_CORE_INTERFACES: Optional[str] = Field(None, description="Spine switch core interfaces") + SSPINE_ADD_DEL_DEBUG_FLAG: Optional[str] = Field(None, description="Super spine add/del debug flag") + SSPINE_COUNT: Optional[str] = Field(None, description="Super spine count") + STATIC_UNDERLAY_IP_ALLOC: Optional[str] = Field(None, description="Static underlay IP allocation") + STP_BRIDGE_PRIORITY: Optional[str] = Field(None, description="STP bridge priority") + STP_ROOT_OPTION: Optional[str] = Field(None, description="STP root option") + STP_VLAN_RANGE: Optional[str] = Field(None, description="STP VLAN range") + STRICT_CC_MODE: Optional[str] = Field(None, description="Strict CC mode") + SUBINTERFACE_RANGE: Optional[str] = Field(None, description="Subinterface range") + SUBNET_RANGE: Optional[str] = Field(None, description="Subnet range") + SUBNET_TARGET_MASK: Optional[str] = Field(None, description="Subnet target mask") + SYSLOG_SERVER_IP_LIST: Optional[str] = Field(None, description="Syslog server IP list") + SYSLOG_SERVER_VRF: Optional[str] = Field(None, description="Syslog server VRF") + SYSLOG_SEV: Optional[str] = Field(None, description="Syslog severity") + TCAM_ALLOCATION: Optional[str] = Field(None, description="TCAM allocation") + TOPDOWN_CONFIG_RM_TRACKING: Optional[str] = Field(None, description="Top-down config RM tracking") + TRUSTPOINT_LABEL: Optional[str] = Field(None, description="Trustpoint label") + UNDERLAY_IS_V6: Optional[str] = Field(None, description="Underlay is V6") + UNDERLAY_IS_V6_PREV: Optional[str] = Field(None, description="Underlay is V6 previous") + UNNUM_BOOTSTRAP_LB_ID: Optional[str] = Field(None, description="Unnumbered bootstrap LB ID") + UNNUM_DHCP_END: Optional[str] = Field(None, description="Unnumbered DHCP end") + UNNUM_DHCP_END_INTERNAL: Optional[str] = Field(None, description="Unnumbered DHCP end internal") + UNNUM_DHCP_START: Optional[str] = Field(None, description="Unnumbered DHCP start") + UNNUM_DHCP_START_INTERNAL: Optional[str] = Field(None, description="Unnumbered DHCP start internal") + UPGRADE_FROM_VERSION: Optional[str] = Field(None, description="Upgrade from version") + USE_LINK_LOCAL: Optional[str] = Field(None, description="Use link local") + V6_SUBNET_RANGE: Optional[str] = Field(None, description="V6 subnet range") + V6_SUBNET_TARGET_MASK: Optional[str] = Field(None, description="V6 subnet target mask") + VPC_AUTO_RECOVERY_TIME: Optional[str] = Field(None, description="VPC auto recovery time") + VPC_DELAY_RESTORE: Optional[str] = Field(None, description="VPC delay restore") + VPC_DELAY_RESTORE_TIME: Optional[str] = Field(None, description="VPC delay restore time") + VPC_DOMAIN_ID_RANGE: Optional[str] = Field(None, description="VPC domain ID range") + VPC_ENABLE_IPv6_ND_SYNC: Optional[str] = Field(None, description="VPC enable IPv6 ND sync") + VPC_PEER_KEEP_ALIVE_OPTION: Optional[str] = Field(None, description="VPC peer keep alive option") + VPC_PEER_LINK_PO: Optional[str] = Field(None, description="VPC peer link PO") + VPC_PEER_LINK_VLAN: Optional[str] = Field(None, description="VPC peer link VLAN") + VRF_LITE_AUTOCONFIG: Optional[str] = Field(None, description="VRF lite auto-config") + VRF_VLAN_RANGE: Optional[str] = Field(None, description="VRF VLAN range") + abstract_anycast_rp: Optional[str] = Field(None, description="Abstract anycast RP") + abstract_bgp: Optional[str] = Field(None, description="Abstract BGP") + abstract_bgp_neighbor: Optional[str] = Field(None, description="Abstract BGP neighbor") + abstract_bgp_rr: Optional[str] = Field(None, description="Abstract BGP RR") + abstract_dhcp: Optional[str] = Field(None, description="Abstract DHCP") + abstract_extra_config_bootstrap: Optional[str] = Field(None, description="Abstract extra config bootstrap") + abstract_extra_config_leaf: Optional[str] = Field(None, description="Abstract extra config leaf") + abstract_extra_config_spine: Optional[str] = Field(None, description="Abstract extra config spine") + abstract_extra_config_tor: Optional[str] = Field(None, description="Abstract extra config ToR") + abstract_feature_leaf: Optional[str] = Field(None, description="Abstract feature leaf") + abstract_feature_spine: Optional[str] = Field(None, description="Abstract feature spine") + abstract_isis: Optional[str] = Field(None, description="Abstract ISIS") + abstract_isis_interface: Optional[str] = Field(None, description="Abstract ISIS interface") + abstract_loopback_interface: Optional[str] = Field(None, description="Abstract loopback interface") + abstract_multicast: Optional[str] = Field(None, description="Abstract multicast") + abstract_ospf: Optional[str] = Field(None, description="Abstract OSPF") + abstract_ospf_interface: Optional[str] = Field(None, description="Abstract OSPF interface") + abstract_pim_interface: Optional[str] = Field(None, description="Abstract PIM interface") + abstract_route_map: Optional[str] = Field(None, description="Abstract route map") + abstract_routed_host: Optional[str] = Field(None, description="Abstract routed host") + abstract_trunk_host: Optional[str] = Field(None, description="Abstract trunk host") + abstract_vlan_interface: Optional[str] = Field(None, description="Abstract VLAN interface") + abstract_vpc_domain: Optional[str] = Field(None, description="Abstract VPC domain") + dcnmUser: Optional[str] = Field(None, description="DCNM user") + default_network: Optional[str] = Field(None, description="Default network") + default_pvlan_sec_network: Optional[str] = Field(None, description="Default PVLAN secondary network") + default_vrf: Optional[str] = Field(None, description="Default VRF") + enableRealTimeBackup: Optional[str] = Field(None, description="Enable real-time backup") + enableScheduledBackup: Optional[str] = Field(None, description="Enable scheduled backup") + network_extension_template: Optional[str] = Field(None, description="Network extension template") + scheduledTime: Optional[str] = Field(None, description="Scheduled time") + temp_anycast_gateway: Optional[str] = Field(None, description="Temp anycast gateway") + temp_vpc_domain_mgmt: Optional[str] = Field(None, description="Temp VPC domain management") + temp_vpc_peer_link: Optional[str] = Field(None, description="Temp VPC peer link") + vrf_extension_template: Optional[str] = Field(None, description="VRF extension template") + + +class ControllerResponseFabricsEasyFabricGet(BaseModel): + """ + Model representing the controller response for the following endpoint: + + - Verb: GET + - Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{fabricName} + + # Raises + + ValueError if validation fails + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + ) + + asn: str = Field(..., min_length=1, description="ASN number") + createdOn: int = Field(..., description="Creation timestamp") + deviceType: str = Field(..., description="Device type") + fabricId: str = Field(..., description="Fabric ID") + fabricName: str = Field(..., description="Fabric name") + fabricTechnology: str = Field(..., description="Fabric technology") + fabricTechnologyFriendly: str = Field(..., description="Fabric technology friendly name") + fabricType: str = Field(..., description="Fabric type") + fabricTypeFriendly: str = Field(..., description="Fabric type friendly name") + id: int = Field(..., description="Fabric ID") + modifiedOn: int = Field(..., description="Modification timestamp") + networkExtensionTemplate: str = Field(default="Default_Network_Extension_Universal", description="Network extension template") + networkTemplate: str = Field(default="Default_Network_Universal", description="Network template") + nvPairs: ControllerResponseFabricsEasyFabricGetNvPairs = Field(..., description="NVPairs configuration") + operStatus: str = Field(..., description="Operational status") + provisionMode: str = Field(..., description="Provisioning mode") + replicationMode: str = Field(..., description="Replication mode") + siteId: str = Field(..., description="Site ID") + templateFabricType: str = Field(..., description="Template fabric type") + templateName: str = Field(..., min_length=1, description="Template name") + vrfExtensionTemplate: str = Field(default="Default_VRF_Extension_Universal", min_length=1, description="VRF extension template") + vrfTemplate: str = Field(default="Default_VRF_Universal", min_length=1, description="VRF template") diff --git a/plugins/module_utils/vrf/model_controller_response_generic_v12.py b/plugins/module_utils/vrf/model_controller_response_generic_v12.py new file mode 100644 index 000000000..3b071d14c --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_generic_v12.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from typing import Any, Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class ControllerResponseGenericV12(BaseModel): + """ + # Summary + + Generic response model for the controller. + + ## Raises + + ValueError if validation fails + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + ) + + DATA: Optional[Any] = Field(default="") + ERROR: Optional[str] = Field(default="") + MESSAGE: Optional[str] = Field(default="") + METHOD: Optional[str] = Field(default="") + REQUEST_PATH: Optional[str] = Field(default="") + RETURN_CODE: Optional[int] = Field(default=500) diff --git a/plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py b/plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py new file mode 100644 index 000000000..bba432b11 --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + +from .model_controller_response_generic_v12 import ControllerResponseGenericV12 + + +class DataVrfInfo(BaseModel): + """ + # Summary + + Data model for VRF information. + + ## Raises + + ValueError if validation fails + + ## Structure + + - l3vni: int - The Layer 3 VNI. + - vrf_prefix: str - The prefix for the VRF. + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + validate_by_alias=True, + ) + + l3_vni: int = Field(alias="l3vni") + vrf_prefix: str = Field(alias="vrf-prefix") + + +class ControllerResponseGetFabricsVrfinfoV12(ControllerResponseGenericV12): + """ + # Summary + + Response model for a request to the controller for the following endpoint. + + Verb: GET + Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric}/vrfinfo + + ## Raises + + ValueError if validation fails + + ## Controller response + + ```json + { + "l3vni": 50000, + "vrf-prefix": "MyVRF_" + } + ``` + + ## Structure + + - DATA: DataVrfInfo - JSON containing l3vni and vrf-prefix. + - ERROR: Optional[str] - Error message if any error occurred. + - MESSAGE: Optional[str] - Additional message. + - METHOD: Optional[str] - The HTTP method used for the request. + - REQUEST_PATH: Optional[str] - The request path for the controller. + - RETURN_CODE: Optional[int] - The HTTP return code, default is 500. + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + ) + + DATA: DataVrfInfo + ERROR: Optional[str] = Field(default="") + MESSAGE: Optional[str] = Field(default="") + METHOD: Optional[str] = Field(default="") + REQUEST_PATH: Optional[str] = Field(default="") + RETURN_CODE: Optional[int] = Field(default=500) diff --git a/plugins/module_utils/vrf/model_controller_response_get_int.py b/plugins/module_utils/vrf/model_controller_response_get_int.py new file mode 100644 index 000000000..26c7fc741 --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_get_int.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from typing import Optional + +from pydantic import ConfigDict, Field + +from .model_controller_response_generic_v12 import ControllerResponseGenericV12 + + +class ControllerResponseGetIntV12(ControllerResponseGenericV12): + """ + # Summary + + Response model for a GET request to the controller that returns an integer. + + ## Raises + + ValueError if validation fails + + ## Structure + + - DATA: int - The integer data returned by the controller. + - ERROR: Optional[str] - Error message if any error occurred. + - MESSAGE: Optional[str] - Additional message. + - METHOD: Optional[str] - The HTTP method used for the request. + - REQUEST_PATH: Optional[str] - The request path for the controller. + - RETURN_CODE: Optional[int] - The HTTP return code, default is 500. + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + ) + + DATA: int + ERROR: Optional[str] = Field(default="") + MESSAGE: Optional[str] = Field(default="") + METHOD: Optional[str] = Field(default="") + REQUEST_PATH: Optional[str] = Field(default="") + RETURN_CODE: Optional[int] = Field(default=500) diff --git a/plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py b/plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py new file mode 100644 index 000000000..2302b1e55 --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +""" +Validation model for controller responses for the following endpoint: + +Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs/attachments?vrf-names={vrf1,vrf2,...} +Verb: GET +""" +from typing import Optional, Union + +from pydantic import BaseModel, ConfigDict, Field + +from .model_controller_response_generic_v12 import ControllerResponseGenericV12 + + +class ControllerResponseLanAttachItem(BaseModel): + """ + # Summary + + A lanAttachList item (see ControllerResponseVrfsAttachmentsDataItem in this file) + + ## Structure + + - `entity_name`: str = alias "entityName" + - `fabric_name`: str - alias "fabricName", max_length=64 + - `instance_values`: Optional[str] = alias="instanceValues" + - `ip_address`: str = alias="ipAddress" + - `is_lan_attached`: bool = alias="isLanAttached" + - `lan_attach_state`: str = alias="lanAttachState" + - `peer_serial_no`: Optional[str] = alias="peerSerialNo", default=None + - `switch_name`: str = alias="switchName" + - `switch_role`: str = alias="switchRole" + - `switch_serial_no`: str = alias="switchSerialNo" + - `vlan_id`: Union[int, None] = alias="vlanId", ge=2, le=4094 + - `vrf_id`: Union[int, None] = alias="vrfId", ge=1, le=16777214 + - `vrf_name`: str = alias="vrfName", min_length=1, max_length=32 + """ + + entity_name: Optional[str] = Field(alias="entityName", default="") + fabric_name: str = Field(alias="fabricName", max_length=64) + instance_values: Optional[str] = Field(alias="instanceValues", default="") + ip_address: str = Field(alias="ipAddress") + is_lan_attached: bool = Field(alias="isLanAttached") + lan_attach_state: str = Field(alias="lanAttachState") + peer_serial_no: Optional[str] = Field(alias="peerSerialNo", default=None) + switch_name: str = Field(alias="switchName") + switch_role: str = Field(alias="switchRole") + switch_serial_no: str = Field(alias="switchSerialNo") + vlan_id: Union[int, None] = Field(alias="vlanId", ge=2, le=4094) + vrf_id: Union[int, None] = Field(alias="vrfId", ge=1, le=16777214) + vrf_name: str = Field(alias="vrfName", min_length=1, max_length=32) + + +class ControllerResponseVrfsAttachmentsDataItem(BaseModel): + """ + # Summary + + A data item in the response for the VRFs attachments endpoint. + + # Structure + + - `lan_attach_list`: list[ControllerResponseLanAttachItem] - alias "lanAttachList" + - `vrf_name`: str - alias "vrfName" + + ## Notes + + `instanceValues` is shortened for brevity in the example. It is a JSON string with the following fields: + + - deviceSupportL3VniNoVlan + - loopbackId + - loopbackIpAddress + - loopbackIpV6Address + - switchRouteTargetExportEvpn + - switchRouteTargetImportEvpn + + ## Example + + ```json + { + "lanAttachList": [ + { + "entityName": "ansible-vrf-int2", + "fabricName": "f1", + "instanceValues": "{\"field1\": \"value1\", \"field2\": \"value2\"}", + "ipAddress": "172.22.150.112", + "isLanAttached": true, + "lanAttachState": "DEPLOYED", + "peerSerialNo": null, + "switchName": "cvd-1211-spine", + "switchRole": "border spine", + "switchSerialNo": "FOX2109PGCS", + "vlanId": 1500, + "vrfId": 9008012, + "vrfName": "ansible-vrf-int2" + } + ], + "vrfName": "ansible-vrf-int1" + } + ``` + """ + + lan_attach_list: list[ControllerResponseLanAttachItem] = Field(alias="lanAttachList") + vrf_name: str = Field(alias="vrfName") + + +class ControllerResponseVrfsAttachmentsV12(ControllerResponseGenericV12): + """ + # Summary + + Controller response model for the following endpoint. + + # Endpoint + + ## Verb + + GET + + ## Path: + + /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs/attachments?vrf-names={vrf1,vrf2,...} + + # Raises + + ValueError if validation fails + + # Structure + + ## Notes + + `instanceValues` is shortened for brevity in the example. It is a JSON string with the following fields: + + - deviceSupportL3VniNoVlan + - loopbackId + - loopbackIpAddress + - loopbackIpV6Address + - switchRouteTargetExportEvpn + - switchRouteTargetImportEvpn + + ## Example + + ```json + { + "DATA": [ + { + "lanAttachList": [ + { + "entityName": "ansible-vrf-int1", + "fabricName": "f1", + "instanceValues": "{\"field1\": \"value1\", \"field2\": \"value2\"}", + "ipAddress": "10.1.2.3", + "isLanAttached": true, + "lanAttachState": "DEPLOYED", + "peerSerialNo": null, + "switchName": "cvd-1211-spine", + "switchRole": "border spine", + "switchSerialNo": "ABC1234DEFG", + "vlanId": 500, + "vrfId": 9008011, + "vrfName": "ansible-vrf-int1" + }, + ], + "vrfName": "ansible-vrf-int1" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://192.168.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/f1/vrfs/attachments?vrf-names=ansible-vrf-int1", + "RETURN_CODE": 200 + } + ``` + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + use_enum_values=True, + validate_assignment=True, + validate_by_alias=True, + validate_by_name=True, + ) + DATA: list[ControllerResponseVrfsAttachmentsDataItem] + MESSAGE: str + METHOD: str + REQUEST_PATH: str + RETURN_CODE: int diff --git a/plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py b/plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py new file mode 100644 index 000000000..022e69318 --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +""" +Validation model for controller responses related to the following endpoint: + +Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs/deployments +Verb: POST +""" + +import warnings +from typing import Optional, Union + +from pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning + +from .model_controller_response_generic_v12 import ControllerResponseGenericV12 + +warnings.filterwarnings("ignore", category=PydanticExperimentalWarning) +warnings.filterwarnings("ignore", category=UserWarning) + +# Base configuration for the Vrf* models +base_vrf_model_config = ConfigDict( + str_strip_whitespace=True, + use_enum_values=True, + validate_assignment=True, +) + + +class VrfDeploymentsDataDictV12(BaseModel): + """ + # Summary + + Validation model for the DATA field within the controller response to + the following endpoint, for the case where DATA is a dictionary. + + ## Endpoint + + - Verb: POST + - Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs/deployments + + ## Raises + + ValueError if validation fails + + ## Structure + + ```json + { + "status": "Deployment of VRF(s) has been initiated successfully", + } + ``` + """ + + model_config = base_vrf_model_config + + status: str = Field( + default="", + description="Status of the VRF deployment.", + ) + + +class ControllerResponseVrfsDeploymentsV12(ControllerResponseGenericV12): + """ + # Summary + + Validation model for the controller response to the following endpoint: + + ## Endpoint + + - Verb: POST + - Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs/deployments + + ## Raises + + - `ValueError` if validation fails + + ## Structure + + ### NOTES + + - DATA.status has been observed to contain the following values + - "Deployment of VRF(s) has been initiated successfully" + - "No switches PENDING for deployment." + + ```json + { + "DATA": { + "status": "Deployment of VRF(s) has been initiated successfully" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/f1/vrfs/deployments", + "RETURN_CODE": 200 + } + ``` + """ + + DATA: Optional[Union[VrfDeploymentsDataDictV12, str]] = Field(default="") + ERROR: Optional[str] = Field(default="") + MESSAGE: Optional[str] = Field(default="") + METHOD: Optional[str] = Field(default="") + REQUEST_PATH: Optional[str] = Field(default="") + RETURN_CODE: Optional[int] = Field(default=500) diff --git a/plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py b/plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py new file mode 100644 index 000000000..a701e2bd5 --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +""" +Validation model for controller responses related to the following endpoint: + +Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs/switches?vrf-names=ansible-vrf-int1&serial-numbers={serial1,serial2} +Verb: GET +""" +import json +from typing import Any, List, Optional, Union + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from .model_controller_response_generic_v12 import ControllerResponseGenericV12 + + +class ControllerResponseVrfsSwitchesVrfLiteConnProtoItem(BaseModel): + asn: Optional[str] = Field(default="", alias="asn") + auto_vrf_lite_flag: Optional[str] = Field(default="", alias="AUTO_VRF_LITE_FLAG") + dot1q_id: Optional[str] = Field(default="", alias="DOT1Q_ID") + enable_border_extension: Optional[str] = Field(default="", alias="enableBorderExtension") + if_name: Optional[str] = Field(default="", alias="IF_NAME") + ip_mask: Optional[str] = Field(default="", alias="IP_MASK") + ipv6_mask: Optional[str] = Field(default="", alias="IPV6_MASK") + ipv6_neighbor: Optional[str] = Field(default="", alias="IPV6_NEIGHBOR") + mtu: Optional[str] = Field(default="", alias="MTU") + neighbor_asn: Optional[str] = Field(default="", alias="NEIGHBOR_ASN") + neighbor_ip: Optional[str] = Field(default="", alias="NEIGHBOR_IP") + peer_vrf_name: Optional[str] = Field(default="", alias="PEER_VRF_NAME") + vrf_lite_jython_template: Optional[str] = Field(default="", alias="VRF_LITE_JYTHON_TEMPLATE") + + +class ControllerResponseVrfsSwitchesExtensionPrototypeValue(BaseModel): + dest_interface_name: Optional[str] = Field(default="", alias="destInterfaceName") + dest_switch_name: Optional[str] = Field(default="", alias="destSwitchName") + extension_type: Optional[str] = Field(default="", alias="extensionType") + extension_values: ControllerResponseVrfsSwitchesVrfLiteConnProtoItem = Field( + default=ControllerResponseVrfsSwitchesVrfLiteConnProtoItem().model_construct(), alias="extensionValues" + ) + interface_name: Optional[str] = Field(default="", alias="interfaceName") + + @field_validator("extension_values", mode="before") + @classmethod + def preprocess_extension_values(cls, data: Any) -> ControllerResponseVrfsSwitchesVrfLiteConnProtoItem: + """ + Convert incoming data + + - If data is a JSON string, use json.loads() to convert to a dict. + - If data is a dict, convert it to an ControllerResponseVrfsSwitchesVrfLiteConnProtoItem instance. + - If data is already an ControllerResponseVrfsSwitchesVrfLiteConnProtoItem instance, return as-is. + """ + if isinstance(data, str): + if data == "": + return ControllerResponseVrfsSwitchesVrfLiteConnProtoItem().model_construct() + data = json.loads(data) + return ControllerResponseVrfsSwitchesVrfLiteConnProtoItem(**data) + if isinstance(data, dict): + data = ControllerResponseVrfsSwitchesVrfLiteConnProtoItem(**data) + return data + + +class ControllerResponseVrfsSwitchesInstanceValues(BaseModel): + """ + ```json + { + "loopbackId": "", + "loopbackIpAddress": "", + "loopbackIpV6Address": "", + "switchRouteTargetExportEvpn": "5000:100", + "switchRouteTargetImportEvpn": "5000:100" + } + ``` + """ + + loopback_id: Optional[str] = Field(default="", alias="loopbackId") + loopback_ip_address: Optional[str] = Field(default="", alias="loopbackIpAddress") + loopback_ipv6_address: Optional[str] = Field(default="", alias="loopbackIpV6Address") + switch_route_target_export_evpn: Optional[str] = Field(default="", alias="switchRouteTargetExportEvpn") + switch_route_target_import_evpn: Optional[str] = Field(default="", alias="switchRouteTargetImportEvpn") + + +class ControllerResponseVrfsSwitchesMultisiteConnOuterItem(BaseModel): + pass + + +class VrfLiteConnOuterItem(BaseModel): + # We set the default value to "NA", which we can check later in dcnm_vrf_v12.py + # to ascertain whether the model was populated with switch data. + auto_vrf_lite_flag: Optional[str] = Field(default="NA", alias="AUTO_VRF_LITE_FLAG") + dot1q_id: Optional[str] = Field(default="", alias="DOT1Q_ID") + if_name: Optional[str] = Field(default="", alias="IF_NAME") + ip_mask: Optional[str] = Field(default="", alias="IP_MASK") + ipv6_mask: Optional[str] = Field(default="", alias="IPV6_MASK") + ipv6_neighbor: Optional[str] = Field(default="", alias="IPV6_NEIGHBOR") + neighbor_asn: Optional[str] = Field(default="", alias="NEIGHBOR_ASN") + neighbor_ip: Optional[str] = Field(default="", alias="NEIGHBOR_IP") + peer_vrf_name: Optional[str] = Field(default="", alias="PEER_VRF_NAME") + vrf_lite_jython_template: Optional[str] = Field(default="", alias="VRF_LITE_JYTHON_TEMPLATE") + + +class ControllerResponseVrfsSwitchesMultisiteConnOuter(BaseModel): + multisite_conn: Optional[List[ControllerResponseVrfsSwitchesMultisiteConnOuterItem]] = Field( + default=[ControllerResponseVrfsSwitchesMultisiteConnOuterItem().model_construct()], alias="MULTISITE_CONN" + ) + + +class ControllerResponseVrfsSwitchesVrfLiteConnOuter(BaseModel): + vrf_lite_conn: Optional[List[VrfLiteConnOuterItem]] = Field(default=[VrfLiteConnOuterItem().model_construct()], alias="VRF_LITE_CONN") + + +class ControllerResponseVrfsSwitchesExtensionValuesOuter(BaseModel): + vrf_lite_conn: Optional[ControllerResponseVrfsSwitchesVrfLiteConnOuter] = Field( + default=ControllerResponseVrfsSwitchesVrfLiteConnOuter().model_construct(), alias="VRF_LITE_CONN" + ) + multisite_conn: Optional[ControllerResponseVrfsSwitchesMultisiteConnOuter] = Field( + default=ControllerResponseVrfsSwitchesMultisiteConnOuter().model_construct(), alias="MULTISITE_CONN" + ) + + @field_validator("multisite_conn", mode="before") + @classmethod + def preprocess_multisite_conn(cls, data: Any) -> ControllerResponseVrfsSwitchesMultisiteConnOuter: + """ + Convert incoming data + + - If data is a JSON string, use json.loads() to convert to a dict. + - If data is a dict, convert it to an ControllerResponseVrfsSwitchesMultisiteConnOuter instance. + - If data is already an ControllerResponseVrfsSwitchesMultisiteConnOuter instance, return as-is. + """ + if isinstance(data, str): + if data in ["", "{}"]: + return ControllerResponseVrfsSwitchesMultisiteConnOuter().model_construct() + return ControllerResponseVrfsSwitchesMultisiteConnOuter(**json.loads(data)) + if isinstance(data, dict): + data = ControllerResponseVrfsSwitchesMultisiteConnOuter(**data) + return data + + @field_validator("vrf_lite_conn", mode="before") + @classmethod + def preprocess_vrf_lite_conn(cls, data: Any) -> ControllerResponseVrfsSwitchesVrfLiteConnOuter: + """ + Convert incoming data + + - If data is a JSON string, use json.loads() to convert to a dict. + - If data is a dict, convert it to an ControllerResponseVrfsSwitchesVrfLiteConnOuter instance. + - If data is already an ControllerResponseVrfsSwitchesVrfLiteConnOuter instance, return as-is. + """ + if isinstance(data, str): + if data in ["", "{}"]: + return ControllerResponseVrfsSwitchesVrfLiteConnOuter().model_construct() + return ControllerResponseVrfsSwitchesVrfLiteConnOuter(**json.loads(data)) + if isinstance(data, dict): + data = ControllerResponseVrfsSwitchesVrfLiteConnOuter(**data) + return data + + +class ControllerResponseVrfsSwitchesSwitchDetails(BaseModel): + error_message: Union[str, None] = Field(alias="errorMessage") + extension_prototype_values: Optional[List[ControllerResponseVrfsSwitchesExtensionPrototypeValue]] = Field( + default=[ControllerResponseVrfsSwitchesExtensionPrototypeValue().model_construct()], alias="extensionPrototypeValues" + ) + extension_values: Optional[ControllerResponseVrfsSwitchesExtensionValuesOuter] = Field( + default=ControllerResponseVrfsSwitchesExtensionValuesOuter().model_construct(), alias="extensionValues" + ) + freeform_config: Union[str, None] = Field(alias="freeformConfig") + instance_values: Optional[ControllerResponseVrfsSwitchesInstanceValues] = Field( + default=ControllerResponseVrfsSwitchesInstanceValues().model_construct(), alias="instanceValues" + ) + is_lan_attached: bool = Field(alias="islanAttached") + lan_attached_state: str = Field(alias="lanAttachedState") + peer_serial_number: Union[str, None] = Field(alias="peerSerialNumber") + role: str + serial_number: str = Field(alias="serialNumber") + switch_name: str = Field(alias="switchName") + vlan: int = Field(alias="vlan", ge=2, le=4094) + vlan_modifiable: bool = Field(alias="vlanModifiable") + + @field_validator("extension_prototype_values", mode="before") + @classmethod + def preprocess_extension_prototype_values(cls, data: Any) -> ControllerResponseVrfsSwitchesExtensionPrototypeValue: + """ + Convert incoming data + + - If data is a JSON string, use json.loads() to convert to a dict. + - If data is a list, convert it to a list of ControllerResponseVrfsSwitchesExtensionPrototypeValue instance. + - If data is already an ControllerResponseVrfsSwitchesExtensionPrototypeValue model, return as-is. + """ + if isinstance(data, str): + if data == "": + return ControllerResponseVrfsSwitchesExtensionPrototypeValue().model_construct() + if isinstance(data, list): + for instance in data: + if isinstance(instance, dict): + instance = ControllerResponseVrfsSwitchesExtensionPrototypeValue(**instance) + return data + + @field_validator("extension_values", mode="before") + @classmethod + def preprocess_extension_values(cls, data: Any) -> Union[ControllerResponseVrfsSwitchesExtensionValuesOuter, str]: + """ + Convert incoming data + + - If data is a JSON string, use json.loads() to convert to a dict. + - If data is a dict, convert it to an ControllerResponseVrfsSwitchesExtensionValuesOuter instance. + - If data is already an ControllerResponseVrfsSwitchesExtensionValuesOuter instance, return as-is. + """ + if isinstance(data, str): + if data in ["", "{}"]: + return ControllerResponseVrfsSwitchesExtensionValuesOuter().model_construct() + return ControllerResponseVrfsSwitchesExtensionValuesOuter(**json.loads(data)) + if isinstance(data, dict): + data = ControllerResponseVrfsSwitchesExtensionValuesOuter(**data) + return data + + @field_validator("instance_values", mode="before") + @classmethod + def preprocess_instance_values(cls, data: Any) -> ControllerResponseVrfsSwitchesInstanceValues: + """ + Convert incoming data + + - If data is a JSON string, use json.loads() to convert to a dict. + - If data is a dict, convert it to an ControllerResponseVrfsSwitchesInstanceValues instance. + - If data is already an ControllerResponseVrfsSwitchesInstanceValues instance, return as-is. + """ + if isinstance(data, str): + if data in ["", "{}"]: + return ControllerResponseVrfsSwitchesInstanceValues().model_construct() + return ControllerResponseVrfsSwitchesInstanceValues(**json.loads(data)) + if isinstance(data, dict): + data = ControllerResponseVrfsSwitchesInstanceValues(**data) + return data + + +class ControllerResponseVrfsSwitchesDataItem(BaseModel): + switch_details_list: List[ControllerResponseVrfsSwitchesSwitchDetails] = Field(alias="switchDetailsList") + template_name: str = Field(alias="templateName") + vrf_name: str = Field(alias="vrfName") + + +class ControllerResponseVrfsSwitchesV12(ControllerResponseGenericV12): + """ + # Summary + Validation model for the controller response to the following endpoint: + Verb: POST + + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + ) + + DATA: List[ControllerResponseVrfsSwitchesDataItem] + MESSAGE: str + METHOD: str + REQUEST_PATH: str + RETURN_CODE: int diff --git a/plugins/module_utils/vrf/model_controller_response_vrfs_v12.py b/plugins/module_utils/vrf/model_controller_response_vrfs_v12.py new file mode 100644 index 000000000..773167020 --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_vrfs_v12.py @@ -0,0 +1,158 @@ +""" +Validation model for payloads conforming the expectations of the +following endpoint: + +Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs +Verb: POST +""" + +import warnings +from typing import Optional, Union + +from pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning, model_validator +from typing_extensions import Self + +from .model_controller_response_generic_v12 import ControllerResponseGenericV12 +from .vrf_template_config_v12 import VrfTemplateConfigV12 + +warnings.filterwarnings("ignore", category=PydanticExperimentalWarning) +warnings.filterwarnings("ignore", category=UserWarning) + +base_vrf_model_config = ConfigDict( + str_strip_whitespace=True, + use_enum_values=True, + validate_assignment=True, + populate_by_name=True, + populate_by_alias=True, +) + + +class VrfObjectV12(BaseModel): + """ + # Summary + + Validation model for the DATA within the controller response to + the following endpoint: + + Verb: GET + Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs + + ## Raises + + ValueError if validation fails + + ## Details + + Note, vrfTemplateConfig is received as a JSON string and converted by + VrfObjectV12 into a dictionary so that its parameters can be validated. + It should be converted back into a JSON string before sending to the + controller. + + One way to do this is to dump this model into VrfPayloadV12, which will + convert the vrfTemplateConfig into a JSON string when it is dumped. + + For example: + + ```python + from .vrf_controller_payload_v12 import VrfPayloadV12 + from .model_controller_response_vrfs_v12 import VrfObjectV12 + + vrf_object = VrfObjectV12(**vrf_object_dict) + vrf_payload = VrfPayloadV12(**vrf_object.model_dump(exclude_unset=True, by_alias=True)) + dcnm_send(self.module, "POST", url, vrf_payload.model_dump(exclude_unset=True, by_alias=True)) + ``` + + ## Structure + ```json + { + "fabric": "fabric_1", + "vrfName": "vrf_1", + "vrfTemplate": "Default_VRF_Universal", + "vrfTemplateConfig": { + "advertiseDefaultRouteFlag": true, + "advertiseHostRouteFlag": false, + "asn": "65002", + "bgpPassword": "", + "bgpPasswordKeyType": 3, + "configureStaticDefaultRouteFlag": true, + "disableRtAuto": false, + "ENABLE_NETFLOW": false, + "ipv6LinkLocalFlag": true, + "isRPAbsent": false, + "isRPExternal": false, + "L3VniMcastGroup": "", + "maxBgpPaths": 1, + "maxIbgpPaths": 2, + "multicastGroup": "", + "mtu": 9216, + "NETFLOW_MONITOR": "", + "nveId": 1, + "routeTargetExport": "", + "routeTargetExportEvpn": "", + "routeTargetExportMvpn": "", + "routeTargetImport": "", + "routeTargetImportEvpn": "", + "routeTargetImportMvpn": "", + "rpAddress": "", + "tag": 12345, + "trmBGWMSiteEnabled": false, + "trmEnabled": false, + "vrfDescription": "", + "vrfIntfDescription": "", + "vrfName": "my_vrf", + "vrfRouteMap": "FABRIC-RMAP-REDIST-SUBNET", + "vrfSegmentId": 50022, + "vrfVlanId": 10, + "vrfVlanName": "vlan10" + }, + "tenantName": "", + "vrfId": 50011, + "serviceVrfTemplate": "", + "hierarchicalKey": "fabric_1" + } + ``` + """ + + model_config = base_vrf_model_config + + fabric: str = Field(..., max_length=64, description="Fabric name in which the VRF resides.") + hierarchicalKey: str = Field(default="", max_length=64) + serviceVrfTemplate: Union[str, None] = Field(default=None) + source: Union[str, None] = Field(default=None) + tenantName: Union[str, None] = Field(default=None) + vrfExtensionTemplate: str = Field(default="Default_VRF_Extension_Universal") + vrfId: int = Field(..., ge=1, le=16777214) + vrfName: str = Field(..., min_length=1, max_length=32, description="Name of the VRF, 1-32 characters.") + vrfStatus: Optional[str] = Field(default="") + vrfTemplate: str = Field(default="Default_VRF_Universal") + vrfTemplateConfig: VrfTemplateConfigV12 + + @model_validator(mode="after") + def validate_hierarchical_key(self) -> Self: + """ + If hierarchicalKey is "", set it to the fabric name. + """ + if self.hierarchicalKey == "": + self.hierarchicalKey = self.fabric + return self + + +class ControllerResponseVrfsV12(ControllerResponseGenericV12): + """ + # Summary + + Validation model for the controller response to the following endpoint: + + Verb: GET + Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs + + ## Raises + + ValueError if validation fails + """ + + DATA: Optional[list[VrfObjectV12] | str] = Field(default=[]) + ERROR: Optional[str] = Field(default="") + MESSAGE: Optional[str] = Field(default="") + METHOD: Optional[str] = Field(default="") + RETURN_CODE: Optional[int] = Field(default=500) diff --git a/plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py b/plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py new file mode 100644 index 000000000..606619879 --- /dev/null +++ b/plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from typing import List, Optional, Union + +from pydantic import BaseModel, ConfigDict, Field + + +class HaveLanAttachItem(BaseModel): + """ + # Summary + + A single lan attach item within lanAttachList. + + ## Structure + + - deployment: bool, alias: deployment + - extension_values: Optional[str], alias: extensionValues, default="" + - fabric: str (min_length=1, max_length=64), alias: fabricName + - freeform_config: Optional[str], alias: freeformConfig, default="" + - instance_values: Optional[str], alias: instanceValues, default="" + - is_attached: bool, alias: isAttached + - is_deploy: bool, alias: is_deploy + - serial_number: str, alias: serialNumber + - vlan: Union(int | None), alias: vlanId + - vrf_name: str (min_length=1, max_length=32), alias: vrfName + """ + + deployment: bool = Field(alias="deployment") + extension_values: Optional[str] = Field(alias="extensionValues", default="") + fabric: str = Field(alias="fabricName", min_length=1, max_length=64) + freeform_config: Optional[str] = Field(alias="freeformConfig", default="") + instance_values: Optional[str] = Field(alias="instanceValues", default="") + is_attached: bool = Field(alias="isAttached") + is_deploy: bool = Field(alias="is_deploy") + serial_number: str = Field(alias="serialNumber") + vlan: Union[int | None] = Field(alias="vlanId") + vrf_name: str = Field(alias="vrfName", min_length=1, max_length=32) + + +class HaveAttachPostMutate(BaseModel): + """ + # Summary + + Validates a mutated VRF attachment. + + See NdfcVrf12.populate_have_attach_model + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + validate_by_alias=True, + validate_by_name=True, + ) + + lan_attach_list: List[HaveLanAttachItem] = Field(alias="lanAttachList") + vrf_name: str = Field(alias="vrfName") diff --git a/plugins/module_utils/vrf/model_payload_vrfs_attachments.py b/plugins/module_utils/vrf/model_payload_vrfs_attachments.py new file mode 100644 index 000000000..93c0aa2ea --- /dev/null +++ b/plugins/module_utils/vrf/model_payload_vrfs_attachments.py @@ -0,0 +1,522 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/vrf/vrf_playbook_model.py +# Copyright (c) 2020-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. +# pylint: disable=wrong-import-position +""" +Validation model for VRF attachment payload. +""" +import json +from typing import Optional, Union + +from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator + +from ..common.models.ipv4_cidr_host import IPv4CidrHostModel +from ..common.models.ipv4_host import IPv4HostModel +from ..common.models.ipv6_cidr_host import IPv6CidrHostModel + + +class PayloadVrfsAttachmentsLanAttachListExtensionValuesMultisiteConn(BaseModel): + """ + # Summary + + Represents the multisite connection values for a single lan attach item within VrfAttachPayload.lan_attach_list. + + # Structure + + - MULTISITE_CONN: list, alias: MULTISITE_CONN + + ## Example + + ```json + { + "MULTISITE_CONN": [] + } + } + ``` + """ + + MULTISITE_CONN: list = Field(alias="MULTISITE_CONN", default_factory=list) + + +class PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConnItem(BaseModel): + """ + # Summary + + Represents a single VRF Lite connection item within VrfAttachPayload.lan_attach_list. + + # Structure + + - AUTO_VRF_LITE_FLAG: bool, alias: AUTO_VRF_LITE_FLAG + - DOT1Q_ID: str, alias: DOT1Q_ID + - IF_NAME: str, alias: IF_NAME + - IP_MASK: str, alias: IP_MASK + - IPV6_MASK: str, alias: IPV6_MASK + - IPV6_NEIGHBOR: str, alias: IPV6_NEIGHBOR + - NEIGHBOR_ASN: str, alias: NEIGHBOR_ASN + - NEIGHBOR_IP: str, alias: NEIGHBOR_IP + - PEER_VRF_NAME: str, alias: PEER_VRF_NAME + - VRF_LITE_JYTHON_TEMPLATE: str, alias: VRF_LITE_JYTHON_TEMPLATE + + ## Example + + ```json + { + "AUTO_VRF_LITE_FLAG": "true", + "DOT1Q_ID": "2", + "IF_NAME": "Ethernet2/10", + "IP_MASK": "10.33.0.2/30", + "IPV6_MASK": "2010::10:34:0:7/64", + "IPV6_NEIGHBOR": "2010::10:34:0:3", + "NEIGHBOR_ASN": "65001", + "NEIGHBOR_IP": "10.33.0.1", + "PEER_VRF_NAME": "ansible-vrf-int1", + "VRF_LITE_JYTHON_TEMPLATE": "Ext_VRF_Lite_Jython" + } + ``` + """ + + AUTO_VRF_LITE_FLAG: bool = Field(alias="AUTO_VRF_LITE_FLAG", default=True) + DOT1Q_ID: str = Field(alias="DOT1Q_ID") + IF_NAME: str = Field(alias="IF_NAME") + IP_MASK: str = Field(alias="IP_MASK", default="") + IPV6_MASK: str = Field(alias="IPV6_MASK", default="") + IPV6_NEIGHBOR: str = Field(alias="IPV6_NEIGHBOR", default="") + NEIGHBOR_ASN: str = Field(alias="NEIGHBOR_ASN", default="") + NEIGHBOR_IP: str = Field(alias="NEIGHBOR_IP", default="") + PEER_VRF_NAME: str = Field(alias="PEER_VRF_NAME", default="") + VRF_LITE_JYTHON_TEMPLATE: str = Field(alias="VRF_LITE_JYTHON_TEMPLATE") + + @field_validator("IP_MASK", mode="before") + @classmethod + def validate_ip_mask(cls, value: str) -> str: + """ + Validate IP_MASK to ensure it is a valid IPv4 CIDR host address. + """ + if value == "": + return value + try: + return IPv4CidrHostModel(ipv4_cidr_host=value).ipv4_cidr_host + except ValueError as error: + msg = f"Invalid IP_MASK: {value}. detail: {error}" + raise ValueError(msg) from error + + @field_validator("IPV6_MASK", mode="before") + @classmethod + def validate_ipv6_mask(cls, value: str) -> str: + """ + Validate IPV6_MASK to ensure it is a valid IPv6 CIDR host address. + """ + if value == "": + return value + try: + return IPv6CidrHostModel(ipv6_cidr_host=value).ipv6_cidr_host + except ValueError as error: + msg = f"Invalid IPV6_MASK: {value}. detail: {error}" + raise ValueError(msg) from error + + @field_validator("NEIGHBOR_IP", mode="before") + @classmethod + def validate_neighbor_ip(cls, value: str) -> str: + """ + Validate NEIGHBOR_IP to ensure it is a valid IPv4 host address without prefix length. + """ + if value == "": + return value + try: + return IPv4HostModel(ipv4_host=value).ipv4_host + except ValueError as error: + msg = f"Invalid neighbor IP address (NEIGHBOR_IP): {value}. detail: {error}" + raise ValueError(msg) from error + + @field_serializer("AUTO_VRF_LITE_FLAG") + def serialize_auto_vrf_lite_flag(self, value) -> str: + """ + Serialize AUTO_VRF_LITE_FLAG to a string representation. + """ + return str(value).lower() + + +class PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConn(BaseModel): + """ + # Summary + + Represents a list of PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConnItem. + + # Structure + + - VRF_LITE_CONN: list[PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConnItem], alias: VRF_LITE_CONN + + ## Example + + ```json + { + "VRF_LITE_CONN": [ + { + "AUTO_VRF_LITE_FLAG": "true", + "DOT1Q_ID": "2", + "IF_NAME": "Ethernet2/10", + "IP_MASK": "10.33.0.2/30", + "IPV6_MASK": "2010::10:34:0:7/64", + "IPV6_NEIGHBOR": "2010::10:34:0:3", + "NEIGHBOR_ASN": "65001", + "NEIGHBOR_IP": "10.33.0.1", + "PEER_VRF_NAME": "ansible-vrf-int1", + "VRF_LITE_JYTHON_TEMPLATE": "Ext_VRF_Lite_Jython" + } + ] + } + ``` + """ + + VRF_LITE_CONN: list[PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConnItem] = Field(alias="VRF_LITE_CONN", default_factory=list) + + +class PayloadVrfsAttachmentsLanAttachListExtensionValues(BaseModel): + """ + # Summary + + Represents the extension values for a single lan attach item within VrfAttachPayload.lan_attach_list. + + # Structure + + # Example + + ```json + { + 'MULTISITE_CONN': {'MULTISITE_CONN': []}, + 'VRF_LITE_CONN': { + 'VRF_LITE_CONN': [ + { + 'AUTO_VRF_LITE_FLAG': 'true', + 'DOT1Q_ID': '2', + 'IF_NAME': 'Ethernet2/10', + 'IP_MASK': '10.33.0.2/30', + 'IPV6_MASK': '2010::10:34:0:7/64', + 'IPV6_NEIGHBOR': '2010::10:34:0:3', + 'NEIGHBOR_ASN': '65001', + 'NEIGHBOR_IP': '10.33.0.1', + 'PEER_VRF_NAME': 'ansible-vrf-int1', + 'VRF_LITE_JYTHON_TEMPLATE': 'Ext_VRF_Lite_Jython' + } + ] + } + } + ``` + """ + + MULTISITE_CONN: PayloadVrfsAttachmentsLanAttachListExtensionValuesMultisiteConn = Field( + alias="MULTISITE_CONN", + default_factory=PayloadVrfsAttachmentsLanAttachListExtensionValuesMultisiteConn, + ) + VRF_LITE_CONN: PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConn = Field( + alias="VRF_LITE_CONN", + default_factory=PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConn, + ) + + @field_serializer("MULTISITE_CONN") + def serialize_multisite_conn(self, value: PayloadVrfsAttachmentsLanAttachListExtensionValuesMultisiteConn) -> str: + """ + Serialize MULTISITE_CONN to a JSON string. + """ + return value.model_dump_json(by_alias=True) + + @field_serializer("VRF_LITE_CONN") + def serialize_vrf_lite_conn(self, value: PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConn) -> str: + """ + Serialize VRF_LITE_CONN to a JSON string. + """ + return value.model_dump_json(by_alias=True) + + @field_validator("MULTISITE_CONN", mode="before") + @classmethod + def preprocess_multisite_conn(cls, value: Union[str, dict]) -> Optional[PayloadVrfsAttachmentsLanAttachListExtensionValuesMultisiteConn]: + """ + Convert incoming data + + - If data is a JSON string, use json.loads() to convert to a dict. + - If data is a dict, return it as is. + """ + if isinstance(value, str): + if value == "": + return "" + return json.loads(value) + return value + + @field_validator("VRF_LITE_CONN", mode="before") + @classmethod + def preprocess_vrf_lite_conn(cls, value: dict) -> Optional[PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConn]: + """ + Convert incoming data + + - If data is a JSON string, use json.loads() to convert to a dict. + - If data is a dict, return it as is. + """ + if isinstance(value, str): + if value == "": + return "" + return json.loads(value) + return value + + +class PayloadVrfsAttachmentsLanAttachListInstanceValues(BaseModel): + """ + # Summary + + Represents the instance values for a single lan attach item within VrfAttachPayload.lan_attach_list. + + # Structure + + - loopback_id: str, alias: loopbackId + - loopback_ip_address: str, alias: loopbackIpAddress + - loopback_ip_v6_address: str, alias: loopbackIpV6Address + - switch_route_target_import_evpn: str, alias: switchRouteTargetImportEvpn + - switch_route_target_export_evpn: str, alias: switchRouteTargetExportEvpn + + ## Example + + ```json + { + "loopbackId": "1", + "loopbackIpAddress": "10.1.1.1", + "loopbackIpV6Address": "f16c:f7ec:cfa2:e1c5:9a3c:cb08:801f:36b8", + "switchRouteTargetImportEvpn": "5000:100", + "switchRouteTargetExportEvpn": "5000:100" + } + ``` + """ + + loopback_id: str = Field(alias="loopbackId", default="") + loopback_ip_address: str = Field(alias="loopbackIpAddress", default="") + loopback_ipv6_address: str = Field(alias="loopbackIpV6Address", default="") + switch_route_target_import_evpn: str = Field(alias="switchRouteTargetImportEvpn", default="") + switch_route_target_export_evpn: str = Field(alias="switchRouteTargetExportEvpn", default="") + + @field_validator("loopback_ip_address", mode="before") + @classmethod + def validate_loopback_ip_address(cls, value: str) -> str: + """ + Validate loopback_ip_address to ensure it is a valid IPv4 CIDR host. + """ + if value == "": + return value + try: + return IPv4CidrHostModel(ipv4_cidr_host=value).ipv4_cidr_host + except ValueError as error: + msg = f"Invalid loopback IP address (loopback_ip_address): {value}. detail: {error}" + raise ValueError(msg) from error + + @field_validator("loopback_ipv6_address", mode="before") + @classmethod + def validate_loopback_ipv6_address(cls, value: str) -> str: + """ + Validate loopback_ipv6_address to ensure it is a valid IPv6 CIDR host. + """ + if value == "": + return value + try: + return IPv6CidrHostModel(ipv6_cidr_host=value).ipv6_cidr_host + except ValueError as error: + msg = f"Invalid loopback IPv6 address (loopback_ipv6_address): {value}. detail: {error}" + raise ValueError(msg) from error + + +class PayloadVrfsAttachmentsLanAttachListItem(BaseModel): + """ + # Summary + + A single lan attach item within VrfAttachPayload.lan_attach_list. + + # Structure + + - deployment: bool, alias: deployment, default=False + - extension_values: Optional[str], alias: extensionValues, default="" + - fabric: str (min_length=1, max_length=64), alias: fabric + - freeform_config: Optional[str], alias: freeformConfig, default="" + - instance_values: Optional[str], alias: instanceValues, default="" + - serial_number: str, alias: serialNumber + - vlan_id: int, alias: vlanId + - vrf_name: str (min_length=1, max_length=32), alias: vrfName + + ## Notes + + 1. extensionValues in the example is shortened for brevity. It is a JSON string with the following structure:: + + ```json + { + 'MULTISITE_CONN': {'MULTISITE_CONN': []}, + 'VRF_LITE_CONN': { + 'VRF_LITE_CONN': [ + { + 'AUTO_VRF_LITE_FLAG': 'true', + 'DOT1Q_ID': '2', + 'IF_NAME': 'Ethernet2/10', + 'IP_MASK': '10.33.0.2/30', + 'IPV6_MASK': '2010::10:34:0:7/64', + 'IPV6_NEIGHBOR': '2010::10:34:0:3', + 'NEIGHBOR_ASN': '65001', + 'NEIGHBOR_IP': '10.33.0.1', + 'PEER_VRF_NAME': 'ansible-vrf-int1', + 'VRF_LITE_JYTHON_TEMPLATE': 'Ext_VRF_Lite_Jython' + } + ] + } + } + ``` + + 2. instanceValues in the example is shortened for brevity. It is a JSON string with the following fields: + + - It has the following structure: + + + - instanceValues in the example is shortened for brevity. It is a JSON string with the following fields: + - loopbackId: str + - loopbackIpAddress: str + - loopbackIpV6Address: str + - switchRouteTargetImportEvpn: str + - switchRouteTargetExportEvpn: str + ## Example + + ```json + { + "deployment": true, + "extensionValues": "{\"field1\":\"field1_value\",\"field2\":\"field2_value\"}", + "fabric": "f1", + "freeformConfig": "", + "instanceValues": "{\"field1\":\"field1_value\",\"field2\":\"field2_value\"}", + "serialNumber": "FOX2109PGD0", + "vlan": 0, + "vrfName": "ansible-vrf-int1" + } + ``` + """ + + deployment: bool = Field(alias="deployment") + extension_values: Optional[PayloadVrfsAttachmentsLanAttachListExtensionValues] = Field( + alias="extensionValues", + default=PayloadVrfsAttachmentsLanAttachListExtensionValues.model_construct(), + ) + fabric: str = Field(alias="fabric", min_length=1, max_length=64) + freeform_config: Optional[str] = Field(alias="freeformConfig", default="") + instance_values: Optional[PayloadVrfsAttachmentsLanAttachListInstanceValues] = Field(alias="instanceValues", default="") + serial_number: str = Field(alias="serialNumber") + vlan: int = Field(alias="vlan") + vrf_name: str = Field(alias="vrfName", min_length=1, max_length=32) + + @field_validator("extension_values", mode="before") + @classmethod + def preprocess_extension_values(cls, value: Union[dict, str]) -> PayloadVrfsAttachmentsLanAttachListExtensionValues: + """ + # Summary + + - If data is a JSON string, use json.loads() to convert to a dict and pipe it to the model. + - If data is an empty string, return an empty PayloadVrfsAttachmentsLanAttachListExtensionValues instance. + - If data is a dict, pipe it to the model. + - If data is already a PayloadVrfsAttachmentsLanAttachListExtensionValues instance, return it as is. + + # Raises + - ValueError: If the value is not a valid type (not dict, str, or PayloadVrfsAttachmentsLanAttachListExtensionValues). + - ValueError: If the JSON string cannot be parsed into a dictionary. + """ + if isinstance(value, str): + if value == "": + return PayloadVrfsAttachmentsLanAttachListExtensionValues.model_construct() + try: + value = json.loads(value) + return PayloadVrfsAttachmentsLanAttachListExtensionValues(**value) + except json.JSONDecodeError as error: + msg = f"Invalid JSON string for extension_values: {value}. detail: {error}" + raise ValueError(msg) from error + if isinstance(value, dict): + return PayloadVrfsAttachmentsLanAttachListExtensionValues(**value) + if isinstance(value, PayloadVrfsAttachmentsLanAttachListExtensionValues): + return value + msg = f"Invalid type for extension_values: {type(value)}. " + msg += "Expected dict, str, or PayloadVrfsAttachmentsLanAttachListExtensionValues." + raise ValueError(msg) + + @field_serializer("extension_values") + def serialize_extension_values(self, value: PayloadVrfsAttachmentsLanAttachListExtensionValues) -> str: + """ + Serialize extension_values to a JSON string. + """ + if value == "": + return json.dumps({}) + if len(value.MULTISITE_CONN.MULTISITE_CONN) == 0 and len(value.VRF_LITE_CONN.VRF_LITE_CONN) == 0: + return json.dumps({}) + result = {} + if len(value.MULTISITE_CONN.MULTISITE_CONN) == 0 and len(value.VRF_LITE_CONN.VRF_LITE_CONN) == 0: + return json.dumps(result) + result["MULTISITE_CONN"] = value.MULTISITE_CONN.model_dump_json(by_alias=True) + result["VRF_LITE_CONN"] = value.VRF_LITE_CONN.model_dump_json(by_alias=True) + return json.dumps(result) + + @field_serializer("instance_values") + def serialize_instance_values(self, value: PayloadVrfsAttachmentsLanAttachListInstanceValues) -> str: + """ + Serialize instance_values to a JSON string. + """ + if value == "": + return json.dumps({}) + return value.model_dump_json(by_alias=True) + + +class PayloadVrfsAttachments(BaseModel): + """ + # Summary + + Represents a POST payload for the following endpoint: + + api.v1.lan_fabric.rest.top_down.fabrics.vrfs.Vrfs.EpVrfPost + + /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/attachments + + See NdfcVrf12.push_diff_attach + + ## Structure + + - lan_attach_list: list[PayloadVrfsAttachmentsLanAttachListItem] + - vrf_name: str + + ## Example payload + + ```json + { + "lanAttachList": [ + { + "deployment": true, + "extensionValues": "", + "fabric": "test_fabric", + "freeformConfig": "", + "instanceValues": "{\"loopbackId\":\"\"}", # content removed for brevity + "serialNumber": "XYZKSJHSMK1", + "vlan": 0, + "vrfName": "test_vrf_1" + }, + ], + "vrfName": "test_vrf" + } + ``` + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + validate_by_alias=True, + validate_by_name=True, + ) + + lan_attach_list: list[PayloadVrfsAttachmentsLanAttachListItem] = Field(alias="lanAttachList") + vrf_name: str = Field(alias="vrfName", min_length=1, max_length=32) diff --git a/plugins/module_utils/vrf/model_payload_vrfs_deployments.py b/plugins/module_utils/vrf/model_payload_vrfs_deployments.py new file mode 100644 index 000000000..0d8d315ac --- /dev/null +++ b/plugins/module_utils/vrf/model_payload_vrfs_deployments.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +Validation for payloads sent to the following controller endpoint: + +- Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs/deployments +- Verb: POST +""" +from pydantic import BaseModel, ConfigDict, Field, field_serializer + + +class PayloadVrfsDeployments(BaseModel): + """ + # Summary + + Represents a payload suitable for sending to the following controller endpoint: + + ## Endpoint + + ### Verb + + POST + + ### Path + + /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs/deployments + + ## Structure + + - `vrf_names`: list[str] - A list of VRF names to be deployed. alias "vrfNames", default_factory=list + + ## Example pre-serialization + + vrf_names=['vrf2', 'vrf1', 'vrf3'] + + ## Example post-serialization, model_dump(by_alias=True) + + ```json + { + "vrfNames": "vrf1,vrf2,vrf3" + } + ``` + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + validate_by_alias=True, + validate_by_name=True, + ) + + vrf_names: list[str] = Field(alias="vrfNames", default_factory=list) + + @field_serializer("vrf_names") + def serialize_vrf_names(self, vrf_names: list[str]) -> str: + """ + Serialize vrf_names to a comma-separated string of unique sorted vrf names. + """ + return ",".join(sorted(set(list(vrf_names)))) diff --git a/plugins/module_utils/vrf/model_playbook_vrf_v11.py b/plugins/module_utils/vrf/model_playbook_vrf_v11.py new file mode 100644 index 000000000..7d6eff69a --- /dev/null +++ b/plugins/module_utils/vrf/model_playbook_vrf_v11.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/vrf/vrf_playbook_model.py +# Copyright (c) 2020-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. +# pylint: disable=wrong-import-position +""" +Validation model for dcnm_vrf playbooks. +""" +from typing import Optional, Union + +from pydantic import BaseModel, ConfigDict, Field, model_validator +from typing_extensions import Self + +from ..common.enums.bgp import BgpPasswordEncrypt +from ..common.models.ipv4_cidr_host import IPv4CidrHostModel +from ..common.models.ipv4_host import IPv4HostModel +from ..common.models.ipv6_cidr_host import IPv6CidrHostModel +from ..common.models.ipv6_host import IPv6HostModel + + +class VrfLiteModel(BaseModel): + """ + # Summary + + Model for VRF Lite configuration. + + ## Raises + + - ValueError if: + - dot1q is not within the range 0-4094 + - ipv4_addr is not valid + - ipv6_addr is not valid + - interface is not provided + - neighbor_ipv4 is not valid + - neighbor_ipv6 is not valid + + ## Attributes: + + - dot1q (int): VLAN ID for the interface. + - interface (str): Interface name. + - ipv4_addr (str): IPv4 address in CIDR format. + - ipv6_addr (str): IPv6 address in CIDR format. + - neighbor_ipv4 (str): IPv4 address without prefix. + - neighbor_ipv6 (str): IPv6 address without prefix. + - peer_vrf (str): Peer VRF name. + + ## Example usage: + + ```python + from pydantic import ValidationError + from vrf_lite_module import VrfLiteModel + try: + vrf_lite = VrfLiteModel( + dot1q=100, + interface="Ethernet1/1", + ipv4_addr="10.1.1.1/24" + ) + except ValidationError as e: + handle_error + ``` + + """ + + dot1q: int = Field(default=0, ge=0, le=4094) + interface: str + ipv4_addr: Optional[str] = Field(default="") + ipv6_addr: Optional[str] = Field(default="") + neighbor_ipv4: Optional[str] = Field(default="") + neighbor_ipv6: Optional[str] = Field(default="") + peer_vrf: Optional[str] = Field(default="") + + @model_validator(mode="after") + def validate_ipv4_host(self) -> Self: + """ + Validate neighbor_ipv4 is an IPv4 host address without prefix. + """ + if self.neighbor_ipv4 != "": + IPv4HostModel(ipv4_host=str(self.neighbor_ipv4)) + return self + + @model_validator(mode="after") + def validate_ipv6_host(self) -> Self: + """ + Validate neighbor_ipv6 is an IPv6 host address without prefix. + """ + if self.neighbor_ipv6 != "": + IPv6HostModel(ipv6_host=str(self.neighbor_ipv6)) + return self + + @model_validator(mode="after") + def validate_ipv4_cidr_host(self) -> Self: + """ + Validate ipv4_addr is a CIDR-format IPv4 host address. + """ + if self.ipv4_addr != "": + IPv4CidrHostModel(ipv4_cidr_host=str(self.ipv4_addr)) + return self + + @model_validator(mode="after") + def validate_ipv6_cidr_host(self) -> Self: + """ + Validate ipv6_addr is a CIDR-format IPv6 host address. + """ + if self.ipv6_addr != "": + IPv6CidrHostModel(ipv6_cidr_host=str(self.ipv6_addr)) + return self + + +class VrfAttachModel(BaseModel): + """ + # Summary + + Model for VRF attachment configuration. + + ## Raises + + - ValueError if: + - deploy is not a boolean + - ip_address is not a valid IPv4 host address + - ip_address is not provided + - vrf_lite (if provided) is not a list of VrfLiteModel instances + + ## Attributes: + + - deploy (bool): Flag to indicate if the VRF should be deployed. + - ip_address (str): IP address of the interface. + - vrf_lite (list[VrfLiteModel]): List of VRF Lite configurations. + - vrf_lite (None): If not provided, defaults to None. + + ## Example usage: + + ```python + from pydantic import ValidationError + from vrf_attach_module import VrfAttachModel + try: + vrf_attach = VrfAttachModel( + deploy=True, + ip_address="10.1.1.1", + vrf_lite=[ + VrfLiteModel( + dot1q=100, + interface="Ethernet1/1", + ipv4_addr="10.1.1.1/24" + ) + ] + ) + except ValidationError as e: + handle_error + ``` + """ + + deploy: bool = Field(default=True) + ip_address: str + vrf_lite: Optional[list[VrfLiteModel]] = Field(default=None) + + @model_validator(mode="after") + def validate_ipv4_host(self) -> Self: + """ + Validate ip_address is an IPv4 host address without prefix. + """ + if self.ip_address != "": + IPv4HostModel(ipv4_host=self.ip_address) + return self + + @model_validator(mode="after") + def vrf_lite_set_to_none_if_empty_list(self) -> Self: + """ + Set vrf_lite to None if it is an empty list. + This mimics the behavior of the original code. + """ + if not self.vrf_lite: + self.vrf_lite = None + return self + + +class VrfPlaybookModelV11(BaseModel): + """ + # Summary + + + Model to validate a playbook VRF configuration. + + All fields can take an alias, which is the name of the field in the + original payload. The alias is used to map the field to the + corresponding field in the playbook. + + ## Raises + + - ValueError if: + - adv_default_routes is not a boolean + - adv_host_routes is not a boolean + - attach (if provided) is not a list of VrfAttachModel instances + - bgp_passwd_encrypt is not a valid BgpPasswordEncrypt enum value + - bgp_password is not a string + - deploy is not a boolean + - ipv6_linklocal_enable is not a boolean + - loopback_route_tag is not an integer between 0 and 4294967295 + - max_bgp_paths is not an integer between 1 and 64 + - max_ibgp_paths is not an integer between 1 and 64 + - overlay_mcast_group is not a string + - redist_direct_rmap is not a string + - rp_address is not a valid IPv4 host address + - rp_external is not a boolean + - rp_loopback_id is not an integer between 0 and 1023 + - service_vrf_template is not a string + - static_default_route is not a boolean + - trm_bgw_msite is not a boolean + - trm_enable is not a boolean + - underlay_mcast_ip is not a string + - vlan_id is not an integer between 0 and 4094 + - vrf_description is not a string + - vrf_extension_template is not a string + - vrf_id is not an integer between 0 and 16777214 + - vrf_int_mtu is not an integer between 68 and 9216 + - vrf_intf_desc is not a string + - vrf_name is not a string + - vrf_template is not a string + - vrf_vlan_name is not a string + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + use_enum_values=True, + validate_assignment=True, + ) + adv_default_routes: bool = Field(default=True, alias="advertiseDefaultRouteFlag") + adv_host_routes: bool = Field(default=False, alias="advertiseHostRouteFlag") + attach: Optional[list[VrfAttachModel]] = None + bgp_passwd_encrypt: Union[BgpPasswordEncrypt, int] = Field(default=BgpPasswordEncrypt.MD5.value, alias="bgpPasswordKeyType") + bgp_password: str = Field(default="", alias="bgpPassword") + deploy: bool = Field(default=True) + ipv6_linklocal_enable: bool = Field(default=True, alias="ipv6LinkLocalFlag") + loopback_route_tag: int = Field(default=12345, ge=0, le=4294967295, alias="tag") + max_bgp_paths: int = Field(default=1, ge=1, le=64, alias="maxBgpPaths") + max_ibgp_paths: int = Field(default=2, ge=1, le=64, alias="maxIbgpPaths") + overlay_mcast_group: str = Field(default="", alias="multicastGroup") + redist_direct_rmap: str = Field(default="FABRIC-RMAP-REDIST-SUBNET", alias="vrfRouteMap") + rp_address: str = Field(default="", alias="rpAddress") + rp_external: bool = Field(default=False, alias="isRPExternal") + rp_loopback_id: Optional[Union[int, str]] = Field(default="", ge=0, le=1023, alias="loopbackNumber") + service_vrf_template: Optional[str] = Field(default=None, alias="serviceVrfTemplate") + source: Optional[str] = None + static_default_route: bool = Field(default=True, alias="configureStaticDefaultRouteFlag") + trm_bgw_msite: bool = Field(default=False, alias="trmBGWMSiteEnabled") + trm_enable: bool = Field(default=False, alias="trmEnabled") + underlay_mcast_ip: str = Field(default="", alias="L3VniMcastGroup") + vlan_id: Optional[int] = Field(default=None, le=4094) + vrf_description: str = Field(default="", alias="vrfDescription") + vrf_extension_template: str = Field(default="Default_VRF_Extension_Universal", alias="vrfExtensionTemplate") + vrf_id: Optional[int] = Field(default=None, le=16777214) + vrf_int_mtu: int = Field(default=9216, ge=68, le=9216, alias="mtu") + vrf_intf_desc: str = Field(default="", alias="vrfIntfDescription") + vrf_name: str = Field(..., max_length=32) + vrf_template: str = Field(default="Default_VRF_Universal") + vrf_vlan_name: str = Field(default="", alias="vrfVlanName") + + @model_validator(mode="after") + def hardcode_source_to_none(self) -> Self: + """ + To mimic original code, hardcode source to None. + """ + if self.source is not None: + self.source = None + return self + + @model_validator(mode="after") + def validate_rp_address(self) -> Self: + """ + Validate rp_address is an IPv4 host address without prefix. + """ + if self.rp_address != "": + IPv4HostModel(ipv4_host=self.rp_address) + return self + + +class VrfPlaybookConfigModelV11(BaseModel): + """ + Model for VRF playbook configuration. + """ + + config: list[VrfPlaybookModelV11] = Field(default_factory=list[VrfPlaybookModelV11]) diff --git a/plugins/module_utils/vrf/model_playbook_vrf_v12.py b/plugins/module_utils/vrf/model_playbook_vrf_v12.py new file mode 100644 index 000000000..2fadec9c0 --- /dev/null +++ b/plugins/module_utils/vrf/model_playbook_vrf_v12.py @@ -0,0 +1,429 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/vrf/vrf_playbook_model.py +# Copyright (c) 2020-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. +# pylint: disable=wrong-import-position +""" +Validation model for dcnm_vrf playbooks. +""" +from typing import Optional, Union + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, field_validator + +from ..common.enums.bgp import BgpPasswordEncrypt +from ..common.models.ipv4_cidr_host import IPv4CidrHostModel +from ..common.models.ipv4_host import IPv4HostModel +from ..common.models.ipv4_multicast_group_address import IPv4MulticastGroupModel +from ..common.models.ipv6_cidr_host import IPv6CidrHostModel +from ..common.models.ipv6_host import IPv6HostModel + + +class PlaybookVrfLiteModel(BaseModel): + """ + # Summary + + Model for VRF Lite configuration. + + ## Raises + + - ValueError if: + - dot1q is not within the range 0-4094 + - ipv4_addr is not valid + - ipv6_addr is not valid + - interface is not provided + - neighbor_ipv4 is not valid + - neighbor_ipv6 is not valid + + ## Attributes: + + - dot1q (int): VLAN ID for the interface. + - interface (str): Interface name. + - ipv4_addr (str): IPv4 address in CIDR format. + - ipv6_addr (str): IPv6 address in CIDR format. + - neighbor_ipv4 (str): IPv4 address without prefix. + - neighbor_ipv6 (str): IPv6 address without prefix. + - peer_vrf (str): Peer VRF name. + + ## Example usage: + + ```python + from pydantic import ValidationError + from vrf_lite_module import PlaybookVrfLiteModel + try: + vrf_lite = PlaybookVrfLiteModel( + dot1q=100, + interface="Ethernet1/1", + ipv4_addr="10.1.1.1/24" + ) + except ValidationError as e: + handle_error + ``` + + """ + + dot1q: str = Field(default="", max_length=4) + interface: str + ipv4_addr: Optional[str] = Field(default="") + ipv6_addr: Optional[str] = Field(default="") + neighbor_ipv4: Optional[str] = Field(default="") + neighbor_ipv6: Optional[str] = Field(default="") + peer_vrf: Optional[str] = Field(default="", min_length=1, max_length=32) + + @field_validator("dot1q", mode="before") + @classmethod + def validate_dot1q_and_serialize_to_str(cls, value: Union[None, int, str]) -> str: + """ + Validate dot1q and serialize it to a str. + + - If value is any of [None, "", "0", 0], return an empty string. + - Else, if value cannot be converted to an int, raise ValueError. + - Convert to int and validate it is within the range 1-4094. + - If it is, return the value as a string. + - If it is not, raise ValueError. + """ + if value in [None, "", "0", 0]: + return "" + try: + value = int(value) + except (ValueError, TypeError) as error: + msg = f"Invalid dot1q value: {value}. It must be an integer between 1 and 4094." + msg += f" Error detail: {error}" + raise ValueError(msg) from error + if value < 1 or value > 4094: + raise ValueError(f"Invalid dot1q value: {value}. It must be an integer between 1 and 4094.") + return str(value) + + @field_validator("neighbor_ipv4", mode="before") + @classmethod + def validate_neighbor_ipv4(cls, value: str) -> str: + """ + Validate neighbor_ipv4 is an IPv4 host address without prefix. + """ + if value != "": + IPv4HostModel(ipv4_host=str(value)) + return value + + @field_validator("neighbor_ipv6", mode="before") + @classmethod + def validate_neighbor_ipv6(cls, value: str) -> str: + """ + Validate neighbor_ipv6 is an IPv6 host address without prefix. + """ + if value != "": + IPv6HostModel(ipv6_host=str(value)) + return value + + @field_validator("ipv4_addr", mode="before") + @classmethod + def validate_ipv4_addr(cls, value: str) -> str: + """ + Validate ipv4_addr is a CIDR-format IPv4 host address. + """ + if value != "": + IPv4CidrHostModel(ipv4_cidr_host=str(value)) + return value + + @field_validator("ipv6_addr", mode="before") + @classmethod + def validate_ipv6_addr(cls, value: str) -> str: + """ + Validate ipv6_addr is a CIDR-format IPv6 host address. + """ + if value != "": + IPv6CidrHostModel(ipv6_cidr_host=str(value)) + return value + + +class PlaybookVrfAttachModel(BaseModel): + """ + # Summary + + Model for VRF attachment configuration. + + ## Raises + + - ValueError if: + - deploy is not a boolean + - export_evpn_rt is not a string + - import_evpn_rt is not a string + - ip_address is not a valid IPv4 host address + - ip_address is not provided + - vrf_lite (if provided) is not a list of PlaybookVrfLiteModel instances + + ## Attributes: + + - deploy (bool): Flag to indicate if the VRF should be deployed. + - export_evpn_rt (str): Route target for EVPN export. + - import_evpn_rt (str): Route target for EVPN import. + - ip_address (str): IP address of the interface. + - vrf_lite (list[PlaybookVrfLiteModel]): List of VRF Lite configurations. + - vrf_lite (None): If not provided, defaults to None. + + ## Example usage: + + ```python + from pydantic import ValidationError + from vrf_attach_module import PlaybookVrfAttachModel + try: + vrf_attach = PlaybookVrfAttachModel( + deploy=True, + export_evpn_rt="target:1:1", + import_evpn_rt="target:1:2", + ip_address="10.1.1.1", + vrf_lite=[ + PlaybookVrfLiteModel( + dot1q=100, + interface="Ethernet1/1", + ipv4_addr="10.1.1.1/24" + ) + ] + ) + except ValidationError as e: + handle_error + ``` + """ + + deploy: StrictBool = Field(default=True) + export_evpn_rt: str = Field(default="") + import_evpn_rt: str = Field(default="") + ip_address: str + vrf_lite: Optional[list[PlaybookVrfLiteModel]] = Field(default=None) + + @field_validator("ip_address", mode="before") + @classmethod + def validate_ip_address(cls, value: str) -> str: + """ + Validate ip_address is an IPv4 host address without prefix. + """ + if value != "": + IPv4HostModel(ipv4_host=str(value)) + return value + + @field_validator("vrf_lite", mode="before") + @classmethod + def vrf_lite_set_to_none_if_empty_list(cls, value: Union[None, list]) -> Optional[list[PlaybookVrfLiteModel]]: + """ + Set vrf_lite to None if it is an empty list. + This mimics the behavior of the original code. + """ + if not value: + return None + return value + + +class PlaybookVrfModelV12(BaseModel): + """ + # Summary + + + Model to validate a playbook VRF configuration. + + ## Raises + + - ValueError if: + - Any field does not meet its validation criteria. + + ## Attributes: + - adv_default_routes - boolean + - adv_host_routes - boolean + - attach - list of PlaybookVrfAttachModel + - bgp_passwd_encrypt - int (BgpPasswordEncrypt enum value, 3, 7) + - bgp_password - string + - deploy - boolean + - disable_rt_auto - boolean + - export_evpn_rt - string + - export_mvpn_rt - string + - export_vpn_rt - string + - import_evpn_rt - string + - import_mvpn_rt - string + - import_vpn_rt - string + - ipv6_linklocal_enable - boolean + - loopback_route_tag- integer range (0-4294967295) + - max_bgp_paths - integer range (1-64) + - max_ibgp_paths - integer range (1-64) + - netflow_enable - boolean + - nf_monitor - string + - no_rp - boolean + - overlay_mcast_group - string (IPv4 multicast group address without prefix) + - redist_direct_rmap - string + - rp_address - string (IPv4 host address without prefix) + - rp_external - boolean + - rp_loopback_id - int range (0-1023) + - service_vrf_template - string + - static_default_route - boolean + - trm_bgw_msite - boolean + - trm_enable - boolean + - underlay_mcast_ip - string (IPv4 multicast group address without prefix) + - vlan_id - integer range (0-4094) + - vrf_description - string + - vrf_extension_template - string + - vrf_id - integer range (0- 16777214) + - vrf_int_mtu - integer range (68-9216) + - vrf_intf_desc - string + - vrf_name - string + - vrf_template - string + - vrf_vlan_name - string + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + use_enum_values=True, + validate_assignment=True, + ) + adv_default_routes: StrictBool = Field(default=True) # advertiseDefaultRouteFlag + adv_host_routes: StrictBool = Field(default=False) # advertiseHostRouteFlag + attach: Optional[list[PlaybookVrfAttachModel]] = None + bgp_passwd_encrypt: BgpPasswordEncrypt = Field(default=BgpPasswordEncrypt.MD5.value) # bgpPasswordKeyType + bgp_password: str = Field(default="") # bgpPassword + deploy: StrictBool = Field(default=True) + disable_rt_auto: StrictBool = Field(default=False) # disableRtAuto + export_evpn_rt: str = Field(default="") # routeTargetExportEvpn + export_mvpn_rt: str = Field(default="") # routeTargetExportMvpn + export_vpn_rt: str = Field(default="") # routeTargetExport + import_evpn_rt: str = Field(default="") # routeTargetImportEvpn + import_mvpn_rt: str = Field(default="") # routeTargetImportMvpn + import_vpn_rt: str = Field(default="") # routeTargetImport + ipv6_linklocal_enable: StrictBool = Field(default=True) # ipv6LinkLocalFlag + loopback_route_tag: int = Field(default=12345, ge=0, le=4294967295) # tag + max_bgp_paths: int = Field(default=1, ge=1, le=64) # maxBgpPaths + max_ibgp_paths: int = Field(default=2, ge=1, le=64) # maxIbgpPaths + netflow_enable: StrictBool = Field(default=False) # ENABLE_NETFLOW + nf_monitor: str = Field(default="") # NETFLOW_MONITOR + no_rp: StrictBool = Field(default=False) # isRPAbsent + overlay_mcast_group: str = Field(default="") # multicastGroup + redist_direct_rmap: str = Field(default="FABRIC-RMAP-REDIST-SUBNET") # vrfRouteMap + rp_address: str = Field(default="") # rpAddress + rp_external: StrictBool = Field(default=False) # isRPExternal + rp_loopback_id: Optional[Union[int, str]] = Field(default="", ge=-1, le=1023) # loopbackNumber + service_vrf_template: Optional[str] = Field(default=None) # serviceVrfTemplate + source: Optional[str] = None + static_default_route: StrictBool = Field(default=True) # configureStaticDefaultRouteFlag + trm_bgw_msite: StrictBool = Field(default=False) # trmBGWMSiteEnabled + trm_enable: StrictBool = Field(default=False) # trmEnabled + underlay_mcast_ip: str = Field(default="") # L3VniMcastGroup + vlan_id: Optional[int] = Field(default=None, le=4094) + vrf_description: str = Field(default="") # vrfDescription + vrf_extension_template: str = Field(default="Default_VRF_Extension_Universal") # vrfExtensionTemplate + vrf_id: Optional[int] = Field(default=None, le=16777214) + vrf_int_mtu: int = Field(default=9216, ge=68, le=9216) # mtu + vrf_intf_desc: str = Field(default="") # vrfIntfDescription + vrf_name: str = Field(..., min_length=1, max_length=32) # vrfName + vrf_template: str = Field(default="Default_VRF_Universal") + vrf_vlan_name: str = Field(default="") # vrfVlanName + + @field_validator("overlay_mcast_group", mode="before") + @classmethod + def validate_overlay_mcast_group(cls, value: str) -> str: + """ + Validate overlay_mcast_group is an IPv4 multicast group address without prefix. + """ + if value != "": + IPv4MulticastGroupModel(ipv4_multicast_group=str(value)) + return value + + @field_validator("source", mode="before") + @classmethod + def hardcode_source_to_none(cls, value) -> None: + """ + To mimic original code, hardcode source to None. + """ + if value is not None: + value = None + return value + + @field_validator("rp_address", mode="before") + @classmethod + def validate_rp_address(cls, value: str) -> str: + """ + Validate rp_address is an IPv4 host address without prefix. + """ + if value != "": + IPv4HostModel(ipv4_host=str(value)) + return value + + @field_validator("rp_loopback_id", mode="before") + @classmethod + def validate_rp_loopback_id_before(cls, value: Union[int, str]) -> Union[int, str]: + """ + Validate rp_loopback_id is an integer between 0 and 1023. + If it is an empty string, return -1. This will be converted to "" in an "after" validator. + """ + if isinstance(value, str) and value == "": + return -1 + if not isinstance(value, int): + raise ValueError(f"Invalid rp_loopback_id: {value}. It must be an integer between 0 and 1023.") + if value < 0 or value > 1023: + raise ValueError(f"Invalid rp_loopback_id: {value}. It must be an integer between 0 and 1023.") + return value + + @field_validator("rp_loopback_id", mode="after") + @classmethod + def validate_rp_loopback_id_after(cls, value: Union[int, str]) -> Union[int, str]: + """ + Convert rp_loopback_id to an empty string if it is -1. + """ + if value == -1: + return "" + return value + + @field_validator("underlay_mcast_ip", mode="before") + @classmethod + def validate_underlay_mcast_ip(cls, value: str) -> str: + """ + Validate underlay_mcast_ip is an IPv4 multicast group address without prefix. + """ + if value != "": + IPv4MulticastGroupModel(ipv4_multicast_group=str(value)) + return value + + @field_validator("vlan_id", mode="before") + @classmethod + def validate_vlan_id_before(cls, value: Union[int, str]) -> Union[int, str]: + """ + Validate vlan_id is an integer between 2 and 4094. + If it is "", return -1. This will be converted to None in an "after" validator. + """ + if isinstance(value, str) and value == "": + return -1 + if isinstance(value, str): + try: + value = int(value) + except (TypeError, ValueError) as error: + msg = f"Invalid vlan_id: {value}. It must be an integer between 2 and 4094." + msg += f" Error detail: {error}" + raise ValueError(msg) from error + if not isinstance(value, int): + raise ValueError(f"Invalid vlan_id: {value}. It must be an integer between 2 and 4094.") + if value < 2 or value > 4094: + raise ValueError(f"Invalid vlan_id: {value}. It must be an integer between 2 and 4094.") + return value + + @field_validator("vlan_id", mode="after") + @classmethod + def validate_vlan_id_after(cls, value: Union[int, str]) -> Union[int, str]: + """ + Convert vlan_id to None if it is -1. + """ + if value == -1: + return None + return value + + +class PlaybookVrfConfigModelV12(BaseModel): + """ + Model for VRF playbook configuration. + """ + + config: list[PlaybookVrfModelV12] = Field(default_factory=list[PlaybookVrfModelV12]) diff --git a/plugins/module_utils/vrf/model_vrf_detach_payload_v12.py b/plugins/module_utils/vrf/model_vrf_detach_payload_v12.py new file mode 100644 index 000000000..794f5733b --- /dev/null +++ b/plugins/module_utils/vrf/model_vrf_detach_payload_v12.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +from typing import List, Optional, Union + +from pydantic import BaseModel, ConfigDict, Field, field_validator + + +class LanDetachListItemV12(BaseModel): + """ + # Summary + + A single lan detach item within VrfDetachPayloadV12.lan_attach_list. + + ## Structure + + - deployment: bool, alias: deployment, default=False + - extension_values: Optional[str], alias: extensionValues, default="" + - fabric: str (min_length=1, max_length=64), alias: fabric + - freeform_config: Optional[str], alias: freeformConfig, default="" + - instance_values: Optional[str], alias: instanceValues, default="" + - is_deploy: Optional[bool], alias: is_deploy + - serial_number: str, alias: serialNumber + - vlan: Union(int | None), alias: vlanId + - vrf_name: str (min_length=1, max_length=32), alias: vrfName + + ## Notes + - `deployment` - False indicates that attachment should be detached. + This model unconditionally forces `deployment` to False. + """ + + deployment: bool = Field(alias="deployment", default=False) + extension_values: Optional[str] = Field(alias="extensionValues", default="") + fabric: str = Field(alias="fabric", min_length=1, max_length=64) + freeform_config: Optional[str] = Field(alias="freeformConfig", default="") + instance_values: Optional[str] = Field(alias="instanceValues", default="") + is_deploy: Optional[bool] = Field(alias="is_deploy") + is_attached: Optional[bool] = Field(alias="isAttached", default=True) + serial_number: str = Field(alias="serialNumber") + vlan: Union[int | None] = Field(alias="vlanId") + vrf_name: str = Field(alias="vrfName", min_length=1, max_length=32) + + @field_validator("deployment", mode="after") + @classmethod + def force_deployment_to_false(cls, value) -> bool: + """ + Force deployment to False. This model is used for detaching + VRF attachments, so deployment should always be False. + """ + return False + + +class VrfDetachPayloadV12(BaseModel): + """ + # Summary + + Represents a payload for detaching VRF attachments. + + See NdfcVrf12.get_items_to_detach_model + + ## Structure + + - lan_attach_list: List[LanDetachListItemV12] + - vrf_name: str + + ## Example payload + + ```json + { + "lanAttachList": [ + { + "deployment": false, + "extensionValues": "", + "fabric": "test_fabric", + "freeformConfig": "", + "instanceValues": "{\"loopbackId\":\"\"}", # content removed for brevity + "serialNumber": "XYZKSJHSMK2", + "vlanId": 202, + "vrfName": "test_vrf_1" + } + ], + "vrfName": "test_vrf" + } + ``` + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + validate_by_alias=True, + validate_by_name=True, + ) + + lan_attach_list: List[LanDetachListItemV12] = Field(alias="lanAttachList") + vrf_name: str = Field(alias="vrfName") diff --git a/plugins/module_utils/vrf/serial_number_to_vrf_lite.py b/plugins/module_utils/vrf/serial_number_to_vrf_lite.py new file mode 100644 index 000000000..4ff62045e --- /dev/null +++ b/plugins/module_utils/vrf/serial_number_to_vrf_lite.py @@ -0,0 +1,191 @@ +import inspect +import json +import logging + +from .model_playbook_vrf_v12 import PlaybookVrfModelV12 + + +class SerialNumberToVrfLite: + """ + Given a list of validated playbook configuration models, + build a mapping of switch serial numbers to lists of PlaybookVrfLiteModel instances. + + Usage: + ```python + + from your_module import SerialNumberToVrfLite + serial_number_to_vrf_lite = SerialNumberToVrfLite() + instance.playbook_models = validated_playbook_config_models + instance.commit() + instance.serial_number = serial_number1 + vrf_lite_list = instance.vrf_lite + instance.serial_number = serial_number2 + vrf_lite_list = instance.vrf_lite + # etc... + ``` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._fabric_inventory: dict = {} + self._playbook_models: list[PlaybookVrfModelV12] = [] + self.serial_number_to_vrf_lite: dict = {} + self.commit_done: bool = False + + def commit(self) -> None: + """ + From self.validated_playbook_config_models, build a dictionary, keyed on switch serial_number, + containing a list of VrfLiteModel. + + ## Example structure + + ```json + { + "XYZKSJHSMK4": [ + PlaybookVrfLiteModel( + dot1q=21, + interface="Ethernet1/1", + ipv4_addr="10.33.0.11/30", + ipv6_addr="2010::10:34:0:1/64", + neighbor_ipv4="10.33.0.12", + neighbor_ipv6="2010::10:34:0:1", + peer_vrf="test_vrf_1" + ) + ] + } + ``` + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}." + self.log.debug(msg) + + if not self.playbook_models: + msg = f"{self.class_name}.{method_name}: " + msg += "Set instance.playbook_models before calling commit()." + raise ValueError(msg) + + if not self.fabric_inventory: + msg = f"{self.class_name}.{method_name}: " + msg += "Set instance.fabric_inventory before calling commit()." + raise ValueError(msg) + + self.commit_done = True + vrf_config_models_with_attachments = [model for model in self._playbook_models if model.attach] + if not vrf_config_models_with_attachments: + msg = f"{self.class_name}.{method_name}: " + msg += "Early return. No playbook configs containing VRF attachments found." + self.log.debug(msg) + return + + for model in vrf_config_models_with_attachments: + for attachment in model.attach: + if not attachment.vrf_lite: + msg = f"{self.class_name}.{method_name}: " + msg += f"switch {attachment.ip_address} VRF attachment does not contain vrf_lite. Skipping." + self.log.debug(msg) + continue + ip_address = attachment.ip_address + self.serial_number_to_vrf_lite.update({self.ipv4_address_to_serial_number(ip_address): attachment.vrf_lite}) + + msg = f"{self.class_name}.{method_name}: " + msg += f"self.serial_number_to_vrf_lite: length: {len(self.serial_number_to_vrf_lite)}." + self.log.debug(msg) + for serial_number, vrf_lite_list in self.serial_number_to_vrf_lite.items(): + msg = f"{self.class_name}.{method_name}: " + msg += f"serial_number {serial_number}: -> {json.dumps([model.model_dump(by_alias=True) for model in vrf_lite_list], indent=4, sort_keys=True)}" + self.log.debug(msg) + + def ipv4_address_to_serial_number(self, ip_address) -> str: + """ + Given a switch ip_address, return the switch serial number. + + If ip_address is not found, return an empty string. + + ## Raises + + - ValueError: If instance.fabric_inventory is not set before calling this method. + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}" + self.log.debug(msg) + + data = self.fabric_inventory.get(ip_address, None) + if not data: + msg = f"{self.class_name}: ip_address {ip_address} not found in fabric_inventory." + raise ValueError(msg) + + serial_number = data.get("serialNumber", None) + if not serial_number: + msg = f"{self.class_name}: ip_address {ip_address} does not have a serial number." + raise ValueError(msg) + return serial_number + + @property + def fabric_inventory(self) -> dict: + """ + Return the fabric inventory. + """ + return self._fabric_inventory + + @fabric_inventory.setter + def fabric_inventory(self, value: str): + """ + Set the fabric_inventory. Used to convert IP addresses to serial numbers. + """ + if not isinstance(value, dict): + msg = f"{self.class_name}: fabric_inventory must be a dict. " + msg += f"Got {type(value).__name__}." + raise TypeError(msg) + self._fabric_inventory = value + + @property + def playbook_models(self) -> list[PlaybookVrfModelV12]: + """ + Return the list of playbook models (list[PlaybookVrfModelV12]). + """ + return self._playbook_models + + @playbook_models.setter + def playbook_models(self, value: list[PlaybookVrfModelV12]): + if not isinstance(value, list): + msg = f"{self.class_name}: playbook_models must be list[PlaybookVrfModelV12]. " + msg += f"Got {type(value).__name__}." + raise TypeError(msg) + self._playbook_models = value + + @property + def serial_number(self) -> str: + """ + Return the serial number for which to retrieve VRF Lite models. + """ + return self._serial_number + + @serial_number.setter + def serial_number(self, value: str): + """ + Set the serial number for which to retrieve VRF Lite models. + """ + if not isinstance(value, str): + msg = f"{self.class_name}: serial_number must be a string. " + msg += f"Got {type(value).__name__}." + raise TypeError(msg) + self._serial_number = value + + @property + def vrf_lite(self) -> list: + """ + Get the list of VrfLiteModel instances for the specified serial number. + """ + if not self.serial_number: + msg = f"{self.class_name}: serial_number must be set before accessing vrf_lite." + raise ValueError(msg) + if not self.commit_done: + self.commit() + self.commit_done = True + return self.serial_number_to_vrf_lite.get(self.serial_number, None) diff --git a/plugins/module_utils/vrf/transmute_diff_attach_to_payload.py b/plugins/module_utils/vrf/transmute_diff_attach_to_payload.py new file mode 100644 index 000000000..0b761c1e0 --- /dev/null +++ b/plugins/module_utils/vrf/transmute_diff_attach_to_payload.py @@ -0,0 +1,752 @@ +import inspect +import json +import logging +import re + +from .inventory_serial_number_to_fabric_name import InventorySerialNumberToFabricName +from .inventory_serial_number_to_ipv4 import InventorySerialNumberToIpv4 +from .model_controller_response_vrfs_switches_v12 import ( + ControllerResponseVrfsSwitchesDataItem, + ControllerResponseVrfsSwitchesExtensionPrototypeValue, + ControllerResponseVrfsSwitchesV12, + ControllerResponseVrfsSwitchesVrfLiteConnProtoItem, +) +from .model_payload_vrfs_attachments import ( + PayloadVrfsAttachments, + PayloadVrfsAttachmentsLanAttachListExtensionValues, + PayloadVrfsAttachmentsLanAttachListExtensionValuesMultisiteConn, + PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConn, + PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConnItem, + PayloadVrfsAttachmentsLanAttachListInstanceValues, + PayloadVrfsAttachmentsLanAttachListItem, +) +from .model_playbook_vrf_v12 import PlaybookVrfModelV12 +from .serial_number_to_vrf_lite import SerialNumberToVrfLite + + +class DiffAttachToControllerPayload: + """ + # Summary + + - Transmute diff_attach to a list of PayloadVrfsAttachments models. + - For each model, update its lan_attach_list + - Set vlan to 0 + - Set the fabric name to the child fabric name, if fabric is MSD + - Update vrf_lite extensions with information from the switch + + ## Raises + + - ValueError if diff_attach cannot be mutated + + ## Usage + ```python + instance = DiffAttachToControllerPayload() + instance.diff_attach = diff_attach + instance.fabric_type = fabric_type + instance.fabric_inventory = get_fabric_inventory_details(self.module, self.fabric) + instance.commit() + payload_models = instance.payload_models + payload = instance.payload + ``` + + Where: + + - `diff_attach` is a list of dictionaries representing the VRF attachments. + - `fabric_name` is the name of the fabric. + - `fabric_type` is the type of the fabric (e.g., "MFD" for multisite fabrics). + - `fabric_inventory` is a dictionary containing inventory details for `fabric_name` + + ## inventory + + ```json + { + "10.10.10.224": { + "ipAddress": "10.10.10.224", + "logicalName": "dt-n9k1", + "serialNumber": "XYZKSJHSMK1", + "switchRole": "leaf" + } + } + ``` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + # Set self._sender to list to avoid pylint not-callable error + self._sender: callable = list + self._diff_attach: list[dict] = [] + self._fabric_name: str = "" + # TODO: remove self.fabric_type once we use fabric_inventory.fabricTechnology for fabric_type + self._fabric_type: str = "" + self._fabric_inventory: dict = {} + self._ansible_module = None # AndibleModule instance + self._payload: list = [] + self._payload_model: list[PayloadVrfsAttachments] = [] + self._playbook_models: list = [] + + self.serial_number_to_fabric_name = InventorySerialNumberToFabricName() + self.serial_number_to_ipv4 = InventorySerialNumberToIpv4() + self.serial_number_to_vrf_lite = SerialNumberToVrfLite() + + def log_list_of_models(self, model_list: list, by_alias: bool = False) -> None: + """ + # Summary + + Log a list of Pydantic models. + """ + caller = inspect.stack()[1][3] + for index, model in enumerate(model_list): + msg = f"caller: {caller}: by_alias={by_alias}, index {index}. " + msg += f"{json.dumps(model.model_dump(by_alias=by_alias), indent=4, sort_keys=True)}" + self.log.debug(msg) + + def commit(self) -> None: + """ + # Summary + + - Transmute diff_attach to a list of PayloadVrfsAttachments models. + - For each model, update its lan_attach_list + - Set vlan to 0 + - Set the fabric name to the child fabric name, if fabric is MSD + - Update vrf_lite extensions with information from the switch + + ## Raises + + - ValueError if diff_attach cannot be mutated + - ValueError if diff_attach is empty when commit() is called + - ValueError if instance.payload_model is accessed before commit() is called + - ValueError if instance.payload is accessed before commit() is called + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = f"ENTERED. caller: {caller}." + self.log.debug(msg) + + required_attrs = [ + ("sender", self.sender), + ("diff_attach", self.diff_attach), + ("fabric_inventory", self.fabric_inventory), + ("playbook_models", self.playbook_models), + ("ansible_module", self.ansible_module), + ] + + for attr_name, attr_value in required_attrs: + if not attr_value: + msg = f"{self.class_name}.{method_name}: {caller}: Set instance.{attr_name} before calling commit()." + self.log.debug(msg) + raise ValueError(msg) + + self.serial_number_to_fabric_name.fabric_inventory = self.fabric_inventory + self.serial_number_to_ipv4.fabric_inventory = self.fabric_inventory + self.serial_number_to_vrf_lite.playbook_models = self.playbook_models + self.serial_number_to_vrf_lite.fabric_inventory = self.fabric_inventory + self.serial_number_to_vrf_lite.commit() + + msg = f"Received diff_attach: {json.dumps(self.diff_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + diff_attach_list: list[PayloadVrfsAttachments] = [ + PayloadVrfsAttachments( + vrfName=item.get("vrfName", ""), + lanAttachList=[ + PayloadVrfsAttachmentsLanAttachListItem( + deployment=lan_attach.get("deployment"), + extensionValues=PayloadVrfsAttachmentsLanAttachListExtensionValues( + **json.loads(lan_attach.get("extensionValues")) if lan_attach.get("extensionValues") else {} + ), + fabric=lan_attach.get("fabric") or lan_attach.get("fabricName"), + freeformConfig=lan_attach.get("freeformConfig"), + instanceValues=PayloadVrfsAttachmentsLanAttachListInstanceValues( + **json.loads(lan_attach.get("instanceValues")) if lan_attach.get("instanceValues") else {} + ), + serialNumber=lan_attach.get("serialNumber"), + vlan=lan_attach.get("vlan") or lan_attach.get("vlanId") or 0, + vrfName=lan_attach.get("vrfName"), + ) + for lan_attach in item.get("lanAttachList", []) + ], + ) + for item in self.diff_attach + ] + + payload_model: list[PayloadVrfsAttachments] = [] + for vrf_attach_payload in diff_attach_list: + lan_attach_list = self.update_lan_attach_list_model(vrf_attach_payload) + vrf_attach_payload.lan_attach_list = lan_attach_list + payload_model.append(vrf_attach_payload) + + msg = f"Setting self._payload_model: type(payload_model[0]): {type(payload_model[0])} length: {len(payload_model)}." + self.log.debug(msg) + self.log_list_of_models(payload_model, by_alias=True) + self._payload_model = payload_model + + self._payload = [model.model_dump(exclude_unset=True, by_alias=True) for model in payload_model] + msg = f"Setting self._payload: {self._payload}" + self.log.debug(msg) + + def update_lan_attach_list_model(self, diff_attach: PayloadVrfsAttachments) -> list[PayloadVrfsAttachmentsLanAttachListItem]: + """ + # Summary + + - Update the lan_attach_list in each PayloadVrfsAttachments + - Set vlan to 0 + - Set the fabric name to the child fabric name, if fabric is MSD + - Update vrf_lite extensions with information from the switch + + ## Raises + + - ValueError if diff_attach cannot be mutated + """ + diff_attach = self.update_lan_attach_list_vlan(diff_attach) + diff_attach = self.update_lan_attach_list_fabric_name(diff_attach) + diff_attach = self.update_lan_attach_list_vrf_lite(diff_attach) + return diff_attach.lan_attach_list + + def update_lan_attach_list_vlan(self, diff_attach: PayloadVrfsAttachments) -> PayloadVrfsAttachments: + """ + # Summary + + Set PayloadVrfsAttachments.lan_attach_list.vlan to 0 and return the updated + PayloadVrfsAttachments instance. + + ## Raises + + - None + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}." + self.log.debug(msg) + + new_lan_attach_list = [] + for vrf_attach in diff_attach.lan_attach_list: + vrf_attach.vlan = 0 + new_lan_attach_list.append(vrf_attach) + diff_attach.lan_attach_list = new_lan_attach_list + msg = f"Returning updated diff_attach: {json.dumps(diff_attach.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + return diff_attach + + def update_lan_attach_list_fabric_name(self, diff_attach: PayloadVrfsAttachments) -> PayloadVrfsAttachments: + """ + # Summary + + Update PayloadVrfsAttachments.lan_attach_list.fabric and return the updated + PayloadVrfsAttachments instance. + + - If fabric_type is not MFD, return the diff_attach unchanged + - If fabric_type is MFD, replace diff_attach.lan_attach_list.fabric with child fabric name + + ## Raises + + - None + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}." + self.log.debug(msg) + + new_lan_attach_list = [] + for vrf_attach in diff_attach.lan_attach_list: + vrf_attach.fabric = self.get_vrf_attach_fabric_name(vrf_attach) + new_lan_attach_list.append(vrf_attach) + + diff_attach.lan_attach_list = new_lan_attach_list + msg = f"Returning updated diff_attach: {json.dumps(diff_attach.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + return diff_attach + + def update_lan_attach_list_vrf_lite(self, diff_attach: PayloadVrfsAttachments) -> PayloadVrfsAttachments: + """ + - If the switch is not a border switch, fail the module + - Get associated extension_prototype_values (ControllerResponseVrfsSwitchesExtensionPrototypeValue) from the switch + - Update vrf lite extensions with information from the extension_prototype_values + + ## Raises + + - fail_json: If the switch is not a border switch + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}." + self.log.debug(msg) + + new_lan_attach_list = [] + msg = f"len(diff_attach.lan_attach_list): {len(diff_attach.lan_attach_list)}" + self.log.debug(msg) + msg = "diff_attach.lan_attach_list: " + self.log.debug(msg) + self.log_list_of_models(diff_attach.lan_attach_list) + + for lan_attach_item in diff_attach.lan_attach_list: + serial_number = lan_attach_item.serial_number + + self.serial_number_to_vrf_lite.serial_number = serial_number + if self.serial_number_to_vrf_lite.vrf_lite is None: + msg = "Appending lan_attach_item to new_lan_attach_list " + msg += f"for serial_number {serial_number} which is not VRF LITE capable. " + msg += f"lan_attach_item: {json.dumps(lan_attach_item.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + new_lan_attach_list.append(lan_attach_item) + continue + + # VRF Lite processing + + msg = f"Processing lan_attach_item for serial_number {serial_number} " + msg += "which is VRF LITE capable. " + msg += f"lan_attach_item: {json.dumps(lan_attach_item.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"lan_attach_item.extension_values: {json.dumps(lan_attach_item.extension_values.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + ip_address = self.serial_number_to_ipv4.convert(lan_attach_item.serial_number) + if not self.is_border_switch(lan_attach_item.serial_number): + msg = f"{self.class_name}.{method_name}: " + msg += f"caller {caller}. " + msg += "VRF LITE cannot be attached to " + msg += "non-border switch. " + msg += f"ip: {ip_address}, " + msg += f"serial number: {lan_attach_item.serial_number}" + raise ValueError(msg) + + lite_objects_model = self.get_list_of_vrfs_switches_data_item_model(lan_attach_item) + + msg = f"ip_address {ip_address} ({lan_attach_item.serial_number}), " + msg += f"lite_objects: length {len(lite_objects_model)}." + self.log_list_of_models(lite_objects_model) + + if not lite_objects_model: + msg = f"ip_address {ip_address} ({lan_attach_item.serial_number}), " + msg += "No lite objects. Append lan_attach_item to new_attach_list and continue." + self.log.debug(msg) + new_lan_attach_list.append(lan_attach_item) + continue + + extension_prototype_values = lite_objects_model[0].switch_details_list[0].extension_prototype_values + msg = f"ip_address {ip_address} ({lan_attach_item.serial_number}), " + msg += f"lite (list[ControllerResponseVrfsSwitchesExtensionPrototypeValue]). length: {len(extension_prototype_values)}." + self.log.debug(msg) + self.log_list_of_models(extension_prototype_values) + + lan_attach_item = self.update_vrf_attach_vrf_lite_extensions(lan_attach_item, extension_prototype_values) + + new_lan_attach_list.append(lan_attach_item) + diff_attach.lan_attach_list = new_lan_attach_list + + msg = f"Returning updated diff_attach: {json.dumps(diff_attach.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + return diff_attach + + def update_vrf_attach_vrf_lite_extensions( + self, vrf_attach: PayloadVrfsAttachmentsLanAttachListItem, lite: list[ControllerResponseVrfsSwitchesExtensionPrototypeValue] + ) -> PayloadVrfsAttachmentsLanAttachListItem: + """ + # Summary + + Will replace update_vrf_attach_vrf_lite_extensions in the future. + + ## params + + - vrf_attach + A PayloadVrfsAttachmentsLanAttachListItem model containing extension_values to update. + - lite: A list of current vrf_lite extension models + (ControllerResponseVrfsSwitchesExtensionPrototypeValue) from the switch + + ## Description + + 1. Merge the values from the vrf_attach object into a matching vrf_lite extension object (if any) from the switch. + 2. Update the vrf_attach object with the merged result. + 3. Return the updated vrf_attach object. + + ## Raises + + - ValueError if: + - No matching ControllerResponseVrfsSwitchesExtensionPrototypeValue model is found, return the unmodified vrf_attach object. + + "matching" in this case means: + + 1. The extensionType of the switch's extension object is VRF_LITE + 2. The IF_NAME in the extensionValues of the extension object matches the interface in vrf_attach.extension_values. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}." + self.log.debug(msg) + + msg = "vrf_attach: " + msg += f"{json.dumps(vrf_attach.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + + serial_number = vrf_attach.serial_number + + msg = f"serial_number: {serial_number}, " + msg += f"Received list of lite_objects (list[ControllerResponseVrfsSwitchesExtensionPrototypeValue]). length: {len(lite)}." + self.log.debug(msg) + self.log_list_of_models(lite) + + ext_values = self.get_extension_values_from_lite_objects(lite) + if ext_values is None: + ip_address = self.serial_number_to_ipv4.convert(serial_number) + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"No VRF LITE capable interfaces found on switch {ip_address} ({serial_number})." + self.log.debug(msg) + self.ansible_module.fail_json(msg=msg) + + matches: dict = {} + user_vrf_lite_interfaces = [] + switch_vrf_lite_interfaces = [] + for item in vrf_attach.extension_values.VRF_LITE_CONN.VRF_LITE_CONN: + item_interface = item.IF_NAME + user_vrf_lite_interfaces.append(item_interface) + for ext_value in ext_values: + ext_value_interface = ext_value.if_name + switch_vrf_lite_interfaces.append(ext_value_interface) + msg = f"item_interface: {item_interface}, " + msg += f"ext_value_interface: {ext_value_interface}" + self.log.debug(msg) + if item_interface != ext_value_interface: + continue + msg = "Found item: " + msg += f"item[interface] {item_interface}, == " + msg += f"ext_values.if_name {ext_value_interface}." + self.log.debug(msg) + msg = f"{json.dumps(item.model_dump(by_alias=True), indent=4, sort_keys=True)}" + self.log.debug(msg) + matches[item_interface] = {"user": item, "switch": ext_value} + if not matches: + ip_address = self.serial_number_to_ipv4.convert(serial_number) + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "No matching interfaces with vrf_lite extensions " + msg += f"found on switch {ip_address} ({serial_number}). " + msg += "playbook vrf_lite_interfaces: " + msg += f"{','.join(sorted(user_vrf_lite_interfaces))}. " + msg += "switch vrf_lite_interfaces: " + msg += f"{','.join(sorted(switch_vrf_lite_interfaces))}." + self.log.debug(msg) + raise ValueError(msg) + + msg = "Matching extension object(s) found on the switch." + self.log.debug(msg) + + vrf_lite_conn_list = PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConn.model_construct() + + for interface, item in matches.items(): + user = item["user"] + switch = item["switch"] + msg = f"interface: {interface}: " + self.log.debug(msg) + msg = "item.user: " + msg += f"{json.dumps(user.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + msg = "item.switch: " + msg += f"{json.dumps(switch.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + vrf_lite_conn_item = PayloadVrfsAttachmentsLanAttachListExtensionValuesVrfLiteConnItem( + IF_NAME=user.IF_NAME, + DOT1Q_ID=str(user.DOT1Q_ID or switch.dot1q_id), + IP_MASK=user.IP_MASK or switch.ip_mask, + NEIGHBOR_IP=user.NEIGHBOR_IP or switch.neighbor_ip, + NEIGHBOR_ASN=switch.neighbor_asn, + IPV6_MASK=user.IPV6_MASK or switch.ipv6_mask, + IPV6_NEIGHBOR=user.IPV6_NEIGHBOR or switch.ipv6_neighbor, + AUTO_VRF_LITE_FLAG=switch.auto_vrf_lite_flag, + PEER_VRF_NAME=user.PEER_VRF_NAME or switch.peer_vrf_name, + VRF_LITE_JYTHON_TEMPLATE=user.VRF_LITE_JYTHON_TEMPLATE or switch.vrf_lite_jython_template or "Ext_VRF_Lite_Jython", + ) + vrf_lite_conn_list.VRF_LITE_CONN.append(vrf_lite_conn_item) + + multisite_conn = PayloadVrfsAttachmentsLanAttachListExtensionValuesMultisiteConn.model_construct() + multisite_conn.MULTISITE_CONN = [] + extension_values_model = PayloadVrfsAttachmentsLanAttachListExtensionValues.model_construct() + extension_values_model.MULTISITE_CONN = multisite_conn + extension_values_model.VRF_LITE_CONN = vrf_lite_conn_list + msg = f"extension_values_model: {json.dumps(extension_values_model.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + + vrf_attach.extension_values = extension_values_model + + msg = "Returning modified vrf_attach: " + msg += f"{json.dumps(vrf_attach.model_dump(), indent=4, sort_keys=True)}" + self.log.debug(msg) + return vrf_attach + + def get_extension_values_from_lite_objects( + self, lite: list[ControllerResponseVrfsSwitchesExtensionPrototypeValue] + ) -> list[ControllerResponseVrfsSwitchesVrfLiteConnProtoItem]: + """ + # Summary + + Given a list of lite objects (ControllerResponseVrfsSwitchesExtensionPrototypeValue), return: + + - A list containing the extensionValues (ControllerResponseVrfsSwitchesVrfLiteConnProtoItem), + if any, from these lite objects. + - An empty list, if the lite objects have no extensionValues + + ## Raises + + None + """ + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}" + self.log.debug(msg) + + extension_values_list: list[ControllerResponseVrfsSwitchesVrfLiteConnProtoItem] = [] + for item in lite: + if item.extension_type != "VRF_LITE": + continue + extension_values_list.append(item.extension_values) + + msg = f"Returning extension_values_list (list[ControllerResponseVrfsSwitchesVrfLiteConnProtoItem]). length: {len(extension_values_list)}." + self.log.debug(msg) + self.log_list_of_models(extension_values_list) + + return extension_values_list + + def get_list_of_vrfs_switches_data_item_model( + self, lan_attach_item: PayloadVrfsAttachmentsLanAttachListItem + ) -> list[ControllerResponseVrfsSwitchesDataItem]: + """ + # Summary + + Will replace get_list_of_vrfs_switches_data_item_model() in the future. + Retrieve the IP/Interface that is connected to the switch with serial_number + + PayloadVrfsAttachmentsLanAttachListItem must contain at least the following fields: + + - fabric: The fabric to search + - serial_number: The serial_number of the switch + - vrf_name: The vrf to search + """ + caller = inspect.stack()[1][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED. " + msg += f"caller: {caller}." + self.log.debug(msg) + + msg = f"lan_attach_item: {json.dumps(lan_attach_item.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + + verb = "GET" + path = "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics" + path += f"/{lan_attach_item.fabric}/vrfs/switches?vrf-names={lan_attach_item.vrf_name}&serial-numbers={lan_attach_item.serial_number}" + msg = f"verb: {verb}, path: {path}" + self.log.debug(msg) + lite_objects = self.sender(self.ansible_module, verb, path) + + if lite_objects is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{caller}: Unable to retrieve lite_objects." + raise ValueError(msg) + + try: + response = ControllerResponseVrfsSwitchesV12(**lite_objects) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{caller}: Unable to parse response: {error}" + raise ValueError(msg) from error + + msg = f"Returning list of VrfSwitchesDataItem. length {len(response.DATA)}." + self.log.debug(msg) + self.log_list_of_models(response.DATA) + + return response.DATA + + def get_vrf_attach_fabric_name(self, vrf_attach: PayloadVrfsAttachmentsLanAttachListItem) -> str: + """ + # Summary + + For multisite fabrics, return the name of the child fabric returned by + `self.serial_number_to_fabric[serial_number]` + + ## params + + - `vrf_attach` + + A PayloadVrfsAttachmentsLanAttachListItem model. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}." + self.log.debug(msg) + + msg = "Received vrf_attach: " + msg += f"{json.dumps(vrf_attach.model_dump(by_alias=True), indent=4, sort_keys=True)}" + self.log.debug(msg) + + if self.fabric_type != "MFD": + msg = f"FABRIC_TYPE {self.fabric_type} is not MFD. " + msg += f"Returning unmodified fabric name {vrf_attach.fabric}." + self.log.debug(msg) + return vrf_attach.fabric + + msg = f"fabric_type: {self.fabric_type}, " + msg += f"vrf_attach.fabric: {vrf_attach.fabric}." + self.log.debug(msg) + + serial_number = vrf_attach.serial_number + + if serial_number is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "Unable to parse vrf_attach.serial_number. " + msg += f"{json.dumps(vrf_attach.model_dump(by_alias=False), indent=4, sort_keys=True)}" + self.log.debug(msg) + raise ValueError(msg) + + try: + child_fabric_name = self.serial_number_to_fabric_name.convert(serial_number) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"Error retrieving child fabric name for serial_number {serial_number}. " + msg += f"Error detail: {error}" + self.log.debug(msg) + raise ValueError(msg) from error + + msg = f"serial_number: {serial_number}. " + msg += f"Returning child_fabric_name: {child_fabric_name}. " + self.log.debug(msg) + + return child_fabric_name + + def is_border_switch(self, serial_number) -> bool: + """ + # Summary + + Given a switch serial_number: + + - Return True if the switch is a border switch + - Return False otherwise + """ + is_border = False + ip_address = self.serial_number_to_ipv4.convert(serial_number) + role = self.fabric_inventory[ip_address].get("switchRole", "") + re_result = re.search(r"\bborder\b", role.lower()) + if re_result: + is_border = True + return is_border + + @property + def diff_attach(self) -> list[dict]: + """ + Return the diff_attach list, containing dictionaries representing the VRF attachments. + """ + return self._diff_attach + + @diff_attach.setter + def diff_attach(self, value: list[dict]): + self._diff_attach = value + + @property + def fabric_type(self) -> str: + """ + Return the fabric_type. + This should be set before calling commit(). + + TODO: remove this property once we use fabric_inventory.fabricTechnology for fabric_type. + """ + if self._fabric_type is None: + raise ValueError("Set instance.fabric_type before calling instance.commit.") + return self._fabric_type + + @fabric_type.setter + def fabric_type(self, value: str): + """ + Set the fabric type + """ + self._fabric_type = value + + @property + def fabric_inventory(self) -> dict: + """ + Return the fabric inventory, which maps IP addresses to switch details. + """ + return self._fabric_inventory + + @fabric_inventory.setter + def fabric_inventory(self, value: dict): + """ + Set the fabric map, which maps serial numbers to fabric names. + Used to determine the child fabric name for multisite fabrics. + """ + self._fabric_inventory = value + + @property + def ansible_module(self): + """ + Return the AnsibleModule instance. + """ + return self._ansible_module + + @ansible_module.setter + def ansible_module(self, value): + """ + Set the AnsibleModule instance. + """ + self._ansible_module = value + + @property + def payload_model(self) -> list[PayloadVrfsAttachments]: + """ + Return the payload as a list of PayloadVrfsAttachments. + """ + if not self._payload_model: + msg = f"{self.class_name}: payload_model is not set. Call commit() before accessing payload_model." + raise ValueError(msg) + return self._payload_model + + @property + def payload(self) -> list: + """ + Return the payload as a JSON string. + """ + if not self._payload: + msg = f"{self.class_name}: payload is not set. Call commit() before accessing payload." + raise ValueError(msg) + return self._payload + + @property + def playbook_models(self) -> list[PlaybookVrfModelV12]: + """ + Return the list of playbook models (list[PlaybookVrfModelV12]). + This should be set before calling commit(). + """ + return self._playbook_models + + @playbook_models.setter + def playbook_models(self, value): + if not isinstance(value, list): + raise TypeError("playbook_models must be a list of validated playbook configuration models.") + self._playbook_models = value + + @property + def sender(self) -> callable: + """ + Return sender. + """ + return self._sender + + @sender.setter + def sender(self, value: callable): + """ + Set sender. + """ + self._sender = value diff --git a/plugins/module_utils/vrf/vrf_controller_payload_v12.py b/plugins/module_utils/vrf/vrf_controller_payload_v12.py new file mode 100644 index 000000000..43ca7f530 --- /dev/null +++ b/plugins/module_utils/vrf/vrf_controller_payload_v12.py @@ -0,0 +1,142 @@ +""" +Validation model for payloads conforming the expectations of the +following endpoint: + +Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs +Verb: POST +""" + +import warnings +from typing import Union + +from pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning, field_serializer, field_validator, model_validator +from typing_extensions import Self + +from .vrf_template_config_v12 import VrfTemplateConfigV12 + +warnings.filterwarnings("ignore", category=PydanticExperimentalWarning) +warnings.filterwarnings("ignore", category=UserWarning) + +base_vrf_model_config = ConfigDict( + str_strip_whitespace=True, + use_enum_values=True, + validate_assignment=True, + populate_by_name=True, + populate_by_alias=True, +) + + +class VrfPayloadV12(BaseModel): + """ + # Summary + + Validation model for payloads conforming the expectations of the + following endpoint: + + On model_dump, the model will convert the vrfTemplateConfig + parameter into a JSON string, which is the expected format for + the controller. + + Verb: POST + Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs + + ## Raises + + ValueError if validation fails + + ## Structure + + Note, vrfTemplateConfig is received as a JSON string and converted by the model + into a dictionary so that its parameters can be validated. It should be + converted back into a JSON string before sending to the controller. + + ```json + { + "fabric": "fabric_1", + "hierarchicalKey": "fabric_1" + "serviceVrfTemplate": "", + "tenantName": "", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfId": 50011, + "vrfName": "vrf_1", + "vrfTemplate": "Default_VRF_Universal", + "vrfTemplateConfig": { + "advertiseDefaultRouteFlag": true, + "advertiseHostRouteFlag": false, + "asn": "65002", + "bgpPassword": "", + "bgpPasswordKeyType": 3, + "configureStaticDefaultRouteFlag": true, + "disableRtAuto": false, + "ENABLE_NETFLOW": false, + "ipv6LinkLocalFlag": true, + "isRPAbsent": false, + "isRPExternal": false, + "L3VniMcastGroup": "", + "maxBgpPaths": 1, + "maxIbgpPaths": 2, + "multicastGroup": "", + "mtu": 9216, + "NETFLOW_MONITOR": "", + "nveId": 1, + "routeTargetExport": "", + "routeTargetExportEvpn": "", + "routeTargetExportMvpn": "", + "routeTargetImport": "", + "routeTargetImportEvpn": "", + "routeTargetImportMvpn": "", + "rpAddress": "", + "tag": 12345, + "trmBGWMSiteEnabled": false, + "trmEnabled": false, + "vrfDescription": "", + "vrfIntfDescription": "", + "vrfName": "my_vrf", + "vrfRouteMap": "FABRIC-RMAP-REDIST-SUBNET", + "vrfSegmentId": 50022, + "vrfVlanId": 10, + "vrfVlanName": "vlan10" + } + } + ``` + """ + + model_config = base_vrf_model_config + + fabric: str = Field(..., alias="fabric", max_length=64, description="Fabric name in which the VRF resides.") + hierarchical_key: str = Field(alias="hierarchicalKey", default="", max_length=64) + service_vrf_template: str = Field(alias="serviceVrfTemplate", default="") + source: Union[str, None] = Field(default=None) + tenant_name: str = Field(alias="tenantName", default="") + vrf_extension_template: str = Field(alias="vrfExtensionTemplate", default="Default_VRF_Extension_Universal") + vrf_id: int = Field(..., alias="vrfId", ge=1, le=16777214) + vrf_name: str = Field(..., alias="vrfName", min_length=1, max_length=32, description="Name of the VRF, 1-32 characters.") + vrf_template: str = Field(alias="vrfTemplate", default="Default_VRF_Universal") + vrf_template_config: VrfTemplateConfigV12 = Field(alias="vrfTemplateConfig") + + @field_serializer("vrf_template_config") + def serialize_vrf_template_config(self, vrf_template_config: VrfTemplateConfigV12) -> str: + """ + Serialize the vrfTemplateConfig field to a JSON string required by the controller. + """ + return vrf_template_config.model_dump_json(exclude_none=True, by_alias=True) + + @field_validator("service_vrf_template", mode="before") + @classmethod + def validate_service_vrf_template(cls, value: Union[str, None]) -> str: + """ + Validate serviceVrfTemplate. If it is not empty, it must be a valid + service VRF template. + """ + if value is None: + return "" + return value + + @model_validator(mode="after") + def validate_hierarchical_key(self) -> Self: + """ + If hierarchicalKey is "", set it to the fabric name. + """ + if self.hierarchical_key == "": + self.hierarchical_key = self.fabric + return self diff --git a/plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py b/plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py new file mode 100644 index 000000000..c4fb0f5d4 --- /dev/null +++ b/plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py +# Copyright (c) 2020-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. +# pylint: disable=wrong-import-position +""" +Serialize NDFC v11 payload fields to fields used in a dcnm_vrf playbook. +""" +from typing import Optional, Union + +from pydantic import BaseModel, ConfigDict, Field + + +class VrfControllerToPlaybookV11Model(BaseModel): + """ + # Summary + + Serialize NDFC v11 payload fields to fields used in a dcnm_vrf playbook. + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + ) + adv_default_routes: Optional[bool] = Field(alias="advertiseDefaultRouteFlag") + adv_host_routes: Optional[bool] = Field(alias="advertiseHostRouteFlag") + + bgp_password: Optional[str] = Field(alias="bgpPassword") + bgp_passwd_encrypt: Optional[int] = Field(alias="bgpPasswordKeyType") + + ipv6_linklocal_enable: Optional[bool] = Field(alias="ipv6LinkLocalFlag") + + loopback_route_tag: Optional[int] = Field(alias="tag") + + max_bgp_paths: Optional[int] = Field(alias="maxBgpPaths") + max_ibgp_paths: Optional[int] = Field(alias="maxIbgpPaths") + + overlay_mcast_group: Optional[str] = Field(alias="multicastGroup") + + redist_direct_rmap: Optional[str] = Field(alias="vrfRouteMap") + rp_address: Optional[str] = Field(alias="rpAddress") + rp_external: Optional[bool] = Field(alias="isRPExternal") + rp_loopback_id: Optional[Union[int, str]] = Field(alias="loopbackNumber") + + static_default_route: Optional[bool] = Field(alias="configureStaticDefaultRouteFlag") + + trm_bgw_msite: Optional[bool] = Field(alias="trmBGWMSiteEnabled") + trm_enable: Optional[bool] = Field(alias="trmEnabled") + + underlay_mcast_ip: Optional[str] = Field(alias="L3VniMcastGroup") + + vrf_description: Optional[str] = Field(alias="vrfDescription") + vrf_int_mtu: Optional[int] = Field(alias="mtu") + vrf_intf_desc: Optional[str] = Field(alias="vrfIntfDescription") + vrf_vlan_name: Optional[str] = Field(alias="vrfVlanName") diff --git a/plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py b/plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py new file mode 100644 index 000000000..6166d130b --- /dev/null +++ b/plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py +# Copyright (c) 2020-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. +# pylint: disable=wrong-import-position +""" +Serialize NDFC version 12 controller payload fields to fields used in a dcnm_vrf playbook. +""" +from typing import Optional, Union + +from pydantic import BaseModel, ConfigDict, Field + + +class VrfControllerToPlaybookV12Model(BaseModel): + """ + # Summary + + Serialize NDFC version 12 controller payload fields to fields used in a dcnm_vrf playbook. + """ + + model_config = ConfigDict( + str_strip_whitespace=True, + ) + adv_default_routes: Optional[bool] = Field(alias="advertiseDefaultRouteFlag") + adv_host_routes: Optional[bool] = Field(alias="advertiseHostRouteFlag") + + bgp_password: Optional[str] = Field(alias="bgpPassword") + bgp_passwd_encrypt: Optional[int] = Field(alias="bgpPasswordKeyType") + + disable_rt_auto: Optional[bool] = Field(alias="disableRtAuto") + + export_evpn_rt: Optional[str] = Field(alias="routeTargetExportEvpn") + export_mvpn_rt: Optional[str] = Field(alias="routeTargetExportMvpn") + export_vpn_rt: Optional[str] = Field(alias="routeTargetExport") + + import_evpn_rt: Optional[str] = Field(alias="routeTargetImportEvpn") + import_mvpn_rt: Optional[str] = Field(alias="routeTargetImportMvpn") + import_vpn_rt: Optional[str] = Field(alias="routeTargetImport") + ipv6_linklocal_enable: Optional[bool] = Field(alias="ipv6LinkLocalFlag") + + loopback_route_tag: Optional[int] = Field(alias="tag") + + max_bgp_paths: Optional[int] = Field(alias="maxBgpPaths") + max_ibgp_paths: Optional[int] = Field(alias="maxIbgpPaths") + + netflow_enable: Optional[bool] = Field(alias="ENABLE_NETFLOW") + nf_monitor: Optional[str] = Field(alias="NETFLOW_MONITOR") + no_rp: Optional[bool] = Field(alias="isRPAbsent") + + overlay_mcast_group: Optional[str] = Field(alias="multicastGroup") + + redist_direct_rmap: Optional[str] = Field(alias="vrfRouteMap") + rp_address: Optional[str] = Field(alias="rpAddress") + rp_external: Optional[bool] = Field(alias="isRPExternal") + rp_loopback_id: Optional[Union[int, str]] = Field(alias="loopbackNumber") + + static_default_route: Optional[bool] = Field(alias="configureStaticDefaultRouteFlag") + + trm_bgw_msite: Optional[bool] = Field(alias="trmBGWMSiteEnabled") + trm_enable: Optional[bool] = Field(alias="trmEnabled") + + underlay_mcast_ip: Optional[str] = Field(alias="L3VniMcastGroup") + + vrf_description: Optional[str] = Field(alias="vrfDescription") + vrf_int_mtu: Optional[int] = Field(alias="mtu") + vrf_intf_desc: Optional[str] = Field(alias="vrfIntfDescription") + vrf_vlan_name: Optional[str] = Field(alias="vrfVlanName") diff --git a/plugins/module_utils/vrf/vrf_template_config_v12.py b/plugins/module_utils/vrf/vrf_template_config_v12.py new file mode 100644 index 000000000..512e89945 --- /dev/null +++ b/plugins/module_utils/vrf/vrf_template_config_v12.py @@ -0,0 +1,258 @@ +""" +Validation model for the vrfTemplateConfig field contents in the controller response +to the following endpoint: + +Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs +Verb: GET +""" + +import json +import warnings +from typing import Any, Optional, Union + +from pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning, field_validator, model_validator + +from ..common.enums.bgp import BgpPasswordEncrypt + +warnings.filterwarnings("ignore", category=PydanticExperimentalWarning) +warnings.filterwarnings("ignore", category=UserWarning) + +# Base configuration for the Vrf* models +base_vrf_model_config = ConfigDict( + str_strip_whitespace=True, + use_enum_values=True, + validate_assignment=True, + populate_by_name=True, + populate_by_alias=True, +) + + +class VrfTemplateConfigV12(BaseModel): + """ + vrfTempateConfig field contents in VrfPayloadV12 + """ + + model_config = base_vrf_model_config + + adv_default_routes: bool = Field(default=True, alias="advertiseDefaultRouteFlag", description="Advertise default route flag") + adv_host_routes: bool = Field(default=False, alias="advertiseHostRouteFlag", description="Advertise host route flag") + bgp_password: str = Field(default="", alias="bgpPassword", description="BGP password") + bgp_passwd_encrypt: int = Field(default=BgpPasswordEncrypt.MD5.value, alias="bgpPasswordKeyType", description="BGP password key type") + disable_rt_auto: bool = Field(default=False, alias="disableRtAuto", description="Disable RT auto") + export_evpn_rt: str = Field(default="", alias="routeTargetExportEvpn", description="Route target export EVPN") + export_mvpn_rt: str = Field(default="", alias="routeTargetExportMvpn", description="Route target export MVPN") + export_vpn_rt: str = Field(default="", alias="routeTargetExport", description="Route target export") + import_evpn_rt: str = Field(default="", alias="routeTargetImportEvpn", description="Route target import EVPN") + import_mvpn_rt: str = Field(default="", alias="routeTargetImportMvpn", description="Route target import MVPN") + import_vpn_rt: str = Field(default="", alias="routeTargetImport", description="Route target import") + ipv6_linklocal_enable: bool = Field( + default=True, + alias="ipv6LinkLocalFlag", + description="Enables IPv6 link-local Option under VRF SVI. Not applicable to L3VNI w/o VLAN config.", + ) + loopback_route_tag: int = Field(default=12345, ge=0, le=4294967295, alias="tag", description="Loopback routing tag") + max_bgp_paths: int = Field( + default=1, + ge=1, + le=64, + alias="maxBgpPaths", + description="Max BGP paths, 1-64 for NX-OS, 1-32 for IOS XE", + ) + max_ibgp_paths: int = Field( + default=2, + ge=1, + le=64, + alias="maxIbgpPaths", + description="Max IBGP paths, 1-64 for NX-OS, 1-32 for IOS XE", + ) + netflow_enable: bool = Field(default=False, alias="ENABLE_NETFLOW", description="Enable NetFlow") + nf_monitor: str = Field(default="", alias="NETFLOW_MONITOR", description="NetFlow monitor") + no_rp: bool = Field(default=False, alias="isRPAbsent", description="There is no RP in TRMv4 as only SSM is used") + overlay_mcast_group: str = Field(default="", alias="multicastGroup", description="Overlay Multicast group") + redist_direct_rmap: str = Field(default="FABRIC-RMAP-REDIST-SUBNET", alias="vrfRouteMap", description="VRF route map") + rp_address: str = Field( + default="", + alias="rpAddress", + description="IPv4 Address. Applicable when trmEnabled is True and isRPAbsent is False", + ) + rp_external: bool = Field(default=False, alias="isRPExternal", description="Is TRMv4 RP external to the fabric?") + rp_loopback_id: Optional[Union[int, str]] = Field(default="", alias="loopbackNumber", description="Loopback number") + static_default_route: bool = Field(default=True, alias="configureStaticDefaultRouteFlag", description="Configure static default route flag") + trm_bgw_msite: bool = Field( + default=False, + alias="trmBGWMSiteEnabled", + description="Tenent routed multicast border-gateway multi-site enabled", + ) + trm_enable: bool = Field(default=False, alias="trmEnabled", description="Enable IPv4 Tenant Routed Multicast (TRMv4)") + underlay_mcast_ip: str = Field(default="", alias="L3VniMcastGroup", description="L3 VNI multicast group") + vlan_id: int = Field(default=0, ge=0, le=4094, alias="vrfVlanId", description="VRF VLAN ID") + vrf_description: str = Field(default="", alias="vrfDescription", description="VRF description") + vrf_id: int = Field(..., ge=1, le=16777214, alias="vrfSegmentId", description="VRF segment ID") + vrf_int_mtu: int = Field(default=9216, ge=68, le=9216, alias="mtu", description="VRF interface MTU") + vrf_intf_desc: str = Field(default="", alias="vrfIntfDescription", description="VRF interface description") + vrf_name: str = Field(..., alias="vrfName", description="VRF name") + vrf_vlan_name: str = Field( + default="", + alias="vrfVlanName", + description="If > 32 chars, enable 'system vlan long-name' for NX-OS. Not applicable to L3VNI w/o VLAN config", + ) + + @field_validator("rp_loopback_id", mode="before") + @classmethod + def validate_rp_loopback_id(cls, data: Any) -> Union[int, str]: + """ + If rp_loopback_id is None, return "" + If rp_loopback_id is an empty string, return "" + If rp_loopback_id is an integer, verify it is within range 0-1023 + If rp_loopback_id is a non-empty string, try to convert to int and verify it is within range 0-1023 + + ## Raises + + - ValueError: If rp_loopback_id is not an integer or string representing an integer + - ValueError: If rp_loopback_id is not in range 0-1023 + + ## Notes + + - Replace this validator with the one using match-case when python 3.10 is the minimum version supported + """ + if data is None: + return "" + if data == "": + return "" + if isinstance(data, str): + try: + data = int(data) + except ValueError as error: + msg = "rp_loopback_id (loopbackNumber) must be an integer " + msg += "or string representing an integer. " + msg += f"Got: {data} of type {type(data)}. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error + if isinstance(data, int): + if data in range(0, 1024): + return data + msg = "rp_loopback_id (loopbackNumber) must be between 0 and 1023. " + msg += f"Got: {data}" + raise ValueError(msg) + # Return invalid data as-is. Type checking is done in the model_validator + return data + + @field_validator("vlan_id", mode="before") + @classmethod + def preprocess_vlan_id(cls, data: Any) -> int: + """ + Preprocess the vlan_id field to ensure it is an integer. + + ## Raises + + - ValueError: If vlan_id is not an integer or string representing an integer + - ValueError: If vlan_id is 1 + """ + if data is None: + return 0 + if isinstance(data, str): + try: + data = int(data) + except ValueError as error: + msg = "vlan_id (vrfVlanId) must be an integer " + msg += "or string representing an integer. " + msg += f"Got: {data} of type {type(data)}. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error + if data == 1: + msg = "vlan_id (vrfVlanId) must not be 1. " + msg += f"Got: {data}" + raise ValueError(msg) + # Further validation is done in the model_validator + return data + + @model_validator(mode="before") + @classmethod + def preprocess_data(cls, data: Any) -> Any: + """ + Convert incoming data + + - If data is a JSON string, use json.loads() to convert to a dict. + - If data is a dict, convert to int all fields that should be int. + - If data is already a VrfTemplateConfig model, return as-is. + """ + + if isinstance(data, str): + data = json.loads(data) + if isinstance(data, dict): + pass + if isinstance(data, VrfTemplateConfigV12): + pass + return data + + # Replace rp_loopback_id validator with this one when python 3.10 is the minimum version supported + ''' + @field_validator("rp_loopback_id", mode="before") + @classmethod + def validate_rp_loopback_id(cls, data: Any) -> Union[int, str]: + """ + If rp_loopback_id is None, return "" + If rp_loopback_id is an empty string, return "" + If rp_loopback_id is an integer, verify it is within range 0-1023 + If rp_loopback_id is a non-empty string, try to convert to int and verify it is within range 0-1023 + + ## Raises + + - ValueError: If rp_loopback_id is not an integer or string representing an integer + - ValueError: If rp_loopback_id is not in range 0-1023 + """ + match data: + case None: + return "" + case "": + return "" + case int(): + pass + case str(): + try: + data = int(data) + except ValueError as error: + msg = "rp_loopback_id (loopbackNumber) must be an integer " + msg += "or string representing an integer. " + msg += f"Got: {data} of type {type(data)}. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error + if data in range(0, 1024): + return data + msg = "rp_loopback_id (loopbackNumber) must be between 0 and 1023. " + msg += f"Got: {data}" + raise ValueError(msg) + ''' + + # Replace vlan_id validator with this one when python 3.10 is the minimum version supported + ''' + @field_validator("vlan_id", mode="before") + @classmethod + def preprocess_vlan_id(cls, data: Any) -> int: + """ + Preprocess the vlan_id field to ensure it is an integer. + + ## Raises + + - ValueError: If vlan_id is not an integer or string representing an integer + - ValueError: If vlan_id is 1 + """ + match data: + case None: + return 0 + case "": + return 0 + case 1 | "1": + msg = "vlan_id (vrfVlanId) must not be 1. " + msg += f"Got: {data}" + raise ValueError(msg) + case str(): + try: + data = int(data) + except ValueError as error: + msg = "vlan_id (vrfVlanId) must be an integer " + msg += "or string representing an integer. " + msg += f"Got: {data} of type {type(data)}. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error + ''' diff --git a/plugins/module_utils/vrf/vrf_utils.py b/plugins/module_utils/vrf/vrf_utils.py new file mode 100644 index 000000000..4d0c1b336 --- /dev/null +++ b/plugins/module_utils/vrf/vrf_utils.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +""" +Utilities specific to the dcnm_vrf module. +""" +from ..network.dcnm.dcnm import dcnm_send, parse_response + + +def calculate_items_per_chunk(query_string_items: str, query_string_item_list: list) -> int: + """ + Calculate the number of items per chunk based on the estimated average item length + and the maximum allowed URL size. + """ + max_url_size = 5900 # Room for path/query params + if not query_string_item_list: + return 1 + avg_item_len = max(1, len(query_string_items) // max(1, len(query_string_item_list))) + return max(1, max_url_size // (avg_item_len + 1)) # +1 for comma + + +def verify_response(module, response: str, fabric_name: str, vrfs: str, caller: str): + """ + Verify the response from the controller. + + Parameters: + module: Ansible module instance + response: Response from the controller + + Raises: + AnsibleModuleError: If the response indicates an error or if the fabric is missing. + """ + missing_fabric, not_ok = parse_response(response=response) + if missing_fabric or not_ok: + msg1 = f"Fabric {fabric_name} not present on the controller" + msg2 = f"{caller}: Unable to find vrfs {vrfs[:-1]} under fabric: {fabric_name}" + module.fail_json(msg=msg1 if missing_fabric else msg2) + + +def get_endpoint_with_long_query_string(module, fabric_name: str, path: str, query_string_items: str, caller: str = "NA"): + """ + ## Summary + + Query the controller endpoint, splitting the query string into chunks if necessary + to avoid exceeding the controller's URL length limit. + + Parameters: + module: An Ansible module instance + fabric_name: Name of the fabric to query + path: Controller endpoint to query + query_string_items: Comma-separated list of query items (e.g. vrf names) + caller: Freeform string used to identify the originator of the query (for debugging) + + Returns: + Consolidated response from the controller. + """ + query_string_item_list = query_string_items.split(",") + attach_objects = None + + items_per_chunk = calculate_items_per_chunk(query_string_items, query_string_item_list) + + for i in range(0, len(query_string_item_list), items_per_chunk): + query_string_subset = query_string_item_list[i : i + items_per_chunk] + url = path.format(fabric_name, ",".join(query_string_subset)) + attachment_objects = dcnm_send(module, "GET", url) + + verify_response(module=module, response=attachment_objects, fabric_name=fabric_name, vrfs=query_string_subset, caller=caller) + + if attach_objects is None: + attach_objects = attachment_objects + else: + attach_objects["DATA"].extend(attachment_objects["DATA"]) + + return attach_objects diff --git a/plugins/modules/dcnm_vrf.py b/plugins/modules/dcnm_vrf.py index 6133a0924..cc3200146 100644 --- a/plugins/modules/dcnm_vrf.py +++ b/plugins/modules/dcnm_vrf.py @@ -570,9 +570,16 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_get_ip_addr_info, dcnm_get_url, dcnm_send, dcnm_version_supported, - get_fabric_details, get_fabric_inventory_details, get_ip_sn_dict, - get_sn_fabric_dict, validate_list_of_dicts) + dcnm_get_ip_addr_info, + dcnm_get_url, + dcnm_send, + dcnm_version_supported, + get_fabric_details, + get_fabric_inventory_details, + get_ip_sn_dict, + get_sn_fabric_dict, + validate_list_of_dicts, +) from ..module_utils.common.log_v2 import Log @@ -868,48 +875,24 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): for have in have_a: if want["serialNumber"] == have["serialNumber"]: # handle instanceValues first - want.update( - {"freeformConfig": have.get("freeformConfig", "")} - ) # copy freeformConfig from have as module is not managing it + want.update({"freeformConfig": have.get("freeformConfig", "")}) # copy freeformConfig from have as module is not managing it want_inst_values = {} have_inst_values = {} - if ( - want["instanceValues"] is not None - and have["instanceValues"] is not None - ): + if want["instanceValues"] is not None and have["instanceValues"] is not None: want_inst_values = ast.literal_eval(want["instanceValues"]) have_inst_values = ast.literal_eval(have["instanceValues"]) # update unsupported parameters using have # Only need ipv4 or ipv6. Don't require both, but both can be supplied (as per the GUI) if "loopbackId" in have_inst_values: - want_inst_values.update( - {"loopbackId": have_inst_values["loopbackId"]} - ) + want_inst_values.update({"loopbackId": have_inst_values["loopbackId"]}) if "loopbackIpAddress" in have_inst_values: - want_inst_values.update( - { - "loopbackIpAddress": have_inst_values[ - "loopbackIpAddress" - ] - } - ) + want_inst_values.update({"loopbackIpAddress": have_inst_values["loopbackIpAddress"]}) if "loopbackIpV6Address" in have_inst_values: - want_inst_values.update( - { - "loopbackIpV6Address": have_inst_values[ - "loopbackIpV6Address" - ] - } - ) - - want.update( - {"instanceValues": json.dumps(want_inst_values)} - ) - if ( - want["extensionValues"] != "" - and have["extensionValues"] != "" - ): + want_inst_values.update({"loopbackIpV6Address": have_inst_values["loopbackIpV6Address"]}) + + want.update({"instanceValues": json.dumps(want_inst_values)}) + if want["extensionValues"] != "" and have["extensionValues"] != "": msg = "want[extensionValues] != '' and " msg += "have[extensionValues] != ''" @@ -923,10 +906,7 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): want_e = ast.literal_eval(want_ext_values["VRF_LITE_CONN"]) have_e = ast.literal_eval(have_ext_values["VRF_LITE_CONN"]) - if replace and ( - len(want_e["VRF_LITE_CONN"]) - != len(have_e["VRF_LITE_CONN"]) - ): + if replace and (len(want_e["VRF_LITE_CONN"]) != len(have_e["VRF_LITE_CONN"])): # In case of replace/override if the length of want and have lite attach of a switch # is not same then we have to push the want to NDFC. No further check is required for # this switch @@ -940,9 +920,7 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): continue found = True interface_match = True - if not self.compare_properties( - wlite, hlite, self.vrf_lite_properties - ): + if not self.compare_properties(wlite, hlite, self.vrf_lite_properties): found = False break @@ -955,15 +933,9 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): if interface_match and not found: break - elif ( - want["extensionValues"] != "" - and have["extensionValues"] == "" - ): + elif want["extensionValues"] != "" and have["extensionValues"] == "": found = False - elif ( - want["extensionValues"] == "" - and have["extensionValues"] != "" - ): + elif want["extensionValues"] == "" and have["extensionValues"] != "": if replace: found = False else: @@ -1040,9 +1012,7 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): msg += f"value {have_deployment}" self.log.debug(msg) - if (want_deployment != have_deployment) or ( - want_is_deploy != have_is_deploy - ): + if (want_deployment != have_deployment) or (want_is_deploy != have_is_deploy): if want_is_deploy is True: deploy_vrf = True @@ -1196,15 +1166,11 @@ def update_attach_params_extension_values(self, attach) -> dict: vrf_lite_connections["VRF_LITE_CONN"].append(copy.deepcopy(vrf_lite_conn)) if extension_values["VRF_LITE_CONN"]: - extension_values["VRF_LITE_CONN"]["VRF_LITE_CONN"].extend( - vrf_lite_connections["VRF_LITE_CONN"] - ) + extension_values["VRF_LITE_CONN"]["VRF_LITE_CONN"].extend(vrf_lite_connections["VRF_LITE_CONN"]) else: extension_values["VRF_LITE_CONN"] = copy.deepcopy(vrf_lite_connections) - extension_values["VRF_LITE_CONN"] = json.dumps( - extension_values["VRF_LITE_CONN"] - ) + extension_values["VRF_LITE_CONN"] = json.dumps(extension_values["VRF_LITE_CONN"]) msg = "Returning extension_values: " msg += f"{json.dumps(extension_values, indent=4, sort_keys=True)}" @@ -1240,9 +1206,7 @@ def update_attach_params(self, attach, vrf_name, deploy, vlan_id) -> dict: # dcnm_get_ip_addr_info converts serial_numbers, # hostnames, etc, to ip addresses. - attach["ip_address"] = dcnm_get_ip_addr_info( - self.module, attach["ip_address"], None, None - ) + attach["ip_address"] = dcnm_get_ip_addr_info(self.module, attach["ip_address"], None, None) serial = self.ip_to_serial_number(attach["ip_address"]) @@ -1274,9 +1238,7 @@ def update_attach_params(self, attach, vrf_name, deploy, vlan_id) -> dict: extension_values = self.update_attach_params_extension_values(attach) if extension_values: - attach.update( - {"extensionValues": json.dumps(extension_values).replace(" ", "")} - ) + attach.update({"extensionValues": json.dumps(extension_values).replace(" ", "")}) else: attach.update({"extensionValues": ""}) @@ -1383,9 +1345,7 @@ def diff_for_create(self, want, have): skip_keys = [] if vlan_id_want == "0": skip_keys = ["vrfVlanId"] - templates_differ = self.dict_values_differ( - json_to_dict_want, json_to_dict_have, skip_keys=skip_keys - ) + templates_differ = self.dict_values_differ(json_to_dict_want, json_to_dict_have, skip_keys=skip_keys) msg = f"templates_differ: {templates_differ}, " msg += f"vlan_id_want: {vlan_id_want}" @@ -1425,9 +1385,7 @@ def update_create_params(self, vrf, vlan_id=""): return vrf v_template = vrf.get("vrf_template", "Default_VRF_Universal") - ve_template = vrf.get( - "vrf_extension_template", "Default_VRF_Extension_Universal" - ) + ve_template = vrf.get("vrf_extension_template", "Default_VRF_Extension_Universal") src = None s_v_template = vrf.get("service_vrf_template", None) @@ -1436,9 +1394,7 @@ def update_create_params(self, vrf, vlan_id=""): "vrfName": vrf["vrf_name"], "vrfTemplate": v_template, "vrfExtensionTemplate": ve_template, - "vrfId": vrf.get( - "vrf_id", None - ), # vrf_id will be auto generated in get_diff_merge() + "vrfId": vrf.get("vrf_id", None), # vrf_id will be auto generated in get_diff_merge() "serviceVrfTemplate": s_v_template, "source": src, } @@ -1532,9 +1488,7 @@ def get_vrf_lite_objects(self, attach) -> dict: self.log.debug(msg) verb = "GET" - path = self.paths["GET_VRF_SWITCH"].format( - attach["fabric"], attach["vrfName"], attach["serialNumber"] - ) + path = self.paths["GET_VRF_SWITCH"].format(attach["fabric"], attach["vrfName"], attach["serialNumber"]) msg = f"verb: {verb}, path: {path}" self.log.debug(msg) lite_objects = dcnm_send(self.module, verb, path) @@ -1597,15 +1551,9 @@ def get_have(self): "L3VniMcastGroup": json_to_dict.get("L3VniMcastGroup", ""), "multicastGroup": json_to_dict.get("multicastGroup", ""), "trmBGWMSiteEnabled": json_to_dict.get("trmBGWMSiteEnabled", False), - "advertiseHostRouteFlag": json_to_dict.get( - "advertiseHostRouteFlag", False - ), - "advertiseDefaultRouteFlag": json_to_dict.get( - "advertiseDefaultRouteFlag", True - ), - "configureStaticDefaultRouteFlag": json_to_dict.get( - "configureStaticDefaultRouteFlag", True - ), + "advertiseHostRouteFlag": json_to_dict.get("advertiseHostRouteFlag", False), + "advertiseDefaultRouteFlag": json_to_dict.get("advertiseDefaultRouteFlag", True), + "configureStaticDefaultRouteFlag": json_to_dict.get("configureStaticDefaultRouteFlag", True), "bgpPassword": json_to_dict.get("bgpPassword", ""), "bgpPasswordKeyType": json_to_dict.get("bgpPasswordKeyType", 3), } @@ -1615,24 +1563,12 @@ def get_have(self): t_conf.update(ENABLE_NETFLOW=json_to_dict.get("ENABLE_NETFLOW", False)) t_conf.update(NETFLOW_MONITOR=json_to_dict.get("NETFLOW_MONITOR", "")) t_conf.update(disableRtAuto=json_to_dict.get("disableRtAuto", False)) - t_conf.update( - routeTargetImport=json_to_dict.get("routeTargetImport", "") - ) - t_conf.update( - routeTargetExport=json_to_dict.get("routeTargetExport", "") - ) - t_conf.update( - routeTargetImportEvpn=json_to_dict.get("routeTargetImportEvpn", "") - ) - t_conf.update( - routeTargetExportEvpn=json_to_dict.get("routeTargetExportEvpn", "") - ) - t_conf.update( - routeTargetImportMvpn=json_to_dict.get("routeTargetImportMvpn", "") - ) - t_conf.update( - routeTargetExportMvpn=json_to_dict.get("routeTargetExportMvpn", "") - ) + t_conf.update(routeTargetImport=json_to_dict.get("routeTargetImport", "")) + t_conf.update(routeTargetExport=json_to_dict.get("routeTargetExport", "")) + t_conf.update(routeTargetImportEvpn=json_to_dict.get("routeTargetImportEvpn", "")) + t_conf.update(routeTargetExportEvpn=json_to_dict.get("routeTargetExportEvpn", "")) + t_conf.update(routeTargetImportMvpn=json_to_dict.get("routeTargetImportMvpn", "")) + t_conf.update(routeTargetExportMvpn=json_to_dict.get("routeTargetExportMvpn", "")) vrf.update({"vrfTemplateConfig": json.dumps(t_conf)}) del vrf["vrfStatus"] @@ -1649,10 +1585,7 @@ def get_have(self): attach_state = not attach["lanAttachState"] == "NA" deploy = attach["isLanAttached"] deployed = False - if deploy and ( - attach["lanAttachState"] == "OUT-OF-SYNC" - or attach["lanAttachState"] == "PENDING" - ): + if deploy and (attach["lanAttachState"] == "OUT-OF-SYNC" or attach["lanAttachState"] == "PENDING"): deployed = False else: deployed = True @@ -1716,22 +1649,14 @@ def get_have(self): for ev in ext_values.get("VRF_LITE_CONN"): ev_dict = copy.deepcopy(ev) ev_dict.update({"AUTO_VRF_LITE_FLAG": "false"}) - ev_dict.update( - {"VRF_LITE_JYTHON_TEMPLATE": "Ext_VRF_Lite_Jython"} - ) + ev_dict.update({"VRF_LITE_JYTHON_TEMPLATE": "Ext_VRF_Lite_Jython"}) if extension_values["VRF_LITE_CONN"]: - extension_values["VRF_LITE_CONN"][ - "VRF_LITE_CONN" - ].extend([ev_dict]) + extension_values["VRF_LITE_CONN"]["VRF_LITE_CONN"].extend([ev_dict]) else: - extension_values["VRF_LITE_CONN"] = { - "VRF_LITE_CONN": [ev_dict] - } + extension_values["VRF_LITE_CONN"] = {"VRF_LITE_CONN": [ev_dict]} - extension_values["VRF_LITE_CONN"] = json.dumps( - extension_values["VRF_LITE_CONN"] - ) + extension_values["VRF_LITE_CONN"] = json.dumps(extension_values["VRF_LITE_CONN"]) ms_con = {} ms_con["MULTISITE_CONN"] = [] @@ -1816,9 +1741,7 @@ def get_want(self): continue for attach in vrf["attach"]: deploy = vrf_deploy - vrfs.append( - self.update_attach_params(attach, vrf_name, deploy, vlan_id) - ) + vrfs.append(self.update_attach_params(attach, vrf_name, deploy, vlan_id)) if vrfs: vrf_attach.update({"vrfName": vrf_name}) @@ -1873,16 +1796,12 @@ def get_items_to_detach(attach_list): for want_c in self.want_create: - if not self.find_dict_in_list_by_key_value( - search=self.have_create, key="vrfName", value=want_c["vrfName"] - ): + if not self.find_dict_in_list_by_key_value(search=self.have_create, key="vrfName", value=want_c["vrfName"]): continue diff_delete.update({want_c["vrfName"]: "DEPLOYED"}) - have_a = self.find_dict_in_list_by_key_value( - search=self.have_attach, key="vrfName", value=want_c["vrfName"] - ) + have_a = self.find_dict_in_list_by_key_value(search=self.have_attach, key="vrfName", value=want_c["vrfName"]) if not have_a: continue @@ -1940,9 +1859,7 @@ def get_diff_override(self): diff_undeploy = self.diff_undeploy for have_a in self.have_attach: - found = self.find_dict_in_list_by_key_value( - search=self.want_create, key="vrfName", value=have_a["vrfName"] - ) + found = self.find_dict_in_list_by_key_value(search=self.want_create, key="vrfName", value=have_a["vrfName"]) detach_list = [] if not found: @@ -2019,9 +1936,7 @@ def get_diff_replace(self): break if not h_in_w: - found = self.find_dict_in_list_by_key_value( - search=self.want_create, key="vrfName", value=have_a["vrfName"] - ) + found = self.find_dict_in_list_by_key_value(search=self.want_create, key="vrfName", value=have_a["vrfName"]) if found: atch_h = have_a["lanAttachList"] @@ -2204,56 +2119,24 @@ def diff_merge_create(self, replace=False): "L3VniMcastGroup": json_to_dict.get("L3VniMcastGroup"), "multicastGroup": json_to_dict.get("multicastGroup"), "trmBGWMSiteEnabled": json_to_dict.get("trmBGWMSiteEnabled"), - "advertiseHostRouteFlag": json_to_dict.get( - "advertiseHostRouteFlag" - ), - "advertiseDefaultRouteFlag": json_to_dict.get( - "advertiseDefaultRouteFlag" - ), - "configureStaticDefaultRouteFlag": json_to_dict.get( - "configureStaticDefaultRouteFlag" - ), + "advertiseHostRouteFlag": json_to_dict.get("advertiseHostRouteFlag"), + "advertiseDefaultRouteFlag": json_to_dict.get("advertiseDefaultRouteFlag"), + "configureStaticDefaultRouteFlag": json_to_dict.get("configureStaticDefaultRouteFlag"), "bgpPassword": json_to_dict.get("bgpPassword"), "bgpPasswordKeyType": json_to_dict.get("bgpPasswordKeyType"), } if self.dcnm_version > 11: template_conf.update(isRPAbsent=json_to_dict.get("isRPAbsent")) - template_conf.update( - ENABLE_NETFLOW=json_to_dict.get("ENABLE_NETFLOW") - ) - template_conf.update( - NETFLOW_MONITOR=json_to_dict.get("NETFLOW_MONITOR") - ) - template_conf.update( - disableRtAuto=json_to_dict.get("disableRtAuto") - ) - template_conf.update( - routeTargetImport=json_to_dict.get("routeTargetImport") - ) - template_conf.update( - routeTargetExport=json_to_dict.get("routeTargetExport") - ) - template_conf.update( - routeTargetImportEvpn=json_to_dict.get( - "routeTargetImportEvpn" - ) - ) - template_conf.update( - routeTargetExportEvpn=json_to_dict.get( - "routeTargetExportEvpn" - ) - ) - template_conf.update( - routeTargetImportMvpn=json_to_dict.get( - "routeTargetImportMvpn" - ) - ) - template_conf.update( - routeTargetExportMvpn=json_to_dict.get( - "routeTargetExportMvpn" - ) - ) + template_conf.update(ENABLE_NETFLOW=json_to_dict.get("ENABLE_NETFLOW")) + template_conf.update(NETFLOW_MONITOR=json_to_dict.get("NETFLOW_MONITOR")) + template_conf.update(disableRtAuto=json_to_dict.get("disableRtAuto")) + template_conf.update(routeTargetImport=json_to_dict.get("routeTargetImport")) + template_conf.update(routeTargetExport=json_to_dict.get("routeTargetExport")) + template_conf.update(routeTargetImportEvpn=json_to_dict.get("routeTargetImportEvpn")) + template_conf.update(routeTargetExportEvpn=json_to_dict.get("routeTargetExportEvpn")) + template_conf.update(routeTargetImportMvpn=json_to_dict.get("routeTargetImportMvpn")) + template_conf.update(routeTargetExportMvpn=json_to_dict.get("routeTargetExportMvpn")) want_c.update({"vrfTemplateConfig": json.dumps(template_conf)}) @@ -2265,9 +2148,7 @@ def diff_merge_create(self, replace=False): continue # arobel: TODO: Not covered by UT - resp = dcnm_send( - self.module, "POST", create_path, json.dumps(want_c) - ) + resp = dcnm_send(self.module, "POST", create_path, json.dumps(want_c)) self.result["response"].append(resp) fail, self.result["changed"] = self.handle_response(resp, "create") @@ -2306,32 +2187,23 @@ def diff_merge_attach(self, replace=False): for want_a in self.want_attach: # Check user intent for this VRF and don't add it to the deploy_vrf # list if the user has not requested a deploy. - want_config = self.find_dict_in_list_by_key_value( - search=self.config, key="vrf_name", value=want_a["vrfName"] - ) + want_config = self.find_dict_in_list_by_key_value(search=self.config, key="vrf_name", value=want_a["vrfName"]) deploy_vrf = "" attach_found = False for have_a in self.have_attach: if want_a["vrfName"] == have_a["vrfName"]: attach_found = True - diff, deploy_vrf_bool = self.diff_for_attach_deploy( - want_a["lanAttachList"], have_a["lanAttachList"], replace - ) + diff, deploy_vrf_bool = self.diff_for_attach_deploy(want_a["lanAttachList"], have_a["lanAttachList"], replace) if diff: base = want_a.copy() del base["lanAttachList"] base.update({"lanAttachList": diff}) diff_attach.append(base) - if (want_config["deploy"] is True) and ( - deploy_vrf_bool is True - ): + if (want_config["deploy"] is True) and (deploy_vrf_bool is True): deploy_vrf = want_a["vrfName"] else: - if want_config["deploy"] is True and ( - deploy_vrf_bool - or self.conf_changed.get(want_a["vrfName"], False) - ): + if want_config["deploy"] is True and (deploy_vrf_bool or self.conf_changed.get(want_a["vrfName"], False)): deploy_vrf = want_a["vrfName"] msg = f"attach_found: {attach_found}" @@ -2403,12 +2275,8 @@ def format_diff(self): diff_create_update = copy.deepcopy(self.diff_create_update) diff_attach = copy.deepcopy(self.diff_attach) diff_detach = copy.deepcopy(self.diff_detach) - diff_deploy = ( - self.diff_deploy["vrfNames"].split(",") if self.diff_deploy else [] - ) - diff_undeploy = ( - self.diff_undeploy["vrfNames"].split(",") if self.diff_undeploy else [] - ) + diff_deploy = self.diff_deploy["vrfNames"].split(",") if self.diff_deploy else [] + diff_undeploy = self.diff_undeploy["vrfNames"].split(",") if self.diff_undeploy else [] msg = "INPUT: diff_create: " msg += f"{json.dumps(diff_create, indent=4, sort_keys=True)}" @@ -2449,9 +2317,7 @@ def format_diff(self): msg += f"{json.dumps(want_d, indent=4, sort_keys=True)}" self.log.debug(msg) - found_a = self.find_dict_in_list_by_key_value( - search=diff_attach, key="vrfName", value=want_d["vrfName"] - ) + found_a = self.find_dict_in_list_by_key_value(search=diff_attach, key="vrfName", value=want_d["vrfName"]) msg = "found_a: " msg += f"{json.dumps(found_a, indent=4, sort_keys=True)}" @@ -2475,79 +2341,37 @@ def format_diff(self): json_to_dict = json.loads(found_c["vrfTemplateConfig"]) found_c.update({"vrf_vlan_name": json_to_dict.get("vrfVlanName", "")}) - found_c.update( - {"vrf_intf_desc": json_to_dict.get("vrfIntfDescription", "")} - ) + found_c.update({"vrf_intf_desc": json_to_dict.get("vrfIntfDescription", "")}) found_c.update({"vrf_description": json_to_dict.get("vrfDescription", "")}) found_c.update({"vrf_int_mtu": json_to_dict.get("mtu", "")}) found_c.update({"loopback_route_tag": json_to_dict.get("tag", "")}) found_c.update({"redist_direct_rmap": json_to_dict.get("vrfRouteMap", "")}) found_c.update({"max_bgp_paths": json_to_dict.get("maxBgpPaths", "")}) found_c.update({"max_ibgp_paths": json_to_dict.get("maxIbgpPaths", "")}) - found_c.update( - {"ipv6_linklocal_enable": json_to_dict.get("ipv6LinkLocalFlag", True)} - ) + found_c.update({"ipv6_linklocal_enable": json_to_dict.get("ipv6LinkLocalFlag", True)}) found_c.update({"trm_enable": json_to_dict.get("trmEnabled", False)}) found_c.update({"rp_external": json_to_dict.get("isRPExternal", False)}) found_c.update({"rp_address": json_to_dict.get("rpAddress", "")}) found_c.update({"rp_loopback_id": json_to_dict.get("loopbackNumber", "")}) - found_c.update( - {"underlay_mcast_ip": json_to_dict.get("L3VniMcastGroup", "")} - ) - found_c.update( - {"overlay_mcast_group": json_to_dict.get("multicastGroup", "")} - ) - found_c.update( - {"trm_bgw_msite": json_to_dict.get("trmBGWMSiteEnabled", False)} - ) - found_c.update( - {"adv_host_routes": json_to_dict.get("advertiseHostRouteFlag", False)} - ) - found_c.update( - { - "adv_default_routes": json_to_dict.get( - "advertiseDefaultRouteFlag", True - ) - } - ) - found_c.update( - { - "static_default_route": json_to_dict.get( - "configureStaticDefaultRouteFlag", True - ) - } - ) + found_c.update({"underlay_mcast_ip": json_to_dict.get("L3VniMcastGroup", "")}) + found_c.update({"overlay_mcast_group": json_to_dict.get("multicastGroup", "")}) + found_c.update({"trm_bgw_msite": json_to_dict.get("trmBGWMSiteEnabled", False)}) + found_c.update({"adv_host_routes": json_to_dict.get("advertiseHostRouteFlag", False)}) + found_c.update({"adv_default_routes": json_to_dict.get("advertiseDefaultRouteFlag", True)}) + found_c.update({"static_default_route": json_to_dict.get("configureStaticDefaultRouteFlag", True)}) found_c.update({"bgp_password": json_to_dict.get("bgpPassword", "")}) - found_c.update( - {"bgp_passwd_encrypt": json_to_dict.get("bgpPasswordKeyType", "")} - ) + found_c.update({"bgp_passwd_encrypt": json_to_dict.get("bgpPasswordKeyType", "")}) if self.dcnm_version > 11: found_c.update({"no_rp": json_to_dict.get("isRPAbsent", False)}) - found_c.update( - {"netflow_enable": json_to_dict.get("ENABLE_NETFLOW", True)} - ) + found_c.update({"netflow_enable": json_to_dict.get("ENABLE_NETFLOW", True)}) found_c.update({"nf_monitor": json_to_dict.get("NETFLOW_MONITOR", "")}) - found_c.update( - {"disable_rt_auto": json_to_dict.get("disableRtAuto", False)} - ) - found_c.update( - {"import_vpn_rt": json_to_dict.get("routeTargetImport", "")} - ) - found_c.update( - {"export_vpn_rt": json_to_dict.get("routeTargetExport", "")} - ) - found_c.update( - {"import_evpn_rt": json_to_dict.get("routeTargetImportEvpn", "")} - ) - found_c.update( - {"export_evpn_rt": json_to_dict.get("routeTargetExportEvpn", "")} - ) - found_c.update( - {"import_mvpn_rt": json_to_dict.get("routeTargetImportMvpn", "")} - ) - found_c.update( - {"export_mvpn_rt": json_to_dict.get("routeTargetExportMvpn", "")} - ) + found_c.update({"disable_rt_auto": json_to_dict.get("disableRtAuto", False)}) + found_c.update({"import_vpn_rt": json_to_dict.get("routeTargetImport", "")}) + found_c.update({"export_vpn_rt": json_to_dict.get("routeTargetExport", "")}) + found_c.update({"import_evpn_rt": json_to_dict.get("routeTargetImportEvpn", "")}) + found_c.update({"export_evpn_rt": json_to_dict.get("routeTargetExportEvpn", "")}) + found_c.update({"import_mvpn_rt": json_to_dict.get("routeTargetImportMvpn", "")}) + found_c.update({"export_mvpn_rt": json_to_dict.get("routeTargetExportMvpn", "")}) del found_c["fabric"] del found_c["vrfName"] @@ -2634,10 +2458,7 @@ def get_diff_query(self): missing_fabric, not_ok = self.handle_response(vrf_objects, "query_dcnm") - if ( - vrf_objects.get("ERROR") == "Not Found" - and vrf_objects.get("RETURN_CODE") == 404 - ): + if vrf_objects.get("ERROR") == "Not Found" and vrf_objects.get("RETURN_CODE") == 404: msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " msg += f"Fabric {self.fabric} does not exist on the controller" @@ -2666,15 +2487,11 @@ def get_diff_query(self): item["parent"] = vrf # Query the Attachment for the found VRF - path = self.paths["GET_VRF_ATTACH"].format( - self.fabric, vrf["vrfName"] - ) + path = self.paths["GET_VRF_ATTACH"].format(self.fabric, vrf["vrfName"]) vrf_attach_objects = dcnm_send(self.module, "GET", path) - missing_fabric, not_ok = self.handle_response( - vrf_attach_objects, "query_dcnm" - ) + missing_fabric, not_ok = self.handle_response(vrf_attach_objects, "query_dcnm") if missing_fabric or not_ok: # arobel: TODO: Not covered by UT @@ -2700,12 +2517,8 @@ def get_diff_query(self): # get_vrf_lite_objects() expects. attach_copy = copy.deepcopy(attach) attach_copy.update({"fabric": self.fabric}) - attach_copy.update( - {"serialNumber": attach["switchSerialNo"]} - ) - lite_objects = self.get_vrf_lite_objects( - attach_copy - ) + attach_copy.update({"serialNumber": attach["switchSerialNo"]}) + lite_objects = self.get_vrf_lite_objects(attach_copy) if not lite_objects.get("DATA"): return item["attach"].append(lite_objects.get("DATA")[0]) @@ -2972,12 +2785,8 @@ def push_diff_create(self, is_rollback=False): "multicastGroup": json_to_dict.get("multicastGroup"), "trmBGWMSiteEnabled": json_to_dict.get("trmBGWMSiteEnabled"), "advertiseHostRouteFlag": json_to_dict.get("advertiseHostRouteFlag"), - "advertiseDefaultRouteFlag": json_to_dict.get( - "advertiseDefaultRouteFlag" - ), - "configureStaticDefaultRouteFlag": json_to_dict.get( - "configureStaticDefaultRouteFlag" - ), + "advertiseDefaultRouteFlag": json_to_dict.get("advertiseDefaultRouteFlag"), + "configureStaticDefaultRouteFlag": json_to_dict.get("configureStaticDefaultRouteFlag"), "bgpPassword": json_to_dict.get("bgpPassword"), "bgpPasswordKeyType": json_to_dict.get("bgpPasswordKeyType"), } @@ -2989,18 +2798,10 @@ def push_diff_create(self, is_rollback=False): t_conf.update(disableRtAuto=json_to_dict.get("disableRtAuto")) t_conf.update(routeTargetImport=json_to_dict.get("routeTargetImport")) t_conf.update(routeTargetExport=json_to_dict.get("routeTargetExport")) - t_conf.update( - routeTargetImportEvpn=json_to_dict.get("routeTargetImportEvpn") - ) - t_conf.update( - routeTargetExportEvpn=json_to_dict.get("routeTargetExportEvpn") - ) - t_conf.update( - routeTargetImportMvpn=json_to_dict.get("routeTargetImportMvpn") - ) - t_conf.update( - routeTargetExportMvpn=json_to_dict.get("routeTargetExportMvpn") - ) + t_conf.update(routeTargetImportEvpn=json_to_dict.get("routeTargetImportEvpn")) + t_conf.update(routeTargetExportEvpn=json_to_dict.get("routeTargetExportEvpn")) + t_conf.update(routeTargetImportMvpn=json_to_dict.get("routeTargetImportMvpn")) + t_conf.update(routeTargetExportMvpn=json_to_dict.get("routeTargetExportMvpn")) vrf.update({"vrfTemplateConfig": json.dumps(t_conf)}) @@ -3012,9 +2813,7 @@ def push_diff_create(self, is_rollback=False): path = self.paths["GET_VRF"].format(self.fabric) payload = copy.deepcopy(vrf) - self.send_to_controller( - action, verb, path, payload, log_response=True, is_rollback=is_rollback - ) + self.send_to_controller(action, verb, path, payload, log_response=True, is_rollback=is_rollback) def is_border_switch(self, serial_number) -> bool: """ @@ -3235,9 +3034,7 @@ def update_vrf_attach_vrf_lite_extensions(self, vrf_attach, lite) -> dict: vrflite_con["VRF_LITE_CONN"] = [] vrflite_con["VRF_LITE_CONN"].append(copy.deepcopy(nbr_dict)) if extension_values["VRF_LITE_CONN"]: - extension_values["VRF_LITE_CONN"]["VRF_LITE_CONN"].extend( - vrflite_con["VRF_LITE_CONN"] - ) + extension_values["VRF_LITE_CONN"]["VRF_LITE_CONN"].extend(vrflite_con["VRF_LITE_CONN"]) else: extension_values["VRF_LITE_CONN"] = vrflite_con @@ -3245,9 +3042,7 @@ def update_vrf_attach_vrf_lite_extensions(self, vrf_attach, lite) -> dict: ms_con["MULTISITE_CONN"] = [] extension_values["MULTISITE_CONN"] = json.dumps(ms_con) - extension_values["VRF_LITE_CONN"] = json.dumps( - extension_values["VRF_LITE_CONN"] - ) + extension_values["VRF_LITE_CONN"] = json.dumps(extension_values["VRF_LITE_CONN"]) vrf_attach["extensionValues"] = json.dumps(extension_values).replace(" ", "") if vrf_attach.get("vrf_lite") is not None: del vrf_attach["vrf_lite"] @@ -3527,9 +3322,7 @@ def push_diff_attach(self, is_rollback=False): self.log.debug(msg) return - lite = lite_objects["DATA"][0]["switchDetailsList"][0][ - "extensionPrototypeValues" - ] + lite = lite_objects["DATA"][0]["switchDetailsList"][0]["extensionPrototypeValues"] msg = f"ip_address {ip_address} ({serial_number}), " msg += "lite: " msg += f"{json.dumps(lite, indent=4, sort_keys=True)}" @@ -3540,9 +3333,7 @@ def push_diff_attach(self, is_rollback=False): msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" self.log.debug(msg) - vrf_attach = self.update_vrf_attach_vrf_lite_extensions( - vrf_attach, lite - ) + vrf_attach = self.update_vrf_attach_vrf_lite_extensions(vrf_attach, lite) msg = f"ip_address {ip_address} ({serial_number}), " msg += "new vrf_attach: " msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" @@ -3805,16 +3596,10 @@ def wait_for_vrf_del_ready(self, vrf_name="not_supplied"): self.log.debug(msg) for attach in attach_list: - if ( - attach["lanAttachState"] == "OUT-OF-SYNC" - or attach["lanAttachState"] == "FAILED" - ): + if attach["lanAttachState"] == "OUT-OF-SYNC" or attach["lanAttachState"] == "FAILED": self.diff_delete.update({vrf: "OUT-OF-SYNC"}) break - if ( - attach["lanAttachState"] == "DEPLOYED" - and attach["isLanAttached"] is True - ): + if attach["lanAttachState"] == "DEPLOYED" and attach["isLanAttached"] is True: vrf_name = attach.get("vrfName", "unknown") fabric_name = attach.get("fabricName", "unknown") switch_ip = attach.get("ipAddress", "unknown") @@ -4003,16 +3788,12 @@ def validate_input(self): vrf["service_vrf_template"] = None if "vrf_name" not in vrf: - fail_msg_list.append( - "vrf_name is mandatory under vrf parameters" - ) + fail_msg_list.append("vrf_name is mandatory under vrf parameters") if isinstance(vrf.get("attach"), list): for attach in vrf["attach"]: if "ip_address" not in attach: - fail_msg_list.append( - "ip_address is mandatory under attach parameters" - ) + fail_msg_list.append("ip_address is mandatory under attach parameters") else: if self.state in ("merged", "replaced"): msg = f"config element is mandatory for {self.state} state" @@ -4024,9 +3805,7 @@ def validate_input(self): self.module.fail_json(msg=msg) if self.config: - valid_vrf, invalid_params = validate_list_of_dicts( - self.config, vrf_spec - ) + valid_vrf, invalid_params = validate_list_of_dicts(self.config, vrf_spec) for vrf in valid_vrf: msg = f"state {self.state}: " @@ -4037,9 +3816,7 @@ def validate_input(self): if vrf.get("attach"): for entry in vrf.get("attach"): entry["deploy"] = vrf["deploy"] - valid_att, invalid_att = validate_list_of_dicts( - vrf["attach"], attach_spec - ) + valid_att, invalid_att = validate_list_of_dicts(vrf["attach"], attach_spec) msg = f"state {self.state}: " msg += "valid_att: " msg += f"{json.dumps(valid_att, indent=4, sort_keys=True)}" @@ -4049,9 +3826,7 @@ def validate_input(self): invalid_params.extend(invalid_att) for lite in vrf.get("attach"): if lite.get("vrf_lite"): - valid_lite, invalid_lite = validate_list_of_dicts( - lite["vrf_lite"], lite_spec - ) + valid_lite, invalid_lite = validate_list_of_dicts(lite["vrf_lite"], lite_spec) msg = f"state {self.state}: " msg += "valid_lite: " msg += f"{json.dumps(valid_lite, indent=4, sort_keys=True)}" @@ -4076,21 +3851,15 @@ def validate_input(self): else: if self.config: - valid_vrf, invalid_params = validate_list_of_dicts( - self.config, vrf_spec - ) + valid_vrf, invalid_params = validate_list_of_dicts(self.config, vrf_spec) for vrf in valid_vrf: if vrf.get("attach"): - valid_att, invalid_att = validate_list_of_dicts( - vrf["attach"], attach_spec - ) + valid_att, invalid_att = validate_list_of_dicts(vrf["attach"], attach_spec) vrf["attach"] = valid_att invalid_params.extend(invalid_att) for lite in vrf.get("attach"): if lite.get("vrf_lite"): - valid_lite, invalid_lite = validate_list_of_dicts( - lite["vrf_lite"], lite_spec - ) + valid_lite, invalid_lite = validate_list_of_dicts(lite["vrf_lite"], lite_spec) msg = f"state {self.state}: " msg += "valid_lite: " msg += f"{json.dumps(valid_lite, indent=4, sort_keys=True)}" @@ -4177,9 +3946,7 @@ def failure(self, resp): if not resp.get("DATA"): data = copy.deepcopy(resp.get("DATA")) if data.get("stackTrace"): - data.update( - {"stackTrace": "Stack trace is hidden, use '-vvvvv' to print it"} - ) + data.update({"stackTrace": "Stack trace is hidden, use '-vvvvv' to print it"}) res.update({"DATA": data}) # pylint: disable=protected-access diff --git a/plugins/modules/dcnm_vrf_v2.py b/plugins/modules/dcnm_vrf_v2.py new file mode 100644 index 000000000..b625e3408 --- /dev/null +++ b/plugins/modules/dcnm_vrf_v2.py @@ -0,0 +1,739 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# mypy: disable-error-code="import-untyped" +# +# Copyright (c) 2020-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. +# pylint: disable=wrong-import-position +from __future__ import absolute_import, division, print_function + +# pylint: disable=invalid-name +__metaclass__ = type +__author__ = "Shrishail Kariyappanavar, Karthik Babu Harichandra Babu, Praveen Ramoorthy, Allen Robel" +# pylint: enable=invalid-name +DOCUMENTATION = """ +--- +module: dcnm_vrf_v2 +short_description: Add and remove VRFs from a DCNM managed VXLAN fabric. +version_added: "0.9.0" +description: + - "Add and remove VRFs and VRF Lite Extension from a DCNM managed VXLAN fabric." + - "In Multisite fabrics, VRFs can be created only on Multisite fabric" + - "In Multisite fabrics, VRFs cannot be created on member fabric" +author: Shrishail Kariyappanavar(@nkshrishail), Karthik Babu Harichandra Babu (@kharicha), Praveen Ramoorthy(@praveenramoorthy) +options: + fabric: + description: + - Name of the target fabric for vrf operations + type: str + required: yes + state: + description: + - The state of DCNM after module completion. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - query + default: merged + config: + description: + - List of details of vrfs being managed. Not required for state deleted + type: list + elements: dict + suboptions: + vrf_name: + description: + - Name of the vrf being managed + type: str + required: true + vrf_id: + description: + - ID of the vrf being managed + type: int + required: false + vlan_id: + description: + - vlan ID for the vrf attachment + - If not specified in the playbook, DCNM will auto-select an available vlan_id + type: int + required: false + vrf_template: + description: + - Name of the config template to be used + type: str + default: 'Default_VRF_Universal' + vrf_extension_template: + description: + - Name of the extension config template to be used + type: str + default: 'Default_VRF_Extension_Universal' + service_vrf_template: + description: + - Service vrf template + type: str + default: None + vrf_vlan_name: + description: + - VRF Vlan Name + - if > 32 chars enable - system vlan long-name + type: str + required: false + vrf_intf_desc: + description: + - VRF Intf Description + type: str + required: false + vrf_description: + description: + - VRF Description + type: str + required: false + vrf_int_mtu: + description: + - VRF interface MTU + type: int + required: false + default: 9216 + loopback_route_tag: + description: + - Loopback Routing Tag + type: int + required: false + default: 12345 + redist_direct_rmap: + description: + - Redistribute Direct Route Map + type: str + required: false + default: 'FABRIC-RMAP-REDIST-SUBNET' + max_bgp_paths: + description: + - Max BGP Paths + type: int + required: false + default: 1 + max_ibgp_paths: + description: + - Max iBGP Paths + type: int + required: false + default: 2 + ipv6_linklocal_enable: + description: + - Enable IPv6 link-local Option + type: bool + required: false + default: true + trm_enable: + description: + - Enable Tenant Routed Multicast + type: bool + required: false + default: false + no_rp: + description: + - No RP, only SSM is used + - supported on NDFC only + type: bool + required: false + default: false + rp_external: + description: + - Specifies if RP is external to the fabric + - Can be configured only when TRM is enabled + type: bool + required: false + default: false + rp_address: + description: + - IPv4 Address of RP + - Can be configured only when TRM is enabled + type: str + required: false + rp_loopback_id: + description: + - loopback ID of RP + - Can be configured only when TRM is enabled + type: int + required: false + underlay_mcast_ip: + description: + - Underlay IPv4 Multicast Address + - Can be configured only when TRM is enabled + type: str + required: false + overlay_mcast_group: + description: + - Underlay IPv4 Multicast group (224.0.0.0/4 to 239.255.255.255/4) + - Can be configured only when TRM is enabled + type: str + required: false + trm_bgw_msite: + description: + - Enable TRM on Border Gateway Multisite + - Can be configured only when TRM is enabled + type: bool + required: false + default: false + adv_host_routes: + description: + - Flag to Control Advertisement of /32 and /128 Routes to Edge Routers + type: bool + required: false + default: false + adv_default_routes: + description: + - Flag to Control Advertisement of Default Route Internally + type: bool + required: false + default: true + static_default_route: + description: + - Flag to Control Static Default Route Configuration + type: bool + required: false + default: true + bgp_password: + description: + - VRF Lite BGP neighbor password + - Password should be in Hex string format + type: str + required: false + bgp_passwd_encrypt: + description: + - VRF Lite BGP Key Encryption Type + - Allowed values are 3 (3DES) and 7 (Cisco) + type: int + choices: + - 3 + - 7 + required: false + default: 3 + netflow_enable: + description: + - Enable netflow on VRF-LITE Sub-interface + - Netflow is supported only if it is enabled on fabric + - Netflow configs are supported on NDFC only + type: bool + required: false + default: false + nf_monitor: + description: + - Netflow Monitor + - Netflow configs are supported on NDFC only + type: str + required: false + disable_rt_auto: + description: + - Disable RT Auto-Generate + - supported on NDFC only + type: bool + required: false + default: false + import_vpn_rt: + description: + - VPN routes to import + - supported on NDFC only + - Use ',' to separate multiple route-targets + type: str + required: false + export_vpn_rt: + description: + - VPN routes to export + - supported on NDFC only + - Use ',' to separate multiple route-targets + type: str + required: false + import_evpn_rt: + description: + - EVPN routes to import + - supported on NDFC only + - Use ',' to separate multiple route-targets + type: str + required: false + export_evpn_rt: + description: + - EVPN routes to export + - supported on NDFC only + - Use ',' to separate multiple route-targets + type: str + required: false + import_mvpn_rt: + description: + - MVPN routes to import + - supported on NDFC only + - Can be configured only when TRM is enabled + - Use ',' to separate multiple route-targets + type: str + required: false + export_mvpn_rt: + description: + - MVPN routes to export + - supported on NDFC only + - Can be configured only when TRM is enabled + - Use ',' to separate multiple route-targets + type: str + required: false + attach: + description: + - List of vrf attachment details + type: list + elements: dict + suboptions: + ip_address: + description: + - IP address of the switch where vrf will be attached or detached + type: str + required: true + suboptions: + vrf_lite: + type: list + description: + - VRF Lite Extensions options + elements: dict + required: false + suboptions: + peer_vrf: + description: + - VRF Name to which this extension is attached + type: str + required: false + interface: + description: + - Interface of the switch which is connected to the edge router + type: str + required: true + ipv4_addr: + description: + - IP address of the interface which is connected to the edge router + type: str + required: false + neighbor_ipv4: + description: + - Neighbor IP address of the edge router + type: str + required: false + ipv6_addr: + description: + - IPv6 address of the interface which is connected to the edge router + type: str + required: false + neighbor_ipv6: + description: + - Neighbor IPv6 address of the edge router + type: str + required: false + dot1q: + description: + - DOT1Q Id + type: str + required: false + import_evpn_rt: + description: + - import evpn route-target + - supported on NDFC only + - Use ',' to separate multiple route-targets + type: str + required: false + export_evpn_rt: + description: + - export evpn route-target + - supported on NDFC only + - Use ',' to separate multiple route-targets + type: str + required: false + deploy: + description: + - Per switch knob to control whether to deploy the attachment + - This knob has been deprecated from Ansible NDFC Collection Version 2.1.0 onwards. + There will not be any functional impact if specified in playbook. + type: bool + default: true + deploy: + description: + - Global knob to control whether to deploy the attachment + - Ansible NDFC Collection Behavior for Version 2.0.1 and earlier + - This knob will create and deploy the attachment in DCNM only when set to "True" in playbook + - Ansible NDFC Collection Behavior for Version 2.1.0 and later + - Attachments specified in the playbook will always be created in DCNM. + This knob, when set to "True", will deploy the attachment in DCNM, by pushing the configs to switch. + If set to "False", the attachments will be created in DCNM, but will not be deployed + type: bool + default: true +""" + +EXAMPLES = """ +# This module supports the following states: +# +# Merged: +# VRFs defined in the playbook will be merged into the target fabric. +# - If the VRF does not exist it will be added. +# - If the VRF exists but properties managed by the playbook are different +# they will be updated if possible. +# - VRFs that are not specified in the playbook will be untouched. +# +# Replaced: +# VRFs defined in the playbook will be replaced in the target fabric. +# - If the VRF does not exist it will be added. +# - If the VRF 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. +# - VRFs that are not specified in the playbook will be untouched. +# +# Overridden: +# VRFs defined in the playbook will be overridden in the target fabric. +# - If the VRF does not exist it will be added. +# - If the VRF 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. +# - VRFs that are not specified in the playbook will be deleted. +# +# Deleted: +# VRFs defined in the playbook will be deleted. +# If no VRFs are provided in the playbook, all VRFs present on that DCNM fabric will be deleted. +# +# Query: +# Returns the current DCNM state for the VRFs listed in the playbook. +# +# rollback functionality: +# This module supports task level rollback functionality. If any task runs into failures, as part of failure +# handling, the module tries to bring the state of the DCNM back to the state captured in have structure at the +# beginning of the task execution. Following few lines provide a logical description of how this works, +# if (failure) +# want data = have data +# have data = get state of DCNM +# Run the module in override state with above set of data to produce the required set of diffs +# and push the diff payloads to DCNM. +# If rollback fails, the module does not attempt to rollback again, it just quits with appropriate error messages. + +# The two VRFs below will be merged into the target fabric. +- name: Merge vrfs + cisco.dcnm.dcnm_vrf: + fabric: vxlan-fabric + state: merged + config: + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + service_vrf_template: null + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + - vrf_name: ansible-vrf-r2 + vrf_id: 9008012 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + service_vrf_template: null + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + +# VRF LITE Extension attached +- name: Merge vrfs + cisco.dcnm.dcnm_vrf: + fabric: vxlan-fabric + state: merged + config: + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + service_vrf_template: null + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + vrf_lite: + - peer_vrf: test_vrf_1 # optional + interface: Ethernet1/16 # mandatory + ipv4_addr: 10.33.0.2/30 # optional + neighbor_ipv4: 10.33.0.1 # optional + ipv6_addr: 2010::10:34:0:7/64 # optional + neighbor_ipv6: 2010::10:34:0:3 # optional + dot1q: 2 # dot1q can be got from dcnm/optional + - peer_vrf: test_vrf_2 # optional + interface: Ethernet1/17 # mandatory + ipv4_addr: 20.33.0.2/30 # optional + neighbor_ipv4: 20.33.0.1 # optional + ipv6_addr: 3010::10:34:0:7/64 # optional + neighbor_ipv6: 3010::10:34:0:3 # optional + dot1q: 3 # dot1q can be got from dcnm/optional + +# The two VRFs below will be replaced in the target fabric. +- name: Replace vrfs + cisco.dcnm.dcnm_vrf: + fabric: vxlan-fabric + state: replaced + config: + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + service_vrf_template: null + attach: + - ip_address: 192.168.1.224 + # Delete this attachment + # - ip_address: 192.168.1.225 + # Create the following attachment + - ip_address: 192.168.1.226 + # Dont touch this if its present on DCNM + # - vrf_name: ansible-vrf-r2 + # vrf_id: 9008012 + # vrf_template: Default_VRF_Universal + # vrf_extension_template: Default_VRF_Extension_Universal + # attach: + # - ip_address: 192.168.1.224 + # - ip_address: 192.168.1.225 + +# The two VRFs below will be overridden in the target fabric. +- name: Override vrfs + cisco.dcnm.dcnm_vrf: + fabric: vxlan-fabric + state: overridden + config: + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + service_vrf_template: null + attach: + - ip_address: 192.168.1.224 + # Delete this attachment + # - ip_address: 192.168.1.225 + # Create the following attachment + - ip_address: 192.168.1.226 + # Delete this vrf + # - vrf_name: ansible-vrf-r2 + # vrf_id: 9008012 + # vrf_template: Default_VRF_Universal + # vrf_extension_template: Default_VRF_Extension_Universal + # vlan_id: 2000 + # service_vrf_template: null + # attach: + # - ip_address: 192.168.1.224 + # - ip_address: 192.168.1.225 + +- name: Delete selected vrfs + cisco.dcnm.dcnm_vrf: + fabric: vxlan-fabric + state: deleted + config: + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + service_vrf_template: null + - vrf_name: ansible-vrf-r2 + vrf_id: 9008012 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + service_vrf_template: null + +- name: Delete all the vrfs + cisco.dcnm.dcnm_vrf: + fabric: vxlan-fabric + state: deleted + +- name: Query vrfs + cisco.dcnm.dcnm_vrf: + fabric: vxlan-fabric + state: query + config: + - vrf_name: ansible-vrf-r1 + - vrf_name: ansible-vrf-r2 +""" +import traceback +from typing import Union + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +HAS_FIRST_PARTY_IMPORTS: set[bool] = set() +HAS_THIRD_PARTY_IMPORTS: set[bool] = set() + +FIRST_PARTY_IMPORT_ERROR: Union[str, None] +THIRD_PARTY_IMPORT_ERROR: Union[str, None] + +FIRST_PARTY_FAILED_IMPORT: set[str] = set() +THIRD_PARTY_FAILED_IMPORT: set[str] = set() + +try: + import pydantic # pylint: disable=unused-import + + HAS_THIRD_PARTY_IMPORTS.add(True) + THIRD_PARTY_IMPORT_ERROR = None +except ImportError as import_error: + HAS_THIRD_PARTY_IMPORTS.add(False) + THIRD_PARTY_FAILED_IMPORT.add("pydantic") + THIRD_PARTY_IMPORT_ERROR = traceback.format_exc() + +from ..module_utils.common.log_v2 import Log +from ..module_utils.common.enums.ansible import AnsibleStates +from ..module_utils.network.dcnm.dcnm import dcnm_version_supported + +DcnmVrf11 = None # pylint: disable=invalid-name +NdfcVrf12 = None # pylint: disable=invalid-name + +try: + from ..module_utils.vrf.dcnm_vrf_v11 import DcnmVrf11 + + HAS_FIRST_PARTY_IMPORTS.add(True) +except ImportError as import_error: + HAS_FIRST_PARTY_IMPORTS.add(False) + FIRST_PARTY_FAILED_IMPORT.add("DcnmVrf11") + FIRST_PARTY_IMPORT_ERROR = traceback.format_exc() + +try: + from ..module_utils.vrf.dcnm_vrf_v12 import NdfcVrf12 + + HAS_FIRST_PARTY_IMPORTS.add(True) +except ImportError as import_error: + HAS_FIRST_PARTY_IMPORTS.add(False) + FIRST_PARTY_FAILED_IMPORT.add("NdfcVrf12") + FIRST_PARTY_IMPORT_ERROR = traceback.format_exc() + + +class DcnmVrf: # pylint: disable=too-few-public-methods + """ + Stub class used only to return the controller version. + + We needed this to satisfy the unittest patch that is done in the dcnm_vrf unit tests. + + TODO: This can be removed when we move to pytest-based unit tests. + """ + + def __init__(self, module: AnsibleModule): + self.module = module + self.version: int = dcnm_version_supported(self.module) + + @property + def controller_version(self) -> int: + """ + # Summary + + Return the controller major version as am integer. + """ + return self.version + + +def main() -> None: + """main entry point for module execution""" + + # Logging setup + try: + log: Log = Log() + log.commit() + except (TypeError, ValueError): + pass + + argument_spec: dict = {} + argument_spec["config"] = {} + argument_spec["config"]["elements"] = "dict" + argument_spec["config"]["required"] = False + argument_spec["config"]["type"] = "list" + argument_spec["fabric"] = {} + argument_spec["fabric"]["required"] = True + argument_spec["fabric"]["type"] = "str" + argument_spec["state"] = {} + argument_spec["state"]["choices"] = [x.value for x in AnsibleStates] + argument_spec["state"]["default"] = AnsibleStates.MERGED.value + + module: AnsibleModule = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if False in HAS_THIRD_PARTY_IMPORTS: + module.fail_json(msg=missing_required_lib(f"3rd party: {','.join(THIRD_PARTY_FAILED_IMPORT)}"), exception=THIRD_PARTY_IMPORT_ERROR) + + if False in HAS_FIRST_PARTY_IMPORTS: + module.fail_json(msg=missing_required_lib(f"1st party: {','.join(FIRST_PARTY_FAILED_IMPORT)}"), exception=FIRST_PARTY_IMPORT_ERROR) + + dcnm_vrf_launch: DcnmVrf = DcnmVrf(module) + + if DcnmVrf11 is None: + module.fail_json(msg="Unable to import DcnmVrf11") + if NdfcVrf12 is None: + module.fail_json(msg="Unable to import DcnmVrf12") + + if dcnm_vrf_launch.controller_version == 12: + dcnm_vrf = NdfcVrf12(module) + else: + dcnm_vrf = DcnmVrf11(module) + if not dcnm_vrf.ip_sn: + msg = f"Fabric {dcnm_vrf.fabric} missing on the controller or " + msg += "does not have any switches" + module.fail_json(msg=msg) + + dcnm_vrf.validate_input() + + dcnm_vrf.get_want() + dcnm_vrf.get_have() + + if module.params["state"] == "merged": + dcnm_vrf.get_diff_merge() + + if module.params["state"] == "replaced": + dcnm_vrf.get_diff_replace() + + if module.params["state"] == "overridden": + dcnm_vrf.get_diff_override() + + if module.params["state"] == "deleted": + dcnm_vrf.get_diff_delete() + + if module.params["state"] == "query": + dcnm_vrf.get_diff_query() + dcnm_vrf.result["response"] = dcnm_vrf.query + + dcnm_vrf.format_diff() + dcnm_vrf.result["diff"] = dcnm_vrf.diff_input_format + + module_result: set[bool] = set() + module_result.add(len(dcnm_vrf.diff_create) != 0) + module_result.add(len(dcnm_vrf.diff_attach) != 0) + module_result.add(len(dcnm_vrf.diff_detach) != 0) + module_result.add(len(dcnm_vrf.diff_deploy) != 0) + module_result.add(len(dcnm_vrf.diff_undeploy) != 0) + module_result.add(len(dcnm_vrf.diff_delete) != 0) + module_result.add(len(dcnm_vrf.diff_create_quick) != 0) + module_result.add(len(dcnm_vrf.diff_create_update) != 0) + + if True in module_result: + dcnm_vrf.result["changed"] = True + else: + module.exit_json(**dcnm_vrf.result) + + if module.check_mode: + dcnm_vrf.result["changed"] = False + msg = f"dcnm_vrf.result: {dcnm_vrf.result}" + dcnm_vrf.log.debug(msg) + module.exit_json(**dcnm_vrf.result) + + dcnm_vrf.push_to_remote() + + msg = f"dcnm_vrf.result: {dcnm_vrf.result}" + dcnm_vrf.log.debug(msg) + module.exit_json(**dcnm_vrf.result) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index 8bf65d124..9df364347 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ ansible requests +pydantic==2.9.0 +pydantic_core==2.23.2 diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/deleted.mermaid b/tests/integration/targets/dcnm_vrf/tests/dcnm/deleted.mermaid new file mode 100644 index 000000000..f96710a52 --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/deleted.mermaid @@ -0,0 +1,56 @@ +block-beta + + block:title:1 + columns 1 + deleted_state_topology + end + space space space space space + columns 3 + + block:switch_3_block:3 + switch_3 + end + block:switch3_int:3 + columns 2 + interface_3a interface_3b + end + + block:switch1_int:1 + columns 1 + interface_1a + end + block:blank:1 + blank + end + block:switch2_inta:1 + columns 1 + interface_2a + end + +columns 3 + + block:switch_1_block:1 + columns 1 + switch_1 + end + + block:blank2:1 + columns 1 + blank + end + + block:switch_2_block:1 + columns 1 + switch_2 + end + + switch_1:1 blank:1 switch_2:1 + +interface_3a --- interface_1a +interface_3b --- interface_2a + +style switch_1 fill:#079,stroke:#110,stroke-width:1px + +style switch_2 fill:#079,stroke:#110,stroke-width:1px + +style switch_3 fill:#968,stroke:#110,stroke-width:1px diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/deleted.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/deleted.yaml index c793b0a1d..d406ed9a2 100644 --- a/tests/integration/targets/dcnm_vrf/tests/dcnm/deleted.yaml +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/deleted.yaml @@ -42,7 +42,15 @@ - "switch_2 : {{ switch_2 }}" - "interface_2a : {{ interface_2a }}" -- name: SETUP.1 - DELETED - [dcnm_rest.GET] Verify fabric is deployed. +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.1 - DELETED - [dcnm_rest.GET] Verify fabric is deployed." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_rest: method: GET path: "{{ rest_path }}" @@ -50,9 +58,17 @@ - assert: that: - - 'result_setup_1.response.DATA != None' + - result_setup_1.response.DATA != None + +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.2 - DELETED - [deleted] Delete all VRFs" -- name: SETUP.2 - DELETED - [deleted] Delete all VRFs +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted @@ -63,7 +79,15 @@ timeout: 40 when: result_setup_2.changed == true -- name: SETUP.3 - DELETED - [merged] Create, Attach, Deploy VLAN+VRF ansible-vrf +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.3 - DELETED - [merged] Create, Attach, Deploy VLAN+VRF ansible-vrf" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -95,23 +119,31 @@ - assert: that: - - 'result_setup_3.changed == true' - - 'result_setup_3.response[0].RETURN_CODE == 200' - - 'result_setup_3.response[1].RETURN_CODE == 200' - - 'result_setup_3.response[2].RETURN_CODE == 200' - - '(result_setup_3.response[1].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_setup_3.response[1].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_setup_3.diff[0].attach[0].deploy == true' - - 'result_setup_3.diff[0].attach[1].deploy == true' - - '"{{ switch_1 }}" in result_setup_3.diff[0].attach[0].ip_address' - - '"{{ switch_2 }}" in result_setup_3.diff[0].attach[1].ip_address' - - 'result_setup_3.diff[0].vrf_name == "ansible-vrf-int1"' + - result_setup_3.changed == true + - result_setup_3.diff[0].attach[0].deploy == true + - result_setup_3.diff[0].attach[1].deploy == true + - result_setup_3.diff[0].vrf_name == "ansible-vrf-int1" + - result_setup_3.response[0].RETURN_CODE == 200 + - result_setup_3.response[1].RETURN_CODE == 200 + - result_setup_3.response[2].RETURN_CODE == 200 + - (result_setup_3.response[1].DATA|dict2items)[0].value == "SUCCESS" + - (result_setup_3.response[1].DATA|dict2items)[1].value == "SUCCESS" + - switch_1 in result_setup_3.diff[0].attach[0].ip_address + - switch_2 in result_setup_3.diff[0].attach[1].ip_address ############################################### ### DELETED ## ############################################### -- name: TEST.1 - DELETED - [deleted] Delete VRF ansible-vrf-int1 +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1 - DELETED - [deleted] Delete VRF ansible-vrf-int1" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf1 fabric: "{{ fabric_1 }}" state: deleted @@ -132,19 +164,27 @@ - assert: that: - - 'result_1.changed == true' - - 'result_1.response[0].RETURN_CODE == 200' - - 'result_1.response[1].RETURN_CODE == 200' - - 'result_1.response[1].MESSAGE == "OK"' - - 'result_1.response[2].RETURN_CODE == 200' - - 'result_1.response[2].METHOD == "DELETE"' - - '(result_1.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_1.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_1.diff[0].attach[0].deploy == false' - - 'result_1.diff[0].attach[1].deploy == false' - - 'result_1.diff[0].vrf_name == "ansible-vrf-int1"' - -- name: TEST.1c - DELETED - [deleted] conf1 - Idempotence + - result_1.changed == true + - result_1.diff[0].attach[0].deploy == false + - result_1.diff[0].attach[1].deploy == false + - result_1.diff[0].vrf_name == "ansible-vrf-int1" + - result_1.response[1].MESSAGE == "OK" + - result_1.response[2].METHOD == "DELETE" + - result_1.response[0].RETURN_CODE == 200 + - result_1.response[1].RETURN_CODE == 200 + - result_1.response[2].RETURN_CODE == 200 + - (result_1.response[0].DATA|dict2items)[0].value == "SUCCESS" + - (result_1.response[0].DATA|dict2items)[1].value == "SUCCESS" + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1c - DELETED - [deleted] conf1 - Idempotence" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf1 register: result_1c @@ -154,11 +194,19 @@ - assert: that: - - 'result_1c.changed == false' - - 'result_1c.response|length == 0' - - 'result_1c.diff|length == 0' + - result_1c.changed == false + - result_1c.response|length == 0 + - result_1c.diff|length == 0 + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2 - DELETED - [merged] Create, Attach, Deploy VLAN+VRF+LITE ansible-vrf-int1 switch_2" -- name: TEST.2 - DELETED - [merged] Create, Attach, Deploy VLAN+VRF+LITE ansible-vrf-int1 switch_2 +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -182,7 +230,15 @@ deploy: true register: result_2 -- name: TEST.2a - DELETED - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2a - DELETED - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -198,19 +254,27 @@ - assert: that: - - 'result_2.changed == true' - - 'result_2.response[0].RETURN_CODE == 200' - - 'result_2.response[1].RETURN_CODE == 200' - - 'result_2.response[2].RETURN_CODE == 200' - - '(result_2.response[1].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_2.response[1].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_2.diff[0].attach[0].deploy == true' - - 'result_2.diff[0].attach[1].deploy == true' - - '"{{ switch_1 }}" in result_2.diff[0].attach[0].ip_address' - - '"{{ switch_2 }}" in result_2.diff[0].attach[1].ip_address' - - 'result_2.diff[0].vrf_name == "ansible-vrf-int1"' - -- name: TEST.2b - DELETED - [deleted] Delete VRF+LITE ansible-vrf-int1 switch_2 + - result_2.changed == true + - result_2.diff[0].attach[0].deploy == true + - result_2.diff[0].attach[1].deploy == true + - result_2.diff[0].vrf_name == "ansible-vrf-int1" + - result_2.response[0].RETURN_CODE == 200 + - result_2.response[1].RETURN_CODE == 200 + - result_2.response[2].RETURN_CODE == 200 + - (result_2.response[1].DATA|dict2items)[0].value == "SUCCESS" + - (result_2.response[1].DATA|dict2items)[1].value == "SUCCESS" + - switch_1 in result_2.diff[0].attach[0].ip_address + - switch_2 in result_2.diff[0].attach[1].ip_address + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2b - DELETED - [deleted] Delete VRF+LITE ansible-vrf-int1 switch_2" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf2 fabric: "{{ fabric_1 }}" state: deleted @@ -240,17 +304,17 @@ - assert: that: - - 'result_2b.changed == true' - - 'result_2b.response[0].RETURN_CODE == 200' - - 'result_2b.response[1].RETURN_CODE == 200' - - 'result_2b.response[1].MESSAGE == "OK"' - - 'result_2b.response[2].RETURN_CODE == 200' - - 'result_2b.response[2].METHOD == "DELETE"' - - '(result_2b.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_2b.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_2b.diff[0].attach[0].deploy == false' - - 'result_2b.diff[0].attach[1].deploy == false' - - 'result_2b.diff[0].vrf_name == "ansible-vrf-int1"' + - result_2b.changed == true + - result_2b.diff[0].attach[0].deploy == false + - result_2b.diff[0].attach[1].deploy == false + - result_2b.diff[0].vrf_name == "ansible-vrf-int1" + - result_2b.response[1].MESSAGE == "OK" + - result_2b.response[2].METHOD == "DELETE" + - result_2b.response[0].RETURN_CODE == 200 + - result_2b.response[1].RETURN_CODE == 200 + - result_2b.response[2].RETURN_CODE == 200 + - (result_2b.response[0].DATA|dict2items)[0].value == "SUCCESS" + - (result_2b.response[0].DATA|dict2items)[1].value == "SUCCESS" - name: TEST.2d - DELETED - [wait_for] Wait 60 seconds for controller and switch to sync # The vrf lite profile removal returns ok for deployment, but the switch @@ -259,6 +323,14 @@ wait_for: timeout: 60 +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2e - DELETED - [deleted] conf2 - Idempotence" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + - name: TEST.2e - DELETED - [deleted] conf2 - Idempotence cisco.dcnm.dcnm_vrf: *conf2 register: result_2e @@ -269,11 +341,19 @@ - assert: that: - - 'result_2e.changed == false' - - 'result_2e.response|length == 0' - - 'result_2e.diff|length == 0' + - result_2e.changed == false + - result_2e.response|length == 0 + - result_2e.diff|length == 0 -- name: TEST.3 - DELETED - [merged] Create, Attach, Deploy VRF+LITE switch_2 +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3 - DELETED - [merged] Create, Attach, Deploy VRF+LITE switch_2" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -297,7 +377,15 @@ deploy: true register: result_3 -- name: TEST.3a - DELETED - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3a - DELETED - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -313,19 +401,27 @@ - assert: that: - - 'result_3.changed == true' - - 'result_3.response[0].RETURN_CODE == 200' - - 'result_3.response[1].RETURN_CODE == 200' - - 'result_3.response[2].RETURN_CODE == 200' - - '(result_3.response[1].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_3.response[1].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_3.diff[0].attach[0].deploy == true' - - 'result_3.diff[0].attach[1].deploy == true' - - '"{{ switch_1 }}" in result_3.diff[0].attach[0].ip_address' - - '"{{ switch_2 }}" in result_3.diff[0].attach[1].ip_address' - - 'result_3.diff[0].vrf_name == "ansible-vrf-int1"' - -- name: TEST.3c - DELETED - [deleted] Delete VRF+LITE - empty config element + - result_3.changed == true + - result_3.diff[0].attach[0].deploy == true + - result_3.diff[0].attach[1].deploy == true + - result_3.diff[0].vrf_name == "ansible-vrf-int1" + - result_3.response[0].RETURN_CODE == 200 + - result_3.response[1].RETURN_CODE == 200 + - result_3.response[2].RETURN_CODE == 200 + - (result_3.response[1].DATA|dict2items)[0].value == "SUCCESS" + - (result_3.response[1].DATA|dict2items)[1].value == "SUCCESS" + - switch_1 in result_3.diff[0].attach[0].ip_address + - switch_2 in result_3.diff[0].attach[1].ip_address + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3c - DELETED - [deleted] Delete VRF+LITE - empty config element" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf3 fabric: "{{ fabric_1 }}" state: deleted @@ -338,17 +434,17 @@ - assert: that: - - 'result_3c.changed == true' - - 'result_3c.response[0].RETURN_CODE == 200' - - 'result_3c.response[1].RETURN_CODE == 200' - - 'result_3c.response[1].MESSAGE == "OK"' - - 'result_3c.response[2].RETURN_CODE == 200' - - 'result_3c.response[2].METHOD == "DELETE"' - - '(result_3c.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_3c.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_3c.diff[0].attach[0].deploy == false' - - 'result_3c.diff[0].attach[1].deploy == false' - - 'result_3c.diff[0].vrf_name == "ansible-vrf-int1"' + - result_3c.changed == true + - result_3c.diff[0].attach[0].deploy == false + - result_3c.diff[0].attach[1].deploy == false + - result_3c.diff[0].vrf_name == "ansible-vrf-int1" + - result_3c.response[1].MESSAGE == "OK" + - result_3c.response[2].METHOD == "DELETE" + - result_3c.response[0].RETURN_CODE == 200 + - result_3c.response[1].RETURN_CODE == 200 + - result_3c.response[2].RETURN_CODE == 200 + - (result_3c.response[0].DATA|dict2items)[0].value == "SUCCESS" + - (result_3c.response[0].DATA|dict2items)[1].value == "SUCCESS" - name: TEST.3d - DELETED - [wait_for] Wait 60 seconds for controller and switch to sync # The vrf lite profile removal returns ok for deployment, but the switch @@ -357,7 +453,15 @@ wait_for: timeout: 60 -- name: TEST.3e - DELETED - conf3 - Idempotence +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3e - DELETED - conf3 - Idempotence" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf3 register: result_3e @@ -367,15 +471,23 @@ - assert: that: - - 'result_3e.changed == false' - - 'result_3e.response|length == 0' - - 'result_3e.diff|length == 0' + - result_3e.changed == false + - result_3e.diff|length == 0 + - result_3e.response|length == 0 ################################################ -#### CLEAN-UP ## +## CLEAN-UP ## ################################################ -- name: CLEANUP.1 - DELETED - [deleted] Delete all VRFs +- name: Set task name + ansible.builtin.set_fact: + task_name: "CLEANUP.1 - DELETED - [deleted] Delete all VRFs" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.md b/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.md new file mode 100644 index 000000000..8aaec1d07 --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.md @@ -0,0 +1,52 @@ +# Topology - merged state + +The topology (fabrics and switches) is not created by the test and must be +created through other means (NDFC GUI, separate Ansible scripts, etc) + +[Topology Diagram](merged.mermaid) + +## ISN + +- Fabric type is `Multi-Site External Network` +- The fabric is not referenced in the test, but needs to exist + +### switch_4 + +- switch_4 role (NDFC GUI) is `Edge Router` +- switch_4 is not referenced in the test, but needs to exist + +## fabric_1 + +- Fabric type (NDFC GUI) is `Data Center VXLAN EVPN` +- Fabric type (NDFC Template) is `Easy_Fabric` +- Fabric type (dcnm_fabric Playbook) is `VXLAN_EVPN` + +### switch_1 + +- switch_1 role (NDFC GUI) is `Border Spine` +- switch_1 does not require an interface + +### switch_2 + +- switch_2 role (NDFC GUI) is `Border Spine` +- interface_2a is connected to switch_4 and must be up + +### switch_3 + +- switch_3 role can be any non-Border role e.g. `Leaf` +- interface_3a on switch_3 does not have to be connected/up. + +```mermaid +architecture-beta + group isn(cloud)[ISN] + group switch_4g[switch_4 edge_router] in isn + service interface_4a(internet)[interface_4a] in switch_4g + + group fabric_1(cloud)[fabric_1] + group switch_1g[switch_1 border_spine] in fabric_1 + group switch_2g[switch_2 border_spine] in fabric_1 + service interface_2a(server)[interface_2a] in switch_2g + group switch_3g[switch_3 non_border] in fabric_1 + + interface_4a:T -- B:interface_2a +``` diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.mermaid b/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.mermaid new file mode 100644 index 000000000..a8183a9d7 --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.mermaid @@ -0,0 +1,12 @@ +architecture-beta + group isn(cloud)[ISN] + group switch_4g[switch_4 edge_router] in isn + service interface_4a(internet)[interface_4a] in switch_4g + + group fabric_1(cloud)[fabric_1] + group switch_1g[switch_1 border_spine] in fabric_1 + group switch_2g[switch_2 border_spine] in fabric_1 + service interface_2a(server)[interface_2a] in switch_2g + group switch_3g[switch_3 non_border] in fabric_1 + + interface_4a:T -- B:interface_2a diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.yaml index b122e8fc7..d897561f8 100644 --- a/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.yaml +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.yaml @@ -80,8 +80,19 @@ ############################################### ### MERGED ## ############################################### +- name: Set fact + ansible.builtin.set_fact: + DEPLOYMENT_OF_VRFS: "Deployment of VRF(s) has been initiated successfully" -- name: TEST.1 - MERGED - [merged] Create, Attach, Deploy VLAN(600)+VRF ansible-vrf-int1 +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1 - MERGED - [merged] Create, Attach, Deploy VLAN(600)+VRF ansible-vrf-int1" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf1 fabric: "{{ fabric_1 }}" state: merged @@ -97,7 +108,15 @@ deploy: true register: result_1 -- name: TEST.1a - MERGED - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1a - MERGED - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -114,18 +133,30 @@ - assert: that: - result_1.changed == true + - result_1.diff[0].attach[0].deploy == true + - result_1.diff[0].attach[1].deploy == true + - result_1.diff[0].vrf_name == "ansible-vrf-int1" + - result_1.response[2].DATA.status == DEPLOYMENT_OF_VRFS + - result_1.response[0].METHOD == "POST" + - result_1.response[1].METHOD == "POST" + - result_1.response[2].METHOD == "POST" - result_1.response[0].RETURN_CODE == 200 - result_1.response[1].RETURN_CODE == 200 - result_1.response[2].RETURN_CODE == 200 - (result_1.response[1].DATA|dict2items)[0].value == "SUCCESS" - (result_1.response[1].DATA|dict2items)[1].value == "SUCCESS" - - result_1.diff[0].attach[0].deploy == true - - result_1.diff[0].attach[1].deploy == true - switch_1 in result_1.diff[0].attach[0].ip_address - switch_2 in result_1.diff[0].attach[1].ip_address - - result_1.diff[0].vrf_name == "ansible-vrf-int1" -- name: TEST.1c - MERGED - [merged] conf1 - Idempotence +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1c - MERGED - [merged] conf1 - Idempotence" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf1 register: result_1c @@ -138,7 +169,15 @@ - result_1c.changed == false - result_1c.response|length == 0 -- name: TEST.1e - MERGED - [deleted] Delete all VRFs +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1e - MERGED - [deleted] Delete all VRFs" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted @@ -152,7 +191,15 @@ timeout: 60 when: result_1e.changed == true -- name: TEST.2 - MERGED - [merged] Create, Attach, Deploy VLAN+VRF (controller provided VLAN) +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2 - MERGED - [merged] Create, Attach, Deploy VLAN+VRF (controller provided VLAN)" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf2 fabric: "{{ fabric_1 }}" state: merged @@ -167,7 +214,15 @@ deploy: true register: result_2 -- name: TEST.2a - MERGED - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2a - MERGED - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -184,18 +239,27 @@ - assert: that: - result_2.changed == true + - result_2.diff[0].attach[0].deploy == true + - result_2.diff[0].attach[1].deploy == true + - result_2.diff[0].vrf_name == "ansible-vrf-int1" - result_2.response[0].RETURN_CODE == 200 - result_2.response[1].RETURN_CODE == 200 - result_2.response[2].RETURN_CODE == 200 + - result_2.response[2].DATA.status == DEPLOYMENT_OF_VRFS - (result_2.response[1].DATA|dict2items)[0].value == "SUCCESS" - (result_2.response[1].DATA|dict2items)[1].value == "SUCCESS" - - result_2.diff[0].attach[0].deploy == true - - result_2.diff[0].attach[1].deploy == true - switch_1 in result_2.diff[0].attach[0].ip_address - switch_2 in result_2.diff[0].attach[1].ip_address - - result_2.diff[0].vrf_name == "ansible-vrf-int1" -- name: TEST.2c - MERGED - [merged] conf2 - Idempotence +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2c - MERGED - [merged] conf2 - Idempotence" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf2 register: result_2c @@ -208,7 +272,15 @@ - result_2c.changed == false - result_2c.response|length == 0 -- name: TEST.2e - MERGED - [deleted] Delete all VRFs +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2e - MERGED - [deleted] Delete all VRFs" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted @@ -219,7 +291,15 @@ wait_for: timeout: 60 -- name: TEST.3 - MERGED - [merged] Create, Attach, Deploy VLAN+VRF+LITE EXTENSION ansible-vrf-int1 switch_2 (user provided VLAN) +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3 - MERGED - [merged] Create, Attach, Deploy VLAN+VRF+LITE EXTENSION ansible-vrf-int1 switch_2 (user provided VLAN)" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf3 fabric: "{{ fabric_1 }}" state: merged @@ -243,7 +323,15 @@ deploy: true register: result_3 -- name: TEST.3a - MERGED - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3a - MERGED - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -260,18 +348,30 @@ - assert: that: - result_3.changed == true + - result_3.diff[0].attach[0].deploy == true + - result_3.diff[0].attach[1].deploy == true + - result_3.diff[0].vrf_name == "ansible-vrf-int1" + - result_3.response[0].METHOD == "POST" + - result_3.response[1].METHOD == "POST" + - result_3.response[2].METHOD == "POST" - result_3.response[0].RETURN_CODE == 200 - result_3.response[1].RETURN_CODE == 200 - result_3.response[2].RETURN_CODE == 200 + - result_3.response[2].DATA.status == DEPLOYMENT_OF_VRFS - (result_3.response[1].DATA|dict2items)[0].value == "SUCCESS" - (result_3.response[1].DATA|dict2items)[1].value == "SUCCESS" - - result_3.diff[0].attach[0].deploy == true - - result_3.diff[0].attach[1].deploy == true - switch_1 in result_3.diff[0].attach[0].ip_address - switch_2 in result_3.diff[0].attach[1].ip_address - - result_3.diff[0].vrf_name == "ansible-vrf-int1" -- name: TEST.3c - MERGED - [merged] conf3 - Idempotence +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3c - MERGED - [merged] conf3 - Idempotence" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf3 register: result_3c @@ -284,7 +384,15 @@ - result_3c.changed == false - result_3c.response|length == 0 -- name: TEST.3e - MERGED - [deleted] Delete all VRFs +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3e - MERGED - [deleted] Delete all VRFs" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted @@ -296,7 +404,15 @@ wait_for: timeout: 60 -- name: TEST.4 - MERGED - [merged] Create, Attach, Deploy VLAN+VRF+LITE EXTENSION - (controller provided VLAN) +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.4 - MERGED - [merged] Create, Attach, Deploy VLAN+VRF+LITE EXTENSION - (controller provided VLAN)" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -314,7 +430,15 @@ deploy: true register: result_4 -- name: TEST.4a - MERGED - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.4a - MERGED - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -331,18 +455,30 @@ - assert: that: - result_4.changed == true + - result_4.diff[0].attach[0].deploy == true + - result_4.diff[0].attach[1].deploy == true + - result_4.diff[0].vrf_name == "ansible-vrf-int1" + - result_4.response[0].METHOD == "POST" + - result_4.response[1].METHOD == "POST" + - result_4.response[2].METHOD == "POST" - result_4.response[0].RETURN_CODE == 200 - result_4.response[1].RETURN_CODE == 200 - result_4.response[2].RETURN_CODE == 200 + - result_4.response[2].DATA.status == DEPLOYMENT_OF_VRFS - (result_4.response[1].DATA|dict2items)[0].value == "SUCCESS" - (result_4.response[1].DATA|dict2items)[1].value == "SUCCESS" - - result_4.diff[0].attach[0].deploy == true - - result_4.diff[0].attach[1].deploy == true - switch_1 in result_4.diff[0].attach[0].ip_address - switch_2 in result_4.diff[0].attach[1].ip_address - - result_4.diff[0].vrf_name == "ansible-vrf-int1" -- name: TEST.5 - MERGED - [merged] Create, Attach, Deploy VRF - Update with incorrect VRF ID. +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.5 - MERGED - [merged] Create, Attach, Deploy VRF - Update with incorrect VRF ID." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -371,7 +507,15 @@ - result_5.changed == false - TEST_PHRASE in result_5.msg -- name: TEST.6 - MERGED - [merged] Create, Attach, Deploy VRF - Update with Out of Range VRF ID. +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.6 - MERGED - [merged] Create, Attach, Deploy VRF - Update with Out of Range vrf_id." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -393,14 +537,24 @@ - name: set fact set_fact: - TEST_PHRASE: "The item exceeds the allowed range of max" + TEST_PARAM: "vrf_id" + TEST_PHRASE: "Input should be less than or equal to 16777214" - assert: that: - result_6.changed == false - - TEST_PHRASE in result_6.msg + - TEST_PARAM in result_6.module_stderr + - TEST_PHRASE in result_6.module_stderr -- name: TEST.7 - MERGED - [merged] Create, Attach, Deploy VRF - VRF LITE missing required parameter +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.7 - MERGED - [merged] Create, Attach, Deploy VRF - VRF LITE missing required parameter" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -425,14 +579,24 @@ - name: set fact set_fact: - TEST_PHRASE: "Invalid parameters in playbook: interface : Required parameter not found" + TEST_PARAM: "attach.1.vrf_lite.0.interface" + TEST_PHRASE: "Field required" - assert: that: - result_7.changed == false - - TEST_PHRASE in result_7.msg + - TEST_PARAM in result_7.module_stderr + - TEST_PHRASE in result_7.module_stderr + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.8 - MERGED - [merged] Create, Attach and Deploy VRF - configure VRF LITE on non border switch" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" -- name: TEST.8 - MERGED - [merged] Create, Attach and Deploy VRF - configure VRF LITE on non border switch +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -473,7 +637,16 @@ ### CLEAN-UP ## ############################################### -- name: CLEANUP.1 - MERGED - [deleted] Delete all VRFs +- name: Set task name + ansible.builtin.set_fact: + task_name: "CLEANUP.1 - MERGED - [deleted] Delete all VRFs" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.md b/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.md new file mode 100644 index 000000000..88712574a --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.md @@ -0,0 +1,46 @@ +# Topology - overridden state + +The topology (fabrics and switches) is not created by the test and must be +created through other means (NDFC GUI, separate Ansible scripts, etc) + +[Topology Diagram](overridden.mermaid) + +## ISN + +- Fabric type is `Multi-Site External Network` +- The fabric is not referenced in the test, but needs to exist + +### switch_4 + +- switch_4 role (NDFC GUI) is `Edge Router` +- switch_4 is not referenced in the test, but needs to exist + +## fabric_1 + +- Fabric type (NDFC GUI) is `Data Center VXLAN EVPN` +- Fabric type (NDFC Template) is `Easy_Fabric` +- Fabric type (dcnm_fabric Playbook) is `VXLAN_EVPN` + +### switch_1 + +- switch_1 role (NDFC GUI) is `Border Spine` +- switch_1 does not require an interface + +### switch_2 + +- switch_2 role (NDFC GUI) is `Border Spine` +- interface_2a is connected to switch_4 and must be up + +```mermaid +architecture-beta + group isn(cloud)[ISN] + group switch_4g[switch_4 edge_router] in isn + service interface_4a(internet)[interface_4a] in switch_4g + + group fabric_1(cloud)[fabric_1] + group switch_1g[switch_1 border_spine] in fabric_1 + group switch_2g[switch_2 border_spine] in fabric_1 + service interface_2a(server)[interface_2a] in switch_2g + + interface_4a:T -- B:interface_2a +``` diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.mermaid b/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.mermaid new file mode 100644 index 000000000..906fdb7af --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.mermaid @@ -0,0 +1,11 @@ +architecture-beta + group isn(cloud)[ISN] + group switch_4g[switch_4 edge_router] in isn + service interface_4a(internet)[interface_4a] in switch_4g + + group fabric_1(cloud)[fabric_1] + group switch_1g[switch_1 border_spine] in fabric_1 + group switch_2g[switch_2 border_spine] in fabric_1 + service interface_2a(server)[interface_2a] in switch_2g + + interface_4a:T -- B:interface_2a diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.yaml index 8e2de4f0a..330a1e40a 100644 --- a/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.yaml +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.yaml @@ -53,7 +53,15 @@ - "switch_2 : {{ switch_2 }}" - "interface_2a : {{ interface_2a }}" -- name: SETUP.1 - OVERRIDDEN - [dcnn_rest.GET] Verify if fabric is deployed. +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.1 - OVERRIDDEN - [dcnn_rest.GET] Verify if fabric is deployed." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_rest: method: GET path: "{{ rest_path }}" @@ -61,9 +69,17 @@ - assert: that: - - 'result.response.DATA != None' + - result.response.DATA != None + +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.2 - OVERRIDDEN - [deleted] Delete all VRFs" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" -- name: SETUP.2 - OVERRIDDEN - [deleted] Delete all VRFs +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted @@ -77,7 +93,15 @@ timeout: 60 when: result_setup_2.changed == true -- name: SETUP.3 OVERRIDDEN - [merged] Create, Attach, Deploy VLAN+VRF ansible-vrf-int1 +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.3 OVERRIDDEN - [merged] Create, Attach, Deploy VLAN+VRF ansible-vrf-int1" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -97,7 +121,15 @@ debug: var: result_setup_3 -- name: SETUP.4 OVERRIDDEN - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.4 OVERRIDDEN - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -109,23 +141,34 @@ - assert: that: - - 'result_setup_3.changed == true' - - 'result_setup_3.response[0].RETURN_CODE == 200' - - 'result_setup_3.response[1].RETURN_CODE == 200' - - 'result_setup_3.response[2].RETURN_CODE == 200' - - '(result_setup_3.response[1].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_setup_3.response[1].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_setup_3.diff[0].attach[0].deploy == true' - - 'result_setup_3.diff[0].attach[1].deploy == true' - - '"{{ switch_1 }}" in result_setup_3.diff[0].attach[0].ip_address' - - '"{{ switch_2 }}" in result_setup_3.diff[0].attach[1].ip_address' - - 'result_setup_3.diff[0].vrf_name == "ansible-vrf-int1"' + - result_setup_3.changed == true + - result_setup_3.diff[0].attach[0].deploy == true + - result_setup_3.diff[0].attach[1].deploy == true + - result_setup_3.diff[0].vrf_name == "ansible-vrf-int1" + - result_setup_3.response[0].RETURN_CODE == 200 + - result_setup_3.response[1].RETURN_CODE == 200 + - result_setup_3.response[2].RETURN_CODE == 200 + - (result_setup_3.response[1].DATA|dict2items)[0].value == "SUCCESS" + - (result_setup_3.response[1].DATA|dict2items)[1].value == "SUCCESS" + - switch_1 in result_setup_3.diff[0].attach[0].ip_address + - switch_2 in result_setup_3.diff[0].attach[1].ip_address ############################################### ### OVERRIDDEN ## ############################################### +- name: Set fact + ansible.builtin.set_fact: + DEPLOYMENT_OF_VRFS: "Deployment of VRF(s) has been initiated successfully" + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1 - OVERRIDDEN - [overridden] Override existing VRF ansible-vrf-int1 to create new VRF ansible-vrf-int2" -- name: TEST.1 - OVERRIDDEN - [overridden] Override existing VRF ansible-vrf-int1 to create new VRF ansible-vrf-int2 +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf1 fabric: "{{ fabric_1 }}" state: overridden @@ -141,7 +184,15 @@ deploy: true register: result_1 -- name: TEST.1a - OVERRIDDEN - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1a - OVERRIDDEN - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -157,27 +208,41 @@ - assert: that: - - 'result_1.changed == true' - - 'result_1.response[0].RETURN_CODE == 200' - - 'result_1.response[1].RETURN_CODE == 200' - - 'result_1.response[2].RETURN_CODE == 200' - - 'result_1.response[3].RETURN_CODE == 200' - - 'result_1.response[4].RETURN_CODE == 200' - - 'result_1.response[5].RETURN_CODE == 200' - - 'result_1.response[6].RETURN_CODE == 200' - - 'result_1.response[7].RETURN_CODE == 200' - - '(result_1.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_1.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - '(result_1.response[6].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_1.response[6].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_1.diff[0].attach[0].deploy == true' - - 'result_1.diff[0].attach[1].deploy == true' - - 'result_1.diff[0].vrf_name == "ansible-vrf-int2"' - - 'result_1.diff[1].attach[0].deploy == false' - - 'result_1.diff[1].attach[1].deploy == false' - - 'result_1.diff[1].vrf_name == "ansible-vrf-int1"' - -- name: TEST.1c - OVERRIDDEN - [overridden] conf1 - Idempotence + - result_1.changed == true + - result_1.diff[0].attach[0].deploy == true + - result_1.diff[0].attach[1].deploy == true + - result_1.diff[0].vrf_name == "ansible-vrf-int2" + - result_1.diff[1].attach[0].deploy == false + - result_1.diff[1].attach[1].deploy == false + - result_1.diff[1].vrf_name == "ansible-vrf-int1" + - result_1.response[0].METHOD == "POST" + - result_1.response[1].METHOD == "POST" + - result_1.response[2].METHOD == "DELETE" + - result_1.response[3].METHOD == "POST" + - result_1.response[4].METHOD == "POST" + - result_1.response[5].METHOD == "POST" + - result_1.response[0].RETURN_CODE == 200 + - result_1.response[1].RETURN_CODE == 200 + - result_1.response[2].RETURN_CODE == 200 + - result_1.response[3].RETURN_CODE == 200 + - result_1.response[4].RETURN_CODE == 200 + - result_1.response[5].RETURN_CODE == 200 + - (result_1.response[0].DATA|dict2items)[0].value == "SUCCESS" + - (result_1.response[0].DATA|dict2items)[1].value == "SUCCESS" + - (result_1.response[4].DATA|dict2items)[0].value == "SUCCESS" + - (result_1.response[4].DATA|dict2items)[1].value == "SUCCESS" + - result_1.response[1].DATA.status == DEPLOYMENT_OF_VRFS + - result_1.response[5].DATA.status == DEPLOYMENT_OF_VRFS + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1c - OVERRIDDEN - [overridden] conf1 - Idempotence" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf1 register: result_1c @@ -187,10 +252,18 @@ - assert: that: - - 'result_1c.changed == false' - - 'result_1c.response|length == 0' + - result_1c.changed == false + - result_1c.response|length == 0 + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1f - OVERRIDDEN - [deleted] Delete all VRFs" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" -- name: TEST.1f - OVERRIDDEN - [deleted] Delete all VRFs +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted @@ -201,7 +274,15 @@ timeout: 60 when: result_1f.changed == true -- name: TEST.2 - OVERRIDDEN - [merged] ansible-vrf-int2 to add vrf_lite extension to switch_2 +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2 - OVERRIDDEN - [merged] ansible-vrf-int2 to add vrf_lite extension to switch_2" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -233,7 +314,15 @@ timeout: 60 when: result_2.changed == true -- name: TEST.2b - OVERRIDDEN - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2b - OVERRIDDEN - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -249,19 +338,28 @@ - assert: that: - - 'result_2.changed == true' - - 'result_2.response[0].RETURN_CODE == 200' - - 'result_2.response[1].RETURN_CODE == 200' - - 'result_2.response[2].RETURN_CODE == 200' - - '(result_2.response[1].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_2.response[1].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_2.diff[0].attach[0].deploy == true' - - 'result_2.diff[0].attach[1].deploy == true' - - '"{{ switch_1 }}" in result_2.diff[0].attach[0].ip_address' - - '"{{ switch_2 }}" in result_2.diff[0].attach[1].ip_address' - - 'result_2.diff[0].vrf_name == "ansible-vrf-int2"' - -- name: TEST.3 - OVERRIDDEN - [overridden] Override vrf_lite extension with new dot1q value + - result_2.changed == true + - result_2.diff[0].attach[0].deploy == true + - result_2.diff[0].attach[1].deploy == true + - result_2.diff[0].vrf_name == "ansible-vrf-int2" + - result_2.response[0].RETURN_CODE == 200 + - result_2.response[1].RETURN_CODE == 200 + - result_2.response[2].RETURN_CODE == 200 + - result_2.response[2].DATA.status == DEPLOYMENT_OF_VRFS + - (result_2.response[1].DATA|dict2items)[0].value == "SUCCESS" + - (result_2.response[1].DATA|dict2items)[1].value == "SUCCESS" + - switch_1 in result_2.diff[0].attach[0].ip_address + - switch_2 in result_2.diff[0].attach[1].ip_address + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3 - OVERRIDDEN - [overridden] Override vrf_lite extension with new dot1q value" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf3 fabric: "{{ fabric_1 }}" state: overridden @@ -301,7 +399,15 @@ # NOTE: this happens ONLY when Fabric Settings -> Advanced -> Overlay Mode # is set to "config-profile" (which is the default). It does not happen # if Overlay Mode is set to "cli". -- name: TEST.3b - OVERRIDDEN - [dcnm_rest.POST] - config-deploy to workaround FAILED/OUT-OF-SYNC VRF status +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3b - OVERRIDDEN - [dcnm_rest.POST] - config-deploy to workaround FAILED/OUT-OF-SYNC VRF status" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_rest: method: POST path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_1 }}/config-deploy?forceShowRun=false" @@ -312,7 +418,15 @@ timeout: 60 when: result_3.changed == true -- name: TEST.3d - OVERRIDDEN - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3d - OVERRIDDEN - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -328,14 +442,23 @@ - assert: that: - - 'result_3.changed == true' - - 'result_3.response[0].RETURN_CODE == 200' - - 'result_3.response[1].RETURN_CODE == 200' - - '(result_3.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - 'result_3.diff[0].attach[0].deploy == true' - - 'result_3.diff[0].vrf_name == "ansible-vrf-int2"' - -- name: TEST.3f - OVERRIDDEN - [overridden] conf2 - Idempotence + - result_3.changed == true + - result_3.diff[0].attach[0].deploy == true + - result_3.diff[0].vrf_name == "ansible-vrf-int2" + - result_3.response[0].RETURN_CODE == 200 + - result_3.response[1].RETURN_CODE == 200 + - result_3.response[1].DATA.status == DEPLOYMENT_OF_VRFS + - (result_3.response[0].DATA|dict2items)[0].value == "SUCCESS" + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3f - OVERRIDDEN - [overridden] conf2 - Idempotence" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf3 register: result_3f @@ -345,10 +468,18 @@ - assert: that: - - 'result_3f.changed == false' - - 'result_3f.response|length == 0' + - result_3f.changed == false + - result_3f.response|length == 0 + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.4 - OVERRIDDEN - [overridden] Override ansible-vrf-int2 to create ansible-vrf-int1 with LITE Extension" -- name: TEST.4 - OVERRIDDEN - [overridden] Override ansible-vrf-int2 to create ansible-vrf-int1 with LITE Extension +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf4 fabric: "{{ fabric_1 }}" state: overridden @@ -380,7 +511,15 @@ timeout: 60 when: result_4.changed == true -- name: TEST.4b - OVERRIDDEN - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.4b - OVERRIDDEN - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -396,28 +535,41 @@ - assert: that: - - 'result_4.changed == true' - - 'result_4.response[0].RETURN_CODE == 200' - - 'result_4.response[1].RETURN_CODE == 200' - - 'result_4.response[2].RETURN_CODE == 200' - - 'result_4.response[3].RETURN_CODE == 200' - - 'result_4.response[4].RETURN_CODE == 200' - - 'result_4.response[5].RETURN_CODE == 200' - - 'result_4.response[6].RETURN_CODE == 200' - - 'result_4.response[7].RETURN_CODE == 200' - - '(result_4.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_4.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - '(result_4.response[6].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_4.response[6].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_4.response[2].METHOD == "DELETE"' - - 'result_4.diff[0].attach[0].deploy == true' - - 'result_4.diff[0].attach[1].deploy == true' - - 'result_4.diff[0].vrf_name == "ansible-vrf-int1"' - - 'result_4.diff[1].attach[0].deploy == false' - - 'result_4.diff[1].attach[1].deploy == false' - - 'result_4.diff[1].vrf_name == "ansible-vrf-int2"' - -- name: TEST.4d - OVERRIDDEN - [overridden] conf3 - Idempotence + - result_4.changed == true + - result_4.diff[0].attach[0].deploy == true + - result_4.diff[0].attach[1].deploy == true + - result_4.diff[0].vrf_name == "ansible-vrf-int1" + - result_4.diff[1].attach[0].deploy == false + - result_4.diff[1].attach[1].deploy == false + - result_4.diff[1].vrf_name == "ansible-vrf-int2" + - result_4.response[0].METHOD == "POST" + - result_4.response[1].METHOD == "POST" + - result_4.response[2].METHOD == "DELETE" + - result_4.response[3].METHOD == "POST" + - result_4.response[4].METHOD == "POST" + - result_4.response[5].METHOD == "POST" + - result_4.response[0].RETURN_CODE == 200 + - result_4.response[1].RETURN_CODE == 200 + - result_4.response[2].RETURN_CODE == 200 + - result_4.response[3].RETURN_CODE == 200 + - result_4.response[4].RETURN_CODE == 200 + - result_4.response[5].RETURN_CODE == 200 + - result_4.response[1].DATA.status == DEPLOYMENT_OF_VRFS + - result_4.response[5].DATA.status == DEPLOYMENT_OF_VRFS + - (result_4.response[0].DATA|dict2items)[0].value == "SUCCESS" + - (result_4.response[0].DATA|dict2items)[1].value == "SUCCESS" + - (result_4.response[4].DATA|dict2items)[0].value == "SUCCESS" + - (result_4.response[4].DATA|dict2items)[1].value == "SUCCESS" + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.4d - OVERRIDDEN - [overridden] conf3 - Idempotence" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf4 register: result_4d @@ -427,14 +579,22 @@ - assert: that: - - 'result_4d.changed == false' - - 'result_4d.response|length == 0' + - result_4d.changed == false + - result_4d.response|length == 0 ############################################## ## CLEAN-UP ## ############################################## -- name: CLEANUP.1 - OVERRIDDEN - [deleted] Delete all VRFs +- name: Set task name + ansible.builtin.set_fact: + task_name: "CLEANUP.1 - OVERRIDDEN - [deleted] Delete all VRFs" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/query.md b/tests/integration/targets/dcnm_vrf/tests/dcnm/query.md new file mode 100644 index 000000000..51be8aca8 --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/query.md @@ -0,0 +1,46 @@ +# Topology - query state + +The topology (fabrics and switches) is not created by the test and must be +created through other means (NDFC GUI, separate Ansible scripts, etc) + +[Topology Diagram](query.mermaid) + +## ISN + +- Fabric type is `Multi-Site External Network` +- The fabric is not referenced in the test, but needs to exist + +### switch_4 + +- switch_4 role (NDFC GUI) is `Edge Router` +- switch_4 is not referenced in the test, but needs to exist + +## fabric_1 + +- Fabric type (NDFC GUI) is `Data Center VXLAN EVPN` +- Fabric type (NDFC Template) is `Easy_Fabric` +- Fabric type (dcnm_fabric Playbook) is `VXLAN_EVPN` + +### switch_1 + +- switch_1 role (NDFC GUI) is `Border Spine` +- switch_1 does not require an interface + +### switch_2 + +- switch_2 role (NDFC GUI) is `Border Spine` +- interface_2a is connected to switch_4 and must be up + +```mermaid +architecture-beta + group isn(cloud)[ISN] + group switch_4g[switch_4 edge_router] in isn + service interface_4a(internet)[interface_4a] in switch_4g + + group fabric_1(cloud)[fabric_1] + group switch_1g[switch_1 border_spine] in fabric_1 + group switch_2g[switch_2 border_spine] in fabric_1 + service interface_2a(server)[interface_2a] in switch_2g + + interface_4a:T -- B:interface_2a +``` diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/query.mermaid b/tests/integration/targets/dcnm_vrf/tests/dcnm/query.mermaid new file mode 100644 index 000000000..906fdb7af --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/query.mermaid @@ -0,0 +1,11 @@ +architecture-beta + group isn(cloud)[ISN] + group switch_4g[switch_4 edge_router] in isn + service interface_4a(internet)[interface_4a] in switch_4g + + group fabric_1(cloud)[fabric_1] + group switch_1g[switch_1 border_spine] in fabric_1 + group switch_2g[switch_2 border_spine] in fabric_1 + service interface_2a(server)[interface_2a] in switch_2g + + interface_4a:T -- B:interface_2a diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/query.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/query.yaml index 8a3e7f7ff..6dde649c8 100644 --- a/tests/integration/targets/dcnm_vrf/tests/dcnm/query.yaml +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/query.yaml @@ -31,7 +31,7 @@ rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_1 }}" when: controller_version >= "12" -- name: SETUP.0 - QUERY - [with_items] print vars +- name: SETUP.0a - QUERY - [with_items] print vars ansible.builtin.debug: var: item with_items: @@ -40,7 +40,24 @@ - "switch_2 : {{ switch_2 }}" - "interface_2a : {{ interface_2a }}" -- name: SETUP.1 - QUERY - [dcnm_rest.GET] Verify if fabric is deployed. +- name: SETUP.0b - QUERY - [with_items] log vars + cisco.dcnm.dcnm_log: + msg: "{{ item }}" + with_items: + - "fabric_1 : {{ fabric_1 }}" + - "switch_1 : {{ switch_1 }}" + - "switch_2 : {{ switch_2 }}" + - "interface_2a : {{ interface_2a }}" + +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.1 - QUERY - [dcnm_rest.GET] Verify if fabric is deployed." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_rest: method: GET path: "{{ rest_path }}" @@ -50,13 +67,29 @@ that: - 'result.response.DATA != None' -- name: SETUP.2 - QUERY - [deleted] Delete all VRFs +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.2 - QUERY - [deleted] Delete all VRFs." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted register: result_setup_2 -- name: SETUP.2a - QUERY - Wait 60 seconds for controller and switch to sync +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.2a - QUERY - Wait 60 seconds for controller and switch to sync." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" # The vrf lite profile removal returns ok for deployment, but the switch # takes time to remove the profile so wait for some time before creating # a new vrf, else the switch goes into OUT-OF-SYNC state @@ -64,7 +97,15 @@ timeout: 60 when: result_setup_2.changed == true -- name: SETUP.3 - QUERY - [merged] Create, Attach, Deploy VLAN+VRF ansible-vrf-int1 +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.3 - QUERY - [merged] Create, Attach, Deploy VLAN+VRF ansible-vrf-int1." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -80,7 +121,15 @@ deploy: true register: result_setup_3 -- name: SETUP.3a - QUERY - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.3a - QUERY - [query] Wait for vrfStatus == DEPLOYED" + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -96,17 +145,17 @@ - assert: that: - - 'result_setup_3.changed == true' - - 'result_setup_3.response[0].RETURN_CODE == 200' - - 'result_setup_3.response[1].RETURN_CODE == 200' - - 'result_setup_3.response[2].RETURN_CODE == 200' - - '(result_setup_3.response[1].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_setup_3.response[1].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_setup_3.diff[0].attach[0].deploy == true' - - 'result_setup_3.diff[0].attach[1].deploy == true' - - '"{{ switch_1 }}" in result_setup_3.diff[0].attach[0].ip_address' - - '"{{ switch_2 }}" in result_setup_3.diff[0].attach[1].ip_address' - - 'result_setup_3.diff[0].vrf_name == "ansible-vrf-int1"' + - result_setup_3.changed == true + - result_setup_3.diff[0].attach[0].deploy == true + - result_setup_3.diff[0].attach[1].deploy == true + - result_setup_3.diff[0].vrf_name == "ansible-vrf-int1" + - result_setup_3.response[0].RETURN_CODE == 200 + - result_setup_3.response[1].RETURN_CODE == 200 + - result_setup_3.response[2].RETURN_CODE == 200 + - (result_setup_3.response[1].DATA|dict2items)[0].value == "SUCCESS" + - (result_setup_3.response[1].DATA|dict2items)[1].value == "SUCCESS" + - switch_1 in result_setup_3.diff[0].attach[0].ip_address + - switch_2 in result_setup_3.diff[0].attach[1].ip_address # ############################################### # ### QUERY ## @@ -134,16 +183,16 @@ - assert: that: - - 'result_1.changed == false' - - 'result_1.response[0].parent.vrfName == "ansible-vrf-int1"' - - 'result_1.response[0].parent.vrfId == 9008011' - - 'result_1.response[0].parent.vrfStatus == "DEPLOYED"' - - 'result_1.response[0].attach[0].switchDetailsList[0].islanAttached == true' - - 'result_1.response[0].attach[0].switchDetailsList[0].lanAttachedState == "DEPLOYED"' - - 'result_1.response[0].attach[0].switchDetailsList[0].vlan == 500' - - 'result_1.response[0].attach[1].switchDetailsList[0].islanAttached == true' - - 'result_1.response[0].attach[1].switchDetailsList[0].lanAttachedState == "DEPLOYED"' - - 'result_1.response[0].attach[1].switchDetailsList[0].vlan == 500' + - result_1.changed == false + - result_1.response[0].attach[0].switchDetailsList[0].islanAttached == true + - result_1.response[0].attach[0].switchDetailsList[0].lanAttachedState == "DEPLOYED" + - result_1.response[0].attach[0].switchDetailsList[0].vlan == 500 + - result_1.response[0].attach[1].switchDetailsList[0].islanAttached == true + - result_1.response[0].attach[1].switchDetailsList[0].lanAttachedState == "DEPLOYED" + - result_1.response[0].attach[1].switchDetailsList[0].vlan == 500 + - result_1.response[0].parent.vrfId == 9008011 + - result_1.response[0].parent.vrfName == "ansible-vrf-int1" + - result_1.response[0].parent.vrfStatus == "DEPLOYED" - name: TEST.2 - QUERY - [deleted] Delete all VRFs cisco.dcnm.dcnm_vrf: @@ -157,17 +206,17 @@ - assert: that: - - 'result_2.changed == true' - - 'result_2.response[0].RETURN_CODE == 200' - - 'result_2.response[1].RETURN_CODE == 200' - - 'result_2.response[1].MESSAGE == "OK"' - - 'result_2.response[2].RETURN_CODE == 200' - - 'result_2.response[2].METHOD == "DELETE"' - - '(result_2.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_2.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_2.diff[0].attach[0].deploy == false' - - 'result_2.diff[0].attach[1].deploy == false' - - 'result_2.diff[0].vrf_name == "ansible-vrf-int1"' + - result_2.changed == true + - result_2.diff[0].attach[0].deploy == false + - result_2.diff[0].attach[1].deploy == false + - result_2.diff[0].vrf_name == "ansible-vrf-int1" + - result_2.response[1].MESSAGE == "OK" + - result_2.response[2].METHOD == "DELETE" + - result_2.response[0].RETURN_CODE == 200 + - result_2.response[1].RETURN_CODE == 200 + - result_2.response[2].RETURN_CODE == 200 + - (result_2.response[0].DATA|dict2items)[0].value == "SUCCESS" + - (result_2.response[0].DATA|dict2items)[1].value == "SUCCESS" - name: TEST.2b - QUERY - [wait_for] Wait 60 seconds for controller and switch to sync wait_for: @@ -205,15 +254,15 @@ - assert: that: - - 'result_3.changed == true' - - 'result_3.response[0].RETURN_CODE == 200' - - 'result_3.response[1].RETURN_CODE == 200' - - 'result_3.response[2].RETURN_CODE == 200' - - '(result_3.response[1].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_3.response[1].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_3.diff[0].attach[0].deploy == true' - - '"{{ switch_1 }}" in result_3.diff[0].attach[0].ip_address' - - '"{{ switch_2 }}" in result_3.diff[0].attach[1].ip_address' + - result_3.changed == true + - result_3.diff[0].attach[0].deploy == true + - result_3.response[0].RETURN_CODE == 200 + - result_3.response[1].RETURN_CODE == 200 + - result_3.response[2].RETURN_CODE == 200 + - (result_3.response[1].DATA|dict2items)[0].value == "SUCCESS" + - (result_3.response[1].DATA|dict2items)[1].value == "SUCCESS" + - switch_1 in result_3.diff[0].attach[0].ip_address + - switch_2 in result_3.diff[0].attach[1].ip_address - name: TEST.4 - QUERY - [merged] Create, Attach, Deploy VRF+LITE EXTENSION ansible-vrf-int2 on switch_2 cisco.dcnm.dcnm_vrf: @@ -255,13 +304,13 @@ - assert: that: - - 'result_4.changed == true' - - 'result_4.response[0].RETURN_CODE == 200' - - 'result_4.response[1].RETURN_CODE == 200' - - '(result_4.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - 'result_4.diff[0].attach[0].deploy == true' - - '"{{ switch_2 }}" in result_4.diff[0].attach[0].ip_address' - - 'result_4.diff[0].vrf_name == "ansible-vrf-int2"' + - result_4.changed == true + - result_4.diff[0].attach[0].deploy == true + - result_4.diff[0].vrf_name == "ansible-vrf-int2" + - result_4.response[0].RETURN_CODE == 200 + - result_4.response[1].RETURN_CODE == 200 + - (result_4.response[0].DATA|dict2items)[0].value == "SUCCESS" + - switch_2 in result_4.diff[0].attach[0].ip_address - name: TEST.5 - QUERY - [query] Query VRF+LITE EXTENSION ansible-vrf-int2 switch_2 cisco.dcnm.dcnm_vrf: @@ -293,16 +342,16 @@ - assert: that: - - 'result_5.changed == false' - - 'result_5.response[0].parent.vrfName == "ansible-vrf-int2"' - - 'result_5.response[0].parent.vrfId == 9008012' - - 'result_5.response[0].parent.vrfStatus == "DEPLOYED"' - - 'result_5.response[0].attach[0].switchDetailsList[0].islanAttached == true' - - 'result_5.response[0].attach[0].switchDetailsList[0].lanAttachedState == "DEPLOYED"' - - 'result_5.response[0].attach[0].switchDetailsList[0].vlan == 1500' - - 'result_5.response[0].attach[1].switchDetailsList[0].islanAttached == true' - - 'result_5.response[0].attach[1].switchDetailsList[0].lanAttachedState == "DEPLOYED"' - - 'result_5.response[0].attach[1].switchDetailsList[0].vlan == 1500' + - result_5.changed == false + - result_5.response[0].parent.vrfId == 9008012 + - result_5.response[0].parent.vrfName == "ansible-vrf-int2" + - result_5.response[0].parent.vrfStatus == "DEPLOYED" + - result_5.response[0].attach[0].switchDetailsList[0].islanAttached == true + - result_5.response[0].attach[0].switchDetailsList[0].lanAttachedState == "DEPLOYED" + - result_5.response[0].attach[0].switchDetailsList[0].vlan == 1500 + - result_5.response[0].attach[1].switchDetailsList[0].islanAttached == true + - result_5.response[0].attach[1].switchDetailsList[0].lanAttachedState == "DEPLOYED" + - result_5.response[0].attach[1].switchDetailsList[0].vlan == 1500 - name: TEST.6 - QUERY - [query] Query without the config element cisco.dcnm.dcnm_vrf: @@ -316,16 +365,16 @@ - assert: that: - - 'result_6.changed == false' - - 'result_6.response[0].parent.vrfName == "ansible-vrf-int2"' - - 'result_6.response[0].parent.vrfId == 9008012' - - 'result_6.response[0].parent.vrfStatus == "DEPLOYED"' - - 'result_6.response[0].attach[0].switchDetailsList[0].islanAttached == true' - - 'result_6.response[0].attach[0].switchDetailsList[0].lanAttachedState == "DEPLOYED"' - - 'result_6.response[0].attach[0].switchDetailsList[0].vlan == 1500' - - 'result_6.response[0].attach[1].switchDetailsList[0].islanAttached == true' - - 'result_6.response[0].attach[1].switchDetailsList[0].lanAttachedState == "DEPLOYED"' - - 'result_6.response[0].attach[1].switchDetailsList[0].vlan == 1500' + - result_6.changed == false + - result_6.response[0].parent.vrfId == 9008012 + - result_6.response[0].parent.vrfName == "ansible-vrf-int2" + - result_6.response[0].parent.vrfStatus == "DEPLOYED" + - result_6.response[0].attach[0].switchDetailsList[0].islanAttached == true + - result_6.response[0].attach[0].switchDetailsList[0].lanAttachedState == "DEPLOYED" + - result_6.response[0].attach[0].switchDetailsList[0].vlan == 1500 + - result_6.response[0].attach[1].switchDetailsList[0].islanAttached == true + - result_6.response[0].attach[1].switchDetailsList[0].lanAttachedState == "DEPLOYED" + - result_6.response[0].attach[1].switchDetailsList[0].vlan == 1500 - name: TEST.7 - QUERY - [query] Query non-existent VRF ansible-vrf-int1 cisco.dcnm.dcnm_vrf: @@ -349,8 +398,8 @@ - assert: that: - - 'result_7.changed == false' - - 'result_7.response|length == 0' + - result_7.changed == false + - result_7.response|length == 0 ############################################### ### CLEAN-UP ## diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.md b/tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.md new file mode 100644 index 000000000..4f2bc576d --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.md @@ -0,0 +1,46 @@ +# Topology - replaced state + +The topology (fabrics and switches) is not created by the test and must be +created through other means (NDFC GUI, separate Ansible scripts, etc) + +[Topology Diagram](replaced.mermaid) + +## ISN + +- Fabric type is `Multi-Site External Network` +- The fabric is not referenced in the test, but needs to exist + +### switch_4 + +- switch_4 role (NDFC GUI) is `Edge Router` +- switch_4 is not referenced in the test, but needs to exist + +## fabric_1 + +- Fabric type (NDFC GUI) is `Data Center VXLAN EVPN` +- Fabric type (NDFC Template) is `Easy_Fabric` +- Fabric type (dcnm_fabric Playbook) is `VXLAN_EVPN` + +### switch_1 + +- switch_1 role (NDFC GUI) is `Border Spine` +- switch_1 does not require an interface + +### switch_2 + +- switch_2 role (NDFC GUI) is `Border Spine` +- interface_2a is connected to switch_4 and must be up + +```mermaid +architecture-beta + group isn(cloud)[ISN] + group switch_4g[switch_4 edge_router] in isn + service interface_4a(internet)[interface_4a] in switch_4g + + group fabric_1(cloud)[fabric_1] + group switch_1g[switch_1 border_spine] in fabric_1 + group switch_2g[switch_2 border_spine] in fabric_1 + service interface_2a(server)[interface_2a] in switch_2g + + interface_4a:T -- B:interface_2a +``` diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.mermaid b/tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.mermaid new file mode 100644 index 000000000..906fdb7af --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.mermaid @@ -0,0 +1,11 @@ +architecture-beta + group isn(cloud)[ISN] + group switch_4g[switch_4 edge_router] in isn + service interface_4a(internet)[interface_4a] in switch_4g + + group fabric_1(cloud)[fabric_1] + group switch_1g[switch_1 border_spine] in fabric_1 + group switch_2g[switch_2 border_spine] in fabric_1 + service interface_2a(server)[interface_2a] in switch_2g + + interface_4a:T -- B:interface_2a diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.yaml index a0a8254d5..769b2b1a8 100644 --- a/tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.yaml +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.yaml @@ -33,7 +33,7 @@ rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_1 }}" when: controller_version >= "12" -- name: SETUP.0 - REPLACED - [with_items] print vars +- name: SETUP.0a - REPLACED - [with_items] print vars ansible.builtin.debug: var: item with_items: @@ -42,7 +42,24 @@ - "switch_2 : {{ switch_2 }}" - "interface_2a : {{ interface_2a }}" -- name: SETUP.1 - REPLACED - [dcnm_rest.GET] Verify if fabric is deployed. +- name: SETUP.0b - REPLACED - [with_items] log vars + cisco.dcnm.dcnm_log: + msg: "{{ item }}" + with_items: + - "fabric_1 : {{ fabric_1 }}" + - "switch_1 : {{ switch_1 }}" + - "switch_2 : {{ switch_2 }}" + - "interface_2a : {{ interface_2a }}" + +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.1 - REPLACED - [dcnm_rest.GET] Verify if fabric is deployed." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_rest: method: GET path: "{{ rest_path }}" @@ -50,18 +67,42 @@ - assert: that: - - 'result.response.DATA != None' + - result.response.DATA != None + +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.2 - REPLACED - [deleted] Delete all VRFs." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" -- name: SETUP.2 - REPLACED - [deleted] Delete all VRFs +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted -- name: SETUP.3 - REPLACED - [wait_for] Wait 60 seconds for controller and switch to sync +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.3 - REPLACED - [wait_for] Wait 60 seconds for controller and switch to sync." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" wait_for: timeout: 60 -- name: SETUP.4 - REPLACED - [merged] Create, Attach, Deploy VLAN+VRF ansible-vrf-int1 +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.4 - REPLACED - [merged] Create, Attach, Deploy VLAN+VRF ansible-vrf-int1." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -77,7 +118,15 @@ deploy: true register: result_setup_4 -- name: SETUP.4a - REPLACED - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "SETUP.4a - REPLACED - [query] Wait for vrfStatus == DEPLOYED." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -97,21 +146,29 @@ - assert: that: - - 'result_setup_4.changed == true' - - 'result_setup_4.response[0].RETURN_CODE == 200' - - 'result_setup_4.response[1].RETURN_CODE == 200' - - 'result_setup_4.response[2].RETURN_CODE == 200' - - '(result_setup_4.response[1].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_setup_4.response[1].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_setup_4.diff[0].attach[0].deploy == true' - - 'result_setup_4.diff[0].attach[1].deploy == true' - - 'result_setup_4.diff[0].vrf_name == "ansible-vrf-int1"' + - result_setup_4.changed == true + - result_setup_4.diff[0].attach[0].deploy == true + - result_setup_4.diff[0].attach[1].deploy == true + - result_setup_4.diff[0].vrf_name == "ansible-vrf-int1" + - result_setup_4.response[0].RETURN_CODE == 200 + - result_setup_4.response[1].RETURN_CODE == 200 + - result_setup_4.response[2].RETURN_CODE == 200 + - (result_setup_4.response[1].DATA|dict2items)[0].value == "SUCCESS" + - (result_setup_4.response[1].DATA|dict2items)[1].value == "SUCCESS" ############################################### ### REPLACED ## ############################################### -- name: TEST.1 - REPLACED - [replaced] Update existing VRF using replace - delete attachments +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1 - REPLACED - [replaced] Update existing VRF using replace - delete attachments." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf1 fabric: "{{ fabric_1 }}" state: replaced @@ -139,16 +196,47 @@ - assert: that: - - 'result_1.changed == true' - - 'result_1.response[0].RETURN_CODE == 200' - - 'result_1.response[1].RETURN_CODE == 200' - - '(result_1.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_1.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_1.diff[0].attach[0].deploy == false' - - 'result_1.diff[0].attach[1].deploy == false' - - 'result_1.diff[0].vrf_name == "ansible-vrf-int1"' - -- name: TEST.1c - REPLACED - conf1 - Idempotence + - result_1.changed == true + - result_1.diff[0].vrf_name == "ansible-vrf-int1" + - result_1.response[0].RETURN_CODE == 200 + - result_1.response[1].RETURN_CODE == 200 + +- name: TEST.1b - Extract the attach list + set_fact: + attach_list: "{{ result_1.diff[0].attach }}" + +- name: TEST.1b - Assert that all items in attach_list have "deploy" set to false + assert: + that: + - attach_list | map(attribute='deploy') | unique | list == [false] + fail_msg: "Not all items in attach_list have 'deploy' set to false" + success_msg: "All items in attach_list have 'deploy' set to false" + +- name: TEST.1b - Count "SUCCESS" items in response.DATA + set_fact: + success_count: "{{ result_1.response[0].DATA | dict2items | selectattr('value', 'equalto', 'SUCCESS') | list | length }}" + +- name: TEST.1b - Debug success_count + ansible.builtin.debug: + var: success_count | int + +- name: TEST.1b - Assert that success_count equals at least 1 + assert: + that: + - success_count | int > 1 + fail_msg: "Expected at least 1 'SUCCESS' response.DATA. Got {{ success_count }}." + success_msg: "The number of 'SUCCESS' items in response.DATA is {{ success_count }}." + + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.1c - REPLACED - conf1 - Idempotence." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf1 register: result_1c @@ -158,9 +246,17 @@ - assert: that: - - 'result_1c.changed == false' + - result_1c.changed == false + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2 - REPLACED - [replaced] Update existing VRF using replace - create attachments." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" -- name: TEST.2 - REPLACED - [replaced] Update existing VRF using replace - create attachments +- name: "{{ task_name}}" cisco.dcnm.dcnm_vrf: &conf2 fabric: "{{ fabric_1 }}" state: replaced @@ -176,7 +272,15 @@ deploy: true register: result_2 -- name: TEST.2a - REPLACED - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2a - REPLACED - [query] Wait for vrfStatus == DEPLOYED." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -185,6 +289,11 @@ - "result_2a.response[0].parent.vrfStatus is search('DEPLOYED')" retries: 30 delay: 2 + ignore_errors: true + +- name: DEBUG register result_2a + debug: + var: result_2a - name: TEST.2b - REPLACED - [debug] print result_2 debug: @@ -192,18 +301,26 @@ - assert: that: - - 'result_2.changed == true' - - 'result_2.response[0].RETURN_CODE == 200' - - 'result_2.response[1].RETURN_CODE == 200' - - '(result_2.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_2.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_2.diff[0].attach[0].deploy == true' - - 'result_2.diff[0].attach[1].deploy == true' - - 'result_2.diff[0].vrf_name == "ansible-vrf-int1"' - - 'result_2.diff[0].attach[0].vlan_id == 500' - - 'result_2.diff[0].attach[1].vlan_id == 500' - -- name: TEST.2c - REPLACED - [replaced] conf2 - Idempotence + - result_2.changed == true + - result_2.diff[0].attach[0].deploy == true + - result_2.diff[0].attach[1].deploy == true + - result_2.diff[0].attach[0].vlan_id == 500 + - result_2.diff[0].attach[1].vlan_id == 500 + - result_2.diff[0].vrf_name == "ansible-vrf-int1" + - result_2.response[0].RETURN_CODE == 200 + - result_2.response[1].RETURN_CODE == 200 + - (result_2.response[0].DATA|dict2items)[0].value == "SUCCESS" + - (result_2.response[0].DATA|dict2items)[1].value == "SUCCESS" + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2c - REPLACED - [replaced] conf2 - Idempotence." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf2 register: result_2c @@ -213,9 +330,17 @@ - assert: that: - - 'result_2c.changed == false' + - result_2c.changed == false + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.2e - REPLACED - [deleted] Delete all VRFs." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" -- name: TEST.2e - REPLACED - [deleted] Delete all VRFs +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted @@ -224,7 +349,15 @@ wait_for: timeout: 60 -- name: TEST.3 - REPLACED - [merged] Create, Attach, Deploy VLAN+VRF+LITE switch_2 (user provided VLAN) +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3 - REPLACED - [merged] Create, Attach, Deploy VLAN+VRF+LITE switch_2 (user provided VLAN)." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: merged @@ -248,7 +381,15 @@ deploy: true register: result_3 -- name: TEST.3a - REPLACED - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.3a - REPLACED - [query] Wait for vrfStatus == DEPLOYED." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -264,19 +405,27 @@ - assert: that: - - 'result_3.changed == true' - - 'result_3.response[0].RETURN_CODE == 200' - - 'result_3.response[1].RETURN_CODE == 200' - - 'result_3.response[2].RETURN_CODE == 200' - - '(result_3.response[1].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_3.response[1].DATA|dict2items)[1].value == "SUCCESS"' - - 'result_3.diff[0].attach[0].deploy == true' - - 'result_3.diff[0].attach[1].deploy == true' - - '"{{ switch_1 }}" in result_3.diff[0].attach[0].ip_address' - - '"{{ switch_2 }}" in result_3.diff[0].attach[1].ip_address' - - 'result_3.diff[0].vrf_name == "ansible-vrf-int1"' - -- name: TEST.4 - REPLACED - [replaced] Update existing VRF - Delete VRF LITE Attachment + - result_3.changed == true + - result_3.diff[0].attach[0].deploy == true + - result_3.diff[0].attach[1].deploy == true + - result_3.diff[0].vrf_name == "ansible-vrf-int1" + - result_3.response[0].RETURN_CODE == 200 + - result_3.response[1].RETURN_CODE == 200 + - result_3.response[2].RETURN_CODE == 200 + - (result_3.response[1].DATA|dict2items)[0].value == "SUCCESS" + - (result_3.response[1].DATA|dict2items)[1].value == "SUCCESS" + - switch_1 in result_3.diff[0].attach[0].ip_address + - switch_2 in result_3.diff[0].attach[1].ip_address + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.4 - REPLACED - [replaced] Update existing VRF - Delete VRF LITE Attachment." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf4 fabric: "{{ fabric_1 }}" state: replaced @@ -295,7 +444,15 @@ wait_for: timeout: 60 -- name: TEST.4b - REPLACED - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.4b - REPLACED - [query] Wait for vrfStatus == DEPLOYED." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -311,14 +468,22 @@ - assert: that: - - 'result_4.changed == true' - - 'result_4.response[0].RETURN_CODE == 200' - - 'result_4.response[1].RETURN_CODE == 200' - - '(result_4.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - 'result_4.diff[0].attach[0].deploy == false' - - 'result_4.diff[0].vrf_name == "ansible-vrf-int1"' - -- name: TEST.4d - REPLACED - conf4 - Idempotence + - result_4.changed == true + - result_4.diff[0].attach[0].deploy == false + - result_4.diff[0].vrf_name == "ansible-vrf-int1" + - result_4.response[0].RETURN_CODE == 200 + - result_4.response[1].RETURN_CODE == 200 + - (result_4.response[0].DATA|dict2items)[0].value == "SUCCESS" + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.4d - REPLACED - conf4 - Idempotence." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf4 register: result_4d @@ -328,9 +493,17 @@ - assert: that: - - 'result_4d.changed == false' + - result_4d.changed == false -- name: TEST.5 - REPLACED - [replaced] Update existing VRF - Create VRF LITE Attachment +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.5 - REPLACED - [replaced] Update existing VRF - Create VRF LITE Attachment." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: &conf5 fabric: "{{ fabric_1 }}" state: replaced @@ -354,7 +527,15 @@ deploy: true register: result_5 -- name: TEST.5a - REPLACED - [query] Wait for vrfStatus == DEPLOYED +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.5a - REPLACED - [query] Wait for vrfStatus == DEPLOYED." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: query @@ -370,15 +551,23 @@ - assert: that: - - 'result_5.changed == true' - - 'result_5.response[0].RETURN_CODE == 200' - - 'result_5.response[1].RETURN_CODE == 200' - - '(result_5.response[0].DATA|dict2items)[0].value == "SUCCESS"' - - 'result_5.diff[0].attach[0].deploy == true' - - 'result_5.diff[0].vrf_name == "ansible-vrf-int1"' - - 'result_5.diff[0].attach[0].vlan_id == 500' - -- name: TEST.5c - REPLACED - conf5 - Idempotence + - result_5.changed == true + - result_5.diff[0].attach[0].deploy == true + - result_5.diff[0].attach[0].vlan_id == 500 + - result_5.diff[0].vrf_name == "ansible-vrf-int1" + - result_5.response[0].RETURN_CODE == 200 + - result_5.response[1].RETURN_CODE == 200 + - (result_5.response[0].DATA|dict2items)[0].value == "SUCCESS" + +- name: Set task name + ansible.builtin.set_fact: + task_name: "TEST.5c - REPLACED - conf5 - Idempotence." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: *conf5 register: result_5c @@ -388,13 +577,21 @@ - assert: that: - - 'result_5c.changed == false' + - result_5c.changed == false ############################################### ### CLEAN-UP ## ############################################### -- name: CLEANUP.1 - REPLACED - [deleted] Delete all VRFs +- name: Set task name + ansible.builtin.set_fact: + task_name: "CLEANUP.1 - REPLACED - [deleted] Delete all VRFs." + +- name: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt deleted file mode 100644 index 7fdc2f2cb..000000000 --- a/tests/sanity/ignore-2.10.txt +++ /dev/null @@ -1,23 +0,0 @@ -plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py import-2.6!skip -plugins/modules/dcnm_rest.py import-2.7!skip -plugins/module_utils/common/sender_requests.py import-3.11 # TODO remove this if/when requests is added to the standard library diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt deleted file mode 100644 index 6390465d2..000000000 --- a/tests/sanity/ignore-2.11.txt +++ /dev/null @@ -1,29 +0,0 @@ -plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py import-2.6!skip -plugins/modules/dcnm_rest.py import-2.7!skip -plugins/httpapi/dcnm.py import-2.7!skip -plugins/httpapi/dcnm.py import-3.5!skip -plugins/httpapi/dcnm.py import-3.6!skip -plugins/httpapi/dcnm.py import-3.7!skip -plugins/httpapi/dcnm.py import-3.8!skip -plugins/httpapi/dcnm.py import-3.9!skip -plugins/module_utils/common/sender_requests.py import-3.11 # TODO remove this if/when requests is added to the standard library diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt deleted file mode 100644 index fb7f3a6aa..000000000 --- a/tests/sanity/ignore-2.12.txt +++ /dev/null @@ -1,26 +0,0 @@ -plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py import-2.6!skip -plugins/modules/dcnm_rest.py import-2.7!skip -plugins/httpapi/dcnm.py import-3.8!skip -plugins/httpapi/dcnm.py import-3.9!skip -plugins/httpapi/dcnm.py import-3.10!skip -plugins/module_utils/common/sender_requests.py import-3.11 # TODO remove this if/when requests is added to the standard library diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt deleted file mode 100644 index 58baaeba7..000000000 --- a/tests/sanity/ignore-2.13.txt +++ /dev/null @@ -1,26 +0,0 @@ -plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py import-2.7!skip -plugins/httpapi/dcnm.py import-3.8!skip -plugins/httpapi/dcnm.py import-3.9!skip -plugins/httpapi/dcnm.py import-3.10!skip -plugins/module_utils/common/sender_requests.py import-3.11 # TODO remove this if/when requests is added to the standard library diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt deleted file mode 100644 index 04bf43559..000000000 --- a/tests/sanity/ignore-2.14.txt +++ /dev/null @@ -1,25 +0,0 @@ -plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py import-2.7!skip -plugins/httpapi/dcnm.py import-3.9!skip -plugins/httpapi/dcnm.py import-3.10!skip -plugins/module_utils/common/sender_requests.py import-3.11 # TODO remove this if/when requests is added to the standard library diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index 9950a8963..717617d91 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -1,27 +1,106 @@ -plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/httpapi/dcnm.py import-3.9!skip +plugins/action/tests/unit/ndfc_pc_members_validate.py action-plugin-docs plugins/httpapi/dcnm.py import-3.10!skip -plugins/module_utils/common/sender_requests.py import-3.9 # TODO remove this if/when requests is added to the standard library -plugins/module_utils/common/sender_requests.py import-3.10 # TODO remove this if/when requests is added to the standard library -plugins/module_utils/common/sender_requests.py import-3.11 # TODO remove this if/when requests is added to the standard library -plugins/action/tests/unit/ndfc_pc_members_validate.py action-plugin-docs # action plugin has no matching module to provide documentation +plugins/httpapi/dcnm.py import-3.9!skip +plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.10!skip +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.11!skip +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.9!skip +plugins/module_utils/common/models/ipv4_host.py import-3.10!skip +plugins/module_utils/common/models/ipv4_host.py import-3.11!skip +plugins/module_utils/common/models/ipv4_host.py import-3.9!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.10!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.11!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.9!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.10!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.11!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.9!skip +plugins/module_utils/common/models/ipv6_host.py import-3.10!skip +plugins/module_utils/common/models/ipv6_host.py import-3.11!skip +plugins/module_utils/common/models/ipv6_host.py import-3.9!skip +plugins/module_utils/common/sender_requests.py import-3.10 +plugins/module_utils/common/sender_requests.py import-3.11 +plugins/module_utils/common/sender_requests.py import-3.9 +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.10!skip +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.11!skip +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.9!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.10!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.11!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.9!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.10!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.11!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.9!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.10!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.11!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.9!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.10!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.11!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.9!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.10!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.11!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.9!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.10!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.11!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.9!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.10!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.11!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.9!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.10!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.11!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.9!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.9!skip +plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vrf_v2.py validate-modules:missing-gplv3-license diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index f573fb4d6..7ff8e0294 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -1,24 +1,103 @@ -plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/module_utils/common/sender_requests.py import-3.10 # TODO remove this if/when requests is added to the standard library -plugins/module_utils/common/sender_requests.py import-3.11 # TODO remove this if/when requests is added to the standard library plugins/action/tests/unit/ndfc_pc_members_validate.py action-plugin-docs # action plugin has no matching module to provide documentation +plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.10!skip +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.11!skip +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.9!skip +plugins/module_utils/common/models/ipv4_host.py import-3.10!skip +plugins/module_utils/common/models/ipv4_host.py import-3.11!skip +plugins/module_utils/common/models/ipv4_host.py import-3.9!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.10!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.11!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.9!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.10!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.11!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.9!skip +plugins/module_utils/common/models/ipv6_host.py import-3.10!skip +plugins/module_utils/common/models/ipv6_host.py import-3.11!skip +plugins/module_utils/common/models/ipv6_host.py import-3.9!skip +plugins/module_utils/common/sender_requests.py import-3.10 +plugins/module_utils/common/sender_requests.py import-3.11 +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.10!skip +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.11!skip +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.9!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.10!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.11!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.9!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.10!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.11!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.9!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.10!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.11!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.9!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.10!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.11!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.9!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.10!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.11!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.9!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.10!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.11!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.9!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.10!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.11!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.9!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.10!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.11!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.9!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.9!skip +plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vrf_v2.py validate-modules:missing-gplv3-license diff --git a/tests/sanity/ignore-2.17.txt b/tests/sanity/ignore-2.17.txt new file mode 100644 index 000000000..1bd8882d0 --- /dev/null +++ b/tests/sanity/ignore-2.17.txt @@ -0,0 +1,103 @@ +plugins/action/tests/unit/ndfc_pc_members_validate.py action-plugin-docs +plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.10!skip +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.11!skip +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.9!skip +plugins/module_utils/common/models/ipv4_host.py import-3.10!skip +plugins/module_utils/common/models/ipv4_host.py import-3.11!skip +plugins/module_utils/common/models/ipv4_host.py import-3.9!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.10!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.11!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.9!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.10!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.11!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.9!skip +plugins/module_utils/common/models/ipv6_host.py import-3.10!skip +plugins/module_utils/common/models/ipv6_host.py import-3.11!skip +plugins/module_utils/common/models/ipv6_host.py import-3.9!skip +plugins/module_utils/common/sender_requests.py import-3.10 +plugins/module_utils/common/sender_requests.py import-3.11 +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.10!skip +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.11!skip +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.9!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.10!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.11!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.9!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.10!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.11!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.9!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.10!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.11!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.9!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.10!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.11!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.9!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.10!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.11!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.9!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.10!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.11!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.9!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.10!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.11!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.9!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.10!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.11!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.9!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.9!skip +plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vrf_v2.py validate-modules:missing-gplv3-license diff --git a/tests/sanity/ignore-2.18.txt b/tests/sanity/ignore-2.18.txt new file mode 100644 index 000000000..1bd8882d0 --- /dev/null +++ b/tests/sanity/ignore-2.18.txt @@ -0,0 +1,103 @@ +plugins/action/tests/unit/ndfc_pc_members_validate.py action-plugin-docs +plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.10!skip +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.11!skip +plugins/module_utils/common/models/ipv4_cidr_host.py import-3.9!skip +plugins/module_utils/common/models/ipv4_host.py import-3.10!skip +plugins/module_utils/common/models/ipv4_host.py import-3.11!skip +plugins/module_utils/common/models/ipv4_host.py import-3.9!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.10!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.11!skip +plugins/module_utils/common/models/ipv4_multicast_group_address.py import-3.9!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.10!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.11!skip +plugins/module_utils/common/models/ipv6_cidr_host.py import-3.9!skip +plugins/module_utils/common/models/ipv6_host.py import-3.10!skip +plugins/module_utils/common/models/ipv6_host.py import-3.11!skip +plugins/module_utils/common/models/ipv6_host.py import-3.9!skip +plugins/module_utils/common/sender_requests.py import-3.10 +plugins/module_utils/common/sender_requests.py import-3.11 +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.10!skip +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.11!skip +plugins/module_utils/vrf/dcnm_vrf_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_generic_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_get_int.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py import-3.9!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.10!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.11!skip +plugins/module_utils/vrf/model_controller_response_vrfs_v12.py import-3.9!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.10!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.11!skip +plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py import-3.9!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.10!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.11!skip +plugins/module_utils/vrf/model_payload_vrfs_attachments.py import-3.9!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.10!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.11!skip +plugins/module_utils/vrf/model_payload_vrfs_deployments.py import-3.9!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.10!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.11!skip +plugins/module_utils/vrf/model_playbook_vrf_v11.py import-3.9!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.10!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.11!skip +plugins/module_utils/vrf/model_playbook_vrf_v12.py import-3.9!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.10!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.11!skip +plugins/module_utils/vrf/model_vrf_detach_payload_v12.py import-3.9!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.10!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.11!skip +plugins/module_utils/vrf/serial_number_to_vrf_lite.py import-3.9!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.10!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.11!skip +plugins/module_utils/vrf/transmute_diff_attach_to_payload.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_payload_v12.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py import-3.9!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py import-3.9!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.10!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.11!skip +plugins/module_utils/vrf/vrf_template_config_v12.py import-3.9!skip +plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license +plugins/modules/dcnm_vrf_v2.py validate-modules:missing-gplv3-license diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt deleted file mode 100644 index 7fdc2f2cb..000000000 --- a/tests/sanity/ignore-2.9.txt +++ /dev/null @@ -1,23 +0,0 @@ -plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_node.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_bootflash.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_log.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module -plugins/modules/dcnm_rest.py import-2.6!skip -plugins/modules/dcnm_rest.py import-2.7!skip -plugins/module_utils/common/sender_requests.py import-3.11 # TODO remove this if/when requests is added to the standard library diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_top_down_fabrics_vrfs.py b/tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_top_down_fabrics_vrfs.py new file mode 100644 index 000000000..023ff8646 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_top_down_fabrics_vrfs.py @@ -0,0 +1,108 @@ +# Copyright (c) 2024 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. +# pylint: disable=invalid-name +# pylint: disable=missing-docstring +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +# pylint: enable=invalid-name + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.top_down.fabrics.vrfs.vrfs import ( + EpVrfPost) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.enums.http_requests import RequestVerb +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + does_not_raise + +PATH_PREFIX = "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics" +FABRIC_NAME = "MyFabric" +TICKET_ID = "MyTicket1234" + + +def test_ep_vrf_create_00000(): + """ + ### Class + - EpVrfPost + + ### Summary + - Verify __init__ method + - Correct class_name + - Correct default values + - Correct contents of required_properties + - Correct contents of properties dict + - Properties return values from properties dict + - path property raises ``ValueError`` when accessed, since + ``fabric_name`` is not yet set. + """ + with does_not_raise(): + instance = EpVrfPost() + assert instance.class_name == "EpVrfPost" + assert "fabric_name" in instance.required_properties + assert len(instance.required_properties) == 1 + assert instance.properties["verb"] == RequestVerb.POST + match = r"EpVrfPost.path_fabric_name:\s+" + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_vrf_create_00010(): + """ + ### Class + - EpVrfPost + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpVrfPost() + instance.fabric_name = FABRIC_NAME + assert f"{PATH_PREFIX}/{FABRIC_NAME}/vrfs" in instance.path + assert instance.verb == RequestVerb.POST + + +def test_ep_vrf_create_00050(): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``fabric_name``. + + """ + with does_not_raise(): + instance = EpVrfPost() + match = r"EpVrfPost.path_fabric_name:\s+" + match += r"fabric_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_vrf_create_00060(): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify ``ValueError`` is raised if ``fabric_name`` + is invalid. + """ + fabric_name = "1_InvalidFabricName" + with does_not_raise(): + instance = EpVrfPost() + match = r"EpVrfPost.fabric_name:\s+" + match += r"ConversionUtils\.validate_fabric_name:\s+" + match += rf"Invalid fabric name: {fabric_name}\." + with pytest.raises(ValueError, match=match): + instance.fabric_name = fabric_name # pylint: disable=pointless-statement diff --git a/tests/unit/module_utils/common/models/__init__.py b/tests/unit/module_utils/common/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/module_utils/common/models/test_ipv4_cidr_host.py b/tests/unit/module_utils/common/models/test_ipv4_cidr_host.py new file mode 100755 index 000000000..230dae631 --- /dev/null +++ b/tests/unit/module_utils/common/models/test_ipv4_cidr_host.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +""" +Unit tests for IPv4CidrHostModel +""" +# pylint: disable=line-too-long +# mypy: disable-error-code="import-untyped" +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.models.ipv4_cidr_host import IPv4CidrHostModel + +from ...common.common_utils import does_not_raise + + +@pytest.mark.parametrize( + "address", + [ + ("10.33.0.1"), + ("2001:db8::1"), + (100), + ({}), + (["2001::1/64"]), + ], +) +def test_ipv4_cidr_host_model_00010(address) -> None: + """ + Test IPv4CidrHostModel with invalid input + """ + match = "1 validation error for IPv4CidrHostModel" + with pytest.raises(ValueError) as excinfo: + IPv4CidrHostModel(ipv4_cidr_host=address) + assert match in str(excinfo.value) + + +def test_ipv4_cidr_host_model_00020() -> None: + """ + Test IPv4HostModel with valid input + """ + with does_not_raise(): + IPv4CidrHostModel(ipv4_cidr_host="10.1.1.1/24") diff --git a/tests/unit/module_utils/common/models/test_ipv4_host.py b/tests/unit/module_utils/common/models/test_ipv4_host.py new file mode 100755 index 000000000..bcad2bb89 --- /dev/null +++ b/tests/unit/module_utils/common/models/test_ipv4_host.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +""" +Unit tests for IPv6HostModel +""" +# pylint: disable=line-too-long +# mypy: disable-error-code="import-untyped" +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.models.ipv4_host import \ + IPv4HostModel + +from ...common.common_utils import does_not_raise + + +@pytest.mark.parametrize( + "address", + [ + ("10.33.0.1"), + ("2001:db8::1/64"), + (100), + ({}), + (["2001:db8::1"]), + ], +) +def test_ipv4_host_model_00010(address) -> None: + """ + Test IPv4HostModel with invalid input + """ + match = "1 validation error for IPv4HostModel" + with pytest.raises(ValueError) as excinfo: + IPv4HostModel(ipv6_host=address) + assert match in str(excinfo.value) + + +def test_ipv4_host_model_00020() -> None: + """ + Test IPv4HostModel with valid input + """ + with does_not_raise(): + IPv4HostModel(ipv4_host="10.1.1.1") diff --git a/tests/unit/module_utils/common/models/test_ipv6_cidr_host.py b/tests/unit/module_utils/common/models/test_ipv6_cidr_host.py new file mode 100755 index 000000000..464aa9c76 --- /dev/null +++ b/tests/unit/module_utils/common/models/test_ipv6_cidr_host.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +""" +Unit tests for IPv6HostCidrModel +""" +# pylint: disable=line-too-long +# mypy: disable-error-code="import-untyped" +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.models.ipv6_cidr_host import \ + IPv6CidrHostModel + +from ...common.common_utils import does_not_raise + + +@pytest.mark.parametrize( + "address", + [ + ("10.33.0.1"), + ("2001:db8::1"), + (100), + ({}), + (["2001::1/64"]), + ], +) +def test_ipv6_cidr_host_model_00010(address) -> None: + """ + Test IPv6CidrHostModel with invalid input + """ + match = "1 validation error for IPv6CidrHostModel" + with pytest.raises(ValueError) as excinfo: + IPv6CidrHostModel(ipv6_cidr_host=address) + assert match in str(excinfo.value) + + +def test_ipv6_cidr_host_model_00020() -> None: + """ + Test IPv6HostModel with valid input + """ + with does_not_raise(): + IPv6CidrHostModel(ipv6_cidr_host="2001:db8::1/64") diff --git a/tests/unit/module_utils/common/models/test_ipv6_host.py b/tests/unit/module_utils/common/models/test_ipv6_host.py new file mode 100755 index 000000000..6fde9836e --- /dev/null +++ b/tests/unit/module_utils/common/models/test_ipv6_host.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +""" +Unit tests for IPv6HostModel +""" +# pylint: disable=line-too-long +# mypy: disable-error-code="import-untyped" +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.models.ipv6_host import \ + IPv6HostModel + +from ...common.common_utils import does_not_raise + + +@pytest.mark.parametrize( + "address", + [ + ("10.33.0.1"), + ("2001:db8::1/64"), + (100), + ({}), + (["2001:db8::1"]), + ], +) +def test_ipv6_host_model_00010(address) -> None: + """ + Test IPv6HostModel with invalid input + """ + match = "1 validation error for IPv6HostModel" + with pytest.raises(ValueError) as excinfo: + IPv6HostModel(ipv6_host=address) + assert match in str(excinfo.value) + + +def test_ipv6_host_model_00020() -> None: + """ + Test IPv6HostModel with valid input + """ + with does_not_raise(): + IPv6HostModel(ipv6_host="2001:db8::1") diff --git a/tests/unit/module_utils/vrf/__init__.py b/tests/unit/module_utils/vrf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/module_utils/vrf/fixtures/load_fixture.py b/tests/unit/module_utils/vrf/fixtures/load_fixture.py new file mode 100644 index 000000000..7b561753c --- /dev/null +++ b/tests/unit/module_utils/vrf/fixtures/load_fixture.py @@ -0,0 +1,103 @@ +""" +Load fixtures for VRF module tests. +""" + +from __future__ import absolute_import, division, print_function + +# Copyright (c) 2025 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. +import json +import os +import sys + +# pylint: disable=invalid-name +__metaclass__ = type +__copyright__ = "Copyright (c) 2025 Cisco and/or its affiliates." +__author__ = "Allen Robel" +# pylint: enable=invalid-name + + +fixture_path = os.path.join(os.path.dirname(__file__), "") + + +def load_fixture(filename): + """ + load test inputs from json files + """ + path = os.path.join(fixture_path, f"{filename}") + + try: + with open(path, encoding="utf-8") as file_handle: + data = file_handle.read() + except IOError as exception: + msg = f"Exception opening test input file {filename} : " + msg += f"Exception detail: {exception}" + print(msg) + sys.exit(1) + + try: + fixture = json.loads(data) + except json.JSONDecodeError as exception: + msg = "Exception reading JSON contents in " + msg += f"test input file {filename} : " + msg += f"Exception detail: {exception}" + print(msg) + sys.exit(1) + + return fixture + + +def load_fixture_data(filename: str, key: str) -> dict[str, str]: + """ + Return fixture data associated with key from data_file. + + :param filename: The name of the fixture data file. + :param key: The key to look up in the fixture data. + :return: The data associated with the key. + """ + data = load_fixture(filename).get(key) + print(f"{filename}: {key} : {data}") + return data + + +def payloads_vrfs_attachments(key: str) -> dict[str, str]: + """ + Return VRF payloads. + """ + filename = "model_payload_vrfs_attachments.json" + data = load_fixture_data(filename=filename, key=key) + return data + + +def playbooks(key: str) -> dict[str, str]: + """ + Return VRF playbooks. + """ + filename = "model_playbook_vrf_v12.json" + data = load_fixture_data(filename=filename, key=key) + return data + + +def controller_response_fabrics_easy_fabric_get(key: str) -> dict[str, str]: + """ + Return controller response fixtures for a GET request to the controller + for the following endpoint, where fabricName is a placeholder for the + actual fabric name, and the fabric type is Easy_Fabric. + + - Verb: GET + - Path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{fabricName} + """ + filename = "model_controller_response_fabrics_easy_fabric_get.json" + data = load_fixture_data(filename=filename, key=key) + return data diff --git a/tests/unit/module_utils/vrf/fixtures/model_controller_response_fabrics_easy_fabric_get.json b/tests/unit/module_utils/vrf/fixtures/model_controller_response_fabrics_easy_fabric_get.json new file mode 100644 index 000000000..b5116bd8a --- /dev/null +++ b/tests/unit/module_utils/vrf/fixtures/model_controller_response_fabrics_easy_fabric_get.json @@ -0,0 +1,667 @@ +{ + "fabric_get": { + "asn": "65001", + "createdOn": 1750784465087, + "deviceType": "n9k", + "fabricId": "FABRIC-2", + "fabricName": "f1", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN EVPN", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", + "id": 2, + "modifiedOn": 1750786386652, + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplate": "Default_Network_Universal", + "nvPairs": { + "AAA_REMOTE_IP_ENABLED": "false", + "AAA_SERVER_CONF": "", + "abstract_anycast_rp": "anycast_rp", + "abstract_bgp": "base_bgp", + "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", + "abstract_bgp_rr": "evpn_bgp_rr", + "abstract_dhcp": "base_dhcp", + "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", + "abstract_extra_config_leaf": "extra_config_leaf", + "abstract_extra_config_spine": "extra_config_spine", + "abstract_extra_config_tor": "extra_config_tor", + "abstract_feature_leaf": "base_feature_leaf_upg", + "abstract_feature_spine": "base_feature_spine_upg", + "abstract_isis": "base_isis_level2", + "abstract_isis_interface": "isis_interface", + "abstract_loopback_interface": "int_fabric_loopback_11_1", + "abstract_multicast": "base_multicast_11_1", + "abstract_ospf": "base_ospf", + "abstract_ospf_interface": "ospf_interface_11_1", + "abstract_pim_interface": "pim_interface", + "abstract_route_map": "route_map", + "abstract_routed_host": "int_routed_host", + "abstract_trunk_host": "int_trunk_host", + "abstract_vlan_interface": "int_fabric_vlan_11_1", + "abstract_vpc_domain": "base_vpc_domain_11_1", + "ACTIVE_MIGRATION": "false", + "ADVERTISE_PIP_BGP": "false", + "ADVERTISE_PIP_ON_BORDER": "true", + "AGENT_INTF": "eth0", + "AGG_ACC_VPC_PO_ID_RANGE": "", + "AI_ML_QOS_POLICY": "AI_Fabric_QOS_400G", + "ALLOW_L3VNI_NO_VLAN": "true", + "ALLOW_L3VNI_NO_VLAN_PREV": "true", + "ALLOW_NXC": "true", + "ALLOW_NXC_PREV": "true", + "ANYCAST_BGW_ADVERTISE_PIP": "false", + "ANYCAST_GW_MAC": "2020.0000.00aa", + "ANYCAST_LB_ID": "", + "ANYCAST_RP_IP_RANGE": "", + "ANYCAST_RP_IP_RANGE_INTERNAL": "", + "AUTO_SYMMETRIC_DEFAULT_VRF": "true", + "AUTO_SYMMETRIC_VRF_LITE": "true", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", + "AUTO_VRFLITE_IFC_DEFAULT_VRF": "true", + "BANNER": "", + "BFD_AUTH_ENABLE": "false", + "BFD_AUTH_KEY": "", + "BFD_AUTH_KEY_ID": "", + "BFD_ENABLE": "false", + "BFD_ENABLE_PREV": "false", + "BFD_IBGP_ENABLE": "false", + "BFD_ISIS_ENABLE": "false", + "BFD_OSPF_ENABLE": "false", + "BFD_PIM_ENABLE": "false", + "BGP_AS": "65001", + "BGP_AS_PREV": "65001", + "BGP_AUTH_ENABLE": "false", + "BGP_AUTH_KEY": "", + "BGP_AUTH_KEY_TYPE": "3", + "BGP_LB_ID": "0", + "BOOTSTRAP_CONF": "", + "BOOTSTRAP_ENABLE": "false", + "BOOTSTRAP_ENABLE_PREV": "false", + "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", + "BOOTSTRAP_MULTISUBNET_INTERNAL": "", + "BRFIELD_DEBUG_FLAG": "Disable", + "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", + "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", + "CDP_ENABLE": "false", + "COPP_POLICY": "strict", + "DCI_MACSEC_ALGORITHM": "", + "DCI_MACSEC_CIPHER_SUITE": "", + "DCI_MACSEC_FALLBACK_ALGORITHM": "", + "DCI_MACSEC_FALLBACK_KEY_STRING": "", + "DCI_MACSEC_KEY_STRING": "", + "DCI_SUBNET_RANGE": "10.33.0.0/16", + "DCI_SUBNET_TARGET_MASK": "30", + "dcnmUser": "admin", + "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", + "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", + "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", + "default_network": "Default_Network_Universal", + "default_pvlan_sec_network": "", + "default_vrf": "Default_VRF_Universal", + "DEFAULT_VRF_REDIS_BGP_RMAP": "extcon-rmap-filter", + "DEPLOYMENT_FREEZE": "false", + "DHCP_ENABLE": "false", + "DHCP_END": "", + "DHCP_END_INTERNAL": "", + "DHCP_IPV6_ENABLE": "", + "DHCP_IPV6_ENABLE_INTERNAL": "", + "DHCP_START": "", + "DHCP_START_INTERNAL": "", + "DNS_SERVER_IP_LIST": "", + "DNS_SERVER_VRF": "", + "DOMAIN_NAME_INTERNAL": "", + "ENABLE_AAA": "false", + "ENABLE_AGENT": "false", + "ENABLE_AGG_ACC_ID_RANGE": "false", + "ENABLE_AI_ML_QOS_POLICY": "false", + "ENABLE_AI_ML_QOS_POLICY_FLAP": "false", + "ENABLE_DCI_MACSEC": "false", + "ENABLE_DCI_MACSEC_PREV": "false", + "ENABLE_DEFAULT_QUEUING_POLICY": "false", + "ENABLE_EVPN": "true", + "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", + "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", + "ENABLE_L3VNI_NO_VLAN": "false", + "ENABLE_MACSEC": "false", + "ENABLE_MACSEC_PREV": "false", + "ENABLE_NETFLOW": "false", + "ENABLE_NETFLOW_PREV": "false", + "ENABLE_NGOAM": "true", + "ENABLE_NXAPI": "true", + "ENABLE_NXAPI_HTTP": "true", + "ENABLE_PBR": "false", + "ENABLE_PVLAN": "false", + "ENABLE_PVLAN_PREV": "false", + "ENABLE_QKD": "false", + "ENABLE_RT_INTF_STATS": "false", + "ENABLE_SGT": "false", + "ENABLE_SGT_PREV": "false", + "ENABLE_TENANT_DHCP": "true", + "ENABLE_TRM": "false", + "ENABLE_TRMv6": "false", + "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", + "ENABLE_VRI_ID_REALLOC": "false", + "enableRealTimeBackup": "", + "enableScheduledBackup": "", + "EXT_FABRIC_TYPE": "", + "EXTRA_CONF_INTRA_LINKS": "", + "EXTRA_CONF_LEAF": "", + "EXTRA_CONF_SPINE": "", + "EXTRA_CONF_TOR": "", + "FABRIC_INTERFACE_TYPE": "p2p", + "FABRIC_MTU": "9216", + "FABRIC_MTU_PREV": "9216", + "FABRIC_NAME": "f1", + "FABRIC_TYPE": "Switch_Fabric", + "FABRIC_VPC_DOMAIN_ID": "", + "FABRIC_VPC_DOMAIN_ID_PREV": "", + "FABRIC_VPC_QOS": "false", + "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", + "FEATURE_PTP": "false", + "FEATURE_PTP_INTERNAL": "false", + "FF": "Easy_Fabric", + "GRFIELD_DEBUG_FLAG": "Disable", + "HD_TIME": "180", + "HOST_INTF_ADMIN_STATE": "true", + "IBGP_PEER_TEMPLATE": "", + "IBGP_PEER_TEMPLATE_LEAF": "", + "IGNORE_CERT": "false", + "INBAND_DHCP_SERVERS": "", + "INBAND_MGMT": "false", + "INBAND_MGMT_PREV": "false", + "INTF_STAT_LOAD_INTERVAL": "", + "IPv6_ANYCAST_RP_IP_RANGE": "", + "IPv6_ANYCAST_RP_IP_RANGE_INTERNAL": "", + "IPv6_MULTICAST_GROUP_SUBNET": "", + "ISIS_AREA_NUM": "0001", + "ISIS_AREA_NUM_PREV": "", + "ISIS_AUTH_ENABLE": "false", + "ISIS_AUTH_KEY": "", + "ISIS_AUTH_KEYCHAIN_KEY_ID": "", + "ISIS_AUTH_KEYCHAIN_NAME": "", + "ISIS_LEVEL": "level-2", + "ISIS_OVERLOAD_ELAPSE_TIME": "", + "ISIS_OVERLOAD_ENABLE": "false", + "ISIS_P2P_ENABLE": "false", + "KME_SERVER_IP": "", + "KME_SERVER_PORT": "", + "L2_HOST_INTF_MTU": "9216", + "L2_HOST_INTF_MTU_PREV": "9216", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "L3_PARTITION_ID_RANGE": "50000-59000", + "L3VNI_IPv6_MCAST_GROUP": "", + "L3VNI_MCAST_GROUP": "", + "LINK_STATE_ROUTING": "ospf", + "LINK_STATE_ROUTING_TAG": "UNDERLAY", + "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", + "LOOPBACK0_IP_RANGE": "10.2.0.0/22", + "LOOPBACK0_IPV6_RANGE": "", + "LOOPBACK1_IP_RANGE": "10.3.0.0/22", + "LOOPBACK1_IPV6_RANGE": "", + "MACSEC_ALGORITHM": "", + "MACSEC_CIPHER_SUITE": "", + "MACSEC_FALLBACK_ALGORITHM": "", + "MACSEC_FALLBACK_KEY_STRING": "", + "MACSEC_KEY_STRING": "", + "MACSEC_REPORT_TIMER": "", + "MGMT_GW": "", + "MGMT_GW_INTERNAL": "", + "MGMT_PREFIX": "", + "MGMT_PREFIX_INTERNAL": "", + "MGMT_V6PREFIX": "", + "MGMT_V6PREFIX_INTERNAL": "", + "MPLS_HANDOFF": "false", + "MPLS_ISIS_AREA_NUM": "0001", + "MPLS_ISIS_AREA_NUM_PREV": "", + "MPLS_LB_ID": "", + "MPLS_LOOPBACK_IP_RANGE": "", + "MSO_CONNECTIVITY_DEPLOYED": "", + "MSO_CONTROLER_ID": "", + "MSO_SITE_GROUP_NAME": "", + "MSO_SITE_ID": "", + "MST_INSTANCE_RANGE": "", + "MULTICAST_GROUP_SUBNET": "", + "MVPN_VRI_ID_RANGE": "", + "NETFLOW_EXPORTER_LIST": "", + "NETFLOW_MONITOR_LIST": "", + "NETFLOW_RECORD_LIST": "", + "network_extension_template": "Default_Network_Extension_Universal", + "NETWORK_VLAN_RANGE": "2300-2999", + "NTP_SERVER_IP_LIST": "", + "NTP_SERVER_VRF": "", + "NVE_LB_ID": "1", + "NXAPI_HTTP_PORT": "80", + "NXAPI_HTTPS_PORT": "443", + "NXC_DEST_VRF": "management", + "NXC_PROXY_PORT": "8080", + "NXC_PROXY_SERVER": "", + "NXC_SRC_INTF": "", + "OBJECT_TRACKING_NUMBER_RANGE": "100-299", + "OSPF_AREA_ID": "0.0.0.0", + "OSPF_AUTH_ENABLE": "false", + "OSPF_AUTH_KEY": "", + "OSPF_AUTH_KEY_ID": "", + "OVERLAY_MODE": "cli", + "OVERLAY_MODE_PREV": "cli", + "OVERWRITE_GLOBAL_NXC": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV": "false", + "PER_VRF_LOOPBACK_IP_RANGE": "", + "PER_VRF_LOOPBACK_IP_RANGE_V6": "", + "PFC_WATCH_INT": "", + "PFC_WATCH_INT_PREV": "", + "PHANTOM_RP_LB_ID1": "", + "PHANTOM_RP_LB_ID2": "", + "PHANTOM_RP_LB_ID3": "", + "PHANTOM_RP_LB_ID4": "", + "PIM_HELLO_AUTH_ENABLE": "false", + "PIM_HELLO_AUTH_KEY": "", + "PM_ENABLE": "false", + "PM_ENABLE_PREV": "false", + "PNP_ENABLE_INTERNAL": "", + "POWER_REDUNDANCY_MODE": "ps-redundant", + "PREMSO_PARENT_FABRIC": "", + "PTP_DOMAIN_ID": "", + "PTP_LB_ID": "", + "PTP_VLAN_ID": "", + "QKD_PROFILE_NAME": "", + "QKD_PROFILE_NAME_PREV": "", + "REPLICATION_MODE": "Ingress", + "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", + "ROUTER_ID_RANGE": "", + "RP_COUNT": "2", + "RP_LB_ID": "", + "RP_MODE": "asm", + "RR_COUNT": "2", + "scheduledTime": "", + "SEED_SWITCH_CORE_INTERFACES": "", + "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", + "SGT_ID_RANGE": "", + "SGT_NAME_PREFIX": "", + "SGT_OPER_STATUS": "off", + "SGT_PREPROV_RECALC_STATUS": "empty", + "SGT_PREPROVISION": "false", + "SGT_PREPROVISION_PREV": "false", + "SGT_RECALC_STATUS": "empty", + "SITE_ID": "65001", + "SITE_ID_POLICY_ID": "", + "SLA_ID_RANGE": "10000-19999", + "SNMP_SERVER_HOST_TRAP": "true", + "SPINE_COUNT": "0", + "SPINE_SWITCH_CORE_INTERFACES": "", + "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", + "SSPINE_COUNT": "0", + "STATIC_UNDERLAY_IP_ALLOC": "false", + "STP_BRIDGE_PRIORITY": "", + "STP_ROOT_OPTION": "unmanaged", + "STP_VLAN_RANGE": "", + "STRICT_CC_MODE": "false", + "SUBINTERFACE_RANGE": "2-511", + "SUBNET_RANGE": "10.4.0.0/16", + "SUBNET_TARGET_MASK": "30", + "SYSLOG_SERVER_IP_LIST": "", + "SYSLOG_SERVER_VRF": "", + "SYSLOG_SEV": "", + "TCAM_ALLOCATION": "true", + "temp_anycast_gateway": "anycast_gateway", + "temp_vpc_domain_mgmt": "vpc_domain_mgmt", + "temp_vpc_peer_link": "int_vpc_peer_link_po", + "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", + "TRUSTPOINT_LABEL": "", + "UNDERLAY_IS_V6": "false", + "UNDERLAY_IS_V6_PREV": "false", + "UNNUM_BOOTSTRAP_LB_ID": "", + "UNNUM_DHCP_END": "", + "UNNUM_DHCP_END_INTERNAL": "", + "UNNUM_DHCP_START": "", + "UNNUM_DHCP_START_INTERNAL": "", + "UPGRADE_FROM_VERSION": "", + "USE_LINK_LOCAL": "false", + "V6_SUBNET_RANGE": "", + "V6_SUBNET_TARGET_MASK": "126", + "VPC_AUTO_RECOVERY_TIME": "360", + "VPC_DELAY_RESTORE": "150", + "VPC_DELAY_RESTORE_TIME": "60", + "VPC_DOMAIN_ID_RANGE": "1-1000", + "VPC_ENABLE_IPv6_ND_SYNC": "true", + "VPC_PEER_KEEP_ALIVE_OPTION": "management", + "VPC_PEER_LINK_PO": "500", + "VPC_PEER_LINK_VLAN": "3600", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "VRF_LITE_AUTOCONFIG": "Back2Back&ToExternal", + "VRF_VLAN_RANGE": "2000-2299" + }, + "operStatus": "MINOR", + "provisionMode": "DCNMTopDown", + "replicationMode": "Ingress", + "siteId": "65001", + "templateFabricType": "Data Center VXLAN EVPN", + "templateName": "Easy_Fabric", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplate": "Default_VRF_Universal" + }, + "fabric_get_nvpairs": { + "AAA_REMOTE_IP_ENABLED": "false", + "AAA_SERVER_CONF": "", + "abstract_anycast_rp": "anycast_rp", + "abstract_bgp": "base_bgp", + "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", + "abstract_bgp_rr": "evpn_bgp_rr", + "abstract_dhcp": "base_dhcp", + "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", + "abstract_extra_config_leaf": "extra_config_leaf", + "abstract_extra_config_spine": "extra_config_spine", + "abstract_extra_config_tor": "extra_config_tor", + "abstract_feature_leaf": "base_feature_leaf_upg", + "abstract_feature_spine": "base_feature_spine_upg", + "abstract_isis": "base_isis_level2", + "abstract_isis_interface": "isis_interface", + "abstract_loopback_interface": "int_fabric_loopback_11_1", + "abstract_multicast": "base_multicast_11_1", + "abstract_ospf": "base_ospf", + "abstract_ospf_interface": "ospf_interface_11_1", + "abstract_pim_interface": "pim_interface", + "abstract_route_map": "route_map", + "abstract_routed_host": "int_routed_host", + "abstract_trunk_host": "int_trunk_host", + "abstract_vlan_interface": "int_fabric_vlan_11_1", + "abstract_vpc_domain": "base_vpc_domain_11_1", + "ACTIVE_MIGRATION": "false", + "ADVERTISE_PIP_BGP": "false", + "ADVERTISE_PIP_ON_BORDER": "true", + "AGENT_INTF": "eth0", + "AGG_ACC_VPC_PO_ID_RANGE": "", + "AI_ML_QOS_POLICY": "AI_Fabric_QOS_400G", + "ALLOW_L3VNI_NO_VLAN": "true", + "ALLOW_L3VNI_NO_VLAN_PREV": "true", + "ALLOW_NXC": "true", + "ALLOW_NXC_PREV": "true", + "ANYCAST_BGW_ADVERTISE_PIP": "false", + "ANYCAST_GW_MAC": "2020.0000.00aa", + "ANYCAST_LB_ID": "", + "ANYCAST_RP_IP_RANGE": "", + "ANYCAST_RP_IP_RANGE_INTERNAL": "", + "AUTO_SYMMETRIC_DEFAULT_VRF": "true", + "AUTO_SYMMETRIC_VRF_LITE": "true", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", + "AUTO_VRFLITE_IFC_DEFAULT_VRF": "true", + "BANNER": "", + "BFD_AUTH_ENABLE": "false", + "BFD_AUTH_KEY": "", + "BFD_AUTH_KEY_ID": "", + "BFD_ENABLE": "false", + "BFD_ENABLE_PREV": "false", + "BFD_IBGP_ENABLE": "false", + "BFD_ISIS_ENABLE": "false", + "BFD_OSPF_ENABLE": "false", + "BFD_PIM_ENABLE": "false", + "BGP_AS": "65001", + "BGP_AS_PREV": "65001", + "BGP_AUTH_ENABLE": "false", + "BGP_AUTH_KEY": "", + "BGP_AUTH_KEY_TYPE": "3", + "BGP_LB_ID": "0", + "BOOTSTRAP_CONF": "", + "BOOTSTRAP_ENABLE": "false", + "BOOTSTRAP_ENABLE_PREV": "false", + "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", + "BOOTSTRAP_MULTISUBNET_INTERNAL": "", + "BRFIELD_DEBUG_FLAG": "Disable", + "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", + "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", + "CDP_ENABLE": "false", + "COPP_POLICY": "strict", + "DCI_MACSEC_ALGORITHM": "", + "DCI_MACSEC_CIPHER_SUITE": "", + "DCI_MACSEC_FALLBACK_ALGORITHM": "", + "DCI_MACSEC_FALLBACK_KEY_STRING": "", + "DCI_MACSEC_KEY_STRING": "", + "DCI_SUBNET_RANGE": "10.33.0.0/16", + "DCI_SUBNET_TARGET_MASK": "30", + "dcnmUser": "admin", + "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", + "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", + "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", + "default_network": "Default_Network_Universal", + "default_pvlan_sec_network": "", + "default_vrf": "Default_VRF_Universal", + "DEFAULT_VRF_REDIS_BGP_RMAP": "extcon-rmap-filter", + "DEPLOYMENT_FREEZE": "false", + "DHCP_ENABLE": "false", + "DHCP_END": "", + "DHCP_END_INTERNAL": "", + "DHCP_IPV6_ENABLE": "", + "DHCP_IPV6_ENABLE_INTERNAL": "", + "DHCP_START": "", + "DHCP_START_INTERNAL": "", + "DNS_SERVER_IP_LIST": "", + "DNS_SERVER_VRF": "", + "DOMAIN_NAME_INTERNAL": "", + "ENABLE_AAA": "false", + "ENABLE_AGENT": "false", + "ENABLE_AGG_ACC_ID_RANGE": "false", + "ENABLE_AI_ML_QOS_POLICY": "false", + "ENABLE_AI_ML_QOS_POLICY_FLAP": "false", + "ENABLE_DCI_MACSEC": "false", + "ENABLE_DCI_MACSEC_PREV": "false", + "ENABLE_DEFAULT_QUEUING_POLICY": "false", + "ENABLE_EVPN": "true", + "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", + "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", + "ENABLE_L3VNI_NO_VLAN": "false", + "ENABLE_MACSEC": "false", + "ENABLE_MACSEC_PREV": "false", + "ENABLE_NETFLOW": "false", + "ENABLE_NETFLOW_PREV": "false", + "ENABLE_NGOAM": "true", + "ENABLE_NXAPI": "true", + "ENABLE_NXAPI_HTTP": "true", + "ENABLE_PBR": "false", + "ENABLE_PVLAN": "false", + "ENABLE_PVLAN_PREV": "false", + "ENABLE_QKD": "false", + "ENABLE_RT_INTF_STATS": "false", + "ENABLE_SGT": "false", + "ENABLE_SGT_PREV": "false", + "ENABLE_TENANT_DHCP": "true", + "ENABLE_TRM": "false", + "ENABLE_TRMv6": "false", + "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", + "ENABLE_VRI_ID_REALLOC": "false", + "enableRealTimeBackup": "", + "enableScheduledBackup": "", + "EXT_FABRIC_TYPE": "", + "EXTRA_CONF_INTRA_LINKS": "", + "EXTRA_CONF_LEAF": "", + "EXTRA_CONF_SPINE": "", + "EXTRA_CONF_TOR": "", + "FABRIC_INTERFACE_TYPE": "p2p", + "FABRIC_MTU": "9216", + "FABRIC_MTU_PREV": "9216", + "FABRIC_NAME": "f1", + "FABRIC_TYPE": "Switch_Fabric", + "FABRIC_VPC_DOMAIN_ID": "", + "FABRIC_VPC_DOMAIN_ID_PREV": "", + "FABRIC_VPC_QOS": "false", + "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", + "FEATURE_PTP": "false", + "FEATURE_PTP_INTERNAL": "false", + "FF": "Easy_Fabric", + "GRFIELD_DEBUG_FLAG": "Disable", + "HD_TIME": "180", + "HOST_INTF_ADMIN_STATE": "true", + "IBGP_PEER_TEMPLATE": "", + "IBGP_PEER_TEMPLATE_LEAF": "", + "IGNORE_CERT": "false", + "INBAND_DHCP_SERVERS": "", + "INBAND_MGMT": "false", + "INBAND_MGMT_PREV": "false", + "INTF_STAT_LOAD_INTERVAL": "", + "IPv6_ANYCAST_RP_IP_RANGE": "", + "IPv6_ANYCAST_RP_IP_RANGE_INTERNAL": "", + "IPv6_MULTICAST_GROUP_SUBNET": "", + "ISIS_AREA_NUM": "0001", + "ISIS_AREA_NUM_PREV": "", + "ISIS_AUTH_ENABLE": "false", + "ISIS_AUTH_KEY": "", + "ISIS_AUTH_KEYCHAIN_KEY_ID": "", + "ISIS_AUTH_KEYCHAIN_NAME": "", + "ISIS_LEVEL": "level-2", + "ISIS_OVERLOAD_ELAPSE_TIME": "", + "ISIS_OVERLOAD_ENABLE": "false", + "ISIS_P2P_ENABLE": "false", + "KME_SERVER_IP": "", + "KME_SERVER_PORT": "", + "L2_HOST_INTF_MTU": "9216", + "L2_HOST_INTF_MTU_PREV": "9216", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "L3_PARTITION_ID_RANGE": "50000-59000", + "L3VNI_IPv6_MCAST_GROUP": "", + "L3VNI_MCAST_GROUP": "", + "LINK_STATE_ROUTING": "ospf", + "LINK_STATE_ROUTING_TAG": "UNDERLAY", + "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", + "LOOPBACK0_IP_RANGE": "10.2.0.0/22", + "LOOPBACK0_IPV6_RANGE": "", + "LOOPBACK1_IP_RANGE": "10.3.0.0/22", + "LOOPBACK1_IPV6_RANGE": "", + "MACSEC_ALGORITHM": "", + "MACSEC_CIPHER_SUITE": "", + "MACSEC_FALLBACK_ALGORITHM": "", + "MACSEC_FALLBACK_KEY_STRING": "", + "MACSEC_KEY_STRING": "", + "MACSEC_REPORT_TIMER": "", + "MGMT_GW": "", + "MGMT_GW_INTERNAL": "", + "MGMT_PREFIX": "", + "MGMT_PREFIX_INTERNAL": "", + "MGMT_V6PREFIX": "", + "MGMT_V6PREFIX_INTERNAL": "", + "MPLS_HANDOFF": "false", + "MPLS_ISIS_AREA_NUM": "0001", + "MPLS_ISIS_AREA_NUM_PREV": "", + "MPLS_LB_ID": "", + "MPLS_LOOPBACK_IP_RANGE": "", + "MSO_CONNECTIVITY_DEPLOYED": "", + "MSO_CONTROLER_ID": "", + "MSO_SITE_GROUP_NAME": "", + "MSO_SITE_ID": "", + "MST_INSTANCE_RANGE": "", + "MULTICAST_GROUP_SUBNET": "", + "MVPN_VRI_ID_RANGE": "", + "NETFLOW_EXPORTER_LIST": "", + "NETFLOW_MONITOR_LIST": "", + "NETFLOW_RECORD_LIST": "", + "network_extension_template": "Default_Network_Extension_Universal", + "NETWORK_VLAN_RANGE": "2300-2999", + "NTP_SERVER_IP_LIST": "", + "NTP_SERVER_VRF": "", + "NVE_LB_ID": "1", + "NXAPI_HTTP_PORT": "80", + "NXAPI_HTTPS_PORT": "443", + "NXC_DEST_VRF": "management", + "NXC_PROXY_PORT": "8080", + "NXC_PROXY_SERVER": "", + "NXC_SRC_INTF": "", + "OBJECT_TRACKING_NUMBER_RANGE": "100-299", + "OSPF_AREA_ID": "0.0.0.0", + "OSPF_AUTH_ENABLE": "false", + "OSPF_AUTH_KEY": "", + "OSPF_AUTH_KEY_ID": "", + "OVERLAY_MODE": "cli", + "OVERLAY_MODE_PREV": "cli", + "OVERWRITE_GLOBAL_NXC": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV": "false", + "PER_VRF_LOOPBACK_IP_RANGE": "", + "PER_VRF_LOOPBACK_IP_RANGE_V6": "", + "PFC_WATCH_INT": "", + "PFC_WATCH_INT_PREV": "", + "PHANTOM_RP_LB_ID1": "", + "PHANTOM_RP_LB_ID2": "", + "PHANTOM_RP_LB_ID3": "", + "PHANTOM_RP_LB_ID4": "", + "PIM_HELLO_AUTH_ENABLE": "false", + "PIM_HELLO_AUTH_KEY": "", + "PM_ENABLE": "false", + "PM_ENABLE_PREV": "false", + "PNP_ENABLE_INTERNAL": "", + "POWER_REDUNDANCY_MODE": "ps-redundant", + "PREMSO_PARENT_FABRIC": "", + "PTP_DOMAIN_ID": "", + "PTP_LB_ID": "", + "PTP_VLAN_ID": "", + "QKD_PROFILE_NAME": "", + "QKD_PROFILE_NAME_PREV": "", + "REPLICATION_MODE": "Ingress", + "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", + "ROUTER_ID_RANGE": "", + "RP_COUNT": "2", + "RP_LB_ID": "", + "RP_MODE": "asm", + "RR_COUNT": "2", + "scheduledTime": "", + "SEED_SWITCH_CORE_INTERFACES": "", + "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", + "SGT_ID_RANGE": "", + "SGT_NAME_PREFIX": "", + "SGT_OPER_STATUS": "off", + "SGT_PREPROV_RECALC_STATUS": "empty", + "SGT_PREPROVISION": "false", + "SGT_PREPROVISION_PREV": "false", + "SGT_RECALC_STATUS": "empty", + "SITE_ID": "65001", + "SITE_ID_POLICY_ID": "", + "SLA_ID_RANGE": "10000-19999", + "SNMP_SERVER_HOST_TRAP": "true", + "SPINE_COUNT": "0", + "SPINE_SWITCH_CORE_INTERFACES": "", + "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", + "SSPINE_COUNT": "0", + "STATIC_UNDERLAY_IP_ALLOC": "false", + "STP_BRIDGE_PRIORITY": "", + "STP_ROOT_OPTION": "unmanaged", + "STP_VLAN_RANGE": "", + "STRICT_CC_MODE": "false", + "SUBINTERFACE_RANGE": "2-511", + "SUBNET_RANGE": "10.4.0.0/16", + "SUBNET_TARGET_MASK": "30", + "SYSLOG_SERVER_IP_LIST": "", + "SYSLOG_SERVER_VRF": "", + "SYSLOG_SEV": "", + "TCAM_ALLOCATION": "true", + "temp_anycast_gateway": "anycast_gateway", + "temp_vpc_domain_mgmt": "vpc_domain_mgmt", + "temp_vpc_peer_link": "int_vpc_peer_link_po", + "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", + "TRUSTPOINT_LABEL": "", + "UNDERLAY_IS_V6": "false", + "UNDERLAY_IS_V6_PREV": "false", + "UNNUM_BOOTSTRAP_LB_ID": "", + "UNNUM_DHCP_END": "", + "UNNUM_DHCP_END_INTERNAL": "", + "UNNUM_DHCP_START": "", + "UNNUM_DHCP_START_INTERNAL": "", + "UPGRADE_FROM_VERSION": "", + "USE_LINK_LOCAL": "false", + "V6_SUBNET_RANGE": "", + "V6_SUBNET_TARGET_MASK": "126", + "VPC_AUTO_RECOVERY_TIME": "360", + "VPC_DELAY_RESTORE": "150", + "VPC_DELAY_RESTORE_TIME": "60", + "VPC_DOMAIN_ID_RANGE": "1-1000", + "VPC_ENABLE_IPv6_ND_SYNC": "true", + "VPC_PEER_KEEP_ALIVE_OPTION": "management", + "VPC_PEER_LINK_PO": "500", + "VPC_PEER_LINK_VLAN": "3600", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "VRF_LITE_AUTOCONFIG": "Back2Back&ToExternal", + "VRF_VLAN_RANGE": "2000-2299" + } +} \ No newline at end of file diff --git a/tests/unit/module_utils/vrf/fixtures/model_payload_vrfs_attachments.json b/tests/unit/module_utils/vrf/fixtures/model_payload_vrfs_attachments.json new file mode 100644 index 000000000..82e6922d1 --- /dev/null +++ b/tests/unit/module_utils/vrf/fixtures/model_payload_vrfs_attachments.json @@ -0,0 +1,20 @@ +{ + "payload_full": { + "TEST_NOTES": [ + "instanceValues is serialized by PayloadVrfsAttachmentsLanAttachListItem.model_dump() into a JSON string" + ], + "lanAttachList": [ + { + "deployment": true, + "extensionValues": {}, + "fabric": "test_fabric", + "freeformConfig": "", + "instanceValues": {"loopbackIpV6Address": "", "loopbackId": "", "deviceSupportL3VniNoVlan": "false", "switchRouteTargetImportEvpn": "", "loopbackIpAddress": "", "switchRouteTargetExportEvpn": ""}, + "serialNumber": "01234567891", + "vlan": 0, + "vrfName": "test_vrf" + } + ], + "vrfName": "test_vrf" + } +} \ No newline at end of file diff --git a/tests/unit/module_utils/vrf/fixtures/model_playbook_vrf_v12.json b/tests/unit/module_utils/vrf/fixtures/model_playbook_vrf_v12.json new file mode 100644 index 000000000..286a15bd3 --- /dev/null +++ b/tests/unit/module_utils/vrf/fixtures/model_playbook_vrf_v12.json @@ -0,0 +1,166 @@ +{ + "playbook_as_dict": { + "adv_default_routes": true, + "adv_host_routes": false, + "attach": [ + { + "deploy": true, + "export_evpn_rt": "", + "import_evpn_rt": "", + "ip_address": "172.22.150.112", + "vrf_lite": null + }, + { + "deploy": true, + "export_evpn_rt": "", + "import_evpn_rt": "", + "ip_address": "172.22.150.113", + "vrf_lite": [ + { + "dot1q": "2", + "interface": "Ethernet2/10", + "ipv4_addr": "10.33.0.2/30", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv4": "10.33.0.1", + "neighbor_ipv6": "2010::10:34:0:3", + "peer_vrf": "ansible-vrf-int1" + } + ] + } + ], + "bgp_passwd_encrypt": 3, + "bgp_password": "", + "deploy": true, + "disable_rt_auto": false, + "export_evpn_rt": "", + "export_mvpn_rt": "", + "export_vpn_rt": "", + "import_evpn_rt": "", + "import_mvpn_rt": "", + "import_vpn_rt": "", + "ipv6_linklocal_enable": true, + "loopback_route_tag": 12345, + "max_bgp_paths": 1, + "max_ibgp_paths": 2, + "netflow_enable": false, + "nf_monitor": "", + "no_rp": false, + "overlay_mcast_group": "", + "redist_direct_rmap": "FABRIC-RMAP-REDIST-SUBNET", + "rp_address": "", + "rp_external": false, + "rp_loopback_id": "", + "service_vrf_template": null, + "source": null, + "static_default_route": true, + "trm_bgw_msite": false, + "trm_enable": false, + "underlay_mcast_ip": "", + "vlan_id": 500, + "vrf_description": "", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vrf_id": 9008011, + "vrf_int_mtu": 9216, + "vrf_intf_desc": "", + "vrf_name": "ansible-vrf-int1", + "vrf_template": "Default_VRF_Universal", + "vrf_vlan_name": "" + }, + "playbook_full_config": { + "config": [ + { + "adv_default_routes": true, + "adv_host_routes": false, + "attach": [ + { + "deploy": true, + "export_evpn_rt": "", + "import_evpn_rt": "", + "ip_address": "172.22.150.112", + "vrf_lite": null + }, + { + "deploy": true, + "export_evpn_rt": "", + "import_evpn_rt": "", + "ip_address": "172.22.150.113", + "vrf_lite": [ + { + "dot1q": "2", + "interface": "Ethernet2/10", + "ipv4_addr": "10.33.0.2/30", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv4": "10.33.0.1", + "neighbor_ipv6": "2010::10:34:0:3", + "peer_vrf": "ansible-vrf-int1" + } + ] + } + ], + "bgp_passwd_encrypt": 3, + "bgp_password": "", + "deploy": true, + "disable_rt_auto": false, + "export_evpn_rt": "", + "export_mvpn_rt": "", + "export_vpn_rt": "", + "import_evpn_rt": "", + "import_mvpn_rt": "", + "import_vpn_rt": "", + "ipv6_linklocal_enable": true, + "loopback_route_tag": 12345, + "max_bgp_paths": 1, + "max_ibgp_paths": 2, + "netflow_enable": false, + "nf_monitor": "", + "no_rp": false, + "overlay_mcast_group": "", + "redist_direct_rmap": "FABRIC-RMAP-REDIST-SUBNET", + "rp_address": "", + "rp_external": false, + "rp_loopback_id": "", + "service_vrf_template": null, + "source": null, + "static_default_route": true, + "trm_bgw_msite": false, + "trm_enable": false, + "underlay_mcast_ip": "", + "vlan_id": 500, + "vrf_description": "", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vrf_id": 9008011, + "vrf_int_mtu": 9216, + "vrf_intf_desc": "", + "vrf_name": "ansible-vrf-int1", + "vrf_template": "Default_VRF_Universal", + "vrf_vlan_name": "" + } + ] + }, + "vrf_lite": { + "dot1q": "2", + "interface": "Ethernet2/10", + "ipv4_addr": "10.33.0.2/30", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv4": "10.33.0.1", + "neighbor_ipv6": "2010::10:34:0:3", + "peer_vrf": "ansible-vrf-int1" + }, + "vrf_attach": { + "deploy": true, + "export_evpn_rt": "", + "import_evpn_rt": "", + "ip_address": "172.22.150.113", + "vrf_lite": [ + { + "dot1q": "2", + "interface": "Ethernet2/10", + "ipv4_addr": "10.33.0.2/30", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv4": "10.33.0.1", + "neighbor_ipv6": "2010::10:34:0:3", + "peer_vrf": "ansible-vrf-int1" + } + ] + } +} \ No newline at end of file diff --git a/tests/unit/module_utils/vrf/test_controller_response_fabrics_easy_fabric_get.py b/tests/unit/module_utils/vrf/test_controller_response_fabrics_easy_fabric_get.py new file mode 100644 index 000000000..26fd863f8 --- /dev/null +++ b/tests/unit/module_utils/vrf/test_controller_response_fabrics_easy_fabric_get.py @@ -0,0 +1,317 @@ +# Copyright (c) 2025 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. +""" +Test cases for ControllerResponseFabricsEasyFabricGet. +""" +from functools import partial + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.vrf.model_controller_response_fabrics_easy_fabric_get import ControllerResponseFabricsEasyFabricGet + +from ..common.common_utils import does_not_raise +from .fixtures.load_fixture import controller_response_fabrics_easy_fabric_get + + +# pylint: disable=too-many-arguments +def base_test_fabric(value, expected, valid: bool, field: str): + """ + Base test function called by other tests to validate the model. + + :param value: Field value to validate. + :param expected: Expected value after model conversion or validation (None for no expectation). + :param valid: Whether the value is valid or not. + :param field: The field in the response to modify. + """ + response = controller_response_fabrics_easy_fabric_get("fabric_get") + if value == "MISSING": + response.pop(field, None) + else: + response[field] = value + + if valid: + with does_not_raise(): + instance = ControllerResponseFabricsEasyFabricGet(**response) + if value != "MISSING": + assert getattr(instance, field) == expected + else: + # Check if field has a default value + field_info = ControllerResponseFabricsEasyFabricGet.model_fields.get(field) + if field_info and field_info.default is not None: + assert getattr(instance, field) == field_info.default + else: + with pytest.raises(ValueError): + ControllerResponseFabricsEasyFabricGet(**response) + + +# pylint: enable=too-many-arguments + +# Create partial functions for common test patterns +base_test_string_field = partial(base_test_fabric) +base_test_int_field = partial(base_test_fabric) + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("65001", "65001", True), # OK, string + ("12345", "12345", True), # OK, string + ("", "", False), # NOK, min_length=1 + ("MISSING", None, False), # NOK, required field + (123, 123, False), # NOK, int (should be string) + ], +) +def test_fabrics_easy_fabric_get_asn(value, expected, valid: bool) -> None: + """ + asn field validation + """ + base_test_string_field(value, expected, valid, field="asn") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + (1750784465087, 1750784465087, True), # OK, int + (0, 0, True), # OK, int + ("123", 123, True), # OK, string is coerced to int + ("MISSING", None, False), # NOK, required field + (None, None, False), # NOK, None not allowed + ], +) +def test_fabrics_easy_fabric_get_created_on(value, expected, valid: bool) -> None: + """ + createdOn field validation + """ + base_test_int_field(value, expected, valid, field="createdOn") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("n9k", "n9k", True), # OK, string + ("n7k", "n7k", True), # OK, string + ("MISSING", None, False), # NOK, required field + ("", "", True), # OK, empty string + (123, 123, False), # NOK, int (should be string) + ], +) +def test_fabrics_easy_fabric_get_device_type(value, expected, valid: bool) -> None: + """ + deviceType field validation + """ + base_test_string_field(value, expected, valid, field="deviceType") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("FABRIC-2", "FABRIC-2", True), # OK, string + ("FABRIC-1", "FABRIC-1", True), # OK, string + ("MISSING", None, False), # NOK, required field + ("", "", True), # OK, empty string + (123, 123, False), # NOK, int (should be string) + ], +) +def test_fabrics_easy_fabric_get_fabric_id(value, expected, valid: bool) -> None: + """ + fabricId field validation + """ + base_test_string_field(value, expected, valid, field="fabricId") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("f1", "f1", True), # OK, string + ("my-fabric", "my-fabric", True), # OK, string + ("MISSING", None, False), # NOK, required field + ("", "", True), # OK, empty string + (123, 123, False), # NOK, int (should be string) + ], +) +def test_fabrics_easy_fabric_get_fabric_name(value, expected, valid: bool) -> None: + """ + fabricName field validation + """ + base_test_string_field(value, expected, valid, field="fabricName") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("VXLANFabric", "VXLANFabric", True), # OK, string + ("LANClassic", "LANClassic", True), # OK, string + ("MISSING", None, False), # NOK, required field + ("", "", True), # OK, empty string + (123, 123, False), # NOK, int (should be string) + ], +) +def test_fabrics_easy_fabric_get_fabric_technology(value, expected, valid: bool) -> None: + """ + fabricTechnology field validation + """ + base_test_string_field(value, expected, valid, field="fabricTechnology") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + (2, 2, True), # OK, int + (100, 100, True), # OK, int + ("2", 2, True), # OK, string is coerced to int + ("MISSING", None, False), # NOK, required field + (None, None, False), # NOK, None not allowed + ], +) +def test_fabrics_easy_fabric_get_id(value, expected, valid: bool) -> None: + """ + id field validation + """ + base_test_int_field(value, expected, valid, field="id") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("Default_Network_Extension_Universal", "Default_Network_Extension_Universal", True), # OK, default value + ("Custom_Network_Extension", "Custom_Network_Extension", True), # OK, custom value + ("MISSING", "Default_Network_Extension_Universal", True), # OK, uses default + ("", "", True), # OK, empty string + ], +) +def test_fabrics_easy_fabric_get_network_extension_template(value, expected, valid: bool) -> None: + """ + networkExtensionTemplate field validation + """ + base_test_string_field(value, expected, valid, field="networkExtensionTemplate") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("Default_Network_Universal", "Default_Network_Universal", True), # OK, default value + ("Custom_Network", "Custom_Network", True), # OK, custom value + ("MISSING", "Default_Network_Universal", True), # OK, uses default + ("", "", True), # OK, empty string + ], +) +def test_fabrics_easy_fabric_get_network_template(value, expected, valid: bool) -> None: + """ + networkTemplate field validation + """ + base_test_string_field(value, expected, valid, field="networkTemplate") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("MINOR", "MINOR", True), # OK, string + ("MAJOR", "MAJOR", True), # OK, string + ("MISSING", None, False), # NOK, required field + ("", "", True), # OK, empty string + (123, 123, False), # NOK, int (should be string) + ], +) +def test_fabrics_easy_fabric_get_oper_status(value, expected, valid: bool) -> None: + """ + operStatus field validation + """ + base_test_string_field(value, expected, valid, field="operStatus") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("DCNMTopDown", "DCNMTopDown", True), # OK, string + ("External", "External", True), # OK, string + ("MISSING", None, False), # NOK, required field + ("", "", True), # OK, empty string + (123, 123, False), # NOK, int (should be string) + ], +) +def test_fabrics_easy_fabric_get_provision_mode(value, expected, valid: bool) -> None: + """ + provisionMode field validation + """ + base_test_string_field(value, expected, valid, field="provisionMode") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("Default_VRF_Extension_Universal", "Default_VRF_Extension_Universal", True), # OK, default value + ("Custom_VRF_Extension", "Custom_VRF_Extension", True), # OK, custom value + ("MISSING", "Default_VRF_Extension_Universal", True), # OK, uses default + ("", "", False), # NOK, min_length=1 + ], +) +def test_fabrics_easy_fabric_get_vrf_extension_template(value, expected, valid: bool) -> None: + """ + vrfExtensionTemplate field validation + """ + base_test_string_field(value, expected, valid, field="vrfExtensionTemplate") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("Default_VRF_Universal", "Default_VRF_Universal", True), # OK, default value + ("Custom_VRF", "Custom_VRF", True), # OK, custom value + ("MISSING", "Default_VRF_Universal", True), # OK, uses default + ("", "", False), # NOK, min_length=1 + ], +) +def test_fabrics_easy_fabric_get_vrf_template(value, expected, valid: bool) -> None: + """ + vrfTemplate field validation + """ + base_test_string_field(value, expected, valid, field="vrfTemplate") + + +def test_fabrics_easy_fabric_get_full_response() -> None: + """ + Test ControllerResponseFabricsEasyFabricGet with full controller response. + """ + response = controller_response_fabrics_easy_fabric_get("fabric_get") + with does_not_raise(): + instance: ControllerResponseFabricsEasyFabricGet = ControllerResponseFabricsEasyFabricGet(**response) + # Verify some key fields are populated correctly + assert instance.asn == "65001" + assert instance.fabricName == "f1" + assert instance.deviceType == "n9k" + assert instance.id == 2 + assert instance.operStatus == "MINOR" + assert instance.nvPairs is not None + assert instance.nvPairs.BGP_AS == "65001" # pylint: disable=no-member + + +def test_fabrics_easy_fabric_get_missing_nvpairs() -> None: + """ + Test ControllerResponseFabricsEasyFabricGet fails when nvPairs is missing. + """ + response = controller_response_fabrics_easy_fabric_get("fabric_get") + response.pop("nvPairs", None) + + with pytest.raises(ValueError): + ControllerResponseFabricsEasyFabricGet(**response) + + +def test_fabrics_easy_fabric_get_invalid_nvpairs() -> None: + """ + Test ControllerResponseFabricsEasyFabricGet fails when nvPairs is invalid. + """ + response = controller_response_fabrics_easy_fabric_get("fabric_get") + response["nvPairs"] = "invalid" + + with pytest.raises(ValueError): + ControllerResponseFabricsEasyFabricGet(**response) diff --git a/tests/unit/module_utils/vrf/test_controller_response_fabrics_easy_fabric_get_nv_pairs.py b/tests/unit/module_utils/vrf/test_controller_response_fabrics_easy_fabric_get_nv_pairs.py new file mode 100644 index 000000000..44ae281a9 --- /dev/null +++ b/tests/unit/module_utils/vrf/test_controller_response_fabrics_easy_fabric_get_nv_pairs.py @@ -0,0 +1,444 @@ +# Copyright (c) 2025 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. +""" +Test cases for ControllerResponseFabricsEasyFabricGetNvPairs. +""" +from functools import partial + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.vrf.model_controller_response_fabrics_easy_fabric_get import ( + ControllerResponseFabricsEasyFabricGetNvPairs, +) + +from ..common.common_utils import does_not_raise +from .fixtures.load_fixture import controller_response_fabrics_easy_fabric_get + + +# pylint: disable=too-many-arguments +def base_test_nvpairs(value, expected, valid: bool, field: str): + """ + Base test function called by other tests to validate the model. + + :param value: Field value to validate. + :param expected: Expected value after model conversion or validation (None for no expectation). + :param valid: Whether the value is valid or not. + :param field: The field in the response to modify. + """ + response = controller_response_fabrics_easy_fabric_get("fabric_get_nvpairs") + if value == "MISSING": + response.pop(field, None) + else: + response[field] = value + + if valid: + with does_not_raise(): + instance = ControllerResponseFabricsEasyFabricGetNvPairs(**response) + if value != "MISSING": + assert getattr(instance, field) == expected + else: + # All fields except BGP_AS and FABRIC_NAME are Optional[str] with default None + if field in ["BGP_AS", "FABRIC_NAME"]: + # These are required fields, so test should fail if missing + assert False, f"Required field {field} should not be missing" + else: + assert getattr(instance, field) is None + else: + with pytest.raises(ValueError): + ControllerResponseFabricsEasyFabricGetNvPairs(**response) + + +# pylint: enable=too-many-arguments + +# Create partial functions for common test patterns +base_test_string_field = partial(base_test_nvpairs) + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("65001", "65001", True), # OK, string + ("12345", "12345", True), # OK, string + ("", "", False), # NOK, min_length=1 + ("MISSING", None, False), # NOK, required field + (123, 123, False), # NOK, int (should be string) + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_bgp_as(value, expected, valid: bool) -> None: + """ + BGP_AS field validation (required field with min_length=1) + """ + base_test_string_field(value, expected, valid, field="BGP_AS") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("f1", "f1", True), # OK, string + ("my-fabric", "my-fabric", True), # OK, string + ("", "", True), # OK, empty string + ("MISSING", None, False), # NOK, required field + (123, 123, False), # NOK, int (should be string) + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_fabric_name(value, expected, valid: bool) -> None: + """ + FABRIC_NAME field validation (required field) + """ + base_test_string_field(value, expected, valid, field="FABRIC_NAME") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("true", "true", True), # OK, string + ("false", "false", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (True, True, False), # NOK, bool (should be string) + (False, False, False), # NOK, bool (should be string) + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_enable_evpn(value, expected, valid: bool) -> None: + """ + ENABLE_EVPN field validation + """ + base_test_string_field(value, expected, valid, field="ENABLE_EVPN") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("9216", "9216", True), # OK, string + ("1500", "1500", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (9216, 9216, False), # NOK, int (should be string) + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_fabric_mtu(value, expected, valid: bool) -> None: + """ + FABRIC_MTU field validation + """ + base_test_string_field(value, expected, valid, field="FABRIC_MTU") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("10.2.0.0/22", "10.2.0.0/22", True), # OK, string + ("192.168.1.0/24", "192.168.1.0/24", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_loopback0_ip_range(value, expected, valid: bool) -> None: + """ + LOOPBACK0_IP_RANGE field validation + """ + base_test_string_field(value, expected, valid, field="LOOPBACK0_IP_RANGE") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("ospf", "ospf", True), # OK, string + ("isis", "isis", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_link_state_routing(value, expected, valid: bool) -> None: + """ + LINK_STATE_ROUTING field validation + """ + base_test_string_field(value, expected, valid, field="LINK_STATE_ROUTING") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("2020.0000.00aa", "2020.0000.00aa", True), # OK, string + ("0000.1111.2222", "0000.1111.2222", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_anycast_gw_mac(value, expected, valid: bool) -> None: + """ + ANYCAST_GW_MAC field validation + """ + base_test_string_field(value, expected, valid, field="ANYCAST_GW_MAC") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("2000-2299", "2000-2299", True), # OK, string + ("100-200", "100-200", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_vrf_vlan_range(value, expected, valid: bool) -> None: + """ + VRF_VLAN_RANGE field validation + """ + base_test_string_field(value, expected, valid, field="VRF_VLAN_RANGE") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("Default_VRF_Universal", "Default_VRF_Universal", True), # OK, string + ("Custom_VRF_Template", "Custom_VRF_Template", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_default_vrf(value, expected, valid: bool) -> None: + """ + default_vrf field validation + """ + base_test_string_field(value, expected, valid, field="default_vrf") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("admin", "admin", True), # OK, string + ("operator", "operator", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_dcnm_user(value, expected, valid: bool) -> None: + """ + dcnmUser field validation + """ + base_test_string_field(value, expected, valid, field="dcnmUser") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("0.0.0.0", "0.0.0.0", True), # OK, string + ("192.168.1.0", "192.168.1.0", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_ospf_area_id(value, expected, valid: bool) -> None: + """ + OSPF_AREA_ID field validation + """ + base_test_string_field(value, expected, valid, field="OSPF_AREA_ID") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("Ingress", "Ingress", True), # OK, string + ("Multicast", "Multicast", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_replication_mode(value, expected, valid: bool) -> None: + """ + REPLICATION_MODE field validation + """ + base_test_string_field(value, expected, valid, field="REPLICATION_MODE") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("65001", "65001", True), # OK, string + ("65002", "65002", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_site_id(value, expected, valid: bool) -> None: + """ + SITE_ID field validation + """ + base_test_string_field(value, expected, valid, field="SITE_ID") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("10.4.0.0/16", "10.4.0.0/16", True), # OK, string + ("192.168.0.0/24", "192.168.0.0/24", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_subnet_range(value, expected, valid: bool) -> None: + """ + SUBNET_RANGE field validation + """ + base_test_string_field(value, expected, valid, field="SUBNET_RANGE") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("30", "30", True), # OK, string + ("24", "24", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_subnet_target_mask(value, expected, valid: bool) -> None: + """ + SUBNET_TARGET_MASK field validation + """ + base_test_string_field(value, expected, valid, field="SUBNET_TARGET_MASK") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("Default_Network_Extension_Universal", "Default_Network_Extension_Universal", True), # OK, string + ("Custom_Network_Extension", "Custom_Network_Extension", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_network_extension_template(value, expected, valid: bool) -> None: + """ + network_extension_template field validation + """ + base_test_string_field(value, expected, valid, field="network_extension_template") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("Default_VRF_Extension_Universal", "Default_VRF_Extension_Universal", True), # OK, string + ("Custom_VRF_Extension", "Custom_VRF_Extension", True), # OK, string + ("MISSING", None, True), # OK, field can be missing + ("", "", True), # OK, empty string + (None, None, True), # OK, None is valid for optional field + ], +) +def test_fabrics_easy_fabric_get_nv_pairs_vrf_extension_template(value, expected, valid: bool) -> None: + """ + vrf_extension_template field validation + """ + base_test_string_field(value, expected, valid, field="vrf_extension_template") + + +def test_fabrics_easy_fabric_get_nv_pairs_full_response() -> None: + """ + Test ControllerResponseFabricsEasyFabricGetNvPairs with full controller response. + """ + response = controller_response_fabrics_easy_fabric_get("fabric_get_nvpairs") + with does_not_raise(): + instance = ControllerResponseFabricsEasyFabricGetNvPairs(**response) + # Verify some key fields are populated correctly + assert instance.BGP_AS == "65001" + assert instance.FABRIC_NAME == "f1" + assert instance.ENABLE_EVPN == "true" + assert instance.FABRIC_MTU == "9216" + assert instance.LOOPBACK0_IP_RANGE == "10.2.0.0/22" + assert instance.LINK_STATE_ROUTING == "ospf" + assert instance.ANYCAST_GW_MAC == "2020.0000.00aa" + assert instance.VRF_VLAN_RANGE == "2000-2299" + assert instance.default_vrf == "Default_VRF_Universal" + assert instance.dcnmUser == "admin" + + +def test_fabrics_easy_fabric_get_nv_pairs_missing_required_bgp_as() -> None: + """ + Test ControllerResponseFabricsEasyFabricGetNvPairs fails when BGP_AS is missing. + """ + response = controller_response_fabrics_easy_fabric_get("fabric_get_nvpairs") + response.pop("BGP_AS", None) + + with pytest.raises(ValueError): + ControllerResponseFabricsEasyFabricGetNvPairs(**response) + + +def test_fabrics_easy_fabric_get_nv_pairs_missing_required_fabric_name() -> None: + """ + Test ControllerResponseFabricsEasyFabricGetNvPairs fails when FABRIC_NAME is missing. + """ + response = controller_response_fabrics_easy_fabric_get("fabric_get_nvpairs") + response.pop("FABRIC_NAME", None) + + with pytest.raises(ValueError): + ControllerResponseFabricsEasyFabricGetNvPairs(**response) + + +def test_fabrics_easy_fabric_get_nv_pairs_invalid_bgp_as_empty() -> None: + """ + Test ControllerResponseFabricsEasyFabricGetNvPairs fails when BGP_AS is empty (violates min_length=1). + """ + response = controller_response_fabrics_easy_fabric_get("fabric_get_nvpairs") + response["BGP_AS"] = "" + + with pytest.raises(ValueError): + ControllerResponseFabricsEasyFabricGetNvPairs(**response) + + +def test_fabrics_easy_fabric_get_nv_pairs_optional_fields_can_be_none() -> None: + """ + Test that optional fields can be None or missing without causing validation errors. + """ + response = controller_response_fabrics_easy_fabric_get("fabric_get_nvpairs") + + # Remove some optional fields + optional_fields = ["ENABLE_EVPN", "FABRIC_MTU", "LOOPBACK0_IP_RANGE", "LINK_STATE_ROUTING", "ANYCAST_GW_MAC", "VRF_VLAN_RANGE", "default_vrf", "dcnmUser"] + + for field in optional_fields: + response.pop(field, None) + + with does_not_raise(): + instance = ControllerResponseFabricsEasyFabricGetNvPairs(**response) + # Verify optional fields default to None + for field in optional_fields: + assert getattr(instance, field) is None + + +def test_fabrics_easy_fabric_get_nv_pairs_with_minimal_data() -> None: + """ + Test ControllerResponseFabricsEasyFabricGetNvPairs with only required fields. + """ + minimal_response = {"BGP_AS": "65001", "FABRIC_NAME": "test-fabric"} + + with does_not_raise(): + instance = ControllerResponseFabricsEasyFabricGetNvPairs(**minimal_response) + assert instance.BGP_AS == "65001" + assert instance.FABRIC_NAME == "test-fabric" + # All other fields should be None + assert instance.ENABLE_EVPN is None + assert instance.FABRIC_MTU is None + assert instance.dcnmUser is None diff --git a/tests/unit/module_utils/vrf/test_model_payload_vrfs_attachments.py b/tests/unit/module_utils/vrf/test_model_payload_vrfs_attachments.py new file mode 100644 index 000000000..d80a2409b --- /dev/null +++ b/tests/unit/module_utils/vrf/test_model_payload_vrfs_attachments.py @@ -0,0 +1,97 @@ +# Copyright (c) 2025 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. +""" +Test cases for PayloadVrfsAttachments. +""" +from functools import partial + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.vrf.model_payload_vrfs_attachments import PayloadVrfsAttachments + +from ..common.common_utils import does_not_raise +from .fixtures.load_fixture import payloads_vrfs_attachments + +vrf_name_tests = [ + ("test_vrf", "test_vrf", True), # Valid, length within min_length and max_length + ("v", "v", True), # Valid, compliant with min_length of 1 character + ( + "vrf_5678901234567890123456789012", + "vrf_5678901234567890123456789012", + True, + ), # Valid, compliant with max_length of 32 characters + (123, None, False), # Invalid, noncompliant with str type + ( + "vrf_56789012345678901234567890123", + None, + False, + ), # Invalid, noncompliant with max_length of 32 characters + ( + "", + None, + False, + ), # Invalid, noncompliant with min_length of 1 character +] + + +# pylint: disable=too-many-arguments +def base_test(value, expected, valid: bool, model_field: str, payload_field: str, key: str, model): + """ + Base test function called by other tests to validate the model. + + :param value: vrf_model value to validate. + :param expected: Expected value after model conversion or validation (None for no expectation). + :param valid: Whether the input value is expected to be valid or not. + :param field: The field in the playbook to modify. + :param key: The key in the playbooks fixture to use. + :param model: The model class to instantiate. + """ + payload = payloads_vrfs_attachments(key) + print(f"payload before: {payload}") + if value == "MISSING": + payload.pop(payload_field, None) + else: + payload[payload_field] = value + print(f"payload after: {payload}") + + if valid: + with does_not_raise(): + instance = model(**payload) + print(f"instance: {instance}") + if value != "MISSING": + assert getattr(instance, model_field) == expected + else: + assert expected == model.model_fields[model_field].default + else: + with pytest.raises(ValueError): + print(f"FINAL PAYLOAD: {payload}") + model(**payload) + + +base_test_vrf_name = partial( + base_test, + model_field="vrf_name", + payload_field="vrfName", + key="payload_full", + model=PayloadVrfsAttachments, +) + +# pylint: enable=too-many-arguments + + +@pytest.mark.parametrize("value, expected, valid", vrf_name_tests) +def test_payload_vrfs_attachments_00000(value, expected, valid) -> None: + """ + vrf_name + """ + base_test_vrf_name(value, expected, valid) diff --git a/tests/unit/module_utils/vrf/test_model_payload_vrfs_deployments.py b/tests/unit/module_utils/vrf/test_model_payload_vrfs_deployments.py new file mode 100644 index 000000000..9231c9668 --- /dev/null +++ b/tests/unit/module_utils/vrf/test_model_payload_vrfs_deployments.py @@ -0,0 +1,51 @@ +# Copyright (c) 2025 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. +""" +Test cases for PayloadVrfsDeployments. +""" +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.vrf.model_payload_vrfs_deployments import ( + PayloadVrfsDeployments, +) + +from ..common.common_utils import does_not_raise + + +@pytest.mark.parametrize( + "value, expected, valid", + [ + (["vrf2", "vrf1", "vrf3"], "vrf1,vrf2,vrf3", True), + ([], "", True), + (["vrf1"], "vrf1", True), + ([1, "vrf2"], None, False), # Invalid type in list + ], +) +def test_vrf_payload_deployments_00000(value, expected, valid) -> None: + """ + Test PayloadVrfsDeployments.vrf_names. + + :param value: The input value for vrf_names. + :param expected: The expected string representation of vrf_names after instance.model_dump(). + :param valid: Whether the input value is expected to be valid or not. + """ + if valid: + with does_not_raise(): + instance = PayloadVrfsDeployments(vrfNames=value) + assert instance.vrf_names == value + assert instance.model_dump(by_alias=True) == { + "vrfNames": expected + } + else: + with pytest.raises(ValueError): + PayloadVrfsDeployments(vrfNames=value) diff --git a/tests/unit/module_utils/vrf/test_model_playbook_vrf_v12.py b/tests/unit/module_utils/vrf/test_model_playbook_vrf_v12.py new file mode 100644 index 000000000..8b45aab4f --- /dev/null +++ b/tests/unit/module_utils/vrf/test_model_playbook_vrf_v12.py @@ -0,0 +1,824 @@ +# Copyright (c) 2025 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. +""" +Test cases for PlaybookVrfModelV12 and PlaybookVrfConfigModelV12. +""" +from functools import partial +from typing import Union + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.enums.bgp import BgpPasswordEncrypt +from ansible_collections.cisco.dcnm.plugins.module_utils.vrf.model_playbook_vrf_v12 import ( + PlaybookVrfAttachModel, + PlaybookVrfConfigModelV12, + PlaybookVrfLiteModel, + PlaybookVrfModelV12, +) + +from ..common.common_utils import does_not_raise +from .fixtures.load_fixture import playbooks + +bool_tests = [ + (True, True, True), # OK, bool + (False, False, True), # OK, bool. TODO: This should not fail. + (1, None, False), # NOK, type is set to StrictBoolean in the model with allows only True or False + (0, None, False), # NOK, type is set to StrictBoolean in the model with allows only True or False + ("abc", None, False), # NOK, type is set to StrictBoolean in the model with allows only True or False +] +bool_tests_missing_default_true = bool_tests + [ + ("MISSING", True, True), # OK, field can be missing. Default is True. +] +bool_tests_missing_default_false = bool_tests + [ + ("MISSING", False, True), # OK, field can be missing. Default is False. +] + +ipv4_addr_host_tests = [ + ("10.1.1.1", "10.1.1.1", True), + ("168.1.1.1", "168.1.1.1", True), + ("172.1.1.1", "172.1.1.1", True), + # ("255.255.255.255/30", None, False), TODO: this should not be valid, but currently is + ("10.1.1.1/24", "10.1.1.1/24", False), + ("168.1.1.1/30", "168.1.1.1/30", False), + ("172.1.1.1/30", "172.1.1.1/30", False), + ("172.1.1.", None, False), + ("2010::10:34:0:7", None, False), + ("2010::10:34:0:7/64", None, False), + (1, None, False), + ("abc", None, False), +] + +ipv4_addr_cidr_tests = [ + ("10.1.1.1/24", "10.1.1.1/24", True), + ("168.1.1.1/30", "168.1.1.1/30", True), + ("172.1.1.1/30", "172.1.1.1/30", True), + ("MISSING", "", True), # OK, field can be missing. Default is "". + # ("255.255.255.255/30", None, False), TODO: this should not be valid, but currently is + ("172.1.1.", None, False), + ("255.255.255.255", None, False), + ("2010::10:34:0:7", None, False), + ("2010::10:34:0:7/64", None, False), + (1, None, False), + ("abc", None, False), +] + +ipv4_multicast_group_tests = [ + ("224.1.1.1", "224.1.1.1", True), + ("MISSING", "", True), # OK, field can be missing. Default is "". + ("10.1.1.1", None, False), + (3, 3, False), # NOK, int + (None, None, False), # NOK, None is not a valid value +] + +ipv6_addr_host_tests = [ + ("2010::10:34:0:7", "2010::10:34:0:7", True), + ("2010:10::7", "2010:10::7", True), + ("2010::10:34:0:7/64", "2010::10:34:0:7/64", False), + ("2010::10::7/128", "2010::10::7/128", False), + ("172.1.1.1/30", None, False), + ("172.1.1.1", None, False), + ("255.255.255.255", None, False), + (1, None, False), + ("abc", None, False), +] + +ipv6_addr_cidr_tests = [ + ("2010::10:34:0:7/64", "2010::10:34:0:7/64", True), + ("2010::10::7/128", "2010::10::7/128", True), + ("2010:10::7", None, False), + ("172.1.1.1/30", None, False), + ("172.1.1.1", None, False), + ("255.255.255.255", None, False), + (1, None, False), + ("abc", None, False), +] + + +# pylint: disable=too-many-arguments +def base_test(value, expected, valid: bool, field: str, key: str, model): + """ + Base test function called by other tests to validate the model. + + :param value: vrf_model value to validate. + :param expected: Expected value after model conversion or validation (None for no expectation). + :param valid: Whether the value is valid or not. + :param field: The field in the playbook to modify. + :param key: The key in the playbooks fixture to use. + :param model: The model class to instantiate. + """ + playbook = playbooks(key) + if value == "MISSING": + playbook.pop(field, None) + else: + playbook[field] = value + + if valid: + with does_not_raise(): + instance = model(**playbook) + if value != "MISSING": + assert getattr(instance, field) == expected + else: + assert expected == model.model_fields[field].default + else: + with pytest.raises(ValueError): + model(**playbook) + + +# pylint: enable=too-many-arguments + + +def test_full_config_00000() -> None: + """ + Test PlaybookVrfConfigModelV12 with JSON representing the structure passed to a playbook. + + The remaining tests will use partial structures (e.g. vrf_lite, attach) for simplicity. + """ + playbook = playbooks("playbook_full_config") + with does_not_raise(): + instance = PlaybookVrfConfigModelV12(**playbook) + assert instance.config[0].vrf_name == "ansible-vrf-int1" + + +base_test_vrf_name = partial(base_test, field="vrf_name", key="playbook_as_dict", model=PlaybookVrfModelV12) +base_test_vrf_lite = partial(base_test, key="vrf_lite", model=PlaybookVrfLiteModel) +base_test_attach = partial(base_test, key="vrf_attach", model=PlaybookVrfAttachModel) +base_test_vrf = partial(base_test, key="playbook_as_dict", model=PlaybookVrfModelV12) + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("ansible-vrf-int1", "ansible-vrf-int1", True), + ("vrf_5678901234567890123456789012", "vrf_5678901234567890123456789012", True), # Valid, exactly 32 characters + (123, None, False), # Invalid, int + ("vrf_56789012345678901234567890123", None, False), # Invalid, longer than 32 characters + ], +) +def test_vrf_name_00000(value: Union[str, int], expected, valid: bool) -> None: + """ + vrf_name + """ + base_test_vrf_name(value, expected, valid) + + +@pytest.mark.parametrize( + "value, expected, valid", + [ + ("1", "1", True), + ("4094", "4094", True), + (1, "1", True), + (4094, "4094", True), + ("0", "", True), + (0, "", True), + ("4095", None, False), + (4095, None, False), + ("-1", None, False), + ("abc", None, False), + ], +) +def test_vrf_lite_00000(value, expected, valid: bool) -> None: + """ + dot1q + """ + base_test_vrf_lite(value, expected, valid, field="dot1q") + + +@pytest.mark.parametrize( + "value, expected, valid", + [ + ("Ethernet1/1", "Ethernet1/1", True), + ("Eth2/1", "Eth2/1", True), + ("MISSING", None, False), + ], +) +def test_vrf_lite_00010(value, expected, valid: bool) -> None: + """ + interface + """ + base_test_vrf_lite(value, expected, valid, field="interface") + + +@pytest.mark.parametrize("value, expected, valid", ipv4_addr_cidr_tests) +def test_vrf_lite_00020(value, expected, valid: bool) -> None: + """ + ipv4_addr + """ + base_test_vrf_lite(value, expected, valid, field="ipv4_addr") + + +@pytest.mark.parametrize("value, expected, valid", ipv6_addr_cidr_tests) +def test_vrf_lite_00030(value, expected, valid: bool) -> None: + """ + ipv6_addr + """ + base_test_vrf_lite(value, expected, valid, field="ipv6_addr") + + +@pytest.mark.parametrize("value, expected, valid", ipv4_addr_host_tests) +def test_vrf_lite_00040(value, expected, valid: bool) -> None: + """ + neighbor_ipv4 + """ + base_test_vrf_lite(value, expected, valid, field="neighbor_ipv4") + + +@pytest.mark.parametrize("value, expected, valid", ipv6_addr_host_tests) +def test_vrf_lite_00050(value, expected, valid: bool) -> None: + """ + neighbor_ipv6 + """ + base_test_vrf_lite(value, expected, valid, field="neighbor_ipv6") + + +@pytest.mark.parametrize( + "value, expected, valid", + [ + ("ansible-vrf-int1", "ansible-vrf-int1", True), # OK, valid VRF name + ("vrf_5678901234567890123456789012", "vrf_5678901234567890123456789012", True), # OK, exactly 32 characters + ("", "", False), # NOK, at least one character is required + (123, None, False), # NOK, int + ("vrf_56789012345678901234567890123", None, False), # NOK, longer than 32 characters + ], +) +def test_vrf_lite_00060(value, expected, valid: bool) -> None: + """ + peer_vrf + """ + base_test_vrf_lite(value, expected, valid, field="peer_vrf") + + +# VRF Attach Tests + + +@pytest.mark.parametrize("value, expected, valid", bool_tests_missing_default_true) +def test_vrf_attach_00000(value, expected, valid: bool) -> None: + """ + deploy + """ + base_test_attach(value, expected, valid, field="deploy") + + +@pytest.mark.parametrize( + "value, expected, valid", + [ + ("", "", True), # OK, empty string + ("target:1:1", "target:1:1", True), # OK, string + (False, False, False), # NOK, bool + (123, None, False), # NOK, int + ], +) +def test_vrf_attach_00010(value, expected, valid: bool) -> None: + """ + export_evpn_rt + """ + base_test_attach(value, expected, valid, field="export_evpn_rt") + + +@pytest.mark.parametrize( + "value, expected, valid", + [ + ("", "", True), # OK, empty string + ("target:1:2", "target:1:2", True), # OK, string + (False, False, False), # NOK, bool + (123, None, False), # NOK, int + ], +) +def test_vrf_attach_00020(value, expected, valid: bool) -> None: + """ + import_evpn_rt + """ + base_test_attach(value, expected, valid, field="import_evpn_rt") + + +@pytest.mark.parametrize("value, expected, valid", ipv4_addr_host_tests) +def test_vrf_attach_00030(value, expected, valid: bool) -> None: + """ + ip_address + """ + base_test_attach(value, expected, valid, field="ip_address") + + +@pytest.mark.parametrize( + "value, expected, valid", + [ + (None, None, True), # OK, vrf_lite null + ("MISSING", None, True), # OK, field can be missing + (1, None, False), # NOK, vrf_lite int + ("abc", None, False), # NOK, vrf_lite string + ], +) +def test_vrf_attach_00040(value, expected, valid: bool) -> None: + """ + vrf_lite + """ + base_test_attach(value, expected, valid, field="vrf_lite") + + +@pytest.mark.parametrize("value,expected,valid", bool_tests_missing_default_true) +def test_vrf_model_00000(value, expected, valid: bool) -> None: + """ + adv_default_routes + """ + base_test_vrf(value, expected, valid, field="adv_default_routes") + + +@pytest.mark.parametrize("value,expected,valid", bool_tests_missing_default_false) +def test_vrf_model_00010(value, expected, valid: bool) -> None: + """ + adv_host_routes + """ + base_test_vrf(value, expected, valid, field="adv_host_routes") + + +@pytest.mark.parametrize( + "value, expected, valid", + [ + (None, None, True), # OK, attach can be null. + ("MISSING", None, True), # OK, field can be missing + ([], [], True), # OK, attach can be an empty list + (0, None, False), + ("abc", None, False), + ], +) +def test_vrf_model_00020(value, expected, valid: bool) -> None: + """ + attach + """ + base_test_vrf(value, expected, valid, field="attach") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + (BgpPasswordEncrypt.MD5, BgpPasswordEncrypt.MD5.value, True), + (BgpPasswordEncrypt.TYPE7, BgpPasswordEncrypt.TYPE7.value, True), + (3, 3, True), # OK, integer corresponding to MD5 + (7, 7, True), # OK, integer corresponding to TYPE7 + (-1, -1, True), # OK, integer corresponding to NONE + (0, None, False), # NOK, not a valid enum value + ("md5", None, False), # NOK, string not in enum + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00030(value, expected, valid): + """ + bgp_passwd_encrypt + """ + base_test_vrf(value, expected, valid, field="bgp_passwd_encrypt") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("MyPassword", "MyPassword", True), + ("MISSING", "", True), # OK, field can be missing + (3, 3, False), # NOK, int + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00040(value, expected, valid): + """ + bgp_password + """ + base_test_vrf(value, expected, valid, field="bgp_password") + + +@pytest.mark.parametrize("value,expected,valid", bool_tests_missing_default_true) +def test_vrf_model_00050(value, expected, valid): + """ + deploy + """ + base_test_vrf(value, expected, valid, field="deploy") + + +@pytest.mark.parametrize("value,expected,valid", bool_tests_missing_default_false) +def test_vrf_model_00060(value, expected, valid): + """ + disable_rt_auto + """ + base_test_vrf(value, expected, valid, field="disable_rt_auto") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("5000:1", "5000:1", True), + ("MISSING", "", True), # OK, field can be missing. Default is "". + (3, 3, False), # NOK, int + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00070(value, expected, valid): + """ + export/import route-target tests + """ + for field in ["export_evpn_rt", "import_evpn_rt", "export_mvpn_rt", "import_mvpn_rt", "export_vpn_rt", "import_vpn_rt"]: + base_test_vrf(value, expected, valid, field=field) + + +@pytest.mark.parametrize("value,expected,valid", bool_tests_missing_default_true) +def test_vrf_model_00080(value, expected, valid): + """ + ipv6_linklocal_enable + """ + base_test_vrf(value, expected, valid, field="ipv6_linklocal_enable") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + (0, 0, True), # OK, integer in range + (4294967295, 4294967295, True), # OK, integer in range + ("MISSING", 12345, True), # OK, field can be missing. Default is 12345. + (-1, None, False), # NOK, must be > 0 + (4294967296, None, False), # NOK, must be <= 4294967295 + ("md5", None, False), # NOK, string + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00090(value, expected, valid): + """ + loopback_route_tag + """ + base_test_vrf(value, expected, valid, field="loopback_route_tag") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + (1, 1, True), # OK, integer in range + (64, 64, True), # OK, integer in range + ("MISSING", 1, True), # OK, field can be missing. Default is 1. + (0, None, False), # NOK, must be > 1 + (65, None, False), # NOK, must be <= 64 + ("md5", None, False), # NOK, string + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00100(value, expected, valid): + """ + max_bgp_paths + """ + base_test_vrf(value, expected, valid, field="max_bgp_paths") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + (1, 1, True), # OK, integer in range + (64, 64, True), # OK, integer in range + ("MISSING", 2, True), # OK, field can be missing. Default is 2. + (0, None, False), # NOK, must be > 1 + (65, None, False), # NOK, must be <= 64 + ("md5", None, False), # NOK, string + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00110(value, expected, valid): + """ + max_ibgp_paths + """ + base_test_vrf(value, expected, valid, field="max_ibgp_paths") + + +@pytest.mark.parametrize("value,expected,valid", bool_tests_missing_default_false) +def test_vrf_model_00120(value, expected, valid): + """ + netflow_enable + """ + base_test_vrf(value, expected, valid, field="netflow_enable") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("5000:1", "5000:1", True), + ("MISSING", "", True), # OK, field can be missing. Default is "". + (3, 3, False), # NOK, int + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00130(value, expected, valid): + """ + nf_monitor + TODO: Revisit for actual values after testing against NDFC. + """ + base_test_vrf(value, expected, valid, field="nf_monitor") + + +@pytest.mark.parametrize("value,expected,valid", bool_tests_missing_default_false) +def test_vrf_model_00140(value, expected, valid): + """ + no_rp + """ + base_test_vrf(value, expected, valid, field="no_rp") + + +@pytest.mark.parametrize("value,expected,valid", ipv4_multicast_group_tests) +def test_vrf_model_00150(value, expected, valid): + """ + overlay_mcast_group + """ + base_test_vrf(value, expected, valid, field="overlay_mcast_group") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("my-route-map", "my-route-map", True), + ("MISSING", "FABRIC-RMAP-REDIST-SUBNET", True), # OK, field can be missing. Default is "FABRIC-RMAP-REDIST-SUBNET". + ("", "", True), # OK, empty string + (3, 3, False), # NOK, int + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00160(value, expected, valid): + """ + redist_direct_rmap + """ + base_test_vrf(value, expected, valid, field="redist_direct_rmap") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("10.1.1.1", "10.1.1.1", True), + ("MISSING", "", True), # OK, field can be missing. Default is "". + ("", "", True), # OK, empty string + ("10.1.1.1/24", "10.1.1.1/24", False), # NOK, prefix is not allowed + (3, 3, False), # NOK, int + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00170(value, expected, valid): + """ + rp_address + """ + base_test_vrf(value, expected, valid, field="rp_address") + + +@pytest.mark.parametrize("value,expected,valid", bool_tests_missing_default_false) +def test_vrf_model_00180(value, expected, valid): + """ + rp_external + """ + base_test_vrf(value, expected, valid, field="rp_external") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + (0, 0, True), # OK, integer in range + (1023, 1023, True), # OK, integer in range + ("MISSING", "", True), # OK, field can be missing. Default is "". + (-1, None, False), # NOK, must be >= 0 + (1024, None, False), # NOK, must be <= 1023 + ("md5", None, False), # NOK, string + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00190(value, expected, valid): + """ + rp_loopback_id + """ + base_test_vrf(value, expected, valid, field="rp_loopback_id") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("MY-SERVICE-VRF-TEMPLATE", "MY-SERVICE-VRF-TEMPLATE", True), # OK, valid string + ("MISSING", None, True), # OK, field can be missing. Default is None. + (None, None, True), # OK, None is valid + (-1, None, False), # NOK, not a string + (1024, None, False), # NOK, not a string + ], +) +def test_vrf_model_00200(value, expected, valid): + """ + service_vrf_template + """ + base_test_vrf(value, expected, valid, field="service_vrf_template") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("MISSING", None, True), # OK, field is always hardcoded to None. + (None, None, True), # OK, field is always hardcoded to None. + ("some-string", None, True), # OK, field is always hardcoded to None. + (-1, None, True), # OK, field is always hardcoded to None. + (1024, None, True), # OK, field is always hardcoded to None. + ], +) +def test_vrf_model_00210(value, expected, valid): + """ + source + """ + base_test_vrf(value, expected, valid, field="source") + + +@pytest.mark.parametrize("value,expected,valid", bool_tests_missing_default_true) +def test_vrf_model_00220(value, expected, valid): + """ + static_default_route + """ + base_test_vrf(value, expected, valid, field="static_default_route") + + +@pytest.mark.parametrize("value,expected,valid", bool_tests_missing_default_false) +def test_vrf_model_00230(value, expected, valid): + """ + trm_bgw_msite + """ + base_test_vrf(value, expected, valid, field="trm_bgw_msite") + + +@pytest.mark.parametrize("value,expected,valid", bool_tests_missing_default_false) +def test_vrf_model_00240(value, expected, valid): + """ + trm_enable + """ + base_test_vrf(value, expected, valid, field="trm_enable") + + +@pytest.mark.parametrize("value,expected,valid", ipv4_multicast_group_tests) +def test_vrf_model_00250(value, expected, valid): + """ + underlay_mcast_ip + """ + base_test_vrf(value, expected, valid, field="underlay_mcast_ip") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + (2, 2, True), # OK, integer in range + (4094, 4094, True), # OK, integer in range + ("2", 2, True), # OK, str convertable to integer in range + ("4094", 4094, True), # OK, str convertable to integer in range + ("MISSING", None, True), # OK, field can be missing. Default is None. + (-1, None, False), # NOK, must be >= 2 + (4095, None, False), # NOK, must be <= 4094 + ("1", None, False), # NOK, str convertable to integer out of range + ("4095", None, False), # NOK, str convertable to integer out in range + ("md5", None, False), # NOK, string + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00260(value, expected, valid): + """ + vlan_id + """ + base_test_vrf(value, expected, valid, field="vlan_id") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("My vrf description", "My vrf description", True), + ("MISSING", "", True), # OK, field can be missing. Default is "". + ("", "", True), # OK, empty string + (3, 3, False), # NOK, int + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00270(value, expected, valid): + """ + vrf_description + """ + base_test_vrf(value, expected, valid, field="vrf_description") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("MY_VRF_EXT_TEMPLATE", "MY_VRF_EXT_TEMPLATE", True), + ("MISSING", "Default_VRF_Extension_Universal", True), # OK, field can be missing. Default is "Default_VRF_Extension_Universal". + ("", "", True), # OK, empty string + (3, 3, False), # NOK, int + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00280(value, expected, valid): + """ + vrf_extension_template + """ + base_test_vrf(value, expected, valid, field="vrf_extension_template") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + (3, 3, True), # OK, int in range + (16777214, 16777214, True), # OK, int in range + ("MISSING", None, True), # OK, field can be missing. Default is None. + (None, None, True), # OK, None is a valid value + ("foo", None, False), # NOK, string + (16777215, None, False), # NOK, out of range + ], +) +def test_vrf_model_00290(value, expected, valid): + """ + vrf_id + """ + base_test_vrf(value, expected, valid, field="vrf_id") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + (68, 68, True), # OK, min value + (9216, 9216, True), # OK, max value + ("MISSING", 9216, True), # OK, field can be missing. Default is 9216. + (None, None, False), # NOK, None is an invalid value + ("foo", None, False), # NOK, string + (67, None, False), # NOK, below min value + (9217, None, False), # NOK, above max value + ], +) +def test_vrf_model_00300(value, expected, valid): + """ + vrf_int_mtu + """ + base_test_vrf(value, expected, valid, field="vrf_int_mtu") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("My vrf interface description", "My vrf interface description", True), + ("MISSING", "", True), # OK, field can be missing. Default is "". + ("", "", True), # OK, empty string + (3, 3, False), # NOK, int + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00310(value, expected, valid): + """ + vrf_intf_desc + """ + base_test_vrf(value, expected, valid, field="vrf_intf_desc") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("ansible-vrf-int1", "ansible-vrf-int1", True), + ("a", "a", True), # Valid, minimum number of characters (1) + ("vrf_5678901234567890123456789012", "vrf_5678901234567890123456789012", True), # Valid, maximum number of characters (32) + ("MISSING", None, False), # Invalid, field is mandatory + (123, None, False), # Invalid, int + ("", None, False), # Invalid, less than 32 characters + ("vrf_56789012345678901234567890123", None, False), # Invalid, more than 32 characters + ], +) +def test_vrf_model_00320(value, expected, valid) -> None: + """ + vrf_name + """ + base_test_vrf(value, expected, valid, field="vrf_name") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("MY_VRF_TEMPLATE", "MY_VRF_TEMPLATE", True), + ("MISSING", "Default_VRF_Universal", True), # OK, field can be missing. Default is "Default_VRF_Universal". + ("", "", True), # OK, empty string + (3, 3, False), # NOK, int + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00330(value, expected, valid): + """ + vrf_template + """ + base_test_vrf(value, expected, valid, field="vrf_template") + + +@pytest.mark.parametrize( + "value,expected,valid", + [ + ("My VRF Vlan Name", "My VRF Vlan Name", True), + ("MISSING", "", True), # OK, field can be missing. Default is "". + ("", "", True), # OK, empty string + (3, 3, False), # NOK, int + (None, None, False), # NOK, None is not a valid value + ], +) +def test_vrf_model_00340(value, expected, valid): + """ + vrf_vlan_name + """ + base_test_vrf(value, expected, valid, field="vrf_vlan_name") diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json b/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json index a924b3c15..1a05538f2 100644 --- a/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json +++ b/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json @@ -2008,4 +2008,4 @@ } ] } -} +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_vrf_11.json b/tests/unit/modules/dcnm/fixtures/dcnm_vrf_11.json new file mode 100644 index 000000000..2c31f153b --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_vrf_11.json @@ -0,0 +1,2011 @@ +{ + "mock_ip_sn" : { + "10.10.10.224": "XYZKSJHSMK1", + "10.10.10.225": "XYZKSJHSMK2", + "10.10.10.226": "XYZKSJHSMK3", + "10.10.10.227": "XYZKSJHSMK4", + "10.10.10.228": "XYZKSJHSMK5" + }, + "mock_ip_fab" : { + "10.10.10.224": "test_fabric", + "10.10.10.225": "test_fabric", + "10.10.10.226": "test_fabric", + "10.10.10.227": "test_fabric", + "10.10.10.228": "test_fabric" + }, + "mock_sn_fab" : { + "XYZKSJHSMK1": "test_fabric", + "XYZKSJHSMK2": "test_fabric", + "XYZKSJHSMK3": "test_fabric", + "XYZKSJHSMK4": "test_fabric", + "XYZKSJHSMK5": "test_fabric" + }, + "playbook_config_input_validation" : [ + { + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "vlan_id": "203", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.225", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_no_attach_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None" + } + ], + "playbook_vrf_lite_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_redeploy_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_redeploy_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_new_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_new_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_override_with_additions_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_override_with_additions_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_override_with_deletions_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.11/30", + "neighbor_ipv4": "10.33.0.12", + "ipv6_addr": "2010::10:34:0:1/64", + "neighbor_ipv6": "2010::10:34:0:1", + "dot1q": "21", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_override_with_deletions_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/17", + "ipv4_addr": "10.33.0.11/30", + "neighbor_ipv4": "10.33.0.12", + "ipv6_addr": "2010::10:34:0:1/64", + "neighbor_ipv6": "2010::10:34:0:1", + "dot1q": "21", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_replace_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/17", + "ipv4_addr": "10.33.0.11/30", + "neighbor_ipv4": "10.33.0.12", + "ipv6_addr": "2010::10:34:0:1/64", + "neighbor_ipv6": "2010::10:34:0:1", + "dot1q": "21", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_replace_config_interface_with_extension_values" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.11/30", + "neighbor_ipv4": "10.33.0.12", + "ipv6_addr": "2010::10:34:0:1/64", + "neighbor_ipv6": "2010::10:34:0:1", + "dot1q": "21", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_update_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_update_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_inv_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.225", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_update": [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "203", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.226", + "deploy": true + }, + { + "ip_address": "10.10.10.225", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_update_vlan": [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "303", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.225", + "deploy": true + }, + { + "ip_address": "10.10.10.226", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_update_vlan_config_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "402", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_update_vlan_config_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "402", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_override": [ + { + "vrf_name": "test_vrf_2", + "vrf_id": "9008012", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "303", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.225", + "deploy": true + }, + { + "ip_address": "10.10.10.226", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_incorrect_vrfid": [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008012", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "vlan_id": "202", + "deploy": true + }, + { + "ip_address": "10.10.10.225", + "vlan_id": "203", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_replace": [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "203", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.225", + "deploy": true + }, + { + "ip_address": "10.10.10.226", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_replace_no_atch": [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "source": "None", + "service_vrf_template": "None" + } + ], + "mock_vrf_attach_object_del_not_ready": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "lanAttachState": "DEPLOYED", + "isLanAttached": false + }, + { + "lanAttachState": "DEPLOYED", + "isLanAttached": false + } + ] + } + ] + }, + "mock_vrf_attach_object_del_oos": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "lanAttachState": "OUT-OF-SYNC" + }, + { + "lanAttachState": "OUT-OF-SYNC" + } + ] + } + ] + }, + "mock_vrf_attach_object_del_ready": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "lanAttachState": "NA", + "isLanAttached": false + }, + { + "lanAttachState": "NA", + "isLanAttached": false + } + ] + } + ] + }, + "mock_vrf_object": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE":"OK", + "DATA": [ + { + "fabric": "test_fabric", + "serviceVrfTemplate": "None", + "source": "None", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfId": 9008011, + "vrfName": "test_vrf_1", + "vrfTemplate": "Default_VRF_Universal", + "vrfTemplateConfig": "{\"advertiseDefaultRouteFlag\":\"true\",\"vrfVlanId\":\"202\",\"isRPExternal\":\"false\",\"vrfDescription\":\"\",\"L3VniMcastGroup\":\"\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"vrfSegmentId\":\"9008013\",\"ipv6LinkLocalFlag\":\"true\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"configureStaticDefaultRouteFlag\":\"true\",\"trmBGWMSiteEnabled\":\"false\",\"tag\":\"12345\",\"rpAddress\":\"\",\"nveId\":\"1\",\"bgpPasswordKeyType\":\"3\",\"bgpPassword\":\"\",\"mtu\":\"9216\",\"multicastGroup\":\"\",\"advertiseHostRouteFlag\":\"false\",\"vrfVlanName\":\"\",\"trmEnabled\":\"false\",\"loopbackNumber\":\"\",\"asn\":\"34343\",\"vrfIntfDescription\":\"\",\"vrfName\":\"test_vrf_1\"}", + "vrfStatus": "DEPLOYED" + } + ] + }, + "mock_vrf_attach_object" : { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "202", + "vrfId": "9008011" + }, + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf2", + "lanAttachState": "DEPLOYED", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK2", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.225", + "vlanId": "202", + "vrfId": "9008011" + } + ] + } + ] + }, + "mock_vrf_attach_object_query" : { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "202", + "vrfId": "9008011" + }, + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf2", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK2", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.225", + "vlanId": "202", + "vrfId": "9008011" + } + ] + } + ] + }, + "mock_vrf_attach_object2" : { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "202", + "vrfId": "9008011" + }, + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf4", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK4", + "switchRole": "border", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.227", + "vlanId": "202", + "vrfId": "9008011" + } + ] + } + ] + }, + "mock_vrf_attach_object2_query" : { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "202", + "vrfId": "9008011" + }, + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf4", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK4", + "switchRole": "border", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.227", + "vlanId": "202", + "vrfId": "9008011" + } + ] + } + ] + }, + "mock_vrf_attach_lite_object" : { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "deployment": true, + "extensionValues": "", + "fabric": "test_fabric", + "freeformConfig": "", + "instanceValues": "", + "serialNumber": "XYZKSJHSMK1", + "vlan": 202, + "vrfName": "test_vrf_1", + "vrf_lite": [] + }, + { + "deployment": true, + "extensionValues": "{\"VRF_LITE_CONN\":\"{\\\"VRF_LITE_CONN\\\":[{\\\"IF_NAME\\\":\\\"Ethernet1/16\\\",\\\"DOT1Q_ID\\\":\\\"2\\\",\\\"IP_MASK\\\":\\\"10.33.0.2/30\\\",\\\"NEIGHBOR_IP\\\":\\\"10.33.0.1\\\",\\\"NEIGHBOR_ASN\\\":\\\"65535\\\",\\\"IPV6_MASK\\\":\\\"2010::10:34:0:7/64\\\",\\\"IPV6_NEIGHBOR\\\":\\\"2010::10:34:0:3\\\",\\\"AUTO_VRF_LITE_FLAG\\\":\\\"false\\\",\\\"PEER_VRF_NAME\\\":\\\"ansible-vrf-int1\\\",\\\"VRF_LITE_JYTHON_TEMPLATE\\\":\\\"Ext_VRF_Lite_Jython\\\"}]}\",\"MULTISITE_CONN\":\"{\\\"MULTISITE_CONN\\\":[]}\"}", + "fabric": "test_fabric", + "freeformConfig": "", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "serialNumber": "XYZKSJHSMK4", + "vlan": 202, + "vrfName": "test_vrf_1" + } + ] + } + ] + }, + "mock_vrf_attach_object_pending": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "PENDING", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "202", + "vrfId": "9008011" + }, + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf2", + "lanAttachState": "PENDING", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK2", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.225", + "vlanId": "202", + "vrfId": "9008011" + } + ] + } + ] + }, + "mock_vrf_object_dcnm_only": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA": [ + { + "fabric": "test_fabric", + "vrfName": "test_vrf_dcnm", + "vrfTemplate": "Default_VRF_Universal", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "serviceVrfTemplate": "None", + "source": "None", + "vrfStatus": "DEPLOYED", + "vrfTemplateConfig": "{\"advertiseDefaultRouteFlag\":\"true\",\"vrfVlanId\":\"202\",\"isRPExternal\":\"false\",\"vrfDescription\":\"\",\"L3VniMcastGroup\":\"\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"vrfSegmentId\":\"9008013\",\"ipv6LinkLocalFlag\":\"true\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"configureStaticDefaultRouteFlag\":\"true\",\"trmBGWMSiteEnabled\":\"false\",\"tag\":\"12345\",\"rpAddress\":\"\",\"nveId\":\"1\",\"bgpPasswordKeyType\":\"3\",\"bgpPassword\":\"\",\"mtu\":\"9216\",\"multicastGroup\":\"\",\"advertiseHostRouteFlag\":\"false\",\"vrfVlanName\":\"\",\"trmEnabled\":\"false\",\"loopbackNumber\":\"\",\"asn\":\"34343\",\"vrfIntfDescription\":\"\",\"vrfName\":\"test_vrf_dcnm\"}", + "vrfId": "9008013" + } + ] + }, + "mock_vrf_attach_object_dcnm_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "vrfName": "test_vrf_dcnm", + "lanAttachList": [ + { + "vrfName": "test_vrf_dcnm", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "402", + "vrfId": "9008013" + }, + { + "vrfName": "test_vrf_dcnm", + "switchName": "n9kv_leaf2", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK2", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.225", + "vlanId": "403", + "vrfId": "9008013" + } + ] + } + ] + }, + "mock_vrf_attach_get_ext_object_dcnm_att1_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "{\"loopbackId\":\"10\",\"loopbackIpAddress\":\"11.1.1.1\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK1", + "switchName": "n9kv_leaf1", + "vlan": "402", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_dcnm" + } + ] + }, + "mock_vrf_attach_get_ext_object_dcnm_att2_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "{\"loopbackId\":\"10\",\"loopbackIpAddress\":\"11.1.1.1\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK2", + "switchName": "n9kv_leaf2", + "vlan": "403", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_dcnm" + } + ] + }, + "mock_vrf_attach_get_ext_object_merge_att1_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK1", + "switchName": "n9kv_leaf1", + "vlan": "202", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ] + }, + "mock_vrf_attach_get_ext_object_merge_att2_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK2", + "switchName": "n9kv_leaf2", + "vlan": "202", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ] + }, + + "mock_vrf_attach_get_ext_object_ov_att1_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK1", + "switchName": "n9kv_leaf1", + "vlan": "303", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ] + }, + "mock_vrf_attach_get_ext_object_ov_att2_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK2", + "switchName": "n9kv_leaf2", + "vlan": "303", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ] + }, + + "mock_vrf_attach_get_ext_object_merge_att3_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK1", + "switchName": "n9kv_leaf1", + "vlan": "202", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ] + }, + "mock_vrf_attach_get_ext_object_merge_att4_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [ + { + "destInterfaceName": "Ethernet1/16", + "destSwitchName": "dt-n9k2-1", + "extensionType": "VRF_LITE", + "extensionValues": "{\"PEER_VRF_NAME\": \"\", \"NEIGHBOR_IP\": \"10.33.0.1\", \"VRF_LITE_JYTHON_TEMPLATE\": \"Ext_VRF_Lite_Jython\", \"enableBorderExtension\": \"VRF_LITE\", \"AUTO_VRF_LITE_FLAG\": \"false\", \"IP_MASK\": \"10.33.0.2/30\", \"MTU\": \"9216\", \"NEIGHBOR_ASN\": \"65535\", \"IF_NAME\": \"Ethernet1/16\", \"IPV6_NEIGHBOR\": \"\", \"IPV6_MASK\": \"\", \"DOT1Q_ID\": \"3\", \"asn\": \"65535\"}", + "interfaceName": "Ethernet1/16" + } + ], + "extensionValues": "{\"VRF_LITE_CONN\":\"{\\\"VRF_LITE_CONN\\\":[{\\\"IF_NAME\\\":\\\"Ethernet1/16\\\",\\\"DOT1Q_ID\\\":\\\"2\\\",\\\"IP_MASK\\\":\\\"10.33.0.2/30\\\",\\\"NEIGHBOR_IP\\\":\\\"10.33.0.1\\\",\\\"NEIGHBOR_ASN\\\":\\\"65535\\\",\\\"IPV6_MASK\\\":\\\"2010::10:34:0:7/64\\\",\\\"IPV6_NEIGHBOR\\\":\\\"2010::10:34:0:3\\\",\\\"AUTO_VRF_LITE_FLAG\\\":\\\"false\\\",\\\"PEER_VRF_NAME\\\":\\\"test_vrf_1\\\",\\\"VRF_LITE_JYTHON_TEMPLATE\\\":\\\"Ext_VRF_Lite_Jython\\\"}]}\",\"MULTISITE_CONN\":\"{\\\"MULTISITE_CONN\\\":[]}\"}", + "freeformConfig": "", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\"}", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "border", + "serialNumber": "XYZKSJHSMK4", + "switchName": "n9kv_leaf4", + "vlan": 202, + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Extension_Universal", + "vrfName": "test_vrf_1" + } + ] + }, + + + "attach_success_resp": { + "DATA": { + "test-vrf-1--XYZKSJHSMK1(leaf1)": "SUCCESS", + "test-vrf-1--XYZKSJHSMK2(leaf2)": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200 + }, + "attach_success_resp2": { + "DATA": { + "test-vrf-2--XYZKSJHSMK2(leaf2)": "SUCCESS", + "test-vrf-2--XYZKSJHSMK3(leaf3)": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200 + }, + "attach_success_resp3": { + "DATA": { + "test-vrf-1--XYZKSJHSMK2(leaf1)": "SUCCESS", + "test-vrf-1--XYZKSJHSMK3(leaf4)": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200 + }, + "deploy_success_resp": { + "DATA": {"status": ""}, + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200 + }, + "blank_data": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200 + }, + "get_have_failure": { + "DATA": "Invalid JSON response: Invalid Fabric: demo-fabric-123", + "ERROR": "Not Found", + "METHOD": "GET", + "RETURN_CODE": 404, + "MESSAGE": "OK" + }, + "error1": { + "DATA": "None", + "ERROR": "There is an error", + "METHOD": "POST", + "RETURN_CODE": 400, + "MESSAGE": "OK" + }, + "error2": { + "DATA": { + "test-vrf-1--XYZKSJHSMK1(leaf1)": "Entered VRF VLAN ID 203 is in use already", + "test-vrf-1--XYZKSJHSMK2(leaf2)": "SUCCESS" + }, + "ERROR": "", + "METHOD": "POST", + "RETURN_CODE": 200, + "MESSAGE": "OK" + }, + "error3": { + "DATA": "No switches PENDING for deployment", + "ERROR": "", + "METHOD": "POST", + "RETURN_CODE": 200, + "MESSAGE": "OK" + }, + "delete_success_resp": { + "ERROR": "", + "METHOD": "POST", + "RETURN_CODE": 200, + "MESSAGE": "OK" + }, + "vrf_inv_data": { + "10.10.10.224":{ + "ipAddress": "10.10.10.224", + "logicalName": "dt-n9k1", + "serialNumber": "XYZKSJHSMK1", + "switchRole": "leaf" + }, + "10.10.10.225":{ + "ipAddress": "10.10.10.225", + "logicalName": "dt-n9k2", + "serialNumber": "XYZKSJHSMK2", + "switchRole": "leaf" + }, + "10.10.10.226":{ + "ipAddress": "10.10.10.226", + "logicalName": "dt-n9k3", + "serialNumber": "XYZKSJHSMK3", + "switchRole": "leaf" + }, + "10.10.10.227":{ + "ipAddress": "10.10.10.227", + "logicalName": "dt-n9k4", + "serialNumber": "XYZKSJHSMK4", + "switchRole": "border spine" + }, + "10.10.10.228":{ + "ipAddress": "10.10.10.228", + "logicalName": "dt-n9k5", + "serialNumber": "XYZKSJHSMK5", + "switchRole": "border" + } + }, + "fabric_details_mfd": { + "id": 4, + "fabricId": "FABRIC-4", + "fabricName": "MSD", + "fabricType": "MFD", + "fabricTypeFriendly": "VXLAN EVPN Multi-Site", + "fabricTechnology": "VXLANFabric", + "templateFabricType": "VXLAN EVPN Multi-Site", + "fabricTechnologyFriendly": "VXLAN EVPN", + "provisionMode": "DCNMTopDown", + "deviceType": "n9k", + "replicationMode": "IngressReplication", + "operStatus": "HEALTHY", + "templateName": "MSD_Fabric", + "nvPairs": { + "SGT_ID_RANGE_PREV": "", + "SGT_PREPROVISION_PREV": "false", + "CLOUDSEC_KEY_STRING": "", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "SGT_NAME_PREFIX_PREV": "", + "ENABLE_PVLAN_PREV": "false", + "SGT_PREPROV_RECALC_STATUS": "empty", + "L3_PARTITION_ID_RANGE": "50000-59000", + "PARENT_ONEMANAGE_FABRIC": "", + "ENABLE_TRM_TRMv6_PREV": "false", + "V6_DCI_SUBNET_RANGE": "", + "DCNM_ID": "", + "RP_SERVER_IP": "", + "MS_UNDERLAY_AUTOCONFIG": "false", + "VXLAN_UNDERLAY_IS_V6": "false", + "ENABLE_SGT": "off", + "ENABLE_BGP_BFD": "false", + "ENABLE_PVLAN": "false", + "ENABLE_BGP_LOG_NEIGHBOR_CHANGE": "false", + "MS_IFC_BGP_PASSWORD": "", + "MS_IFC_BGP_AUTH_KEY_TYPE_PREV": "", + "BGW_ROUTING_TAG_PREV": "54321", + "default_network": "Default_Network_Universal", + "scheduledTime": "", + "CLOUDSEC_ENFORCEMENT": "", + "enableScheduledBackup": "", + "CLOUDSEC_ALGORITHM": "", + "PREMSO_PARENT_FABRIC": "", + "MS_IFC_BGP_PASSWORD_ENABLE_PREV": "", + "FABRIC_NAME": "MSD", + "MSO_CONTROLER_ID": "", + "RS_ROUTING_TAG": "", + "MS_IFC_BGP_PASSWORD_ENABLE": "false", + "BGW_ROUTING_TAG": "54321", + "ANYCAST_GW_MAC": "2020.0000.00aa", + "SGT_RECALC_STATUS": "empty", + "DCI_SUBNET_TARGET_MASK": "30", + "default_pvlan_sec_network": "", + "BORDER_GWY_CONNECTIONS": "Manual", + "V6_DCI_SUBNET_TARGET_MASK": "", + "SGT_OPER_STATUS": "off", + "ENABLE_SGT_PREV": "off", + "FF": "MSD", + "ENABLE_RS_REDIST_DIRECT": "false", + "SGT_NAME_PREFIX": "", + "FABRIC_TYPE": "MFD", + "TOR_AUTO_DEPLOY": "false", + "EXT_FABRIC_TYPE": "", + "CLOUDSEC_REPORT_TIMER": "", + "network_extension_template": "Default_Network_Extension_Universal", + "MS_IFC_BGP_AUTH_KEY_TYPE": "", + "default_vrf": "Default_VRF_Universal", + "BGP_RP_ASN": "", + "DELAY_RESTORE": "300", + "MSO_SITE_GROUP_NAME": "", + "ENABLE_BGP_SEND_COMM": "false", + "DCI_SUBNET_RANGE": "10.10.1.0/24", + "SGT_PREPROVISION": "false", + "CLOUDSEC_AUTOCONFIG": "false", + "LOOPBACK100_IP_RANGE": "10.10.0.0/24", + "MS_LOOPBACK_ID": "100", + "SGT_ID_RANGE": "", + "LOOPBACK100_IPV6_RANGE": "", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "MS_IFC_BGP_PASSWORD_PREV": "", + "ENABLE_TRM_TRMv6": "false" + }, + "vrfTemplate": "Default_VRF_Universal", + "networkTemplate": "Default_Network_Universal", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "createdOn": 1737248524471, + "modifiedOn": 1737248525048 + }, + "fabric_details_vxlan": { + "id": 5, + "fabricId": "FABRIC-5", + "fabricName": "VXLAN", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", + "fabricTechnology": "VXLANFabric", + "templateFabricType": "Data Center VXLAN EVPN", + "fabricTechnologyFriendly": "VXLAN EVPN", + "provisionMode": "DCNMTopDown", + "deviceType": "n9k", + "replicationMode": "Multicast", + "operStatus": "WARNING", + "asn": "65001", + "siteId": "65001", + "templateName": "Easy_Fabric", + "nvPairs": { + "MSO_SITE_ID": "", + "PHANTOM_RP_LB_ID1": "", + "PHANTOM_RP_LB_ID2": "", + "PHANTOM_RP_LB_ID3": "", + "IBGP_PEER_TEMPLATE": "", + "PHANTOM_RP_LB_ID4": "", + "abstract_ospf": "base_ospf", + "L3_PARTITION_ID_RANGE": "50000-59000", + "FEATURE_PTP": "false", + "DHCP_START_INTERNAL": "", + "SSPINE_COUNT": "0", + "ENABLE_SGT": "false", + "ENABLE_MACSEC_PREV": "false", + "NXC_DEST_VRF": "management", + "ADVERTISE_PIP_BGP": "false", + "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", + "BFD_PIM_ENABLE": "false", + "DHCP_END": "", + "FABRIC_VPC_DOMAIN_ID": "", + "SEED_SWITCH_CORE_INTERFACES": "", + "UNDERLAY_IS_V6": "false", + "ALLOW_NXC_PREV": "true", + "FABRIC_MTU_PREV": "9216", + "BFD_ISIS_ENABLE": "false", + "HD_TIME": "180", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", + "LOOPBACK1_IPV6_RANGE": "", + "OSPF_AUTH_ENABLE": "false", + "ROUTER_ID_RANGE": "", + "MSO_CONNECTIVITY_DEPLOYED": "", + "ENABLE_MACSEC": "false", + "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", + "UNNUM_DHCP_START_INTERNAL": "", + "MACSEC_REPORT_TIMER": "", + "PFC_WATCH_INT_PREV": "", + "PREMSO_PARENT_FABRIC": "", + "MPLS_ISIS_AREA_NUM": "0001", + "UNNUM_DHCP_END_INTERNAL": "", + "PTP_DOMAIN_ID": "", + "USE_LINK_LOCAL": "false", + "AUTO_SYMMETRIC_VRF_LITE": "false", + "BGP_AS_PREV": "65001", + "ENABLE_PBR": "false", + "DCI_SUBNET_TARGET_MASK": "30", + "ENABLE_TRMv6": "false", + "VPC_PEER_LINK_PO": "500", + "ISIS_AUTH_ENABLE": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "REPLICATION_MODE": "Multicast", + "ENABLE_DCI_MACSEC_PREV": "false", + "SITE_ID_POLICY_ID": "", + "SGT_NAME_PREFIX": "", + "ANYCAST_RP_IP_RANGE": "10.254.254.0/24", + "VPC_ENABLE_IPv6_ND_SYNC": "true", + "abstract_isis_interface": "isis_interface", + "TCAM_ALLOCATION": "true", + "ENABLE_RT_INTF_STATS": "false", + "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", + "MACSEC_ALGORITHM": "", + "ISIS_LEVEL": "level-2", + "SUBNET_TARGET_MASK": "30", + "abstract_anycast_rp": "anycast_rp", + "AUTO_SYMMETRIC_DEFAULT_VRF": "false", + "ENABLE_NETFLOW": "false", + "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", + "PER_VRF_LOOPBACK_IP_RANGE_V6": "", + "temp_vpc_peer_link": "int_vpc_peer_link_po", + "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", + "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", + "IBGP_PEER_TEMPLATE_LEAF": "", + "DCI_SUBNET_RANGE": "10.33.0.0/16", + "MGMT_GW_INTERNAL": "", + "ENABLE_NXAPI": "true", + "VRF_LITE_AUTOCONFIG": "Manual", + "GRFIELD_DEBUG_FLAG": "Disable", + "VRF_VLAN_RANGE": "2000-2299", + "ISIS_AUTH_KEYCHAIN_NAME": "", + "OBJECT_TRACKING_NUMBER_RANGE": "100-299", + "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", + "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", + "OSPF_AUTH_KEY_ID": "", + "PIM_HELLO_AUTH_ENABLE": "false", + "abstract_feature_leaf": "base_feature_leaf_upg", + "BFD_AUTH_ENABLE": "false", + "INTF_STAT_LOAD_INTERVAL": "", + "BGP_LB_ID": "0", + "LOOPBACK1_IP_RANGE": "10.3.0.0/22", + "AGG_ACC_VPC_PO_ID_RANGE": "", + "EXTRA_CONF_TOR": "", + "AAA_SERVER_CONF": "", + "VPC_PEER_KEEP_ALIVE_OPTION": "management", + "AUTO_VRFLITE_IFC_DEFAULT_VRF": "false", + "enableRealTimeBackup": "", + "DCI_MACSEC_KEY_STRING": "", + "ENABLE_AI_ML_QOS_POLICY": "false", + "V6_SUBNET_TARGET_MASK": "126", + "STRICT_CC_MODE": "false", + "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", + "VPC_PEER_LINK_VLAN": "3600", + "abstract_trunk_host": "int_trunk_host", + "NXAPI_HTTP_PORT": "80", + "MST_INSTANCE_RANGE": "", + "BGP_AUTH_ENABLE": "false", + "PM_ENABLE_PREV": "false", + "NXC_PROXY_PORT": "8080", + "ENABLE_AGG_ACC_ID_RANGE": "false", + "RP_MODE": "asm", + "enableScheduledBackup": "", + "abstract_ospf_interface": "ospf_interface_11_1", + "BFD_OSPF_ENABLE": "false", + "MACSEC_FALLBACK_ALGORITHM": "", + "UNNUM_DHCP_END": "", + "LOOPBACK0_IP_RANGE": "10.2.0.0/22", + "ENABLE_AAA": "false", + "DEPLOYMENT_FREEZE": "false", + "L2_HOST_INTF_MTU_PREV": "9216", + "SGT_RECALC_STATUS": "empty", + "NETFLOW_MONITOR_LIST": "", + "ENABLE_AGENT": "false", + "NTP_SERVER_IP_LIST": "", + "MACSEC_FALLBACK_KEY_STRING": "", + "OVERLAY_MODE": "cli", + "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", + "FF": "Easy_Fabric", + "STP_ROOT_OPTION": "unmanaged", + "FABRIC_TYPE": "Switch_Fabric", + "ISIS_OVERLOAD_ENABLE": "false", + "NETFLOW_RECORD_LIST": "", + "SPINE_COUNT": "0", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6": "false", + "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", + "L3VNI_IPv6_MCAST_GROUP": "", + "MPLS_LOOPBACK_IP_RANGE": "", + "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", + "DHCP_ENABLE": "false", + "BFD_AUTH_KEY_ID": "", + "ALLOW_L3VNI_NO_VLAN": "true", + "MSO_SITE_GROUP_NAME": "", + "MGMT_PREFIX_INTERNAL": "", + "DHCP_IPV6_ENABLE_INTERNAL": "", + "BGP_AUTH_KEY_TYPE": "3", + "SITE_ID": "65001", + "temp_anycast_gateway": "anycast_gateway", + "BRFIELD_DEBUG_FLAG": "Disable", + "BGP_AS": "65001", + "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", + "ISIS_P2P_ENABLE": "false", + "ENABLE_NGOAM": "true", + "CDP_ENABLE": "false", + "PTP_LB_ID": "", + "DHCP_IPV6_ENABLE": "", + "MACSEC_KEY_STRING": "", + "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", + "ENABLE_L3VNI_NO_VLAN": "false", + "KME_SERVER_PORT": "", + "OSPF_AUTH_KEY": "", + "QKD_PROFILE_NAME": "", + "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", + "MVPN_VRI_ID_RANGE": "", + "ENABLE_DCI_MACSEC": "false", + "EXTRA_CONF_LEAF": "", + "ENABLE_AI_ML_QOS_POLICY_FLAP": "false", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "DHCP_START": "", + "ENABLE_TRM": "false", + "ENABLE_PVLAN_PREV": "false", + "FEATURE_PTP_INTERNAL": "false", + "SGT_PREPROV_RECALC_STATUS": "empty", + "ENABLE_NXAPI_HTTP": "true", + "abstract_isis": "base_isis_level2", + "MPLS_LB_ID": "", + "FABRIC_VPC_DOMAIN_ID_PREV": "", + "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", + "NETWORK_VLAN_RANGE": "2300-2999", + "STATIC_UNDERLAY_IP_ALLOC": "false", + "MGMT_V6PREFIX_INTERNAL": "", + "MPLS_HANDOFF": "false", + "STP_BRIDGE_PRIORITY": "", + "scheduledTime": "", + "ANYCAST_LB_ID": "", + "MACSEC_CIPHER_SUITE": "", + "STP_VLAN_RANGE": "", + "MSO_CONTROLER_ID": "", + "POWER_REDUNDANCY_MODE": "ps-redundant", + "BFD_ENABLE": "false", + "abstract_extra_config_leaf": "extra_config_leaf", + "ANYCAST_GW_MAC": "2020.0000.00aa", + "abstract_dhcp": "base_dhcp", + "default_pvlan_sec_network": "", + "EXTRA_CONF_SPINE": "", + "NTP_SERVER_VRF": "", + "SPINE_SWITCH_CORE_INTERFACES": "", + "ENABLE_VRI_ID_REALLOC": "false", + "LINK_STATE_ROUTING_TAG": "UNDERLAY", + "ISIS_OVERLOAD_ELAPSE_TIME": "", + "DCI_MACSEC_ALGORITHM": "", + "RP_LB_ID": "254", + "AI_ML_QOS_POLICY": "AI_Fabric_QOS_400G", + "PTP_VLAN_ID": "", + "BOOTSTRAP_CONF": "", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV": "false", + "LINK_STATE_ROUTING": "ospf", + "ISIS_AUTH_KEY": "", + "network_extension_template": "Default_Network_Extension_Universal", + "DNS_SERVER_IP_LIST": "", + "DOMAIN_NAME_INTERNAL": "", + "ENABLE_EVPN": "true", + "abstract_multicast": "base_multicast_11_1", + "VPC_DELAY_RESTORE_TIME": "60", + "BFD_AUTH_KEY": "", + "IPv6_MULTICAST_GROUP_SUBNET": "", + "AGENT_INTF": "eth0", + "FABRIC_MTU": "9216", + "QKD_PROFILE_NAME_PREV": "", + "L3VNI_MCAST_GROUP": "", + "UNNUM_BOOTSTRAP_LB_ID": "", + "HOST_INTF_ADMIN_STATE": "true", + "VPC_DOMAIN_ID_RANGE": "1-1000", + "ALLOW_L3VNI_NO_VLAN_PREV": "true", + "BFD_IBGP_ENABLE": "false", + "SGT_PREPROVISION": "false", + "DCI_MACSEC_FALLBACK_KEY_STRING": "", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", + "IPv6_ANYCAST_RP_IP_RANGE_INTERNAL": "", + "DCI_MACSEC_FALLBACK_ALGORITHM": "", + "VPC_AUTO_RECOVERY_TIME": "360", + "DNS_SERVER_VRF": "", + "UPGRADE_FROM_VERSION": "", + "ISIS_AREA_NUM": "0001", + "BANNER": "", + "NXC_SRC_INTF": "", + "SGT_ID_RANGE": "", + "ENABLE_QKD": "false", + "PER_VRF_LOOPBACK_IP_RANGE": "", + "SGT_PREPROVISION_PREV": "false", + "SYSLOG_SEV": "", + "abstract_loopback_interface": "int_fabric_loopback_11_1", + "SYSLOG_SERVER_VRF": "", + "EXTRA_CONF_INTRA_LINKS": "", + "SNMP_SERVER_HOST_TRAP": "true", + "abstract_extra_config_spine": "extra_config_spine", + "PIM_HELLO_AUTH_KEY": "", + "KME_SERVER_IP": "", + "temp_vpc_domain_mgmt": "vpc_domain_mgmt", + "V6_SUBNET_RANGE": "", + "SUBINTERFACE_RANGE": "2-511", + "abstract_routed_host": "int_routed_host", + "BGP_AUTH_KEY": "", + "INBAND_DHCP_SERVERS": "", + "ENABLE_PVLAN": "false", + "MPLS_ISIS_AREA_NUM_PREV": "", + "default_network": "Default_Network_Universal", + "PFC_WATCH_INT": "", + "ISIS_AUTH_KEYCHAIN_KEY_ID": "", + "MGMT_V6PREFIX": "", + "abstract_feature_spine": "base_feature_spine_upg", + "ENABLE_DEFAULT_QUEUING_POLICY": "false", + "PNP_ENABLE_INTERNAL": "", + "ANYCAST_BGW_ADVERTISE_PIP": "false", + "NETFLOW_EXPORTER_LIST": "", + "abstract_vlan_interface": "int_fabric_vlan_11_1", + "RP_COUNT": "2", + "FABRIC_NAME": "VXLAN", + "abstract_pim_interface": "pim_interface", + "PM_ENABLE": "false", + "LOOPBACK0_IPV6_RANGE": "", + "IGNORE_CERT": "false", + "DEFAULT_VRF_REDIS_BGP_RMAP": "", + "NVE_LB_ID": "1", + "OVERLAY_MODE_PREV": "cli", + "VPC_DELAY_RESTORE": "150", + "IPv6_ANYCAST_RP_IP_RANGE": "", + "UNDERLAY_IS_V6_PREV": "false", + "SGT_OPER_STATUS": "off", + "NXAPI_HTTPS_PORT": "443", + "ENABLE_SGT_PREV": "false", + "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", + "L2_HOST_INTF_MTU": "9216", + "abstract_route_map": "route_map", + "TRUSTPOINT_LABEL": "", + "INBAND_MGMT_PREV": "false", + "EXT_FABRIC_TYPE": "", + "abstract_vpc_domain": "base_vpc_domain_11_1", + "ACTIVE_MIGRATION": "false", + "ISIS_AREA_NUM_PREV": "", + "COPP_POLICY": "strict", + "DHCP_END_INTERNAL": "", + "DCI_MACSEC_CIPHER_SUITE": "", + "BOOTSTRAP_ENABLE": "false", + "default_vrf": "Default_VRF_Universal", + "ADVERTISE_PIP_ON_BORDER": "true", + "NXC_PROXY_SERVER": "", + "OSPF_AREA_ID": "0.0.0.0", + "abstract_extra_config_tor": "extra_config_tor", + "SYSLOG_SERVER_IP_LIST": "", + "BOOTSTRAP_ENABLE_PREV": "false", + "ENABLE_TENANT_DHCP": "true", + "ANYCAST_RP_IP_RANGE_INTERNAL": "", + "RR_COUNT": "2", + "BOOTSTRAP_MULTISUBNET_INTERNAL": "", + "MGMT_GW": "", + "UNNUM_DHCP_START": "", + "MGMT_PREFIX": "", + "BFD_ENABLE_PREV": "", + "abstract_bgp_rr": "evpn_bgp_rr", + "INBAND_MGMT": "false", + "abstract_bgp": "base_bgp", + "SLA_ID_RANGE": "10000-19999", + "ENABLE_NETFLOW_PREV": "false", + "SUBNET_RANGE": "10.4.0.0/16", + "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", + "MULTICAST_GROUP_SUBNET": "239.1.1.0/25", + "FABRIC_INTERFACE_TYPE": "p2p", + "ALLOW_NXC": "true", + "OVERWRITE_GLOBAL_NXC": "false", + "FABRIC_VPC_QOS": "false", + "AAA_REMOTE_IP_ENABLED": "false", + "L2_SEGMENT_ID_RANGE": "30000-49000" + }, + "vrfTemplate": "Default_VRF_Universal", + "networkTemplate": "Default_Network_Universal", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "createdOn": 1737248896991, + "modifiedOn": 1737248902373 + }, + "fabric_details": { + "createdOn": 1613750822779, + "deviceType": "n9k", + "fabricId": "FABRIC-15", + "fabricName": "MS-fabric", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN Fabric", + "fabricType": "MFD", + "fabricTypeFriendly": "Multi-Fabric Domain", + "id": 15, + "modifiedOn": 1613750822779, + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplate": "Default_Network_Universal", + "nvPairs": { + "ANYCAST_GW_MAC": "2020.0000.00aa", + "BGP_RP_ASN": "", + "BORDER_GWY_CONNECTIONS": "Direct_To_BGWS", + "CLOUDSEC_ALGORITHM": "", + "CLOUDSEC_AUTOCONFIG": "false", + "CLOUDSEC_ENFORCEMENT": "", + "CLOUDSEC_KEY_STRING": "", + "DCI_SUBNET_RANGE": "10.10.1.0/24", + "DCI_SUBNET_TARGET_MASK": "30", + "DELAY_RESTORE": "300", + "FABRIC_NAME": "MS-fabric", + "FABRIC_TYPE": "MFD", + "FF": "MSD", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "L3_PARTITION_ID_RANGE": "50000-59000", + "LOOPBACK100_IP_RANGE": "10.10.0.0/24", + "MS_LOOPBACK_ID": "100", + "MS_UNDERLAY_AUTOCONFIG": "true", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "RP_SERVER_IP": "", + "TOR_AUTO_DEPLOY": "false", + "default_network": "Default_Network_Universal", + "default_vrf": "Default_VRF_Universal", + "enableScheduledBackup": "false", + "network_extension_template": "Default_Network_Extension_Universal", + "scheduledTime": "", + "vrf_extension_template": "Default_VRF_Extension_Universal" + }, + "provisionMode": "DCNMTopDown", + "replicationMode": "IngressReplication", + "templateName": "MSD_Fabric_11_1", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplate": "Default_VRF_Universal" + }, + "mock_vrf12_object": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE":"OK", + "DATA": [ + { + "fabric": "test_fabric", + "serviceVrfTemplate": "None", + "source": "None", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfId": 9008011, + "vrfName": "test_vrf_1", + "vrfTemplate": "Default_VRF_Universal", + "vrfTemplateConfig": "{\"advertiseDefaultRouteFlag\":\"true\",\"routeTargetImport\":\"\",\"vrfVlanId\":\"202\",\"isRPExternal\":\"false\",\"vrfDescription\":\"\",\"disableRtAuto\":\"false\",\"L3VniMcastGroup\":\"\",\"vrfSegmentId\":\"9008013\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"routeTargetExport\":\"\",\"ipv6LinkLocalFlag\":\"true\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"routeTargetExportMvpn\":\"\",\"ENABLE_NETFLOW\":\"false\",\"configureStaticDefaultRouteFlag\":\"true\",\"tag\":\"12345\",\"rpAddress\":\"\",\"trmBGWMSiteEnabled\":\"false\",\"nveId\":\"1\",\"routeTargetExportEvpn\":\"\",\"NETFLOW_MONITOR\":\"\",\"bgpPasswordKeyType\":\"3\",\"bgpPassword\":\"\",\"mtu\":\"9216\",\"multicastGroup\":\"\",\"routeTargetImportMvpn\":\"\",\"isRPAbsent\":\"false\",\"advertiseHostRouteFlag\":\"false\",\"vrfVlanName\":\"\",\"trmEnabled\":\"false\",\"loopbackNumber\":\"\",\"asn\":\"52125\",\"vrfIntfDescription\":\"\",\"vrfName\":\"test_vrf_1\",\"routeTargetImportEvpn\":\"\"}", + "vrfStatus": "DEPLOYED" + } + ] + }, + "mock_pools_top_down_vrf_vlan": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE":"OK", + "DATA": [ + { + "allocatedFlag": true, + "allocatedIp": "201", + "allocatedOn": 1734051260507, + "allocatedScopeValue": "FDO211218HH", + "entityName": "VRF_1", + "entityType": "Device", + "hierarchicalKey": "0", + "id": 36407, + "ipAddress": "172.22.150.104", + "resourcePool": { + "dynamicSubnetRange": null, + "fabricName": "f1", + "hierarchicalKey": "f1", + "id": 0, + "overlapAllowed": false, + "poolName": "TOP_DOWN_VRF_VLAN", + "poolType": "ID_POOL", + "targetSubnet": 0, + "vrfName": "VRF_1" + }, + "switchName": "cvd-1313-leaf" + } + ] + }, + "mock_vrf_lite_obj": { + "RETURN_CODE":200, + "METHOD":"GET", + "MESSAGE":"OK", + "DATA": [ + { + "vrfName":"test_vrf", + "templateName":"Default_VRF_Extension_Universal", + "switchDetailsList":[ + { + "switchName":"poap_test", + "vlan":2001, + "serialNumber":"9D2DAUJJFQQ", + "peerSerialNumber":"None", + "extensionValues":"", + "extensionPrototypeValues":[ + { + "interfaceName":"Ethernet1/3", + "extensionType":"VRF_LITE", + "extensionValues":"{\"PEER_VRF_NAME\": \"\", \"NEIGHBOR_IP\": \"10.33.0.1\", \"VRF_LITE_JYTHON_TEMPLATE\": \"Ext_VRF_Lite_Jython\", \"enableBorderExtension\": \"VRF_LITE\", \"AUTO_VRF_LITE_FLAG\":\"false\", \"IP_MASK\": \"10.33.0.2/30\", \"MTU\": \"9216\", \"NEIGHBOR_ASN\": \"23132\", \"IF_NAME\": \"Ethernet1/3\", \"IPV6_NEIGHBOR\": \"\", \"IPV6_MASK\": \"\", \"DOT1Q_ID\": \"2\", \"asn\": \"52125\"}", + "destInterfaceName":"Ethernet1/1", + "destSwitchName":"poap-import-static" + }, + { + "interfaceName":"Ethernet1/2", + "extensionType":"VRF_LITE", + "extensionValues":"{\"PEER_VRF_NAME\": \"\", \"NEIGHBOR_IP\": \"20.33.0.1\", \"VRF_LITE_JYTHON_TEMPLATE\": \"Ext_VRF_Lite_Jython\", \"enableBorderExtension\": \"VRF_LITE\", \"AUTO_VRF_LITE_FLAG\": \"false\", \"IP_MASK\": \"20.33.0.2/30\", \"MTU\": \"9216\", \"NEIGHBOR_ASN\": \"23132\", \"IF_NAME\": \"Ethernet1/2\", \"IPV6_NEIGHBOR\": \"\", \"IPV6_MASK\": \"\", \"DOT1Q_ID\": \"2\", \"asn\": \"52125\"}", + "destInterfaceName":"Ethernet1/2", + "destSwitchName":"poap-import-static" + } + ], + "islanAttached":false, + "lanAttachedState":"NA", + "errorMessage":"None", + "instanceValues":"", + "freeformConfig":"None", + "role":"border gateway", + "vlanModifiable":true + } + ] + } + ] + } +} diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_vrf_12.json b/tests/unit/modules/dcnm/fixtures/dcnm_vrf_12.json new file mode 100644 index 000000000..52ddedb16 --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_vrf_12.json @@ -0,0 +1,1618 @@ +{ + "mock_ip_sn" : { + "10.10.10.224": "XYZKSJHSMK1", + "10.10.10.225": "XYZKSJHSMK2", + "10.10.10.226": "XYZKSJHSMK3", + "10.10.10.227": "XYZKSJHSMK4", + "10.10.10.228": "XYZKSJHSMK5" + }, + "mock_ip_fab" : { + "10.10.10.224": "test_fabric", + "10.10.10.225": "test_fabric", + "10.10.10.226": "test_fabric", + "10.10.10.227": "test_fabric", + "10.10.10.228": "test_fabric" + }, + "mock_sn_fab" : { + "XYZKSJHSMK1": "test_fabric", + "XYZKSJHSMK2": "test_fabric", + "XYZKSJHSMK3": "test_fabric", + "XYZKSJHSMK4": "test_fabric", + "XYZKSJHSMK5": "test_fabric" + }, + "playbook_config_input_validation" : [ + { + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "vlan_id": "203", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.225", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_no_attach_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None" + } + ], + "playbook_vrf_lite_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_redeploy_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_redeploy_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_new_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_new_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_override_with_additions_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_override_with_additions_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "deploy": true + }, + { + "ip_address": "10.10.10.227", + "export_evpn_rt": "5000:100", + "import_evpn_rt": "5000:100", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_override_with_deletions_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.11/30", + "neighbor_ipv4": "10.33.0.12", + "ipv6_addr": "2010::10:34:0:1/64", + "neighbor_ipv6": "2010::10:34:0:1", + "dot1q": "21", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_override_with_deletions_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/17", + "ipv4_addr": "10.33.0.11/30", + "neighbor_ipv4": "10.33.0.12", + "ipv6_addr": "2010::10:34:0:1/64", + "neighbor_ipv6": "2010::10:34:0:1", + "dot1q": "21", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_replace_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/17", + "ipv4_addr": "10.33.0.11/30", + "neighbor_ipv4": "10.33.0.12", + "ipv6_addr": "2010::10:34:0:1/64", + "neighbor_ipv6": "2010::10:34:0:1", + "dot1q": "21", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_replace_config_interface_with_extension_values" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.11/30", + "neighbor_ipv4": "10.33.0.12", + "ipv6_addr": "2010::10:34:0:1/64", + "neighbor_ipv6": "2010::10:34:0:1", + "dot1q": "21", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_update_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_merged_lite_update_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_inv_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "202", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.225", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_update": [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "203", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.226", + "deploy": true + }, + { + "ip_address": "10.10.10.225", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_update_vlan": [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "303", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.225", + "deploy": true + }, + { + "ip_address": "10.10.10.226", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_update_vlan_config_interface_with_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "402", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/2", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_vrf_lite_update_vlan_config_interface_without_extensions" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "402", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + }, + { + "ip_address": "10.10.10.228", + "vrf_lite": [ + { + "interface": "Ethernet1/16", + "ipv4_addr": "10.33.0.2/30", + "neighbor_ipv4": "10.33.0.1", + "ipv6_addr": "2010::10:34:0:7/64", + "neighbor_ipv6": "2010::10:34:0:3", + "dot1q": "2", + "peer_vrf": "test_vrf_1" + } + ], + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_override": [ + { + "vrf_name": "test_vrf_2", + "vrf_id": "9008012", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "303", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.225", + "deploy": true + }, + { + "ip_address": "10.10.10.226", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_incorrect_vrfid": [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008012", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "vlan_id": "202", + "deploy": true + }, + { + "ip_address": "10.10.10.225", + "vlan_id": "203", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_replace": [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "203", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.225", + "deploy": true + }, + { + "ip_address": "10.10.10.226", + "deploy": true + } + ], + "deploy": true + } + ], + "playbook_config_replace_no_atch": [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "source": "None", + "service_vrf_template": "None" + } + ], + "mock_vrf_attach_object_del_not_ready": { + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "lanAttachState": "DEPLOYED", + "isLanAttached": false + }, + { + "lanAttachState": "DEPLOYED", + "isLanAttached": false + } + ] + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/attachments?vrf-names=test_vrf_1", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_object_del_oos": { + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "lanAttachState": "OUT-OF-SYNC" + }, + { + "lanAttachState": "OUT-OF-SYNC" + } + ] + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/attachments?vrf-names=test_vrf_1", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_object_del_ready": { + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "lanAttachState": "NA", + "isLanAttached": false + }, + { + "lanAttachState": "NA", + "isLanAttached": false + } + ] + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/attachments?vrf-names=test_vrf_1", + "RETURN_CODE": 200 + }, + "mock_vrf_object": { + "DATA": [ + { + "fabric": "test_fabric", + "serviceVrfTemplate": "None", + "source": "None", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfId": 9008011, + "vrfName": "test_vrf_1", + "vrfTemplate": "Default_VRF_Universal", + "vrfTemplateConfig": "{\"advertiseDefaultRouteFlag\":\"true\",\"vrfVlanId\":\"202\",\"isRPExternal\":\"false\",\"vrfDescription\":\"\",\"L3VniMcastGroup\":\"\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"vrfSegmentId\":\"9008013\",\"ipv6LinkLocalFlag\":\"true\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"configureStaticDefaultRouteFlag\":\"true\",\"trmBGWMSiteEnabled\":\"false\",\"tag\":\"12345\",\"rpAddress\":\"\",\"nveId\":\"1\",\"bgpPasswordKeyType\":\"3\",\"bgpPassword\":\"\",\"mtu\":\"9216\",\"multicastGroup\":\"\",\"advertiseHostRouteFlag\":\"false\",\"vrfVlanName\":\"\",\"trmEnabled\":\"false\",\"loopbackNumber\":\"\",\"asn\":\"34343\",\"vrfIntfDescription\":\"\",\"vrfName\":\"test_vrf_1\"}", + "vrfStatus": "DEPLOYED" + } + ], + "ERROR": "", + "MESSAGE":"OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_object" : { + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "202", + "vrfId": "9008011" + }, + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf2", + "lanAttachState": "DEPLOYED", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK2", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.225", + "vlanId": "202", + "vrfId": "9008011" + } + ] + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test-fabric/vrfs/attachments?vrf-names=test_vrf_1", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_object_query" : { + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "202", + "vrfId": "9008011" + }, + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf2", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK2", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.225", + "vlanId": "202", + "vrfId": "9008011" + } + ] + } + ], + "MESSAGE":"OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test-fabric/vrfs/attachments?vrf-names=test_vrf_1", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_object2" : { + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "202", + "vrfId": "9008011" + }, + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf4", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK4", + "switchRole": "border", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.227", + "vlanId": "202", + "vrfId": "9008011" + } + ] + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test-fabric/vrfs/attachments?vrf-names=test_vrf_1", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_object2_query" : { + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "202", + "vrfId": "9008011" + }, + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf4", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK4", + "switchRole": "border", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.227", + "vlanId": "202", + "vrfId": "9008011" + } + ] + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test-fabric/vrfs/attachments?vrf-names=test_vrf_1", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_lite_object" : { + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "deployment": true, + "extensionValues": "", + "fabric": "test_fabric", + "freeformConfig": "", + "instanceValues": "", + "serialNumber": "XYZKSJHSMK1", + "vlan": 202, + "vrfName": "test_vrf_1", + "vrf_lite": [] + }, + { + "deployment": true, + "extensionValues": "{\"VRF_LITE_CONN\":\"{\\\"VRF_LITE_CONN\\\":[{\\\"IF_NAME\\\":\\\"Ethernet1/16\\\",\\\"DOT1Q_ID\\\":\\\"2\\\",\\\"IP_MASK\\\":\\\"10.33.0.2/30\\\",\\\"NEIGHBOR_IP\\\":\\\"10.33.0.1\\\",\\\"NEIGHBOR_ASN\\\":\\\"65535\\\",\\\"IPV6_MASK\\\":\\\"2010::10:34:0:7/64\\\",\\\"IPV6_NEIGHBOR\\\":\\\"2010::10:34:0:3\\\",\\\"AUTO_VRF_LITE_FLAG\\\":\\\"false\\\",\\\"PEER_VRF_NAME\\\":\\\"ansible-vrf-int1\\\",\\\"VRF_LITE_JYTHON_TEMPLATE\\\":\\\"Ext_VRF_Lite_Jython\\\"}]}\",\"MULTISITE_CONN\":\"{\\\"MULTISITE_CONN\\\":[]}\"}", + "fabric": "test_fabric", + "freeformConfig": "", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "serialNumber": "XYZKSJHSMK4", + "vlan": 202, + "vrfName": "test_vrf_1" + } + ] + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test-fabric/vrfs/attachments?vrf-names=test_vrf_1", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_object_pending": { + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "PENDING", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "202", + "vrfId": "9008011" + }, + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf2", + "lanAttachState": "PENDING", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK2", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.225", + "vlanId": "202", + "vrfId": "9008011" + } + ] + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test-fabric/vrfs/attachments?vrf-names=test_vrf_1", + "RETURN_CODE": 200 + }, + "mock_vrf_object_dcnm_only": { + "DATA": [ + { + "fabric": "test_fabric", + "vrfName": "test_vrf_dcnm", + "vrfTemplate": "Default_VRF_Universal", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "serviceVrfTemplate": "None", + "source": "None", + "vrfStatus": "DEPLOYED", + "vrfTemplateConfig": "{\"advertiseDefaultRouteFlag\":\"true\",\"vrfVlanId\":\"202\",\"isRPExternal\":\"false\",\"vrfDescription\":\"\",\"L3VniMcastGroup\":\"\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"vrfSegmentId\":\"9008013\",\"ipv6LinkLocalFlag\":\"true\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"configureStaticDefaultRouteFlag\":\"true\",\"trmBGWMSiteEnabled\":\"false\",\"tag\":\"12345\",\"rpAddress\":\"\",\"nveId\":\"1\",\"bgpPasswordKeyType\":\"3\",\"bgpPassword\":\"\",\"mtu\":\"9216\",\"multicastGroup\":\"\",\"advertiseHostRouteFlag\":\"false\",\"vrfVlanName\":\"\",\"trmEnabled\":\"false\",\"loopbackNumber\":\"\",\"asn\":\"34343\",\"vrfIntfDescription\":\"\",\"vrfName\":\"test_vrf_dcnm\"}", + "vrfId": "9008013" + } + ], + "ERROR": "", + "MESSAGE":"OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_object_dcnm_only": { + "DATA": [ + { + "vrfName": "test_vrf_dcnm", + "lanAttachList": [ + { + "vrfName": "test_vrf_dcnm", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "402", + "vrfId": "9008013" + }, + { + "vrfName": "test_vrf_dcnm", + "switchName": "n9kv_leaf2", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK2", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.225", + "vlanId": "403", + "vrfId": "9008013" + } + ] + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test-fabric/vrfs/switches?vrf-names=test_vrf_dcnm&serial-numbers=XYZKSJHSMK1,XYZKSJHSMK2", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_get_ext_object_dcnm_att1_only": { + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "{\"loopbackId\":\"10\",\"loopbackIpAddress\":\"11.1.1.1\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK1", + "switchName": "n9kv_leaf1", + "vlan": "402", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_dcnm" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/switches?vrf-names=test_vrf_dcnm&serial-numbers=XYZKSJHSMK1", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_get_ext_object_dcnm_att2_only": { + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "{\"loopbackId\":\"10\",\"loopbackIpAddress\":\"11.1.1.1\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK2", + "switchName": "n9kv_leaf2", + "vlan": "403", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_dcnm" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/switches?vrf-names=test_vrf_dcnm&serial-numbers=XYZKSJHSMK1", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_get_ext_object_merge_att1_only": { + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK1", + "switchName": "n9kv_leaf1", + "vlan": "202", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/switches?vrf-names=test_vrf_1&serial-numbers=XYZKSJHSMK1", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_get_ext_object_merge_att2_only": { + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK2", + "switchName": "n9kv_leaf2", + "vlan": "202", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/switches?vrf-names=test_vrf_1&serial-numbers=XYZKSJHSMK2", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_get_ext_object_ov_att1_only": { + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK1", + "switchName": "n9kv_leaf1", + "vlan": "303", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/switches?vrf-names=test_vrf_1&serial-numbers=XYZKSJHSMK2", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_get_ext_object_ov_att2_only": { + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK2", + "switchName": "n9kv_leaf2", + "vlan": "303", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/switches?vrf-names=test_vrf_1&serial-numbers=XYZKSJHSMK2", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_get_ext_object_merge_att3_only": { + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK1", + "switchName": "n9kv_leaf1", + "vlan": "202", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/switches?vrf-names=test_vrf_1&serial-numbers=XYZKSJHSMK1", + "RETURN_CODE": 200 + }, + "mock_vrf_attach_get_ext_object_merge_att4_only": { + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [ + { + "destInterfaceName": "Ethernet1/16", + "destSwitchName": "dt-n9k2-1", + "extensionType": "VRF_LITE", + "extensionValues": "{\"PEER_VRF_NAME\": \"\", \"NEIGHBOR_IP\": \"10.33.0.1\", \"VRF_LITE_JYTHON_TEMPLATE\": \"Ext_VRF_Lite_Jython\", \"enableBorderExtension\": \"VRF_LITE\", \"AUTO_VRF_LITE_FLAG\": \"false\", \"IP_MASK\": \"10.33.0.2/30\", \"MTU\": \"9216\", \"NEIGHBOR_ASN\": \"65535\", \"IF_NAME\": \"Ethernet1/16\", \"IPV6_NEIGHBOR\": \"\", \"IPV6_MASK\": \"\", \"DOT1Q_ID\": \"3\", \"asn\": \"65535\"}", + "interfaceName": "Ethernet1/16" + } + ], + "extensionValues": "{\"VRF_LITE_CONN\":\"{\\\"VRF_LITE_CONN\\\":[{\\\"IF_NAME\\\":\\\"Ethernet1/16\\\",\\\"DOT1Q_ID\\\":\\\"2\\\",\\\"IP_MASK\\\":\\\"10.33.0.2/30\\\",\\\"NEIGHBOR_IP\\\":\\\"10.33.0.1\\\",\\\"NEIGHBOR_ASN\\\":\\\"65535\\\",\\\"IPV6_MASK\\\":\\\"2010::10:34:0:7/64\\\",\\\"IPV6_NEIGHBOR\\\":\\\"2010::10:34:0:3\\\",\\\"AUTO_VRF_LITE_FLAG\\\":\\\"false\\\",\\\"PEER_VRF_NAME\\\":\\\"test_vrf_1\\\",\\\"VRF_LITE_JYTHON_TEMPLATE\\\":\\\"Ext_VRF_Lite_Jython\\\"}]}\",\"MULTISITE_CONN\":\"{\\\"MULTISITE_CONN\\\":[]}\"}", + "freeformConfig": "", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\"}", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "border", + "serialNumber": "XYZKSJHSMK4", + "switchName": "n9kv_leaf4", + "vlan": 202, + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Extension_Universal", + "vrfName": "test_vrf_1" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/switches?vrf-names=test_vrf_1&serial-numbers=XYZKSJHSMK4", + "RETURN_CODE": 200 + }, + "attach_success_resp": { + "DATA": { + "test-vrf-1--XYZKSJHSMK1(leaf1)": "SUCCESS", + "test-vrf-1--XYZKSJHSMK2(leaf2)": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/f1/vrfs/attachments", + "RETURN_CODE": 200 + }, + "attach_success_resp2": { + "DATA": { + "test-vrf-2--XYZKSJHSMK2(leaf2)": "SUCCESS", + "test-vrf-2--XYZKSJHSMK3(leaf3)": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/f1/vrfs/attachments", + "RETURN_CODE": 200 + }, + "attach_success_resp3": { + "DATA": { + "test-vrf-1--XYZKSJHSMK2(leaf1)": "SUCCESS", + "test-vrf-1--XYZKSJHSMK3(leaf4)": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/f1/vrfs/attachments", + "RETURN_CODE": 200 + }, + "deploy_success_resp": { + "DATA": { + "status": "Deployment of VRF(s) has been initiated successfully" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/f1/vrfs/deployments", + "RETURN_CODE": 200 + }, + "blank_data": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200 + }, + "get_have_failure": { + "DATA": "Invalid JSON response: Invalid Fabric: demo-fabric-123", + "ERROR": "Not Found", + "METHOD": "GET", + "RETURN_CODE": 404, + "MESSAGE": "OK" + }, + "error1": { + "DATA": "None", + "ERROR": "There is an error", + "METHOD": "POST", + "RETURN_CODE": 400, + "MESSAGE": "OK" + }, + "error2": { + "DATA": { + "test-vrf-1--XYZKSJHSMK1(leaf1)": "Entered VRF VLAN ID 203 is in use already", + "test-vrf-1--XYZKSJHSMK2(leaf2)": "SUCCESS" + }, + "ERROR": "", + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/f1/vrfs/attachments", + "RETURN_CODE": 200 + }, + "error3": { + "DATA": "No switches PENDING for deployment", + "ERROR": "", + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/f1/vrfs/deployments", + "RETURN_CODE": 200 + }, + "delete_success_resp": { + "ERROR": "", + "METHOD": "POST", + "RETURN_CODE": 200, + "MESSAGE": "OK" + }, + "vrf_inv_data": { + "10.10.10.224":{ + "fabricName": "MS-fabric", + "fabricTechnology": "VXLANFabric", + "ipAddress": "10.10.10.224", + "logicalName": "dt-n9k1", + "serialNumber": "XYZKSJHSMK1", + "switchRole": "leaf" + }, + "10.10.10.225":{ + "fabricName": "MS-fabric", + "fabricTechnology": "VXLANFabric", + "ipAddress": "10.10.10.225", + "logicalName": "dt-n9k2", + "serialNumber": "XYZKSJHSMK2", + "switchRole": "leaf" + }, + "10.10.10.226":{ + "fabricName": "MS-fabric", + "fabricTechnology": "VXLANFabric", + "ipAddress": "10.10.10.226", + "logicalName": "dt-n9k3", + "serialNumber": "XYZKSJHSMK3", + "switchRole": "leaf" + }, + "10.10.10.227":{ + "fabricName": "MS-fabric", + "fabricTechnology": "VXLANFabric", + "ipAddress": "10.10.10.227", + "logicalName": "dt-n9k4", + "serialNumber": "XYZKSJHSMK4", + "switchRole": "border spine" + }, + "10.10.10.228":{ + "fabricName": "MS-fabric", + "fabricTechnology": "VXLANFabric", + "ipAddress": "10.10.10.228", + "logicalName": "dt-n9k5", + "serialNumber": "XYZKSJHSMK5", + "switchRole": "border" + } + }, + "fabric_details": { + "createdOn": 1613750822779, + "deviceType": "n9k", + "fabricId": "FABRIC-15", + "fabricName": "MS-fabric", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN Fabric", + "fabricType": "MFD", + "fabricTypeFriendly": "Multi-Fabric Domain", + "id": 15, + "modifiedOn": 1613750822779, + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplate": "Default_Network_Universal", + "nvPairs": { + "ANYCAST_GW_MAC": "2020.0000.00aa", + "BGP_RP_ASN": "", + "BORDER_GWY_CONNECTIONS": "Direct_To_BGWS", + "CLOUDSEC_ALGORITHM": "", + "CLOUDSEC_AUTOCONFIG": "false", + "CLOUDSEC_ENFORCEMENT": "", + "CLOUDSEC_KEY_STRING": "", + "DCI_SUBNET_RANGE": "10.10.1.0/24", + "DCI_SUBNET_TARGET_MASK": "30", + "DELAY_RESTORE": "300", + "FABRIC_NAME": "MS-fabric", + "FABRIC_TYPE": "MFD", + "FF": "MSD", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "L3_PARTITION_ID_RANGE": "50000-59000", + "LOOPBACK100_IP_RANGE": "10.10.0.0/24", + "MS_LOOPBACK_ID": "100", + "MS_UNDERLAY_AUTOCONFIG": "true", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "RP_SERVER_IP": "", + "TOR_AUTO_DEPLOY": "false", + "default_network": "Default_Network_Universal", + "default_vrf": "Default_VRF_Universal", + "enableScheduledBackup": "false", + "network_extension_template": "Default_Network_Extension_Universal", + "scheduledTime": "", + "vrf_extension_template": "Default_VRF_Extension_Universal" + }, + "provisionMode": "DCNMTopDown", + "replicationMode": "IngressReplication", + "templateName": "MSD_Fabric_11_1", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplate": "Default_VRF_Universal" + }, + "mock_vrf12_object": { + "DATA": [ + { + "fabric": "test_fabric", + "serviceVrfTemplate": "None", + "source": "None", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfId": 9008011, + "vrfName": "test_vrf_1", + "vrfTemplate": "Default_VRF_Universal", + "vrfTemplateConfig": "{\"advertiseDefaultRouteFlag\":\"true\",\"routeTargetImport\":\"\",\"vrfVlanId\":\"202\",\"isRPExternal\":\"false\",\"vrfDescription\":\"\",\"disableRtAuto\":\"false\",\"L3VniMcastGroup\":\"\",\"vrfSegmentId\":\"9008013\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"routeTargetExport\":\"\",\"ipv6LinkLocalFlag\":\"true\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"routeTargetExportMvpn\":\"\",\"ENABLE_NETFLOW\":\"false\",\"configureStaticDefaultRouteFlag\":\"true\",\"tag\":\"12345\",\"rpAddress\":\"\",\"trmBGWMSiteEnabled\":\"false\",\"nveId\":\"1\",\"routeTargetExportEvpn\":\"\",\"NETFLOW_MONITOR\":\"\",\"bgpPasswordKeyType\":\"3\",\"bgpPassword\":\"\",\"mtu\":\"9216\",\"multicastGroup\":\"\",\"routeTargetImportMvpn\":\"\",\"isRPAbsent\":\"false\",\"advertiseHostRouteFlag\":\"false\",\"vrfVlanName\":\"\",\"trmEnabled\":\"false\",\"loopbackNumber\":\"\",\"asn\":\"52125\",\"vrfIntfDescription\":\"\",\"vrfName\":\"test_vrf_1\",\"routeTargetImportEvpn\":\"\"}", + "vrfStatus": "DEPLOYED" + } + ], + "ERROR": "", + "MESSAGE":"OK", + "METHOD": "GET", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs", + "RETURN_CODE": 200 + }, + "mock_pools_top_down_vrf_vlan": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE":"OK", + "DATA": [ + { + "allocatedFlag": true, + "allocatedIp": "201", + "allocatedOn": 1734051260507, + "allocatedScopeValue": "FDO211218HH", + "entityName": "VRF_1", + "entityType": "Device", + "hierarchicalKey": "0", + "id": 36407, + "ipAddress": "172.22.150.104", + "resourcePool": { + "dynamicSubnetRange": null, + "fabricName": "f1", + "hierarchicalKey": "f1", + "id": 0, + "overlapAllowed": false, + "poolName": "TOP_DOWN_VRF_VLAN", + "poolType": "ID_POOL", + "targetSubnet": 0, + "vrfName": "VRF_1" + }, + "switchName": "cvd-1313-leaf" + } + ] + }, + "mock_vrf_lite_obj": { + "DATA": [ + { + "vrfName":"test_vrf", + "templateName":"Default_VRF_Extension_Universal", + "switchDetailsList":[ + { + "switchName":"poap_test", + "vlan":2001, + "serialNumber":"9D2DAUJJFQQ", + "peerSerialNumber":"None", + "extensionValues":"", + "extensionPrototypeValues":[ + { + "interfaceName":"Ethernet1/3", + "extensionType":"VRF_LITE", + "extensionValues":"{\"PEER_VRF_NAME\": \"\", \"NEIGHBOR_IP\": \"10.33.0.1\", \"VRF_LITE_JYTHON_TEMPLATE\": \"Ext_VRF_Lite_Jython\", \"enableBorderExtension\": \"VRF_LITE\", \"AUTO_VRF_LITE_FLAG\":\"false\", \"IP_MASK\": \"10.33.0.2/30\", \"MTU\": \"9216\", \"NEIGHBOR_ASN\": \"23132\", \"IF_NAME\": \"Ethernet1/3\", \"IPV6_NEIGHBOR\": \"\", \"IPV6_MASK\": \"\", \"DOT1Q_ID\": \"2\", \"asn\": \"52125\"}", + "destInterfaceName":"Ethernet1/1", + "destSwitchName":"poap-import-static" + }, + { + "interfaceName":"Ethernet1/2", + "extensionType":"VRF_LITE", + "extensionValues":"{\"PEER_VRF_NAME\": \"\", \"NEIGHBOR_IP\": \"20.33.0.1\", \"VRF_LITE_JYTHON_TEMPLATE\": \"Ext_VRF_Lite_Jython\", \"enableBorderExtension\": \"VRF_LITE\", \"AUTO_VRF_LITE_FLAG\": \"false\", \"IP_MASK\": \"20.33.0.2/30\", \"MTU\": \"9216\", \"NEIGHBOR_ASN\": \"23132\", \"IF_NAME\": \"Ethernet1/2\", \"IPV6_NEIGHBOR\": \"\", \"IPV6_MASK\": \"\", \"DOT1Q_ID\": \"2\", \"asn\": \"52125\"}", + "destInterfaceName":"Ethernet1/2", + "destSwitchName":"poap-import-static" + } + ], + "islanAttached":false, + "lanAttachedState":"NA", + "errorMessage":"None", + "instanceValues":"", + "freeformConfig":"None", + "role":"border gateway", + "vlanModifiable":true + } + ] + } + ], + "METHOD":"GET", + "MESSAGE":"OK", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/f1/vrfs/switches?vrf-names=test_vrf&serial-numbers=9D2DAUJJFQQ", + "RETURN_CODE":200 + } +} diff --git a/tests/unit/modules/dcnm/test_dcnm_vrf.py b/tests/unit/modules/dcnm/test_dcnm_vrf.py index 0764191ba..17b5f640f 100644 --- a/tests/unit/modules/dcnm/test_dcnm_vrf.py +++ b/tests/unit/modules/dcnm/test_dcnm_vrf.py @@ -608,16 +608,16 @@ def load_fixtures(self, response=None, device=""): else: pass - def test_dcnm_vrf_blank_fabric(self): + def test_dcnm_vrf_v1_blank_fabric(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=False, failed=True) self.assertEqual( result.get("msg"), - "Fabric test_fabric missing on the controller or does not have any switches", + "caller: get_have. Unable to find vrfs under fabric: test_fabric", ) - def test_dcnm_vrf_get_have_failure(self): + def test_dcnm_vrf_v1_get_have_failure(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=False, failed=True) @@ -625,13 +625,13 @@ def test_dcnm_vrf_get_have_failure(self): result.get("msg"), "caller: get_have. Fabric test_fabric not present on the controller" ) - def test_dcnm_vrf_merged_redeploy(self): + def test_dcnm_vrf_v1_merged_redeploy(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=True, failed=False) self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") - def test_dcnm_vrf_merged_lite_redeploy_interface_with_extensions(self): + def test_dcnm_vrf_v1_merged_lite_redeploy_interface_with_extensions(self): playbook = self.test_data.get( "playbook_vrf_merged_lite_redeploy_interface_with_extensions" ) @@ -645,7 +645,7 @@ def test_dcnm_vrf_merged_lite_redeploy_interface_with_extensions(self): result = self.execute_module(changed=True, failed=False) self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") - def test_dcnm_vrf_merged_lite_redeploy_interface_without_extensions(self): + def test_dcnm_vrf_v1_merged_lite_redeploy_interface_without_extensions(self): playbook = self.test_data.get( "playbook_vrf_merged_lite_redeploy_interface_without_extensions" ) @@ -660,7 +660,7 @@ def test_dcnm_vrf_merged_lite_redeploy_interface_without_extensions(self): self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) - def test_dcnm_vrf_check_mode(self): + def test_dcnm_vrf_v1_check_mode(self): playbook = self.test_data.get("playbook_config") set_module_args( dict( @@ -674,7 +674,7 @@ def test_dcnm_vrf_check_mode(self): self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) - def test_dcnm_vrf_merged_new(self): + def test_dcnm_vrf_v1_merged_new(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=True, failed=False) @@ -696,7 +696,7 @@ def test_dcnm_vrf_merged_new(self): self.assertEqual(result["response"][2]["DATA"]["status"], "") self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_merged_lite_new_interface_with_extensions(self): + def test_dcnm_vrf_v1_merged_lite_new_interface_with_extensions(self): playbook = self.test_data.get( "playbook_vrf_merged_lite_new_interface_with_extensions" ) @@ -726,7 +726,7 @@ def test_dcnm_vrf_merged_lite_new_interface_with_extensions(self): self.assertEqual(result["response"][2]["DATA"]["status"], "") self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_merged_lite_new_interface_without_extensions(self): + def test_dcnm_vrf_v1_merged_lite_new_interface_without_extensions(self): playbook = self.test_data.get( "playbook_vrf_merged_lite_new_interface_without_extensions" ) @@ -741,13 +741,13 @@ def test_dcnm_vrf_merged_lite_new_interface_without_extensions(self): self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) - def test_dcnm_vrf_merged_duplicate(self): + def test_dcnm_vrf_v1_merged_duplicate(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=False, failed=False) self.assertFalse(result.get("diff")) - def test_dcnm_vrf_merged_lite_duplicate(self): + def test_dcnm_vrf_v1_merged_lite_duplicate(self): playbook = self.test_data.get("playbook_vrf_lite_config") set_module_args( dict( @@ -759,7 +759,7 @@ def test_dcnm_vrf_merged_lite_duplicate(self): result = self.execute_module(changed=False, failed=False) self.assertFalse(result.get("diff")) - def test_dcnm_vrf_merged_with_incorrect_vrfid(self): + def test_dcnm_vrf_v1_merged_with_incorrect_vrfid(self): playbook = self.test_data.get("playbook_config_incorrect_vrfid") set_module_args( dict( @@ -774,7 +774,7 @@ def test_dcnm_vrf_merged_with_incorrect_vrfid(self): "DcnmVrf.diff_for_create: vrf_id for vrf test_vrf_1 cannot be updated to a different value", ) - def test_dcnm_vrf_merged_lite_invalidrole(self): + def test_dcnm_vrf_v1_merged_lite_invalidrole(self): playbook = self.test_data.get("playbook_vrf_lite_inv_config") set_module_args( dict( @@ -792,7 +792,7 @@ def test_dcnm_vrf_merged_lite_invalidrole(self): msg += "switch 10.10.10.225 with role leaf need review." self.assertEqual(result["msg"], msg) - def test_dcnm_vrf_merged_with_update(self): + def test_dcnm_vrf_v1_merged_with_update(self): playbook = self.test_data.get("playbook_config_update") set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=True, failed=False) @@ -802,7 +802,7 @@ def test_dcnm_vrf_merged_with_update(self): ) self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") - def test_dcnm_vrf_merged_lite_update_interface_with_extensions(self): + def test_dcnm_vrf_v1_merged_lite_update_interface_with_extensions(self): playbook = self.test_data.get( "playbook_vrf_merged_lite_update_interface_with_extensions" ) @@ -820,7 +820,7 @@ def test_dcnm_vrf_merged_lite_update_interface_with_extensions(self): ) self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") - def test_dcnm_vrf_merged_lite_update_interface_without_extensions(self): + def test_dcnm_vrf_v1_merged_lite_update_interface_without_extensions(self): playbook = self.test_data.get( "playbook_vrf_merged_lite_update_interface_without_extensions" ) @@ -835,7 +835,7 @@ def test_dcnm_vrf_merged_lite_update_interface_without_extensions(self): self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) - def test_dcnm_vrf_merged_with_update_vlan(self): + def test_dcnm_vrf_v1_merged_with_update_vlan(self): playbook = self.test_data.get("playbook_config_update_vlan") set_module_args( dict( @@ -865,7 +865,7 @@ def test_dcnm_vrf_merged_with_update_vlan(self): self.assertEqual(result["response"][2]["DATA"]["status"], "") self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_merged_lite_vlan_update_interface_with_extensions(self): + def test_dcnm_vrf_v1_merged_lite_vlan_update_interface_with_extensions(self): playbook = self.test_data.get( "playbook_vrf_lite_update_vlan_config_interface_with_extensions" ) @@ -892,7 +892,7 @@ def test_dcnm_vrf_merged_lite_vlan_update_interface_with_extensions(self): self.assertEqual(result["response"][2]["DATA"]["status"], "") self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_merged_lite_vlan_update_interface_without_extensions(self): + def test_dcnm_vrf_v1_merged_lite_vlan_update_interface_without_extensions(self): playbook = self.test_data.get( "playbook_vrf_lite_update_vlan_config_interface_without_extensions" ) @@ -907,14 +907,14 @@ def test_dcnm_vrf_merged_lite_vlan_update_interface_without_extensions(self): self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) - def test_dcnm_vrf_error1(self): + def test_dcnm_vrf_v1_error1(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=False, failed=True) self.assertEqual(result["msg"]["RETURN_CODE"], 400) self.assertEqual(result["msg"]["ERROR"], "There is an error") - def test_dcnm_vrf_error2(self): + def test_dcnm_vrf_v1_error2(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=False, failed=True) @@ -923,7 +923,7 @@ def test_dcnm_vrf_error2(self): str(result["msg"]["DATA"].values()), ) - def test_dcnm_vrf_error3(self): + def test_dcnm_vrf_v1_error3(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=False, failed=False) @@ -931,7 +931,7 @@ def test_dcnm_vrf_error3(self): result["response"][2]["DATA"], "No switches PENDING for deployment" ) - def test_dcnm_vrf_replace_with_changes(self): + def test_dcnm_vrf_v1_replace_with_changes(self): playbook = self.test_data.get("playbook_config_replace") set_module_args( dict( @@ -954,7 +954,7 @@ def test_dcnm_vrf_replace_with_changes(self): self.assertEqual(result["response"][1]["DATA"]["status"], "") self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_replace_lite_changes_interface_with_extension_values(self): + def test_dcnm_vrf_v1_replace_lite_changes_interface_with_extension_values(self): playbook = self.test_data.get( "playbook_vrf_lite_replace_config_interface_with_extension_values" ) @@ -979,7 +979,7 @@ def test_dcnm_vrf_replace_lite_changes_interface_with_extension_values(self): self.assertEqual(result["response"][1]["DATA"]["status"], "") self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_replace_lite_changes_interface_without_extensions(self): + def test_dcnm_vrf_v1_replace_lite_changes_interface_without_extensions(self): playbook = self.test_data.get("playbook_vrf_lite_replace_config") set_module_args( dict( @@ -992,7 +992,7 @@ def test_dcnm_vrf_replace_lite_changes_interface_without_extensions(self): self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) - def test_dcnm_vrf_replace_with_no_atch(self): + def test_dcnm_vrf_v1_replace_with_no_atch(self): playbook = self.test_data.get("playbook_config_replace_no_atch") set_module_args( dict( @@ -1017,7 +1017,7 @@ def test_dcnm_vrf_replace_with_no_atch(self): self.assertEqual(result["response"][1]["DATA"]["status"], "") self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_replace_lite_no_atch(self): + def test_dcnm_vrf_v1_replace_lite_no_atch(self): playbook = self.test_data.get("playbook_config_replace_no_atch") set_module_args( dict( @@ -1042,14 +1042,14 @@ def test_dcnm_vrf_replace_lite_no_atch(self): self.assertEqual(result["response"][1]["DATA"]["status"], "") self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_replace_without_changes(self): + def test_dcnm_vrf_v1_replace_without_changes(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="replaced", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=False, failed=False) self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) - def test_dcnm_vrf_replace_lite_without_changes(self): + def test_dcnm_vrf_v1_replace_lite_without_changes(self): playbook = self.test_data.get("playbook_vrf_lite_config") set_module_args( dict( @@ -1062,7 +1062,7 @@ def test_dcnm_vrf_replace_lite_without_changes(self): self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) - def test_dcnm_vrf_lite_override_with_additions_interface_with_extensions(self): + def test_dcnm_vrf_v1_lite_override_with_additions_interface_with_extensions(self): playbook = self.test_data.get( "playbook_vrf_lite_override_with_additions_interface_with_extensions" ) @@ -1092,7 +1092,7 @@ def test_dcnm_vrf_lite_override_with_additions_interface_with_extensions(self): self.assertEqual(result["response"][2]["DATA"]["status"], "") self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_lite_override_with_additions_interface_without_extensions(self): + def test_dcnm_vrf_v1_lite_override_with_additions_interface_without_extensions(self): playbook = self.test_data.get( "playbook_vrf_lite_override_with_additions_interface_without_extensions" ) @@ -1107,7 +1107,7 @@ def test_dcnm_vrf_lite_override_with_additions_interface_without_extensions(self self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) - def test_dcnm_vrf_override_with_deletions(self): + def test_dcnm_vrf_v1_override_with_deletions(self): playbook = self.test_data.get("playbook_config_override") set_module_args( dict( @@ -1145,7 +1145,7 @@ def test_dcnm_vrf_override_with_deletions(self): result["response"][5]["DATA"]["test-vrf-2--XYZKSJHSMK3(leaf3)"], "SUCCESS" ) - def test_dcnm_vrf_lite_override_with_deletions_interface_with_extensions(self): + def test_dcnm_vrf_v1_lite_override_with_deletions_interface_with_extensions(self): playbook = self.test_data.get( "playbook_vrf_lite_override_with_deletions_interface_with_extensions" ) @@ -1171,7 +1171,7 @@ def test_dcnm_vrf_lite_override_with_deletions_interface_with_extensions(self): self.assertEqual(result["response"][1]["DATA"]["status"], "") self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_lite_override_with_deletions_interface_without_extensions(self): + def test_dcnm_vrf_v1_lite_override_with_deletions_interface_without_extensions(self): playbook = self.test_data.get( "playbook_vrf_lite_override_with_deletions_interface_without_extensions" ) @@ -1186,14 +1186,14 @@ def test_dcnm_vrf_lite_override_with_deletions_interface_without_extensions(self self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) - def test_dcnm_vrf_override_without_changes(self): + def test_dcnm_vrf_v1_override_without_changes(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="overridden", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=False, failed=False) self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) - def test_dcnm_vrf_override_no_changes_lite(self): + def test_dcnm_vrf_v1_override_no_changes_lite(self): playbook = self.test_data.get("playbook_vrf_lite_config") set_module_args( dict( @@ -1206,7 +1206,7 @@ def test_dcnm_vrf_override_no_changes_lite(self): self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) - def test_dcnm_vrf_delete_std(self): + def test_dcnm_vrf_v1_delete_std(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="deleted", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=True, failed=False) @@ -1226,7 +1226,7 @@ def test_dcnm_vrf_delete_std(self): self.assertEqual(result["response"][1]["DATA"]["status"], "") self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_delete_std_lite(self): + def test_dcnm_vrf_v1_delete_std_lite(self): playbook = self.test_data.get("playbook_vrf_lite_config") set_module_args( dict( @@ -1252,7 +1252,7 @@ def test_dcnm_vrf_delete_std_lite(self): self.assertEqual(result["response"][1]["DATA"]["status"], "") self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_delete_dcnm_only(self): + def test_dcnm_vrf_v1_delete_dcnm_only(self): set_module_args(dict(state="deleted", fabric="test_fabric", config=[])) result = self.execute_module(changed=True, failed=False) self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) @@ -1271,14 +1271,14 @@ def test_dcnm_vrf_delete_dcnm_only(self): self.assertEqual(result["response"][1]["DATA"]["status"], "") self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) - def test_dcnm_vrf_delete_failure(self): + def test_dcnm_vrf_v1_delete_failure(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="deleted", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=False, failed=True) msg = "DcnmVrf.push_diff_delete: Deletion of vrfs test_vrf_1 has failed" self.assertEqual(result["msg"]["response"][2], msg) - def test_dcnm_vrf_query(self): + def test_dcnm_vrf_v1_query(self): playbook = self.test_data.get("playbook_config") set_module_args(dict(state="query", fabric="test_fabric", config=playbook)) result = self.execute_module(changed=False, failed=False) @@ -1306,7 +1306,7 @@ def test_dcnm_vrf_query(self): "202", ) - def test_dcnm_vrf_query_vrf_lite(self): + def test_dcnm_vrf_v1_query_vrf_lite(self): playbook = self.test_data.get("playbook_vrf_lite_config") set_module_args( dict( @@ -1352,7 +1352,7 @@ def test_dcnm_vrf_query_vrf_lite(self): "", ) - def test_dcnm_vrf_query_lite_without_config(self): + def test_dcnm_vrf_v1_query_lite_without_config(self): set_module_args(dict(state="query", fabric="test_fabric", config=[])) result = self.execute_module(changed=False, failed=False) self.assertFalse(result.get("diff")) @@ -1391,7 +1391,7 @@ def test_dcnm_vrf_query_lite_without_config(self): "", ) - def test_dcnm_vrf_validation(self): + def test_dcnm_vrf_v1_validation(self): playbook = self.test_data.get("playbook_config_input_validation") set_module_args( dict( @@ -1406,13 +1406,13 @@ def test_dcnm_vrf_validation(self): msg += "ip_address is mandatory under attach parameters" self.assertEqual(result["msg"], msg) - def test_dcnm_vrf_validation_no_config(self): + def test_dcnm_vrf_v1_validation_no_config(self): set_module_args(dict(state="merged", fabric="test_fabric", config=[])) result = self.execute_module(changed=False, failed=True) msg = "DcnmVrf.validate_input: config element is mandatory for merged state" self.assertEqual(result["msg"], msg) - def test_dcnm_vrf_12check_mode(self): + def test_dcnm_vrf_v1_12check_mode(self): self.version = 12 playbook = self.test_data.get("playbook_config") set_module_args( @@ -1428,7 +1428,7 @@ def test_dcnm_vrf_12check_mode(self): self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) - def test_dcnm_vrf_12merged_new(self): + def test_dcnm_vrf_v1_12merged_new(self): self.version = 12 playbook = self.test_data.get("playbook_config") set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) diff --git a/tests/unit/modules/dcnm/test_dcnm_vrf_v2_11.py b/tests/unit/modules/dcnm/test_dcnm_vrf_v2_11.py new file mode 100644 index 000000000..3656aae51 --- /dev/null +++ b/tests/unit/modules/dcnm/test_dcnm_vrf_v2_11.py @@ -0,0 +1,1249 @@ +# Copyright (c) 2020-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. + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import copy +from unittest.mock import patch + +from ansible_collections.cisco.dcnm.plugins.modules import dcnm_vrf_v2 as dcnm_vrf + +from .dcnm_module import TestDcnmModule, loadPlaybookData, set_module_args + +# from units.compat.mock import patch + + +class TestDcnmVrfModule(TestDcnmModule): + + module = dcnm_vrf + + test_data = loadPlaybookData("dcnm_vrf_11") + + SUCCESS_RETURN_CODE = 200 + + version = 11 + + mock_ip_sn = test_data.get("mock_ip_sn") + vrf_inv_data = test_data.get("vrf_inv_data") + fabric_details = test_data.get("fabric_details") + fabric_details_mfd = test_data.get("fabric_details_mfd") + fabric_details_vxlan = test_data.get("fabric_details_vxlan") + + mock_vrf_attach_object_del_not_ready = test_data.get("mock_vrf_attach_object_del_not_ready") + mock_vrf_attach_object_del_oos = test_data.get("mock_vrf_attach_object_del_oos") + mock_vrf_attach_object_del_ready = test_data.get("mock_vrf_attach_object_del_ready") + + attach_success_resp = test_data.get("attach_success_resp") + attach_success_resp2 = test_data.get("attach_success_resp2") + attach_success_resp3 = test_data.get("attach_success_resp3") + deploy_success_resp = test_data.get("deploy_success_resp") + get_have_failure = test_data.get("get_have_failure") + error1 = test_data.get("error1") + error2 = test_data.get("error2") + error3 = test_data.get("error3") + delete_success_resp = test_data.get("delete_success_resp") + blank_data = test_data.get("blank_data") + + def init_data(self): + # Some of the mock data is re-initialized after each test as previous test might have altered portions + # of the mock data. + + self.mock_sn_fab_dict = copy.deepcopy(self.test_data.get("mock_sn_fab")) + self.mock_vrf_object = copy.deepcopy(self.test_data.get("mock_vrf_object")) + self.mock_vrf12_object = copy.deepcopy(self.test_data.get("mock_vrf12_object")) + self.mock_vrf_attach_object = copy.deepcopy(self.test_data.get("mock_vrf_attach_object")) + self.mock_vrf_attach_object_query = copy.deepcopy(self.test_data.get("mock_vrf_attach_object_query")) + self.mock_vrf_attach_object2 = copy.deepcopy(self.test_data.get("mock_vrf_attach_object2")) + self.mock_vrf_attach_object2_query = copy.deepcopy(self.test_data.get("mock_vrf_attach_object2_query")) + self.mock_vrf_attach_object_pending = copy.deepcopy(self.test_data.get("mock_vrf_attach_object_pending")) + self.mock_vrf_object_dcnm_only = copy.deepcopy(self.test_data.get("mock_vrf_object_dcnm_only")) + self.mock_vrf_attach_object_dcnm_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_object_dcnm_only")) + self.mock_vrf_attach_get_ext_object_dcnm_att1_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_dcnm_att1_only")) + self.mock_vrf_attach_get_ext_object_dcnm_att2_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_dcnm_att2_only")) + self.mock_vrf_attach_get_ext_object_merge_att1_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_merge_att1_only")) + self.mock_vrf_attach_get_ext_object_merge_att2_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_merge_att2_only")) + self.mock_vrf_attach_get_ext_object_merge_att3_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_merge_att3_only")) + self.mock_vrf_attach_get_ext_object_merge_att4_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_merge_att4_only")) + self.mock_vrf_attach_get_ext_object_ov_att1_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_ov_att1_only")) + self.mock_vrf_attach_get_ext_object_ov_att2_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_ov_att2_only")) + self.mock_vrf_attach_lite_object = copy.deepcopy(self.test_data.get("mock_vrf_attach_lite_object")) + self.mock_vrf_lite_obj = copy.deepcopy(self.test_data.get("mock_vrf_lite_obj")) + self.mock_pools_top_down_vrf_vlan = copy.deepcopy(self.test_data.get("mock_pools_top_down_vrf_vlan")) + + def setUp(self): + super(TestDcnmVrfModule, self).setUp() + + self.mock_dcnm_sn_fab = patch("ansible_collections.cisco.dcnm.plugins.module_utils.vrf.dcnm_vrf_v11.get_sn_fabric_dict") + self.run_dcnm_sn_fab = self.mock_dcnm_sn_fab.start() + + self.mock_dcnm_ip_sn = patch("ansible_collections.cisco.dcnm.plugins.module_utils.vrf.dcnm_vrf_v11.get_fabric_inventory_details") + self.run_dcnm_ip_sn = self.mock_dcnm_ip_sn.start() + + self.mock_dcnm_send = patch("ansible_collections.cisco.dcnm.plugins.module_utils.vrf.dcnm_vrf_v11.dcnm_send") + self.run_dcnm_send = self.mock_dcnm_send.start() + + self.mock_dcnm_fabric_details = patch("ansible_collections.cisco.dcnm.plugins.module_utils.vrf.dcnm_vrf_v11.get_fabric_details") + self.run_dcnm_fabric_details = self.mock_dcnm_fabric_details.start() + + self.mock_dcnm_version_supported = patch("ansible_collections.cisco.dcnm.plugins.modules.dcnm_vrf_v2.dcnm_version_supported") + self.run_dcnm_version_supported = self.mock_dcnm_version_supported.start() + + self.mock_dcnm_get_url = patch("ansible_collections.cisco.dcnm.plugins.module_utils.vrf.dcnm_vrf_v11.dcnm_get_url") + self.run_dcnm_get_url = self.mock_dcnm_get_url.start() + + def tearDown(self): + super(TestDcnmVrfModule, self).tearDown() + self.mock_dcnm_send.stop() + self.mock_dcnm_ip_sn.stop() + self.mock_dcnm_fabric_details.stop() + self.mock_dcnm_version_supported.stop() + self.mock_dcnm_get_url.stop() + + def load_fixtures(self, response=None, device=""): + + self.run_dcnm_version_supported.return_value = 11 + + if "vrf_blank_fabric" in self._testMethodName: + self.run_dcnm_ip_sn.side_effect = [{}] + else: + self.run_dcnm_ip_sn.side_effect = [self.vrf_inv_data] + + self.run_dcnm_fabric_details.side_effect = [self.fabric_details] + + if "get_have_failure" in self._testMethodName: + self.run_dcnm_send.side_effect = [self.get_have_failure] + + elif "_check_mode" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "_merged_new" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "_merged_lite_new" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "error1" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.error1, + self.blank_data, + ] + + elif "error2" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.error2, + self.blank_data, + ] + + elif "error3" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.attach_success_resp, + self.error3, + self.blank_data, + ] + + elif "_merged_duplicate" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "_merged_lite_duplicate" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + ] + + elif "_merged_with_incorrect" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "_merged_with_update" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "_merged_lite_update" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "_merged_lite_vlan_update" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.blank_data, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "_merged_redeploy" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_object_pending, + self.blank_data, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.deploy_success_resp, + ] + elif "_merged_lite_redeploy" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_lite_obj, + self.mock_vrf_attach_object_pending, + self.blank_data, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.deploy_success_resp, + ] + + elif "merged_lite_invalidrole" in self._testMethodName: + self.run_dcnm_send.side_effect = [self.blank_data, self.blank_data] + + elif "replace_with_no_atch" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.delete_success_resp, + ] + + elif "replace_lite_no_atch" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.attach_success_resp, + self.deploy_success_resp, + self.delete_success_resp, + ] + + elif "replace_with_changes" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.delete_success_resp, + ] + + elif "replace_lite_changes" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + self.delete_success_resp, + ] + + elif "replace_without_changes" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "replace_lite_without_changes" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + ] + + elif "lite_override_with_additions" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "override_with_additions" in self._testMethodName: + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "lite_override_with_deletions" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + self.mock_vrf_attach_object_del_not_ready, + self.mock_vrf_attach_object_del_ready, + self.delete_success_resp, + self.blank_data, + self.attach_success_resp2, + self.deploy_success_resp, + ] + + elif "override_with_deletions" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_ov_att1_only, + self.mock_vrf_attach_get_ext_object_ov_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.mock_vrf_attach_object_del_not_ready, + self.mock_vrf_attach_object_del_ready, + self.delete_success_resp, + self.mock_pools_top_down_vrf_vlan, + self.blank_data, + self.attach_success_resp2, + self.deploy_success_resp, + ] + + elif "override_without_changes" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "override_no_changes_lite" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att3_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + ] + + elif "delete_std" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_dcnm_att1_only, + self.mock_vrf_attach_get_ext_object_dcnm_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.mock_vrf_attach_object_del_not_ready, + self.mock_vrf_attach_object_del_ready, + self.delete_success_resp, + self.mock_pools_top_down_vrf_vlan, + ] + + elif "delete_std_lite" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_dcnm_att1_only, + self.mock_vrf_attach_get_ext_object_dcnm_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.mock_vrf_attach_object_del_not_ready, + self.mock_vrf_attach_object_del_ready, + self.delete_success_resp, + ] + + elif "delete_failure" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_dcnm_att1_only, + self.mock_vrf_attach_get_ext_object_dcnm_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.mock_vrf_attach_object_del_not_ready, + self.mock_vrf_attach_object_del_oos, + ] + + elif "delete_dcnm_only" in self._testMethodName: + self.init_data() + obj1 = copy.deepcopy(self.mock_vrf_attach_object_del_not_ready) + obj2 = copy.deepcopy(self.mock_vrf_attach_object_del_ready) + + obj1["DATA"][0].update({"vrfName": "test_vrf_dcnm"}) + obj2["DATA"][0].update({"vrfName": "test_vrf_dcnm"}) + + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object_dcnm_only] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object_dcnm_only, + self.mock_vrf_attach_get_ext_object_dcnm_att1_only, + self.mock_vrf_attach_get_ext_object_dcnm_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + obj1, + obj2, + self.delete_success_resp, + self.mock_pools_top_down_vrf_vlan, + ] + + elif "query" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.mock_vrf_object, + self.mock_vrf_attach_object_query, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "query_vrf_lite" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.mock_vrf_object, + self.mock_vrf_attach_object2_query, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + ] + + elif "query_vrf_lite_without_config" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.mock_vrf_object, + self.mock_vrf_attach_object2_query, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + ] + + elif "_12check_mode" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf12_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "_12merged_new" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] + + else: + pass + + def test_dcnm_vrf_v2_11_blank_fabric(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get("msg"), + "caller: get_have. Unable to find vrfs under fabric: test_fabric", + ) + + def test_dcnm_vrf_v2_11_get_have_failure(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True) + self.assertEqual(result.get("msg"), "caller: get_have. Fabric test_fabric not present on the controller") + + def test_dcnm_vrf_v2_11_merged_redeploy(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + + def test_dcnm_vrf_v2_11_merged_lite_redeploy_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_redeploy_interface_with_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + + def test_dcnm_vrf_v2_11_merged_lite_redeploy_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_redeploy_interface_without_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_11_check_mode(self): + playbook = self.test_data.get("playbook_config") + set_module_args( + dict( + _ansible_check_mode=True, + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertFalse(result.get("response")) + + def test_dcnm_vrf_v2_11_merged_new(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224") + self.assertEqual(result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.225") + self.assertEqual(result.get("diff")[0]["vrf_id"], 9008011) + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][2]["DATA"]["status"], "") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_merged_lite_new_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_new_interface_with_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224") + self.assertEqual(result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.227") + self.assertEqual(result.get("diff")[0]["vrf_id"], 9008011) + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][2]["DATA"]["status"], "") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_merged_lite_new_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_new_interface_without_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_11_merged_duplicate(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + + def test_dcnm_vrf_v2_11_merged_lite_duplicate(self): + playbook = self.test_data.get("playbook_vrf_lite_config") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + + def test_dcnm_vrf_v2_11_merged_with_incorrect_vrfid(self): + playbook = self.test_data.get("playbook_config_incorrect_vrfid") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get("msg"), + "DcnmVrf11.diff_for_create: vrf_id for vrf test_vrf_1 cannot be updated to a different value", + ) + + def test_dcnm_vrf_v2_11_merged_lite_invalidrole(self): + playbook = self.test_data.get("playbook_vrf_lite_inv_config") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + msg = "DcnmVrf11.update_attach_params_extension_values: " + msg += "caller: update_attach_params. " + msg += "VRF LITE attachments are appropriate only for switches " + msg += "with Border roles e.g. Border Gateway, Border Spine, etc. " + msg += "The playbook and/or controller settings for " + msg += "switch 10.10.10.225 with role leaf need review." + self.assertEqual(result["msg"], msg) + + def test_dcnm_vrf_v2_11_merged_with_update(self): + playbook = self.test_data.get("playbook_config_update") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.226") + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + + def test_dcnm_vrf_v2_11_merged_lite_update_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_update_interface_with_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.228") + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + + def test_dcnm_vrf_v2_11_merged_lite_update_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_update_interface_without_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_11_merged_with_update_vlan(self): + playbook = self.test_data.get("playbook_config_update_vlan") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.225") + self.assertEqual(result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.226") + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 303) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 303) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][2]["DATA"]["status"], "") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_merged_lite_vlan_update_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_update_vlan_config_interface_with_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.228") + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 402) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][2]["DATA"]["status"], "") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_merged_lite_vlan_update_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_update_vlan_config_interface_without_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_11_error1(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True) + self.assertEqual(result["msg"]["RETURN_CODE"], 400) + self.assertEqual(result["msg"]["ERROR"], "There is an error") + + def test_dcnm_vrf_v2_11_error2(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True) + self.assertIn( + "Entered VRF VLAN ID 203 is in use already", + str(result["msg"]["DATA"].values()), + ) + + def test_dcnm_vrf_v2_11_error3(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False) + self.assertEqual(result["response"][2]["DATA"], "No switches PENDING for deployment") + + def test_dcnm_vrf_v2_11_replace_with_changes(self): + playbook = self.test_data.get("playbook_config_replace") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 203) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], "202") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_replace_lite_changes_interface_with_extension_values(self): + playbook = self.test_data.get("playbook_vrf_lite_replace_config_interface_with_extension_values") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], "202") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_replace_lite_changes_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_replace_config") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_11_replace_with_no_atch(self): + playbook = self.test_data.get("playbook_config_replace_no_atch") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], "202") + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], "202") + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("diff")[0]) + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_replace_lite_no_atch(self): + playbook = self.test_data.get("playbook_config_replace_no_atch") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], "202") + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], "202") + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("diff")[0]) + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_replace_without_changes(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="replaced", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertFalse(result.get("response")) + + def test_dcnm_vrf_v2_11_replace_lite_without_changes(self): + playbook = self.test_data.get("playbook_vrf_lite_config") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertFalse(result.get("response")) + + def test_dcnm_vrf_v2_11_lite_override_with_additions_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_override_with_additions_interface_with_extensions") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224") + self.assertEqual(result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.227") + self.assertEqual(result.get("diff")[0]["vrf_id"], 9008011) + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][2]["DATA"]["status"], "") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_lite_override_with_additions_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_override_with_additions_interface_without_extensions") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_11_override_with_deletions(self): + playbook = self.test_data.get("playbook_config_override") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 303) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 303) + self.assertEqual(result.get("diff")[0]["vrf_id"], 9008012) + + self.assertFalse(result.get("diff")[1]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[1]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[1]["attach"][0]["vlan_id"], "202") + self.assertEqual(result.get("diff")[1]["attach"][1]["vlan_id"], "202") + self.assertEqual(result.get("diff")[1]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("diff")[1]) + + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_lite_override_with_deletions_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_override_with_deletions_interface_with_extensions") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], "202") + + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_lite_override_with_deletions_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_override_with_deletions_interface_without_extensions") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_11_override_without_changes(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="overridden", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertFalse(result.get("response")) + + def test_dcnm_vrf_v2_11_override_no_changes_lite(self): + playbook = self.test_data.get("playbook_vrf_lite_config") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertFalse(result.get("response")) + + def test_dcnm_vrf_v2_11_delete_std(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="deleted", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False) + self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], "202") + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], "202") + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("diff")[0]) + + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_delete_std_lite(self): + playbook = self.test_data.get("playbook_vrf_lite_config") + set_module_args( + dict( + state="deleted", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], "202") + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], "202") + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("diff")[0]) + + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_delete_dcnm_only(self): + set_module_args(dict(state="deleted", fabric="test_fabric", config=[])) + result = self.execute_module(changed=True, failed=False) + self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], "402") + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], "403") + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_dcnm") + self.assertNotIn("vrf_id", result.get("diff")[0]) + + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_11_delete_failure(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="deleted", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True) + msg = "DcnmVrf11.push_diff_delete: Deletion of vrfs test_vrf_1 has failed" + self.assertEqual(result["msg"]["response"][2], msg) + + def test_dcnm_vrf_v2_11_query(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="query", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertEqual(result.get("response")[0]["parent"]["vrfName"], "test_vrf_1") + self.assertEqual(result.get("response")[0]["parent"]["vrfId"], 9008011) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["vlan"], + "202", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["vlan"], + "202", + ) + + def test_dcnm_vrf_v2_11_query_vrf_lite(self): + playbook = self.test_data.get("playbook_vrf_lite_config") + set_module_args( + dict( + state="query", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertEqual(result.get("response")[0]["parent"]["vrfName"], "test_vrf_1") + self.assertEqual(result.get("response")[0]["parent"]["vrfId"], 9008011) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["vlan"], + "202", + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["extensionValues"], + "", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["vlan"], + "202", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["extensionValues"], + "", + ) + + def test_dcnm_vrf_v2_11_query_lite_without_config(self): + set_module_args(dict(state="query", fabric="test_fabric", config=[])) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertEqual(result.get("response")[0]["parent"]["vrfName"], "test_vrf_1") + self.assertEqual(result.get("response")[0]["parent"]["vrfId"], 9008011) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["vlan"], + "202", + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["extensionValues"], + "", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["vlan"], + "202", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["extensionValues"], + "", + ) + + def test_dcnm_vrf_v2_11_validation(self): + """ + # Summary + + Verify that two missing mandatory fields are detected and an appropriate + error is generated. The fields are: + + - ip_address + - vrf_name + + The Pydantic model VrfPlaybookModel() is used for validation in the + method DcnmVrf.validate_input_merged_state(). + """ + playbook = self.test_data.get("playbook_config_input_validation") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + pydantic_result = result["msg"] + self.assertEqual(pydantic_result.error_count(), 2) + self.assertEqual(pydantic_result.errors()[0]["loc"], ("attach", 1, "ip_address")) + self.assertEqual(pydantic_result.errors()[0]["msg"], "Field required") + self.assertEqual(pydantic_result.errors()[1]["loc"], ("vrf_name",)) + self.assertEqual(pydantic_result.errors()[1]["msg"], "Field required") + + def test_dcnm_vrf_v2_11_validation_no_config(self): + """ + # Summary + + Verify that an empty config object results in an error when + state is merged. + """ + set_module_args(dict(state="merged", fabric="test_fabric", config=[])) + result = self.execute_module(changed=False, failed=True) + msg = "DcnmVrf11.validate_input_merged_state: " + msg += "config element is mandatory for merged state" + self.assertEqual(result.get("msg"), msg) diff --git a/tests/unit/modules/dcnm/test_dcnm_vrf_v2_12.py b/tests/unit/modules/dcnm/test_dcnm_vrf_v2_12.py new file mode 100644 index 000000000..2fb500e6b --- /dev/null +++ b/tests/unit/modules/dcnm/test_dcnm_vrf_v2_12.py @@ -0,0 +1,1294 @@ +# Copyright (c) 2020-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. + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import copy +from unittest.mock import patch + +from ansible_collections.cisco.dcnm.plugins.modules import dcnm_vrf_v2 as dcnm_vrf + +from .dcnm_module import TestDcnmModule, loadPlaybookData, set_module_args + +# from units.compat.mock import patch + + +class TestDcnmVrfModule12(TestDcnmModule): + module = dcnm_vrf + + test_data = loadPlaybookData("dcnm_vrf_12") + + SUCCESS_RETURN_CODE = 200 + + mock_ip_sn = test_data.get("mock_ip_sn") + vrf_inv_data = test_data.get("vrf_inv_data") + fabric_details = test_data.get("fabric_details") + + mock_vrf_attach_object_del_not_ready = test_data.get("mock_vrf_attach_object_del_not_ready") + mock_vrf_attach_object_del_oos = test_data.get("mock_vrf_attach_object_del_oos") + mock_vrf_attach_object_del_ready = test_data.get("mock_vrf_attach_object_del_ready") + + attach_success_resp = test_data.get("attach_success_resp") + attach_success_resp2 = test_data.get("attach_success_resp2") + attach_success_resp3 = test_data.get("attach_success_resp3") + deploy_success_resp = test_data.get("deploy_success_resp") + get_have_failure = test_data.get("get_have_failure") + error1 = test_data.get("error1") + error2 = test_data.get("error2") + error3 = test_data.get("error3") + delete_success_resp = test_data.get("delete_success_resp") + blank_data = test_data.get("blank_data") + + def init_data(self): + # Some of the mock data is re-initialized after each test as previous test might have altered portions + # of the mock data. + + self.mock_sn_fab_dict = copy.deepcopy(self.test_data.get("mock_sn_fab")) + self.mock_vrf_object = copy.deepcopy(self.test_data.get("mock_vrf_object")) + self.mock_vrf12_object = copy.deepcopy(self.test_data.get("mock_vrf12_object")) + self.mock_vrf_attach_object = copy.deepcopy(self.test_data.get("mock_vrf_attach_object")) + self.mock_vrf_attach_object_query = copy.deepcopy(self.test_data.get("mock_vrf_attach_object_query")) + self.mock_vrf_attach_object2 = copy.deepcopy(self.test_data.get("mock_vrf_attach_object2")) + self.mock_vrf_attach_object2_query = copy.deepcopy(self.test_data.get("mock_vrf_attach_object2_query")) + self.mock_vrf_attach_object_pending = copy.deepcopy(self.test_data.get("mock_vrf_attach_object_pending")) + self.mock_vrf_object_dcnm_only = copy.deepcopy(self.test_data.get("mock_vrf_object_dcnm_only")) + self.mock_vrf_attach_object_dcnm_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_object_dcnm_only")) + self.mock_vrf_attach_get_ext_object_dcnm_att1_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_dcnm_att1_only")) + self.mock_vrf_attach_get_ext_object_dcnm_att2_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_dcnm_att2_only")) + self.mock_vrf_attach_get_ext_object_merge_att1_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_merge_att1_only")) + self.mock_vrf_attach_get_ext_object_merge_att2_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_merge_att2_only")) + self.mock_vrf_attach_get_ext_object_merge_att3_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_merge_att3_only")) + self.mock_vrf_attach_get_ext_object_merge_att4_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_merge_att4_only")) + self.mock_vrf_attach_get_ext_object_ov_att1_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_ov_att1_only")) + self.mock_vrf_attach_get_ext_object_ov_att2_only = copy.deepcopy(self.test_data.get("mock_vrf_attach_get_ext_object_ov_att2_only")) + self.mock_vrf_attach_lite_object = copy.deepcopy(self.test_data.get("mock_vrf_attach_lite_object")) + self.mock_vrf_lite_obj = copy.deepcopy(self.test_data.get("mock_vrf_lite_obj")) + self.mock_pools_top_down_vrf_vlan = copy.deepcopy(self.test_data.get("mock_pools_top_down_vrf_vlan")) + + def setUp(self): + super(TestDcnmVrfModule12, self).setUp() + + self.mock_dcnm_sn_fab = patch("ansible_collections.cisco.dcnm.plugins.module_utils.vrf.dcnm_vrf_v12.get_sn_fabric_dict") + self.run_dcnm_sn_fab = self.mock_dcnm_sn_fab.start() + + self.mock_dcnm_ip_sn = patch("ansible_collections.cisco.dcnm.plugins.module_utils.vrf.dcnm_vrf_v12.get_fabric_inventory_details") + self.run_dcnm_ip_sn = self.mock_dcnm_ip_sn.start() + + self.mock_dcnm_send = patch("ansible_collections.cisco.dcnm.plugins.module_utils.vrf.dcnm_vrf_v12.dcnm_send") + self.run_dcnm_send = self.mock_dcnm_send.start() + + self.mock_dcnm_fabric_details = patch("ansible_collections.cisco.dcnm.plugins.module_utils.vrf.dcnm_vrf_v12.get_fabric_details") + self.run_dcnm_fabric_details = self.mock_dcnm_fabric_details.start() + + self.mock_dcnm_version_supported = patch("ansible_collections.cisco.dcnm.plugins.modules.dcnm_vrf_v2.dcnm_version_supported") + self.run_dcnm_version_supported = self.mock_dcnm_version_supported.start() + + self.mock_get_endpoint_with_long_query_string = patch( + "ansible_collections.cisco.dcnm.plugins.module_utils.vrf.dcnm_vrf_v12.get_endpoint_with_long_query_string" + ) + self.run_get_endpoint_with_long_query_string = self.mock_get_endpoint_with_long_query_string.start() + + def tearDown(self): + super(TestDcnmVrfModule12, self).tearDown() + self.mock_dcnm_send.stop() + self.mock_dcnm_ip_sn.stop() + self.mock_dcnm_fabric_details.stop() + self.mock_dcnm_version_supported.stop() + self.mock_get_endpoint_with_long_query_string.stop() + + def load_fixtures(self, response=None, device=""): + + self.run_dcnm_version_supported.return_value = 12 + + if "vrf_blank_fabric" in self._testMethodName: + self.run_dcnm_ip_sn.side_effect = [{}] + else: + self.run_dcnm_ip_sn.side_effect = [self.vrf_inv_data] + + self.run_dcnm_fabric_details.side_effect = [self.fabric_details] + + if "get_have_failure" in self._testMethodName: + self.run_dcnm_send.side_effect = [self.get_have_failure] + + elif "_check_mode" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "_merged_new" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "_merged_lite_new" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "error1" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.error1, + self.blank_data, + ] + + elif "error2" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.error2, + self.blank_data, + ] + + elif "error3" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.attach_success_resp, + self.error3, + self.blank_data, + ] + + elif "_merged_duplicate" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "_merged_lite_duplicate" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + ] + + elif "_merged_with_incorrect" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "_merged_with_update" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "_merged_lite_update" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "_merged_lite_vlan_update" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.blank_data, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "_merged_redeploy" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object_pending] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.mock_vrf_attach_object_pending, + self.blank_data, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.deploy_success_resp, + ] + elif "_merged_lite_redeploy" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object_pending] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_lite_obj, + self.mock_vrf_lite_obj, + self.mock_vrf_lite_obj, + self.mock_vrf_attach_object_pending, + # self.blank_data, + # self.mock_vrf_attach_get_ext_object_merge_att1_only, + # self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.deploy_success_resp, + ] + + elif "merged_lite_invalidrole" in self._testMethodName: + self.run_dcnm_send.side_effect = [self.blank_data, self.blank_data] + + elif "replace_with_no_atch" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.delete_success_resp, + ] + + elif "replace_lite_no_atch" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.attach_success_resp, + self.deploy_success_resp, + self.delete_success_resp, + ] + + elif "replace_with_changes" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.delete_success_resp, + ] + + elif "replace_lite_changes" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + self.delete_success_resp, + ] + + elif "replace_without_changes" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "replace_lite_without_changes" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + ] + + elif "lite_override_with_additions" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "override_with_additions" in self._testMethodName: + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "lite_override_with_deletions" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.mock_vrf_lite_obj, + self.attach_success_resp, + self.deploy_success_resp, + self.mock_vrf_attach_object_del_not_ready, + self.mock_vrf_attach_object_del_ready, + self.delete_success_resp, + self.blank_data, + self.attach_success_resp2, + self.deploy_success_resp, + ] + + elif "override_with_deletions" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_ov_att1_only, + self.mock_vrf_attach_get_ext_object_ov_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.mock_vrf_attach_object_del_not_ready, + self.mock_vrf_attach_object_del_ready, + self.delete_success_resp, + self.mock_pools_top_down_vrf_vlan, + self.blank_data, + self.attach_success_resp2, + self.deploy_success_resp, + ] + + elif "override_without_changes" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "override_no_changes_lite" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att3_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + ] + + elif "delete_std" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_dcnm_att1_only, + self.mock_vrf_attach_get_ext_object_dcnm_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.mock_vrf_attach_object_del_not_ready, + self.mock_vrf_attach_object_del_ready, + self.delete_success_resp, + self.mock_pools_top_down_vrf_vlan, + ] + + elif "delete_std_lite" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_dcnm_att1_only, + self.mock_vrf_attach_get_ext_object_dcnm_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.mock_vrf_attach_object_del_not_ready, + self.mock_vrf_attach_object_del_ready, + self.delete_success_resp, + ] + + elif "delete_failure" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_dcnm_att1_only, + self.mock_vrf_attach_get_ext_object_dcnm_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + self.mock_vrf_attach_object_del_not_ready, + self.mock_vrf_attach_object_del_oos, + ] + + elif "delete_dcnm_only" in self._testMethodName: + self.init_data() + obj1 = copy.deepcopy(self.mock_vrf_attach_object_del_not_ready) + obj2 = copy.deepcopy(self.mock_vrf_attach_object_del_ready) + + obj1["DATA"][0].update({"vrfName": "test_vrf_dcnm"}) + obj2["DATA"][0].update({"vrfName": "test_vrf_dcnm"}) + + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object_dcnm_only] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object_dcnm_only, + self.mock_vrf_attach_get_ext_object_dcnm_att1_only, + self.mock_vrf_attach_get_ext_object_dcnm_att2_only, + self.attach_success_resp, + self.deploy_success_resp, + obj1, + obj2, + self.delete_success_resp, + self.mock_pools_top_down_vrf_vlan, + ] + + elif "query" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + self.mock_vrf_object, + self.mock_vrf_attach_object_query, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "query_vrf_lite" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.mock_vrf_object, + self.mock_vrf_attach_object2_query, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + ] + + elif "query_vrf_lite_without_config" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object2] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + self.mock_vrf_object, + self.mock_vrf_attach_object2_query, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att4_only, + ] + + elif "_12check_mode" in self._testMethodName: + self.init_data() + self.run_get_endpoint_with_long_query_string.side_effect = [self.mock_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf12_object, + self.mock_vrf_attach_get_ext_object_merge_att1_only, + self.mock_vrf_attach_get_ext_object_merge_att2_only, + ] + + elif "_12merged_new" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] + + else: + pass + + def test_dcnm_vrf_v2_12_blank_fabric(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get("msg"), + "caller: get_have. Unable to find vrfs under fabric: test_fabric", + ) + + def test_dcnm_vrf_v2_12_get_have_failure(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True) + self.assertEqual(result.get("msg"), "caller: get_have. Fabric test_fabric not present on the controller") + + def test_dcnm_vrf_v2_12_merged_redeploy(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + + def test_dcnm_vrf_v2_12_merged_lite_redeploy_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_redeploy_interface_with_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + + def test_dcnm_vrf_v2_12_merged_lite_redeploy_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_redeploy_interface_without_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_12_check_mode(self): + playbook = self.test_data.get("playbook_config") + set_module_args( + dict( + _ansible_check_mode=True, + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertFalse(result.get("response")) + + def test_dcnm_vrf_v2_12_merged_new(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224") + self.assertEqual(result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.225") + self.assertEqual(result.get("diff")[0]["vrf_id"], 9008011) + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][2]["DATA"]["status"], "") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_merged_lite_new_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_new_interface_with_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224") + self.assertEqual(result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.227") + self.assertEqual(result.get("diff")[0]["vrf_id"], 9008011) + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][2]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_merged_lite_new_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_new_interface_without_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_12_merged_duplicate(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + + def test_dcnm_vrf_v2_12_merged_lite_duplicate(self): + playbook = self.test_data.get("playbook_vrf_lite_config") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + + def test_dcnm_vrf_v2_12_merged_with_incorrect_vrfid(self): + playbook = self.test_data.get("playbook_config_incorrect_vrfid") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get("msg"), + "NdfcVrf12.diff_for_create: vrf_id for vrf test_vrf_1 cannot be updated to a different value", + ) + + def test_dcnm_vrf_v2_12_merged_lite_invalidrole(self): + playbook = self.test_data.get("playbook_vrf_lite_inv_config") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + msg = "NdfcVrf12.update_attach_params_extension_values: " + msg += "caller: transmute_attach_params_to_payload. " + msg += "VRF LITE attachments are appropriate only for switches " + msg += "with Border roles e.g. Border Gateway, Border Spine, etc. " + msg += "The playbook and/or controller settings for " + msg += "switch 10.10.10.225 with role leaf need review." + self.assertEqual(result["msg"], msg) + + def test_dcnm_vrf_v2_12_merged_with_update(self): + playbook = self.test_data.get("playbook_config_update") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.226") + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + + def test_dcnm_vrf_v2_12_merged_lite_update_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_update_interface_with_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + # TODO: arobel - Asserts below have been modified so that this test passes + # We need to review for correctness. + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + # self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.228") + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224") + self.assertEqual(result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.228") + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + + def test_dcnm_vrf_v2_12_merged_lite_update_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_merged_lite_update_interface_without_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_12_merged_with_update_vlan(self): + playbook = self.test_data.get("playbook_config_update_vlan") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.225") + self.assertEqual(result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.226") + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 303) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 303) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][2]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_merged_lite_vlan_update_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_update_vlan_config_interface_with_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + # TODO: arobel - Asserts below have been modified so that this test passes + # We need to review for correctness. + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + # self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.228") + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224") + self.assertEqual(result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.228") + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 402) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][2]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_merged_lite_vlan_update_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_update_vlan_config_interface_without_extensions") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_12_error1(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True) + self.assertEqual(result["msg"]["RETURN_CODE"], 400) + self.assertEqual(result["msg"]["ERROR"], "There is an error") + + def test_dcnm_vrf_v2_12_error2(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True) + self.assertIn( + "Entered VRF VLAN ID 203 is in use already", + str(result["msg"]["DATA"].values()), + ) + + def test_dcnm_vrf_v2_12_error3(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False) + self.assertEqual(result["response"][2]["DATA"], "No switches PENDING for deployment") + + def test_dcnm_vrf_v2_12_replace_with_changes(self): + playbook = self.test_data.get("playbook_config_replace") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + # TODO: arobel - Asserts below have been modified so that this test passes + # We need to review for correctness. + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + # self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 203) + # self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], "202") + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 203) + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_replace_lite_changes_interface_with_extension_values(self): + playbook = self.test_data.get("playbook_vrf_lite_replace_config_interface_with_extension_values") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 202) + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_replace_lite_changes_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_replace_config") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_12_replace_with_no_atch(self): + playbook = self.test_data.get("playbook_config_replace_no_atch") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("diff")[0]) + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_replace_lite_no_atch(self): + playbook = self.test_data.get("playbook_config_replace_no_atch") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("diff")[0]) + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_replace_without_changes(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="replaced", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertFalse(result.get("response")) + + def test_dcnm_vrf_v2_12_replace_lite_without_changes(self): + playbook = self.test_data.get("playbook_vrf_lite_config") + set_module_args( + dict( + state="replaced", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertFalse(result.get("response")) + + def test_dcnm_vrf_v2_12_lite_override_with_additions_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_override_with_additions_interface_with_extensions") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224") + self.assertEqual(result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.227") + self.assertEqual(result.get("diff")[0]["vrf_id"], 9008011) + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][2]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_lite_override_with_additions_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_override_with_additions_interface_without_extensions") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_12_override_with_deletions(self): + playbook = self.test_data.get("playbook_config_override") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 303) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 303) + self.assertEqual(result.get("diff")[0]["vrf_id"], 9008012) + + self.assertFalse(result.get("diff")[1]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[1]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[1]["attach"][0]["vlan_id"], 202) + self.assertEqual(result.get("diff")[1]["attach"][1]["vlan_id"], 202) + self.assertEqual(result.get("diff")[1]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("diff")[1]) + + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_lite_override_with_deletions_interface_with_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_override_with_deletions_interface_with_extensions") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 202) + + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_lite_override_with_deletions_interface_without_extensions(self): + playbook = self.test_data.get("playbook_vrf_lite_override_with_deletions_interface_without_extensions") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertFalse(result.get("changed")) + self.assertTrue(result.get("failed")) + + def test_dcnm_vrf_v2_12_override_without_changes(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="overridden", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertFalse(result.get("response")) + + def test_dcnm_vrf_v2_12_override_no_changes_lite(self): + playbook = self.test_data.get("playbook_vrf_lite_config") + set_module_args( + dict( + state="overridden", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertFalse(result.get("response")) + + def test_dcnm_vrf_v2_12_delete_std(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="deleted", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False) + self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("diff")[0]) + + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_delete_std_lite(self): + playbook = self.test_data.get("playbook_vrf_lite_config") + set_module_args( + dict( + state="deleted", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 202) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("diff")[0]) + + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_delete_dcnm_only(self): + set_module_args(dict(state="deleted", fabric="test_fabric", config=[])) + result = self.execute_module(changed=True, failed=False) + self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 402) + self.assertEqual(result.get("diff")[0]["attach"][1]["vlan_id"], 403) + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_dcnm") + self.assertNotIn("vrf_id", result.get("diff")[0]) + + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_v2_12_delete_failure(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="deleted", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True) + msg = "NdfcVrf12.push_diff_delete: Deletion of vrfs test_vrf_1 has failed" + self.assertEqual(result["msg"]["response"][2], msg) + + def test_dcnm_vrf_v2_12_query(self): + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="query", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertEqual(result.get("response")[0]["parent"]["vrfName"], "test_vrf_1") + self.assertEqual(result.get("response")[0]["parent"]["vrfId"], 9008011) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["vlan"], + 202, + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["vlan"], + 202, + ) + + def test_dcnm_vrf_v2_12_query_vrf_lite(self): + playbook = self.test_data.get("playbook_vrf_lite_config") + set_module_args( + dict( + state="query", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertEqual(result.get("response")[0]["parent"]["vrfName"], "test_vrf_1") + self.assertEqual(result.get("response")[0]["parent"]["vrfId"], 9008011) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["vlan"], + 202, + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["extensionValues"]["VRF_LITE_CONN"]["VRF_LITE_CONN"][0]["AUTO_VRF_LITE_FLAG"], + "NA", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["vlan"], + 202, + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["extensionValues"]["VRF_LITE_CONN"]["VRF_LITE_CONN"][0]["AUTO_VRF_LITE_FLAG"], + "NA", + ) + + def test_dcnm_vrf_v2_12_query_lite_without_config(self): + set_module_args(dict(state="query", fabric="test_fabric", config=[])) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertEqual(result.get("response")[0]["parent"]["vrfName"], "test_vrf_1") + self.assertEqual(result.get("response")[0]["parent"]["vrfId"], 9008011) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["vlan"], + 202, + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["extensionValues"]["VRF_LITE_CONN"]["VRF_LITE_CONN"][0]["AUTO_VRF_LITE_FLAG"], + "NA", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["lanAttachedState"], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["vlan"], + 202, + ) + self.assertEqual( + result.get("response")[0]["attach"][1]["switchDetailsList"][0]["extensionValues"]["VRF_LITE_CONN"]["VRF_LITE_CONN"][0]["AUTO_VRF_LITE_FLAG"], + "NA", + ) + + def test_dcnm_vrf_v2_12_validation(self): + """ + # Summary + + Verify that two missing mandatory fields are detected and an appropriate + error is generated. The fields are: + + - ip_address + - vrf_name + + The Pydantic model VrfPlaybookModelV12() is used for validation in the + method DcnmVrf.validate_playbook_config_model(). + """ + playbook = self.test_data.get("playbook_config_input_validation") + set_module_args( + dict( + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=True) + pydantic_result = result["msg"] + self.assertEqual(pydantic_result.error_count(), 2) + self.assertEqual(pydantic_result.errors()[0]["loc"], ("attach", 1, "ip_address")) + self.assertEqual(pydantic_result.errors()[0]["msg"], "Field required") + self.assertEqual(pydantic_result.errors()[1]["loc"], ("vrf_name",)) + self.assertEqual(pydantic_result.errors()[1]["msg"], "Field required") + + def test_dcnm_vrf_v2_12_validation_no_config(self): + """ + # Summary + + Verify that an empty config object results in an error when + state is merged. + """ + set_module_args(dict(state="merged", fabric="test_fabric", config=[])) + result = self.execute_module(changed=False, failed=True) + msg = "NdfcVrf12.validate_playbook_config_merged_state: " + msg += "config element is mandatory for merged state" + self.assertEqual(result.get("msg"), msg) + + def test_dcnm_vrf_v2_12_check_mode(self): + self.version = 12 + playbook = self.test_data.get("playbook_config") + set_module_args( + dict( + _ansible_check_mode=True, + state="merged", + fabric="test_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertFalse(result.get("diff")) + self.assertFalse(result.get("response")) + + def test_dcnm_vrf_v2_12_merged_new(self): + self.version = 12 + playbook = self.test_data.get("playbook_config") + set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False) + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224") + self.assertEqual(result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.225") + self.assertEqual(result.get("diff")[0]["vrf_id"], 9008011) + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + self.assertEqual(result["response"][2]["DATA"]["status"], "Deployment of VRF(s) has been initiated successfully") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE)