diff --git a/cliconf_plugins/aoscx.py b/cliconf_plugins/aoscx.py index dd250cb..d3b0c82 100644 --- a/cliconf_plugins/aoscx.py +++ b/cliconf_plugins/aoscx.py @@ -6,15 +6,9 @@ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = """ ---- -author: Aruba Networks (@ArubaNetworks) -network_cli: aoscx -short_description: Use CLI to run commands to CX devices -description: - - This ArubaOSCX plugin provides CLI operations with ArubaOS-CX Devices -version_added: "2.9" -""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + import json import re @@ -30,6 +24,8 @@ class Cliconf(CliconfBase): ''' Cliconf class for AOS-CX + + This ArubaOSCX plugin provides CLI operations with ArubaOS-CX Devices ''' def __init__(self, *args, **kwargs): @@ -44,8 +40,8 @@ def get_config(self, source='running', format='text', flags=None): Get the switch config ''' if source not in ('running', 'startup'): - return self.invalid_params("fetching configuration from {} is not" - " supported".format(source)) + return self.invalid_params("fetching configuration from {source} is" + " not supported".format(source=source)) if source == 'running': cmd = 'show running-config all' else: diff --git a/docs/aoscx_checkpoint.md b/docs/aoscx_checkpoint.md index 31b7e9b..7a31730 100644 --- a/docs/aoscx_checkpoint.md +++ b/docs/aoscx_checkpoint.md @@ -7,13 +7,14 @@ description: This module creates a new checkpoint or copies an existing checkpoi source_config: description: Name of the source configuration from which checkpoint needs to be created or copied. + type: str required: False default: 'running-config' destination_config: description: Name of the destination configuration or name of checkpoint. + type: str required: False - default: 'startup-config' ``` ##### EXAMPLES diff --git a/docs/aoscx_command.md b/docs/aoscx_command.md index 5a189b3..bf0eb4b 100644 --- a/docs/aoscx_command.md +++ b/docs/aoscx_command.md @@ -46,15 +46,16 @@ just configuration commands, aoscx_config. from commands[1], and so on. required: False type: list - + aliases: ['waitfor'] + match: description: Specifies whether all conditions in 'wait_for' must be satisfied or if just any one condition can be satisfied. To be used with 'wait_for'. default: 'all' - choice: ['any', 'all'] + choices: ['any', 'all'] required: False type: str - + retries: description: Maximum number of retries to check for the expected prompt. default: 10 @@ -69,13 +70,13 @@ just configuration commands, aoscx_config. output_file: description: Full path of the local system file to which commands' results will be output. - The directory must exist, but if the file doesn't exist, it will be created. + The directory must exist, but if the file doesn't exist, it will be created. required: False type: str output_file_format: description: Format to output the file in, either JSON or plain text. - To be used with 'output_file'. + To be used with 'output_file'. default: json choices: ['json', 'plain-text'] required: False diff --git a/docs/aoscx_config.md b/docs/aoscx_config.md index 84f2ffd..1326aab 100644 --- a/docs/aoscx_config.md +++ b/docs/aoscx_config.md @@ -12,6 +12,7 @@ description: This module allows configuration of running-configs on AOS-CX devic These commands must correspond with what would be found in the device's running-config. required: False type: list + aliases: ['commands'] parents: description: @@ -29,7 +30,7 @@ description: This module allows configuration of running-configs on AOS-CX devic indentation as a live switch config. The operation is purely additive, as it doesn't remove any lines that are present in the existing running-config, but not in the source config. required: False - type: str + type: path before: description: @@ -96,7 +97,7 @@ description: This module allows configuration of running-configs on AOS-CX devic description: - Path to directory in which the backup file should reside. required: False - type: str + type: path type: dict running_config: diff --git a/docs/aoscx_facts.md b/docs/aoscx_facts.md index 77f966f..aa69f16 100644 --- a/docs/aoscx_facts.md +++ b/docs/aoscx_facts.md @@ -7,23 +7,26 @@ Facts will be printed out when the playbook execution is done with increased ver ```YAML gather_subset: description: - - Retrieve a subset of all device information. This can be a - single category or it can be a list. Warning: leaving this field blank + - Retrieve a subset of all device information. This can be a single + category or it can be a list. As warning, leaving this field blank returns all facts, which may be an intensive process. - options: ['software_info', 'software_images', 'host_name', 'platform_name', + choices: ['software_info', 'software_images', 'host_name', 'platform_name', 'management_interface', 'software_version', 'config', 'fans', 'power_supplies', 'product_info', 'physical_interfaces', 'resource_utilization', 'domain_name'] required: False - default: '!config' + default: ['software_info', 'software_images', 'host_name', 'platform_name', + 'management_interface', 'software_version', 'fans', 'power_supplies', + 'product_info', 'physical_interfaces', 'resource_utilization', + 'domain_name'] type: list - + gather_network_resources: description: - Retrieve vlan, interface, or vrf information. This can be a single category or it can be a list. Leaving this field blank returns all all interfaces, vlans, and vrfs. - options: ['interfaces', 'vlans', 'vrfs'] + choices: ['interfaces', 'vlans', 'vrfs'] required: False type: list ``` diff --git a/docs/aoscx_upload_firmware.md b/docs/aoscx_upload_firmware.md index f974966..eff8171 100644 --- a/docs/aoscx_upload_firmware.md +++ b/docs/aoscx_upload_firmware.md @@ -20,12 +20,6 @@ description: This module uploads a firmware image onto the switch stored locally ex) http://192.168.1.2:8000/TL_10_04_0030A.swi" type: str required: false - config_type: - description: Configuration type to be downloaded, JSON or CLI version of the config. - type: str - choices: ['json', 'cli'] - default: 'json' - required: false vrf: description: VRF to be used to contact HTTP server, required if remote_firmware_file_path is provided type: str diff --git a/httpapi_plugins/aoscx.py b/httpapi_plugins/aoscx.py index 62304b4..97f0f6a 100644 --- a/httpapi_plugins/aoscx.py +++ b/httpapi_plugins/aoscx.py @@ -7,9 +7,9 @@ from __future__ import (absolute_import, division, print_function) - __metaclass__ = type + DOCUMENTATION = """ --- author: Aruba Networks (@ArubaNetworks) @@ -146,13 +146,13 @@ def handle_httperror(self, exc): # Try to login with the out-of-the-box values of a zeroized # device, a zeroized device uses a blank password and won't # accept any operation on REST until a new password is set. - login_path = '/rest/v1/login?username={}'.format( - self.connection.get_option('remote_user')) + login_path = '/rest/v1/login?username={username}'.format( + username=self.connection.get_option('remote_user')) - login_resp, _ = self.connection.send( + login_resp = self.connection.send( data=None, path=login_path, method='POST') - if login_resp.code == 268: + if login_resp[0].code == 268: # Login was succesfull, but the session is restricted, the # administrator password must be set. admin_path = '/rest/v1/system/users/admin' @@ -160,10 +160,10 @@ def handle_httperror(self, exc): "password": self.connection.get_option('password') } - admin_response, _ = self.connection.send( + admin_response = self.connection.send( data=json.dumps(data), path=admin_path, method='PUT') - if admin_response.code == 200: - return login_resp + if admin_response[0].code == 200: + return login_resp[0] return exc diff --git a/library/aoscx_acl_vlan.py b/library/aoscx_acl_vlan.py index af8f40b..ed73f72 100644 --- a/library/aoscx_acl_vlan.py +++ b/library/aoscx_acl_vlan.py @@ -9,6 +9,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type + ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], diff --git a/library/aoscx_backup_config.py b/library/aoscx_backup_config.py index 14a995d..bd17df3 100644 --- a/library/aoscx_backup_config.py +++ b/library/aoscx_backup_config.py @@ -6,6 +6,10 @@ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], diff --git a/library/aoscx_banner.py b/library/aoscx_banner.py index 73fbe3d..7c354bc 100644 --- a/library/aoscx_banner.py +++ b/library/aoscx_banner.py @@ -5,6 +5,7 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/library/aoscx_boot_firmware.py b/library/aoscx_boot_firmware.py index 62d69ee..a50eb7b 100644 --- a/library/aoscx_boot_firmware.py +++ b/library/aoscx_boot_firmware.py @@ -4,6 +4,12 @@ # (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], diff --git a/library/aoscx_checkpoint.py b/library/aoscx_checkpoint.py index 49e4588..00c1b24 100644 --- a/library/aoscx_checkpoint.py +++ b/library/aoscx_checkpoint.py @@ -5,6 +5,11 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], @@ -19,17 +24,18 @@ description: - This module creates a new checkpoint or copies existing checkpoint to the running or startup config of an AOS-CX switch. -author: - - Aruba Networks +author: Aruba Networks (@ArubaNetworks) options: source_config: description: Name of the source configuration from which checkpoint needs to be created or copied. + type: str required: False default: 'running-config' destination_config: description: Name of the destination configuration or name of checkpoint. + type: str required: False default: 'startup-config' ''' diff --git a/library/aoscx_command.py b/library/aoscx_command.py index 6b94d97..ac3622f 100644 --- a/library/aoscx_command.py +++ b/library/aoscx_command.py @@ -5,9 +5,11 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + from __future__ import (absolute_import, division, print_function) __metaclass__ = type + ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], @@ -43,15 +45,16 @@ from commands[1], and so on. required: False type: list - + aliases: ['waitfor'] + match: description: Specifies whether all conditions in 'wait_for' must be satisfied or if just any one condition can be satisfied. To be used with 'wait_for'. default: 'all' - choice: ['any', 'all'] + choices: ['any', 'all'] required: False type: str - + retries: description: Maximum number of retries to check for the expected prompt. default: 10 @@ -66,17 +69,71 @@ output_file: description: Full path of the local system file to which commands' results will be output. - The directory must exist, but if the file doesn't exist, it will be created. + The directory must exist, but if the file doesn't exist, it will be created. required: False type: str output_file_format: description: Format to output the file in, either JSON or plain text. - To be used with 'output_file'. + To be used with 'output_file'. default: json choices: ['json', 'plain-text'] required: False type: str + + provider: + description: A dict object containing connection details. + suboptions: + auth_pass: + description: + - Specifies the password to use if required to enter privileged mode on the + remote device. If authorize is false, then this argument does nothing. + If the value is not specified in the task, the value of environment variable + ANSIBLE_NET_AUTH_PASS will be used instead. + type: str + authorize: + description: + - Instructs the module to enter privileged mode on the remote device before + sending any commands. If not specified, the device will attempt to execute + all commands in non-privileged mode. If the value is not specified in the + task, the value of environment variable ANSIBLE_NET_AUTHORIZE will be used instead. + type: bool + host: + description: + - Specifies the DNS host name or address for connecting to the remote device over the + specified transport. The value of host is used as the destination address for the transport. + required: True + type: str + password: + description: + - Specifies the password to use to authenticate the connection to the remote device. + This value is used to authenticate the SSH session. If the value is not specified + in the task, the value of environment variable ANSIBLE_NET_PASSWORD will be used instead. + type: str + port: + description: + - Specifies the port to use when building the connection to the remote device. + type: int + ssh_keyfile: + description: + - Specifies the SSH key to use to authenticate the connection to the remote device. + This value is the path to the key used to authenticate the SSH session. If the value + is not specified in the task, the value of environment variable ANSIBLE_NET_SSH_KEYFILE + will be used instead. + type: path + timeout: + description: + - Specifies the timeout in seconds for communicating with the network device for either + connecting or sending commands. If the timeout is exceeded before the operation is completed, + the module will error. + type: int + username: + description: + - Configures the username to use to authenticate the connection to the remote device. + This value is used to authenticate the SSH session. If the value is not specified in the task, + the value of environment variable ANSIBLE_NET_USERNAME will be used instead. + type: str + type: dict ''' # NOQA EXAMPLES = ''' @@ -177,7 +234,7 @@ def main(): argument_spec = dict( commands=dict(type='list', required=True), wait_for=dict(type='list', aliases=['waitfor']), - match=dict(default='all', choices=['all', 'any']), + match=dict(default='all', choices=['any', 'all']), retries=dict(default=10, type='int'), interval=dict(default=1, type='int'), output_file=dict(type='str', default=None), @@ -191,9 +248,7 @@ def main(): result = {'changed': False, 'warnings': warnings} module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True - ) + argument_spec=argument_spec, supports_check_mode=True) commands = parse_commands(module, warnings) wait_for = module.params['wait_for'] or list() diff --git a/library/aoscx_config.py b/library/aoscx_config.py index 8426622..21bf272 100644 --- a/library/aoscx_config.py +++ b/library/aoscx_config.py @@ -5,10 +5,11 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import (absolute_import, division, print_function) +from __future__ import (absolute_import, division, print_function) __metaclass__ = type + ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], @@ -33,6 +34,7 @@ These commands must correspond with what would be found in the device's running-config. required: False type: list + aliases: ['commands'] parents: description: @@ -50,7 +52,7 @@ indentation as a live switch config. The operation is purely additive, as it doesn't remove any lines that are present in the existing running-config, but not in the source config. required: False - type: str + type: path before: description: @@ -117,7 +119,7 @@ description: - Path to directory in which the backup file should reside. required: False - type: str + type: path type: dict running_config: @@ -177,6 +179,60 @@ which should be set to "intended." required: False type: str + + provider: + description: A dict object containing connection details. + suboptions: + auth_pass: + description: + - Specifies the password to use if required to enter privileged mode on the + remote device. If authorize is false, then this argument does nothing. + If the value is not specified in the task, the value of environment variable + ANSIBLE_NET_AUTH_PASS will be used instead. + type: str + authorize: + description: + - Instructs the module to enter privileged mode on the remote device before + sending any commands. If not specified, the device will attempt to execute + all commands in non-privileged mode. If the value is not specified in the + task, the value of environment variable ANSIBLE_NET_AUTHORIZE will be used instead. + type: bool + host: + description: + - Specifies the DNS host name or address for connecting to the remote device over the + specified transport. The value of host is used as the destination address for the transport. + required: True + type: str + password: + description: + - Specifies the password to use to authenticate the connection to the remote device. + This value is used to authenticate the SSH session. If the value is not specified + in the task, the value of environment variable ANSIBLE_NET_PASSWORD will be used instead. + type: str + port: + description: + - Specifies the port to use when building the connection to the remote device. + type: int + ssh_keyfile: + description: + - Specifies the SSH key to use to authenticate the connection to the remote device. + This value is the path to the key used to authenticate the SSH session. If the value + is not specified in the task, the value of environment variable ANSIBLE_NET_SSH_KEYFILE + will be used instead. + type: path + timeout: + description: + - Specifies the timeout in seconds for communicating with the network device for either + connecting or sending commands. If the timeout is exceeded before the operation is completed, + the module will error. + type: int + username: + description: + - Configures the username to use to authenticate the connection to the remote device. + This value is used to authenticate the SSH session. If the value is not specified in the task, + the value of environment variable ANSIBLE_NET_USERNAME will be used instead. + type: str + type: dict ''' # NOQA EXAMPLES = ''' @@ -290,6 +346,7 @@ def main(): filename=dict(), dir_path=dict(type='path') ) + argument_spec = dict( src=dict(type='path'), @@ -357,7 +414,7 @@ def main(): else: filename = "backup.cfg" - with open(dir_path+'/'+filename, 'w') as backupfile: + with open(dir_path + '/' + filename, 'w') as backupfile: backupfile.write(contents) backupfile.write("\n") diff --git a/library/aoscx_dns.py b/library/aoscx_dns.py index 988dd43..67ccaca 100644 --- a/library/aoscx_dns.py +++ b/library/aoscx_dns.py @@ -5,6 +5,7 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + from __future__ import (absolute_import, division, print_function) __metaclass__ = type @@ -66,8 +67,8 @@ - name: DNS configuration creation aoscx_dns: mgmt_nameservers: - "Primary": "10.10.2.10" - "Secondary": "10.10.2.10" + "Primary": "10.10.2.10" + "Secondary": "10.10.2.10" dns_domain_name: "hpe.com" dns_domain_list: 0: "hp.com" @@ -123,11 +124,14 @@ RETURN = r''' # ''' +from ansible.module_utils.vrfs.aoscx_vrf import VRF from ansible.module_utils.aoscx import ArubaAnsibleModule -from ansible.module_utils.aoscx_vrf import VRF def main(): + ''' + Ansible module to configure DNS on AOS-CX switch + ''' module_args = dict( mgmt_nameservers=dict(type='dict', required=False), dns_domain_list=dict(type='dict', required=False), @@ -146,66 +150,98 @@ def main(): dns_domain_list = aruba_ansible_module.module.params['dns_domain_list'] vrf_name = aruba_ansible_module.module.params['vrf'] dns_name_servers = aruba_ansible_module.module.params['dns_name_servers'] - dns_host_v4_address_mapping = aruba_ansible_module.module.params['dns_host_v4_address_mapping'] # NOQA + dns_host_v4_address_mapping = aruba_ansible_module.module.params[ + 'dns_host_v4_address_mapping'] state = aruba_ansible_module.module.params['state'] vrf = VRF() if state == 'create' or state == 'update': if mgmt_nameservers is not None: - if 'mode' in aruba_ansible_module.running_config['System']['mgmt_intf']: # NOQA - mgmt_if_mode = aruba_ansible_module.running_config['System']['mgmt_intf']['mode'] # NOQA + if 'mode' in aruba_ansible_module.running_config['System']['mgmt_intf']: + mgmt_if_mode = aruba_ansible_module.running_config['System']['mgmt_intf']['mode'] else: mgmt_if_mode = 'dhcp' if mgmt_if_mode != 'static': - aruba_ansible_module.module.fail_json(msg="The management interface must have static IP to configure management interface name servers") # NOQA - - for k, v in mgmt_nameservers.items(): - if k.lower() == 'primary': - aruba_ansible_module.running_config['System']['mgmt_intf']['dns_server_1'] = v # NOQA - elif k.lower() == 'secondary': - aruba_ansible_module.running_config['System']['mgmt_intf']['dns_server_2'] = v # NOQA + message_part1 = "The management interface must have static" + message_part2 = "IP to configure management interface name servers" + aruba_ansible_module.module.fail_json( + msg=message_part1 + message_part2) + + for key, value in mgmt_nameservers.items(): + if key.lower() == 'primary': + aruba_ansible_module.running_config[ + 'System']['mgmt_intf']['dns_server_1'] = value + elif key.lower() == 'secondary': + aruba_ansible_module.running_config[ + 'System']['mgmt_intf']['dns_server_2'] = value if vrf_name is None: vrf_name = 'default' + if not vrf.check_vrf_exists(aruba_ansible_module, vrf_name): + aruba_ansible_module.module.fail_json( + msg="VRF {vrf} is not configured".format(vrf=vrf_name)) + return aruba_ansible_module + if dns_domain_name is not None: - aruba_ansible_module = vrf.update_vrf_dns_domain_name(aruba_ansible_module, vrf_name, dns_domain_name, update_type="insert") # NOQA + aruba_ansible_module = vrf.update_vrf_fields( + aruba_ansible_module, vrf_name, "dns_domain_name", dns_domain_name) if dns_domain_list is not None: - aruba_ansible_module = vrf.update_vrf_dns_domain_list(aruba_ansible_module, vrf_name, dns_domain_list, update_type="insert") # NOQA + aruba_ansible_module = vrf.update_vrf_fields( + aruba_ansible_module, vrf_name, "dns_domain_list", dns_domain_list) if dns_name_servers is not None: - aruba_ansible_module = vrf.update_vrf_dns_name_servers(aruba_ansible_module, vrf_name, dns_name_servers, update_type="insert") # NOQA + aruba_ansible_module = vrf.update_vrf_fields( + aruba_ansible_module, vrf_name, "dns_name_servers", dns_name_servers) if dns_host_v4_address_mapping is not None: - aruba_ansible_module = vrf.update_vrf_dns_host_v4_address_mapping(aruba_ansible_module, vrf_name, dns_host_v4_address_mapping, update_type="insert") # NOQA + aruba_ansible_module = vrf.update_vrf_fields( + aruba_ansible_module, + vrf_name, + "dns_host_v4_address_mapping", + dns_host_v4_address_mapping) if state == 'delete': if vrf_name is None: vrf_name = 'default' + if not vrf.check_vrf_exists(aruba_ansible_module, vrf_name): + aruba_ansible_module.warnings.append( + "VRF {vrf} is not configured" "".format(vrf=vrf_name)) + return aruba_ansible_module + if mgmt_nameservers is not None: - for k, v in mgmt_nameservers.items(): + for k in mgmt_nameservers.keys(): if k.lower() == 'primary': - aruba_ansible_module.running_config['System']['mgmt_intf'].pop('dns_server_1') # NOQA + aruba_ansible_module.running_config['System']['mgmt_intf'].pop( + 'dns_server_1') elif k.lower() == 'secondary': - aruba_ansible_module.running_config['System']['mgmt_intf'].pop('dns_server_2') # NOQA + aruba_ansible_module.running_config['System']['mgmt_intf'].pop( + 'dns_server_2') if dns_domain_name is not None: - aruba_ansible_module = vrf.update_vrf_dns_domain_name(aruba_ansible_module, vrf_name, dns_domain_name, update_type="delete") # NOQA + aruba_ansible_module = vrf.delete_vrf_field( + aruba_ansible_module, vrf_name, "dns_domain_name", dns_domain_name) if dns_domain_list is not None: - aruba_ansible_module = vrf.update_vrf_dns_domain_list(aruba_ansible_module, vrf_name, dns_domain_list, update_type="delete") # NOQA + aruba_ansible_module = vrf.delete_vrf_field( + aruba_ansible_module, vrf_name, "dns_domain_list", dns_domain_list) if dns_name_servers is not None: - aruba_ansible_module = vrf.update_vrf_dns_name_servers(aruba_ansible_module, vrf_name, dns_name_servers, update_type="delete") # NOQA + aruba_ansible_module = vrf.delete_vrf_field( + aruba_ansible_module, vrf_name, "dns_name_servers", dns_name_servers) if dns_host_v4_address_mapping is not None: - aruba_ansible_module = vrf.update_vrf_dns_host_v4_address_mapping(aruba_ansible_module, vrf_name, dns_host_v4_address_mapping, update_type="delete") # NOQA + aruba_ansible_module = vrf.delete_vrf_field( + aruba_ansible_module, + vrf_name, + "dns_host_v4_address_mapping", + dns_host_v4_address_mapping) aruba_ansible_module.update_switch_config() diff --git a/library/aoscx_facts.py b/library/aoscx_facts.py index 54c6fa7..ae34ad3 100644 --- a/library/aoscx_facts.py +++ b/library/aoscx_facts.py @@ -6,6 +6,10 @@ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], @@ -25,25 +29,56 @@ gather_subset: description: - - Retrieve a subset of all device information. This can be a - single category or it can be a list. Warning: leaving this field blank + - Retrieve a subset of all device information. This can be a single + category or it can be a list. As warning, leaving this field blank returns all facts, which may be an intensive process. - options: ['software_info', 'software_images', 'host_name', 'platform_name', + choices: ['software_info', 'software_images', 'host_name', 'platform_name', 'management_interface', 'software_version', 'config', 'fans', 'power_supplies', 'product_info', 'physical_interfaces', 'resource_utilization', 'domain_name'] required: False - default: '!config' + default: ['software_info', 'software_images', 'host_name', 'platform_name', + 'management_interface', 'software_version', 'fans', 'power_supplies', + 'product_info', 'physical_interfaces', 'resource_utilization', + 'domain_name'] type: list - + gather_network_resources: description: - Retrieve vlan, interface, or vrf information. This can be a single category or it can be a list. Leaving this field blank returns all all interfaces, vlans, and vrfs. - options: ['interfaces', 'vlans', 'vrfs'] + choices: ['interfaces', 'vlans', 'vrfs'] required: False type: list + + provider: + description: A dict object containing connection details. + suboptions: + host: + description: + - Specifies the DNS host name or address for connecting to the remote device over the + specified transport. The value of host is used as the destination address for the transport. + required: True + type: str + password: + description: + - Specifies the password to use to authenticate the connection to the remote device. + This value is used to authenticate the SSH session. If the value is not specified + in the task, the value of environment variable ANSIBLE_NET_PASSWORD will be used instead. + type: str + port: + description: + - Specifies the port to use when building the connection to the remote device. + type: int + username: + description: + - Configures the username to use to authenticate the connection to the remote device. + This value is used to authenticate the SSH session. If the value is not specified in the task, + the value of environment variable ANSIBLE_NET_USERNAME will be used instead. + type: str + type: dict + ''' # NOQA EXAMPLES = ''' @@ -124,9 +159,9 @@ type: dict ''' -from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.facts.facts import Facts from ansible.module_utils.aoscx import aoscx_http_argument_spec, get_connection +from ansible.module_utils.basic import AnsibleModule def main(): @@ -135,14 +170,32 @@ def main(): :returns: ansible_facts """ argument_spec = { - 'gather_subset': dict(default=['!config'], type='list'), - 'gather_network_resources': dict(type='list'), + 'gather_subset': dict(default=['software_info', 'software_images', + 'host_name', 'platform_name', + 'management_interface', + 'software_version', 'fans', + 'power_supplies', 'product_info', + 'physical_interfaces', + 'resource_utilization', 'domain_name'], + type='list', + choices=['software_info', 'software_images', + 'host_name', 'platform_name', + 'management_interface', + 'software_version', + 'config', 'fans', 'power_supplies', + 'product_info', 'physical_interfaces', + 'resource_utilization', 'domain_name']), + 'gather_network_resources': dict(type='list', + choices=['interfaces', 'vlans', + 'vrfs']) } + argument_spec.update(aoscx_http_argument_spec) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - module._connection = get_connection(module) #noqa + module._connection = get_connection(module) # noqa warnings = [] if module.params["gather_subset"] == "!config": diff --git a/library/aoscx_l3_interface.py b/library/aoscx_l3_interface.py index fc27a71..3202b91 100644 --- a/library/aoscx_l3_interface.py +++ b/library/aoscx_l3_interface.py @@ -9,6 +9,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type + ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], diff --git a/library/aoscx_static_route.py b/library/aoscx_static_route.py index ab8aa57..6b5f31d 100644 --- a/library/aoscx_static_route.py +++ b/library/aoscx_static_route.py @@ -5,6 +5,7 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + from __future__ import (absolute_import, division, print_function) __metaclass__ = type @@ -127,8 +128,8 @@ RETURN = r''' # ''' +from ansible.module_utils.vrfs.aoscx_vrf import VRF from ansible.module_utils.aoscx import ArubaAnsibleModule -from ansible.module_utils.aoscx_vrf import VRF def main(): @@ -150,8 +151,8 @@ def main(): prefix = aruba_ansible_module.module.params['destination_address_prefix'] route_type = aruba_ansible_module.module.params['type'] distance = aruba_ansible_module.module.params['distance'] - next_hop_interface = aruba_ansible_module.module.params['next_hop_interface'] # NOQA - next_hop_ip_address = aruba_ansible_module.module.params['next_hop_ip_address'] # NOQA + next_hop_interface = aruba_ansible_module.module.params['next_hop_interface'] + next_hop_ip_address = aruba_ansible_module.module.params['next_hop_ip_address'] state = aruba_ansible_module.module.params['state'] vrf = VRF() @@ -165,54 +166,58 @@ def main(): aruba_ansible_module = vrf.create_vrf(aruba_ansible_module, vrf_name) else: - aruba_ansible_module.module.fail_json(msg="VRF {vrf} is not " - "configured" - "".format(vrf=vrf_name)) + aruba_ansible_module.module.fail_json( + msg="VRF {vrf} is not " "configured" "".format(vrf=vrf_name)) encoded_prefix = prefix.replace("/", "%2F") encoded_prefix = encoded_prefix.replace(":", "%3A") index = vrf_name + '/' + encoded_prefix - if (state == 'create') or (state == 'update'): - address_family = 'ipv6' if ':' in prefix else 'ipv4' - - if not aruba_ansible_module.running_config['System']['vrfs'][vrf_name].has_key('Static_Route'): # NOQA - aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'] = {} # NOQA - - if not aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'].has_key(index): # NOQA - aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'][index] = {} # NOQA - + static_route = {} + static_route[index] = {} if address_family is not None: - aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'][index]["address_family"] = address_family # NOQA - + static_route[index]["address_family"] = address_family if prefix is not None: - aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'][index]["prefix"] = prefix # NOQA - + static_route[index]["prefix"] = prefix if route_type is not None: - aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'][index]["type"] = route_type # NOQA - + static_route[index]["type"] = route_type if route_type == 'forward': - if not aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'][index].has_key('static_nexthops'): # NOQA - aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'][index]['static_nexthops'] = {"0": {"bfd_enable": False, "distance": distance}} # NOQA - - if next_hop_interface is not None: - encoded_interface = next_hop_interface.replace('/', '%2F') - aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'][index]['static_nexthops']["0"]["port"] = encoded_interface # NOQA - - if next_hop_ip_address is not None: - aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'][index]['static_nexthops']["0"]["ip_address"] = next_hop_ip_address # NOQA - - if state == 'delete': - - if not aruba_ansible_module.running_config['System']['vrfs'][vrf_name].has_key('Static_Route'): # NOQA - aruba_ansible_module.warnings.append("Static route for destination {dest} and does not exist in VRF{vrf}".format(dest=prefix, vrf=vrf_name)) # NOQA - - elif not aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'].has_key(index): # NOQA - aruba_ansible_module.warnings.append("Static route for destination {dest} and does not exist in VRF{vrf}".format(dest=prefix, vrf=vrf_name)) # NOQA - + static_route[index]['static_nexthops'] = { + "0": { + "bfd_enable": False, + "distance": distance + } + } + if next_hop_interface is not None: + encoded_interface = next_hop_interface.replace( + '/', '%2F') + static_route[index]['static_nexthops']["0"]["port"] = encoded_interface + if next_hop_ip_address is not None: + static_route[index]['static_nexthops']["0"]["ip_address"] = next_hop_ip_address + + aruba_ansible_module = vrf.update_vrf_fields( + aruba_ansible_module, vrf_name, 'Static_Route', static_route) + + if (state == 'delete'): + if not vrf.check_vrf_exists(aruba_ansible_module, vrf_name): + aruba_ansible_module.module.fail_json( + msg="VRF {vrf_name} does not exist".format( + vrf_name=vrf_name)) + static_route = vrf.get_vrf_field_value( + aruba_ansible_module, vrf_name, 'Static_Route') + if not static_route: + aruba_ansible_module.warnings.append( + "Static route for destination {dest} does not exist in VRF {vrf}".format( + dest=prefix, vrf=vrf_name)) + elif index not in static_route.keys(): + aruba_ansible_module.warnings.append( + "Static route for destination {dest} does not exist in VRF {vrf}".format( + dest=prefix, vrf=vrf_name)) else: - aruba_ansible_module.running_config['System']['vrfs'][vrf_name]['Static_Route'].pop(index) # NOQA + static_route.pop(index) + aruba_ansible_module = vrf.update_vrf_fields( + aruba_ansible_module, vrf_name, 'Static_Route', static_route) aruba_ansible_module.update_switch_config() diff --git a/library/aoscx_upload_config.py b/library/aoscx_upload_config.py index 9f280fe..6ddcd80 100644 --- a/library/aoscx_upload_config.py +++ b/library/aoscx_upload_config.py @@ -5,6 +5,11 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], @@ -54,14 +59,14 @@ - name: Copy Running Config from local JSON file as JSON aoscx_upload_config: config_name: 'running-config' - remote_config_file_tftp_path: '/user/admin/running.json' + config_json: '/user/admin/running.json' - name: Copy Running Config from TFTP server as JSON aoscx_upload_config: config_name: 'running-config' remote_config_file_tftp_path: 'tftp://192.168.1.2/running.json' vrf: 'mgmt' - + - name: Copy CLI from TFTP Server to Running Config aoscx_upload_config: config_name: 'running-config' @@ -114,7 +119,11 @@ def main(): aruba_ansible_module.tftp_switch_config_from_remote_location( tftp_path_encoded, config_name, vrf) else: - if config_json is None: + if config_json: + with open(config_json) as json_file: + config_json = json.load(json_file) + + if config_file: with open(config_file) as json_file: config_json = json.load(json_file) diff --git a/library/aoscx_upload_firmware.py b/library/aoscx_upload_firmware.py index 4bbd01a..5f2047f 100644 --- a/library/aoscx_upload_firmware.py +++ b/library/aoscx_upload_firmware.py @@ -5,6 +5,11 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], @@ -37,12 +42,6 @@ ex) http://192.168.1.2:8000/TL_10_04_0030A.swi" type: str required: false - config_type: - description: Configuration type to be downloaded, JSON or CLI version of the config. - type: str - choices: ['json', 'cli'] - default: 'json' - required: false vrf: description: VRF to be used to contact HTTP server, required if remote_firmware_file_path is provided type: str diff --git a/library/aoscx_vrf.py b/library/aoscx_vrf.py index 85310ea..f136987 100644 --- a/library/aoscx_vrf.py +++ b/library/aoscx_vrf.py @@ -51,8 +51,8 @@ RETURN = r''' # ''' +from ansible.module_utils.vrfs.aoscx_vrf import VRF from ansible.module_utils.aoscx import ArubaAnsibleModule -from ansible.module_utils.aoscx_vrf import VRF def main(): diff --git a/module_utils/aoscx.py b/module_utils/aoscx.py index 902a597..7c78f1e 100644 --- a/module_utils/aoscx.py +++ b/module_utils/aoscx.py @@ -9,17 +9,27 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type + import copy import json -import requests import re +import traceback from ansible.module_utils._text import to_text from ansible.module_utils.basic import env_fallback from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.network.common.utils import to_list, ComplexList from ansible.module_utils.connection import exec_command, Connection, ConnectionError from ansible.module_utils.aoscx_ztp import connect_ztp_device +REQUESTS_IMP_ERR = None +try: + import requests + HAS_REQUESTS_LIB = True +except ImportError: + HAS_REQUESTS_LIB = False + REQUESTS_IMP_ERR = traceback.format_exc() + _DEVICE_CONNECTION = None _DEVICE_CONFIGS = {} _DEVICE_ZTP = False @@ -50,18 +60,21 @@ 'provider': dict(type='dict', options=aoscx_http_provider_spec, removed_in_version=2.14), } + def get_provider_argspec(): ''' Returns the provider argument specification ''' return aoscx_provider_spec + def check_args(module, warnings): ''' Checks the argument ''' pass + def get_config(module, flags=None): ''' Obtains the switch configuration @@ -77,28 +90,33 @@ def get_config(module, flags=None): except KeyError: rc, out, err = exec_command(module, cmd) if rc != 0: - module.fail_json(msg='unable to retrieve current config', stderr=to_text(err, errors='surrogate_then_replace')) + module.fail_json(msg='unable to retrieve current config', stderr=to_text( + err, errors='surrogate_then_replace')) cfg = to_text(out, errors='surrogate_then_replace').strip() _DEVICE_CONFIGS[cmd] = cfg return cfg + def load_config(module, commands): ''' Loads the configuration onto the switch ''' rc, out, err = exec_command(module, 'configure terminal') if rc != 0: - module.fail_json(msg='unable to enter configuration mode', err=to_text(out, errors='surrogate_then_replace')) + module.fail_json(msg='unable to enter configuration mode', + err=to_text(out, errors='surrogate_then_replace')) for command in to_list(commands): if command == 'end': continue rc, out, err = exec_command(module, command) if rc != 0: - module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), command=command, rc=rc) + module.fail_json(msg=to_text( + err, errors='surrogate_then_replace'), command=command, rc=rc) exec_command(module, 'end') + def exec_command(module, command): ''' Execute command on the switch @@ -113,6 +131,7 @@ def exec_command(module, command): return 0, out, '' + def sanitize(resp): ''' Sanitizes the string to remove additiona white spaces @@ -123,10 +142,12 @@ def sanitize(resp): cleaned.append(re.sub(r"^\s+", " ", line)) return '\n'.join(cleaned).strip() + class HttpApi: ''' Module utils class for AOS-CX HTTP API connection ''' + def __init__(self, module): self._module = module self._connection_obj = None @@ -147,22 +168,26 @@ def get(self, url, data=None): res = self._connection.send_request(data=data, method='GET', path=url) return res - def put(self, url, data=None, headers={}): + def put(self, url, data=None, headers=None): ''' PUT REST call ''' + if headers is None: + headers = {} return self._connection.send_request(data=data, method='PUT', path=url, headers=headers) - def post(self, url, data=None, headers={}): + def post(self, url, data=None, headers=None): ''' POST REST call ''' + if headers is None: + headers = {} return self._connection.send_request(data=data, method='POST', path=url, headers=headers) - def file_upload(self, url, files, headers={}): + def file_upload(self, url, files, headers=None): """ Workaround with requests library for lack of support in httpapi for multipart POST @@ -170,6 +195,13 @@ def file_upload(self, url, files, headers={}): ansible/blob/devel/lib/ansible/plugins/connection/httpapi.py ansible/blob/devel/lib/ansible/module_utils/urls.py """ + + if not HAS_REQUESTS_LIB: + self._module.fail_json(msg=missing_required_lib( + "requests"), exception=REQUESTS_IMP_ERR) + + if headers is None: + headers = {} connection_details = self._connection.get_connection_details() if 'auth'in connection_details.keys(): headers.update(connection_details['auth']) @@ -200,6 +232,7 @@ def create_ssh_connection(module): if not _DEVICE_ZTP: # For zeroize devices, configure authentication connect_ztp_device( + module, connection.get_option('host'), connection.get_option('remote_user'), connection.get_option('password')) @@ -234,34 +267,40 @@ def get(module, url, data=None): return res -def put(module, url, data=None, headers={}): +def put(module, url, data=None, headers=None): ''' Perform PUT REST call ''' + if headers is None: + headers = {} conn = get_connection(module) res = conn.put(url, data, headers) return res -def post(module, url, data=None, headers={}): +def post(module, url, data=None, headers=None): ''' Perform POST REST call ''' + if headers is None: + headers = {} conn = get_connection(module) res = conn.post(url, data, headers) return res -def file_upload(module, url, files, headers={}): +def file_upload(module, url, files, headers=None): ''' Upload File through REST ''' + if headers is None: + headers = {} conn = get_connection(module) res = conn.file_upload(url, files, headers) return res -def to_command(module, commands): +def to_command(module, commands): ''' Convert command to ComplexList ''' @@ -276,6 +315,7 @@ def to_command(module, commands): return transform(to_list(commands)) + def run_commands(module, commands, check_rc=False): ''' Execute command on the switch diff --git a/module_utils/aoscx_acl.py b/module_utils/aoscx_acl.py index 42967e1..d4577c5 100644 --- a/module_utils/aoscx_acl.py +++ b/module_utils/aoscx_acl.py @@ -97,7 +97,7 @@ def update_acl_entry(self, aruba_ansible_module, acl_name, acl_type, acl_entry_s if "cfg_aces" not in aruba_ansible_module.running_config["ACL"][acl_index].keys(): # NOQA aruba_ansible_module.running_config["ACL"][acl_index]["cfg_aces"] = {} # NOQA - if aruba_ansible_module.running_config["ACL"][acl_index]["cfg_aces"].has_key(acl_entry_sequence_number): # NOQA + if 'acl_entry_sequence_number' in aruba_ansible_module.running_config["ACL"][acl_index]["cfg_aces"].keys(): # NOQA if aruba_ansible_module.running_config["ACL"][acl_index]["cfg_aces"][acl_entry_sequence_number] != acl_entry_details: # NOQA aruba_ansible_module.running_config["ACL"][acl_index]["cfg_aces"][acl_entry_sequence_number] = acl_entry_details # NOQA aruba_ansible_module.running_config["ACL"][acl_index]["cfg_version"] = randint(-900719925474099, 900719925474099) # NOQA diff --git a/module_utils/aoscx_interface.py b/module_utils/aoscx_interface.py index 15d90cd..8051dd4 100644 --- a/module_utils/aoscx_interface.py +++ b/module_utils/aoscx_interface.py @@ -5,12 +5,13 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + from __future__ import (absolute_import, division, print_function) __metaclass__ = type from ansible.module_utils.aoscx_port import Port -from ansible.module_utils.aoscx_vrf import VRF +from ansible.module_utils.vrfs.aoscx_vrf import VRF from random import randint diff --git a/module_utils/aoscx_ztp.py b/module_utils/aoscx_ztp.py index 2b55e33..2fb4325 100644 --- a/module_utils/aoscx_ztp.py +++ b/module_utils/aoscx_ztp.py @@ -5,16 +5,23 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import missing_required_lib from contextlib import closing import time -import paramiko +import traceback -from ansible.utils.display import Display -from ansible.module_utils._text import to_text - -display = Display(verbosity=4) +PARAMIKO_IMP_ERR = None +try: + import paramiko + HAS_PARAMIKO_LIB = True +except ImportError: + HAS_PARAMIKO_LIB = False + PARAMIKO_IMP_ERR = traceback.format_exc() CHANNEL_TIMEOUT = 8 READ_TIMEOUT = 10 @@ -26,7 +33,7 @@ SHELL_PROMPT = '#' -def connect_ztp_device(hostname, username, password): +def connect_ztp_device(module, hostname, username, password): """Connects to a ZTP device using SSH and configures authentication. The function tries to login with the out-of-the-box values of a zeroized @@ -39,11 +46,16 @@ def connect_ztp_device(hostname, username, password): already configured, or there is an error with the connection parameters, the function logs the error and returns. + :param module: Ansible module. :param hostname: The Switch to connect to. :param username: The username to authenticate as. :param password: A password to use for authentication. """ + if not HAS_PARAMIKO_LIB: + module.fail_json(msg=missing_required_lib( + "paramiko"), exception=PARAMIKO_IMP_ERR) + with closing(paramiko.SSHClient()) as ssh_client: # Define SSH parameters @@ -82,11 +94,10 @@ def connect_ztp_device(hostname, username, password): wait_for_channel_msg(shell_channel, SHELL_PROMPT) except paramiko.ssh_exception.AuthenticationException as e: - display.vvvv( - "Unable to authenticate: {0}".format(to_text(e)), "ztp") + module.log("Unable to authenticate: {0}".format(to_text(e))) except Exception as e: - display.vvvv(to_text(e), "ztp") + module.log(to_text(e)) def wait_for_channel_msg(shell_channel, msg): diff --git a/module_utils/facts/facts.py b/module_utils/facts/facts.py index 00592d9..f33051a 100644 --- a/module_utils/facts/facts.py +++ b/module_utils/facts/facts.py @@ -1,10 +1,14 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # (C) Copyright 2020 Hewlett Packard Enterprise Development LP. # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + from ansible.module_utils.aoscx import get from ansible.module_utils.facts.interfaces import InterfacesFacts from ansible.module_utils.facts.legacy import Default, SoftwareInfo, \ @@ -15,6 +19,7 @@ from ansible.module_utils.facts.vrfs import VrfsFacts from ansible.module_utils.network.common.facts.facts import FactsBase + FACT_LEGACY_SUBSETS = dict( default=Default, software_info=SoftwareInfo, @@ -48,12 +53,10 @@ class Facts(FactsBase): def get_facts(self, legacy_facts_type=None, resource_facts_type=None, data=None): - ''' Returns the facts for aoscx ''' - if data is None: data = get_switch_running_config(self._module) if self.VALID_RESOURCE_SUBSETS: @@ -66,8 +69,8 @@ def get_facts(self, legacy_facts_type=None, resource_facts_type=None, return self.ansible_facts, self._warnings -def get_switch_running_config(module): +def get_switch_running_config(module): ''' Gets the switch running-config ''' diff --git a/module_utils/facts/interfaces.py b/module_utils/facts/interfaces.py index cc2e761..120d914 100644 --- a/module_utils/facts/interfaces.py +++ b/module_utils/facts/interfaces.py @@ -1,10 +1,14 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # (C) Copyright 2020 Hewlett Packard Enterprise Development LP. # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + from ansible.module_utils.aoscx import get @@ -12,6 +16,7 @@ class InterfacesFacts(object): ''' Class for AOS-CX Interface facts ''' + def __init__(self, module, subspec='config', options='options'): ''' init function diff --git a/module_utils/facts/legacy.py b/module_utils/facts/legacy.py index 71b4911..0decec0 100644 --- a/module_utils/facts/legacy.py +++ b/module_utils/facts/legacy.py @@ -1,16 +1,22 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # (C) Copyright 2020 Hewlett Packard Enterprise Development LP. # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + from ansible.module_utils.aoscx import get + class FactsBase(object): ''' FactsBase class ''' + def __init__(self, module): self._module = module self.warnings = list() @@ -117,6 +123,7 @@ def populate(self): self._url = '/rest/v10.04/system?attributes=software_version' super(SoftwareVersion, self).populate() + class Config(FactsBase): ''' Config facts class @@ -178,7 +185,7 @@ def populate(self): for sub_system in self.data.keys(): sub_system_details = self.data[sub_system] - if self._fact_name in sub_system_details.keys(): + if self._fact_name in sub_system_details.keys(): output_data[sub_system] = sub_system_details[self._fact_name] diff --git a/module_utils/facts/vlans.py b/module_utils/facts/vlans.py index 4b92e3a..82a6346 100644 --- a/module_utils/facts/vlans.py +++ b/module_utils/facts/vlans.py @@ -1,10 +1,14 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # (C) Copyright 2020 Hewlett Packard Enterprise Development LP. # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + from ansible.module_utils.aoscx import get @@ -12,6 +16,7 @@ class VlansFacts(object): ''' VLANs Facts Class ''' + def __init__(self, module, subspec='config', options='options'): ''' init function diff --git a/module_utils/facts/vrfs.py b/module_utils/facts/vrfs.py index 6d3e83c..f65e5e5 100644 --- a/module_utils/facts/vrfs.py +++ b/module_utils/facts/vrfs.py @@ -1,10 +1,14 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # (C) Copyright 2020 Hewlett Packard Enterprise Development LP. # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + from ansible.module_utils.aoscx import get @@ -12,6 +16,7 @@ class VrfsFacts(object): ''' VRFs facts class ''' + def __init__(self, module, subspec='config', options='options'): ''' init function diff --git a/module_utils/providers.py b/module_utils/providers.py index c893609..74434c1 100644 --- a/module_utils/providers.py +++ b/module_utils/providers.py @@ -5,6 +5,10 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + import json from threading import RLock from ansible.module_utils.six import itervalues diff --git a/module_utils/vrfs/__init__.py b/module_utils/vrfs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/vrfs/aoscx_vrf.py b/module_utils/vrfs/aoscx_vrf.py new file mode 100644 index 0000000..b863c7e --- /dev/null +++ b/module_utils/vrfs/aoscx_vrf.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re +from ansible.module_utils.vrfs.aoscx_vrf_base import VRF_Base +from ansible.module_utils.vrfs.aoscx_vrf_10_04_1000 import VRF_10_04_1000 + + +class VRF: + ''' + VRF APIs to perform CRUD operations + ''' + + def get_vrf_version(self, aruba_ansible_module): + ''' + Get the correct VRF implementation based on firmware + ''' + firmware = aruba_ansible_module.switch_current_firmware + regex = r'.(\d+).(\d+).(\d+)' + comp_regex = re.compile(regex) + output = re.search(comp_regex, firmware) + if int(output.group(2)) == 3: + return VRF_Base(aruba_ansible_module.running_config) + + if int(output.group(2)) == 4: + if int(output.group(3)) <= 40: + return VRF_Base(aruba_ansible_module.running_config) + else: + return VRF_10_04_1000( + aruba_ansible_module.running_config) + + return VRF_10_04_1000(aruba_ansible_module.running_config) + + def create_vrf(self, aruba_ansible_module, vrf_name): + ''' + Create a new VRF + ''' + vrfs = self.get_vrf_version(aruba_ansible_module) + vrfs.create_vrf(vrf_name) + aruba_ansible_module.running_config = vrfs.get_modified_config() + return aruba_ansible_module + + def delete_vrf(self, aruba_ansible_module, vrf_name): + ''' + Delete an exisiting VRF + ''' + vrfs = self.get_vrf_version(aruba_ansible_module) + try: + vrfs.delete_vrf(vrf_name) + aruba_ansible_module.running_config = vrfs.get_modified_config() + return aruba_ansible_module + except Exception as error: + if "does not exist" in str(error): + aruba_ansible_module.warnings.append(str(error)) + return aruba_ansible_module + else: + aruba_ansible_module.module.fail_json(msg=str(error)) + + def check_vrf_exists(self, aruba_ansible_module, vrf_name): + ''' + Check if the VRF exists on the switch + ''' + vrfs = self.get_vrf_version(aruba_ansible_module) + return vrfs.check_vrf_exists(vrf_name) + + def get_vrf_field_value( + self, + aruba_ansible_module, + vrf_name, + field): + ''' + Get the particular VRF field value + ''' + vrfs = self.get_vrf_version(aruba_ansible_module) + try: + return vrfs.get_vrf_field_value(vrf_name, field) + except Exception as error: + aruba_ansible_module.module.fail_json(msg=str(error)) + + def update_vrf_fields( + self, + aruba_ansible_module, + vrf_name, + field, + value): + ''' + Update the value of the particular VRF field value + ''' + vrfs = self.get_vrf_version(aruba_ansible_module) + try: + vrfs.update_vrf_field_value(vrf_name, field, value) + aruba_ansible_module.running_config = vrfs.get_modified_config() + return aruba_ansible_module + except Exception as error: + aruba_ansible_module.module.fail_json(msg=str(error)) + + def delete_vrf_field( + self, + aruba_ansible_module, + vrf_name, + field, + value): + ''' + Delete the value of the particular VRF field value + ''' + vrfs = self.get_vrf_version(aruba_ansible_module) + try: + vrfs.delete_vrf_field_value(vrf_name, field, value) + aruba_ansible_module.running_config = vrfs.get_modified_config() + return aruba_ansible_module + except Exception as error: + aruba_ansible_module.warnings.append(str(error)) diff --git a/module_utils/vrfs/aoscx_vrf_10_04_1000.py b/module_utils/vrfs/aoscx_vrf_10_04_1000.py new file mode 100644 index 0000000..bc722d2 --- /dev/null +++ b/module_utils/vrfs/aoscx_vrf_10_04_1000.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (C) Copyright 2020 Hewlett Packard Enterprise Development LP. +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils.vrfs.aoscx_vrf_base import VRF_Base + + +class VRF_10_04_1000(VRF_Base): + + ''' + VRF implementation for 10.04.1000 and above + firmware. + ''' + + def __init__(self, running_config): + ''' + Exact the VRF details from the root table. + ''' + self.config = running_config + if 'VRF' in self.config: + self.vrfs = self.config['VRF'] + else: + self.vrfs = {} + + def get_modified_config(self): + ''' + Generate the modified running-configuration. + ''' + self.config['VRF'] = self.vrfs + return self.config diff --git a/module_utils/vrfs/aoscx_vrf_base.py b/module_utils/vrfs/aoscx_vrf_base.py new file mode 100644 index 0000000..f7f65bd --- /dev/null +++ b/module_utils/vrfs/aoscx_vrf_base.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (C) Copyright 2020 Hewlett Packard Enterprise Development LP. +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils.vrfs.aoscx_vrf_entry import VRF_Entry + + +class VRF_Base: + ''' + VRF implementation for fimrware earlier than 10.04.1000 + ''' + + def __init__(self, running_config): + ''' + Extract the VRF details from system details + ''' + + self.config = running_config + if 'vrfs' in self.config['System'].keys(): + self.vrfs = self.config['System']['vrfs'] + else: + self.vrfs = {} + + def create_vrf(self, vrf_name): + ''' + Create a new VRF + ''' + vrf_entry = VRF_Entry(name=vrf_name, type="user") + self.vrfs[vrf_name] = vrf_entry.__dict__ + + def delete_vrf(self, vrf_name): + ''' + Delete an existing VRF + ''' + if vrf_name in self.vrfs.keys(): + error = ( + "VRF {vrf_name} is attached to {int}. Interface must be deleted " + "and " + "created under new VRF before VRF can " + "be deleted.") + if 'Port' in self.config.keys(): + port_dict = self.config['Port'] + for encoded_port_name in port_dict.keys(): + temp_port_dict = port_dict[encoded_port_name] + if 'vrf' in temp_port_dict.keys(): + if temp_port_dict['vrf'] == vrf_name: + error.format( + vrf_name=vrf_name, + int=encoded_port_name.replace( + '%2F', + '/')) + raise Exception(error) + + del self.vrfs[vrf_name] + + else: + raise Exception( + "VRF {name} does not exist".format( + name=vrf_name)) + + def check_vrf_exists(self, vrf_name): + ''' + Check if the VRF is configured + ''' + if vrf_name in self.vrfs.keys(): + return True + + return False + + def get_vrf_field_value(self, vrf_name, field_name): + ''' + Get the value for a particular field of the VRF table + ''' + if vrf_name in self.vrfs.keys(): + vrf_entry = VRF_Entry(**self.vrfs[vrf_name]) + return vrf_entry.get_field(field_name) + else: + raise Exception( + "VRF {name} does not exist".format( + name=vrf_name)) + + def get_modified_config(self): + ''' + Generate the modified running-config + ''' + self.config['System']['vrfs'] = self.vrfs + return self.config + + def update_vrf_field_value(self, vrf_name, field, value): + ''' + Update a particular field of the VRF + ''' + if vrf_name in self.vrfs.keys(): + vrf_entry = VRF_Entry(**self.vrfs[vrf_name]) + fields = { + field: value + } + vrf_entry.update_field(**fields) + self.vrfs[vrf_name] = vrf_entry.__dict__ + else: + raise Exception( + "VRF {name} does not exist".format( + name=vrf_name)) + + def delete_vrf_field_value(self, vrf_name, field, value): + ''' + Delete the value of the particular field of the VRF. + ''' + if vrf_name in self.vrfs.keys(): + try: + vrf_entry = VRF_Entry(**self.vrfs[vrf_name]) + fields = { + field: value + } + vrf_entry.delete_field(**fields) + self.vrfs[vrf_name] = vrf_entry.__dict__ + except Exception as error: + raise error + else: + raise Exception( + "VRF {name} does not exist".format( + name=vrf_name)) diff --git a/module_utils/vrfs/aoscx_vrf_entry.py b/module_utils/vrfs/aoscx_vrf_entry.py new file mode 100644 index 0000000..6652e9a --- /dev/null +++ b/module_utils/vrfs/aoscx_vrf_entry.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (C) Copyright 2020 Hewlett Packard Enterprise Development LP. +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class VRF_Entry: + ''' + Represent a VRF entry + ''' + + def __init__(self, **kwargs): + ''' + New VRF Entry + ''' + for key in kwargs.keys(): + setattr(self, key, kwargs[key]) + + def update_field(self, **kwargs): + ''' + Update a particular field of the VRF + ''' + for key in kwargs.keys(): + value = kwargs[key] + if hasattr(self, key): + if isinstance(getattr(self, key), list): + current_values = getattr(self, key) + current_values.append(value) + setattr(self, key, current_values) + elif isinstance(getattr(self, key), dict): + current_values = getattr(self, key) + for k in value.keys(): + val = value[k] + current_values[k] = val + setattr(self, key, current_values) + else: + setattr(self, key, value) + else: + setattr(self, key, value) + + def delete_field(self, **kwargs): + ''' + Delete the value of particular field of the VRF + ''' + for key in kwargs.keys(): + value = kwargs[key] + if hasattr(self, key): + if isinstance(getattr(self, key), list): + current_values = getattr(self, key) + if value in current_values: + current_values.remove(value) + setattr(self, key, current_values) + else: + name = getattr(self, "name") + raise Exception( + "VRF {name} does not have {key} set as {value}".format( + name=name, key=key, value=value)) + elif isinstance(getattr(self, key), dict): + current_values = getattr(self, key) + for k in value: + if k in current_values.keys(): + del current_values[k] + else: + name = getattr(self, "name") + raise Exception( + "VRF {name} does not have {key} with sub option {value}".format( + name=name, key=key, value=value)) + setattr(self, key, current_values) + else: + delattr(self, key) + else: + name = getattr(self, "name") + raise Exception( + "VRF {name} has no field {key}".format( + name=name, key=key)) + + def get_field(self, field): + ''' + Get the value of the particular field of the VRF + ''' + return getattr(self, field, None) diff --git a/terminal_plugins/aoscx.py b/terminal_plugins/aoscx.py index b023910..fe2543c 100644 --- a/terminal_plugins/aoscx.py +++ b/terminal_plugins/aoscx.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # # (C) Copyright 2019 Hewlett Packard Enterprise Development LP. @@ -16,6 +16,11 @@ # specific language governing permissions and limitations # under the License. + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + from ansible.plugins.terminal import TerminalBase from ansible.errors import AnsibleConnectionFailure from ansible.utils.display import Display @@ -23,6 +28,7 @@ display = Display() + class TerminalModule(TerminalBase): ''' Terminal Module class for AOS-CX @@ -40,9 +46,9 @@ def on_open_shell(self): Tasks to be executed immediately after connecting to switch. ''' try: - self._exec_cli_command(b'terminal length 0') + self._exec_cli_command(b'no page') except AnsibleConnectionFailure: - raise AnsibleConnectionFailure('unable to set terminal parameters') + raise AnsibleConnectionFailure('unable to remove terminal paging') try: self._exec_cli_command(b'terminal width 512') @@ -54,7 +60,6 @@ def on_open_shell(self): display.display('WARNING: Unable to set terminal width, ' 'command responses may be truncated') - def on_become(self, passwd=None): ''' Priveleged mode