diff --git a/FEATURE_PORTING_PLAN.md b/FEATURE_PORTING_PLAN.md new file mode 100644 index 000000000..bc9bb1009 --- /dev/null +++ b/FEATURE_PORTING_PLAN.md @@ -0,0 +1,522 @@ +# Feature Porting Plan: dcnm_vrf.py → dcnm_vrf_v2.py + +This document tracks features to be ported from `dcnm_vrf.py` (develop branch) into the new Pydantic-based `dcnm_vrf_v2.py` module. + +## Status Legend + +- ⬜ Not Started +- 🟡 In Progress +- ✅ Completed +- ❌ Not Applicable / Won't Implement + +--- + +## High Priority Features + +### 1. L3VNI Without VLAN Support ✅ + +**Issue:** #337, #435, #481, #508 +**Commits:** 1ee37630, 050b1222, 9070a444 +**Status:** COMPLETED (2025-11-12) +**Commit:** 954ce991 + +**Description:** Support for L3VNI without requiring a VLAN configuration. + +**Changes Required in dcnm_vrf_v2.py:** + +- [x] Add `l3vni_wo_vlan` parameter to module arguments + - Type: bool + - Default: Inherited from fabric level settings + - Documentation: "Enable L3 VNI without VLAN" + +- [x] Add fabric-level detection in `__init__()`: + ```python + self.fabric_nvpairs = self.fabric_data.get("nvPairs") + self.fabric_l3vni_wo_vlan = False + if self.fabric_nvpairs and self.fabric_nvpairs.get("ENABLE_L3VNI_NO_VLAN") == "true": + self.fabric_l3vni_wo_vlan = True + ``` + +- [x] Update documentation notes for: + - `vrf_vlan_name`: "Not applicable to L3VNI w/o VLAN config" + - `vrf_intf_desc`: "Not applicable to L3VNI w/o VLAN config" + - `vrf_int_mtu`: "Not applicable to L3VNI w/o VLAN config" + - `ipv6_linklocal_enable`: "Not applicable to L3VNI w/o VLAN config" + +- [x] Update template config in `update_create_params()`: + - Handled automatically via Pydantic model + - Added `l3vni_wo_vlan` field to `VrfTemplateConfigV12` with alias `enableL3VniNoVlan` + +- [x] Update `get_want_attach()` logic for vlan_id handling: + ```python + # Handle vlan_id based on l3vni_wo_vlan setting + if validated_playbook_config_model.l3vni_wo_vlan: + vlan_id: int = 0 + else: + vlan_id: int = validated_playbook_config_model.vlan_id or 0 + ``` + +- [x] Pydantic models updated: + - Added `l3vni_wo_vlan` to `PlaybookVrfModelV12` + - Added `l3vni_wo_vlan` to `VrfTemplateConfigV12` with alias `enableL3VniNoVlan` + +**Note:** The Pydantic-based implementation in dcnm_vrf_v2 handles template config automatically through models, so manual updates to `diff_for_create()`, `push_diff_create_update()`, and `get_have()` are not required. The models handle serialization/deserialization. + +**Reference Code:** `plugins/modules/dcnm_vrf.py:147, 703-706, 1495, 1847-1852, 2246, 2556, 2856-2871` + +--- + +### 2. Deploy Flag Handling Fix ✅ + +**Issue:** #491 +**Commit:** 4aa56027 +**Status:** COMPLETED (2025-11-12) +**Commit:** 5cf8e407 + +**Description:** VRFs should never deploy when the `deploy` flag is explicitly set to False. + +**Changes Required in dcnm_vrf_v2.py:** + +- [x] Update `get_diff_replace()` to filter VRFs with `deploy=False`: + ```python + modified_all_vrfs = copy.deepcopy(all_vrfs) + for vrf in all_vrfs: + want_vrf_data = find_dict_in_list_by_key_value( + search=self.config, key="vrf_name", value=vrf + ) + if want_vrf_data.get('deploy', True) is False: + modified_all_vrfs.remove(vrf) + + if modified_all_vrfs: + if not diff_deploy: + diff_deploy.update({"vrfNames": ",".join(modified_all_vrfs)}) + else: + vrfs = self.diff_deploy["vrfNames"] + "," + ",".join(modified_all_vrfs) + diff_deploy.update({"vrfNames": vrfs}) + ``` + +- [x] Verified `diff_merge_attach()` already has correct logic: + - Already checks `want_config_deploy is True` before adding VRFs to deploy list (lines 2548, 2551) + - No changes needed - existing implementation is correct + +**Reference Code:** `plugins/modules/dcnm_vrf.py:2098-2113, 2411-2426` + +**Test Cases:** +- VRF with `deploy: false` should be created but not deployed +- VRF with `deploy: false` should not appear in deploy API call +- VRF without deploy flag should default to `deploy: true` + +--- + +### 3. VRF Lite DOT1Q Auto-Allocation ✅ + +**Issue:** #210, #467 +**Commit:** c475d351 +**Status:** COMPLETED (2025-11-12) +**Commit:** ef522122 + +**Description:** Auto-allocate DOT1Q IDs for VRF Lite extensions when not explicitly provided. + +**Changes Required in dcnm_vrf_v2.py:** + +- [x] Add new method `get_vrf_lite_dot1q_id()`: + - Implemented at `dcnm_vrf_v12.py:543-608` + - Calls NDFC resource reservation API endpoint `/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/resource-manager/reserve-id` + - Returns allocated DOT1Q ID or calls fail_json on error + +- [x] Update `update_attach_params_extension_values()` to auto-allocate: + - Method signature updated to accept `serial_number` and `vrf_name` parameters + - Auto-allocation logic added at lines 996-1001 + - Checks if `playbook_vrf_lite_model.dot1q` is empty + - If empty, calls `get_vrf_lite_dot1q_id()` to allocate DOT1Q ID + +- [x] Update `property_values_match()` to accept `skip_prop` parameter: + - Updated method signature at line 465 + - Added logic to skip properties in `skip_prop` list + - Defaults to empty list if `skip_prop` is None + +- [x] Use skip_prop in `_extension_values_match()`: + - Updated at lines 830-834 + - Creates `skip_prop` list + - Adds "DOT1Q_ID" to `skip_prop` if `want_vrf_lite["DOT1Q_ID"]` is empty + - Passes `skip_prop` to `property_values_match()` + +**Note:** The Pydantic-based implementation automatically handles serialization/deserialization through the `PlaybookVrfLiteModel`, which stores `dot1q` as a string and validates it properly. + +**Reference Code:** `plugins/modules/dcnm_vrf.py:3160-3209, 3338-3354, 845-857, 970-977` + +--- + +### 4. IPv6 Redistribute Route Map ✅ + +**Issue:** #492 +**Commit:** 349bbeb6 +**Status:** COMPLETED (2025-11-12) +**Commit:** c75ee142 + +**Description:** Add support for IPv6 redistribute route-map configuration. + +**Changes Required in dcnm_vrf_v2.py:** + +- [x] Add `v6_redist_direct_rmap` parameter to module arguments: + - Type: str + - Default: 'FABRIC-RMAP-REDIST-SUBNET' + - Description: "IPv6 Redistribute Direct Route Map" + - Implemented at `dcnm_vrf_v2.py:126-131` + +- [x] Add `v6_redist_direct_rmap` to PlaybookVrfModelV12: + - Field added at `model_playbook_vrf_v12.py:310` + - Maps to v6VrfRouteMap in template config + - Default: 'FABRIC-RMAP-REDIST-SUBNET' + +- [x] Add `v6_redist_direct_rmap` to VrfTemplateConfigV12: + - Field added at `vrf_template_config_v12.py:74` + - Alias: v6VrfRouteMap + - Automatically serializes to/from controller payload + +**Note:** The Pydantic-based implementation automatically handles serialization/deserialization between playbook parameters (`v6_redist_direct_rmap`) and controller API fields (`v6VrfRouteMap`). Manual updates to `update_create_params()` and `get_have()` are not required as the models handle this automatically. + +**Reference Code:** `plugins/modules/dcnm_vrf.py:122-127, 1491, 1628, 2249, 2550, 3050` + +--- + +## Medium Priority Features + +### 5. Empty InstanceValues Handling ✅ + +**Issue:** #522 +**Commit:** 3acfab8c +**Status:** COMPLETED (2025-11-12) +**Commit:** 4141f8ae + +**Description:** Better handling of empty string vs None for `instanceValues` field. + +**Changes Required in dcnm_vrf_v2.py:** + +- [x] Update condition in `diff_for_attach_deploy()`: + - Updated at `dcnm_vrf_v12.py:705-708` + - Changed from simple truthy check to explicit None and empty string check + - Before: `if want_attach.get("instanceValues") and have_lan_attach_model.instance_values:` + - After: Explicitly checks `(is not None and != "")` for both want and have + - Prevents attempting to parse empty strings as JSON + - Ensures consistent handling of missing/empty instanceValues + +**Reference Code:** `plugins/modules/dcnm_vrf.py:901-906` + +--- + +### 6. Network Attachment Check During Deletion ✅ + +**Issue:** #456 +**Commit:** 28c16fea +**Status:** COMPLETED (2025-11-12) +**Commit:** 40aa7bfb + +**Description:** Prevent VRF deletion if networks are still attached. + +**Changes Required in dcnm_vrf_v2.py:** + +- [x] Add `GET_NET_VRF` path to `dcnm_vrf_paths`: + - Added at `dcnm_vrf_v12.py:69` + - Path: `/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{}/networks?vrf-name={}` + +- [x] Add network check in deletion flow: + - Created `check_network_attachments()` method at `dcnm_vrf_v12.py:3129-3169` + - Queries controller for networks attached to VRF + - Calls fail_json if networks are found + - Provides clear error message directing users to dcnm_network module + +- [x] Call network check before deletion: + - Added calls in both `push_to_remote()` and `push_to_remote_model()` methods + - Checks performed before `push_diff_detach()` is called + - Located at lines 4017-4019 and 4054-4056 + +**Note:** The implementation uses direct list comparison (`resp["DATA"] != []`) rather than `search_nested_json`, which is simpler and equally effective. The check validates data exists and provides appropriate error messages. + +**Reference Code:** `plugins/modules/dcnm_vrf.py:603, 611, 3926-3943` + +--- + +### 7. Orphaned Resources Cleanup Enhancement ✅ + +**Issue:** Various cleanup issues +**Commit:** Multiple +**Status:** COMPLETED (2025-11-12) +**Commit:** 5f0eba0f + +**Description:** Improved cleanup of orphaned VRF resources across multiple pool types. + +**Changes Required in dcnm_vrf_v2.py:** + +- [x] Update `release_orphaned_resources()` signature: + - Updated at `dcnm_vrf_v12.py:3900` + - Now accepts `vrf_del_list: list` instead of single `vrf: str` + - Signature: `def release_orphaned_resources(self, vrf_del_list: list, is_rollback=False) -> None:` + +- [x] Support multiple resource pools: + - Implemented at `dcnm_vrf_v12.py:3965-4022` + - Processes both TOP_DOWN_VRF_VLAN and TOP_DOWN_L3_DOT1Q pools + - Loops through each pool and queries for orphaned resources + +- [x] Update filtering logic to use `vrf_del_list`: + - Updated at `dcnm_vrf_v12.py:3998` + - Changed from `if item["entityName"] != vrf:` to `if item["entityName"] not in vrf_del_list:` + - Added validation for ipAddress and switchName to avoid deleting invalid Fabric-scoped resources + +- [x] Update call sites: + - Modified in both `push_to_remote()` and `push_to_remote_model()` methods + - Changed from looping and calling once per VRF to calling once with entire list + - Located at lines 4058-4062 and 4099-4103 + +**Note:** This enhancement improves efficiency by processing all VRFs in a single operation and ensures proper cleanup of both VLAN and DOT1Q resources (e.g., VRF Lite extensions). + +**Reference Code:** `plugins/modules/dcnm_vrf.py:3826-3885` + +--- + +### 8. Attach State Logic Refinement ✅ + +**Issue:** #522 (partial) +**Commit:** 3acfab8c (partial) +**Status:** COMPLETED (2025-11-12) +**Commit:** d4b5c4f6 + +**Description:** Improved attach state determination logic. + +**Changes Required in dcnm_vrf_v2.py:** + +- [x] Update attach state logic in `get_have_deploy()`: + - Updated at `dcnm_vrf_v12.py:1575-1576` + - Changed from: `deploy = attach.get("isLanAttached")` + - Changed to: `attach_state = bool(attach.get("isLanAttached", False))` followed by `deploy = attach_state` + - Provides explicit False default when isLanAttached is missing from controller response + - Ensures attach_state is always a boolean rather than potentially None + +**Note:** The explicit bool conversion with default value prevents subtle bugs that could occur if the controller response doesn't include the isLanAttached field. This is particularly important for handling edge cases in controller responses. + +**Reference Code:** `plugins/modules/dcnm_vrf.py:1689-1690` + +--- + +### 9. VRF Deletion Failure Fix ✅ + +**Issue:** #451 +**Commit:** faeae9b0 +**Status:** COMPLETED (2025-11-12) - Already implemented in Feature #7 +**Commit:** 5f0eba0f (Feature #7) + +**Description:** Better error handling during VRF deletion failures, specifically preventing attempts to delete invalid TOP_DOWN_VRF_VLAN resources and preventing multiple GET calls to NDFC while deleting orphaned resources. + +**Changes Required in dcnm_vrf_v2.py:** + +- [x] Review and port error handling improvements from deletion flow +- [x] Add validation for ipAddress and switchName in release_orphaned_resources() +- [x] Update signature to accept vrf_del_list instead of single vrf +- [x] Add debug logging for resource cleanup operations + +**Implementation Notes:** + +All fixes from commit faeae9b0 were already implemented as part of Feature #7 (Orphaned Resources Cleanup Enhancement - commit 5f0eba0f): + +- Validation checks for `ipAddress` and `switchName` at `dcnm_vrf_v12.py:4008-4011` +- Comment explaining invalid resources at lines 4005-4007 +- Signature updated to accept `vrf_del_list: list` at line 3901 +- Call sites updated to build list and call once at lines 4060-4064, 4101-4105 +- Debug logging added at lines 4021-4022, 4062-4063, 4103-4104 + +**Note:** Feature #7 comprehensively addressed all the issues from commit faeae9b0, including: +1. Preventing deletion of invalid Fabric-scoped resources (no ipAddress or switchName) +2. Preventing multiple GET calls by accepting a list of VRFs +3. Adding appropriate debug logging throughout the cleanup process + +**Reference Code:** `plugins/modules/dcnm_vrf.py:3668-3807` + +--- + +### 10. Response Data "Fail" Message Handling ✅ + +**Issue:** #324, #457 +**Commit:** 416fa1a9 +**Status:** COMPLETED (2025-11-12) +**Commit:** 3647b9c9 + +**Description:** Proper handling of "Fail" messages in response DATA from NDFC. The controller sometimes returns a 200 OK status but includes "Fail" messages in nested DATA fields, which should be treated as failures. + +**Changes Required in dcnm_vrf_v2.py:** + +- [x] Add `search_nested_json()` utility function to dcnm.py +- [x] Import `search_nested_json` in dcnm_vrf_v12.py +- [x] Update `handle_response()` to check for "fail" in response DATA +- [x] Set fail=True and changed=False when "fail" found in DATA + +**Implementation Details:** + +Added `search_nested_json()` utility function to `plugins/module_utils/network/dcnm/dcnm.py`: + +- Recursively searches nested dictionaries and lists for a search string +- Case-insensitive search of all string values +- Returns True if found, False otherwise +- Handles dict, list, and str types + +Updated `dcnm_vrf_v12.py`: + +- Added import for `search_nested_json` at line 46 +- Added check in `handle_response()` at lines 4479-4483: + +```python +if response_model.DATA: + resp_val = search_nested_json(response_model.DATA, "fail") + if resp_val: + fail = True + changed = False +``` + +**Note:** This prevents the module from incorrectly succeeding when the controller returns a "Fail" status embedded in nested response DATA fields, even if the HTTP status is 200 OK. + +**Reference Code:** `plugins/modules/dcnm_vrf.py:4141-4147, plugins/module_utils/network/dcnm/dcnm.py:889-933` + +--- + +## Low Priority / Nice-to-Have + +### 11. Import Statement Cleanup ✅ + +**Status:** COMPLETED (2025-11-12) - Already properly handled +**Commit:** N/A (No changes needed) + +**Description:** Verify all imports are properly formatted and no unused imports exist. + +**Changes Required:** + +- [x] Verify imports are properly formatted +- [x] Remove unused imports +- [x] Verify `find_dict_in_list_by_key_value` is properly used + +**Implementation Notes:** + +All imports in `dcnm_vrf_v12.py` are: + +- Properly formatted by isort (alphabetical order, proper grouping) +- All imported functions are used in the code: + - `dcnm_get_ip_addr_info`: Used 2 times + - `dcnm_send`: Used 9 times + - `get_fabric_details`: Used 1 time + - `get_fabric_inventory_details`: Used 1 time + - `get_sn_fabric_dict`: Used 1 time + - `search_nested_json`: Used 1 time (added in Feature #10) + +- `find_dict_in_list_by_key_value` is defined as a static method within the `DcnmVrfV12` class (line 340) rather than imported from dcnm.py, which is appropriate for this module's architecture + +**Note:** No changes were needed. Import statements are already clean and properly maintained through continuous use of isort and black formatters. + +**Reference Code:** `plugins/modules/dcnm_vrf.py:589-591` + +--- + +## Implementation Notes + +### Utility Functions Needed + +These utility functions should be available in module_utils or imported from dcnm: + +1. ✅ `find_dict_in_list_by_key_value()` - Available in dcnm +2. ✅ `search_nested_json()` - Available in dcnm +3. ⬜ May need Pydantic equivalents for some operations + +### Pydantic Model Updates + +For each feature, consider whether Pydantic models need updates: + +- [ ] Create/update models for L3VNI without VLAN +- [ ] Add validators for IPv6 route-map +- [ ] Add validators for DOT1Q ID handling +- [ ] Model for VRF Lite extensions + +### Testing Requirements + +For each ported feature: + +- [ ] Unit tests mirroring dcnm_vrf tests +- [ ] Integration tests where applicable +- [ ] Update fixture files +- [ ] Test rollback scenarios + +### Documentation Updates + +- [ ] Update module documentation with new parameters +- [ ] Update EXAMPLES section +- [ ] Update README if needed +- [ ] Add notes about feature parity with dcnm_vrf.py + +--- + +## Progress Tracking + +Use this section to track overall progress: + +- **Total Features Identified:** 11 +- **Completed:** 11 🎉 +- **In Progress:** 0 +- **Not Started:** 0 +- **Won't Implement:** 0 + +### Completed Features + +1. ✅ L3VNI Without VLAN Support (2025-11-12) - Commit 954ce991 +2. ✅ Deploy Flag Handling Fix (2025-11-12) - Commit 5cf8e407 +3. ✅ VRF Lite DOT1Q Auto-Allocation (2025-11-12) - Commit ef522122 +4. ✅ IPv6 Redistribute Route Map (2025-11-12) - Commit c75ee142 +5. ✅ Empty InstanceValues Handling (2025-11-12) - Commit 4141f8ae +6. ✅ Network Attachment Check During Deletion (2025-11-12) - Commit 40aa7bfb +7. ✅ Orphaned Resources Cleanup Enhancement (2025-11-12) - Commit 5f0eba0f +8. ✅ Attach State Logic Refinement (2025-11-12) - Commit d4b5c4f6 +9. ✅ VRF Deletion Failure Fix (2025-11-12) - Already in Feature #7 (Commit 5f0eba0f) +10. ✅ Response Data "Fail" Message Handling (2025-11-12) - Commit 3647b9c9 +11. ✅ Import Statement Cleanup (2025-11-12) - No changes needed (Already clean) + +--- + +## Next Steps + +1. ✅ Replace dcnm_vrf.py with develop version +2. ⬜ Review dcnm_vrf_v2.py current architecture +3. ⬜ Prioritize feature porting based on user needs +4. ⬜ Start with Feature #1 (L3VNI without VLAN) +5. ⬜ Create unit tests for each ported feature +6. ⬜ Update integration tests + +--- + +## Reference Commits in Develop Branch + +| Commit | Issue | Description | +|--------|-------|-------------| +| 9070a444 | #505, #508 | Fix for L3VNI W/O VLAN_ID generation | +| 3acfab8c | #522 | Fixing handling of '' InstanceValues | +| 4aa56027 | #491 | VRFs should never deploy when deploy flag is False | +| c475d351 | #210, #467 | Fix for VRF_Lite Issue & DOT1Q_ID Auto Allocation | +| 349bbeb6 | #492 | Add support for IPv6 redistribute route-map | +| 050b1222 | #481 | DCNM_VRF: L3 VNI W/O VLAN IT Tests & Documentation | +| faeae9b0 | #451 | dcnm_vrf: VRF Deletion Failure Fix | +| 28c16fea | #456 | dcnm_vrf: raises error if networks attached during deletion | +| 416fa1a9 | #324, #457 | Handling "Fail" messages in Response Data from NDFC | +| 1ee37630 | #337, #435 | Add L3VNI w/o VLAN option support | + +--- + +## Questions / Blockers + +Add any questions or blockers encountered during porting: + +1. **Q:** Does dcnm_vrf_v2.py use the same API paths structure? + **A:** [To be determined] + +2. **Q:** How are Pydantic models structured for VRF attachments? + **A:** [To be determined] + +3. **Q:** Should we maintain 100% feature parity or can we skip some legacy features? + **A:** [To be determined based on requirements] + +--- + +Last Updated: 2025-11-12 diff --git a/plugins/module_utils/common/api/onemanage/endpoints.py b/plugins/module_utils/common/api/onemanage/endpoints.py index 615ed85ad..951b5d569 100644 --- a/plugins/module_utils/common/api/onemanage/endpoints.py +++ b/plugins/module_utils/common/api/onemanage/endpoints.py @@ -20,37 +20,20 @@ are all set using the same consistent interface. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, annotations, division, print_function __metaclass__ = type # pylint: disable=invalid-name __author__ = "Allen Robel" import traceback -from typing import Literal, Optional, Union +from typing import Literal, Optional try: from pydantic import BaseModel, Field except ImportError: HAS_PYDANTIC = False - PYDANTIC_IMPORT_ERROR: Union[str, None] = traceback.format_exc() # pylint: disable=invalid-name - - # Fallback: object base class - BaseModel = object # type: ignore[assignment,misc] - - # Fallback: Field that does nothing - def Field(**kwargs): # type: ignore[no-redef] # pylint: disable=unused-argument,invalid-name - """Pydantic Field fallback when pydantic is not available.""" - return None - - # Fallback: field_validator decorator that does nothing - def field_validator(*args, **kwargs): # pylint: disable=unused-argument,invalid-name - """Pydantic field_validator fallback when pydantic is not available.""" - - def decorator(func): - return func - - return decorator - + PYDANTIC_IMPORT_ERROR: str | None = traceback.format_exc() # pylint: disable=invalid-name + from ...third_party.pydantic import BaseModel, Field else: HAS_PYDANTIC = True PYDANTIC_IMPORT_ERROR = None # pylint: disable=invalid-name diff --git a/plugins/module_utils/common/api/query_params.py b/plugins/module_utils/common/api/query_params.py index 33942cc2e..1b826a089 100644 --- a/plugins/module_utils/common/api/query_params.py +++ b/plugins/module_utils/common/api/query_params.py @@ -19,38 +19,21 @@ filtering with type safety via Pydantic. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, annotations, division, print_function __metaclass__ = type # pylint: disable=invalid-name __author__ = "Allen Robel" import traceback from abc import ABC, abstractmethod -from typing import Optional, Union +from typing import Optional try: from pydantic import BaseModel, Field, field_validator except ImportError: HAS_PYDANTIC = False - PYDANTIC_IMPORT_ERROR: Union[str, None] = traceback.format_exc() # pylint: disable=invalid-name - - # Fallback: object base class - BaseModel = object # type: ignore[assignment,misc] - - # Fallback: Field that does nothing - def Field(**kwargs): # type: ignore[no-redef] # pylint: disable=unused-argument,invalid-name - """Pydantic Field fallback when pydantic is not available.""" - return None - - # Fallback: field_validator decorator that does nothing - def field_validator(*args, **kwargs): # type: ignore[no-redef] # pylint: disable=unused-argument,invalid-name - """Pydantic field_validator fallback when pydantic is not available.""" - - def decorator(func): - return func - - return decorator - + PYDANTIC_IMPORT_ERROR: str | None = traceback.format_exc() # pylint: disable=invalid-name + from ...common.third_party.pydantic import BaseModel, Field, field_validator else: HAS_PYDANTIC = True PYDANTIC_IMPORT_ERROR = None # pylint: disable=invalid-name @@ -246,9 +229,9 @@ class CompositeQueryParams: """ def __init__(self) -> None: - self._param_groups: list[Union[EndpointQueryParams, LuceneQueryParams]] = [] + self._param_groups: list[EndpointQueryParams | LuceneQueryParams] = [] - def add(self, params: Union[EndpointQueryParams, LuceneQueryParams]) -> "CompositeQueryParams": + def add(self, params: EndpointQueryParams | LuceneQueryParams) -> "CompositeQueryParams": """ Add a query parameter group to the composite. 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..8f0ad0068 --- /dev/null +++ b/plugins/module_utils/common/models/ipv4_cidr_host.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/models/ipv4_cidr_host.py +""" +Validate CIDR-format IPv4 host address. +""" +import traceback +try: + from pydantic import BaseModel, Field, field_validator +except ImportError: + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() + from ..third_party.pydantic import BaseModel, Field, field_validator +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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..e30f7d60e --- /dev/null +++ b/plugins/module_utils/common/models/ipv4_host.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/models/ipv4_host.py +""" +Validate IPv4 host address. +""" +import traceback +try: + from pydantic import BaseModel, Field, field_validator +except ImportError: + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() + from ..third_party.pydantic import BaseModel, Field, field_validator +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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..667eb545e --- /dev/null +++ b/plugins/module_utils/common/models/ipv4_multicast_group_address.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/models/ipv4_host.py +""" +Validate IPv4 host address. +""" +import traceback +try: + from pydantic import BaseModel, Field, field_validator +except ImportError: + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() + from ..third_party.pydantic import BaseModel, Field, field_validator +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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..4d5cbf306 --- /dev/null +++ b/plugins/module_utils/common/models/ipv6_cidr_host.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/models/ipv6_cidr_host.py +""" +Validate CIDR-format IPv6 host address. +""" +import traceback +try: + from pydantic import BaseModel, Field, field_validator +except ImportError: + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() + from ..third_party.pydantic import BaseModel, Field, field_validator +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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..7fb3f3359 --- /dev/null +++ b/plugins/module_utils/common/models/ipv6_host.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/common/models/ipv6_host.py +""" +Validate IPv6 host address. +""" +import traceback +try: + from pydantic import BaseModel, Field, field_validator +except ImportError: + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() + from ..third_party.pydantic import BaseModel, Field, field_validator +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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/third_party/pydantic.py b/plugins/module_utils/common/third_party/pydantic.py index 49094981a..ae2a5cf22 100644 --- a/plugins/module_utils/common/third_party/pydantic.py +++ b/plugins/module_utils/common/third_party/pydantic.py @@ -17,7 +17,21 @@ Pydantic mocks for environments where pydantic is not installed. """ -BaseModel = object + +class BaseModel: + """Pydantic BaseModel fallback when pydantic is not available.""" + + @classmethod + def model_construct(cls, **kwargs): + """ + Construct a model instance without validation. + + This is a bare-bones implementation for Ansible sanity test compatibility. + """ + instance = cls() + for key, value in kwargs.items(): + setattr(instance, key, value) + return instance def AfterValidator(func): # pylint: disable=invalid-name @@ -35,7 +49,7 @@ def ConfigDict(**kwargs): # pylint: disable=unused-argument,invalid-name return {} -def Field(**kwargs): # pylint: disable=unused-argument,invalid-name +def Field(*args, **kwargs): # pylint: disable=unused-argument,invalid-name """Pydantic Field fallback when pydantic is not available.""" return None 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..37bffc403 --- /dev/null +++ b/plugins/module_utils/common/validators/ipv4_host.py @@ -0,0 +1,47 @@ +# -*- 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) + """ + try: + __, __ = value.split("/") + except (AttributeError, ValueError): + # Value is not a string or does not contain a prefix. + # Let downstream validation handle it. + 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: + __ = 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..93d52e3a0 --- /dev/null +++ b/plugins/module_utils/common/validators/ipv4_multicast_group_address.py @@ -0,0 +1,50 @@ +# -*- 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) + """ + try: + __, __ = value.split("/") + except (AttributeError, ValueError): + # Value is not a string or does not contain a prefix. + # Let downstream validation handle it. + 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..0b88472a6 --- /dev/null +++ b/plugins/module_utils/vrf/dcnm_vrf_v11.py @@ -0,0 +1,3535 @@ +# -*- 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 + + 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..4702360ee --- /dev/null +++ b/plugins/module_utils/vrf/dcnm_vrf_v12.py @@ -0,0 +1,4554 @@ +# -*- coding: utf-8 -*- +# +# 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=too-many-lines,wrong-import-position +""" +dcnm_vrf_v2 module implementation for NDFC version 12 +""" + +from __future__ import absolute_import, annotations, division, print_function + +__metaclass__ = type # pylint: disable=invalid-name +__author__ = "Shrishail Kariyappanavar, Karthik Babu Harichandra Babu, Praveen Ramoorthy, Allen Robel" + +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 + +from ansible.module_utils.basic import AnsibleModule + +try: + from pydantic import ValidationError +except ImportError: + from ..common.third_party.pydantic import ValidationError + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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, + search_nested_json, +) +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", + "GET_NET_VRF": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{}/networks?vrf-name={}", +} + + +@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[dict | list] = None + 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) + + # Check if fabric-level L3VNI without VLAN is enabled + self.fabric_nvpairs: dict = self.fabric_data.get("nvPairs", {}) + self.fabric_l3vni_wo_vlan: bool = False + if self.fabric_nvpairs and self.fabric_nvpairs.get("ENABLE_L3VNI_NO_VLAN") == "true": + self.fabric_l3vni_wo_vlan = True + msg = f"self.fabric_l3vni_wo_vlan: {self.fabric_l3vni_wo_vlan}" + self.log.debug(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, skip_prop: Optional[list[str]] = None) -> bool: + """ + Given two dictionaries, a list of keys, and optional keys to skip: + + - Return True if all property values match (excluding skipped properties). + - Return False otherwise + + ## Parameters + + - dict1: First dictionary + - dict2: Second dictionary + - property_list: List of property names to compare + - skip_prop: Optional list of property names to skip in comparison + """ + if skip_prop is None: + skip_prop = [] + for prop in property_list: + if prop in skip_prop: + continue + 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_vrf_lite_dot1q_id(self, serial_number: str, vrf_name: str, interface: str) -> int: + """ + # Summary + + Given a switch serial, vrf name and interface name, return the dot1q ID + reserved for the vrf_lite extension on that switch. + + ## Raises + + Calls fail_json if DCNM fails to reserve the dot1q ID. + + ## Parameters + + - serial_number: The serial number of the switch + - vrf_name: The VRF name + - interface: The interface name + + ## Returns + + The reserved DOT1Q ID as an integer + + ## Notes + + - This method calls the DCNM/NDFC resource reservation API + - The API endpoint is version 12+ only + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += f"serial_number: {serial_number}, " + msg += f"vrf_name: {vrf_name}, " + msg += f"interface: {interface}" + self.log.debug(msg) + + path = "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/resource-manager/reserve-id" + verb = "POST" + payload = { + "scopeType": "DeviceInterface", + "usageType": "TOP_DOWN_L3_DOT1Q", + "serialNumber": serial_number, + "ifName": interface, + "allocatedTo": vrf_name, + } + + response: ControllerResponseGetIntV12 = ControllerResponseGetIntV12(**dcnm_send(self.module, verb, path, json.dumps(payload))) + + if response.RETURN_CODE != 200: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"Failed to get dot1q ID for vrf_lite extension on switch {serial_number} " + msg += f"for vrf {vrf_name} and interface {interface}. " + msg += f"Response: {response.model_dump()}" + self.module.fail_json(msg=msg) + + dot1q_id = response.DATA + + msg = f"Successfully got dot1q ID {dot1q_id} for vrf_lite extension on switch {serial_number} " + msg += f"for vrf {vrf_name} and interface {interface}." + self.log.debug(msg) + + return dot1q_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") is not None and want_attach.get("instanceValues") != "") and ( + have_lan_attach_model.instance_values is not None 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"]: + # Build skip_prop list to skip DOT1Q_ID comparison if it was auto-allocated + skip_prop = [] + if not want_vrf_lite.get("DOT1Q_ID"): + skip_prop.append("DOT1Q_ID") + if self.property_values_match(want_vrf_lite, have_vrf_lite, self.vrf_lite_properties, skip_prop): + 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, serial_number: str, vrf_name: str) -> 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. + + ## Parameters + + - playbook_vrf_attach_model: The VRF attachment model from the playbook + - serial_number: The serial number of the switch + - vrf_name: The VRF name + + ## 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 + # Auto-allocate DOT1Q ID if not provided in the playbook + if playbook_vrf_lite_model.dot1q: + vrf_lite_conn["DOT1Q_ID"] = playbook_vrf_lite_model.dot1q + else: + dot1q_vlan = self.get_vrf_lite_dot1q_id(serial_number, vrf_name, playbook_vrf_lite_model.interface) + vrf_lite_conn["DOT1Q_ID"] = str(dot1q_vlan) + 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, serial_number=serial_number, vrf_name=vrf_name + ) + 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, (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: + attach_state = bool(attach.get("isLanAttached", False)) + deploy = attach_state + 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 + + # Handle vlan_id based on l3vni_wo_vlan setting + if validated_playbook_config_model.l3vni_wo_vlan: + vlan_id: int = 0 + else: + 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]) -> 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}) + + # Filter out VRFs where deploy=False + modified_all_vrfs: set = set() + for vrf in all_vrfs: + want_vrf_model: PlaybookVrfModelV12 = self.find_model_in_list_by_key_value(search=self.validated_playbook_config_models, key="vrf_name", value=vrf) + # Only include VRFs where deploy is not explicitly set to False + if want_vrf_model and want_vrf_model.deploy is not False: + modified_all_vrfs.add(vrf) + msg = f"VRF: {vrf}, deploy: {want_vrf_model.deploy if want_vrf_model else 'N/A'}, included: {vrf in modified_all_vrfs}" + self.log.debug(msg) + + if modified_all_vrfs: + self.diff_deploy.update({"vrfNames": ",".join(modified_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 check_network_attachments(self, vrf_name: str) -> None: + """ + # Summary + + Check if a VRF has any attached networks before deletion. + + ## Raises + + Calls fail_json if the VRF has attached networks. + + ## Parameters + + - vrf_name: The VRF name to check + + ## Notes + + Networks must be removed before deleting a VRF. + Use the dcnm_network module to remove network attachments. + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + msg += f"vrf_name: {vrf_name}" + self.log.debug(msg) + + path = self.paths["GET_NET_VRF"].format(self.fabric, vrf_name) + resp = dcnm_send(self.module, "GET", path) + + if resp.get("DATA") is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid Response from Controller. {resp}" + self.module.fail_json(msg=msg) + + if resp["DATA"] != []: + msg = f"{vrf_name} in fabric: {self.fabric} has associated network attachments. " + self.log.debug("%s. Number of networks: %d", msg, len(resp["DATA"])) + msg += "Please remove the network attachments " + msg += "before deleting the VRF. (maybe using dcnm_network module)" + self.module.fail_json(msg=msg) + + 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_del_list: list, is_rollback=False) -> None: + """ + # Summary + + Release orphaned resources for multiple VRFs across multiple resource pools. + + ## Description + + After a VRF delete operation, resources such as the TOP_DOWN_VRF_VLAN + and TOP_DOWN_L3_DOT1Q resources 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 in vrf_del_list + - fabricName == self.fabric + - ipAddress is not None + - switchName is not None + + ## Parameters + + - vrf_del_list: List of VRF names to release orphaned resources for + - is_rollback: Whether this is a rollback operation + + ```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" + } + ] + ``` + + ## Notes + + - Processes both TOP_DOWN_VRF_VLAN and TOP_DOWN_L3_DOT1Q resource pools + - Resources with no ipAddress or switchName are invalid (Fabric scope) and not deleted + """ + 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}/" + resource_pool = ["TOP_DOWN_VRF_VLAN", "TOP_DOWN_L3_DOT1Q"] + + for pool in resource_pool: + msg = f"Processing orphaned resources in pool: {pool}" + self.log.debug(msg) + + req_path = path + f"pools/{pool}" + + args = SendToControllerArgs( + action="release_resources", + path=req_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"] not in vrf_del_list: + continue + if item.get("allocatedFlag") is not False: + continue + if item.get("id") is None: + continue + # Resources with no ipAddress or switchName + # are invalid and of Fabric's scope and + # should not be attempted to be deleted here. + if not item.get("ipAddress"): + continue + if not item.get("switchName"): + continue + + msg = f"item {json.dumps(item, indent=4, sort_keys=True)}" + self.log.debug(msg) + + delete_ids.append(item["id"]) + + if len(delete_ids) == 0: + continue + + msg = f"Releasing orphaned resources with IDs: {delete_ids}" + self.log.debug(msg) + 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 + + # Check for network attachments before attempting VRF deletion + for vrf_name in self.diff_delete: + self.check_network_attachments(vrf_name) + + 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) + + if self.diff_delete: + vrf_del_list = list(self.diff_delete.keys()) + msg = f"VRF(s) to be deleted: {vrf_del_list}." + self.log.debug(msg) + self.release_orphaned_resources(vrf_del_list, 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 + + # Check for network attachments before attempting VRF deletion + for vrf_name in self.diff_delete: + self.check_network_attachments(vrf_name) + + 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) + + if self.diff_delete: + vrf_del_list = list(self.diff_delete.keys()) + msg = f"VRF(s) to be deleted: {vrf_del_list}." + self.log.debug(msg) + self.release_orphaned_resources(vrf_del_list, 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 response_model.DATA: + resp_val = search_nested_json(response_model.DATA, "fail") + if resp_val: + 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..bdb0ec053 --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_fabrics_easy_fabric_get.py @@ -0,0 +1,412 @@ +# -*- coding: utf-8 -*- +# @file: plugins/module_utils/vrf/vrf_playbook_model.py +# 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 +""" +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} +""" + +import traceback +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + + +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(default=None, description="AAA remote IP enabled") + AAA_SERVER_CONF: Optional[str] = Field(default=None, description="AAA server configuration") + ACTIVE_MIGRATION: Optional[str] = Field(default=None, description="Active migration") + ADVERTISE_PIP_BGP: Optional[str] = Field(default=None, description="Advertise PIP BGP") + ADVERTISE_PIP_ON_BORDER: Optional[str] = Field(default=None, description="Advertise PIP on border") + AGENT_INTF: Optional[str] = Field(default=None, description="Agent interface") + AGG_ACC_VPC_PO_ID_RANGE: Optional[str] = Field(default=None, description="Aggregate access VPC PO ID range") + AI_ML_QOS_POLICY: Optional[str] = Field(default=None, description="AI/ML QoS policy") + ALLOW_L3VNI_NO_VLAN: Optional[str] = Field(default=None, description="Allow L3VNI no VLAN") + ALLOW_L3VNI_NO_VLAN_PREV: Optional[str] = Field(default=None, description="Allow L3VNI no VLAN previous") + ALLOW_NXC: Optional[str] = Field(default=None, description="Allow NXC") + ALLOW_NXC_PREV: Optional[str] = Field(default=None, description="Allow NXC previous") + ANYCAST_BGW_ADVERTISE_PIP: Optional[str] = Field(default=None, description="Anycast BGW advertise PIP") + ANYCAST_GW_MAC: Optional[str] = Field(default=None, description="Anycast GW MAC") + ANYCAST_LB_ID: Optional[str] = Field(default=None, description="Anycast LB ID") + ANYCAST_RP_IP_RANGE: Optional[str] = Field(default=None, description="Anycast RP IP range") + ANYCAST_RP_IP_RANGE_INTERNAL: Optional[str] = Field(default=None, description="Anycast RP IP range internal") + AUTO_SYMMETRIC_DEFAULT_VRF: Optional[str] = Field(default=None, description="Auto symmetric default VRF") + AUTO_SYMMETRIC_VRF_LITE: Optional[str] = Field(default=None, description="Auto symmetric VRF lite") + AUTO_UNIQUE_VRF_LITE_IP_PREFIX: Optional[str] = Field(default=None, description="Auto unique VRF lite IP prefix") + AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV: Optional[str] = Field(default=None, description="Auto unique VRF lite IP prefix previous") + AUTO_VRFLITE_IFC_DEFAULT_VRF: Optional[str] = Field(default=None, description="Auto VRF lite interface default VRF") + BANNER: Optional[str] = Field(default=None, description="Banner") + BFD_AUTH_ENABLE: Optional[str] = Field(default=None, description="BFD authentication enable") + BFD_AUTH_KEY: Optional[str] = Field(default=None, description="BFD authentication key") + BFD_AUTH_KEY_ID: Optional[str] = Field(default=None, description="BFD authentication key ID") + BFD_ENABLE: Optional[str] = Field(default=None, description="BFD enable") + BFD_ENABLE_PREV: Optional[str] = Field(default=None, description="BFD enable previous") + BFD_IBGP_ENABLE: Optional[str] = Field(default=None, description="BFD iBGP enable") + BFD_ISIS_ENABLE: Optional[str] = Field(default=None, description="BFD ISIS enable") + BFD_OSPF_ENABLE: Optional[str] = Field(default=None, description="BFD OSPF enable") + BFD_PIM_ENABLE: Optional[str] = Field(default=None, description="BFD PIM enable") + BGP_AS: str = Field(min_length=1, description="BGP AS") + BGP_AS_PREV: Optional[str] = Field(default=None, description="BGP AS previous") + BGP_AUTH_ENABLE: Optional[str] = Field(default=None, description="BGP authentication enable") + BGP_AUTH_KEY: Optional[str] = Field(default=None, description="BGP authentication key") + BGP_AUTH_KEY_TYPE: Optional[str] = Field(default=None, description="BGP authentication key type") + BGP_LB_ID: Optional[str] = Field(default=None, description="BGP LB ID") + BOOTSTRAP_CONF: Optional[str] = Field(default=None, description="Bootstrap configuration") + BOOTSTRAP_ENABLE: Optional[str] = Field(default=None, description="Bootstrap enable") + BOOTSTRAP_ENABLE_PREV: Optional[str] = Field(default=None, description="Bootstrap enable previous") + BOOTSTRAP_MULTISUBNET: Optional[str] = Field(default=None, description="Bootstrap multi-subnet") + BOOTSTRAP_MULTISUBNET_INTERNAL: Optional[str] = Field(default=None, description="Bootstrap multi-subnet internal") + BRFIELD_DEBUG_FLAG: Optional[str] = Field(default=None, description="BR field debug flag") + BROWNFIELD_NETWORK_NAME_FORMAT: Optional[str] = Field(default=None, description="Brownfield network name format") + BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS: Optional[str] = Field(default=None, description="Brownfield skip overlay network attachments") + CDP_ENABLE: Optional[str] = Field(default=None, description="CDP enable") + COPP_POLICY: Optional[str] = Field(default=None, description="COPP policy") + DCI_MACSEC_ALGORITHM: Optional[str] = Field(default=None, description="DCI MACsec algorithm") + DCI_MACSEC_CIPHER_SUITE: Optional[str] = Field(default=None, description="DCI MACsec cipher suite") + DCI_MACSEC_FALLBACK_ALGORITHM: Optional[str] = Field(default=None, description="DCI MACsec fallback algorithm") + DCI_MACSEC_FALLBACK_KEY_STRING: Optional[str] = Field(default=None, description="DCI MACsec fallback key string") + DCI_MACSEC_KEY_STRING: Optional[str] = Field(default=None, description="DCI MACsec key string") + DCI_SUBNET_RANGE: Optional[str] = Field(default=None, description="DCI subnet range") + DCI_SUBNET_TARGET_MASK: Optional[str] = Field(default=None, description="DCI subnet target mask") + DEAFULT_QUEUING_POLICY_CLOUDSCALE: Optional[str] = Field(default=None, description="Default queuing policy CloudScale") + DEAFULT_QUEUING_POLICY_OTHER: Optional[str] = Field(default=None, description="Default queuing policy other") + DEAFULT_QUEUING_POLICY_R_SERIES: Optional[str] = Field(default=None, description="Default queuing policy R-series") + DEFAULT_VRF_REDIS_BGP_RMAP: Optional[str] = Field(default=None, description="Default VRF Redis BGP route map") + DEPLOYMENT_FREEZE: Optional[str] = Field(default=None, description="Deployment freeze") + DHCP_ENABLE: Optional[str] = Field(default=None, description="DHCP enable") + DHCP_END: Optional[str] = Field(default=None, description="DHCP end") + DHCP_END_INTERNAL: Optional[str] = Field(default=None, description="DHCP end internal") + DHCP_IPV6_ENABLE: Optional[str] = Field(default=None, description="DHCP IPv6 enable") + DHCP_IPV6_ENABLE_INTERNAL: Optional[str] = Field(default=None, description="DHCP IPv6 enable internal") + DHCP_START: Optional[str] = Field(default=None, description="DHCP start") + DHCP_START_INTERNAL: Optional[str] = Field(default=None, description="DHCP start internal") + DNS_SERVER_IP_LIST: Optional[str] = Field(default=None, description="DNS server IP list") + DNS_SERVER_VRF: Optional[str] = Field(default=None, description="DNS server VRF") + DOMAIN_NAME_INTERNAL: Optional[str] = Field(default=None, description="Domain name internal") + ENABLE_AAA: Optional[str] = Field(default=None, description="Enable AAA") + ENABLE_AGENT: Optional[str] = Field(default=None, description="Enable agent") + ENABLE_AGG_ACC_ID_RANGE: Optional[str] = Field(default=None, description="Enable aggregate access ID range") + ENABLE_AI_ML_QOS_POLICY: Optional[str] = Field(default=None, description="Enable AI/ML QoS policy") + ENABLE_AI_ML_QOS_POLICY_FLAP: Optional[str] = Field(default=None, description="Enable AI/ML QoS policy flap") + ENABLE_DCI_MACSEC: Optional[str] = Field(default=None, description="Enable DCI MACsec") + ENABLE_DCI_MACSEC_PREV: Optional[str] = Field(default=None, description="Enable DCI MACsec previous") + ENABLE_DEFAULT_QUEUING_POLICY: Optional[str] = Field(default=None, description="Enable default queuing policy") + ENABLE_EVPN: Optional[str] = Field(default=None, description="Enable EVPN") + ENABLE_FABRIC_VPC_DOMAIN_ID: Optional[str] = Field(default=None, description="Enable fabric VPC domain ID") + ENABLE_FABRIC_VPC_DOMAIN_ID_PREV: Optional[str] = Field(default=None, description="Enable fabric VPC domain ID previous") + ENABLE_L3VNI_NO_VLAN: Optional[str] = Field(default=None, description="Enable L3VNI no VLAN") + ENABLE_MACSEC: Optional[str] = Field(default=None, description="Enable MACsec") + ENABLE_MACSEC_PREV: Optional[str] = Field(default=None, description="Enable MACsec previous") + ENABLE_NETFLOW: Optional[str] = Field(default=None, description="Enable NetFlow") + ENABLE_NETFLOW_PREV: Optional[str] = Field(default=None, description="Enable NetFlow previous") + ENABLE_NGOAM: Optional[str] = Field(default=None, description="Enable NGOAM") + ENABLE_NXAPI: Optional[str] = Field(default=None, description="Enable NXAPI") + ENABLE_NXAPI_HTTP: Optional[str] = Field(default=None, description="Enable NXAPI HTTP") + ENABLE_PBR: Optional[str] = Field(default=None, description="Enable PBR") + ENABLE_PVLAN: Optional[str] = Field(default=None, description="Enable PVLAN") + ENABLE_PVLAN_PREV: Optional[str] = Field(default=None, description="Enable PVLAN previous") + ENABLE_QKD: Optional[str] = Field(default=None, description="Enable QKD") + ENABLE_RT_INTF_STATS: Optional[str] = Field(default=None, description="Enable RT interface stats") + ENABLE_SGT: Optional[str] = Field(default=None, description="Enable SGT") + ENABLE_SGT_PREV: Optional[str] = Field(default=None, description="Enable SGT previous") + ENABLE_TENANT_DHCP: Optional[str] = Field(default=None, description="Enable tenant DHCP") + ENABLE_TRM: Optional[str] = Field(default=None, description="Enable TRM") + ENABLE_TRMv6: Optional[str] = Field(default=None, description="Enable TRMv6") + ENABLE_VPC_PEER_LINK_NATIVE_VLAN: Optional[str] = Field(default=None, description="Enable VPC peer link native VLAN") + ENABLE_VRI_ID_REALLOC: Optional[str] = Field(default=None, description="Enable VRI ID reallocation") + EXTRA_CONF_INTRA_LINKS: Optional[str] = Field(default=None, description="Extra config intra links") + EXTRA_CONF_LEAF: Optional[str] = Field(default=None, description="Extra config leaf") + EXTRA_CONF_SPINE: Optional[str] = Field(default=None, description="Extra config spine") + EXTRA_CONF_TOR: Optional[str] = Field(default=None, description="Extra config ToR") + EXT_FABRIC_TYPE: Optional[str] = Field(default=None, description="External fabric type") + FABRIC_INTERFACE_TYPE: Optional[str] = Field(default=None, description="Fabric interface type") + FABRIC_MTU: Optional[str] = Field(default=None, description="Fabric MTU") + FABRIC_MTU_PREV: Optional[str] = Field(default=None, description="Fabric MTU previous") + FABRIC_NAME: str = Field(description="Fabric name") + FABRIC_TYPE: Optional[str] = Field(default=None, description="Fabric type") + FABRIC_VPC_DOMAIN_ID: Optional[str] = Field(default=None, description="Fabric VPC domain ID") + FABRIC_VPC_DOMAIN_ID_PREV: Optional[str] = Field(default=None, description="Fabric VPC domain ID previous") + FABRIC_VPC_QOS: Optional[str] = Field(default=None, description="Fabric VPC QoS") + FABRIC_VPC_QOS_POLICY_NAME: Optional[str] = Field(default=None, description="Fabric VPC QoS policy name") + FEATURE_PTP: Optional[str] = Field(default=None, description="Feature PTP") + FEATURE_PTP_INTERNAL: Optional[str] = Field(default=None, description="Feature PTP internal") + FF: Optional[str] = Field(default=None, description="FF") + GRFIELD_DEBUG_FLAG: Optional[str] = Field(default=None, description="GR field debug flag") + HD_TIME: Optional[str] = Field(default=None, description="HD time") + HOST_INTF_ADMIN_STATE: Optional[str] = Field(default=None, description="Host interface admin state") + IBGP_PEER_TEMPLATE: Optional[str] = Field(default=None, description="iBGP peer template") + IBGP_PEER_TEMPLATE_LEAF: Optional[str] = Field(default=None, description="iBGP peer template leaf") + IGNORE_CERT: Optional[str] = Field(default=None, description="Ignore certificate") + INBAND_DHCP_SERVERS: Optional[str] = Field(default=None, description="Inband DHCP servers") + INBAND_MGMT: Optional[str] = Field(default=None, description="Inband management") + INBAND_MGMT_PREV: Optional[str] = Field(default=None, description="Inband management previous") + INTF_STAT_LOAD_INTERVAL: Optional[str] = Field(default=None, description="Interface stat load interval") + IPv6_ANYCAST_RP_IP_RANGE: Optional[str] = Field(default=None, description="IPv6 anycast RP IP range") + IPv6_ANYCAST_RP_IP_RANGE_INTERNAL: Optional[str] = Field(default=None, description="IPv6 anycast RP IP range internal") + IPv6_MULTICAST_GROUP_SUBNET: Optional[str] = Field(default=None, description="IPv6 multicast group subnet") + ISIS_AREA_NUM: Optional[str] = Field(default=None, description="ISIS area number") + ISIS_AREA_NUM_PREV: Optional[str] = Field(default=None, description="ISIS area number previous") + ISIS_AUTH_ENABLE: Optional[str] = Field(default=None, description="ISIS authentication enable") + ISIS_AUTH_KEY: Optional[str] = Field(default=None, description="ISIS authentication key") + ISIS_AUTH_KEYCHAIN_KEY_ID: Optional[str] = Field(default=None, description="ISIS authentication keychain key ID") + ISIS_AUTH_KEYCHAIN_NAME: Optional[str] = Field(default=None, description="ISIS authentication keychain name") + ISIS_LEVEL: Optional[str] = Field(default=None, description="ISIS level") + ISIS_OVERLOAD_ELAPSE_TIME: Optional[str] = Field(default=None, description="ISIS overload elapse time") + ISIS_OVERLOAD_ENABLE: Optional[str] = Field(default=None, description="ISIS overload enable") + ISIS_P2P_ENABLE: Optional[str] = Field(default=None, description="ISIS P2P enable") + KME_SERVER_IP: Optional[str] = Field(default=None, description="KME server IP") + KME_SERVER_PORT: Optional[str] = Field(default=None, description="KME server port") + L2_HOST_INTF_MTU: Optional[str] = Field(default=None, description="L2 host interface MTU") + L2_HOST_INTF_MTU_PREV: Optional[str] = Field(default=None, description="L2 host interface MTU previous") + L2_SEGMENT_ID_RANGE: Optional[str] = Field(default=None, description="L2 segment ID range") + L3VNI_IPv6_MCAST_GROUP: Optional[str] = Field(default=None, description="L3VNI IPv6 multicast group") + L3VNI_MCAST_GROUP: Optional[str] = Field(default=None, description="L3VNI multicast group") + L3_PARTITION_ID_RANGE: Optional[str] = Field(default=None, description="L3 partition ID range") + LINK_STATE_ROUTING: Optional[str] = Field(default=None, description="Link state routing") + LINK_STATE_ROUTING_TAG: Optional[str] = Field(default=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(default=None, description="Loopback0 IPv6 range") + LOOPBACK0_IP_RANGE: Optional[str] = Field(default=None, description="Loopback0 IP range") + LOOPBACK1_IPV6_RANGE: Optional[str] = Field(default=None, description="Loopback1 IPv6 range") + LOOPBACK1_IP_RANGE: Optional[str] = Field(default=None, description="Loopback1 IP range") + MACSEC_ALGORITHM: Optional[str] = Field(default=None, description="MACsec algorithm") + MACSEC_CIPHER_SUITE: Optional[str] = Field(default=None, description="MACsec cipher suite") + MACSEC_FALLBACK_ALGORITHM: Optional[str] = Field(default=None, description="MACsec fallback algorithm") + MACSEC_FALLBACK_KEY_STRING: Optional[str] = Field(default=None, description="MACsec fallback key string") + MACSEC_KEY_STRING: Optional[str] = Field(default=None, description="MACsec key string") + MACSEC_REPORT_TIMER: Optional[str] = Field(default=None, description="MACsec report timer") + MGMT_GW: Optional[str] = Field(default=None, description="Management gateway") + MGMT_GW_INTERNAL: Optional[str] = Field(default=None, description="Management gateway internal") + MGMT_PREFIX: Optional[str] = Field(default=None, description="Management prefix") + MGMT_PREFIX_INTERNAL: Optional[str] = Field(default=None, description="Management prefix internal") + MGMT_V6PREFIX: Optional[str] = Field(default=None, description="Management V6 prefix") + MGMT_V6PREFIX_INTERNAL: Optional[str] = Field(default=None, description="Management V6 prefix internal") + MPLS_HANDOFF: Optional[str] = Field(default=None, description="MPLS handoff") + MPLS_ISIS_AREA_NUM: Optional[str] = Field(default=None, description="MPLS ISIS area number") + MPLS_ISIS_AREA_NUM_PREV: Optional[str] = Field(default=None, description="MPLS ISIS area number previous") + MPLS_LB_ID: Optional[str] = Field(default=None, description="MPLS LB ID") + MPLS_LOOPBACK_IP_RANGE: Optional[str] = Field(default=None, description="MPLS loopback IP range") + MSO_CONNECTIVITY_DEPLOYED: Optional[str] = Field(default=None, description="MSO connectivity deployed") + MSO_CONTROLER_ID: Optional[str] = Field(default=None, description="MSO controller ID") + MSO_SITE_GROUP_NAME: Optional[str] = Field(default=None, description="MSO site group name") + MSO_SITE_ID: Optional[str] = Field(default=None, description="MSO site ID") + MST_INSTANCE_RANGE: Optional[str] = Field(default=None, description="MST instance range") + MULTICAST_GROUP_SUBNET: Optional[str] = Field(default=None, description="Multicast group subnet") + MVPN_VRI_ID_RANGE: Optional[str] = Field(default=None, description="MVPN VRI ID range") + NETFLOW_EXPORTER_LIST: Optional[str] = Field(default=None, description="NetFlow exporter list") + NETFLOW_MONITOR_LIST: Optional[str] = Field(default=None, description="NetFlow monitor list") + NETFLOW_RECORD_LIST: Optional[str] = Field(default=None, description="NetFlow record list") + NETWORK_VLAN_RANGE: Optional[str] = Field(default=None, description="Network VLAN range") + NTP_SERVER_IP_LIST: Optional[str] = Field(default=None, description="NTP server IP list") + NTP_SERVER_VRF: Optional[str] = Field(default=None, description="NTP server VRF") + NVE_LB_ID: Optional[str] = Field(default=None, description="NVE LB ID") + NXAPI_HTTPS_PORT: Optional[str] = Field(default=None, description="NXAPI HTTPS port") + NXAPI_HTTP_PORT: Optional[str] = Field(default=None, description="NXAPI HTTP port") + NXC_DEST_VRF: Optional[str] = Field(default=None, description="NXC destination VRF") + NXC_PROXY_PORT: Optional[str] = Field(default=None, description="NXC proxy port") + NXC_PROXY_SERVER: Optional[str] = Field(default=None, description="NXC proxy server") + NXC_SRC_INTF: Optional[str] = Field(default=None, description="NXC source interface") + OBJECT_TRACKING_NUMBER_RANGE: Optional[str] = Field(default=None, description="Object tracking number range") + OSPF_AREA_ID: Optional[str] = Field(default=None, description="OSPF area ID") + OSPF_AUTH_ENABLE: Optional[str] = Field(default=None, description="OSPF authentication enable") + OSPF_AUTH_KEY: Optional[str] = Field(default=None, description="OSPF authentication key") + OSPF_AUTH_KEY_ID: Optional[str] = Field(default=None, description="OSPF authentication key ID") + OVERLAY_MODE: Optional[str] = Field(default=None, description="Overlay mode") + OVERLAY_MODE_PREV: Optional[str] = Field(default=None, description="Overlay mode previous") + OVERWRITE_GLOBAL_NXC: Optional[str] = Field(default=None, description="Overwrite global NXC") + PER_VRF_LOOPBACK_AUTO_PROVISION: Optional[str] = Field(default=None, description="Per VRF loopback auto provision") + PER_VRF_LOOPBACK_AUTO_PROVISION_PREV: Optional[str] = Field(default=None, description="Per VRF loopback auto provision previous") + PER_VRF_LOOPBACK_AUTO_PROVISION_V6: Optional[str] = Field(default=None, description="Per VRF loopback auto provision V6") + PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV: Optional[str] = Field(default=None, description="Per VRF loopback auto provision V6 previous") + PER_VRF_LOOPBACK_IP_RANGE: Optional[str] = Field(default=None, description="Per VRF loopback IP range") + PER_VRF_LOOPBACK_IP_RANGE_V6: Optional[str] = Field(default=None, description="Per VRF loopback IP range V6") + PFC_WATCH_INT: Optional[str] = Field(default=None, description="PFC watch interval") + PFC_WATCH_INT_PREV: Optional[str] = Field(default=None, description="PFC watch interval previous") + PHANTOM_RP_LB_ID1: Optional[str] = Field(default=None, description="Phantom RP LB ID1") + PHANTOM_RP_LB_ID2: Optional[str] = Field(default=None, description="Phantom RP LB ID2") + PHANTOM_RP_LB_ID3: Optional[str] = Field(default=None, description="Phantom RP LB ID3") + PHANTOM_RP_LB_ID4: Optional[str] = Field(default=None, description="Phantom RP LB ID4") + PIM_HELLO_AUTH_ENABLE: Optional[str] = Field(default=None, description="PIM hello authentication enable") + PIM_HELLO_AUTH_KEY: Optional[str] = Field(default=None, description="PIM hello authentication key") + PM_ENABLE: Optional[str] = Field(default=None, description="PM enable") + PM_ENABLE_PREV: Optional[str] = Field(default=None, description="PM enable previous") + PNP_ENABLE_INTERNAL: Optional[str] = Field(default=None, description="PNP enable internal") + POWER_REDUNDANCY_MODE: Optional[str] = Field(default=None, description="Power redundancy mode") + PREMSO_PARENT_FABRIC: Optional[str] = Field(default=None, description="Pre-MSO parent fabric") + PTP_DOMAIN_ID: Optional[str] = Field(default=None, description="PTP domain ID") + PTP_LB_ID: Optional[str] = Field(default=None, description="PTP LB ID") + PTP_VLAN_ID: Optional[str] = Field(default=None, description="PTP VLAN ID") + QKD_PROFILE_NAME: Optional[str] = Field(default=None, description="QKD profile name") + QKD_PROFILE_NAME_PREV: Optional[str] = Field(default=None, description="QKD profile name previous") + REPLICATION_MODE: Optional[str] = Field(default=None, description="Replication mode") + ROUTER_ID_RANGE: Optional[str] = Field(default=None, description="Router ID range") + ROUTE_MAP_SEQUENCE_NUMBER_RANGE: Optional[str] = Field(default=None, description="Route map sequence number range") + RP_COUNT: Optional[str] = Field(default=None, description="RP count") + RP_LB_ID: Optional[str] = Field(default=None, description="RP LB ID") + RP_MODE: Optional[str] = Field(default=None, description="RP mode") + RR_COUNT: Optional[str] = Field(default=None, description="RR count") + SEED_SWITCH_CORE_INTERFACES: Optional[str] = Field(default=None, description="Seed switch core interfaces") + SERVICE_NETWORK_VLAN_RANGE: Optional[str] = Field(default=None, description="Service network VLAN range") + SGT_ID_RANGE: Optional[str] = Field(default=None, description="SGT ID range") + SGT_NAME_PREFIX: Optional[str] = Field(default=None, description="SGT name prefix") + SGT_OPER_STATUS: Optional[str] = Field(default=None, description="SGT operational status") + SGT_PREPROVISION: Optional[str] = Field(default=None, description="SGT pre-provision") + SGT_PREPROVISION_PREV: Optional[str] = Field(default=None, description="SGT pre-provision previous") + SGT_PREPROV_RECALC_STATUS: Optional[str] = Field(default=None, description="SGT pre-provision recalc status") + SGT_RECALC_STATUS: Optional[str] = Field(default=None, description="SGT recalc status") + SITE_ID: Optional[str] = Field(default=None, description="Site ID") + SITE_ID_POLICY_ID: Optional[str] = Field(default=None, description="Site ID policy ID") + SLA_ID_RANGE: Optional[str] = Field(default=None, description="SLA ID range") + SNMP_SERVER_HOST_TRAP: Optional[str] = Field(default=None, description="SNMP server host trap") + SPINE_COUNT: Optional[str] = Field(default=None, description="Spine count") + SPINE_SWITCH_CORE_INTERFACES: Optional[str] = Field(default=None, description="Spine switch core interfaces") + SSPINE_ADD_DEL_DEBUG_FLAG: Optional[str] = Field(default=None, description="Super spine add/del debug flag") + SSPINE_COUNT: Optional[str] = Field(default=None, description="Super spine count") + STATIC_UNDERLAY_IP_ALLOC: Optional[str] = Field(default=None, description="Static underlay IP allocation") + STP_BRIDGE_PRIORITY: Optional[str] = Field(default=None, description="STP bridge priority") + STP_ROOT_OPTION: Optional[str] = Field(default=None, description="STP root option") + STP_VLAN_RANGE: Optional[str] = Field(default=None, description="STP VLAN range") + STRICT_CC_MODE: Optional[str] = Field(default=None, description="Strict CC mode") + SUBINTERFACE_RANGE: Optional[str] = Field(default=None, description="Subinterface range") + SUBNET_RANGE: Optional[str] = Field(default=None, description="Subnet range") + SUBNET_TARGET_MASK: Optional[str] = Field(default=None, description="Subnet target mask") + SYSLOG_SERVER_IP_LIST: Optional[str] = Field(default=None, description="Syslog server IP list") + SYSLOG_SERVER_VRF: Optional[str] = Field(default=None, description="Syslog server VRF") + SYSLOG_SEV: Optional[str] = Field(default=None, description="Syslog severity") + TCAM_ALLOCATION: Optional[str] = Field(default=None, description="TCAM allocation") + TOPDOWN_CONFIG_RM_TRACKING: Optional[str] = Field(default=None, description="Top-down config RM tracking") + TRUSTPOINT_LABEL: Optional[str] = Field(default=None, description="Trustpoint label") + UNDERLAY_IS_V6: Optional[str] = Field(default=None, description="Underlay is V6") + UNDERLAY_IS_V6_PREV: Optional[str] = Field(default=None, description="Underlay is V6 previous") + UNNUM_BOOTSTRAP_LB_ID: Optional[str] = Field(default=None, description="Unnumbered bootstrap LB ID") + UNNUM_DHCP_END: Optional[str] = Field(default=None, description="Unnumbered DHCP end") + UNNUM_DHCP_END_INTERNAL: Optional[str] = Field(default=None, description="Unnumbered DHCP end internal") + UNNUM_DHCP_START: Optional[str] = Field(default=None, description="Unnumbered DHCP start") + UNNUM_DHCP_START_INTERNAL: Optional[str] = Field(default=None, description="Unnumbered DHCP start internal") + UPGRADE_FROM_VERSION: Optional[str] = Field(default=None, description="Upgrade from version") + USE_LINK_LOCAL: Optional[str] = Field(default=None, description="Use link local") + V6_SUBNET_RANGE: Optional[str] = Field(default=None, description="V6 subnet range") + V6_SUBNET_TARGET_MASK: Optional[str] = Field(default=None, description="V6 subnet target mask") + VPC_AUTO_RECOVERY_TIME: Optional[str] = Field(default=None, description="VPC auto recovery time") + VPC_DELAY_RESTORE: Optional[str] = Field(default=None, description="VPC delay restore") + VPC_DELAY_RESTORE_TIME: Optional[str] = Field(default=None, description="VPC delay restore time") + VPC_DOMAIN_ID_RANGE: Optional[str] = Field(default=None, description="VPC domain ID range") + VPC_ENABLE_IPv6_ND_SYNC: Optional[str] = Field(default=None, description="VPC enable IPv6 ND sync") + VPC_PEER_KEEP_ALIVE_OPTION: Optional[str] = Field(default=None, description="VPC peer keep alive option") + VPC_PEER_LINK_PO: Optional[str] = Field(default=None, description="VPC peer link PO") + VPC_PEER_LINK_VLAN: Optional[str] = Field(default=None, description="VPC peer link VLAN") + VRF_LITE_AUTOCONFIG: Optional[str] = Field(default=None, description="VRF lite auto-config") + VRF_VLAN_RANGE: Optional[str] = Field(default=None, description="VRF VLAN range") + abstract_anycast_rp: Optional[str] = Field(default=None, description="Abstract anycast RP") + abstract_bgp: Optional[str] = Field(default=None, description="Abstract BGP") + abstract_bgp_neighbor: Optional[str] = Field(default=None, description="Abstract BGP neighbor") + abstract_bgp_rr: Optional[str] = Field(default=None, description="Abstract BGP RR") + abstract_dhcp: Optional[str] = Field(default=None, description="Abstract DHCP") + abstract_extra_config_bootstrap: Optional[str] = Field(default=None, description="Abstract extra config bootstrap") + abstract_extra_config_leaf: Optional[str] = Field(default=None, description="Abstract extra config leaf") + abstract_extra_config_spine: Optional[str] = Field(default=None, description="Abstract extra config spine") + abstract_extra_config_tor: Optional[str] = Field(default=None, description="Abstract extra config ToR") + abstract_feature_leaf: Optional[str] = Field(default=None, description="Abstract feature leaf") + abstract_feature_spine: Optional[str] = Field(default=None, description="Abstract feature spine") + abstract_isis: Optional[str] = Field(default=None, description="Abstract ISIS") + abstract_isis_interface: Optional[str] = Field(default=None, description="Abstract ISIS interface") + abstract_loopback_interface: Optional[str] = Field(default=None, description="Abstract loopback interface") + abstract_multicast: Optional[str] = Field(default=None, description="Abstract multicast") + abstract_ospf: Optional[str] = Field(default=None, description="Abstract OSPF") + abstract_ospf_interface: Optional[str] = Field(default=None, description="Abstract OSPF interface") + abstract_pim_interface: Optional[str] = Field(default=None, description="Abstract PIM interface") + abstract_route_map: Optional[str] = Field(default=None, description="Abstract route map") + abstract_routed_host: Optional[str] = Field(default=None, description="Abstract routed host") + abstract_trunk_host: Optional[str] = Field(default=None, description="Abstract trunk host") + abstract_vlan_interface: Optional[str] = Field(default=None, description="Abstract VLAN interface") + abstract_vpc_domain: Optional[str] = Field(default=None, description="Abstract VPC domain") + dcnmUser: Optional[str] = Field(default=None, description="DCNM user") + default_network: Optional[str] = Field(default=None, description="Default network") + default_pvlan_sec_network: Optional[str] = Field(default=None, description="Default PVLAN secondary network") + default_vrf: Optional[str] = Field(default=None, description="Default VRF") + enableRealTimeBackup: Optional[str] = Field(default=None, description="Enable real-time backup") + enableScheduledBackup: Optional[str] = Field(default=None, description="Enable scheduled backup") + network_extension_template: Optional[str] = Field(default=None, description="Network extension template") + scheduledTime: Optional[str] = Field(default=None, description="Scheduled time") + temp_anycast_gateway: Optional[str] = Field(default=None, description="Temp anycast gateway") + temp_vpc_domain_mgmt: Optional[str] = Field(default=None, description="Temp VPC domain management") + temp_vpc_peer_link: Optional[str] = Field(default=None, description="Temp VPC peer link") + vrf_extension_template: Optional[str] = Field(default=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..feaf83185 --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_generic_v12.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +Generic response model for the controller +""" +import traceback +from typing import Any, Optional + +try: + from pydantic import BaseModel, ConfigDict, Field +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field + + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + + +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..89d1ff41a --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_get_fabrics_vrfinfo.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +import traceback +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + + +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..865314deb --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_get_int.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +import traceback +from typing import Optional + +try: + from pydantic import ConfigDict, Field +except ImportError: + from ..common.third_party.pydantic import ConfigDict, Field + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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..7d9592113 --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_vrfs_attachments_v12.py @@ -0,0 +1,194 @@ +# -*- 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 __future__ import annotations + +import traceback +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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`: int | None = alias="vlanId", ge=2, le=4094 + - `vrf_id`: 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: int | None = Field(alias="vlanId", ge=2, le=4094) + vrf_id: 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..703a540cf --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_vrfs_deployments_v12.py @@ -0,0 +1,111 @@ +# -*- 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 +""" +from __future__ import annotations + +import traceback +import warnings +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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[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..317628d6d --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_vrfs_switches_v12.py @@ -0,0 +1,266 @@ +# -*- 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 +""" +from __future__ import annotations + +import json +import traceback +from typing import Any, List, Optional + +try: + from pydantic import BaseModel, ConfigDict, Field, field_validator +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field, field_validator + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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: 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: 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: 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 a 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) -> 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..5f50003d0 --- /dev/null +++ b/plugins/module_utils/vrf/model_controller_response_vrfs_v12.py @@ -0,0 +1,168 @@ +""" +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 +""" + +from __future__ import annotations + +import traceback +import warnings +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning, model_validator +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning, model_validator + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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: str | None = Field(default=None) + source: str | None = Field(default=None) + tenantName: 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) -> "VrfObjectV12": + """ + If hierarchicalKey is "", set it to the fabric name. + """ + if self.hierarchicalKey == "": + self.hierarchicalKey = self.fabric # pylint: disable=invalid-name + 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..b09d85e02 --- /dev/null +++ b/plugins/module_utils/vrf/model_have_attach_post_mutate_v12.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import traceback +from typing import List, Optional + +try: + from pydantic import BaseModel, ConfigDict, Field +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + + +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: 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: 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..77e08511b --- /dev/null +++ b/plugins/module_utils/vrf/model_payload_vrfs_attachments.py @@ -0,0 +1,542 @@ +# -*- 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. +""" +from __future__ import annotations + +import json +import traceback +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + + +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: 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 + + @classmethod + def model_construct(cls, *args, **kwargs): # pylint: disable=signature-differs + """For ansible-sanity import tests. Construct model instance, with fallback for when pydantic is not available.""" + if HAS_PYDANTIC: + return super().model_construct(*args, **kwargs) + # Fallback: return self when pydantic is not available + return cls() + + +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: str | dict) -> 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..b89be14ab --- /dev/null +++ b/plugins/module_utils/vrf/model_payload_vrfs_deployments.py @@ -0,0 +1,68 @@ +# -*- 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 +""" +import traceback + +try: + from pydantic import BaseModel, ConfigDict, Field, field_serializer +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field, field_serializer + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + + +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..bf59f5803 --- /dev/null +++ b/plugins/module_utils/vrf/model_playbook_vrf_v11.py @@ -0,0 +1,303 @@ +# -*- 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 __future__ import annotations + +import traceback +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field, model_validator +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field, model_validator + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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) -> "VrfLiteModel": + """ + 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) -> "VrfLiteModel": + """ + 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) -> "VrfLiteModel": + """ + 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) -> "VrfLiteModel": + """ + 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) -> "VrfAttachModel": + """ + 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) -> "VrfAttachModel": + """ + 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: 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[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) -> "VrfPlaybookModelV11": + """ + 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) -> "VrfPlaybookModelV11": + """ + 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..4e4702cb4 --- /dev/null +++ b/plugins/module_utils/vrf/model_playbook_vrf_v12.py @@ -0,0 +1,443 @@ +# -*- coding: utf-8 -*- +# @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 __future__ import annotations + +import traceback +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field, StrictBool, field_validator +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field, StrictBool, field_validator + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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: Optional[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: Optional[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 + - l3vni_wo_vlan - 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 + - v6_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 + l3vni_wo_vlan: StrictBool = Field(default=False) # enableL3VniNoVlan + 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 + v6_redist_direct_rmap: str = Field(default="FABRIC-RMAP-REDIST-SUBNET") # v6VrfRouteMap + rp_address: str = Field(default="") # rpAddress + rp_external: StrictBool = Field(default=False) # isRPExternal + rp_loopback_id: Optional[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: int | str) -> 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: int | str) -> 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: int | str) -> 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: int | str) -> 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..86e30c14a --- /dev/null +++ b/plugins/module_utils/vrf/model_vrf_detach_payload_v12.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import traceback +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field, field_validator +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field, field_validator + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + + +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: 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: Optional[int] = Field(alias="vlanId", default=None) + 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) -> 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..8ce90b6e3 --- /dev/null +++ b/plugins/module_utils/vrf/vrf_controller_payload_v12.py @@ -0,0 +1,152 @@ +""" +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 +""" +from __future__ import annotations + +import traceback +import warnings +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning, field_serializer, field_validator, model_validator +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning, field_serializer, field_validator, model_validator + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + + +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: Optional[str] = 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: Optional[str]) -> 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) -> "VrfPayloadV12": + """ + 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..f443b7022 --- /dev/null +++ b/plugins/module_utils/vrf/vrf_controller_to_playbook_v11.py @@ -0,0 +1,77 @@ +# -*- 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 __future__ import annotations + +import traceback +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + + +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[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..b19240b30 --- /dev/null +++ b/plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# @author: Allen Robel +# @file: plugins/module_utils/vrf/vrf_controller_to_playbook_v12.py +# 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 +""" +Serialize NDFC version 12 controller payload fields to fields used in a dcnm_vrf playbook. +""" +from __future__ import annotations + +import traceback +from typing import Optional + +try: + from pydantic import BaseModel, ConfigDict, Field +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + + +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[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..7d3f119ae --- /dev/null +++ b/plugins/module_utils/vrf/vrf_template_config_v12.py @@ -0,0 +1,270 @@ +""" +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 +""" +from __future__ import annotations + +import json +import traceback +import warnings +from typing import Any, Optional + +try: + from pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning, field_validator, model_validator +except ImportError: + from ..common.third_party.pydantic import BaseModel, ConfigDict, Field, PydanticExperimentalWarning, field_validator, model_validator + HAS_PYDANTIC = False + PYDANTIC_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYDANTIC = True + PYDANTIC_IMPORT_ERROR = None + +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.", + ) + l3vni_wo_vlan: bool = Field(default=False, alias="enableL3VniNoVlan", description="Enable L3 VNI without VLAN") + 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") + v6_redist_direct_rmap: str = Field(default="FABRIC-RMAP-REDIST-SUBNET", alias="v6VrfRouteMap", description="IPv6 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[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) -> 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) -> 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_v2.py b/plugins/modules/dcnm_vrf_v2.py new file mode 100644 index 000000000..d0cc7c64f --- /dev/null +++ b/plugins/modules/dcnm_vrf_v2.py @@ -0,0 +1,740 @@ +#!/usr/bin/python +# -*- 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, annotations, 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 + - Not applicable to L3VNI w/o VLAN config + type: str + required: false + vrf_intf_desc: + description: + - VRF Intf Description + - Not applicable to L3VNI w/o VLAN config + type: str + required: false + vrf_description: + description: + - VRF Description + type: str + required: false + vrf_int_mtu: + description: + - VRF interface MTU + - Not applicable to L3VNI w/o VLAN config + 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' + v6_redist_direct_rmap: + description: + - IPv6 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 + - Not applicable to L3VNI w/o VLAN config + type: bool + required: false + default: true + l3vni_wo_vlan: + description: + - Enable L3 VNI without VLAN + type: bool + required: false + default: Inherited from fabric level settings + 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] +FIRST_PARTY_FAILED_IMPORT: set[str] = set() + +from ..module_utils.common.enums.ansible import AnsibleStates +from ..module_utils.common.log_v2 import Log +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): + # Logging setup failed, continue without logging + 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_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/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 f3e3e3c6d..d587f78ff 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,10 +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: Log task + cisco.dcnm.dcnm_log: + msg: "{{ task_name }}" + +- name: "{{ task_name }}" - name: TEST.1e - DELETED - [merged] Create, Attach VRF ansible-vrf (L3VNI W/O VLAN) cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" @@ -263,7 +312,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 @@ -279,19 +336,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 @@ -321,17 +386,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 @@ -340,6 +405,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 @@ -350,11 +423,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 @@ -378,7 +459,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 @@ -394,19 +483,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 @@ -419,17 +516,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 @@ -438,7 +535,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 @@ -448,15 +553,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 e5f1033f1..6d7385df3 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 VRF ansible-vrf-int1 (L3VNI W/O 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 @@ -168,7 +215,15 @@ deploy: false register: result_2 -- name: TEST.2a - MERGED - [query] query VRF state +- 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 @@ -181,16 +236,28 @@ - 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" - '"{{ switch_1 }}" in result_2.diff[0].attach[0].ip_address or "{{ switch_1 }}" in result_2.diff[0].attach[1].ip_address' - '"{{ switch_2 }}" in result_2.diff[0].attach[0].ip_address or "{{ switch_2 }}" in result_2.diff[0].attach[1].ip_address' - - result_2.diff[0].vrf_name == "ansible-vrf-int1" - '"enableL3VniNoVlan\":\"true\"" in result_2a.response[0].parent.vrfTemplateConfig' -- 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 @@ -203,12 +270,34 @@ - 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 -- name: TEST.3 - MERGED - [merged] Create, Attach, Deploy VLAN+VRF (controller provided VLAN) +- name: TEST.2f - MERGED - [wait_for] Wait 60 seconds for controller and switch to sync + # While vrf-lite extension was not configured above, we still hit VRF + # OUT-OF-SYNC. Let's see if waiting helps here too. + wait_for: + timeout: 60 + +- 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 @@ -223,7 +312,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 @@ -240,18 +337,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 @@ -264,7 +373,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 @@ -275,11 +392,19 @@ wait_for: timeout: 60 -- name: TEST.4 - MERGED - [merged] Create, Attach, Deploy VLAN+VRF+LITE EXTENSION ansible-vrf-int1 switch_2 (user provided VLAN) - cisco.dcnm.dcnm_vrf: &conf4 +- 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 - config: + config: &conf4 - vrf_name: ansible-vrf-int1 vrf_id: 9008011 vrf_template: Default_VRF_Universal @@ -299,7 +424,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 @@ -316,17 +449,22 @@ - 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: "{{ task_name }}" - name: TEST.4c - MERGED - [merged] conf4 - Idempotence cisco.dcnm.dcnm_vrf: *conf4 register: result_4c @@ -512,7 +650,8 @@ - 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: @@ -544,7 +683,8 @@ - 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: @@ -592,7 +732,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 fa7b2a448..f7ba61c95 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: SETUP.2 - 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 @@ -105,20 +121,28 @@ - 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[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 (L3VNI W/O VLAN) cisco.dcnm.dcnm_vrf: &conf1 @@ -148,24 +172,41 @@ - assert: that: - - 'result_1.changed == true' - - 'result_1.response[0].RETURN_CODE == 200' - - 'result_1.response[1].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[5].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_1.response[5].DATA|dict2items)[1].value == "SUCCESS"' - - '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"' - - '"enableL3VniNoVlan\":\"true\"" in result_1a.response[0].parent.vrfTemplateConfig' - - 'result_1a.response[0].parent.vrfStatus == "PENDING"' - -- 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 @@ -175,10 +216,18 @@ - assert: that: - - 'result_1c.changed == false' - - 'result_1c.response|length == 0' + - result_1c.changed == false + - result_1c.response|length == 0 -- name: TEST.1f - OVERRIDDEN - [deleted] Delete all VRFs +- 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: "{{ task_name }}" cisco.dcnm.dcnm_vrf: fabric: "{{ fabric_1 }}" state: deleted @@ -524,7 +573,15 @@ ## 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 aa8298ec5..8622f1374 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 - name: TEST.7b - QUERY - [deleted] Delete all VRFs cisco.dcnm.dcnm_vrf: 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 d37199d57..d4e15b03c 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,7 +577,7 @@ - assert: that: - - 'result_5c.changed == false' + - result_5c.changed == false - name: TEST.5e - REPLACED - [deleted] Delete all VRFs cisco.dcnm.dcnm_vrf: @@ -552,7 +741,15 @@ ### 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.15.txt b/tests/sanity/ignore-2.15.txt index 76d7d4da7..380e96da1 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -20,3 +20,4 @@ plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-lic plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # ignore license check +plugins/modules/dcnm_vrf_v2.py validate-modules:missing-gplv3-license # ignore license check diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index 76d7d4da7..380e96da1 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -20,3 +20,4 @@ plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-lic plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # ignore license check +plugins/modules/dcnm_vrf_v2.py validate-modules:missing-gplv3-license # ignore license check diff --git a/tests/sanity/ignore-2.17.txt b/tests/sanity/ignore-2.17.txt index 76d7d4da7..380e96da1 100644 --- a/tests/sanity/ignore-2.17.txt +++ b/tests/sanity/ignore-2.17.txt @@ -20,3 +20,4 @@ plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-lic plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # ignore license check +plugins/modules/dcnm_vrf_v2.py validate-modules:missing-gplv3-license # ignore license check diff --git a/tests/sanity/ignore-2.18.txt b/tests/sanity/ignore-2.18.txt index 76d7d4da7..380e96da1 100644 --- a/tests/sanity/ignore-2.18.txt +++ b/tests/sanity/ignore-2.18.txt @@ -20,3 +20,4 @@ plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-lic plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # ignore license check +plugins/modules/dcnm_vrf_v2.py validate-modules:missing-gplv3-license # ignore license check diff --git a/tests/sanity/ignore-2.19.txt b/tests/sanity/ignore-2.19.txt index 76d7d4da7..380e96da1 100644 --- a/tests/sanity/ignore-2.19.txt +++ b/tests/sanity/ignore-2.19.txt @@ -20,3 +20,4 @@ plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-lic plugins/modules/dcnm_template.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # ignore license check +plugins/modules/dcnm_vrf_v2.py validate-modules:missing-gplv3-license # ignore license check 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 a3fc3ae46..dc0288b3a 100644 --- a/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json +++ b/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json @@ -2014,4 +2014,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..bf9168f05 --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_vrf_12.json @@ -0,0 +1,1637 @@ +{ + "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 + }, + "empty_network_data": { + "MESSAGE": "OK", + "METHOD": "GET", + "RETURN_CODE": 200, + "DATA": [] + }, + "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_pools_top_down_l3_dot1q": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE":"OK", + "DATA": [] + }, + "empty_vrf_lite_data": { + "DATA": [], + "METHOD": "GET", + "MESSAGE": "OK", + "REQUEST_PATH": "https://10.1.1.1:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/test_fabric/vrfs/switches", + "RETURN_CODE": 200 + }, + "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 c437bb866..e92a2a7c4 100644 --- a/tests/unit/modules/dcnm/test_dcnm_vrf.py +++ b/tests/unit/modules/dcnm/test_dcnm_vrf.py @@ -613,16 +613,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) @@ -630,13 +630,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" ) @@ -650,7 +650,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" ) @@ -665,7 +665,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( @@ -679,7 +679,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) @@ -701,7 +701,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" ) @@ -731,7 +731,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" ) @@ -746,13 +746,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( @@ -764,7 +764,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( @@ -779,7 +779,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( @@ -797,7 +797,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) @@ -807,7 +807,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" ) @@ -825,7 +825,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" ) @@ -840,7 +840,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( @@ -870,7 +870,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" ) @@ -897,7 +897,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" ) @@ -912,14 +912,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) @@ -928,7 +928,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) @@ -936,7 +936,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( @@ -959,7 +959,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" ) @@ -984,7 +984,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( @@ -997,7 +997,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( @@ -1022,7 +1022,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( @@ -1047,14 +1047,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( @@ -1067,7 +1067,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" ) @@ -1097,7 +1097,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" ) @@ -1112,7 +1112,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( @@ -1150,7 +1150,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" ) @@ -1176,7 +1176,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" ) @@ -1191,14 +1191,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( @@ -1211,7 +1211,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) @@ -1231,7 +1231,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( @@ -1257,7 +1257,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"]) @@ -1276,14 +1276,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) @@ -1311,7 +1311,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( @@ -1357,7 +1357,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")) @@ -1396,7 +1396,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( @@ -1411,13 +1411,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( @@ -1433,7 +1433,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..c99b94c4b --- /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 # pylint: disable=invalid-name + +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..5ea2e987d --- /dev/null +++ b/tests/unit/modules/dcnm/test_dcnm_vrf_v2_12.py @@ -0,0 +1,1278 @@ +# 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") + empty_network_data = test_data.get("empty_network_data") + empty_vrf_lite_data = test_data.get("empty_vrf_lite_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")) + self.mock_pools_top_down_l3_dot1q = copy.deepcopy(self.test_data.get("mock_pools_top_down_l3_dot1q")) + + 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, # VRF Lite fetch for initial processing + self.empty_network_data, # Network attachment check returns empty + 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.empty_vrf_lite_data, # Empty VRF Lite response with REQUEST_PATH for new attach + 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.empty_network_data, # Network attachment check returns empty + 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, # Resource cleanup - VLAN pool + self.mock_pools_top_down_l3_dot1q, # Resource cleanup - DOT1Q pool + 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.empty_network_data, # Network attachment check returns empty + 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, # Resource cleanup - VLAN pool + self.mock_pools_top_down_l3_dot1q, # Resource cleanup - DOT1Q pool + ] + + 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.empty_network_data, # Network attachment check returns empty + 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.empty_network_data, # Network attachment check returns empty + 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.empty_network_data, # Network attachment check returns empty + self.attach_success_resp, + self.deploy_success_resp, + obj1, + obj2, + self.delete_success_resp, + self.mock_pools_top_down_vrf_vlan, # Resource cleanup - VLAN pool + self.mock_pools_top_down_l3_dot1q, # Resource cleanup - DOT1Q pool + ] + + 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"], "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_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) + + # For VRF Lite override with deletions, responses are structured differently: + # response[0] is the network attachment check (empty DATA) + # response[1] is the attach success for the new VRF + # Note: The detach/delete responses for the old VRF and deploy response are not included + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK2(leaf2)"], "SUCCESS") + + 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)