├── plugins ├── modules │ ├── __init__.py │ ├── dnac_activate_credential.py │ ├── dnac_del_archived_config.py │ ├── dnac_ntp.py │ ├── dnac_dhcp.py │ ├── dnac_device_role.py │ ├── dnac_syslog.py │ ├── dnac_snmp.py │ ├── dnac_netflow.py │ ├── dnac_archive_config.py │ ├── dnac_timezone.py │ ├── dnac_ippool.py │ ├── dnac_dns.py │ ├── dnac_banner.py │ ├── dnac_snmpv2_credential.py │ ├── dnac_wireless_provision.py │ ├── dnac_wireless_ssid.py │ ├── dnac_cli_credential.py │ ├── dnac_device_assign_site.py │ ├── dnac_wireless_profile.py │ ├── dnac_site.py │ └── dnac_discovery.py ├── module_utils │ └── network │ │ └── dnac │ │ ├── __init__.py │ │ └── dnac.py ├── inventory │ ├── dna_center.yml │ └── dna_center.py ├── README.md └── lookup │ └── geo.py ├── .flake8 ├── .gitignore ├── NOTICE ├── playbooks └── test_dnac_collection.yaml ├── ansible.cfg ├── galaxy.yml ├── README.md └── docs └── README.md /plugins/modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/module_utils/network/dnac/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = tests/* 4 | max-complexity = 10 5 | ignore = 6 | E402 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | *.tar.gz 3 | *local* 4 | dnac_api_support.xlsx 5 | .vscode/settings.json 6 | scratch 7 | *.pyc 8 | *.backup 9 | hosts.yaml 10 | dnac_api_support.xlsx 11 | .vscode/settings.json 12 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ansible-dnac-modules 2 | Copyright (c) 2019 World Wide Technology 3 | 4 | This project includes software developed at World Wide Technology. 5 | 6 | This project includes: 7 | geopy, timezonefinder under the MIT license 8 | requests under the Apache license 9 | -------------------------------------------------------------------------------- /playbooks/test_dnac_collection.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test DNAC Collection 3 | hosts: localhost 4 | gather_facts: false 5 | 6 | collections: 7 | - wwt.ansible_dnac 8 | 9 | tasks: 10 | - name: Create a Site 11 | dnac_site: 12 | host: "{{ dnac.hostname }}" 13 | username: "{{ dnac.username }}" 14 | password: "{{ dnac.password }}" 15 | name: nj-Site 16 | site_type: area 17 | validate_certs: false 18 | 19 | 20 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | library = . 3 | host_key_checking = False 4 | retry_files_enabled = False 5 | inventory = inventory 6 | # Use the YAML callback plugin. 7 | stdout_callback = yaml 8 | # Use the stdout_callback when running ad-hoc commands. 9 | bin_ansible_callbacks = True 10 | #vault_password_file = ~/.vault_password 11 | filter_plugins = playbooks/filter_plugins 12 | action_warnings = False 13 | display_skipped_hosts = false 14 | [inventory] 15 | enable_plugins = wwt.ansible_dnac.dna_center 16 | -------------------------------------------------------------------------------- /plugins/inventory/dna_center.yml: -------------------------------------------------------------------------------- 1 | plugin: dna_center 2 | host: 'dnac-prod.campus.wwtatc.local' 3 | validate_certs: 'no' 4 | use_dnac_mgmt_int: false 5 | username: 'andiorij' 6 | password: !vault | 7 | $ANSIBLE_VAULT;1.1;AES256 8 | 63343336393966306461313539326662376165353935633133326364336435666538653362346561 9 | 3364646333353633653862643262323362373631353632340a616436633331633437333837353966 10 | 32383964336536343662616364343233653361646634343166626131303930643431343939333832 11 | 6564373836383139310a643931613861643133626263656233333762613332353432313636633938 12 | 3239 -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Collections Plugins Directory 2 | 3 | This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that 4 | is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that 5 | would contain module utils and modules respectively. 6 | 7 | Here is an example directory of the majority of plugins currently supported by Ansible: 8 | 9 | ``` 10 | └── plugins 11 | ├── action 12 | ├── become 13 | ├── cache 14 | ├── callback 15 | ├── cliconf 16 | ├── connection 17 | ├── filter 18 | ├── httpapi 19 | ├── inventory 20 | ├── lookup 21 | ├── module_utils 22 | ├── modules 23 | ├── netconf 24 | ├── shell 25 | ├── strategy 26 | ├── terminal 27 | ├── test 28 | └── vars 29 | ``` 30 | 31 | A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible/2.9/plugins/plugins.html). -------------------------------------------------------------------------------- /plugins/lookup/geo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2019 World Wide Technology, Inc. 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl- 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | __metaclass__ = type 7 | 8 | DOCUMENTATION = """ 9 | lookup: geo 10 | author: Jeff Andiorio (@jandiorio) 11 | version_added: "2.9" 12 | short_description: resolve an address to latitude and longitude 13 | description: 14 | - Allows you to obtain the latitude and longitude for a given address 15 | options: 16 | address: 17 | description: address to resolve 18 | required: True 19 | """ 20 | 21 | EXAMPLES = """ 22 | - debug: msg="{{ lookup('geo','mullica hill, nj') }} is the lat long for Mullica Hill" 23 | """ 24 | 25 | RETURN = """ 26 | _list: 27 | description: 28 | - latitude and longitude of the provided address 29 | type: list 30 | """ 31 | 32 | from ansible.plugins.lookup import LookupBase 33 | from ansible.errors import AnsibleError, AnsibleParserError 34 | 35 | try: 36 | from geopy.geocoders import Nominatim 37 | except ImportError as e: 38 | raise AnsibleError('geopy python module not installed: {}'.format(e)) 39 | 40 | 41 | class LookupModule(LookupBase): 42 | 43 | def run(self, address, variables, **kwargs): 44 | 45 | ret = [] 46 | 47 | geolocator = Nominatim(user_agent='dnac_ansible', timeout=30) 48 | try: 49 | location = geolocator.geocode(address) 50 | except Exception as e: 51 | print(e) 52 | raise AnsibleParserError("Could not resolve address to lat/long: {}".format(e)) 53 | 54 | if location is None: 55 | raise AnsibleError("Lookup was unable to resolve the address provided using geopy: {}".format(address)) 56 | else: 57 | ret = [{'latitude': location.latitude, 'longitude': location.longitude}] 58 | return ret 59 | -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | ### REQUIRED 2 | 3 | # The namespace of the collection. This can be a company/brand/organization or product namespace under which all 4 | # content lives. May only contain alphanumeric characters and underscores. Additionally namespaces cannot start with 5 | # underscores or numbers and cannot contain consecutive underscores 6 | namespace: wwt 7 | 8 | # The name of the collection. Has the same character restrictions as 'namespace' 9 | name: ansible_dnac 10 | 11 | # The version of the collection. Must be compatible with semantic versioning 12 | version: 1.1.5 13 | 14 | # The path to the Markdown (.md) readme file. This path is relative to the root of the collection 15 | readme: README.md 16 | 17 | # A list of the collection's content authors. Can be just the name or in the format 'Full Name (url) 18 | # @nicks:irc/im.site#channel' 19 | authors: 20 | - Jeff Andiorio 21 | 22 | 23 | ### OPTIONAL but strongly recommended 24 | 25 | # A short summary description of the collection 26 | description: Collection of Ansible modules and plugins for DNA Center 27 | 28 | # Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only 29 | # accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file' 30 | # license: 31 | # - GPL-2.0-or-later 32 | 33 | # The path to the license file for the collection. This path is relative to the root of the collection. This key is 34 | # mutually exclusive with 'license' 35 | license_file: 'LICENSE' 36 | 37 | # A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character 38 | # requirements as 'namespace' and 'name' 39 | tags: [dnac, wwt, ansible_dnac] 40 | 41 | # Collections that this collection requires to be installed for it to be usable. The key of the dict is the 42 | # collection label 'namespace.name'. The value is a version range 43 | # L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version 44 | # range specifiers can be set and are separated by ',' 45 | dependencies: {} 46 | 47 | # The URL of the originating SCM repository 48 | repository: https://github.com/jandiorio/ansible-dnac-modules/tree/v1.1.5 49 | 50 | # The URL to any online docs 51 | documentation: "" 52 | 53 | # The URL to the homepage of the collection/project 54 | homepage: "http://www.wwt.com/" 55 | 56 | # The URL to the collection issue tracker 57 | issues: "https://github.com/jandiorio/ansible-dnac-modules/issues" 58 | 59 | # Ignored Files 60 | build_ignore: 61 | - "*.local" 62 | -------------------------------------------------------------------------------- /plugins/modules/dnac_activate_credential.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 5 | 6 | ANSIBLE_METADATA = { 7 | 'metadata_version': '1.0', 8 | 'status': ['development'], 9 | 'supported_by': 'jandiorio' 10 | } 11 | 12 | """ 13 | Copyright (c) 2018 World Wide Technology, Inc. 14 | All rights reserved. 15 | Revision history: 16 | 22 Mar 2018 | .1 - prototype release 17 | """ 18 | 19 | # ----------------------------------------------- 20 | # define static required variables 21 | # ----------------------------------------------- 22 | # ----------------------------------------------- 23 | # main 24 | # ----------------------------------------------- 25 | 26 | 27 | def main(): 28 | 29 | module_args = dnac_argument_spec 30 | module_args.update( 31 | credential_name=dict(type='str', required=True), 32 | credential_type=dict(type='str', default='SNMPV2_WRITE_COMMUNITY', 33 | choices=['SNMPV2_READ_COMMUNITY', 34 | 'SNMPV2_WRITE_COMMUNITY', 'CLI']), 35 | group_name=dict(type='str', default='Global', required=False) 36 | ) 37 | 38 | # result = dict( 39 | # changed=False, 40 | # original_message='', 41 | # message='') 42 | 43 | module = AnsibleModule( 44 | argument_spec=module_args, 45 | supports_check_mode=False 46 | ) 47 | 48 | # instantiate dnac object 49 | dnac = DnaCenter(module) 50 | 51 | # lookup credential id 52 | dnac.api_path = 'api/v1/global-credential?credentialSubType=' + \ 53 | module.params['credential_type'] 54 | 55 | settings = dnac.get_obj() 56 | 57 | if module.params['credential_type'] == 'CLI': 58 | _cred_id = [user['id'] for user in settings['response'] 59 | if user['username'] == module.params['credential_name']] 60 | elif module.params['credential_type'] == 'SNMPV2_READ_COMMUNITY' or \ 61 | module.params['credential_type'] == 'SNMPV2_WRITE_COMMUNITY': 62 | _cred_id = [cred['id'] for cred in settings['response'] if 63 | cred['description'] == module.params['credential_name']] 64 | 65 | # set key string 66 | if module.params['credential_type'] == 'CLI': 67 | _credential_key = 'credential.cli' 68 | _credential_val_type = 'credential_cli' 69 | elif module.params['credential_type'] == 'SNMPV2_READ_COMMUNITY': 70 | _credential_key = 'credential.snmp_v2_read' 71 | _credential_val_type = 'credential_snmp_v2_read' 72 | elif module.params['credential_type'] == 'SNMPV2_WRITE_COMMUNITY': 73 | _credential_key = 'credential.snmp_v2_write' 74 | _credential_val_type = 'credential_snmp_v2_write' 75 | 76 | # lookup group id 77 | dnac.api_path = 'api/v1/group' 78 | groups = dnac.get_obj()['response'] 79 | _group_id = [g['id'] for g in groups 80 | if g['name'] == module.params['group_name']] 81 | 82 | if len(_group_id) == 1: 83 | _group_id = _group_id[0] 84 | 85 | if len(_cred_id) == 1: 86 | _cred_id = _cred_id[0] 87 | 88 | # build the payload dictionary 89 | payload = [ 90 | { 91 | "instanceUuid": "", 92 | "inheritedGroupName": "", 93 | "version": "1", 94 | "namespace": "global", 95 | "groupUuid": _group_id, 96 | "key": _credential_key, 97 | "instanceType": "reference", 98 | "type": "reference.setting", 99 | "value": 100 | [ 101 | { 102 | "type": _credential_val_type, 103 | "objReferences": [ 104 | _cred_id 105 | ], "url": "" 106 | } 107 | ] 108 | } 109 | ] 110 | dnac.api_path = 'api/v1/commonsetting/global/' + _group_id 111 | dnac.create_obj(payload) 112 | 113 | 114 | if __name__ == "__main__": 115 | main() 116 | -------------------------------------------------------------------------------- /plugins/modules/dnac_del_archived_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2018 World Wide Technology, Inc. 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import absolute_import, division, print_function 6 | __metaclass__ = type 7 | 8 | ANSIBLE_METADATA = { 9 | 'metadata_version': '1.1', 10 | 'status': ['preview'], 11 | 'supported_by': 'community' 12 | } 13 | 14 | DOCUMENTATION = r''' 15 | 16 | module: dnac_del_archive_config 17 | short_description: Create an archive of the configuration. 18 | description: 19 | - Create an archive of the device configuration. 20 | 21 | version_added: "2.5" 22 | author: Jeff Andiorio (@jandiorio) 23 | 24 | options: 25 | host: 26 | description: 27 | - Host is the target Cisco DNA Center controller to execute against. 28 | required: true 29 | 30 | port: 31 | description: 32 | - Port is the TCP port for the HTTP connection. 33 | required: true 34 | default: 443 35 | choices: 36 | - 80 37 | - 443 38 | 39 | username: 40 | description: 41 | - Provide the username for the connection to the Cisco DNA Center Controller. 42 | required: true 43 | 44 | password: 45 | description: 46 | - Provide the password for connection to the Cisco DNA Center Controller. 47 | required: true 48 | 49 | use_proxy: 50 | description: 51 | - Enter a boolean value for whether to use proxy or not. 52 | required: false 53 | default: true 54 | choices: 55 | - true 56 | - false 57 | 58 | use_ssl: 59 | description: 60 | - Enter the boolean value for whether to use SSL or not. 61 | required: false 62 | default: true 63 | choices: 64 | - true 65 | - false 66 | 67 | timeout: 68 | description: 69 | - The timeout provides a value for how long to wait for the executed command complete. 70 | required: false 71 | default: 30 72 | 73 | validate_certs: 74 | description: 75 | - Specify if verifying the certificate is desired. 76 | required: false 77 | default: true 78 | choices: 79 | - true 80 | - false 81 | 82 | state: 83 | description: 84 | - State provides the action to be executed using the terms present, absent, etc. 85 | required: true 86 | default: present 87 | choices: 88 | - present 89 | - absent 90 | 91 | device_name: 92 | description: 93 | - name of the device in the inventory database that you would like to update 94 | required: false 95 | 96 | device_mgmt_ip: 97 | description: 98 | - Management IP Address of the device you would like to update 99 | required: false 100 | 101 | running_config: 102 | description: 103 | - Boolean for whether to backup the running configuration 104 | required: false 105 | default: false 106 | 107 | startup_config: 108 | description: 109 | - Boolean for whether to backup the startup configuration 110 | required: false 111 | default: false 112 | 113 | vlans: 114 | description: 115 | - Boolean for whether to backup the vlan database 116 | required: false 117 | default: false 118 | 119 | all: 120 | description: 121 | - Boolean for whether to backup all options (running, startup, vlans) 122 | required: false 123 | default: true 124 | 125 | notes: 126 | - Either device_name or device_mgmt_ip is required, but not both. 127 | 128 | ''' 129 | 130 | EXAMPLES = r''' 131 | 132 | !!! NEED EXAMPLES !!! 133 | 134 | ''' 135 | 136 | RETURN = r''' 137 | # 138 | ''' 139 | 140 | from ansible.module_utils.basic import AnsibleModule 141 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 142 | 143 | 144 | def main(): 145 | 146 | # result = dict( 147 | # changed=False, 148 | # original_message='', 149 | # message='') 150 | 151 | module_args = dnac_argument_spec 152 | module = AnsibleModule( 153 | argument_spec=module_args, 154 | supports_check_mode=False 155 | ) 156 | 157 | # Instantiate the DnaCenter class object 158 | dnac = DnaCenter(module) 159 | 160 | # Get device details based on either the Management IP or the Name Provided 161 | if module.params['device_mgmt_ip'] is not None: 162 | dnac.api_path = 'api/v1/network-device?managementIpAddress=' + module.params['device_mgmt_ip'] 163 | elif module.params['device_name'] is not None: 164 | dnac.api_path = 'api/v1/network-device?hostname=' + module.params['device_name'] 165 | 166 | # extract the device iD 167 | device_results = dnac.get_obj() 168 | device_id = device_results['response'][0]['id'] 169 | 170 | # delete the associated archives 171 | 172 | dnac.api_path = 'api/v1/archive-config/network-device' 173 | dnac.delete_obj(device_id) 174 | 175 | 176 | if __name__ == "__main__": 177 | main() 178 | -------------------------------------------------------------------------------- /plugins/modules/dnac_ntp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2018, World Wide Technology, Inc. 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | __metaclass__ = type 8 | 9 | ANSIBLE_METADATA = { 10 | 'metadata_version': '1.0', 11 | 'status': ['development'], 12 | 'supported_by': 'jandiorio' 13 | } 14 | 15 | DOCUMENTATION = r''' 16 | --- 17 | module: dnac_ntp.py 18 | short_description: Manage ntp servers within Cisco DNA Center 19 | description: Based on 1.1+ version of DNAC API 20 | author: 21 | - Jeff Andiorio (@jandiorio) 22 | version_added: '2.4' 23 | requirements: 24 | - DNA Center 1.1+ 25 | 26 | options: 27 | host: 28 | description: 29 | - Host is the target Cisco DNA Center controller to execute against. 30 | required: true 31 | 32 | port: 33 | description: 34 | - Port is the TCP port for the HTTP connection. 35 | required: false 36 | default: 443 37 | choices: 38 | - 80 39 | - 443 40 | 41 | username: 42 | description: 43 | - Provide the username for the connection to the Cisco DNA Center Controller. 44 | required: true 45 | 46 | password: 47 | description: 48 | - Provide the password for connection to the Cisco DNA Center Controller. 49 | required: true 50 | 51 | use_proxy: 52 | description: 53 | - Enter a boolean value for whether to use proxy or not. 54 | required: false 55 | default: true 56 | choices: 57 | - true 58 | - false 59 | 60 | use_ssl: 61 | description: 62 | - Enter the boolean value for whether to use SSL or not. 63 | required: false 64 | default: true 65 | choices: 66 | - true 67 | - false 68 | 69 | timeout: 70 | description: 71 | - The timeout provides a value for how long to wait for the executed command complete. 72 | required: false 73 | default: 30 74 | 75 | validate_certs: 76 | description: 77 | - Specify if verifying the certificate is desired. 78 | required: false 79 | default: true 80 | choices: 81 | - true 82 | - false 83 | 84 | state: 85 | description: 86 | - State provides the action to be executed using the terms present, absent, etc. 87 | required: false 88 | default: present 89 | choices: 90 | - present 91 | - absent 92 | 93 | ntp_servers: 94 | description: The ip address(es) of the ntp server(s). 95 | required: false 96 | type: list 97 | 98 | group_name: 99 | description: Name of the group where the setting will be applied. 100 | required: false 101 | default: Global 102 | type: string 103 | 104 | ''' 105 | EXAMPLES = r''' 106 | 107 | - name: create NTP server 108 | dnac_ntp: 109 | host: "{{host}}" 110 | port: "{{port}}" 111 | username: "{{username}}" 112 | password: "{{password}}" 113 | state: present 114 | ntp_servers: [192.168.200.5, 192.168.200.6] 115 | ''' 116 | 117 | RETURN = r''' 118 | previous: 119 | description: Configuration from DNA Center prior to any changes. 120 | returned: success 121 | type: list 122 | sample: 123 | - groupUuid: '-1' 124 | inheritedGroupName: '' 125 | inheritedGroupUuid: '' 126 | instanceType: ip 127 | instanceUuid: 77120a67-f579-4028-8529-7c68a0af5ada 128 | key: ntp.server 129 | namespace: global 130 | type: ip.address 131 | value: [] 132 | version: 60 133 | proprosed: 134 | description: Configuration to be sent to DNA Center. 135 | returned: success 136 | type: list 137 | sample: 138 | - groupUuid: '-1' 139 | instanceType: ip 140 | key: ntp.server 141 | namespace: global 142 | type: ip.address 143 | value: 144 | - 192.168.200.5 145 | - 192.168.200.6 146 | ''' 147 | 148 | from ansible.module_utils.basic import AnsibleModule 149 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 150 | 151 | # ----------------------------------------------- 152 | # define static required variales 153 | # ----------------------------------------------- 154 | # ----------------------------------------------- 155 | # main 156 | # ----------------------------------------------- 157 | 158 | 159 | def main(): 160 | module_args = dnac_argument_spec 161 | module_args.update( 162 | ntp_servers=dict(type='list', required=False), 163 | group_name=dict(type='str', default='-1') 164 | ) 165 | 166 | module = AnsibleModule( 167 | argument_spec=module_args, 168 | supports_check_mode=True 169 | ) 170 | # Define Static Variables 171 | group_name = module.params['group_name'] 172 | ntp_servers = module.params['ntp_servers'] 173 | 174 | # Build the payload dictionary 175 | payload = [ 176 | {"instanceType": "ip", 177 | "namespace": "global", 178 | "type": "ip.address", 179 | "key": "ntp.server", 180 | "value": ntp_servers, 181 | "groupUuid": "-1" 182 | } 183 | ] 184 | 185 | # instansiate the dnac class 186 | dnac = DnaCenter(module) 187 | 188 | # obtain the groupUuid and update the payload dictionary 189 | group_id = dnac.get_group_id(group_name) 190 | 191 | # Set API Path 192 | dnac.api_path = 'api/v1/commonsetting/global/' + group_id + '?key=ntp.server' 193 | 194 | # Process Setting Changes 195 | dnac.process_common_settings(payload, group_id) 196 | 197 | 198 | if __name__ == "__main__": 199 | main() 200 | -------------------------------------------------------------------------------- /plugins/modules/dnac_dhcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2018 World Wide Technology, Inc. 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import absolute_import, division, print_function 6 | __metaclass__ = type 7 | 8 | ANSIBLE_METADATA = { 9 | 'metadata_version': '1.1', 10 | 'status': ['preview'], 11 | 'supported_by': 'community' 12 | } 13 | 14 | DOCUMENTATION = r''' 15 | 16 | module: dnac_dhcp 17 | short_description: Add or Delete DHCP Server(s) 18 | description: Add or delete DHCP Server(s) in the Cisco DNA Center Design Workflow. \ 19 | The DHCP Severs can be different values at different levels in the group hierarchy. 20 | 21 | version_added: "2.5" 22 | author: "Jeff Andiorio (@jandiorio)" 23 | 24 | options: 25 | host: 26 | description: 27 | - Host is the target Cisco DNA Center controller to execute against. 28 | required: true 29 | 30 | port: 31 | description: 32 | - Port is the TCP port for the HTTP connection. 33 | required: false 34 | default: 443 35 | choices: 36 | - 80 37 | - 443 38 | 39 | username: 40 | description: 41 | - Provide the username for the connection to the Cisco DNA Center Controller. 42 | required: true 43 | 44 | password: 45 | description: 46 | - Provide the password for connection to the Cisco DNA Center Controller. 47 | required: true 48 | 49 | use_proxy: 50 | description: 51 | - Enter a boolean value for whether to use proxy or not. 52 | required: false 53 | default: true 54 | choices: 55 | - true 56 | - false 57 | 58 | use_ssl: 59 | description: 60 | - Enter the boolean value for whether to use SSL or not. 61 | required: false 62 | default: true 63 | choices: 64 | - true 65 | - false 66 | 67 | timeout: 68 | description: 69 | - The timeout provides a value for how long to wait for the executed command complete. 70 | required: false 71 | default: 30 72 | 73 | validate_certs: 74 | description: 75 | - Specify if verifying the certificate is desired. 76 | required: false 77 | default: true 78 | choices: 79 | - true 80 | - false 81 | 82 | state: 83 | description: 84 | - State provides the action to be executed using the terms present, absent, etc. 85 | required: false 86 | default: present 87 | choices: 88 | - present 89 | - absent 90 | 91 | dhcp_servers: 92 | description: 93 | - IP address of the DHCP Server to manipulate. 94 | required: true 95 | type: list 96 | group_name: 97 | description: 98 | - group_name is the name of the group in the hierarchy where you would like to apply the dhcp_server. 99 | required: false 100 | default: Global 101 | 102 | ''' 103 | 104 | EXAMPLES = r''' 105 | 106 | - name: create dhcp server 107 | dnac_dhcp: 108 | host: 10.253.177.230 109 | port: 443 110 | username: "{{username}}" 111 | password: "{{password}}" 112 | state: present 113 | dhcp_server: 192.168.200.1 192.168.200.2 114 | 115 | - name: delete dhcp server 116 | dnac_dhcp: 117 | host: 10.253.177.230 118 | port: 443 119 | username: "{{username}}" 120 | password: "{{password}}" 121 | state: absent 122 | dhcp_server: 192.168.200.1 192.168.200.2 123 | 124 | ''' 125 | RETURN = r''' 126 | previous: 127 | description: Configuration from DNA Center prior to any changes. 128 | returned: success 129 | type: list 130 | sample: 131 | - groupUuid: 91404471-9c8d-492c-9c4c-230c7fd54bf9 132 | inheritedGroupName: Global 133 | inheritedGroupUuid: '-1' 134 | instanceType: ip 135 | instanceUuid: d069b8ac-39c6-4379-98d9-ac198675c410 136 | key: dhcp.server 137 | namespace: global 138 | type: ip.address 139 | value: 140 | - 192.168.200.1 141 | - 192.168.200.2 142 | version: 48 143 | proprosed: 144 | description: Configuration to be sent to DNA Center. 145 | returned: success 146 | type: list 147 | sample: 148 | - groupUuid: 91404471-9c8d-492c-9c4c-230c7fd54bf9 149 | instanceType: ip 150 | key: dhcp.server 151 | namespace: global 152 | type: ip.address 153 | value: 154 | - 192.168.200.1 155 | - 192.168.200.2 156 | ''' 157 | 158 | from ansible.module_utils.basic import AnsibleModule 159 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 160 | 161 | # ----------------------------------------------- 162 | # main 163 | # ----------------------------------------------- 164 | 165 | 166 | def main(): 167 | 168 | module_args = dnac_argument_spec 169 | module_args.update( 170 | dhcp_servers=dict(type='list', required=False), 171 | group_name=dict(type='str', default='-1') 172 | ) 173 | 174 | module = AnsibleModule( 175 | argument_spec=module_args, 176 | supports_check_mode=True 177 | ) 178 | 179 | # Define Local Variables 180 | 181 | dhcp_servers = module.params['dhcp_servers'] 182 | group_name = module.params['group_name'] 183 | 184 | # Build the payload dictionary 185 | payload = [ 186 | {"instanceType": "ip", 187 | "namespace": "global", 188 | "type": "ip.address", 189 | "key": "dhcp.server", 190 | "value": dhcp_servers, 191 | "groupUuid": "-1" 192 | } 193 | ] 194 | 195 | # instansiate the dnac class 196 | dnac = DnaCenter(module) 197 | 198 | # obtain the groupUuid and update the payload dictionary 199 | group_id = dnac.get_group_id(group_name) 200 | 201 | # Set the api_path 202 | dnac.api_path = 'api/v1/commonsetting/global/' + group_id + '?key=dhcp.server' 203 | 204 | dnac.process_common_settings(payload, group_id) 205 | 206 | 207 | if __name__ == "__main__": 208 | main() 209 | -------------------------------------------------------------------------------- /plugins/modules/dnac_device_role.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2018 World Wide Technology, Inc. 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import absolute_import, division, print_function 6 | __metaclass__ = type 7 | 8 | ANSIBLE_METADATA = { 9 | 'metadata_version': '1.1', 10 | 'status': ['preview'], 11 | 'supported_by': 'community' 12 | } 13 | 14 | DOCUMENTATION = r''' 15 | 16 | module: dnac_device_role 17 | short_description: Set the role of the devices in your network. 18 | description: 19 | - Set the device roles in the DNA Center Inventory Database. 20 | 21 | version_added: "2.5" 22 | author: "Jeff Andiorio (@jandiorio)" 23 | 24 | options: 25 | host: 26 | description: 27 | - Host is the target Cisco DNA Center controller to execute against. 28 | required: true 29 | 30 | port: 31 | description: 32 | - Port is the TCP port for the HTTP connection. 33 | required: false 34 | default: 443 35 | choices: 36 | - 80 37 | - 443 38 | 39 | username: 40 | description: 41 | - Provide the username for the connection to the Cisco DNA Center Controller. 42 | required: true 43 | 44 | password: 45 | description: 46 | - Provide the password for connection to the Cisco DNA Center Controller. 47 | required: true 48 | 49 | use_proxy: 50 | description: 51 | - Enter a boolean value for whether to use proxy or not. 52 | required: false 53 | default: true 54 | choices: 55 | - true 56 | - false 57 | 58 | use_ssl: 59 | description: 60 | - Enter the boolean value for whether to use SSL or not. 61 | required: false 62 | default: true 63 | choices: 64 | - true 65 | - false 66 | 67 | timeout: 68 | description: 69 | - The timeout provides a value for how long to wait for the executed command complete. 70 | required: false 71 | default: 30 72 | 73 | validate_certs: 74 | description: 75 | - Specify if verifying the certificate is desired. 76 | required: false 77 | default: true 78 | choices: 79 | - true 80 | - false 81 | 82 | state: 83 | description: 84 | - State provides the action to be executed using the terms present, absent, etc. 85 | required: false 86 | default: present 87 | choices: 88 | - present 89 | - absent 90 | 91 | device_name: 92 | description: 93 | - name of the device in the inventory database that you would like to update 94 | required: false 95 | 96 | device_mgmt_ip: 97 | description: 98 | - Management IP Address of the device you would like to update 99 | required: false 100 | 101 | device_role: 102 | description: 103 | - Role of the device 104 | required: true 105 | choices: 106 | - ACCESS 107 | - DISTRIBUTION 108 | - CORE 109 | - BORDER ROUTER 110 | 111 | notes: 112 | - Either device_name or device_mgmt_ip is required, but not both. 113 | 114 | ''' 115 | 116 | EXAMPLES = r''' 117 | 118 | - name: update device role 119 | dnac_device_role: 120 | host: "{{host}}" 121 | port: 443 122 | username: "{{username}}" 123 | password: "{{password}}" 124 | device_mgmt_ip: 192.168.200.1 125 | device_role: "DISTRIBUTION" 126 | 127 | - name: update device role 128 | dnac_device_role: 129 | host: "{{host}}" 130 | port: 443 131 | username: "{{username}}" 132 | password: "{{password}}" 133 | device_name: my_switch_name 134 | device_role: "DISTRIBUTION" 135 | 136 | - name: update device role 137 | dnac_device_role: 138 | host: "{{host}}" 139 | port: 443 140 | username: "{{username}}" 141 | password: "{{password}}" 142 | device_mgmt_ip: "{{item.key}}" 143 | device_role: "{{item.value.device_role}}" 144 | with_dict: "{{roles}}" 145 | 146 | ''' 147 | 148 | RETURN = r''' 149 | # 150 | ''' 151 | 152 | from ansible.module_utils.basic import AnsibleModule 153 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 154 | 155 | 156 | def main(): 157 | 158 | payload = '' 159 | module_args = dnac_argument_spec 160 | module_args.update( 161 | device_name=dict(type='str', required=False), 162 | device_mgmt_ip=dict(type='str', required=False), 163 | device_role=dict( 164 | type='str', required=True, choices=[ 165 | 'ACCESS', 'DISTRIBUTION', 'CORE', 'BORDER ROUTER', 'UNKOWN' 166 | ] 167 | ) 168 | ) 169 | 170 | result = dict( 171 | changed=False, 172 | original_message='', 173 | message='') 174 | 175 | module = AnsibleModule( 176 | argument_spec=module_args, 177 | supports_check_mode=False 178 | ) 179 | 180 | # Instantiate the DnaCenter class object 181 | dnac = DnaCenter(module) 182 | 183 | # Get device details based on either the Management IP or the Name Provided 184 | if module.params['device_mgmt_ip'] is not None: 185 | dnac.api_path = 'api/v1/network-device?managementIpAddress=' + module.params['device_mgmt_ip'] 186 | elif module.params['device_name'] is not None: 187 | dnac.api_path = 'api/v1/network-device?hostname=' + module.params['device_name'] 188 | 189 | device_results = dnac.get_obj() 190 | current_device_role = device_results['response'][0]['role'] 191 | device_id = device_results['response'][0]['id'] 192 | 193 | if current_device_role != module.params['device_role']: 194 | dnac.api_path = 'api/v1/network-device/brief' 195 | payload = {'id': device_id, 'role': module.params['device_role'], 'roleSource': 'MANUAL'} 196 | dnac.update_obj(payload) 197 | 198 | else: 199 | result['changed'] = False 200 | module.exit_json(msg='Device Already in desired Role') 201 | 202 | 203 | if __name__ == "__main__": 204 | main() 205 | -------------------------------------------------------------------------------- /plugins/modules/dnac_syslog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2018, World Wide Technology, Inc. 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | __metaclass__ = type 8 | 9 | ANSIBLE_METADATA = { 10 | 'metadata_version': '1.0', 11 | 'status': ['development'], 12 | 'supported_by': 'jandiorio' 13 | } 14 | 15 | DOCUMENTATION = r''' 16 | --- 17 | module: dnac_syslog.py 18 | short_description: Manage Syslog server(s) within Cisco DNA Center 19 | description: Manage Syslog Server(s) settings in Cisco DNA Center. Based on 1.1+ version of DNAC API 20 | author: 21 | - Jeff Andiorio (@jandiorio) 22 | version_added: '2.4' 23 | requirements: 24 | - DNA Center 1.1+ 25 | 26 | options: 27 | host: 28 | description: 29 | - Host is the target Cisco DNA Center controller to execute against. 30 | required: true 31 | port: 32 | description: 33 | - Port is the TCP port for the HTTP connection. 34 | required: false 35 | default: 443 36 | choices: 37 | - 80 38 | - 443 39 | username: 40 | description: 41 | - Provide the username for the connection to the Cisco DNA Center Controller. 42 | required: true 43 | 44 | password: 45 | description: 46 | - Provide the password for connection to the Cisco DNA Center Controller. 47 | required: true 48 | 49 | use_proxy: 50 | description: 51 | - Enter a boolean value for whether to use proxy or not. 52 | required: false 53 | default: true 54 | choices: 55 | - true 56 | - false 57 | 58 | use_ssl: 59 | description: 60 | - Enter the boolean value for whether to use SSL or not. 61 | required: false 62 | default: true 63 | choices: 64 | - true 65 | - false 66 | 67 | timeout: 68 | description: 69 | - The timeout provides a value for how long to wait for the executed command complete. 70 | required: false 71 | default: 30 72 | 73 | validate_certs: 74 | description: 75 | - Specify if verifying the certificate is desired. 76 | required: false 77 | default: true 78 | choices: 79 | - true 80 | - false 81 | 82 | state: 83 | description: 84 | - State provides the action to be executed using the terms present, absent, etc. 85 | required: false 86 | default: present 87 | choices: 88 | - present 89 | - absent 90 | 91 | syslog_servers: 92 | description: The ip address(es) of the syslog server(s). 93 | required: true 94 | type: list 95 | 96 | group_name: 97 | description: Name of the group where the setting will be applied. 98 | required: false 99 | default: Global 100 | type: string 101 | 102 | ''' 103 | 104 | EXAMPLES = r''' 105 | 106 | --- 107 | 108 | - name: create syslog server 109 | dnac_syslog: 110 | host: "{{host}}" 111 | port: "{{port}}" 112 | username: "{{username}}" 113 | password: "{{password}}" 114 | state: present 115 | syslog_servers: [192.168.200.1, 192.168.200.2] 116 | ''' 117 | 118 | RETURN = r''' 119 | previous: 120 | description: Configuration from DNA Center prior to any changes. 121 | returned: success 122 | type: list 123 | sample: 124 | - groupUuid: '-1' 125 | inheritedGroupName: '' 126 | inheritedGroupUuid: '' 127 | instanceType: ip 128 | instanceUuid: 1ca3a703-4720-472d-9314-ba1fbe48e139 129 | key: syslog.server 130 | namespace: global 131 | type: ip.address 132 | value: 133 | - 192.168.200.1 134 | version: 32 135 | proposed: 136 | description: Configuration to be sent to DNA Center. 137 | returned: success 138 | type: list 139 | sample: 140 | - groupUuid: '-1' 141 | instanceType: ip 142 | key: syslog.server 143 | namespace: global 144 | type: ip.address 145 | value: 146 | - 192.168.200.1 147 | - 192.168.200.2 148 | ''' 149 | 150 | from ansible.module_utils.basic import AnsibleModule 151 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 152 | 153 | # ----------------------------------------------- 154 | # define static required variales 155 | # ----------------------------------------------- 156 | # ----------------------------------------------- 157 | # main 158 | # ----------------------------------------------- 159 | 160 | 161 | def main(): 162 | module_args = dnac_argument_spec 163 | module_args.update( 164 | group_name=dict(type='str', default='-1'), 165 | syslog_servers=dict(type='list', required=False), 166 | enable_dnac=dict(type='bool', required=False, default=True) 167 | ) 168 | 169 | module = AnsibleModule( 170 | argument_spec=module_args, 171 | supports_check_mode=True 172 | ) 173 | 174 | # Define Local Variables 175 | group_name = module.params['group_name'] 176 | syslog_servers = module.params['syslog_servers'] 177 | enable_dnac = module.params['enable_dnac'] 178 | 179 | # Build the payload dictionary 180 | payload = [ 181 | {"instanceType": "syslog", 182 | "namespace": "global", 183 | "type": "syslog.setting", 184 | "key": "syslog.server", 185 | "value": [ 186 | {"ipAddresses": syslog_servers, 187 | "configureDnacIP": enable_dnac 188 | } 189 | ], 190 | "groupUuid": "-1", 191 | } 192 | ] 193 | 194 | # instansiate the dnac class 195 | dnac = DnaCenter(module) 196 | 197 | # obtain the groupUuid and update the payload dictionary 198 | group_id = dnac.get_group_id(group_name) 199 | 200 | # Set API Path 201 | dnac.api_path = 'api/v1/commonsetting/global/' + group_id + '?key=syslog.server' 202 | 203 | dnac.process_common_settings(payload, group_id) 204 | 205 | 206 | if __name__ == "__main__": 207 | main() 208 | -------------------------------------------------------------------------------- /plugins/modules/dnac_snmp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2018, World Wide Technology, Inc. 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | __metaclass__ = type 8 | 9 | ANSIBLE_METADATA = { 10 | 'metadata_version': '1.1', 11 | 'status': ['development'], 12 | 'supported_by': 'jandiorio' 13 | } 14 | 15 | 16 | DOCUMENTATION = r''' 17 | --- 18 | module: dnac_snmp.py 19 | short_description: Manage SNMP server(s) within Cisco DNA Center 20 | description: Manage SNMP Server(s) settings in Cisco DNA Center. Based on 1.1+ version of DNAC API 21 | author: 22 | - Jeff Andiorio (@jandiorio) 23 | version_added: '2.4' 24 | requirements: 25 | - DNA Center 1.1+ 26 | 27 | options: 28 | host: 29 | description: 30 | - Host is the target Cisco DNA Center controller to execute against. 31 | required: true 32 | port: 33 | description: 34 | - Port is the TCP port for the HTTP connection. 35 | required: false 36 | default: 443 37 | choices: 38 | - 80 39 | - 443 40 | 41 | username: 42 | description: 43 | - Provide the username for the connection to the Cisco DNA Center Controller. 44 | required: true 45 | 46 | password: 47 | description: 48 | - Provide the password for connection to the Cisco DNA Center Controller. 49 | required: true 50 | 51 | use_proxy: 52 | description: 53 | - Enter a boolean value for whether to use proxy or not. 54 | required: false 55 | default: true 56 | choices: 57 | - true 58 | - false 59 | 60 | use_ssl: 61 | description: 62 | - Enter the boolean value for whether to use SSL or not. 63 | required: false 64 | default: true 65 | choices: 66 | - true 67 | - false 68 | 69 | timeout: 70 | description: 71 | - The timeout provides a value for how long to wait for the executed command complete. 72 | required: false 73 | default: 30 74 | 75 | validate_certs: 76 | description: 77 | - Specify if verifying the certificate is desired. 78 | required: false 79 | default: true 80 | choices: 81 | - true 82 | - false 83 | 84 | state: 85 | description: 86 | - State provides the action to be executed using the terms present, absent, etc. 87 | required: false 88 | default: present 89 | choices: 90 | - present 91 | - absent 92 | 93 | snmp_servers: 94 | description: 95 | - The ip address(es) of the snmp server(s). 96 | required: false 97 | type: list 98 | group_name: 99 | description: 100 | - Name of the group where the setting will be applied. 101 | required: false 102 | default: Global 103 | type: string 104 | ''' 105 | EXAMPLES = r''' 106 | 107 | 108 | - name: create snmp server 109 | dnac_snmp: 110 | host: "{{host}}" 111 | port: "{{port}}" 112 | username: "{{username}}" 113 | password: "{{password}}" 114 | state: present 115 | snmp_servers: [192.168.200.1, 192.168.200.2] 116 | ''' 117 | 118 | RETURN = r''' 119 | previous: 120 | description: Configuration from DNA Center prior to any changes. 121 | returned: success 122 | type: list 123 | sample: 124 | - groupUuid: '-1' 125 | inheritedGroupName: '' 126 | inheritedGroupUuid: '' 127 | instanceType: ip 128 | instanceUuid: beebe744-8a95-4688-b396-b33ce952e458 129 | key: snmp.trap.receiver 130 | namespace: global 131 | type: ip.address 132 | value: 133 | - 192.168.200.1 134 | - 192.168.200.2 135 | version: 67 136 | proprosed: 137 | description: Configuration to be sent to DNA Center. 138 | returned: success 139 | type: list 140 | sample: 141 | - groupUuid: '-1' 142 | instanceType: ip 143 | key: snmp.trap.receiver 144 | namespace: global 145 | type: ip.address 146 | value: 147 | - 192.168.200.1 148 | - 192.168.200.2 149 | ''' 150 | 151 | from ansible.module_utils.basic import AnsibleModule 152 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 153 | 154 | # ----------------------------------------------- 155 | # define static required variales 156 | # ----------------------------------------------- 157 | # ----------------------------------------------- 158 | # main 159 | # ----------------------------------------------- 160 | 161 | 162 | def main(): 163 | module_args = dnac_argument_spec 164 | module_args.update( 165 | group_name=dict(type='str', default='-1', required=False), 166 | snmp_servers=dict(type='list', required=False), 167 | enable_dnac=dict(type='bool', required=False, default=True) 168 | ) 169 | 170 | module = AnsibleModule( 171 | argument_spec=module_args, 172 | supports_check_mode=True 173 | ) 174 | 175 | # Set Local Variables 176 | snmp_servers = module.params['snmp_servers'] 177 | group_name = module.params['group_name'] 178 | enable_dnac = module.params['enable_dnac'] 179 | 180 | # Build the payload dictionary 181 | payload = [ 182 | {"instanceType": "snmp", 183 | "namespace": "global", 184 | "type": "snmp.setting", 185 | "key": "snmp.trap.receiver", 186 | "value": [{ 187 | "ipAddresses": snmp_servers, 188 | "configureDnacIP": enable_dnac 189 | } 190 | ], 191 | "groupUuid": "-1", 192 | } 193 | ] 194 | 195 | # instansiate the dnac class 196 | dnac = DnaCenter(module) 197 | 198 | # obtain the groupUuid and update the payload dictionary 199 | group_id = dnac.get_group_id(group_name) 200 | 201 | # Set the api path 202 | dnac.api_path = 'api/v1/commonsetting/global/' + group_id + '?key=snmp.trap.receiver' 203 | 204 | # Process Setting Changes 205 | dnac.process_common_settings(payload, group_id) 206 | 207 | 208 | if __name__ == "__main__": 209 | main() 210 | -------------------------------------------------------------------------------- /plugins/modules/dnac_netflow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2018, World Wide Technology, Inc. 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | __metaclass__ = type 8 | 9 | ANSIBLE_METADATA = { 10 | 'metadata_version': '1.0', 11 | 'status': ['development'], 12 | 'supported_by': 'jandiorio' 13 | } 14 | 15 | DOCUMENTATION = r''' 16 | --- 17 | module: dnac_netflow.py 18 | short_description: Manage netflow exporters within Cisco DNA Center 19 | description: Based on 1.1+ version of DNAC API 20 | author: 21 | - Jeff Andiorio (@jandiorio) 22 | version_added: 'x.x' 23 | requirements: 24 | - DNA Center 1.1+ 25 | 26 | options: 27 | host: 28 | description: 29 | - Host is the target Cisco DNA Center controller to execute against. 30 | required: true 31 | 32 | port: 33 | description: 34 | - Port is the TCP port for the HTTP connection. 35 | required: false 36 | default: 443 37 | choices: 38 | - 80 39 | - 443 40 | 41 | username: 42 | description: 43 | - Provide the username for the connection to the Cisco DNA Center Controller. 44 | required: true 45 | 46 | password: 47 | description: 48 | - Provide the password for connection to the Cisco DNA Center Controller. 49 | required: true 50 | 51 | use_proxy: 52 | description: 53 | - Enter a boolean value for whether to use proxy or not. 54 | required: false 55 | default: true 56 | choices: 57 | - true 58 | - false 59 | 60 | use_ssl: 61 | description: 62 | - Enter the boolean value for whether to use SSL or not. 63 | required: false 64 | default: true 65 | choices: 66 | - true 67 | - false 68 | 69 | timeout: 70 | description: 71 | - The timeout provides a value for how long to wait for the executed command complete. 72 | required: false 73 | default: 30 74 | 75 | validate_certs: 76 | description: 77 | - Specify if verifying the certificate is desired. 78 | required: false 79 | default: true 80 | choices: 81 | - true 82 | - false 83 | 84 | state: 85 | description: 86 | - State provides the action to be executed using the terms present, absent, etc. 87 | required: false 88 | default: present 89 | choices: 90 | - present 91 | - absent 92 | 93 | netflow_collector: 94 | description: The ip address of the netflow collector. 95 | required: false 96 | type: string 97 | netflow_port: 98 | description: The port used for the target netflow collector. 99 | type: string 100 | required: false 101 | type: string 102 | group_name: 103 | description: Name of the group where the setting will be applied. 104 | required: false 105 | default: Global 106 | type: string 107 | 108 | ''' 109 | EXAMPLES = r''' 110 | - name: create a netflow 111 | dnac_netflow: 112 | host: "{{host}}" 113 | port: "{{port}}" 114 | username: "{{username}}" 115 | password: "{{password}}" 116 | state: present 117 | netflow_collector: "{{nf_ip}}" 118 | netflow_port: "{{nf_port}}" 119 | ''' 120 | 121 | RETURN = r''' 122 | previous: 123 | description: Configuration from DNA Center prior to any changes. 124 | returned: success 125 | type: list 126 | sample: 127 | - groupUuid: '-1' 128 | inheritedGroupName: '' 129 | inheritedGroupUuid: '' 130 | instanceType: netflow 131 | instanceUuid: e1fae968-18e7-456a-98dd-06db3fe475e8 132 | key: netflow.collector 133 | namespace: global 134 | type: netflow.setting 135 | value: [] 136 | version: 64 137 | proprosed: 138 | description: Configuration to be sent to DNA Center. 139 | returned: success 140 | type: list 141 | sample: 142 | - groupUuid: '-1' 143 | instanceType: netflow 144 | key: netflow.collector 145 | namespace: global 146 | type: netflow.setting 147 | value: 148 | - ipAddress: 192.168.91.150 149 | port: '6007' 150 | ''' 151 | 152 | from ansible.module_utils.basic import AnsibleModule 153 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 154 | 155 | # ----------------------------------------------- 156 | # define static required variales 157 | # ----------------------------------------------- 158 | # ----------------------------------------------- 159 | # main 160 | # ----------------------------------------------- 161 | 162 | 163 | def main(): 164 | module_args = dnac_argument_spec 165 | module_args.update( 166 | netflow_collector=dict(type='str', required=False), 167 | netflow_port=dict(type='str', required=False), 168 | group_name=dict(type='str', default='-1') 169 | ) 170 | 171 | module = AnsibleModule( 172 | argument_spec=module_args, 173 | supports_check_mode=True 174 | ) 175 | 176 | # Define local variables 177 | group_name = module.params['group_name'] 178 | netflow_collector = module.params['netflow_collector'] 179 | netflow_port = module.params['netflow_port'] 180 | 181 | # Build the payload dictionary 182 | payload = [ 183 | {"instanceType": "netflow", 184 | "namespace": "global", 185 | "type": "netflow.setting", 186 | "key": "netflow.collector", 187 | "value": [ 188 | {"ipAddress": netflow_collector, 189 | "port": netflow_port 190 | } 191 | ], 192 | "groupUuid": "-1" 193 | } 194 | ] 195 | 196 | # instansiate the dnac class 197 | dnac = DnaCenter(module) 198 | 199 | # obtain the groupUuid and update the payload dictionary 200 | group_id = dnac.get_group_id(group_name) 201 | 202 | # set the api_path 203 | dnac.api_path = 'api/v1/commonsetting/global/' + group_id + '?key=netflow.collector' 204 | 205 | dnac.process_common_settings(payload, group_id) 206 | 207 | 208 | if __name__ == "__main__": 209 | main() 210 | -------------------------------------------------------------------------------- /plugins/modules/dnac_archive_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2018 World Wide Technology, Inc. 3 | # GNU General Public License v3.0+ (see COPYING 4 | # or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | __metaclass__ = type 8 | 9 | ANSIBLE_METADATA = { 10 | 'metadata_version': '1.1', 11 | 'status': ['preview'], 12 | 'supported_by': 'community' 13 | } 14 | 15 | DOCUMENTATION = r''' 16 | 17 | module: dnac_archive_config 18 | short_description: Create an archive of the configuration. 19 | description: 20 | - Create an archive of the device configuration. 21 | 22 | version_added: "2.5" 23 | author: "Jeff Andiorio (@jandiorio)" 24 | 25 | options: 26 | host: 27 | description: 28 | - Host is the target Cisco DNA Center controller to execute against. 29 | required: true 30 | 31 | port: 32 | description: 33 | - Port is the TCP port for the HTTP connection. 34 | required: true 35 | default: 443 36 | choices: 37 | - 80 38 | - 443 39 | 40 | username: 41 | description: 42 | - Provide the username for the connection to the 43 | Cisco DNA Center Controller. 44 | required: true 45 | 46 | password: 47 | description: 48 | - Provide the password for connection to the Cisco DNA Center Controller. 49 | required: true 50 | 51 | use_proxy: 52 | description: 53 | - Enter a boolean value for whether to use proxy or not. 54 | required: false 55 | default: true 56 | choices: 57 | - true 58 | - false 59 | 60 | use_ssl: 61 | description: 62 | - Enter the boolean value for whether to use SSL or not. 63 | required: false 64 | default: true 65 | choices: 66 | - true 67 | - false 68 | 69 | timeout: 70 | description: 71 | - The timeout provides a value for how long to wait for the 72 | executed command complete. 73 | required: false 74 | default: 30 75 | 76 | validate_certs: 77 | description: 78 | - Specify if verifying the certificate is desired. 79 | required: false 80 | default: true 81 | choices: 82 | - true 83 | - false 84 | 85 | state: 86 | description: 87 | - State provides the action to be executed using the terms present, 88 | absent, etc. 89 | required: true 90 | default: present 91 | choices: 92 | - present 93 | - absent 94 | 95 | device_name: 96 | description: 97 | - name of the device in the inventory database that you would like 98 | to update 99 | required: false 100 | 101 | device_mgmt_ip: 102 | description: 103 | - Management IP Address of the device you would like to update 104 | required: false 105 | 106 | running_config: 107 | description: 108 | - Boolean for whether to backup the running configuration 109 | required: false 110 | default: false 111 | 112 | startup_config: 113 | description: 114 | - Boolean for whether to backup the startup configuration 115 | required: false 116 | default: false 117 | 118 | vlans: 119 | description: 120 | - Boolean for whether to backup the vlan database 121 | required: false 122 | default: false 123 | 124 | all: 125 | description: 126 | - Boolean for whether to backup all options (running, startup, vlans) 127 | required: false 128 | default: true 129 | 130 | notes: 131 | - Either device_name or device_mgmt_ip is required, but not both. 132 | 133 | ''' 134 | 135 | EXAMPLES = r''' 136 | 137 | !! NEED EXAMPLE!! 138 | 139 | ''' 140 | 141 | RETURN = r''' 142 | # 143 | ''' 144 | 145 | from ansible.module_utils.basic import AnsibleModule 146 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 147 | 148 | 149 | def main(): 150 | 151 | payload = '' 152 | module_args = dnac_argument_spec 153 | module_args.update( 154 | device_name=dict(type='str', required=False), 155 | device_mgmt_ip=dict(type='str', required=False), 156 | running_config=dict(type='str', required=False, default=False), 157 | startup_config=dict(type='str', required=False, default=False), 158 | vlans=dict(type='str', required=False, default=False), 159 | all=dict(type='str', required=False, default=True) 160 | ) 161 | 162 | # result = dict( 163 | # changed=False, 164 | # original_message='', 165 | # message='') 166 | 167 | module = AnsibleModule( 168 | argument_spec=module_args, 169 | supports_check_mode=False 170 | ) 171 | 172 | # Instantiate the DnaCenter class object 173 | dnac = DnaCenter(module) 174 | 175 | # Get device details based on either the Management IP or the Name Provided 176 | if module.params['device_mgmt_ip'] is not None: 177 | dnac.api_path = 'api/v1/network-device?managementIpAddress=' \ 178 | + module.params['device_mgmt_ip'] 179 | elif module.params['device_name'] is not None: 180 | dnac.api_path = 'api/v1/network-device?hostname=' \ 181 | + module.params['device_name'] 182 | 183 | device_results = dnac.get_obj() 184 | device_id = device_results['response'][0]['id'] 185 | 186 | payload = { 187 | "deviceIds": [ 188 | device_id 189 | ], 190 | "configFileType": { 191 | "runningconfig": module.params['running_config'], 192 | "startupconfig": module.params['startup_config'], 193 | "vlan": module.params['vlans'], 194 | "all": module.params['all'] 195 | } 196 | } 197 | 198 | dnac.api_path = 'api/v1/archive-config' 199 | dnac.create_obj(payload) 200 | # if not archive_config_results.get('isError'): 201 | # result['changed'] = True 202 | # result['original_message'] = archive_config_results 203 | # module.exit_json(msg='Device Config Archived Successfully.', **result) 204 | # elif archive_config_results.get('isError'): 205 | # result['changed'] = False 206 | # result['original_message'] = archive_config_results 207 | # module.fail_json(msg='Failed to Archive Device Configuration!', 208 | # **result) 209 | 210 | 211 | if __name__ == "__main__": 212 | main() 213 | -------------------------------------------------------------------------------- /plugins/modules/dnac_timezone.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2018, World Wide Technology, Inc. 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | __metaclass__ = type 8 | 9 | ANSIBLE_METADATA = { 10 | 'metadata_version': '1.1', 11 | 'status': ['preview'], 12 | 'supported_by': 'community' 13 | } 14 | 15 | 16 | DOCUMENTATION = r''' 17 | --- 18 | module: dnac_timezone 19 | short_description: Manage Timezone Settings within Cisco DNA Center 20 | description: Manage Timezone Settings in Cisco DNA Center. Based on 1.1+ version of DNAC API 21 | author: 22 | - Jeff Andiorio (@jandiorio) 23 | version_added: '2.5' 24 | requirements: 25 | - DNA Center 1.1+ 26 | 27 | options: 28 | host: 29 | description: 30 | - Host is the target Cisco DNA Center controller to execute against. 31 | required: true 32 | 33 | port: 34 | description: 35 | - Port is the TCP port for the HTTP connection. 36 | required: false 37 | default: 443 38 | choices: 39 | - 80 40 | - 443 41 | 42 | username: 43 | description: 44 | - Provide the username for the connection to the Cisco DNA Center Controller. 45 | required: true 46 | 47 | password: 48 | description: 49 | - Provide the password for connection to the Cisco DNA Center Controller. 50 | required: true 51 | 52 | use_proxy: 53 | description: 54 | - Enter a boolean value for whether to use proxy or not. 55 | required: false 56 | default: true 57 | choices: 58 | - true 59 | - false 60 | 61 | use_ssl: 62 | description: 63 | - Enter the boolean value for whether to use SSL or not. 64 | required: false 65 | default: true 66 | choices: 67 | - true 68 | - false 69 | 70 | timeout: 71 | description: 72 | - The timeout provides a value for how long to wait for the executed command complete. 73 | required: false 74 | default: 30 75 | 76 | validate_certs: 77 | description: 78 | - Specify if verifying the certificate is desired. 79 | required: false 80 | default: true 81 | choices: 82 | - true 83 | - false 84 | 85 | state: 86 | description: 87 | - State provides the action to be executed using the terms present, absent, etc. 88 | required: false 89 | default: present 90 | choices: 91 | - present 92 | - absent 93 | 94 | timezone: 95 | description: 96 | - "The timezone string matching the timezone you are targeting. example: America/Chicago" 97 | required: false 98 | type: string 99 | 100 | group_name: 101 | description: 102 | - Name of the group where the setting will be applied. 103 | required: false 104 | default: Global 105 | type: string 106 | 107 | location: 108 | description: 109 | - address of a location in the timezone. A lookup will be performed to resolve the timezone. 110 | required: false 111 | type: string 112 | 113 | ''' 114 | 115 | EXAMPLES = r''' 116 | 117 | --- 118 | 119 | - name: create timezone 120 | dnac_timezone: 121 | host: "{{host}}" 122 | port: "{{port}}" 123 | username: "{{username}}" 124 | password: "{{password}}" 125 | state: present 126 | location: 56 weldon parkway, maryland heights, mo 127 | 128 | - name: create timezone 129 | dnac_timezone: 130 | host: "{{host}}" 131 | port: "{{port}}" 132 | username: "{{username}}" 133 | password: "{{password}}" 134 | state: present 135 | timezone: "America/Chicago" 136 | 137 | ''' 138 | 139 | RETURN = r''' 140 | previous: 141 | description: Configuration from DNA Center prior to any changes. 142 | returned: success 143 | type: list 144 | sample: 145 | - groupUuid: 91404471-9c8d-492c-9c4c-230c7fd54bf9 146 | inheritedGroupName: '' 147 | inheritedGroupUuid: '' 148 | instanceType: timezone 149 | instanceUuid: cae8ced9-eab7-4dae-b1b9-1bd300a58311 150 | key: timezone.site 151 | namespace: global 152 | type: timezone.setting 153 | value: [] 154 | version: 2 155 | proprosed: 156 | description: Configuration to be sent to DNA Center. 157 | returned: success 158 | type: list 159 | sample: 160 | - groupUuid: 91404471-9c8d-492c-9c4c-230c7fd54bf9 161 | instanceType: timezone 162 | instanceUuid: '' 163 | key: timezone.site 164 | namespace: global 165 | type: timezone.setting 166 | value: 167 | - America/Chicago 168 | ''' 169 | 170 | from ansible.module_utils.basic import AnsibleModule 171 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 172 | 173 | # ----------------------------------------------- 174 | # define static required variales 175 | # ----------------------------------------------- 176 | # ----------------------------------------------- 177 | # main 178 | # ----------------------------------------------- 179 | 180 | 181 | def main(): 182 | module_args = dnac_argument_spec 183 | module_args.update( 184 | timezone=dict(type='str', default='GMT'), 185 | group_name=dict(type='str', default='-1'), 186 | location=dict(type='str') 187 | ) 188 | 189 | module = AnsibleModule( 190 | argument_spec=module_args, 191 | supports_check_mode=True 192 | ) 193 | 194 | # set local variables 195 | group_name = module.params['group_name'] 196 | location = module.params['location'] 197 | timezone = module.params['timezone'] 198 | 199 | # Build the payload dictionary 200 | payload = [ 201 | {"instanceType": "timezone", 202 | "instanceUuid": "", 203 | "namespace": "global", 204 | "type": "timezone.setting", 205 | "key": "timezone.site", 206 | "value": [""], 207 | "groupUuid": "-1" 208 | } 209 | ] 210 | 211 | # instansiate the dnac class 212 | dnac = DnaCenter(module) 213 | 214 | # obtain the groupUuid and update the payload dictionary 215 | group_id = dnac.get_group_id(group_name) 216 | 217 | # update payload with timezone 218 | if location: 219 | timezone = dnac.timezone_lookup(location) 220 | 221 | payload[0].update({'value': [timezone]}) 222 | 223 | # # check if the configuration is already in the desired state 224 | dnac.api_path = 'api/v1/commonsetting/global/' + group_id + '?key=timezone.site' 225 | 226 | # process common settings 227 | dnac.process_common_settings(payload, group_id) 228 | 229 | 230 | if __name__ == "__main__": 231 | main() 232 | -------------------------------------------------------------------------------- /plugins/modules/dnac_ippool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import, division, print_function 5 | __metaclass__ = type 6 | 7 | ANSIBLE_METADATA = {'metadata_version': '1.0', 8 | 'status': ['preview'], 9 | 'supported_by': 'jeff andiorio'} 10 | 11 | DOCUMENTATION = r''' 12 | --- 13 | module: dnac_ippool.py 14 | short_description: Manage ip pools within Cisco DNA Center 15 | description: Based on 1.1+ version of DNAC API 16 | author: 17 | - Jeff Andiorio (@jandiorio) 18 | version_added: '2.4' 19 | requirements: 20 | - DNA Center 1.1+ 21 | 22 | options: 23 | host: 24 | description: 25 | - Host is the target Cisco DNA Center controller to execute against. 26 | required: true 27 | 28 | port: 29 | description: 30 | - Port is the TCP port for the HTTP connection. 31 | required: false 32 | default: 443 33 | choices: 34 | - 80 35 | - 443 36 | 37 | username: 38 | description: 39 | - Provide the username for the connection to the Cisco DNA Center Controller. 40 | required: true 41 | 42 | password: 43 | description: 44 | - Provide the password for connection to the Cisco DNA Center Controller. 45 | required: true 46 | 47 | use_proxy: 48 | description: 49 | - Enter a boolean value for whether to use proxy or not. 50 | required: false 51 | default: true 52 | choices: 53 | - true 54 | - false 55 | 56 | use_ssl: 57 | description: 58 | - Enter the boolean value for whether to use SSL or not. 59 | required: false 60 | default: true 61 | choices: 62 | - true 63 | - false 64 | 65 | timeout: 66 | description: 67 | - The timeout provides a value for how long to wait for the executed command complete. 68 | required: false 69 | default: 30 70 | 71 | validate_certs: 72 | description: 73 | - Specify if verifying the certificate is desired. 74 | required: false 75 | default: true 76 | choices: 77 | - true 78 | - false 79 | 80 | state: 81 | description: 82 | - State provides the action to be executed using the terms present, absent, etc. 83 | required: false 84 | default: present 85 | choices: 86 | - present 87 | - absent 88 | 89 | ip_pool_name: 90 | description: Name of the pool. 91 | required: true 92 | type: string 93 | ip_pool_subnet: 94 | description: Subnet represented by the pool. 95 | required: true 96 | type: string 97 | ip_pool_prefix_len: 98 | description: Prefix length expresses in slash notation (/24) 99 | required: false 100 | default: /8 101 | type: string 102 | ip_pool_gateway: 103 | description: The gateway associated with the subnet specified. 104 | required: true 105 | type: string 106 | ip_pool_dhcp_servers: 107 | description: A list of DHCP Servers (Maximum 2) 108 | type: list 109 | required: false 110 | ip_pool_dns_servers: 111 | description: A list of DNS Servers (Maximum 2) 112 | type: list 113 | required: false 114 | ip_pool_overlapping: 115 | description: Indicate if the pool has overlapping networks. 116 | type: bool 117 | default: false 118 | required: false 119 | 120 | ''' 121 | 122 | EXAMPLES = r''' 123 | - name: ip pool management 124 | dnac_ippool: 125 | host: 10.253.176.237 126 | port: 443 127 | username: admin 128 | password: M0bility@ccess 129 | state: present 130 | ip_pool_name: TEST_IP_POOL1 131 | ip_pool_subnet: 172.31.102.0 132 | ip_pool_prefix_len: /24 133 | ip_pool_gateway: 172.31.102.1 134 | ip_pool_dhcp_servers: 192.168.200.1 135 | ip_pool_dns_servers: 192.168.200.1 136 | ''' 137 | 138 | RETURN = r''' 139 | # 140 | ''' 141 | 142 | from ansible.module_utils.basic import AnsibleModule 143 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 144 | 145 | 146 | def main(): 147 | _ip_pool_exists = False 148 | 149 | module_args = dnac_argument_spec 150 | module_args.update( 151 | # removed api_path local variable 152 | state=dict(type='str', default='present', choices=['absent', 'present', 'update']), 153 | ip_pool_name=dict(type='str', required=True), 154 | ip_pool_subnet=dict(type='str', required=True), 155 | ip_pool_prefix_len=dict(type='str', default='/8'), 156 | ip_pool_gateway=dict(type='str', required=True), 157 | ip_pool_dhcp_servers=dict(type='list'), 158 | ip_pool_dns_servers=dict(type='list'), 159 | ip_pool_overlapping=dict(type='bool', default=False) 160 | ) 161 | 162 | result = dict( 163 | changed=False, 164 | original_message='', 165 | message='') 166 | 167 | module = AnsibleModule( 168 | argument_spec=module_args, 169 | supports_check_mode=False 170 | ) 171 | 172 | # build the required payload data structure 173 | payload = { 174 | "ipPoolName": module.params['ip_pool_name'], 175 | "ipPoolCidr": module.params['ip_pool_subnet'] + module.params['ip_pool_prefix_len'], 176 | "gateways": module.params['ip_pool_gateway'].split(','), 177 | "dhcpServerIps": module.params['ip_pool_dhcp_servers'], 178 | "dnsServerIps": module.params['ip_pool_dns_servers'], 179 | "overlapping": module.params['ip_pool_overlapping'] 180 | } 181 | 182 | # Instantiate the DnaCenter class object 183 | dnac = DnaCenter(module) 184 | dnac.api_path = 'api/v2/ippool' 185 | # check if the configuration is already in the desired state 186 | 187 | # Get the ip pools 188 | ip_pools = dnac.get_obj() 189 | 190 | _ip_pool_names = [pool['ipPoolName'] for pool in ip_pools['response']] 191 | 192 | # does pool provided exist 193 | if module.params['ip_pool_name'] in _ip_pool_names: 194 | _ip_pool_exists = True 195 | else: 196 | _ip_pool_exists = False 197 | 198 | # actions 199 | if module.params['state'] == 'present' and _ip_pool_exists: 200 | result['changed'] = False 201 | module.exit_json(msg='IP Pool already exists.', **result) 202 | elif module.params['state'] == 'present' and not _ip_pool_exists: 203 | dnac.create_obj(payload) 204 | elif module.params['state'] == 'absent' and _ip_pool_exists: 205 | _ip_pool_id = [pool['id'] for pool in ip_pools['response'] 206 | if pool['ipPoolName'] == module.params['ip_pool_name']] 207 | dnac.delete_obj(_ip_pool_id[0]) 208 | elif module.params['state'] == 'absent' and not _ip_pool_exists: 209 | result['changed'] = False 210 | module.exit_json(msg='Ip pool Does not exist. Cannot delete.', **result) 211 | 212 | 213 | if __name__ == "__main__": 214 | main() 215 | -------------------------------------------------------------------------------- /plugins/modules/dnac_dns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2018 World Wide Technology, Inc. 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import absolute_import, division, print_function 6 | __metaclass__ = type 7 | 8 | ANSIBLE_METADATA = { 9 | 'metadata_version': '1.1', 10 | 'status': ['preview'], 11 | 'supported_by': 'community' 12 | } 13 | 14 | DOCUMENTATION = r''' 15 | 16 | module: dnac_dns 17 | short_description: Add or Delete DNS Server(s) 18 | description: Add or delete DNS Server(s) in the Cisco DNA Center Design Workflow. \ 19 | The DNS Severs can be different values at different levels in the group hierarchy. 20 | 21 | version_added: "2.5" 22 | author: "Jeff Andiorio (@jandiorio)" 23 | 24 | options: 25 | host: 26 | description: 27 | - Host is the target Cisco DNA Center controller to execute against. 28 | required: true 29 | 30 | port: 31 | description: 32 | - Port is the TCP port for the HTTP connection. 33 | required: false 34 | default: 443 35 | choices: 36 | - 80 37 | - 443 38 | username: 39 | description: 40 | - Provide the username for the connection to the Cisco DNA Center Controller. 41 | required: true 42 | 43 | password: 44 | description: 45 | - Provide the password for connection to the Cisco DNA Center Controller. 46 | required: true 47 | 48 | use_proxy: 49 | description: 50 | - Enter a boolean value for whether to use proxy or not. 51 | required: false 52 | default: true 53 | choices: 54 | - true 55 | - false 56 | 57 | use_ssl: 58 | description: 59 | - Enter the boolean value for whether to use SSL or not. 60 | required: false 61 | default: true 62 | choices: 63 | - true 64 | - false 65 | 66 | timeout: 67 | description: 68 | - The timeout provides a value for how long to wait for the executed command complete. 69 | required: false 70 | default: 30 71 | 72 | validate_certs: 73 | description: 74 | - Specify if verifying the certificate is desired. 75 | required: false 76 | default: true 77 | choices: 78 | - true 79 | - false 80 | 81 | state: 82 | description: 83 | - State provides the action to be executed using the terms present, absent, etc. 84 | required: false 85 | default: present 86 | choices: 87 | - present 88 | - absent 89 | 90 | primary_dns_server: 91 | description: 92 | - IP address of the primary DNS Server to manipulate. 93 | required: false 94 | 95 | secondary_dns_server: 96 | description: 97 | - IP address of the secondary DNS Server to manipulate. 98 | required: false 99 | 100 | domain_name: 101 | description: 102 | - DNS domain name of the environment within Cisco DNA Center 103 | required: true 104 | 105 | group_name: 106 | description: 107 | - group_name is the name of the group in the hierarchy where you would like to apply these settings. 108 | required: false 109 | default: Global 110 | 111 | ''' 112 | 113 | EXAMPLES = r''' 114 | 115 | - name: create dns server 116 | dnac_dns: 117 | host: "{{ dnac.hostname }}" 118 | port: "{{ dnac.port }}" 119 | username: "{{ dnac.username }}" 120 | password: "{{ dnac.password }}" 121 | state: present 122 | primary_dns_server: 192.168.200.1 123 | secondary_dns_server: 192.168.200.2 124 | domain_name: dna.center.local 125 | 126 | 127 | - name: delete dns server 128 | dnac_dns: 129 | host: "{{ dnac.hostname }}" 130 | port: "{{ dnac.port }}" 131 | username: "{{ dnac.username }}" 132 | password: "{{ dnac.password }}" 133 | state: absent 134 | 135 | 136 | ''' 137 | 138 | RETURN = r''' 139 | previous: 140 | description: Configuration from DNA Center prior to any changes. 141 | returned: success 142 | type: list 143 | sample: 144 | - groupUuid: '-1' 145 | inheritedGroupName: '' 146 | inheritedGroupUuid: '' 147 | instanceType: dns 148 | instanceUuid: 799b3bc7-61fa-41b2-8fb3-db611af9db67 149 | key: dns.server 150 | namespace: global 151 | type: dns.setting 152 | value: [] 153 | version: 52 154 | proprosed: 155 | description: Configuration to be sent to DNA Center. 156 | returned: success 157 | type: list 158 | sample: 159 | - groupUuid: '-1' 160 | instanceType: dns 161 | key: dns.server 162 | namespace: global 163 | type: dns.setting 164 | value: 165 | - domainName: campus.wwtatc.local 166 | primaryIpAddress: 192.168.200.1 167 | secondaryIpAddress: 192.168.200.2 168 | ''' 169 | 170 | from ansible.module_utils.basic import AnsibleModule 171 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 172 | 173 | # ----------------------------------------------- 174 | # define static required variales 175 | # ----------------------------------------------- 176 | # ----------------------------------------------- 177 | # main 178 | # ----------------------------------------------- 179 | 180 | 181 | def main(): 182 | module_args = dnac_argument_spec 183 | module_args.update(primary_dns_server=dict(type='str', required=False, default=''), 184 | secondary_dns_server=dict(type='str', required=False), 185 | domain_name=dict(type='str', required=False, default=''), 186 | group_name=dict(type='str', required=False, default='-1') 187 | ) 188 | 189 | module = AnsibleModule(argument_spec=module_args, 190 | supports_check_mode=True 191 | ) 192 | 193 | # Define local variables 194 | domain_name = module.params['domain_name'] 195 | primary_dns_server = module.params['primary_dns_server'] 196 | secondary_dns_server = module.params['secondary_dns_server'] 197 | group_name = module.params['group_name'] 198 | 199 | # Build the payload dictionary 200 | payload = [{"instanceType": "dns", 201 | "namespace": "global", 202 | "type": "dns.setting", 203 | "key": "dns.server", 204 | "value": [{"domainName": domain_name, 205 | "primaryIpAddress": primary_dns_server, 206 | "secondaryIpAddress": secondary_dns_server 207 | }], 208 | "groupUuid": "-1", 209 | } 210 | ] 211 | 212 | # instansiate the dnac class 213 | dnac = DnaCenter(module) 214 | 215 | # obtain the groupUuid and update the payload dictionary 216 | group_id = dnac.get_group_id(group_name) 217 | 218 | # Set the api_path 219 | dnac.api_path = 'api/v1/commonsetting/global/' + group_id + '?key=dns.server' 220 | 221 | dnac.process_common_settings(payload, group_id) 222 | 223 | 224 | if __name__ == "__main__": 225 | main() 226 | -------------------------------------------------------------------------------- /plugins/modules/dnac_banner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2018, World Wide Technology, Inc. 4 | # GNU General Public License v3.0+ 5 | # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | __metaclass__ = type 9 | 10 | ANSIBLE_METADATA = { 11 | 'metadata_version': '1.1', 12 | 'status': ['preview'], 13 | 'supported_by': 'community' 14 | } 15 | 16 | DOCUMENTATION = r''' 17 | 18 | --- 19 | 20 | module: dnac_banner 21 | short_description: Create a banner in Cisco DNA Center 22 | description: 23 | - Create a banner in Cisco DNA Center at any valid level in the hierarchy. 24 | version_added: "2.5" 25 | author: "Jeff Andiorio (@jandiorio)" 26 | options: 27 | host: 28 | description: 29 | - Host is the target Cisco DNA Center controller to execute against. 30 | required: true 31 | port: 32 | description: 33 | - Port is the TCP port for the HTTP connection. 34 | required: false 35 | default: 443 36 | choices: 37 | - 80 38 | - 443 39 | username: 40 | description: 41 | - Provide the username for the connection to the 42 | Cisco DNA Center Controller. 43 | required: true 44 | password: 45 | description: 46 | - Provide the password for connection to the 47 | Cisco DNA Center Controller. 48 | required: true 49 | use_proxy: 50 | description: 51 | - Enter a boolean value for whether to use proxy or not. 52 | required: false 53 | default: true 54 | choices: 55 | - true 56 | - false 57 | use_ssl: 58 | description: 59 | - Enter the boolean value for whether to use SSL or not. 60 | required: false 61 | default: true 62 | choices: 63 | - true 64 | - false 65 | timeout: 66 | description: 67 | - The timeout provides a value for how long to wait for the executed 68 | command complete. 69 | required: false 70 | default: 30 71 | validate_certs: 72 | description: 73 | - Specify if verifying the certificate is desired. 74 | required: false 75 | default: true 76 | choices: 77 | - true 78 | - false 79 | state: 80 | description: 81 | - State provides the action to be executed using the terms present, 82 | absent, etc. 83 | required: false 84 | default: present 85 | choices: 86 | - present 87 | - absent 88 | banner_message: 89 | description: 90 | - Enter the desired Message of the Day banner to post to 91 | Cisco DNA Center. 92 | required: false 93 | default: '' 94 | group_name: 95 | description: 96 | - group_name is the name of the group in the hierarchy where 97 | you would like to apply the banner. 98 | required: false 99 | default: Global 100 | retain_banner: 101 | description: 102 | - Boolean attribute for whether to overwrite the device existing 103 | banner or not. 104 | required: false 105 | default: true 106 | notes: 107 | requirements: 108 | - geopy 109 | - TimezoneFinder 110 | - requests 111 | ''' 112 | 113 | EXAMPLES = r''' 114 | --- 115 | 116 | - name: create a banner 117 | dnac_banner: 118 | host: 1.1.1.1 119 | port: 443 120 | username: "{{username}}" 121 | password: "{{password}}" 122 | state: present 123 | group_name: "Global" 124 | banner_message: "Welcome to DNAC/SDA Lab" 125 | 126 | - name: delete a banner 127 | dnac_banner: 128 | host: 1.1.1.1 129 | port: 443 130 | username: "{{username}}" 131 | password: "{{password}}" 132 | state: absent 133 | banner_message: "Welcome to DNAC/SDA Lab" 134 | 135 | ''' 136 | 137 | RETURN = r''' 138 | --- 139 | previous: 140 | description: Configuration from DNA Center prior to any changes. 141 | returned: success 142 | type: list 143 | sample: 144 | - groupUuid: '-1' 145 | inheritedGroupName: '' 146 | inheritedGroupUuid: '' 147 | instanceType: banner 148 | instanceUuid: 6a8bbd9c-2346-46ae-8948-2010aad18f77 149 | key: device.banner 150 | namespace: global 151 | type: banner.setting 152 | value: 153 | - bannerMessage: We are testing the new dnac.py logic 154 | retainExistingBanner: true 155 | version: 5 156 | version: '1.0' 157 | proposed: 158 | description: Configuration to be sent to DNA Center. 159 | returned: success 160 | sample: 161 | - groupUuid: '-1' 162 | instanceType: banner 163 | key: device.banner 164 | namespace: global 165 | type: banner.setting 166 | value: 167 | - bannerMessage: We are testing the new dnac.py logic 168 | retainExistingBanner: true 169 | ''' 170 | 171 | from ansible.module_utils.basic import AnsibleModule 172 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 173 | 174 | 175 | # ----------------------------------------------- 176 | # main 177 | # ----------------------------------------------- 178 | 179 | def main(): 180 | 181 | module_args = dnac_argument_spec 182 | module_args.update( 183 | banner_message=dict(type='str', default='', required=False), 184 | group_name=dict(type='str', default='-1'), 185 | retain_banner=dict(type='bool', default=True) 186 | ) 187 | 188 | module = AnsibleModule( 189 | argument_spec=module_args, 190 | supports_check_mode=True 191 | ) 192 | 193 | # Set Local Variables 194 | banner_message = module.params['banner_message'] 195 | retain_banner = module.params['retain_banner'] 196 | group_name = module.params['group_name'] 197 | # state = module.params['state'] 198 | 199 | # Build the payload dictionary 200 | payload = [{"instanceType": "banner", 201 | "namespace": "global", 202 | "type": "banner.setting", 203 | "key": "device.banner", 204 | "value": [{"bannerMessage": banner_message, 205 | "retainExistingBanner": retain_banner 206 | }], 207 | "groupUuid": "-1" 208 | }] 209 | 210 | # instansiate the dnac class 211 | dnac = DnaCenter(module) 212 | 213 | # obtain the groupUuid and update the payload dictionary 214 | group_id = dnac.get_group_id(group_name) 215 | 216 | # set the retain banner attribute 217 | if retain_banner: 218 | payload[0]['value'][0]['retainExistingBanner'] = True 219 | else: 220 | payload[0]['value'][0]['retainExistingBanner'] = False 221 | 222 | # set the api_path 223 | dnac.api_path = 'api/v1/commonsetting/global/' + group_id + \ 224 | '?key=device.banner' 225 | 226 | dnac.process_common_settings(payload, group_id) 227 | 228 | 229 | if __name__ == "__main__": 230 | main() 231 | -------------------------------------------------------------------------------- /plugins/modules/dnac_snmpv2_credential.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2018, World Wide Technology, Inc. 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | ANSIBLE_METADATA = { 7 | 'metadata_version': '1.0', 8 | 'status': ['development'], 9 | 'supported_by': 'jandiorio' 10 | } 11 | 12 | 13 | DOCUMENTATION = r''' 14 | --- 15 | module: dnac_snmpv2_credential 16 | short_description: Manage SNMPv2 Credential(s) within Cisco DNA Center 17 | description: Manage SNMPv2 Credential(s) settings in Cisco DNA Center. Based on 1.1+ version of DNAC API 18 | author: 19 | - Jeff Andiorio (@jandiorio) 20 | version_added: '2.4' 21 | requirements: 22 | - DNA Center 1.2+ 23 | 24 | options: 25 | host: 26 | description: 27 | - Host is the target Cisco DNA Center controller to execute against. 28 | required: true 29 | port: 30 | description: 31 | - Port is the TCP port for the HTTP connection. 32 | required: false 33 | default: 443 34 | choices: 35 | - 80 36 | - 443 37 | username: 38 | description: 39 | - Provide the username for the connection to the Cisco DNA Center Controller. 40 | required: true 41 | password: 42 | description: 43 | - Provide the password for connection to the Cisco DNA Center Controller. 44 | required: true 45 | use_proxy: 46 | description: 47 | - Enter a boolean value for whether to use proxy or not. 48 | required: false 49 | default: true 50 | choices: 51 | - true 52 | - false 53 | use_ssl: 54 | description: 55 | - Enter the boolean value for whether to use SSL or not. 56 | required: false 57 | default: true 58 | choices: 59 | - true 60 | - false 61 | timeout: 62 | description: 63 | - The timeout provides a value for how long to wait for the executed command complete. 64 | required: false 65 | default: 30 66 | validate_certs: 67 | description: 68 | - Specify if verifying the certificate is desired. 69 | required: false 70 | default: true 71 | choices: 72 | - true 73 | - false 74 | state: 75 | description: 76 | - State provides the action to be executed using the terms present, absent, etc. 77 | required: false 78 | default: present 79 | choices: 80 | - present 81 | - absent 82 | credential_type: 83 | description: Specify whether the SNMPv2 Community is READ or WRITE. 84 | default: SNMPV2_WRITE_COMMUNITY 85 | choices: ['SNMPV2_READ_COMMUNITY','SNMPV2_WRITE_COMMUNITY'] 86 | required: false 87 | type: string 88 | snmp_community: 89 | description: The SNMPv2 community to be managed. 90 | required: true 91 | type: string 92 | snmp_description: 93 | description: A description of the SNMPv2 Community. 94 | type: string 95 | required: true 96 | snmp_comments: 97 | description: Comments about the SNMPv2 Community. 98 | required: true 99 | type: string 100 | 101 | ''' 102 | 103 | EXAMPLES = r''' 104 | 105 | - name: create snmpv2 communities 106 | dnac_snmpv2_credential: 107 | host: "{{host}}" 108 | port: "{{port}}" 109 | username: "{{username}}" 110 | password: "{{password}}" 111 | state: present 112 | credential_type: SNMPV2_WRITE_COMMUNITY 113 | snmp_community: write-community 114 | snmp_description: TEST-SNMP-WRITE 115 | snmp_comments: snmp write community 116 | ''' 117 | 118 | RETURN = r''' 119 | # 120 | ''' 121 | 122 | from ansible.module_utils.basic import AnsibleModule 123 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 124 | 125 | # ----------------------------------------------- 126 | # define static required variales 127 | # ----------------------------------------------- 128 | # ----------------------------------------------- 129 | # main 130 | # ----------------------------------------------- 131 | 132 | 133 | def main(): 134 | _credential_exists = False 135 | module_args = dnac_argument_spec 136 | module_args.update( 137 | credential_type=dict( 138 | type='str', default='SNMPV2_WRITE_COMMUNITY', 139 | choices=['SNMPV2_READ_COMMUNITY', 'SNMPV2_WRITE_COMMUNITY'] 140 | ), 141 | snmp_community=dict(type='str', required=True), 142 | snmp_description=dict(type='str', required=True), 143 | snmp_comments=dict(type='str', required=True) 144 | ) 145 | 146 | result = dict( 147 | changed=False, 148 | original_message='', 149 | message='') 150 | 151 | module = AnsibleModule( 152 | argument_spec=module_args, 153 | supports_check_mode=False 154 | ) 155 | if module.params['credential_type'] == 'SNMPV2_WRITE_COMMUNITY': 156 | _community_key_name = 'writeCommunity' 157 | _url_suffix = 'snmpv2-write-community' 158 | elif module.params['credential_type'] == 'SNMPV2_READ_COMMUNITY': 159 | _community_key_name = 'readCommunity' 160 | _url_suffix = 'snmpv2-read-community' 161 | 162 | # Build the payload dictionary 163 | payload = [ 164 | {_community_key_name: module.params['snmp_community'], 165 | "description": module.params['snmp_description'], 166 | "comments": module.params['snmp_comments'] 167 | } 168 | ] 169 | 170 | # instansiate the dnac class 171 | dnac = DnaCenter(module) 172 | dnac.api_path = 'api/v1/global-credential?credentialSubType=' + module.params['credential_type'] 173 | # 174 | # check if the configuration is already in the desired state 175 | settings = dnac.get_obj() 176 | 177 | # _creds = [ cred['description'] for cred in settings['response']] 178 | _creds = [(cred['description'], cred['id']) 179 | for cred in settings['response'] 180 | if cred['description'] == module.params['snmp_description']] 181 | 182 | if len(_creds) > 1: 183 | module.fail_json(msg="Multiple matching entries...invalid.", **result) 184 | elif len(_creds) == 0: 185 | _credential_exists = False 186 | else: 187 | _credential_exists = True 188 | 189 | ''' 190 | check if cred exists 191 | check state flag: present = create, absent = delete, update = change url_password 192 | if state = present and cred doesn't exist, create user 193 | if state = absent and cred exists, delete user 194 | if state = update and cred exists, use put to update user ''' 195 | 196 | if _credential_exists: 197 | 198 | if module.params['state'] == 'present': 199 | # in desired state 200 | result['changed'] = False 201 | result['msg'] = 'Credential exists. Use state: update to change credential' 202 | module.exit_json(**result) 203 | 204 | elif module.params['state'] == 'absent': 205 | dnac.api_path = 'api/v1/global-credential/' 206 | dnac.delete_obj(_creds[0][1]) 207 | 208 | elif not _credential_exists: 209 | 210 | if module.params['state'] == 'present': 211 | dnac.api_path = 'api/v1/global-credential/' + _url_suffix 212 | dnac.create_obj(payload) 213 | 214 | elif module.params['state'] == 'absent': 215 | module.fail_json(msg="Credential doesn't exist. Cannot delete or update.", **result) 216 | 217 | 218 | if __name__ == "__main__": 219 | main() 220 | -------------------------------------------------------------------------------- /plugins/modules/dnac_wireless_provision.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import, division, print_function 5 | __metaclass__ = type 6 | 7 | ANSIBLE_METADATA = {'metadata_version': '1.0', 8 | 'status': ['preview'], 9 | 'supported_by': 'jeff andiorio'} 10 | 11 | DOCUMENTATION = r''' 12 | --- 13 | module: dnac_wireless_provision.py 14 | short_description: Provision WLC 15 | description: Provision the wireless LAN controller(s) 16 | version_added: "2.8" 17 | author: 18 | - Jeff Andiorio (@jandiorio) 19 | 20 | requirements: 21 | - requests 22 | 23 | options: 24 | host: 25 | description: 26 | - Host is the target Cisco DNA Center controller to execute against. 27 | required: true 28 | 29 | port: 30 | description: 31 | - Port is the TCP port for the HTTP connection. 32 | required: false 33 | default: 443 34 | choices: 35 | - 80 36 | - 443 37 | 38 | username: 39 | description: 40 | - Provide the username for the connection to the Cisco DNA Center Controller. 41 | required: true 42 | 43 | password: 44 | description: 45 | - Provide the password for connection to the Cisco DNA Center Controller. 46 | required: true 47 | 48 | use_proxy: 49 | description: 50 | - Enter a boolean value for whether to use proxy or not. 51 | required: false 52 | default: true 53 | choices: 54 | - true 55 | - false 56 | 57 | use_ssl: 58 | description: 59 | - Enter the boolean value for whether to use SSL or not. 60 | required: false 61 | default: true 62 | choices: 63 | - true 64 | - false 65 | 66 | timeout: 67 | description: 68 | - The timeout provides a value for how long to wait for the executed command complete. 69 | required: false 70 | default: 30 71 | 72 | validate_certs: 73 | description: 74 | - Specify if verifying the certificate is desired. 75 | required: false 76 | default: true 77 | choices: 78 | - true 79 | - false 80 | 81 | state: 82 | description: 83 | - State provides the action to be executed using the terms present, absent, etc. 84 | required: false 85 | default: present 86 | choices: 87 | - present 88 | - absent 89 | 90 | name: 91 | description: 92 | - Name of the wireless LAN controller as it appears in DNA Center 93 | required: true 94 | 95 | site: 96 | description: 97 | - site hierarchy path to associate the device to ('Global/Central/Maryland Heights/ATC56') 98 | required: false 99 | 100 | managed_ap_locations: 101 | description: 102 | - list of site hierarchy path of the locations of managed access points 103 | required: false 104 | 105 | interface_ip: 106 | description: 107 | - name of the wireless management interface 108 | required: false 109 | default: 1.1.1.1 110 | 111 | 112 | interface_prefix_length: 113 | description: prefix length for netmask 114 | required: false 115 | default: 24 116 | 117 | interface_gateway: 118 | description: 119 | - default_gateway for the management interface 120 | required: false 121 | default: 1.1.1.2 122 | 123 | vlan: 124 | description: 125 | - vlan number for flexconnect 126 | required: false 127 | 128 | interface: 129 | description: 130 | - interface for wireless management 131 | required: false 132 | 133 | reprovision: 134 | description: 135 | - bool for if this is a reprovision (false = initial provision) 136 | 137 | ''' 138 | 139 | EXAMPLES = r''' 140 | - name: provision wireless 141 | dnac_wireless_provision: 142 | host: "{{ inventory_hostname }}" 143 | port: '443' 144 | username: "{{ username }}" 145 | password: "{{ password }}" 146 | state: present 147 | # 148 | name: 'dna-3-wlc' 149 | site: 'Global/Central/Maryland Heights/ATC56' 150 | managed_ap_locations: 151 | - 'Global/Central/Maryland Heights/ATC56' 152 | - 'Global/Central/Maryland Heights/ATC56/floor_1' 153 | vlan: 30 154 | interface: vlan_30 155 | reprovision: yes 156 | 157 | ''' 158 | 159 | 160 | RETURN = r''' 161 | # 162 | ''' 163 | 164 | from ansible.module_utils.basic import AnsibleModule 165 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 166 | 167 | 168 | def main(): 169 | 170 | module_args = dnac_argument_spec 171 | module_args.update( 172 | state=dict(type='str', choices=['present']), 173 | name=dict(type='str', required=True), 174 | site=dict(type='str', required=False), 175 | managed_ap_locations=dict(type='list', required=False), 176 | interface_ip=dict(type='str', required=False, default='1.1.1.1'), 177 | interface_prefix_length=dict(type='str', required=False, default='24'), 178 | interface_gateway=dict(type='str', required=False, default='1.1.1.2'), 179 | lag_or_port_number=dict(type='str', required=False), 180 | vlan=dict(type='str', required=False), 181 | interface=dict(type='str', required=False), 182 | reprovision=dict(type='bool', required=False, default=False) 183 | ) 184 | 185 | module = AnsibleModule( 186 | argument_spec=module_args, 187 | supports_check_mode=False 188 | ) 189 | 190 | # build the required payload data structure 191 | payload = [ 192 | {"deviceName": module.params['name'], 193 | "site": module.params['site'], 194 | "managedAPLocations": module.params['managed_ap_locations'] 195 | } 196 | ] 197 | 198 | if module.params['interface']: 199 | payload[0].update( 200 | {"dynamicInterfaces": [ 201 | {"interfaceIPAddress": module.params['interface_ip'], 202 | "interfaceNetmaskInCIDR": module.params['interface_prefix_length'], 203 | "interfaceGateway": module.params['interface_gateway'], 204 | "lagOrPortNumber":module.params['lag_or_port_number'], 205 | "vlanId": module.params['vlan'], 206 | "interfaceName": module.params['interface'], 207 | } 208 | ] 209 | } 210 | ) 211 | 212 | # Instantiate the DnaCenter class object 213 | dnac = DnaCenter(module) 214 | 215 | # Check if Device Has been Provisioned 216 | # dnac.api_path = 'dna/intent/api/v1/network-device?hostname=' + module.params['name'] 217 | # device = dnac.get_obj() 218 | 219 | # if device['response']: 220 | # if device['response'][0]['location'] == None: 221 | # _PROVISIONED = False 222 | # else: 223 | # _PROVISIONED = True 224 | # else: 225 | # _PROVISIONED = False 226 | 227 | if module.params['reprovision']: 228 | _PROVISIONED = True 229 | else: 230 | _PROVISIONED = False 231 | # Reset API Path 232 | dnac.api_path = 'dna/intent/api/v1/wireless/provision' 233 | 234 | # actions 235 | if module.params['state'] == 'present' and _PROVISIONED: 236 | # module.exit_json(msg=payload) 237 | dnac.update_obj(payload) 238 | elif module.params['state'] == 'present' and not _PROVISIONED: 239 | # module.exit_json(msg='provision') 240 | dnac.create_obj(payload) 241 | 242 | 243 | if __name__ == "__main__": 244 | main() 245 | -------------------------------------------------------------------------------- /plugins/modules/dnac_wireless_ssid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import, division, print_function 5 | __metaclass__ = type 6 | 7 | ANSIBLE_METADATA = {'metadata_version': '1.0', 8 | 'status': ['preview'], 9 | 'supported_by': 'jeff andiorio'} 10 | 11 | DOCUMENTATION = r''' 12 | --- 13 | module: dnac_wireless_ssid.py 14 | short_description: Manage SSID 15 | description: manage the create/update/delete of SSIDs in DNAC 16 | version_added: "2.8" 17 | author: 18 | - Jeff Andiorio (@jandiorio) 19 | 20 | requirements: 21 | - requests 22 | 23 | options: 24 | host: 25 | description: 26 | - Host is the target Cisco DNA Center controller to execute against. 27 | required: true 28 | 29 | port: 30 | description: 31 | - Port is the TCP port for the HTTP connection. 32 | required: false 33 | default: 443 34 | choices: 35 | - 80 36 | - 443 37 | 38 | username: 39 | description: 40 | - Provide the username for the connection to the Cisco DNA Center Controller. 41 | required: true 42 | 43 | password: 44 | description: 45 | - Provide the password for connection to the Cisco DNA Center Controller. 46 | required: true 47 | 48 | use_proxy: 49 | description: 50 | - Enter a boolean value for whether to use proxy or not. 51 | required: false 52 | default: true 53 | choices: 54 | - true 55 | - false 56 | 57 | use_ssl: 58 | description: 59 | - Enter the boolean value for whether to use SSL or not. 60 | required: false 61 | default: true 62 | choices: 63 | - true 64 | - false 65 | 66 | timeout: 67 | description: 68 | - The timeout provides a value for how long to wait for the executed command complete. 69 | required: false 70 | default: 30 71 | 72 | validate_certs: 73 | description: 74 | - Specify if verifying the certificate is desired. 75 | required: false 76 | default: true 77 | choices: 78 | - true 79 | - false 80 | 81 | state: 82 | description: 83 | - State provides the action to be executed using the terms present, absent, etc. 84 | required: false 85 | default: present 86 | choices: 87 | - present 88 | - absent 89 | 90 | name: 91 | description: 92 | - Name of the wireless SSID 93 | required: true 94 | 95 | security_level: 96 | description: 97 | - security level for the SSID 98 | required: true 99 | choices: 100 | - WPA2_ENTERPRISE 101 | - WPA2_PERSONAL 102 | - OPEN 103 | 104 | passphrase: 105 | description: 106 | - secret passphrase for WPA2_PERSONAL 107 | required: false 108 | 109 | enable_fastlane: 110 | description: 111 | - boolean to enable fastlane 112 | required: false 113 | default: false 114 | 115 | enable_mac_filtering: 116 | description: 117 | - boolean for enableing MAC filtering 118 | required: false 119 | default: false 120 | 121 | traffic_type: 122 | description: 123 | - type of traffic for SSID 124 | required: false 125 | choices: 126 | - voicedata 127 | - data 128 | default: voicedata 129 | 130 | radio_policy: 131 | description: 132 | - SSID radio policy to associate 133 | required: false 134 | choices: 135 | - 'Dual band operation (2.4GHz and 5GHz)' 136 | - 'Dual band operation with band select' 137 | - '5GHz only' 138 | - '2.4GHz only' 139 | 140 | enable_broadcast_ssid: 141 | description: 142 | - boolean for SSID broadcast 143 | required: false 144 | default: true 145 | 146 | fast_transition: 147 | description: 148 | - configuration of fast transition 149 | required: false 150 | choices: 151 | - 'Adaptive' 152 | - 'Enable' 153 | - 'Disable' 154 | default: 'Disable' 155 | 156 | ''' 157 | 158 | EXAMPLES = r''' 159 | 160 | - name: build wireless ssid 161 | dnac_wireless_ssid: 162 | host: "{{ inventory_hostname }}" 163 | port: '443' 164 | username: "{{ username }}" 165 | password: "{{ password }}" 166 | state: present 167 | # 168 | name: 'SSID-1' 169 | security_level: 'WPA2_PERSONAL' 170 | passphrase: SUPERSECRET 171 | 172 | ''' 173 | 174 | 175 | RETURN = r''' 176 | # 177 | ''' 178 | 179 | from ansible.module_utils.basic import AnsibleModule 180 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 181 | 182 | 183 | def main(): 184 | _ssid_exists = False 185 | 186 | module_args = dnac_argument_spec 187 | module_args.update( 188 | state=dict(type='str', choices=['absent', 'present']), 189 | name=dict(type='str', required=True), 190 | security_level=dict(type='str', required=True, choices=['WPA2_ENTERPRISE', 'WPA2_PERSONAL', 'OPEN']), 191 | passphrase=dict(type='str', required=False, no_log=True), 192 | enable_fastlane=dict(type='bool', required=False, default=False), 193 | enable_mac_filtering=dict(type='bool', required=False, default=False), 194 | traffic_type=dict(type='str', required=False, choices=['voicedata', 'data'], default='voicedata'), 195 | radio_policy=dict(type='str', required=False, 196 | choices=['Dual band operation (2.4GHz and 5GHz)', 197 | 'Dual band operation with band select', 198 | '5GHz only', 199 | '2.4GHz only)'], 200 | default='Dual band operation (2.4GHz and 5GHz)'), 201 | enable_broadcast_ssid=dict(type=bool, required=False, default=True), 202 | fast_transition=dict(type='str', required=False, choices=['Adaptive', 'Enable', 'Disable'], default='Disable') 203 | 204 | ) 205 | 206 | result = dict( 207 | changed=False, 208 | original_message='', 209 | message='') 210 | 211 | module = AnsibleModule( 212 | argument_spec=module_args, 213 | supports_check_mode=False 214 | ) 215 | 216 | # build the required payload data structure 217 | payload = { 218 | "name": module.params['name'], 219 | "securityLevel": module.params['security_level'], 220 | "passphrase": module.params['passphrase'], 221 | "enableFastLane": module.params['enable_fastlane'], 222 | "enableMACFiltering": module.params['enable_mac_filtering'], 223 | "trafficType": module.params['traffic_type'], 224 | "radioPolicy": module.params['radio_policy'], 225 | "enableBroadcastSSID": module.params['enable_broadcast_ssid'], 226 | "fastTransition": module.params['fast_transition'] 227 | } 228 | 229 | # Instantiate the DnaCenter class object 230 | dnac = DnaCenter(module) 231 | dnac.api_path = 'dna/intent/api/v1/enterprise-ssid' 232 | 233 | # check if the configuration is already in the desired state 234 | 235 | # get the SSIDs 236 | ssids = dnac.get_obj() 237 | 238 | _ssid_names = [ssid['ssidDetails'][0]['name'] for ssid in ssids] 239 | 240 | # does pool provided exist 241 | if module.params['name'] in _ssid_names: 242 | _ssid_exists = True 243 | else: 244 | _ssid_exists = False 245 | 246 | dnac.api_path = 'dna/intent/api/v1/enterprise-ssid' 247 | 248 | # actions 249 | if module.params['state'] == 'present' and _ssid_exists: 250 | result['changed'] = False 251 | module.exit_json(msg='SSID already exists.', **result) 252 | elif module.params['state'] == 'present' and not _ssid_exists: 253 | dnac.create_obj(payload) 254 | elif module.params['state'] == 'absent' and _ssid_exists: 255 | # _ssid_id = [ssid['instanceUuid'] for ssid in ssids if ssid['ssidDetails'][0]['name'] == module.params['name']] 256 | # dnac.delete_obj(_ssid_id[0]) 257 | dnac.delete_obj(module.params['name']) 258 | elif module.params['state'] == 'absent' and not _ssid_exists: 259 | result['changed'] = False 260 | module.exit_json(msg='SSID Does not exist. Cannot delete.', **result) 261 | 262 | 263 | if __name__ == "__main__": 264 | main() 265 | -------------------------------------------------------------------------------- /plugins/modules/dnac_cli_credential.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2018, World Wide Technology, Inc. 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | ANSIBLE_METADATA = { 7 | 'metadata_version': '1.1', 8 | 'status': ['preview'], 9 | 'supported_by': 'community' 10 | } 11 | 12 | DOCUMENTATION = r''' 13 | 14 | --- 15 | 16 | module: dnac_cli_credential 17 | short_description: Add or Delete Global CLI credentials 18 | description: 19 | - Add or Delete Global CLI Credentials in Cisco DNA Center Controller. \ 20 | These credentials can be created at any level in the hierarchy.\ 21 | This module creates the credential at the specified level of hierarchy only. * Assigning the \ 22 | credential as active in the Design Workflow will be handled in the \ 23 | dnac_assign_credential module. 24 | version_added: "2.5" 25 | author: "Jeff Andiorio (@jandiorio)" 26 | 27 | options: 28 | host: 29 | description: 30 | - Host is the target Cisco DNA Center controller to execute against. 31 | required: true 32 | version_added: "2.5" 33 | port: 34 | description: 35 | - Port is the TCP port for the HTTP connection. 36 | required: false 37 | default: 443 38 | choices: 39 | - 80 40 | - 443 41 | version_added: "2.5" 42 | username: 43 | description: 44 | - Provide the username for the connection to the Cisco DNA Center Controller. 45 | required: true 46 | version_added: "2.5" 47 | password: 48 | description: 49 | - Provide the password for connection to the Cisco DNA Center Controller. 50 | required: true 51 | version_added: "2.5" 52 | use_proxy: 53 | description: 54 | - Enter a boolean value for whether to use proxy or not. 55 | required: false 56 | default: true 57 | choices: 58 | - true 59 | - false 60 | version_added: "2.5" 61 | use_ssl: 62 | description: 63 | - Enter the boolean value for whether to use SSL or not. 64 | required: false 65 | default: true 66 | choices: 67 | - true 68 | - false 69 | version_added: "2.5" 70 | timeout: 71 | description: 72 | - The timeout provides a value for how long to wait for the executed command complete. 73 | required: false 74 | default: 30 75 | version_added: "2.5" 76 | validate_certs: 77 | description: 78 | - Specify if verifying the certificate is desired. 79 | required: false 80 | default: true 81 | choices: 82 | - true 83 | - false 84 | version_added: "2.5" 85 | state: 86 | description: 87 | - State provides the action to be executed using the terms present, absent, etc. 88 | required: false 89 | default: present 90 | choices: 91 | - present 92 | - absent 93 | version_added: "2.5" 94 | cli_user: 95 | description: 96 | - Global CLI username to be manipulated 97 | required: true 98 | version_added: "2.5" 99 | cli_password: 100 | description: 101 | - Provide the Global CLI password to associate with the CLI username being created. 102 | required: true 103 | version_added: "2.5" 104 | cli_enable_password: 105 | description: 106 | - Provide a value for the CLI Enable password. 107 | required: true 108 | version_added: "2.5" 109 | cli_desc: 110 | description: 111 | - cli_desc is a friendly description of the CLI credential being created. 112 | required: true 113 | version_added: "2.5" 114 | cli_comments: 115 | description: 116 | - cli_comments is space for any additional information about the CLI credential. 117 | required: false 118 | version_added: "2.5" 119 | group_name: 120 | description: 121 | - group_name is the name of the group in the hierarchy where you would like to apply the banner. 122 | required: false 123 | default: Global 124 | version_added: "2.5" 125 | notes: 126 | requirements: 127 | - geopy 128 | - TimezoneFinder 129 | - requests 130 | 131 | ''' 132 | 133 | EXAMPLES = r''' 134 | 135 | --- 136 | 137 | - name: create a user 138 | dnac_cli_credential: 139 | host: "{{host}}" 140 | port: 443 141 | username: "{{username}}" 142 | password: "{{password}}" 143 | state: present 144 | cli_user: "cisco" 145 | cli_password: "cisco" 146 | cli_enable_password: "your_password" 147 | cli_desc: "User Description" 148 | cli_comments: "some comments" 149 | 150 | ''' 151 | 152 | RETURN = r''' 153 | --- 154 | # 155 | ''' 156 | 157 | from ansible.module_utils.basic import AnsibleModule 158 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 159 | 160 | # ----------------------------------------------- 161 | # define static required variales 162 | # ----------------------------------------------- 163 | # ----------------------------------------------- 164 | # main 165 | # ----------------------------------------------- 166 | 167 | 168 | def main(): 169 | _user_exists = False 170 | module_args = dnac_argument_spec 171 | module_args.update( 172 | cli_user=dict(type='str', required=True), 173 | cli_password=dict(type='str', required=True, no_log=True), 174 | cli_enable_password=dict(type='str', required=True, no_log=True), 175 | cli_desc=dict(type='str', required=True), 176 | cli_comments=dict(type='str', required=False), 177 | group_name=dict(type='str', default='-1') 178 | ) 179 | 180 | result = dict( 181 | changed=False, 182 | original_message='', 183 | message='') 184 | 185 | module = AnsibleModule( 186 | argument_spec=module_args, 187 | supports_check_mode=False 188 | ) 189 | 190 | # Build the payload dictionary 191 | payload = [ 192 | {"username": module.params['cli_user'], 193 | "password": module.params['cli_password'], 194 | "enablePassword": module.params['cli_enable_password'], 195 | "description": module.params['cli_desc'], 196 | "comments": module.params['cli_comments'] 197 | } 198 | ] 199 | 200 | # instansiate the dnac class 201 | dnac = DnaCenter(module) 202 | dnac.api_path = 'api/v1/global-credential?credentialSubType=CLI' 203 | 204 | # check if the configuration is already in the desired state 205 | settings = dnac.get_obj() 206 | 207 | _usernames = [user['username'] for user in settings['response']] 208 | if module.params['cli_user'] in _usernames: 209 | _user_exists = True 210 | else: 211 | _user_exists = False 212 | ''' 213 | check if username exists 214 | check state flag: present = create, absent = delete, update = change url_password 215 | if state = present and user doesn't exist, create user 216 | if state = absent and user exists, delete user 217 | if state = update and user exists, use put to update user ''' 218 | 219 | for setting in settings['response']: 220 | if setting['username'] == payload[0]['username']: 221 | if module.params['state'] == 'absent': 222 | dnac.api_path = 'api/v1/global-credential' 223 | dnac.delete_obj(setting['id']) 224 | elif module.params['state'] == 'update': 225 | # call update function 226 | payload = payload[0].update({'id': setting['id']}) 227 | dnac.api_path = 'api/v1/global-credential/cli' 228 | # dnac.api_path = 'dna/intent/api/v1/global-credential/cli' 229 | dnac.update_obj(payload) 230 | 231 | if not _user_exists and module.params['state'] == 'present': 232 | # call create function 233 | dnac.api_path = 'api/v1/global-credential/cli' 234 | # dnac.api_path = 'dna/intent/api/v1/global-credential/cli' 235 | dnac.create_obj(payload) 236 | elif not _user_exists and module.params['state'] == 'update': 237 | module.fail_json(msg="User doesn't exist. Cannot delete or update.", **result) 238 | elif not _user_exists and module.params['state'] == 'absent': 239 | module.fail_json(msg="User doesn't exist. Cannot delete or update.", **result) 240 | elif _user_exists and module.params['state'] == 'present': 241 | result['changed'] = False 242 | result['msg'] = 'User exists. Use state: update to change user' 243 | module.exit_json(**result) 244 | 245 | 246 | if __name__ == "__main__": 247 | main() 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Collection - wwt.ansible_dnac 2 | 3 | ## Ansible Modules for DNA Center 4 | 5 | These modules provide declarative and idempotent access to configure the design elements of [Cisco's DNA Center](https://www.cisco.com/c/en/us/products/cloud-systems.../dna-center/index.html). 6 | 7 | ### DevNet Code Exchange 8 | 9 | This repository is featured on the Cisco DevNet Code Exchange. 10 | 11 | [![published](https://static.production.devnetcloud.com/codeexchange/assets/images/devnet-published.svg)](https://developer.cisco.com/codeexchange/github/repo/jandiorio/ansible-dnac-modules) 12 | 13 | ### Content 14 | 15 | * The webinar below was hosted by Redhat and delivered by Jeff Andiorio of World Wide Technology on 8/7/2018. 16 | 17 | [WWT / Redhat Ansible Webinar](https://www.ansible.com/resources/webinars-training/lab-automation-by-wwt-with-ansible-tower-and-cisco-dna-center) 18 | 19 | * AnsibleFest 2019 Presentation 20 | 21 | [DO I CHOOSE ANSIBLE, DNA CENTER OR BOTH?](https://www.ansible.com/do-i-choose-ansible-dna-center-or-both) 22 | 23 | * Additional slides providing an overview of the modules can be found here: 24 | 25 | [Ansible DNA Center Modules Overview](https://www.slideshare.net/secret/1l5xe5ORzTN3Uv) 26 | 27 | ### Included Modules 28 | 29 | The documentation can be viewed using `ansible-doc` and will provide all of the details including examples of usage. 30 | 31 | - `dnac_syslog` 32 | - `dnac_snmpv2_credential` 33 | - `dnac_snmp` 34 | - `dnac_ntp` 35 | - `dnac_ippool` 36 | - `dnac_group` 37 | - `dnac_dns` 38 | - `dnac_discovery` 39 | - `dnac_dhcp` 40 | - `dnac_device_role` 41 | - `dnac_device_assign_site` 42 | - `dnac_cli_credential` 43 | - `dnac_activate_credential` 44 | - `dnac_banner` 45 | - `dnac_archive_config` 46 | - `dnac_del_archived_config` 47 | - `dnac_netflow` 48 | - `dnac_timezone` 49 | - `dnac_wireless_ssid` 50 | - `dnac_wireless_provision` 51 | - `dnac_wireless_profile` 52 | 53 | ## Inventory Plugin 54 | 55 | This collection also includes an inventory plugin enabling the use of DNA Center as the source of truth for inventory. 56 | 57 | 1. Install the collection 58 | ```shell 59 | ansible-galaxy collection install wwt.ansible_dnac 60 | ``` 61 | 2. Configure the plugin by creating a file named `dna_center.yml`. This is the plugin configuration file and I usually save it in a directory named `inventory`. 62 | 63 | ```yaml 64 | plugin: dna_center 65 | host: 66 | validate_certs: 67 | use_dnac_mgmt_int: 68 | username: 69 | password: 70 | ``` 71 | 72 | 3. Enable the plugin by editing `ansible.cfg` 73 | 74 | ```ini 75 | [inventory] 76 | enable_plugins = wwt.ansible_dnac.dna_center 77 | ``` 78 | 79 | 4. Validate it works 80 | 81 | ```shell 82 | ansible-inventory -i --graph --ask-vault-pass 83 | ``` 84 | 85 | **Example output:** 86 | 87 | ```shell 88 | @all: 89 | |--@barcelona: 90 | |--@demo_environment: 91 | | |--@data_center_1: 92 | | | |--DC1-Border-INET.campus.local 93 | | | |--DC1-Border-MPLS.campus.local 94 | | | |--csr-atc-integration.campus.local 95 | | | |--dc1-nexus-7702.campus.local 96 | | |--@data_center_2: 97 | |--@fira: 98 | |--@tech_campus: 99 | | |--@bldg_56: 100 | | | |--@dnac: 101 | | | | |--dc1-9300-a.campus.local 102 | | | | |--dc1-9300-b.campus.local 103 | | | | |--dc1-9500-a.campus.local 104 | | | | |--prod-9800wlc-01.campus.local 105 | |--@the_cloud: 106 | | |--@aws: 107 | | | |--FNH-HOSP-0BMT-WLC1A.us-east-2.compute.internal 108 | |--@ungrouped: 109 | /development/wwt/ansible_dnac # 110 | ``` 111 | 112 | ## Geo Lookup Plugin 113 | 114 | This collection includes a lookup plugin which performs a resolution of the location provided to return the latitude and longitude. When adding buildings in DNAC, an address is required as well as the lat/long of that address. In the UI this resolution is performed for you. This plugin provides that functionality in this collection. 115 | 116 | Below is an example task using the `geo` plugin. 117 | 118 | ```yaml 119 | # DNA Center Create Buildings 120 | - name: create buildings 121 | dnac_site: 122 | host: "{{ inventory_hostname }}" 123 | port: '443' 124 | username: "{{ username }}" 125 | password: "{{ password }}" 126 | state: "{{ desired_state }}" 127 | name: "{{ item.name }}" 128 | site_type: "{{ item.site_type }}" 129 | parent_name: "{{ item.parent_name }}" 130 | address: "{{ item.building_address }}" 131 | latitude: "{{ lookup('wwt.ansible_dnac.geo',item.building_address).latitude }}" 132 | longitude: "{{ lookup('wwt.ansible_dnac.geo',item.building_address).longitude }}" 133 | loop: "{{ sites }}" 134 | when: item.site_type == 'building' 135 | ``` 136 | 137 | > **NOTE:** The `geo` lookup plugin is completely optional. Alternatively, you could manually resolve the lat/long and include them in the task. See the `dnac_site` module documentation for more information. 138 | 139 | ## Requirements 140 | 141 | Ansible version 2.9 or later is required for installation using Ansible Collections. 142 | 143 | This solution requires the installation of the following python modules: 144 | 145 | - **geopy** to resolve building addresses and populate lat/long 146 | `pip install geopy` 147 | - **requests** for http requests 148 | `pip install requests` 149 | - **timezonefinder** for resolving the timezone based on physical address 150 | `pip install timezonefinder==3.4.2` 151 | 152 | ## Installation 153 | 154 | These Ansible modules have now been packaged into an Ansible Collection. 155 | 156 | **STEP 1.** Install the `ansible_dnac` collection 157 | 158 | ```shell 159 | ansible-galaxy collection install wwt.ansible_dnac 160 | ``` 161 | 162 | **STEP 2.** Validation that the modules have been installed properly can be performed by executing: 163 | 164 | `ansible-doc wwt.ansible_dnac.dnac_dhcp` 165 | 166 | If the results show the module documentation your installation was successful. 167 | 168 | ```shell 169 | vagrant@ubuntu-xenial:~/ansible-dnac-modules$ ansible-doc dnac_dhcp 170 | > DNAC_DHCP (/home/vagrant/ansible-dnac-modules/dnac_dhcp.py) 171 | 172 | Add or delete DHCP Server(s) in the Cisco DNA Center Design Workflow. The DHCP Severs can be different values \ at different 173 | levels in the group hierarchy. 174 | 175 | OPTIONS (= is mandatory): 176 | 177 | = dhcp_servers 178 | IP address of the DHCP Server to manipulate. 179 | 180 | type: list 181 | ``` 182 | 183 | ## Examples 184 | 185 | The examples below set the common-settings in the DNA Center Design workflow. Additional examples are included in the module documentation. `ansible-doc *module_name*` 186 | 187 | ```yaml 188 | name: test my new module 189 | connection: local 190 | hosts: localhost 191 | gather_facts: false 192 | no_log: true 193 | 194 | collections: 195 | - wwt.ansible_dnac 196 | 197 | tasks: 198 | 199 | - name: set the banner 200 | dnac_banner: 201 | host: 10.253.176.237 202 | port: 443 203 | username: admin 204 | password: ***** 205 | banner_message: "created by a new ansible module for banners" 206 | - name: set the ntp server 207 | dnac_ntp: 208 | host: 10.253.176.237 209 | port: 443 210 | username: admin 211 | password: ***** 212 | ntp_server: 192.168.200.1 213 | - name: set the dhcp server 214 | dnac_dhcp: 215 | host: 10.253.176.237 216 | port: 443 217 | username: admin 218 | password: ***** 219 | dhcp_server: 192.168.200.1 220 | - name: set the dns server and domain name 221 | dnac_dns: 222 | host: 10.253.176.237 223 | port: 443 224 | username: admin 225 | password: ***** 226 | primary_dns_server: 192.168.200.1 227 | secondary_dns_server: 192.168.200.2 228 | domain_name: wwtatc.local 229 | - name: set the syslog server 230 | dnac_syslog: 231 | host: 10.253.176.237 232 | port: 443 233 | username: admin 234 | password: ***** 235 | syslog_server: 172.31.3.237 236 | - name: set the snmp server 237 | dnac_snmp: 238 | host: 10.253.176.237 239 | port: 443 240 | username: admin 241 | password: ***** 242 | snmp_server: 172.31.3.237 243 | - name: set the netflow 244 | dnac_netflow: 245 | host: 10.253.176.237 246 | port: 443 247 | username: admin 248 | password: ***** 249 | netflow_collector: 172.31.3.237 250 | netflow_port: 6007 251 | - name: set the timezone 252 | dnac_timezone: 253 | host: 10.253.176.237 254 | port: 443 255 | username: admin 256 | password: ***** 257 | timezone: America/Chicago 258 | ``` 259 | 260 | ## Author 261 | 262 | **Jeff Andiorio** - World Wide Technology 263 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Ansible Collection - wwt.ansible_dnac 2 | 3 | ## Ansible Modules for DNA Center 4 | 5 | These modules provide declarative and idempotent access to configure the design elements of [Cisco's DNA Center](https://www.cisco.com/c/en/us/products/cloud-systems.../dna-center/index.html). 6 | 7 | ### DevNet Code Exchange 8 | 9 | This repository is featured on the Cisco DevNet Code Exchange. 10 | 11 | [![published](https://static.production.devnetcloud.com/codeexchange/assets/images/devnet-published.svg)](https://developer.cisco.com/codeexchange/github/repo/jandiorio/ansible-dnac-modules) 12 | 13 | ### Content 14 | 15 | * The webinar below was hosted by Redhat and delivered by Jeff Andiorio of World Wide Technology on 8/7/2018. 16 | 17 | [WWT / Redhat Ansible Webinar](https://www.ansible.com/resources/webinars-training/lab-automation-by-wwt-with-ansible-tower-and-cisco-dna-center) 18 | 19 | * AnsibleFest 2019 Presentation 20 | 21 | [DO I CHOOSE ANSIBLE, DNA CENTER OR BOTH?](https://www.ansible.com/do-i-choose-ansible-dna-center-or-both) 22 | 23 | * Additional slides providing an overview of the modules can be found here: 24 | 25 | [Ansible DNA Center Modules Overview](https://www.slideshare.net/secret/1l5xe5ORzTN3Uv) 26 | 27 | ### Included Modules 28 | 29 | The documentation can be viewed using `ansible-doc` and will provide all of the details including examples of usage. 30 | 31 | - `dnac_syslog` 32 | - `dnac_snmpv2_credential` 33 | - `dnac_snmp` 34 | - `dnac_ntp` 35 | - `dnac_ippool` 36 | - `dnac_group` 37 | - `dnac_dns` 38 | - `dnac_discovery` 39 | - `dnac_dhcp` 40 | - `dnac_device_role` 41 | - `dnac_device_assign_site` 42 | - `dnac_cli_credential` 43 | - `dnac_activate_credential` 44 | - `dnac_banner` 45 | - `dnac_archive_config` 46 | - `dnac_del_archived_config` 47 | - `dnac_netflow` 48 | - `dnac_timezone` 49 | - `dnac_wireless_ssid` 50 | - `dnac_wireless_provision` 51 | - `dnac_wireless_profile` 52 | 53 | ## Inventory Plugin 54 | 55 | This collection also includes an inventory plugin enabling the use of DNA Center as the source of truth for inventory. 56 | 57 | 1. Install the collection 58 | ```shell 59 | ansible-galaxy collection install wwt.ansible.dnac 60 | ``` 61 | 2. Configure the plugin by creating a file named `dna_center.yml`. This is the plugin configuration file and I usually save it in a directory named `inventory`. 62 | 63 | ```yaml 64 | plugin: dna_center 65 | host: 66 | validate_certs: 67 | use_dnac_mgmt_int: 68 | username: 69 | password: 70 | ``` 71 | 72 | 3. Enable the plugin by editing `ansible.cfg` 73 | 74 | ```ini 75 | [inventory] 76 | enable_plugins = wwt.ansible_dnac.dna_center 77 | ``` 78 | 79 | 4. Validate it works 80 | 81 | ```shell 82 | ansible-inventory -i --graph --ask-vault-pass 83 | ``` 84 | 85 | **Example output:** 86 | 87 | ```shell 88 | @all: 89 | |--@barcelona: 90 | |--@demo_environment: 91 | | |--@data_center_1: 92 | | | |--DC1-Border-INET.campus.local 93 | | | |--DC1-Border-MPLS.campus.local 94 | | | |--csr-atc-integration.campus.local 95 | | | |--dc1-nexus-7702.campus.local 96 | | |--@data_center_2: 97 | |--@fira: 98 | |--@tech_campus: 99 | | |--@bldg_56: 100 | | | |--@dnac: 101 | | | | |--dc1-9300-a.campus.local 102 | | | | |--dc1-9300-b.campus.local 103 | | | | |--dc1-9500-a.campus.local 104 | | | | |--prod-9800wlc-01.campus.local 105 | |--@the_cloud: 106 | | |--@aws: 107 | | | |--FNH-HOSP-0BMT-WLC1A.us-east-2.compute.internal 108 | |--@ungrouped: 109 | /development/wwt/ansible_dnac # 110 | ``` 111 | 112 | ## Geo Lookup Plugin 113 | 114 | This collection includes a lookup plugin which performs a resolution of the location provided to return the latitude and longitude. When adding buildings in DNAC, an address is required as well as the lat/long of that address. In the UI this resolution is performed for you. This plugin provides that functionality in this collection. 115 | 116 | Below is an example task using the `geo` plugin. 117 | 118 | ```yaml 119 | # DNA Center Create Buildings 120 | - name: create buildings 121 | dnac_site: 122 | host: "{{ inventory_hostname }}" 123 | port: '443' 124 | username: "{{ username }}" 125 | password: "{{ password }}" 126 | state: "{{ desired_state }}" 127 | name: "{{ item.name }}" 128 | site_type: "{{ item.site_type }}" 129 | parent_name: "{{ item.parent_name }}" 130 | address: "{{ item.building_address }}" 131 | latitude: "{{ lookup('wwt.ansible_dnac.geo',item.building_address).latitude }}" 132 | longitude: "{{ lookup('wwt.ansible_dnac.geo',item.building_address).longitude }}" 133 | loop: "{{ sites }}" 134 | when: item.site_type == 'building' 135 | ``` 136 | 137 | > **NOTE:** The `geo` lookup plugin is completely optional. Alternatively, you could manually resolve the lat/long and include them in the task. See the `dnac_site` module documentation for more information. 138 | 139 | ## Requirements 140 | 141 | Ansible version 2.9 or later is required for installation using Ansible Collections. 142 | 143 | This solution requires the installation of the following python modules: 144 | 145 | - **geopy** to resolve building addresses and populate lat/long 146 | `pip install geopy` 147 | - **requests** for http requests 148 | `pip install requests` 149 | - **timezonefinder** for resolving the timezone based on physical address 150 | `pip install timezonefinder==3.4.2` 151 | 152 | ## Installation 153 | 154 | These Ansible modules have now been packaged into an Ansible Collection. 155 | 156 | **STEP 1.** Install the `ansible_dnac` collection 157 | 158 | ```shell 159 | ansible-galaxy collection install wwt.ansible_dnac 160 | ``` 161 | 162 | **STEP 2.** Validation that the modules have been installed properly can be performed by executing: 163 | 164 | `ansible-doc wwt.ansible_dnac.dnac_dhcp` 165 | 166 | If the results show the module documentation your installation was successful. 167 | 168 | ```shell 169 | vagrant@ubuntu-xenial:~/ansible-dnac-modules$ ansible-doc dnac_dhcp 170 | > DNAC_DHCP (/home/vagrant/ansible-dnac-modules/dnac_dhcp.py) 171 | 172 | Add or delete DHCP Server(s) in the Cisco DNA Center Design Workflow. The DHCP Severs can be different values \ at different 173 | levels in the group hierarchy. 174 | 175 | OPTIONS (= is mandatory): 176 | 177 | = dhcp_servers 178 | IP address of the DHCP Server to manipulate. 179 | 180 | type: list 181 | ``` 182 | 183 | ## Examples 184 | 185 | The examples below set the common-settings in the DNA Center Design workflow. Additional examples are included in the module documentation. `ansible-doc *module_name*` 186 | 187 | ```yaml 188 | name: test my new module 189 | connection: local 190 | hosts: localhost 191 | gather_facts: false 192 | no_log: true 193 | 194 | collections: 195 | - wwt.ansible_dnac 196 | 197 | tasks: 198 | 199 | - name: set the banner 200 | dnac_banner: 201 | host: 10.253.176.237 202 | port: 443 203 | username: admin 204 | password: ***** 205 | banner_message: "created by a new ansible module for banners" 206 | - name: set the ntp server 207 | dnac_ntp: 208 | host: 10.253.176.237 209 | port: 443 210 | username: admin 211 | password: ***** 212 | ntp_server: 192.168.200.1 213 | - name: set the dhcp server 214 | dnac_dhcp: 215 | host: 10.253.176.237 216 | port: 443 217 | username: admin 218 | password: ***** 219 | dhcp_server: 192.168.200.1 220 | - name: set the dns server and domain name 221 | dnac_dns: 222 | host: 10.253.176.237 223 | port: 443 224 | username: admin 225 | password: ***** 226 | primary_dns_server: 192.168.200.1 227 | secondary_dns_server: 192.168.200.2 228 | domain_name: wwtatc.local 229 | - name: set the syslog server 230 | dnac_syslog: 231 | host: 10.253.176.237 232 | port: 443 233 | username: admin 234 | password: ***** 235 | syslog_server: 172.31.3.237 236 | - name: set the snmp server 237 | dnac_snmp: 238 | host: 10.253.176.237 239 | port: 443 240 | username: admin 241 | password: ***** 242 | snmp_server: 172.31.3.237 243 | - name: set the netflow 244 | dnac_netflow: 245 | host: 10.253.176.237 246 | port: 443 247 | username: admin 248 | password: ***** 249 | netflow_collector: 172.31.3.237 250 | netflow_port: 6007 251 | - name: set the timezone 252 | dnac_timezone: 253 | host: 10.253.176.237 254 | port: 443 255 | username: admin 256 | password: ***** 257 | timezone: America/Chicago 258 | ``` 259 | 260 | ## Author 261 | 262 | **Jeff Andiorio** - World Wide Technology 263 | -------------------------------------------------------------------------------- /plugins/modules/dnac_device_assign_site.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2018 World Wide Technology, Inc. 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import absolute_import, division, print_function 6 | __metaclass__ = type 7 | 8 | ANSIBLE_METADATA = { 9 | 'metadata_version': '1.1', 10 | 'status': ['preview'], 11 | 'supported_by': 'community' 12 | } 13 | 14 | DOCUMENTATION = r''' 15 | 16 | module: dnac_device_assign_site 17 | short_description: Assign the device(s) to a site 18 | description: 19 | - Set the device site assignment in the DNA Center Inventory Database. 20 | 21 | version_added: "2.5" 22 | author: "Jeff Andiorio (@jandiorio)" 23 | 24 | options: 25 | host: 26 | description: 27 | - Host is the target Cisco DNA Center controller to execute against. 28 | required: true 29 | 30 | port: 31 | description: 32 | - Port is the TCP port for the HTTP connection. 33 | required: false 34 | default: 443 35 | choices: 36 | - 80 37 | - 443 38 | 39 | username: 40 | description: 41 | - Provide the username for the connection to the Cisco DNA Center Controller. 42 | required: true 43 | 44 | password: 45 | description: 46 | - Provide the password for connection to the Cisco DNA Center Controller. 47 | required: true 48 | 49 | use_proxy: 50 | description: 51 | - Enter a boolean value for whether to use proxy or not. 52 | required: false 53 | default: true 54 | choices: 55 | - true 56 | - false 57 | 58 | use_ssl: 59 | description: 60 | - Enter the boolean value for whether to use SSL or not. 61 | required: false 62 | default: true 63 | choices: 64 | - true 65 | - false 66 | 67 | timeout: 68 | description: 69 | - The timeout provides a value for how long to wait for the executed command complete. 70 | required: false 71 | default: 30 72 | 73 | validate_certs: 74 | description: 75 | - Specify if verifying the certificate is desired. 76 | required: false 77 | default: true 78 | choices: 79 | - true 80 | - false 81 | 82 | state: 83 | description: 84 | - State provides the action to be executed using the terms present, absent, etc. 85 | required: false 86 | default: present 87 | choices: 88 | - present 89 | - absent 90 | 91 | device_name: 92 | description: 93 | - name of the device in the inventory database that you would like to update 94 | required: false 95 | 96 | device_mgmt_ip: 97 | description: 98 | - Management IP Address of the device you would like to update 99 | required: false 100 | 101 | group_name: 102 | description: 103 | - Short group name to assign the site to. 104 | required: false 105 | 106 | group_name_hierarchy: 107 | description: 108 | - fully qualified group hierarchy to assign the site to. 109 | required: false 110 | 111 | notes: 112 | - Either device_name or device_mgmt_ip is required, but not both. 113 | 114 | ''' 115 | 116 | EXAMPLES = r''' 117 | 118 | - name: add device to site 119 | dnac_device_assign_site: 120 | host: "{{host}}" 121 | port: 443 122 | state: present 123 | username: "{{username}}" 124 | password: "{{password}}" 125 | device_mgmt_ip: 192.168.200.1 126 | group_name_hierarchy: "Global/Central/ATC56" 127 | 128 | - name: update device site assignment 129 | dnac_device_assign_site: 130 | host: "{{host}}" 131 | port: 443 132 | state: update 133 | username: "{{username}}" 134 | password: "{{password}}" 135 | device_mgmt_ip: 192.168.200.1 136 | group_name_hierarchy: "Global/Central/ATC56" 137 | 138 | - name: remove device site assignment 139 | dnac_device_assign_site: 140 | host: "{{host}}" 141 | port: 443 142 | state: update 143 | username: "{{username}}" 144 | password: "{{password}}" 145 | device_mgmt_ip: 192.168.200.1 146 | group_name_hierarchy: "Global/Central/ATC56/Floor-1" 147 | 148 | ''' 149 | 150 | RETURN = r''' 151 | # 152 | ''' 153 | 154 | from ansible.module_utils.basic import AnsibleModule 155 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 156 | 157 | 158 | def main(): 159 | 160 | payload = '' 161 | module_args = dnac_argument_spec 162 | module_args.update( 163 | device_name=dict(type='str', required=False), 164 | device_mgmt_ip=dict(type='str', required=False), 165 | group_name=dict(type='str', required=False), 166 | group_name_hierarchy=dict(type='str', required=False) 167 | ) 168 | 169 | result = dict( 170 | changed=False, 171 | original_message='', 172 | message='') 173 | 174 | module = AnsibleModule(argument_spec=module_args, 175 | supports_check_mode=False 176 | ) 177 | 178 | # Instantiate the DnaCenter class object 179 | dnac = DnaCenter(module) 180 | 181 | # Get device details based on either the Management IP or the Name Provided 182 | if module.params['device_mgmt_ip'] is not None: 183 | dnac.api_path = 'api/v1/network-device?managementIpAddress=' + module.params['device_mgmt_ip'] 184 | elif module.params['device_name'] is not None: 185 | dnac.api_path = 'api/v1/network-device?hostname=' + module.params['device_name'] 186 | 187 | device_results = dnac.get_obj() 188 | 189 | try: 190 | device_id = device_results['response'][0]['id'] 191 | except IndexError: 192 | module.fail_json(msg='Unable to find device with supplied information.') 193 | 194 | # get the group id 195 | if module.params['group_name'] is not None: 196 | dnac.api_path = 'api/v1/group?groupName=' + module.params['group_name'] 197 | elif module.params['group_name_hierarchy'] is not None: 198 | dnac.api_path = 'api/v1/group?groupNameHierarchy=' + module.params['group_name_hierarchy'] 199 | 200 | # dnac.api_path = 'api/v1/group?groupName=' + module.params['group_name'] 201 | group_results = dnac.get_obj() 202 | 203 | try: 204 | group_id = group_results['response'][0]['id'] 205 | except IndexError: 206 | module.fail_json(msg='Unable to find group with the supplied information.') 207 | 208 | # check if the device is already a member of that group 209 | # 1.2 ??? dnac.api_path = 'api/v1/member/group?groupType=SITE&id=' + device_id 210 | dnac.api_path = 'api/v1/member/group?groupType=SITE&id=' + device_id 211 | group_assignment = dnac.get_obj() 212 | 213 | payload = {'networkdevice': [device_id]} 214 | dnac.api_path = 'api/v1/group/' + group_id + '/member' 215 | 216 | if module.params['state'] == 'present': 217 | if len(group_assignment['response'][device_id]) > 0: 218 | if group_assignment['response'][device_id][0]['id'] == group_id: 219 | result['changed'] = False 220 | module.exit_json(msg='Device assigned to the correct group.', **result) 221 | else: 222 | result['changed'] = False 223 | module.fail_json(msg='Device is already assigned to another group. Use update as the state.', **result) 224 | else: 225 | dnac.create_obj(payload) 226 | elif module.params['state'] == 'absent': 227 | if not group_assignment['response'][device_id][0]: 228 | module.fail_json(msg='Device is not assigned to a group. Cannot remove assignment') 229 | elif group_assignment['response'][device_id][0]['id'] != group_id: 230 | module.fail_json(msg='Device is not assigned to the group provided. \ 231 | Device is currently in group:' + group_assignment['response'][device_id][0]['groupNameHierarchy']) 232 | else: 233 | dnac.delete_obj(device_id) 234 | elif module.params['state'] == 'update': 235 | if not len(group_assignment['response'][device_id]) > 0: 236 | # if not group_assignment['response'][device_id][0]: 237 | dnac.create_obj(payload) 238 | elif group_assignment['response'][device_id][0]['id'] == group_id: 239 | result['changed'] = False 240 | module.exit_json(msg='Device already assigned to the target group. No changes required.') 241 | elif group_assignment['response'][device_id][0]['id'] != group_id: 242 | _current_group_id = group_assignment['response'][device_id][0]['id'] 243 | dnac.api_path = 'api/v1/group/' + _current_group_id + '/member' 244 | dnac.delete_obj(device_id) 245 | 246 | # change the API to the new group 247 | dnac.api_path = 'api/v1/group/' + group_id + '/member' 248 | dnac.create_obj(payload) 249 | 250 | 251 | if __name__ == "__main__": 252 | main() 253 | -------------------------------------------------------------------------------- /plugins/modules/dnac_wireless_profile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import, division, print_function 5 | __metaclass__ = type 6 | 7 | ANSIBLE_METADATA = {'metadata_version': '1.0', 8 | 'status': ['preview'], 9 | 'supported_by': 'jeff andiorio'} 10 | 11 | DOCUMENTATION = r''' 12 | --- 13 | module: dnac_wireless_profile.py 14 | short_description: Add or Delete sites in DNA Center 15 | description: Add or delete sites in the network hierarchy within Cisco DNA Center controller. 16 | version_added: "2.8" 17 | author: 18 | - Jeff Andiorio (@jandiorio) 19 | 20 | requirements: 21 | - requests 22 | 23 | options: 24 | host: 25 | description: 26 | - Host is the target Cisco DNA Center controller to execute against. 27 | required: true 28 | 29 | port: 30 | description: 31 | - Port is the TCP port for the HTTP connection. 32 | required: false 33 | default: 443 34 | choices: 35 | - 80 36 | - 443 37 | 38 | username: 39 | description: 40 | - Provide the username for the connection to the Cisco DNA Center Controller. 41 | required: true 42 | 43 | password: 44 | description: 45 | - Provide the password for connection to the Cisco DNA Center Controller. 46 | required: true 47 | 48 | use_proxy: 49 | description: 50 | - Enter a boolean value for whether to use proxy or not. 51 | required: false 52 | default: true 53 | choices: 54 | - true 55 | - false 56 | 57 | use_ssl: 58 | description: 59 | - Enter the boolean value for whether to use SSL or not. 60 | required: false 61 | default: true 62 | choices: 63 | - true 64 | - false 65 | 66 | timeout: 67 | description: 68 | - The timeout provides a value for how long to wait for the executed command complete. 69 | required: false 70 | default: 30 71 | 72 | validate_certs: 73 | description: 74 | - Specify if verifying the certificate is desired. 75 | required: false 76 | default: true 77 | choices: 78 | - true 79 | - false 80 | 81 | state: 82 | description: 83 | - State provides the action to be executed using the terms present, absent, etc. 84 | required: false 85 | default: present 86 | choices: 87 | - present 88 | - absent 89 | 90 | name: 91 | description: 92 | - Name of the profile. 93 | required: true 94 | 95 | sites: 96 | description: 97 | - list of sites to associate profile to (Global/Central/Maryland Heights) 98 | required: false 99 | 100 | ssid_name: 101 | description: 102 | - name of the SSID to associate with the profile 103 | required: false 104 | 105 | ssid_type: 106 | description: 107 | - type of SSID you are associating 108 | required: false 109 | choices: 110 | - Enterprise 111 | - Guest 112 | default: Enterprise 113 | 114 | fabric_enabled: 115 | description: Cisco SD Access Fabric Wireless 116 | required: false 117 | default: false 118 | 119 | flexconnect: 120 | description: 121 | - is it a flexconnect profile 122 | required: false 123 | default: false 124 | 125 | flexconnect_vlan: 126 | description: 127 | - vlan number for flexconnect 128 | required: false 129 | 130 | interface: 131 | description: 132 | - interface for wireless management 133 | required: false 134 | 135 | ''' 136 | 137 | EXAMPLES = r''' 138 | 139 | - name: wireless profile 140 | dnac_wireless_profile: 141 | host: "{{ inventory_hostname }}" 142 | port: '443' 143 | username: "{{ username }}" 144 | password: "{{ password }}" 145 | state: present 146 | name: Site-1-Profile 147 | sites: 148 | - "Global/Central/Maryland Heights" 149 | ssid_name: SSID-1 150 | ssid_type: Enterprise 151 | flexconnect: true 152 | flexconnect_vlan: '30' 153 | fabric_enabled: false 154 | interface: '' 155 | 156 | ''' 157 | 158 | 159 | RETURN = r''' 160 | 161 | proposed_config: 162 | description: 163 | - the json payload data being proposed 164 | 165 | orig_config: 166 | description: 167 | - the json payload data of the existing profile if exists 168 | ''' 169 | 170 | 171 | from ansible.module_utils.basic import AnsibleModule 172 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 173 | 174 | 175 | def main(): 176 | _profile_exists = False 177 | 178 | module_args = dnac_argument_spec 179 | module_args.update( 180 | state=dict(type='str', choices=['absent', 'present']), 181 | name=dict(type='str', required=True), 182 | sites=dict(type='list', required=False), 183 | ssid_name=dict(type='str', required=False), 184 | ssid_type=dict(type='str', 185 | required=False, 186 | default='Enterprise', 187 | choices=['Guest', 'Enterprise']), 188 | fabric_enabled=dict(type='bool', required=False, default=False), 189 | flexconnect=dict(type='bool', required=False, default=False), 190 | flexconnect_vlan=dict(type='str', required=False), 191 | interface=dict(type='str', required=False) 192 | ) 193 | 194 | result = dict( 195 | changed=False, 196 | original_message='', 197 | message='', 198 | orig_config='', 199 | proposed_config='') 200 | 201 | module = AnsibleModule( 202 | argument_spec=module_args, 203 | supports_check_mode=False 204 | ) 205 | 206 | # build the required payload data structure 207 | payload = { 208 | "profileDetails": { 209 | "name": module.params['name'], 210 | "sites": module.params['sites'], 211 | } 212 | } 213 | # If ssid information is provided add to the payload 214 | if module.params['ssid_name'] and module.params['ssid_type']: 215 | payload['profileDetails'].update( 216 | {"ssidDetails": [ 217 | { 218 | "name": module.params['ssid_name'], 219 | "type": module.params['ssid_type'], 220 | "enableFabric": module.params['fabric_enabled'], 221 | } 222 | ] 223 | } 224 | ) 225 | if module.params['interface']: 226 | payload.update( 227 | {"interfaceName": module.params['interface']} 228 | ) 229 | # If Flexconnect is in play, add flexconnect variables 230 | if module.params['flexconnect']: 231 | flexconnect = { 232 | "flexConnect": { 233 | "enableFlexConnect": module.params['flexconnect'], 234 | "localToVlan": module.params['flexconnect_vlan'] 235 | } 236 | } 237 | else: 238 | flexconnect = { 239 | "flexConnect": { 240 | "enableFlexConnect": module.params['flexconnect']}} 241 | 242 | payload['profileDetails']['ssidDetails'][0].update(flexconnect) 243 | 244 | # Instantiate the DnaCenter class object 245 | dnac = DnaCenter(module) 246 | dnac.api_path = 'dna/intent/api/v1/wireless/profile' 247 | 248 | dnac.result = result 249 | # check if the configuration is already in the desired state 250 | 251 | # get the SSIDs 252 | profiles = dnac.get_obj() 253 | 254 | result['orig_config'] = [profile for profile in profiles 255 | if profile['profileDetails']['name'] == module.params['name']] 256 | result['proposed_config'] = payload 257 | 258 | if len(profiles) > 0: 259 | _profile_names = [profile['profileDetails']['name'] for profile in profiles] 260 | else: 261 | _profile_names = [] 262 | 263 | # does pool provided exist 264 | if module.params['name'] in _profile_names: 265 | _profile_exists = True 266 | else: 267 | _profile_exists = False 268 | 269 | # actions 270 | if module.params['state'] == 'present' and _profile_exists: 271 | orig_config = dnac.result['orig_config'] 272 | proposed_config = dnac.result['proposed_config'] 273 | 274 | if len(orig_config) > 0: 275 | del(orig_config[0]['profileDetails']['instanceUuid']) 276 | if orig_config == proposed_config: 277 | result['changed'] = False 278 | module.exit_json(msg='Wireless Profile already exists.', **result) 279 | else: 280 | dnac.update_obj(proposed_config) 281 | dnac.result['changed'] = True 282 | module.exit_json(msg='Updated Wireless Profile.', **dnac.result) 283 | else: 284 | dnac.result['changed'] = True 285 | dnac.update_obj(proposed_config) 286 | 287 | # result['changed'] = False 288 | # module.exit_json(msg='Wireless Profile already exists.', **result) 289 | 290 | elif module.params['state'] == 'present' and not _profile_exists: 291 | dnac.create_obj(payload) 292 | elif module.params['state'] == 'absent' and _profile_exists: 293 | # Create payload of existing profile 294 | payload = [profile for profile in profiles if profile['profileDetails']['name'] == module.params['name']] 295 | # Remove Site Assignment 296 | payload[0]['profileDetails']['sites'] = [] 297 | dnac.update_obj(payload[0]) 298 | # Delete the Wireless Profile 299 | dnac.delete_obj(module.params['name']) 300 | 301 | elif module.params['state'] == 'absent' and not _profile_exists: 302 | result['changed'] = False 303 | module.exit_json(msg='Wireless Profile Does not exist. Cannot delete.', **result) 304 | 305 | 306 | if __name__ == "__main__": 307 | main() 308 | -------------------------------------------------------------------------------- /plugins/modules/dnac_site.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2019 World Wide Technology, Inc. 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl- 4 | 5 | from __future__ import absolute_import, division, print_function 6 | 7 | ANSIBLE_METADATA = { 8 | 'metadata_version': '1.0', 9 | 'status': ['preview'], 10 | 'supported_by': 'community' 11 | } 12 | 13 | DOCUMENTATION = r''' 14 | --- 15 | module: dnac_site 16 | short_description: Add or Delete sites in DNA Center 17 | description: Add or delete sites in the network hierarchy within Cisco DNA Center controller. 18 | version_added: "2.8" 19 | author: 20 | - Jeff Andiorio (@jandiorio) 21 | 22 | requirements: 23 | - requests 24 | 25 | options: 26 | host: 27 | description: 28 | - Host is the target Cisco DNA Center controller to execute against. 29 | required: true 30 | 31 | port: 32 | description: 33 | - Port is the TCP port for the HTTP connection. 34 | required: false 35 | default: 443 36 | choices: 37 | - 80 38 | - 443 39 | 40 | username: 41 | description: 42 | - Provide the username for the connection to the Cisco DNA Center Controller. 43 | required: true 44 | 45 | password: 46 | description: 47 | - Provide the password for connection to the Cisco DNA Center Controller. 48 | required: true 49 | 50 | use_proxy: 51 | description: 52 | - Enter a boolean value for whether to use proxy or not. 53 | required: false 54 | default: true 55 | choices: 56 | - true 57 | - false 58 | 59 | use_ssl: 60 | description: 61 | - Enter the boolean value for whether to use SSL or not. 62 | required: false 63 | default: true 64 | choices: 65 | - true 66 | - false 67 | 68 | timeout: 69 | description: 70 | - The timeout provides a value for how long to wait for the executed command complete. 71 | required: false 72 | default: 30 73 | 74 | validate_certs: 75 | description: 76 | - Specify if verifying the certificate is desired. 77 | required: false 78 | default: true 79 | choices: 80 | - true 81 | - false 82 | 83 | state: 84 | description: 85 | - State provides the action to be executed using the terms present, absent, etc. 86 | required: false 87 | default: present 88 | choices: 89 | - present 90 | - absent 91 | 92 | name: 93 | description: 94 | - Name of the site. 95 | required: true 96 | 97 | site_type: 98 | description: 99 | - type of site 100 | required: true 101 | default: area 102 | choices: 103 | - area 104 | - building 105 | - floor 106 | 107 | parent_name: 108 | description: 109 | - name of the containing site 110 | required: true 111 | default: Global 112 | 113 | address: 114 | description: 115 | - site address of the building 116 | required: false 117 | 118 | latitude: 119 | description: latitude of the building 120 | required: false 121 | 122 | longitude: 123 | description: 124 | - longitude of the building 125 | required: false 126 | 127 | rf_model: 128 | description: 129 | - rf model for the floor 130 | choices: 131 | - 'Cubes And Walled Offices' 132 | - 'Drywall Office Only' 133 | - 'Indoor High Ceiling' 134 | - 'Outdoor Open Space]' 135 | required: false 136 | 137 | width: 138 | description: 139 | - width of the floor 140 | required: false 141 | 142 | length: 143 | description: 144 | - length of the floor 145 | required: false 146 | 147 | height: 148 | description: 149 | - height of the ceiling for the floor 150 | required: false 151 | ''' 152 | 153 | EXAMPLES = r''' 154 | 155 | - name: create areas 156 | dnac_site: 157 | host: "{{ inventory_hostname }}" 158 | port: '443' 159 | username: "{{ username }}" 160 | password: "{{ password }}" 161 | state: "{{ desired_state }}" 162 | name: Site-1 163 | site_type: area 164 | parent_name: Global 165 | 166 | - name: create buildings 167 | dnac_site: 168 | host: "{{ inventory_hostname }}" 169 | port: '443' 170 | username: "{{ username }}" 171 | password: "{{ password }}" 172 | state: "{{ desired_state }}" 173 | name: Building-1 174 | site_type: building 175 | parent_name: Site-a 176 | address: 1 World Wide Way, St Louis, Mo 177 | latitude: 38.540450 178 | longitude: -90.443660 179 | 180 | - name: create floors 181 | dnac_site: 182 | host: "{{ inventory_hostname }}" 183 | port: '443' 184 | username: "{{ username }}" 185 | password: "{{ password }}" 186 | state: "{{ desired_state }}" 187 | name: Floor-1 188 | site_type: floor 189 | parent_name: Building-1 190 | rf_model: 'Cubes And Walled Offices' 191 | height: 10 192 | width: 100 193 | length: 200 194 | 195 | ''' 196 | 197 | 198 | RETURN = r''' 199 | # 200 | ''' 201 | 202 | from ansible.module_utils.basic import AnsibleModule 203 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 204 | 205 | 206 | __metaclass__ = type 207 | 208 | 209 | def main(): 210 | _site_exists = False 211 | _parent_exists = False 212 | 213 | module_args = dnac_argument_spec 214 | module_args.update( 215 | state=dict(type='str', default='present', choices=['absent', 'present', 'update']), 216 | name=dict(type='str', required=True), 217 | site_type=dict(type='str', default='area', choices=['area', 'building', 'floor']), 218 | parent_name=dict(type='str', default='Global'), 219 | address=dict(type='str'), 220 | latitude=dict(type='str', required=False), 221 | longitude=dict(type='str', required=False), 222 | rf_model=dict(type='str', choices=['Cubes And Walled Offices', 223 | 'Drywall Office Only', 224 | 'Indoor High Ceiling', 225 | 'Outdoor Open Space]']), 226 | width=dict(type='str', required=False), 227 | length=dict(type='str', required=False), 228 | height=dict(type='str', required=False), 229 | ) 230 | 231 | result = dict( 232 | changed=False, 233 | original_message='', 234 | message='') 235 | 236 | module = AnsibleModule( 237 | argument_spec=module_args, 238 | supports_check_mode=False 239 | ) 240 | 241 | # Instantiate the DnaCenter class object 242 | dnac = DnaCenter(module) 243 | dnac.api_path = 'api/v1/group' 244 | 245 | # Get the sites 246 | sites = dnac.get_obj() 247 | try: 248 | _site_names = [site['name'] for site in sites['response']] 249 | except TypeError: 250 | module.fail_json(msg=sites) 251 | 252 | # does site provided exist 253 | if module.params['name'] in _site_names: 254 | _site_exists = True 255 | else: 256 | _site_exists = False 257 | 258 | # does parent provided exist 259 | if module.params['parent_name'] in _site_names or module.params['parent_name'] == 'Global': 260 | _parent_exists = True 261 | else: 262 | _parent_exists = False 263 | module.fail_json(msg='Parent Site does not exist...') 264 | 265 | # Obtain Parent groupNameHierarchy 266 | if module.params['parent_name'] == "Global": 267 | parent_hierarchy = "Global" 268 | else: 269 | parent_hierarchy = [site['groupNameHierarchy'] 270 | for site in sites['response'] 271 | if site['name'] == module.params['parent_name']][0] 272 | 273 | # build the required payload data structure 274 | if module.params['site_type'] == 'area': 275 | payload = { 276 | "type": "area", 277 | "site": { 278 | "area": { 279 | "name": module.params['name'], 280 | "parentName": parent_hierarchy 281 | } 282 | } 283 | } 284 | 285 | elif module.params['site_type'] == 'building': 286 | payload = { 287 | "type": "building", 288 | "site": { 289 | "building": { 290 | "name": module.params['name'], 291 | "address": module.params['address'], 292 | "parentName": parent_hierarchy, 293 | "latitude": module.params['latitude'], 294 | "longitude": module.params['longitude'] 295 | } 296 | } 297 | } 298 | elif module.params['site_type'] == 'floor': 299 | payload = { 300 | "type": "floor", 301 | "site": { 302 | "floor": { 303 | "name": module.params['name'], 304 | "parentName": parent_hierarchy, 305 | "rfModel": module.params['rf_model'], 306 | "width": module.params['width'], 307 | "length": module.params['length'], 308 | "height": module.params['height'] 309 | } 310 | } 311 | } 312 | 313 | # Do the stuff 314 | dnac.api_path = 'dna/intent/api/v1/site' 315 | if module.params['state'] == 'present' and _site_exists: 316 | result['changed'] = False 317 | result['intended_payload'] = payload 318 | module.exit_json(msg='Site already exists.', **result) 319 | elif module.params['state'] == 'present' and not _site_exists: 320 | dnac.create_obj(payload) 321 | elif module.params['state'] == 'absent' and _site_exists: 322 | _site_id = [site['id'] for site in sites['response'] if site['name'] == module.params['name']] 323 | dnac.delete_obj(_site_id[0]) 324 | elif module.params['state'] == 'absent' and not _site_exists: 325 | result['changed'] = False 326 | module.exit_json(msg='Site Does not exist. Cannot delete.', **result) 327 | 328 | 329 | if __name__ == "__main__": 330 | main() 331 | -------------------------------------------------------------------------------- /plugins/modules/dnac_discovery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2018 World Wide Technology, Inc. 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import absolute_import, division, print_function 6 | __metaclass__ = type 7 | 8 | ANSIBLE_METADATA = { 9 | 'metadata_version': '1.1', 10 | 'status': ['preview'], 11 | 'supported_by': 'community' 12 | } 13 | 14 | DOCUMENTATION = r''' 15 | --- 16 | 17 | module: dnac_discovery 18 | short_description: Create network discovery jobs. 19 | description: Create network discovery jobs to populate the DNA Center Inventory Database. 20 | 21 | version_added: "2.5" 22 | author: 23 | - Jeff Andiorio (@jandiorio) 24 | 25 | options: 26 | host: 27 | description: 28 | - Host is the target Cisco DNA Center controller to execute against. 29 | required: true 30 | 31 | port: 32 | description: 33 | - Port is the TCP port for the HTTP connection. 34 | required: false 35 | default: 443 36 | choices: 37 | - 80 38 | - 443 39 | 40 | username: 41 | description: 42 | - Provide the username for the connection to the Cisco DNA Center Controller. 43 | required: true 44 | 45 | password: 46 | description: 47 | - Provide the password for connection to the Cisco DNA Center Controller. 48 | required: true 49 | 50 | use_proxy: 51 | description: 52 | - Enter a boolean value for whether to use proxy or not. 53 | required: false 54 | default: true 55 | choices: 56 | - true 57 | - false 58 | 59 | use_ssl: 60 | description: 61 | - Enter the boolean value for whether to use SSL or not. 62 | required: false 63 | default: true 64 | choices: 65 | - true 66 | - false 67 | 68 | timeout: 69 | description: 70 | - The timeout provides a value for how long to wait for the executed command complete. 71 | required: false 72 | default: 30 73 | 74 | validate_certs: 75 | description: 76 | - Specify if verifying the certificate is desired. 77 | required: false 78 | default: true 79 | choices: 80 | - true 81 | - false 82 | 83 | state: 84 | description: 85 | - State provides the action to be executed using the terms present, absent, etc. 86 | required: false 87 | default: present 88 | choices: 89 | - present 90 | - absent 91 | 92 | discovery_name: 93 | description: 94 | - A name for the discovery. 95 | required: true 96 | alias: name 97 | type: string 98 | discovery_type: 99 | description: 100 | - Type of Discovery. Either Range or CDP. 101 | type: string 102 | choices: 103 | - Range 104 | - CDP 105 | required: true 106 | discovery_cdp_level: 107 | description: 108 | - If type is CDP, a cdp level of depth is needed. This is how many hops away from the seed device. 109 | alias: cdp_level 110 | type: string 111 | required: false 112 | discovery_preferred_ip_method: 113 | description: 114 | - Specify to use the Loopback for management or not. 115 | alias: preferred_ip_method 116 | type: string 117 | choices: 118 | - None 119 | - UseLoopBack 120 | required: false 121 | 122 | discovery_ip_filter_list: 123 | description: 124 | - A string of IP addresses to exclude from the discovery. (Example 192.168.200.1-192.168.200.100) 125 | type: string 126 | required: false 127 | 128 | discovery_ip_address_list: 129 | description: 130 | - A string of IP addresses to include in the discovery. (Example 192.168.200.101-192.168.200.200) 131 | type: string 132 | required: false 133 | 134 | global_cli_cred: 135 | description: 136 | - The CLI Username to utilize during discovery. 137 | type: string 138 | required: true 139 | 140 | global_snmp_cred: 141 | description: 142 | - The SNMP credential to use for discovery. 143 | type: string 144 | required: true 145 | 146 | netconf_port: 147 | description: 148 | - Netconf Port to use for discovery 149 | type: string 150 | required: false 151 | 152 | ''' 153 | 154 | EXAMPLES = r''' 155 | 156 | --- 157 | 158 | - name: create discovery 159 | dnac_discovery: 160 | host: "{{host}}" 161 | port: "{{port}}" 162 | username: "{{username}}" 163 | password: "{{password}}" 164 | state: present 165 | discovery_name: test-1 166 | discovery_type: Range 167 | discovery_preferred_ip_method: UseLoopBack 168 | discovery_ip_addr_list: 192.168.90.50-192.168.90.50 169 | global_cli_cred: wwt 170 | global_snmp_cred: SNMP-RW 171 | 172 | ''' 173 | 174 | RETURN = r''' 175 | 176 | ''' 177 | 178 | from ansible.module_utils.basic import AnsibleModule 179 | from ansible_collections.wwt.ansible_dnac.plugins.module_utils.network.dnac.dnac import DnaCenter, dnac_argument_spec 180 | 181 | 182 | def main(): 183 | _discovery_exists = False 184 | payload = '' 185 | module_args = dnac_argument_spec 186 | module_args.update( 187 | 188 | discovery_name=dict(alias='name', type='str', required=True), 189 | discovery_type=dict(type='str', required=True, choices=['CDP', 'Range']), 190 | discovery_cdp_level=dict(alias='cdp_level', type='str', required=False), 191 | discovery_preferred_ip_method=dict( 192 | alias='preferred_ip_method', 193 | type='str', required=False, 194 | default='None', 195 | choices=['None', 'UseLoopBack'] 196 | ), 197 | discovery_ip_filter_list=dict(type='str', required=False), 198 | discovery_ip_addr_list=dict(type='str', required=True), 199 | global_cli_cred=dict(type='str', required=True), 200 | global_snmp_cred=dict(type='str', required=True), 201 | netconf_port=dict(type='str', required=False), 202 | rediscovery=dict(type='bool', required=False, default=False) 203 | ) 204 | 205 | result = dict( 206 | changed=False, 207 | original_message='', 208 | message='') 209 | 210 | module = AnsibleModule( 211 | argument_spec=module_args, 212 | supports_check_mode=False 213 | ) 214 | 215 | # Instantiate the DnaCenter class object 216 | dnac = DnaCenter(module) 217 | 218 | # build the required payload data structure 219 | 220 | # lookup global credentials. 221 | dnac.api_path = 'api/v1/global-credential?credentialSubType=CLI' 222 | cli_cred = dnac.get_obj() 223 | for cli in cli_cred['response']: 224 | if cli['username'] == module.params['global_cli_cred']: 225 | cli_id = cli['id'] 226 | 227 | dnac.api_path = 'api/v1/global-credential?credentialSubType=SNMPV2_WRITE_COMMUNITY' 228 | snmp_cred = dnac.get_obj() 229 | for snmp in snmp_cred['response']: 230 | if snmp['description'] == module.params['global_snmp_cred']: 231 | snmp_id = snmp['id'] 232 | 233 | payload = { 234 | "preferredMgmtIPMethod": module.params['discovery_preferred_ip_method'], 235 | "name": module.params['discovery_name'], 236 | "cdpLevel": module.params['discovery_cdp_level'], 237 | "globalCredentialIdList": [ 238 | cli_id, 239 | snmp_id 240 | ], 241 | "ipFilterList": module.params['discovery_ip_filter_list'], 242 | "ipAddressList": module.params['discovery_ip_addr_list'], 243 | "discoveryType": module.params['discovery_type'], 244 | "protocolOrder": "ssh", 245 | "retry": 3, 246 | "timeout": 5, 247 | "lldpLevel": "16", 248 | "netconfPort": module.params['netconf_port'], 249 | "rediscovery": module.params['rediscovery'] 250 | 251 | } 252 | 253 | ''' 254 | { 255 | "preferredMgmtIPMethod": module.params['discovery_preferred_ip_method'], 256 | "name": module.params['discovery_name'], 257 | "snmpROCommunityDesc": "", 258 | "snmpRWCommunityDesc": "", 259 | "parentDiscoveryId": "", 260 | "globalCredentialIdList": [ 261 | "" 262 | ], 263 | "httpReadCredential": { 264 | "port": 0, 265 | "password": "", 266 | "username": "", 267 | "secure": false, 268 | "description": "", 269 | "credentialType": "", 270 | "comments": "", 271 | "instanceUuid": "", 272 | "id": "" 273 | }, 274 | "httpWriteCredential": { 275 | "port": 0, 276 | "password": "", 277 | "username": "", 278 | "secure": false, 279 | "description": "", 280 | "credentialType": "", 281 | "comments": "", 282 | "instanceUuid": "", 283 | "id": "" 284 | }, 285 | "snmpUserName": "", 286 | "snmpMode": "", 287 | "netconfPort": "", 288 | "cdpLevel": 0, 289 | "enablePasswordList": [ 290 | "" 291 | ], 292 | "ipFilterList": [ 293 | "" 294 | ], 295 | "passwordList": [ 296 | "" 297 | ], 298 | "protocolOrder": "", 299 | "reDiscovery": false, 300 | "retry": 0, 301 | "snmpAuthPassphrase": "", 302 | "snmpAuthProtocol": "", 303 | "snmpPrivPassphrase": "", 304 | "snmpPrivProtocol": "", 305 | "snmpROCommunity": "", 306 | "snmpRWCommunity": "", 307 | "userNameList": [ 308 | "" 309 | ], 310 | "ipAddressList": "", 311 | "snmpVersion": "", 312 | "timeout": 0, 313 | "discoveryType": "" 314 | } 315 | ''' 316 | 317 | # Get the discoveries 318 | dnac.api_path = 'api/v1/discovery' 319 | discoveries = dnac.get_obj() 320 | 321 | _discovery_names = [discovery['name'] for discovery in discoveries['response']] 322 | 323 | # does discovery provided exist 324 | if module.params['discovery_name'] in _discovery_names: 325 | _discovery_exists = True 326 | _discovery_id = [d['id'] for d in discoveries['response'] if d['name'] == module.params['discovery_name']][0] 327 | else: 328 | _discovery_exists = False 329 | 330 | # actions 331 | if module.params['state'] == 'present' and _discovery_exists and module.params['rediscovery']: 332 | result['changed'] = True 333 | dnac.api_path = 'api/v1/discovery' 334 | payload.update({'id': _discovery_id}) 335 | dnac.update_obj(payload) 336 | module.exit_json(msg='Discovery already exists.', **result) 337 | elif module.params['state'] == 'present' and _discovery_exists: 338 | result['changed'] = False 339 | module.exit_json(msg='Discovery already exists.', **result) 340 | elif module.params['state'] == 'present' and not _discovery_exists: 341 | dnac.create_obj(payload) 342 | 343 | elif module.params['state'] == 'absent' and _discovery_exists: 344 | _discovery_id = [discovery['id'] for discovery in discoveries['response'] 345 | if discovery['name'] == module.params['discovery_name']] 346 | dnac.delete_obj(_discovery_id[0]) 347 | 348 | elif module.params['state'] == 'absent' and not _discovery_exists: 349 | result['changed'] = False 350 | module.exit_json(msg='Discovery Does not exist. Cannot delete.', **result) 351 | 352 | 353 | if __name__ == "__main__": 354 | main() 355 | -------------------------------------------------------------------------------- /plugins/inventory/dna_center.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 World Wide Technology 2 | # GNU General Public License v3.0+ 3 | # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | __metaclass__ = type 7 | from ansible.errors import AnsibleError, AnsibleParserError 8 | from ansible.plugins.inventory import BaseInventoryPlugin 9 | 10 | DOCUMENTATION = r''' 11 | name: wwt.ansible_dnac.dna_center 12 | plugin_type: inventory 13 | short_description: Returns Inventory from DNA Center 14 | description: 15 | - Retrieves inventory from DNA Center 16 | - Adds inventory to ansible working inventory 17 | 18 | options: 19 | plugin: 20 | description: Name of the plugin 21 | required: true 22 | choices: ['dna_center'] 23 | host: 24 | description: FQDN of the target host 25 | required: true 26 | username: 27 | description: user credential for target system 28 | required: true 29 | password: 30 | description: user pass for the target system 31 | required: true 32 | validate_certs: 33 | description: certificate validation 34 | required: false 35 | choices: ['yes', 'no'] 36 | use_dnac_mgmt_int: 37 | description: map the dnac mgmt interface to `ansible_host` 38 | required: false 39 | default: true 40 | choices: [true, false] 41 | ''' 42 | 43 | EXAMPLES = r''' 44 | ansible-inventory --graph 45 | 46 | ansible-inventory --list 47 | ''' 48 | 49 | try: 50 | import requests 51 | except ImportError: 52 | raise AnsibleError("Python requests module is required for this plugin.") 53 | 54 | 55 | class InventoryModule(BaseInventoryPlugin): 56 | 57 | NAME = 'dna_center' 58 | 59 | def __init__(self): 60 | super(InventoryModule, self).__init__() 61 | 62 | # from config 63 | self.username = None 64 | self.password = None 65 | self.host = None 66 | self.session = None 67 | self.use_dnac_mgmt_int = None 68 | 69 | # global attributes 70 | self._site_list = None 71 | self._inventory = None 72 | self._host_list = None 73 | 74 | def _login(self): 75 | ''' 76 | :return Login results from the request. 77 | ''' 78 | login_url = 'https://' + self.host + '/dna/system/api/v1/auth/token' 79 | self.session = requests.session() 80 | self.session.auth = self.username, self.password 81 | self.session.verify = False 82 | self.session.headers.update({'Content-Type': 'application/json'}) 83 | 84 | try: 85 | login_results = self.session.post(login_url) 86 | except Exception as e: 87 | raise AnsibleError('failed to login to DNA Center: {}'.format(e)) 88 | 89 | if login_results.status_code not in [200, 201, 202, 203, 204]: 90 | raise AnsibleError('failed to login. \ 91 | Status code was not in the 200s') 92 | else: 93 | self.session.headers.update({'x-auth-token': 94 | login_results.json()['Token']}) 95 | 96 | return login_results 97 | 98 | def _get_inventory(self): 99 | ''' 100 | :return The json output from the request object response. 101 | ''' 102 | 103 | inventory_url = 'https://' + self.host + \ 104 | '/dna/intent/api/v1/network-device' 105 | inventory_results = self.session.get(inventory_url) 106 | 107 | self._inventory = inventory_results.json() 108 | 109 | return inventory_results.json() 110 | 111 | def _get_hosts(self): 112 | ''' 113 | :param inventory A list of dictionaries representing 114 | the entire DNA Center inventory. 115 | :return A List of tuples that include the management IP, 116 | device hostnanme, and the unique indentifier of the device. 117 | ''' 118 | 119 | host_list = [] 120 | 121 | for host in self._inventory['response']: 122 | if host['type'].find('Access Point') == -1: 123 | host_dict = {} 124 | host_dict.update({ 125 | 'managementIpAddress': host['managementIpAddress'], 126 | 'hostname': host['hostname'], 127 | 'id': host['id'], 128 | 'os': host['softwareType'], 129 | 'version': host['softwareVersion'] 130 | }) 131 | host_list.append(host_dict) 132 | 133 | self._host_list = host_list 134 | 135 | return host_list 136 | 137 | def _get_sites(self): 138 | ''' 139 | :return A list of tuples for sites containing the site name 140 | and the unique ID of the site. 141 | ''' 142 | site_url = 'https://' + self.host + \ 143 | '/dna/intent/api/v1/topology/site-topology' 144 | site_results = self.session.get(site_url) 145 | 146 | sites = site_results.json()['response']['sites'] 147 | 148 | site_list = [] 149 | site_dict = {} 150 | 151 | for site in sites: 152 | 153 | site_dict = {} 154 | site_dict.update({'name': site['name'].replace(' ', '_').lower(), 155 | 'id': site['id'], 'parentId': site['parentId']}) 156 | site_list.append(site_dict) 157 | 158 | self._site_list = site_list 159 | 160 | return site_list 161 | 162 | def _get_member_site(self, device_id): 163 | ''' 164 | :param device_id: The unique identifier of the target device. 165 | :return A single string representing the name of the SITE group 166 | of which the device is a member. 167 | ''' 168 | 169 | url = 'https://' + self.host + \ 170 | '/dna/intent/api/v1/topology/physical-topology?nodeType=device' 171 | results = self.session.get(url) 172 | devices = results.json()['response']['nodes'] 173 | 174 | # Get the one device we are looking for 175 | device = [dev for dev in devices if dev['id'] == device_id][0] 176 | 177 | # Extract the siteid from the device data 178 | site_id = device.get('additionalInfo').get('siteid') 179 | 180 | # set the site name from the self._site_list 181 | site_name = [site['name'] for site in self._site_list 182 | if site['id'] == site_id] 183 | 184 | # return the name if it exists 185 | if len(site_name) == 1: 186 | return site_name[0] 187 | elif len(site_name) == 0: 188 | return 'ungrouped' 189 | 190 | def _add_sites(self): 191 | ''' Add groups and associate them with parent groups 192 | :param site_list: list of group dictionaries containing name, id, 193 | parentId 194 | ''' 195 | 196 | # Global is a system group and the parent of all top level groups 197 | site_ids = [ste['id'] for ste in self._site_list] 198 | parent_name = '' 199 | 200 | # Add all sites 201 | for site in self._site_list: 202 | self.inventory.add_group(site['name']) 203 | 204 | # Add parent/child relationship 205 | for site in self._site_list: 206 | 207 | if site['parentId'] in site_ids: 208 | parent_name = [ste['name'] for ste in self._site_list 209 | if ste['id'] == site['parentId']][0] 210 | 211 | try: 212 | self.inventory.add_child(parent_name, site['name']) 213 | except Exception as e: 214 | raise AnsibleParserError('adding child sites failed: {} \n {}:{}'.format(e, site['name'], parent_name)) 215 | 216 | def _add_hosts(self): 217 | """ 218 | Add the devicies from DNAC Inventory to the Ansible Inventory 219 | :param host_list: list of dictionaries for hosts retrieved from 220 | DNAC 221 | 222 | """ 223 | for h in self._host_list: 224 | site_name = self._get_member_site(h['id']) 225 | if site_name: 226 | self.inventory.add_host(h['hostname'], group=site_name) 227 | 228 | # add variables to the hosts 229 | if self.use_dnac_mgmt_int: 230 | self.inventory.set_variable(h['hostname'], 231 | 'ansible_host', 232 | h['managementIpAddress']) 233 | 234 | self.inventory.set_variable(h['hostname'], 'os', h['os']) 235 | self.inventory.set_variable(h['hostname'], 236 | 'version', 237 | h['version']) 238 | if h['os'].lower() in ['ios', 'ios-xe']: 239 | self.inventory.set_variable(h['hostname'], 240 | 'ansible_network_os', 241 | 'ios') 242 | self.inventory.set_variable(h['hostname'], 243 | 'ansible_connection', 244 | 'network_cli') 245 | self.inventory.set_variable(h['hostname'], 246 | 'ansible_become', 247 | 'yes') 248 | self.inventory.set_variable(h['hostname'], 249 | 'ansible_become_method', 250 | 'enable') 251 | elif h['os'].lower() in ['nxos', 'nx-os']: 252 | self.inventory.set_variable(h['hostname'], 253 | 'ansible_network_os', 254 | 'nxos') 255 | self.inventory.set_variable(h['hostname'], 256 | 'ansible_connection', 257 | 'network_cli') 258 | self.inventory.set_variable(h['hostname'], 259 | 'ansible_become', 260 | 'yes') 261 | self.inventory.set_variable(h['hostname'], 262 | 'ansible_become_method', 263 | 'enable') 264 | else: 265 | raise AnsibleError('no site name found for host: {} with site_id {}'.format(h['id'], self._site_list)) 266 | 267 | def verify_file(self, path): 268 | 269 | valid = False 270 | if super(InventoryModule, self).verify_file(path): 271 | if path.endswith(('dnac.yaml', 272 | 'dnac.yml', 273 | 'dna_center.yaml', 274 | 'dna_center.yml')): 275 | valid = True 276 | return valid 277 | 278 | def parse(self, inventory, loader, path, cache=True): 279 | 280 | super(InventoryModule, self).parse(inventory, loader, path, cache) 281 | 282 | # initializes variables read from the config file based on the 283 | # documentation string definition. 284 | # If the options are not defined in the docstring, the are not 285 | # imported from config file 286 | self._read_config_data(path) 287 | 288 | # Set options values from configuration file 289 | try: 290 | self.host = self.get_option('host') 291 | self.username = self.get_option('username') 292 | self.password = self.get_option('password') 293 | self.map_mgmt_ip = self.get_option('use_dnac_mgmt_int') 294 | except Exception as e: 295 | raise AnsibleParserError('getting options failed: {}'.format(e)) 296 | 297 | # Attempt login to DNAC 298 | login_results = self._login() 299 | if login_results.status_code not in [200, 201, 202, 203]: 300 | raise AnsibleError('failed to login: {}'.format(login_results.status_code)) 301 | 302 | # Obtain Inventory Data 303 | self._get_inventory() 304 | 305 | # Add groups to the inventory 306 | self._get_sites() 307 | self._add_sites() 308 | 309 | # Add the hosts to the inventory 310 | self._get_hosts() 311 | self._add_hosts() 312 | -------------------------------------------------------------------------------- /plugins/module_utils/network/dnac/dnac.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import requests 4 | import json 5 | import time 6 | import sys 7 | from geopy.geocoders import Nominatim 8 | from timezonefinder import TimezoneFinder 9 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 10 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 11 | 12 | dnac_argument_spec = dict( 13 | host=dict(required=True, type='str'), 14 | port=dict(required=False, type='str', default='443'), 15 | username=dict(required=True, type='str'), 16 | password=dict(required=True, type='str', no_log=True), 17 | use_proxy=dict(required=False, type='bool', default=True), 18 | use_ssl=dict(type='bool', default=True), 19 | timeout=dict(type='int', default=30), 20 | validate_certs=dict(type='bool', default=False), 21 | state=dict(type='str', default='present', choices=['absent', 'present', 'update', 'query']) 22 | ) 23 | 24 | 25 | class DnaCenter(object): 26 | 27 | def __init__(self, module): 28 | self.module = module 29 | self.params = module.params 30 | self.cookie = None 31 | self.session = None 32 | self.api_path = '' 33 | self.response = None 34 | self.result = dict( 35 | changed=False, 36 | original_message='', 37 | message='') 38 | self.login() 39 | 40 | def __setattr__(self, key, value): 41 | """ 42 | Control what attributes can be attached to the object to avoid mistypes or invalid variable names. 43 | 44 | :param key: 45 | :param value: 46 | :return: 47 | """ 48 | if key not in ['api_path', 'username', 'password', 'host', 'session', 'response', 'module', 'params', 49 | 'cookie', 'credential_type', 'credential_subtype', 'credential_name', 'result']: 50 | raise AttributeError(key + " : Attribute not permitted") 51 | else: 52 | self.__dict__[key] = value 53 | 54 | # Login to DNA Center 55 | def login(self): 56 | """ 57 | Establish a session to the DNA Center Controller. 58 | 59 | :return: A session object is returned. 60 | 61 | """ 62 | 63 | login_url = 'https://' + self.params['host'] + '/api/system/v1/auth/token' 64 | # issue with 1.3.0.4 update broke the auth URI below - investigating 65 | # login_url = 'https://' + self.params['host'] + '/dna/system/api/v1/auth/token' 66 | 67 | # create a session object 68 | self.session = requests.session() 69 | 70 | # set configuration elements 71 | self.session.auth = (self.params['username'], self.params['password']) 72 | self.session.verify = False 73 | 74 | # send to controller 75 | try: 76 | self.response = self.session.post(login_url) 77 | except Exception as e: 78 | self.result['changed'] = False 79 | self.result['original_message'] = e 80 | self.module.fail_json(msg='Failed to Connect to target host.', **self.result) 81 | 82 | if self.response.status_code not in [200, 201, 202]: 83 | self.session.close() 84 | self.result['changed'] = False 85 | self.result['original_message'] = self.response.content 86 | self.module.fail_json(msg='Failed to establish session. ', **self.result) 87 | 88 | # update the headers with received sessions cookies 89 | self.session.headers.update({'X-Auth-Token': self.response.json()['Token']}) 90 | 91 | # set the content-type 92 | self.session.headers.update({'content-type': 'application/json'}) 93 | 94 | # provide session object to functions 95 | return self.session 96 | 97 | def intent_task_checker(self, task_id): 98 | """ 99 | Obtain the status of the task based on taskId for asynchronous operations. 100 | 101 | :param task_id: Internal ID assigned to asynchronous tasks. 102 | 103 | :return: Response data from the task status lookup. 104 | """ 105 | 106 | # task_checker will loop until the given task completes and return the results of the task execution 107 | 108 | url = 'https://' + self.params['host'] + '/' + 'api/dnacaap/v1/dnacaap/management/execution-status/' + task_id 109 | response = self.session.get(url) 110 | response = response.json() 111 | # response = response['response'] 112 | 113 | while not response.get('endTime'): 114 | time.sleep(2) 115 | response = self.session.get(url) 116 | response = response.json() 117 | # response = response['response'] 118 | 119 | if response.get('status') == 'SUCCESS' and response.get('bapiName').find('Update') >= 0: 120 | return response 121 | elif response.get('status') == "SUCCESS": 122 | self.result['changed'] = True 123 | self.result['original_message'] = response 124 | self.module.exit_json(msg='Task Completed successfully.', **self.result) 125 | elif response.get('status') == "FAILURE": 126 | self.result['changed'] = False 127 | self.result['original_message'] = response 128 | self.module.fail_json(msg='Task Failed to Complete!', **self.result) 129 | 130 | return response 131 | 132 | def task_checker(self, task_id): 133 | """ 134 | Obtain the status of the task based on taskId for asynchronous operations. 135 | 136 | :param task_id: Internal ID assigned to asynchronous tasks. 137 | 138 | :return: Response data from the task status lookup. 139 | """ 140 | 141 | # task_checker will loop until the given task completes and return the results of the task execution 142 | url = 'https://' + self.params['host'] + '/' + 'api/v1/task/' + task_id 143 | response = self.session.get(url) 144 | response = response.json() 145 | response = response['response'] 146 | 147 | while not response.get('endTime'): 148 | time.sleep(2) 149 | response = self.session.get(url) 150 | response = response.json() 151 | response = response['response'] 152 | 153 | if not response.get('isError'): 154 | self.result['changed'] = True 155 | self.result['original_message'] = response 156 | self.module.exit_json(msg='Task completed successfully.', **self.result) 157 | elif response.get('isError'): 158 | self.result['changed'] = False 159 | self.result['original_message'] = response 160 | self.module.fail_json(msg='Task failed to complete.', **self.result) 161 | 162 | return response 163 | 164 | # generalized get object function 165 | def get_obj(self): 166 | """ 167 | Retrieve information from the DNA Center Controller. 168 | 169 | :return: JSON data structure returned from a successful call or the response object. 170 | 171 | """ 172 | 173 | url = 'https://' + self.params['host'] + '/' + self.api_path.rstrip('/') 174 | response = self.session.get(url) 175 | if response.status_code in [200, 201, 202]: 176 | try: 177 | r = response.json() 178 | return r 179 | except Exception: 180 | r = [] 181 | return r 182 | # if response.text.find('No_.*found'): 183 | # r = [] 184 | # return r 185 | # else: 186 | # r = response.json() 187 | # return r 188 | 189 | elif response.status_code in [500]: 190 | # put in just for a bug in profiles get 191 | if response.text.find("Profile Not Found") >= 0: 192 | r = [] 193 | return r 194 | else: 195 | self.result['changed'] = False 196 | self.result['original_message'] = response.text 197 | self.module.fail_json(msg='Failed to get object!', **self.result) 198 | 199 | # generalized create call 200 | def create_obj(self, payload): 201 | """ 202 | Create object in DNA Center Controller. 203 | 204 | :param payload: Data structure formatted for the specific call and target setting or attribute. 205 | 206 | :return: JSON data structure returned from a successful call or the response object. 207 | """ 208 | try: 209 | payload = json.dumps(payload) 210 | # self.module.fail_json(msg=payload) 211 | except Exception: 212 | self.module.fail_json(msg='failed to convert payload to json. invalid json') 213 | url = "https://" + self.params['host'] + '/' + self.api_path.rstrip('/') 214 | 215 | if not self.module.check_mode: 216 | 217 | response = self.session.post(url, data=payload) 218 | if response.status_code in [200, 201, 202]: 219 | r = response.json() 220 | try: 221 | if url.find('intent') >= 0: 222 | self.intent_task_checker(r['executionId']) 223 | else: 224 | self.task_checker(r['response']['taskId']) 225 | 226 | except Exception as e: 227 | self.result['original_message'] = e 228 | self.module.fail_json(msg='Failed at task_checker', **self.result) 229 | else: 230 | self.result['changed'] = False 231 | self.result['original_message'] = response 232 | self.module.fail_json(msg='Failed to create object!', **self.result) 233 | else: 234 | self.result['changed'] = True 235 | self.module.exit_json(msg='In check_mode. Changes would be required.', **self.result) 236 | 237 | # generalized delete call 238 | def delete_obj(self, payload): 239 | """ 240 | 241 | :param payload: ID of the attribute to be deleted. 242 | :return: JSON data structure returned from a successful call or the response object. 243 | """ 244 | if not self.module.check_mode: 245 | url = 'https://' + self.params['host'] + '/' + self.api_path.rstrip('/') + '/' + payload 246 | response = self.session.delete(url) 247 | if response.status_code in [200, 201, 202]: 248 | r = response.json() 249 | # if self.api_path.find('dna'): 250 | if url.find('intent') >= 0: 251 | self.intent_task_checker(r['executionId']) 252 | else: 253 | self.task_checker(r['response']['taskId']) 254 | else: 255 | self.result['changed'] = False 256 | self.result['original_message'] = response.text 257 | self.module.fail_json(msg='Failed to create object!', **self.result) 258 | else: 259 | self.result['changed'] = True 260 | self.module.exit_json(msg='In check_mode. Changes would be required.', **self.result) 261 | 262 | # generalized update call 263 | def update_obj(self, payload): 264 | url = 'https://' + self.params['host'] + '/' + self.api_path.rstrip('/') 265 | response = self.session.request(method='PUT', url=url, json=payload, verify=False) 266 | 267 | if response.status_code in [200, 201, 202]: 268 | r = response.json() 269 | if url.find('intent') >= 0: 270 | self.intent_task_checker(r['executionId']) 271 | else: 272 | self.task_checker(r['response']['taskId']) 273 | 274 | else: 275 | self.result['changed'] = False 276 | self.result['original_message'] = response.text 277 | self.module.fail_json(msg='Failed to update object!', **self.result) 278 | 279 | # Group ID lookup 280 | def get_group_id(self, group_name): 281 | 282 | if (self.module.params['group_name'] == '-1' or self.module.params['group_name'].lower() == 'global'): 283 | return '-1' 284 | else: 285 | self.api_path = 'api/v1/group' 286 | groups = self.get_obj() 287 | group_ids = [group['id'] for group in groups['response'] if group['name'] == group_name] 288 | if len(group_ids) == 1: 289 | group_id = group_ids[0] 290 | return group_id 291 | 292 | def parse_geo(self, address): 293 | """ 294 | Supporting lookup addresses provided to return latitude, longitude to DNA Center. 295 | 296 | :param address: Physical address to lookup. 297 | 298 | :return: attributes dictionary 299 | """ 300 | 301 | geolocator = Nominatim(user_agent='dnac_ansible', timeout=30) 302 | try: 303 | location = geolocator.geocode(address) 304 | except Exception as e: 305 | print(e) 306 | sys.exit(0) 307 | 308 | location_parts = location.address.split(',') 309 | country = location_parts[len(location_parts) - 1] 310 | attributes = {'address': location.address, 311 | 'country': country, 312 | 'latitude': location.latitude, 313 | 'longitude': location.longitude, 314 | 'type': 'building' 315 | } 316 | return attributes 317 | 318 | def timezone_lookup(self, address): 319 | """ 320 | Provide for automated timezone resolution based on physical address provided. Avoid long lookups or specific 321 | string matching. 322 | 323 | :param address: Physical address of desired target timezone. 324 | :return: string of timezone based on address provided 325 | 326 | """ 327 | location_attributes = self.parse_geo(address) 328 | tf = TimezoneFinder() 329 | tz = tf.timezone_at(lng=location_attributes['longitude'], lat=location_attributes['latitude']) 330 | return tz 331 | 332 | def process_common_settings(self, payload, group_id): 333 | 334 | if group_id: 335 | payload[0].update({'groupUuid': group_id}) 336 | else: 337 | self.result['changed'] = False 338 | self.result['original_message'] = group_id 339 | self.module.fail_json(msg='Failed to create setting! Unable to locate group provided.', **self.result) 340 | 341 | # Define local variables 342 | state = self.module.params['state'] 343 | 344 | # Get current settings 345 | settings = self.get_obj() 346 | settings = settings['response'] 347 | setting_count = len(settings) 348 | 349 | # Save the existing and proposed datasets 350 | self.result['previous'] = settings 351 | self.result['proprosed'] = payload 352 | 353 | if state == 'present': 354 | if setting_count == 1: 355 | # compare previous to proposed 356 | if settings[0]['value'] != payload[0]['value']: 357 | self.create_obj(payload) 358 | else: 359 | self.result['changed'] = False 360 | self.result['msg'] = 'Already in desired state.' 361 | self.module.exit_json(**self.result) 362 | elif setting_count == 0: 363 | # create the object 364 | self.create_obj(payload) 365 | 366 | elif state == 'absent': 367 | payload[0].update({'value': []}) 368 | self.create_obj(payload) 369 | 370 | 371 | def main(): 372 | pass 373 | 374 | 375 | if __name__ == '__main__': 376 | main() 377 | --------------------------------------------------------------------------------