From 44366b24ce625b36c503e231d2743a13ef8a37fb Mon Sep 17 00:00:00 2001 From: pipiwowa <189855476+ppiwowa-csco@users.noreply.github.com> Date: Fri, 18 Jul 2025 08:26:47 +0200 Subject: [PATCH 1/6] Example --- .../policies/tasks/example-forwarding-qos.yml | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 roles/policies/tasks/example-forwarding-qos.yml diff --git a/roles/policies/tasks/example-forwarding-qos.yml b/roles/policies/tasks/example-forwarding-qos.yml new file mode 100644 index 0000000..027db8d --- /dev/null +++ b/roles/policies/tasks/example-forwarding-qos.yml @@ -0,0 +1,133 @@ +--- +# Copyright 2024 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +# https://www.cisco.com/c/en/us/td/docs/routers/sdwan/configuration/qos/vEdge-20-x/qos-book/forwarding-qos.html#Cisco_Concept.dita_aa3e0d07-462e-463f-8f45-681f38f61ab0 + +- name: Save list of QoS schedulers + set_fact: + qos_schedulers: + - name: VOICE + queue: 0 + bandwidth: 10 + buffer: 10 + dscp: 0 + l2cos: 0 + - name: CRITICAL_DATA + queue: 1 + bandwidth: 30 + buffer: 30 + dscp: 2 + l2cos: 18 + - name: BULK + queue: 2 + bandwidth: 10 + buffer: 10 + dscp: 10 + l2cos: 1 + - name: DEFAULT + queue: 3 + bandwidth: 20 + buffer: 20 + dscp: 0 + l2cos: 0 + - name: INTERACTIVE_VIDEO + queue: 4 + bandwidth: 20 + buffer: 20 + dscp: 4 + l2cos: 10 + - name: CONTROL_SIGNALING + queue: 5 + bandwidth: 10 + buffer: 10 + dscp: 18 + l2cos: 2 + drops: "tail-drop" + +- name: Create ClassMap lists + cisco.catalystwan.policy: + name: "{{ item.name }}" + list: + type: "class_map" + entries: + - queue: "{{ item.queue }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: created_class_map_lists + loop: "{{ qos_schedulers }}" + +- name: Prepare QoS schedulers for policy definition + set_fact: + policy_qos_schedulers: "{{ policy_qos_schedulers | default([]) + [qos_scheduler] }}" + loop: "{{ created_class_map_lists.results }}" + vars: + qos_scheduler: + queue: "{{ item.item.queue }}" + classMapRef: "{{ item.id | default(omit) }}" + bandwidthPercent: "{{ item.item.bandwidth | default(omit) }}" + bufferPercent: "{{ item.item.buffer | default(omit) }}" + drops: "{{ item.item.drops | default(omit) }}" + scheduling: "{{ item.item.scheduling | default(omit) }}" + +- name: Create QoS map policy definition + cisco.catalystwan.policy: + name: "My-QosMap-Policy" + definition: + type: qos_map + definition: + qosSchedulers: "{{ policy_qos_schedulers }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: created_qos_map + +- name: Prepare rewrite rules for policy definition + set_fact: + policy_rewrite_rules: "{{ policy_rewrite_rules | default([]) + rewrite_rules }}" + loop: "{{ created_class_map_lists.results }}" + when: item.item.queue > 0 + vars: + rewrite_rules: + - class: "{{ item.id | default(omit) }}" + plp: low + dscp: "{{ item.item.dscp | default(omit) }}" + l2cos: "{{ item.item.l2cos | default(omit) }}" + - class: "{{ item.id | default(omit) }}" + plp: high + dscp: "{{ item.item.dscp | default(omit) }}" + l2cos: "{{ item.item.l2cos | default(omit) }}" + +- name: Create Rewrite policy definition + cisco.catalystwan.policy: + name: "My-Rewrite-Policy" + definition: + type: rewrite + definition: + rules: "{{ policy_rewrite_rules }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: created_rewrite_rules + +- name: Create localized policy + cisco.catalystwan.policy: + name: "My-Localized-Policy" + localized: + definition: + settings: + cloudQos: true + cloud_qos_service_side: true + assembly: + - type: qosMap + definitionId: "{{ created_qos_map.id }}" + - type: rewriteRule + definitionId: "{{ created_rewrite_rules.id }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" From e38370add2cdb1f3327bd8d85d8f78dfbcbcbdb6 Mon Sep 17 00:00:00 2001 From: pipiwowa <189855476+ppiwowa-csco@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:13:30 +0200 Subject: [PATCH 2/6] Support for security policy --- .../module_utils/policy_templates/security.py | 15 ++++ plugins/modules/policy.py | 85 +++++++++---------- 2 files changed, 54 insertions(+), 46 deletions(-) create mode 100644 plugins/module_utils/policy_templates/security.py diff --git a/plugins/module_utils/policy_templates/security.py b/plugins/module_utils/policy_templates/security.py new file mode 100644 index 0000000..3fbb6ce --- /dev/null +++ b/plugins/module_utils/policy_templates/security.py @@ -0,0 +1,15 @@ +policy_security_definition = { + "security": { + "default": None, + "required": False, + "type": "dict", + "options": { + "type": { + "type": "str", + "choices": ["feature", "cli"], + "default": "feature", + }, + "definition": {"type": "dict"}, + }, + } +} diff --git a/plugins/modules/policy.py b/plugins/modules/policy.py index f561adf..78c6fc4 100644 --- a/plugins/modules/policy.py +++ b/plugins/modules/policy.py @@ -114,12 +114,15 @@ CentralizedPolicyInfo, LocalizedPolicy, LocalizedPolicyInfo, + SecurityPolicy, + AnySecurityPolicyInfo, ) from catalystwan.models.policy.centralized import CentralizedPolicyEditPayload from catalystwan.session import ManagerHTTPError from catalystwan.typed_list import DataSequence from ..module_utils.policy_templates.centralized import policy_centralized_definition +from ..module_utils.policy_templates.security import policy_security_definition from ..module_utils.policy_templates.definition import policy_definition_definition, policy_definition_type_mapping from ..module_utils.policy_templates.list import policy_list_definition, policy_list_type_mapping from ..module_utils.policy_templates.localized import policy_localized_definition @@ -157,6 +160,7 @@ def run_module(): **policy_definition_definition, **policy_centralized_definition, **policy_localized_definition, + **policy_security_definition, ) result = ModuleResult() @@ -169,6 +173,7 @@ def run_module(): "localized", "list", "definition", + "security", ), ], mutually_exclusive=[ @@ -177,6 +182,7 @@ def run_module(): "localized", "list", "definition", + "security", ), ], ) @@ -184,16 +190,34 @@ def run_module(): object_name: str = module.params.get("name") object_description: str = module.params.get("description") - if module.params.get("centralized"): - object_pretty_name = "Centralized Policy" - object_type = CentralizedPolicy - object_endpoint = module.session.api.policy.centralized - all_centralized_policy: DataSequence[CentralizedPolicyInfo] = module.get_response_safely(object_endpoint.get) - filtered_definitions: Optional[DataSequence[CentralizedPolicyInfo]] = all_centralized_policy.filter( + if module.params.get("centralized") or module.params.get("localized") or module.params.get("security"): + if module.params.get("centralized"): + object_pretty_name = "Centralized Policy" + object_type = CentralizedPolicy + object_edit_type = CentralizedPolicyEditPayload + object_endpoint = module.session.api.policy.centralized + policy_definition = module.params.get("centralized") + elif module.params.get("localized"): + object_pretty_name = "Localized Policy" + object_type = LocalizedPolicy + object_edit_type = object_type + object_endpoint = module.session.api.policy.localized + policy_definition = module.params.get("localized") + elif module.params.get("security"): + object_pretty_name = "Security Policy" + object_type = SecurityPolicy + object_edit_type = object_type + object_endpoint = module.session.api.policy.security + policy_definition = module.params.get("security") + + all_policies: DataSequence[CentralizedPolicyInfo | LocalizedPolicyInfo | AnySecurityPolicyInfo] = module.get_response_safely(object_endpoint.get) + if module.params.get("security"): + all_policies = all_policies.security + filtered_definitions: Optional[DataSequence[CentralizedPolicyInfo | LocalizedPolicyInfo | AnySecurityPolicyInfo]] = all_policies.filter( policy_name=object_name ) if filtered_definitions: - existing_object: DataSequence[CentralizedPolicy] = [ + existing_object: DataSequence[CentralizedPolicy | LocalizedPolicy | SecurityPolicy] = [ module.get_response_safely(object_endpoint.get, id=filtered_definitions[0].policy_id) ] existing_object_id = filtered_definitions[0].policy_id @@ -204,52 +228,20 @@ def run_module(): object_to_create = object_type( policy_name=object_name, policy_description=object_description, - policy_type=module.params.get("centralized").get("type"), - policy_definition=module.params.get("centralized").get("definition"), + policy_type=policy_definition.get("type"), + policy_definition=policy_definition.get("definition"), is_policy_activated=module.params.get("state") == "active", ) elif module.params.get("state") in ("active", "present"): - object_to_create = CentralizedPolicyEditPayload( + object_to_create = object_edit_type( policy_name=object_name, policy_description=object_description, - policy_type=module.params.get("centralized").get("type"), - policy_definition=module.params.get("centralized").get("definition"), + policy_type=policy_definition.get("type"), + policy_definition=policy_definition.get("definition"), is_policy_activated=module.params.get("state") == "active", policy_id=existing_object_id, ) - elif module.params.get("localized"): - object_pretty_name = "Localized Policy" - object_type = LocalizedPolicy - object_endpoint = module.session.api.policy.localized - all_localized_policy: DataSequence[LocalizedPolicyInfo] = module.get_response_safely(object_endpoint.get) - filtered_definitions: Optional[DataSequence[LocalizedPolicyInfo]] = all_localized_policy.filter( - policy_name=object_name - ) - if filtered_definitions: - existing_object: DataSequence[LocalizedPolicy] = [ - module.get_response_safely(object_endpoint.get, id=filtered_definitions[0].policy_id) - ] - existing_object_id = filtered_definitions[0].policy_id - else: - existing_object = [] - - if module.params.get("state") in ("active", "present") and not existing_object: - object_to_create = object_type( - policy_name=object_name, - policy_description=object_description, - policy_type=module.params.get("localized").get("type"), - policy_definition=module.params.get("localized").get("definition"), - ) - elif module.params.get("state") in ("active", "present"): - object_to_create = LocalizedPolicy( - policy_name=object_name, - policy_description=object_description, - policy_type=module.params.get("localized").get("type"), - policy_definition=module.params.get("localized").get("definition"), - policy_id=existing_object_id, - ) - elif module.params.get("definition"): object_pretty_name = "Policy Definition" object_type = policy_definition_type_mapping[module.params.get("definition").get("type")] @@ -259,6 +251,7 @@ def run_module(): name=object_name, description=object_description, definition=module.params.get("definition").get("definition"), + sequences=module.params.get("definition").get("sequences"), ) all_policy_definitions: DataSequence[AnyPolicyDefinition] = module.get_response_safely( object_endpoint.get, type=object_type @@ -315,7 +308,7 @@ def run_module(): ) device_action.wait_for_completed() object_endpoint.edit(policy=object_to_create) - elif module.params.get("localized"): + elif module.params.get("localized") or module.params.get("security"): object_endpoint.edit(policy=object_to_create) elif module.params.get("definition"): object_endpoint.edit( @@ -340,7 +333,7 @@ def run_module(): # object doesn't exist in Manager and needs to be created else: try: - if module.params.get("centralized") or module.params.get("localized"): + if module.params.get("centralized") or module.params.get("localized") or module.params.get("security"): created_uuid: UUID = module.get_response_safely(object_endpoint.create, policy=object_to_create) elif module.params.get("definition"): created_uuid: UUID = module.get_response_safely( From 0167f5217f016831bf9bf48a7d292776479c28b0 Mon Sep 17 00:00:00 2001 From: pipiwowa <189855476+ppiwowa-csco@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:13:48 +0200 Subject: [PATCH 3/6] Add missing policy definition type --- plugins/module_utils/policy_templates/definition.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/module_utils/policy_templates/definition.py b/plugins/module_utils/policy_templates/definition.py index ad167dc..3608249 100644 --- a/plugins/module_utils/policy_templates/definition.py +++ b/plugins/module_utils/policy_templates/definition.py @@ -5,6 +5,7 @@ AclPolicy, AdvancedInspectionProfilePolicy, AdvancedMalwareProtectionPolicy, + AppRoutePolicy, CflowdPolicy, ControlPolicy, DeviceAccessIPv6Policy, @@ -31,6 +32,7 @@ "access_control_policy_ipv6": AclIPv6Policy, "aip": AdvancedInspectionProfilePolicy, "amp": AdvancedMalwareProtectionPolicy, + "app_route": AppRoutePolicy, "cflowd": CflowdPolicy, "control": ControlPolicy, "device_access": DeviceAccessPolicy, @@ -64,6 +66,7 @@ "default": "feature", }, "definition": {"type": "dict"}, + "sequences": {"type": "list"}, }, } } From baa360b76f91e0c6ad11ea51af6a126d655d4a0c Mon Sep 17 00:00:00 2001 From: pipiwowa <189855476+ppiwowa-csco@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:20:10 +0200 Subject: [PATCH 4/6] Example scenarions --- plugins/modules/policy.py | 12 +-- roles/policies/defaults/main.yml | 19 +++++ roles/policies/meta/main.yml | 15 ++++ roles/policies/tasks/acl_policy.yml | 51 ++++++++++++ roles/policies/tasks/app_route.yml | 73 +++++++++++++++++ .../policies/tasks/example-forwarding-qos.yml | 2 +- roles/policies/tasks/geolocation_block.yml | 72 +++++++++++++++++ roles/policies/tasks/hub_and_spoke.yml | 80 +++++++++++++++++++ roles/policies/tasks/main.yml | 75 +++++++++++++++++ roles/policies/tasks/mesh.yml | 70 ++++++++++++++++ 10 files changed, 463 insertions(+), 6 deletions(-) create mode 100644 roles/policies/defaults/main.yml create mode 100644 roles/policies/meta/main.yml create mode 100644 roles/policies/tasks/acl_policy.yml create mode 100644 roles/policies/tasks/app_route.yml create mode 100644 roles/policies/tasks/geolocation_block.yml create mode 100644 roles/policies/tasks/hub_and_spoke.yml create mode 100644 roles/policies/tasks/main.yml create mode 100644 roles/policies/tasks/mesh.yml diff --git a/plugins/modules/policy.py b/plugins/modules/policy.py index 78c6fc4..7001e60 100644 --- a/plugins/modules/policy.py +++ b/plugins/modules/policy.py @@ -210,12 +210,14 @@ def run_module(): object_endpoint = module.session.api.policy.security policy_definition = module.params.get("security") - all_policies: DataSequence[CentralizedPolicyInfo | LocalizedPolicyInfo | AnySecurityPolicyInfo] = module.get_response_safely(object_endpoint.get) + all_policies: DataSequence[CentralizedPolicyInfo | LocalizedPolicyInfo | AnySecurityPolicyInfo] = ( + module.get_response_safely(object_endpoint.get) + ) if module.params.get("security"): all_policies = all_policies.security - filtered_definitions: Optional[DataSequence[CentralizedPolicyInfo | LocalizedPolicyInfo | AnySecurityPolicyInfo]] = all_policies.filter( - policy_name=object_name - ) + filtered_definitions: Optional[ + DataSequence[CentralizedPolicyInfo | LocalizedPolicyInfo | AnySecurityPolicyInfo] + ] = all_policies.filter(policy_name=object_name) if filtered_definitions: existing_object: DataSequence[CentralizedPolicy | LocalizedPolicy | SecurityPolicy] = [ module.get_response_safely(object_endpoint.get, id=filtered_definitions[0].policy_id) @@ -309,7 +311,7 @@ def run_module(): device_action.wait_for_completed() object_endpoint.edit(policy=object_to_create) elif module.params.get("localized") or module.params.get("security"): - object_endpoint.edit(policy=object_to_create) + object_endpoint.edit(id=existing_object_id, policy=object_to_create) elif module.params.get("definition"): object_endpoint.edit( id=existing_object_id, diff --git a/roles/policies/defaults/main.yml b/roles/policies/defaults/main.yml new file mode 100644 index 0000000..a2f48a0 --- /dev/null +++ b/roles/policies/defaults/main.yml @@ -0,0 +1,19 @@ +# Copyright 2025 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- + +policies: {} +default_policy: + name: "Ansible Managed centralized policy" + hub_and_spoke: [] + mesh: [] + acl_policy: [] + app_route: [] + geolocation_block: [] + +combined_policies: "{{ default_policy | combine(policies, recursive=True) }}" + +created_centralized_policies: [] +created_localized_policies: [] +created_security_policies: [] diff --git a/roles/policies/meta/main.yml b/roles/policies/meta/main.yml new file mode 100644 index 0000000..aa3c3b6 --- /dev/null +++ b/roles/policies/meta/main.yml @@ -0,0 +1,15 @@ +--- + +galaxy_info: + author: Piotr Piwowarski + description: Allow user to configure pre defined policies in Cisco SD-WAN + license: GPL-3.0-or-later + min_ansible_version: "2.16.6" + + galaxy_tags: + - cisco + - sdwan + - catalystwan + - networking + +dependencies: [] diff --git a/roles/policies/tasks/acl_policy.yml b/roles/policies/tasks/acl_policy.yml new file mode 100644 index 0000000..03c643d --- /dev/null +++ b/roles/policies/tasks/acl_policy.yml @@ -0,0 +1,51 @@ +# Copyright 2025 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- +- name: "Create acl policy {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}" + definition: + type: "access_control_list" + sequences: "{{ _sequences | from_yaml }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: result_policy + vars: + _sequences: | + - sequenceId: 1 + {% if "next_hop" in policy_item['action'] %} + actions: + - type: set + parameter: + - field: nextHop + value: "{{ policy_item['action']['next_hop'] }}" + {% endif %} + match: + entries: + {% if "source_port" in policy_item['match'] %} + - field: sourcePort + value: "{{ policy_item['match']['source_port'] }}" + {% endif %} + {% if "destination_port" in policy_item['match'] %} + - field: destinationPort + value: "{{ policy_item['match']['destination_port'] }}" + {% endif %} + {% if "source_ip" in policy_item['match'] %} + - field: sourceIp + value: "{{ policy_item['match']['source_ip'] }}" + {% endif %} + {% if "destination_ip" in policy_item['match'] %} + - field: destinationIp + value: "{{ policy_item['match']['destination_ip'] }}" + {% endif %} + +- name: Save policy id + ansible.builtin.set_fact: + created_localized_policies: "{{ created_localized_policies + [_created_policy] }}" + vars: + _created_policy: + type: acl + definitionId: "{{ result_policy['id'] }}" diff --git a/roles/policies/tasks/app_route.yml b/roles/policies/tasks/app_route.yml new file mode 100644 index 0000000..819f93f --- /dev/null +++ b/roles/policies/tasks/app_route.yml @@ -0,0 +1,73 @@ +# Copyright 2025 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- +- name: "Create SLA class list for application route policy {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}_sla" + list: + type: "sla" + entries: + - loss: "{{ policy_item['action']['sla_class']['loss'] }}" + latency: "{{ policy_item['action']['sla_class']['latency'] }}" + jitter: "{{ policy_item['action']['sla_class']['jitter'] }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + when: "'sla_class' in policy_item['action']" + register: result_sla_class + +- name: "Create application route policy {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}" + definition: + type: "app_route" + sequences: "{{ _sequences | from_yaml }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: result_policy + vars: + _sequences: | + - sequenceId: 1 + actions: + {% if "counter" in policy_item['action'] %} + - type: count + parameter: "{{ policy_item['action']['counter'] }}" + {% endif %} + {% if "log" in policy_item['action'] %} + - type: log + parameter: "{{ policy_item['action']['log'] }}" + {% endif %} + {% if "log" in policy_item['action'] %} + - type: slaClass + parameter: "{{ policy_item['action']['sla_class'] }}" + {% endif %} + match: + entries: + {% if "source_port" in policy_item['match'] %} + - field: sourcePort + value: "{{ policy_item['match']['source_port'] }}" + {% endif %} + {% if "destination_port" in policy_item['match'] %} + - field: destinationPort + value: "{{ policy_item['match']['destination_port'] }}" + {% endif %} + {% if "source_ip" in policy_item['match'] %} + - field: sourceIp + value: "{{ policy_item['match']['source_ip'] }}" + {% endif %} + {% if "destination_ip" in policy_item['match'] %} + - field: destinationIp + value: "{{ policy_item['match']['destination_ip'] }}" + {% endif %} + +- name: Save policy id + ansible.builtin.set_fact: + created_centralized_policies: "{{ created_centralized_policies + [_created_policy] }}" + vars: + _created_policy: + type: appRoute + definitionId: "{{ result_policy['id'] }}" diff --git a/roles/policies/tasks/example-forwarding-qos.yml b/roles/policies/tasks/example-forwarding-qos.yml index 027db8d..08b57e1 100644 --- a/roles/policies/tasks/example-forwarding-qos.yml +++ b/roles/policies/tasks/example-forwarding-qos.yml @@ -1,5 +1,5 @@ --- -# Copyright 2024 Cisco Systems, Inc. and its affiliates +# Copyright 2025 Cisco Systems, Inc. and its affiliates # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) # https://www.cisco.com/c/en/us/td/docs/routers/sdwan/configuration/qos/vEdge-20-x/qos-book/forwarding-qos.html#Cisco_Concept.dita_aa3e0d07-462e-463f-8f45-681f38f61ab0 diff --git a/roles/policies/tasks/geolocation_block.yml b/roles/policies/tasks/geolocation_block.yml new file mode 100644 index 0000000..ead6322 --- /dev/null +++ b/roles/policies/tasks/geolocation_block.yml @@ -0,0 +1,72 @@ +# Copyright 2025 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- +- name: "Create source zone for geolocation block policy {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}_src" + list: + type: "zone" + entries: + - vpn: "{{ policy_item['source_vpn'] }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: result_source_zone + +- name: "Create destination zone for geolocation block policy {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}_dst" + list: + type: "zone" + entries: + - vpn: "{{ policy_item['destination_vpn'] }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: result_destination_zone + + +- name: "Create security policy {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}" + definition: + type: "zone_based_firewall" + definition: + entries: + - sourceZone: "{{ result_source_zone['id'] }}" + destinationZone: "{{ result_destination_zone['id'] }}" + sequences: "{{ _sequences | from_yaml }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: result_policy + vars: + _sequences: | + - sequenceId: 1 + sequenceName: sequence 1 + match: + entries: + {% for geolocation in policy_item['geolocations'] %} + - field: sourceGeoLocation + value: "{{ geolocation }}" + {% endfor %} + - sequenceId: 2 + sequenceName: sequence 2 + match: + entries: + {% for geolocation in policy_item['geolocations'] %} + - field: destinationGeoLocation + value: "{{ geolocation }}" + {% endfor %} + +- name: Save policy id + ansible.builtin.set_fact: + created_security_policies: "{{ created_security_policies + [_created_policy] }}" + vars: + _created_policy: + type: zoneBasedFW + definitionId: "{{ result_policy['id'] }}" diff --git a/roles/policies/tasks/hub_and_spoke.yml b/roles/policies/tasks/hub_and_spoke.yml new file mode 100644 index 0000000..a72d1c9 --- /dev/null +++ b/roles/policies/tasks/hub_and_spoke.yml @@ -0,0 +1,80 @@ +# Copyright 2025 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- +- name: "Create vpn list for hub and spoke policy {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}_vpn" + list: + type: "vpn" + entries: "{{ _entries | from_yaml }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: result_vpn_list + vars: + _entries: | + {% for id in policy_item['vpns'] %} + - vpn: {{ id }} + {% endfor %} + +- name: "Create hub list for hub and spoke policy {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}_hub" + list: + type: "site" + entries: "{{ _entries | from_yaml }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: result_hub_list + vars: + _entries: | + {% for id in policy_item['hubs'] %} + - site_id: "{{ id }}" + {% endfor %} + +- name: "Create spoke list for hub and spoke policy {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}_spoke" + list: + type: "site" + entries: "{{ _entries | from_yaml }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: result_spoke_list + vars: + _entries: | + {% for id in policy_item['spokes'] %} + - site_id: "{{ id }}" + {% endfor %} + +- name: "Create hub and spoke policy {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}" + definition: + type: "hub_and_spoke" + definition: + vpnList: "{{ result_vpn_list['id'] }}" + subDefinitions: + - spokes: + - siteList: "{{ result_spoke_list['id'] }}" + hubs: + - siteList: "{{ result_spoke_list['id'] }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: result_policy + +- name: Save policy id + ansible.builtin.set_fact: + created_centralized_policies: "{{ created_centralized_policies + [_created_policy] }}" + vars: + _created_policy: + type: hubAndSpoke + definitionId: "{{ result_policy['id'] }}" diff --git a/roles/policies/tasks/main.yml b/roles/policies/tasks/main.yml new file mode 100644 index 0000000..185693d --- /dev/null +++ b/roles/policies/tasks/main.yml @@ -0,0 +1,75 @@ +# Copyright 2025 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- + +- name: Create hub and spoke policy + ansible.builtin.include_tasks: hub_and_spoke.yml + loop: "{{ combined_policies['hub_and_spoke'] }}" + loop_control: + loop_var: policy_item + label: "{{ policy_item['name'] }}" + +- name: Create mesh policy + ansible.builtin.include_tasks: mesh.yml + loop: "{{ combined_policies['mesh'] }}" + loop_control: + loop_var: policy_item + label: "{{ policy_item['name'] }}" + +- name: Create application route policy + ansible.builtin.include_tasks: app_route.yml + loop: "{{ combined_policies['app_route'] }}" + loop_control: + loop_var: policy_item + label: "{{ policy_item['name'] }}" + +- name: Create centralized policy + cisco.catalystwan.policy: + name: "ansible-managed-policy" + centralized: + definition: + assembly: "{{ created_centralized_policies }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + when: created_centralized_policies + +- name: Create acl policy + ansible.builtin.include_tasks: acl_policy.yml + loop: "{{ combined_policies['acl_policy'] }}" + loop_control: + loop_var: policy_item + label: "{{ policy_item['name'] }}" + +- name: Create localized policy + cisco.catalystwan.policy: + name: "ansible-managed-policy" + localized: + definition: + assembly: "{{ created_localized_policies }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + when: created_localized_policies + +- name: Create geolocation block policy + ansible.builtin.include_tasks: geolocation_block.yml + loop: "{{ combined_policies['geolocation_block'] }}" + loop_control: + loop_var: policy_item + label: "{{ policy_item['name'] }}" + +- name: Create security policy + cisco.catalystwan.policy: + name: "ansible-managed-policy" + security: + definition: + assembly: "{{ created_security_policies }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + when: created_security_policies diff --git a/roles/policies/tasks/mesh.yml b/roles/policies/tasks/mesh.yml new file mode 100644 index 0000000..5e04b35 --- /dev/null +++ b/roles/policies/tasks/mesh.yml @@ -0,0 +1,70 @@ +# Copyright 2025 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- +- name: "Create vpn list for mesh topology {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}_vpn" + list: + type: "vpn" + entries: "{{ _entries | from_yaml }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: result_vpn_list + vars: + _entries: | + {% for id in policy_item['vpns'] %} + - vpn: {{ id }} + {% endfor %} + +- name: "Create site list for mesh topology {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ region_item['name'] }}" + list: + type: "site" + entries: "{{ _entries | from_yaml }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + loop: "{{ policy_item['regions'] }}" + loop_control: + loop_var: region_item + label: "{{ region_item['name'] }}" + register: result_region_list + vars: + _entries: | + {% for id in region_item['sites'] %} + - site_id: "{{ id }}" + {% endfor %} + +- name: "Create mesh policy {{ policy_item['name'] }}" + cisco.catalystwan.policy: + name: "{{ policy_item['name'] }}" + definition: + type: "mesh" + definition: + vpnList: "{{ result_vpn_list['id'] }}" + regions: "{{ _regions | from_yaml }}" + manager_credentials: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + register: result_policy + vars: + _regions: | + {% for region in result_region_list['results'] %} + - name: "{{ region['region_item']['name'] }}" + siteLists: + - {{ region['id'] }} + {% endfor %} + +- name: Save policy id + ansible.builtin.set_fact: + created_centralized_policies: "{{ created_centralized_policies + [_created_policy] }}" + vars: + _created_policy: + type: mesh + definitionId: "{{ result_policy['id'] }}" From cf3014cdbee47562aa697e2ba34039abc963ab5b Mon Sep 17 00:00:00 2001 From: pipiwowa <189855476+ppiwowa-csco@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:20:22 +0200 Subject: [PATCH 5/6] Readme --- roles/policies/README.md | 73 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 roles/policies/README.md diff --git a/roles/policies/README.md b/roles/policies/README.md new file mode 100644 index 0000000..483f004 --- /dev/null +++ b/roles/policies/README.md @@ -0,0 +1,73 @@ +# Ansible Role: policies + +This Ansible role provides user with a few, ready to use, legacy policies scenarios. It handles creating legacy policies along will all required policies definitions and lists. + +## Role Description + +The `policies` role performs the following tasks: + +1. Create legacy policies based on user provided configuration. Supported scenarios: + - hub and spoke topology + - mesh topology + - application route policy + - acl policy + - geolocation blockade + +## Requirements + +- `cisco.catalystwan` collection installed. +- Access details for the Cisco Manager instance must be provided. + +## Dependencies + +There are no external role dependencies. Only `cisco.catalystwan` collection is required. + +## Role Variables + +Variables expected by this role: + +- `vmanage_instances`: A list of vManage instances containing management IP, admin username, and admin password. +- `policies`: A dictionary containing configuration of policies + +Example of `vmanage_instances`: + +```yaml +vmanage_instances: + - hostname: 'vmanage01' + system_ip: '192.0.2.10' + mgmt_public_ip: '198.51.100.10' + admin_username: 'admin' + admin_password: 'password' +``` + +Example of `policies`: +```yaml +policies: + mesh: + - name: my_mesh_policy + vpns: + - 100 + - 101 + regions: + - name: mesh_region1 + sites: + - 100 + - name: mesh_region2 + sites: + - 101 + app_route: + - name: my_app_counter + match: + source_ip: 10.0.0.0/24 + destination_port: 64534 + action: + counter: my_counter +``` + +## License + +"GPL-3.0-only" + +## Author Information + +This role was created by Piotr Piwowarski From c675150d8323c4a62ff3499690ec8a4e86730f89 Mon Sep 17 00:00:00 2001 From: pipiwowa <189855476+ppiwowa-csco@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:31:48 +0200 Subject: [PATCH 6/6] Fixes --- .github/workflows/linters.yml | 2 +- plugins/modules/cli_templates_info.py | 2 -- plugins/modules/policy.py | 10 +++++----- roles/policies/defaults/main.yml | 2 +- roles/policies/tasks/example-forwarding-qos.yml | 6 +++--- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index f14a4f3..b346fc0 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -43,7 +43,7 @@ jobs: uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 - name: Run ansible-lint - uses: ansible/ansible-lint@95382d398ea1744bf6bfa47b030f14c38b3f6957 # v24.7.0 + uses: ansible/ansible-lint@v25.7.0 - name: Install detect-secrets run: pip install detect-secrets==1.4.0 diff --git a/plugins/modules/cli_templates_info.py b/plugins/modules/cli_templates_info.py index fce7dfe..54a65bd 100644 --- a/plugins/modules/cli_templates_info.py +++ b/plugins/modules/cli_templates_info.py @@ -126,8 +126,6 @@ type: list """ -from typing import Literal - from catalystwan.api.template_api import CLITemplate from catalystwan.api.templates.device_template.device_template import DeviceTemplate from catalystwan.models.templates import DeviceTemplateInformation diff --git a/plugins/modules/policy.py b/plugins/modules/policy.py index 7001e60..5a13e12 100644 --- a/plugins/modules/policy.py +++ b/plugins/modules/policy.py @@ -110,22 +110,22 @@ from catalystwan.models.policy import ( AnyPolicyDefinition, AnyPolicyList, + AnySecurityPolicyInfo, CentralizedPolicy, CentralizedPolicyInfo, LocalizedPolicy, LocalizedPolicyInfo, SecurityPolicy, - AnySecurityPolicyInfo, ) from catalystwan.models.policy.centralized import CentralizedPolicyEditPayload from catalystwan.session import ManagerHTTPError from catalystwan.typed_list import DataSequence from ..module_utils.policy_templates.centralized import policy_centralized_definition -from ..module_utils.policy_templates.security import policy_security_definition from ..module_utils.policy_templates.definition import policy_definition_definition, policy_definition_type_mapping from ..module_utils.policy_templates.list import policy_list_definition, policy_list_type_mapping from ..module_utils.policy_templates.localized import policy_localized_definition +from ..module_utils.policy_templates.security import policy_security_definition from ..module_utils.result import ModuleResult from ..module_utils.vmanage_module import AnsibleCatalystwanModule @@ -210,9 +210,9 @@ def run_module(): object_endpoint = module.session.api.policy.security policy_definition = module.params.get("security") - all_policies: DataSequence[CentralizedPolicyInfo | LocalizedPolicyInfo | AnySecurityPolicyInfo] = ( - module.get_response_safely(object_endpoint.get) - ) + all_policies: DataSequence[ + CentralizedPolicyInfo | LocalizedPolicyInfo | AnySecurityPolicyInfo + ] = module.get_response_safely(object_endpoint.get) if module.params.get("security"): all_policies = all_policies.security filtered_definitions: Optional[ diff --git a/roles/policies/defaults/main.yml b/roles/policies/defaults/main.yml index a2f48a0..403c7d5 100644 --- a/roles/policies/defaults/main.yml +++ b/roles/policies/defaults/main.yml @@ -5,7 +5,7 @@ policies: {} default_policy: - name: "Ansible Managed centralized policy" + name: "Ansible Managed policy" hub_and_spoke: [] mesh: [] acl_policy: [] diff --git a/roles/policies/tasks/example-forwarding-qos.yml b/roles/policies/tasks/example-forwarding-qos.yml index 08b57e1..48201b9 100644 --- a/roles/policies/tasks/example-forwarding-qos.yml +++ b/roles/policies/tasks/example-forwarding-qos.yml @@ -5,7 +5,7 @@ # https://www.cisco.com/c/en/us/td/docs/routers/sdwan/configuration/qos/vEdge-20-x/qos-book/forwarding-qos.html#Cisco_Concept.dita_aa3e0d07-462e-463f-8f45-681f38f61ab0 - name: Save list of QoS schedulers - set_fact: + ansible.builtin.set_fact: qos_schedulers: - name: VOICE queue: 0 @@ -60,7 +60,7 @@ loop: "{{ qos_schedulers }}" - name: Prepare QoS schedulers for policy definition - set_fact: + ansible.builtin.set_fact: policy_qos_schedulers: "{{ policy_qos_schedulers | default([]) + [qos_scheduler] }}" loop: "{{ created_class_map_lists.results }}" vars: @@ -86,7 +86,7 @@ register: created_qos_map - name: Prepare rewrite rules for policy definition - set_fact: + ansible.builtin.set_fact: policy_rewrite_rules: "{{ policy_rewrite_rules | default([]) + rewrite_rules }}" loop: "{{ created_class_map_lists.results }}" when: item.item.queue > 0