From da0d43564b1fc08cef573810dd6c8ed2bf4da397 Mon Sep 17 00:00:00 2001 From: tiffany-l-chiapuzio-wong Date: Fri, 20 Mar 2020 08:55:36 -0700 Subject: [PATCH] New modules and bug fixes - This release includes new modules that allow for SSH/CLI commands and configuration. - New Modules - aoscx_command - This module connects to the CX device via SSH and allows CLI commands to be executed. - aoscx_config - This module connects to the CX device via SSH and allows CLI configuration commands to be executed. - aoscx_facts - This module used REST API to retrieve a subset of information from the CX device. - Known Issues - To use the aoscx_facts module, the device must be running firmware version 10.4 or higher. --- README.md | 142 +++++++-- RELEASE-NOTES.md | 18 +- cliconf_plugins/aoscx.py | 138 ++++++++ docs/aoscx_command.md | 127 ++++++++ docs/aoscx_config.md | 215 +++++++++++++ docs/aoscx_facts.md | 46 +++ httpapi_plugins/aoscx.py | 11 +- library/aoscx_command.py | 269 ++++++++++++++++ library/aoscx_config.py | 466 ++++++++++++++++++++++++++++ library/aoscx_facts.py | 162 ++++++++++ module_utils/aoscx.py | 219 +++++++++++-- module_utils/argspec/facts/facts.py | 18 ++ module_utils/facts/facts.py | 76 +++++ module_utils/facts/interfaces.py | 31 ++ module_utils/facts/legacy.py | 255 +++++++++++++++ module_utils/facts/vlans.py | 41 +++ module_utils/facts/vrfs.py | 31 ++ module_utils/providers.py | 104 +++++++ terminal_plugins/aoscx.py | 68 ++++ 19 files changed, 2382 insertions(+), 55 deletions(-) create mode 100644 cliconf_plugins/aoscx.py create mode 100644 docs/aoscx_command.md create mode 100644 docs/aoscx_config.md create mode 100644 docs/aoscx_facts.md create mode 100644 library/aoscx_command.py create mode 100644 library/aoscx_config.py create mode 100644 library/aoscx_facts.py create mode 100644 module_utils/argspec/facts/facts.py create mode 100644 module_utils/facts/facts.py create mode 100644 module_utils/facts/interfaces.py create mode 100644 module_utils/facts/legacy.py create mode 100644 module_utils/facts/vlans.py create mode 100644 module_utils/facts/vrfs.py create mode 100644 module_utils/providers.py create mode 100644 terminal_plugins/aoscx.py diff --git a/README.md b/README.md index a0f808b..1b4a3cb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ aoscx ========= -This Ansible Network role provides a set of platform dependent configuration +This Ansible network role provides a set of platform-dependent configuration management modules specifically designed for the AOS-CX network device. Requirements @@ -16,6 +16,26 @@ Requirements switch(config)# https-server rest access-mode read-write switch(config)# https-server vrf mgmt ``` + +SSH/CLI Modules +--------------- +* To use the SSH/CLI modules `aoscx_config` and `aoscx_command`, SSH access must + be enabled on your AOS-CX device. It is enabled by default. + * If necessary, re-enable SSH access on the device with the following command: + ``` + switch(config)# ssh server vrf mgmt + ``` +* The control machine's `known_hosts` file must contain the target device's public key. + * Alternatively, host key checking by the control machine may be disabled, although this is not recommended. + * To disable host key checking modify the ansible.cfg file (default /etc/ansible/ansible.cfg) to include: + `host_key_checking = false` + +#### Limitations and Notes +* The default command timeout is 30 seconds. If a command takes more than 30 + seconds to execute, the task will time out. + * If you regularly encounter the `command timeout triggered, timeout value + is 30 secs` error, consider setting the environment variable + `ANSIBLE_PERSISTENT_COMMAND_TIMEOUT` to a greater value. See Ansible documentation [here](https://docs.ansible.com/ansible/latest/network/user_guide/network_debug_troubleshooting.html). Installation ------------ @@ -33,28 +53,32 @@ ansible-galaxy install arubanetworks.aoscx_role ``` Inventory Variables --------------- +------------------- The variables that should be defined in your inventory for your AOS-CX host are: * `ansible_host`: IP address of switch in `A.B.C.D` format. For IPv6 hosts use a string and enclose in square brackets E.G. `'[2001::1]'`. * `ansible_user`: Username for switch in `plaintext` format -* `ansible_password`: Password for switch in `plaintext` format -* `ansible_connection`: Must always be set to `httpapi` -* `ansible_network_os`: Must always be set to `aoscx` -* `ansible_httpapi_use_ssl`: Must always be `True` as AOS-CX uses port 443 for REST -* `ansible_httpapi_validate_certs`: Set `True` or `False` depending on if Ansible should attempt to validate certificates -* `ansible_acx_no_proxy`: Set `True` or `False` depending if Ansible should bypass environment proxies to connect to AOS-CX +* `ansible_password`: Password for switch in `plaintext` format +* `ansible_network_os`: Must always be set to `aoscx` +* `ansible_connection`: Set to `httpapi` to use REST API modules, and to `network_cli` to use SSH/CLI modules + * See [below](#using-both-rest-api-and-sshcli-modules-on-a-host) for info on using both REST API modules and SSH/CLI modules on a host +* `ansible_httpapi_use_ssl`: (Only required for REST API modules) Must always be `True` as AOS-CX uses port 443 for REST +* `ansible_httpapi_validate_certs`: (Only required for REST API modules) Set `True` or `False` depending on if Ansible should attempt to validate certificates +* `ansible_acx_no_proxy`: Set to `True` or `False` depending if Ansible should bypass environment proxies to connect to AOS-CX + +### Sample Inventories: -### Sample Inventory: +#### REST API Modules Only: -#### INI +##### INI ```INI -aoscx_1 ansible_host=10.0.0.1 ansible_user=admin ansible_password=password ansible_connection=httpapi ansible_network_os=aoscx ansible_httpapi_validate_certs=False ansible_httpapi_use_ssl=True ansible_acx_no_proxy=True +aoscx_1 ansible_host=10.0.0.1 ansible_user=admin ansible_password=password ansible_network_os=aoscx ansible_connection=httpapi ansible_httpapi_validate_certs=False ansible_httpapi_use_ssl=True ansible_acx_no_proxy=True ``` -#### YAML +##### YAML + ```yaml all: @@ -63,21 +87,43 @@ all: ansible_host: 10.0.0.1 ansible_user: admin ansible_password: password - ansible_connection: httpapi # Do not change ansible_network_os: aoscx + ansible_connection: httpapi # REST API connection method ansible_httpapi_validate_certs: False ansible_httpapi_use_ssl: True ansible_acx_no_proxy: True ``` -Example Playbook ----------------- +#### SSH/CLI Modules Only: + +##### INI + +```INI +aoscx_1 ansible_host=10.0.0.1 ansible_user=admin ansible_password=password ansible_network_os=aoscx ansible_connection=network_cli +``` + +##### YAML + +```yaml +all: + hosts: + aoscx_1: + ansible_host: 10.0.0.1 + ansible_user: admin + ansible_password: password + ansible_network_os: aoscx + ansible_connection: network_cli # SSH connection method +``` + +Example Playbooks +----------------- + +### Including the Role If role installed through [Github](https://github.com/aruba/aoscx-ansible-role) set role to `aoscx-ansible-role`: ```yaml ---- - hosts: all roles: - role: aoscx-ansible-role @@ -94,7 +140,6 @@ If role installed through [Galaxy](https://galaxy.ansible.com/arubanetworks/aosc set role to `arubanetworks.aoscx_role`: ```yaml ---- - hosts: all roles: - role: arubanetworks.aoscx_role @@ -107,9 +152,69 @@ set role to `arubanetworks.aoscx_role`: ipv6: ['2001:db8::1234/64'] ``` +Using Both REST API and SSH/CLI Modules on a Host +------------------------------------------------- + +To use both REST API and SSH/CLI modules on the same host, +you must create separate plays such +that each play uses either only REST API modules or only SSH/CLI modules. +A play cannot mix and match REST API and SSH/CLI module calls. +In each play, `ansible_connection` must possess the appropriate value +according to the modules used. +If the play uses REST API modules, the value should be `httpapi`. +If the play uses SSH/CLI modules, the value should be `network_cli`. + +A recommended approach to successfully using both types of modules for a host +is as follows: +1. Set the host variables such that Ansible will connect to the host using REST API, +like seen [above](#rest-api-modules-only). +2. In the playbook, in each play wherein the SSH/CLI +modules are used, set the `ansible_connection` to `network_cli`. + +The inventory should look something like this: + +```yaml +all: + hosts: + aoscx_1: + ansible_host: 10.0.0.1 + ansible_user: admin + ansible_password: password + ansible_network_os: aoscx + ansible_connection: httpapi # REST API connection method + ansible_httpapi_validate_certs: False + ansible_httpapi_use_ssl: True + ansible_acx_no_proxy: True +``` + +and the playbook like this (note how the second play, which uses the SSH/CLI module `aoscx_command`, +sets the `ansible_connection` value accordingly): + +```yaml +- hosts: all + roles: + - role: arubanetworks.aoscx_role + tasks: + - name: Adding or Updating Banner + aoscx_banner: + banner_type: banner + banner: "Hi!" + +- hosts: all + roles: + - role: arubanetworks.aoscx_role + vars: + ansible_connection: network_cli + tasks: + - name: Execute show run on the switch + aoscx_command: + commands: ['show run'] +``` + + Contribution ------- -At Aruba Networks we're dedicated to ensuring the quality of our products, if you find any +At Aruba Networks we're dedicated to ensuring the quality of our products, so if you find any issues at all please open an issue on our [Github](https://github.com/aruba/aoscx-ansible-role) and we'll be sure to respond promptly! @@ -123,4 +228,5 @@ Author Information - Madhusudan Pranav Venugopal (@madhusudan-pranav-venugopal) - Yang Liu (@yliu-aruba) - Tiffany Chiapuzio-Wong (@tchiapuziowong) + - Derek Wang (@derekwangHPEAruba) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index dc52f7e..171d1a9 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,17 @@ +# 2.1.0 + +## Notable Changes +* New Modules and Bug fixes +* This release includes new modules that allow for SSH/CLI commands and configuration. Refer to module documentation found in [docs/](https://github.com/aruba/aoscx-ansible-role/tree/master/docs). + +## New Modules +* aoscx_command - This module connects to the CX device via SSH and allows CLI commands to be executed. +* aoscx_config - This module connects to the CX device via SSH and allows CLI configuration commands to be executed. +* aoscx_facts - This module used REST API to retrieve a subset of information from the CX device. + +## Known Issues +* To use the aoscx_facts module, the device must be running firmware version 10.4 or higher. + # 2.0.0 ## Notable Changes @@ -13,8 +27,8 @@ ## Known Issues * There is a known issue with the following modules on all platforms and 10.4 firmware version. Please only use these modules with 10.3 at this time: - * aoscx_l3_interface - * aoscx_vlan_interface +** aoscx_l3_interface +** aoscx_vlan_interface * To upload firmware using HTTP server with the module aoscx_upload_firmware, the device must be running firmware version 10.4 or higher. # 1.0.5 diff --git a/cliconf_plugins/aoscx.py b/cliconf_plugins/aoscx.py new file mode 100644 index 0000000..dd250cb --- /dev/null +++ b/cliconf_plugins/aoscx.py @@ -0,0 +1,138 @@ +#!/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) + + +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" +""" + +import json +import re +from itertools import chain + +from ansible.errors import AnsibleConnectionFailure +from ansible.module_utils._text import to_text +from ansible.module_utils.common._collections_compat import Mapping +from ansible.module_utils.network.common.utils import to_list +from ansible.plugins.cliconf import CliconfBase, enable_mode + + +class Cliconf(CliconfBase): + ''' + Cliconf class for AOS-CX + ''' + + def __init__(self, *args, **kwargs): + ''' + init function + ''' + super(Cliconf, self).__init__(*args, **kwargs) + + @enable_mode + 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)) + if source == 'running': + cmd = 'show running-config all' + else: + cmd = 'show configuration' + return self.send_command(cmd) + + @enable_mode + def edit_config(self, command): + ''' + Edit the switch config + ''' + for cmd in chain(['configure terminal'], to_list(command), ['end']): + self.send_command(cmd) + + def get(self, command, prompt=None, answer=None, sendonly=False, + newline=True, check_all=False): + ''' + Get command output from switch + ''' + return self.send_command(command=command, prompt=prompt, answer=answer, + sendonly=sendonly, newline=newline, + check_all=check_all) + + def get_device_info(self): + ''' + Get device info + ''' + device_info = {} + + device_info['network_os'] = 'aruba' + reply = self.get('show version') + data = to_text(reply, errors='surrogate_or_strict').strip() + + match = re.search(r'Version (\S+)', data) + if match: + device_info['network_os_version'] = match.group(1) + + match = re.search(r'^MODEL: (\S+)\),', data, re.M) + if match: + device_info['network_os_model'] = match.group(1) + + reply = self.get('show hostname') + data = to_text(reply, errors='surrogate_or_strict').strip() + + match = re.search(r'^Hostname is (.+)', data, re.M) + if match: + device_info['network_os_hostname'] = match.group(1) + + return device_info + + def get_capabilities(self): + ''' + Get capabilities + ''' + result = super(Cliconf, self).get_capabilities() + return json.dumps(result) + + def run_commands(self, commands=None, check_rc=False): + ''' + Run commands on the switch + ''' + if commands is None: + raise ValueError("'commands' value is required") + responses = list() + for cmd in to_list(commands): + + if not isinstance(cmd, Mapping): + cmd = {'command': cmd} + + try: + out = self.send_command(**cmd) + except AnsibleConnectionFailure as exception: + + if check_rc: + raise + out = getattr(exception, 'err', exception) + + out = to_text(out, errors='surrogate_or_strict') + + responses.append(out) + + return responses + + def set_cli_prompt_context(self): + """ + Make sure we are in the operational cli mode + :return: None + """ + if self._connection.connected: + self._update_cli_prompt_context(config_context=')#') diff --git a/docs/aoscx_command.md b/docs/aoscx_command.md new file mode 100644 index 0000000..5a189b3 --- /dev/null +++ b/docs/aoscx_command.md @@ -0,0 +1,127 @@ +# module: aoscx_command + +description: This module allows execution of CLI commands on AOS-CX devices via SSH connection. +Although this module can execute configuration commands, there is another module designed to execute +just configuration commands, aoscx_config. + +##### NOTES +* `aoscx_command` will not handle commands that repeat forever such as `repeat` +* Besides simple "yes/no" prompts, `aoscx_command` is unable to process commands which require user input + * This includes operations such as password entry + * If a command requires a "yes/no" confirmation, include the line `auto-confirm` at the beginning of the task, like in the below example + * `auto-confirm` does not allow copying AOS-CX images with TFTP +```yaml +- hosts: all + roles: + - role: aoscx-ansible-role + tasks: + - name: VSF Renumber-To with Autoconfirm + aoscx_command: + lines: [ + 'auto-confirm', + 'configure', + 'vsf renumber-to 2', + ] +``` + + +##### ARGUMENTS +```YAML + commands: + description: List of commands to be executed in sequence on the switch. Every command + will attempt to be executed regardless of the success or failure of the previous + command in the list. To execute commands in the 'configure' context, you must include + the 'configure terminal' command or one of its variations before the configuration commands. + 'Show' commands are valid and their output will be printed to the screen, returned by the + module, and optionally saved to a file. The default module timeout is 30 seconds. To change the + command timeout, set the variable 'ansible_command_timeout' to the desired time in seconds. + required: True + type: list + + wait_for: + description: A list of conditions to wait to be satisfied before continuing execution. + Each condition must include a test of the 'result' variable, which contains the output + results of each already-executed command in the 'commands' list. 'result' is a list + such that result[0] contains the output from commands[0], results[1] contains the output + from commands[1], and so on. + required: False + type: list + + 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'] + required: False + type: str + + retries: + description: Maximum number of retries to check for the expected prompt. + default: 10 + required: False + type: int + + interval: + description: Interval between retries, in seconds. + default: 1 + required: False + type: int + + 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. + 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'. + default: json + choices: ['json', 'plain-text'] + required: False + type: str +``` + +##### EXAMPLES +```YAML +- name: Execute show commands and configure commands, and output results to file in plaintext + aoscx_command: + commands: ['show run', + 'show vsf', + 'show interface 1/1/1', + 'config', + 'interface 1/1/2', + 'no shut', + 'ip address 10.10.10.10/24', + 'routing', + 'ip address 10.10.10.11/24', + 'exit', + 'vlan 2', + 'end'] + output_file: /users/Home/configure.cfg + output_file_format: plain-text + +- name: Show running-config and show interface mgmt, and pass only if all (both) results match + aoscx_command: + commands: + - 'show run' + - 'show int mgmt' + wait_for: + - result[0] contains "vlan " + - result[1] contains "127.0.0.1" + match: all + retries: 5 + interval: 5 + +- name: Show all available commands and output them to a file (as JSON) + aoscx_command: + commands: ['list'] + output_file: /users/Home/config_list.cfg + +- name: Run ping command with increased command timeout + vars: + - ansible_command_timeout: 60 + aoscx_command: + commands: + - ping 10.80.2.120 vrf mgmt repetitions 100 +``` \ No newline at end of file diff --git a/docs/aoscx_config.md b/docs/aoscx_config.md new file mode 100644 index 0000000..84f2ffd --- /dev/null +++ b/docs/aoscx_config.md @@ -0,0 +1,215 @@ +# module: aoscx_config + +description: This module allows configuration of running-configs on AOS-CX devices via SSH connection. + +##### ARGUMENTS +```YAML + lines: + description: + - List of configuration commands to be executed. If "parents" is specified, these + are the child lines contained under/within the parent entry. If "parents" is not + specified, these lines will be checked and/or placed under the global config level. + These commands must correspond with what would be found in the device's running-config. + required: False + type: list + + parents: + description: + - Parent lines that identify the configuration section or context under which the + "lines" lines should be checked and/or placed. + required: False + type: list + + src: + description: + - Path to the file containing the configuration to load into the device. The path can + be either a full system path to the configuration file if the value starts with "/" + or a path relative to the directory containing the playbook. This argument is mutually + exclusive with the "lines" and "parents" arguments. This src file must have same + 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 + + before: + description: + - Commands to be executed prior to execution of the parent and child lines. This option + can be used to guarantee idempotency. + required: False + type: list + + after: + description: + - Commands to be executed following the execution of the parent and child lines. This + option can be used to guarantee idempotency. + required: False + type: list + + match: + description: + - Specifies the method of matching. Matching is the comparison against the existing + running-config to determine whether changes need to be applied. + If "match" is set to "line," commands are matched line by line. + If "match" is set to "strict," command lines are matched with respect to position. + If "match" is set to "exact," command lines must be an equal match. + If "match" is set to "none," the module will not attempt to compare the source + configuration with the running-config on the remote device. + default: line + choices: ['line', 'strict', 'exact', 'none'] + required: False + type: str + + replace: + description: + - Specifies the approach the module will take when performing configuration on the + device. + If "replace" is set to "line," then only the differing and missing configuration lines + are pushed to the device. + If "replace" is set to "block," then the entire command block is pushed to the device + if there is any differing or missing line at all. + default: line + choices: ['line', 'block'] + required: False + type: str + + backup: + description: + - Specifies whether a full backup of the existing running-config on the device will be + performed before any changes are potentially made. If the "backup_options" value is not + specified, the backup file is written to the "backup" folder in the playbook root + directory. If the directory does not exist, it is created. + required: False + type: bool + default: False + + backup_options: + description: + - File path and name options for backing up the existing running-config. + To be used with "backup." + suboptions: + filename: + description: + - Name of file in which the running-config will be saved. + required: False + type: str + dir_path: + description: + - Path to directory in which the backup file should reside. + required: False + type: str + type: dict + + running_config: + description: + - Specifies an alternative running-config to be used as the base config for matching. The + module, by default, will connect to the device and retrieve the current running-config + to use as the basis for comparison against the source. This argument is handy for times + when it is not desirable to have the task get the current running-config, and instead use + another config for matching. + aliases: ['config'] + required: False + type: str + + save_when: + description: + - Specifies when to copy the running-config to the startup-config. When changes are made to + the device running-configuration, the changes are not copied to non-volatile storage by default. + If "save_when" is set to "always," the running-config will unconditionally be copied to + startup-config. + If "save_when" is set to "never," the running-config will never be copied to startup-config. + If "save_when" is set to "modified," the running-config will be copied to startup-config + if the two differ. + If "save_when" is set to "changed," the running-config will be copied to startup-config + if the task modified the running-config. + default: never + choices: ['always', 'never', 'modified', 'changed'] + required: False + type: str + + diff_against: + description: + - When using the "ansible-playbook --diff" command line argument this module can generate + diffs against different sources. This argument specifies the particular config against + which a diff of the running-config will be performed. + If "diff_against" is set to "startup," the module will return the diff of the running-config + against the startup configuration. + If "diff_against" is set to "intended," the module will return the diff of the running-config + against the configuration provided in the "intended_config" argument. + If "diff_against" is set to "running," the module will return before and after diff of the + running-config with respect to any changes made to the device configuration. + choices: ['startup', 'intended', 'running'] + required: False + type: str + + diff_ignore_lines: + description: + - Specifies one or more lines that should be ignored during the diff. This is used to + ignore lines in the configuration that are automatically updated by the system. This + argument takes a list of regular expressions or exact commands. + required: False + type: list + + intended_config: + description: + - Path to file containing the intended configuration that the device should conform to, and + that is used to check the final running-config against. To be used with "diff_against," + which should be set to "intended." + required: False + type: str +``` + +##### EXAMPLES +```YAML +- name: First delete VLAN 44, then configure VLAN 45, and lastly create VLAN 46 + aoscx_config: + before: + - no vlan 44 + parents: + - vlan 45 + lines: + - name testvlan + - description test_vlan + after: + - vlan 46 + +- name: Back up running-config, then create VLAN 100, and save running-config to startup-config if change was made + aoscx_config: + backup: True + lines: + - vlan 100 + backup_options: + filename: backup.cfg + dir_path: /users/Home/ + save_when: changed + +- name: Compare running-config with saved config + aoscx_config: + diff_against: intended + intended_config: /users/Home/backup.cfg + +- name: Configure VLAN 2345 and compare resulting running-config with previous running-config + aoscx_config: + lines: + - vlan 2345 + diff_against: running + +- name: Upload a config from local system file onto device + aoscx_config: + src: /users/Home/golden.cfg + +- name: Update interface 1/1/4, matching only if both "parents" and "lines" are present + aoscx_config: + lines: + - ip address 4.4.4.5/24 + parents: interface 1/1/4 + match: strict + +- name: Configure a multi-line banner + aoscx_config: + lines: + - hello this is a banner_motd + - this is banner line 2 banner_motd + - this is banner line 3 banner_motd + before: "banner motd `" + after: "`" +``` \ No newline at end of file diff --git a/docs/aoscx_facts.md b/docs/aoscx_facts.md new file mode 100644 index 0000000..77f966f --- /dev/null +++ b/docs/aoscx_facts.md @@ -0,0 +1,46 @@ +# module: aoscx_facts + +description: This module retrieves facts from Aruba devices running the AOS-CX operating system. +Facts will be printed out when the playbook execution is done with increased verbosity. + +##### ARGUMENTS +```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 + returns all facts, which may be an intensive process. + options: ['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' + 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'] + required: False + type: list +``` + +##### EXAMPLES +```YAML +- name: Retrieve all information from the device and save into a variable "facts_output" + aoscx_facts: + register: facts_output + +- name: Retrieve power supply and domain name info from the device + aoscx_facts: + gather_subset: ['power_supplies', 'domain_name'] + +- name: Retrieve VRF info, host name, and fan info from the device and save into a variable + aoscx_facts: + gather_subset: ['host_name', 'fans'] + gather_network_resources: ['vrfs'] + register: facts_subset_output +``` \ No newline at end of file diff --git a/httpapi_plugins/aoscx.py b/httpapi_plugins/aoscx.py index 6b707a9..6a3e97b 100644 --- a/httpapi_plugins/aoscx.py +++ b/httpapi_plugins/aoscx.py @@ -7,8 +7,8 @@ from __future__ import (absolute_import, division, print_function) -__metaclass__ = type +__metaclass__ = type DOCUMENTATION = """ --- @@ -42,6 +42,7 @@ # Removed the exception handling as only required pre 2.8 and collection is # supported in >= 2.9 from ansible.utils.display import Display + display = Display() @@ -104,7 +105,8 @@ def handle_response(self, response, response_data): if response_data: if 'errors' in response_data: errors = response_data['errors']['error'] - error_text = '\n'.join((error['error-message'] for error in errors)) # NOQA + error_text = '\n'.join( + (error['error-message'] for error in errors)) # NOQA else: error_text = response_data @@ -115,3 +117,8 @@ def handle_response(self, response, response_data): if auth: self.connection._auth = auth return response_data_json + + def get_capabilities(self): + result = {} + + return json.dumps(result) diff --git a/library/aoscx_command.py b/library/aoscx_command.py new file mode 100644 index 0000000..6b94d97 --- /dev/null +++ b/library/aoscx_command.py @@ -0,0 +1,269 @@ +#!/usr/bin/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 + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'certified' +} + +DOCUMENTATION = ''' +--- +module: aoscx_command +version_added: "2.9" +short_description: Logs in and executes CLI commands on AOS-CX device via SSH connection +description: + - This module allows execution of CLI commands on AOS-CX devices via SSH connection +author: Aruba Networks (@ArubaNetworks) +options: + + commands: + description: List of commands to be executed in sequence on the switch. Every command + will attempt to be executed regardless of the success or failure of the previous + command in the list. To execute commands in the 'configure' context, you must include + the 'configure terminal' command or one of its variations before the configuration commands. + 'Show' commands are valid and their output will be printed to the screen, returned by the + module, and optionally saved to a file. The default module timeout is 30 seconds. To change the + command timeout, set the variable 'ansible_command_timeout' to the desired time in seconds. + required: True + type: list + + wait_for: + description: A list of conditions to wait to be satisfied before continuing execution. + Each condition must include a test of the 'result' variable, which contains the output + results of each already-executed command in the 'commands' list. 'result' is a list + such that result[0] contains the output from commands[0], results[1] contains the output + from commands[1], and so on. + required: False + type: list + + 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'] + required: False + type: str + + retries: + description: Maximum number of retries to check for the expected prompt. + default: 10 + required: False + type: int + + interval: + description: Interval between retries, in seconds. + default: 1 + required: False + type: int + + 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. + 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'. + default: json + choices: ['json', 'plain-text'] + required: False + type: str +''' # NOQA + +EXAMPLES = ''' +- name: Execute show commands and configure commands, and output results to file in plaintext + aoscx_command: + commands: ['show run', + 'show vsf', + 'show interface 1/1/1', + 'config', + 'interface 1/1/2', + 'no shut', + 'ip address 10.10.10.10/24', + 'routing', + 'ip address 10.10.10.11/24', + 'exit', + 'vlan 2', + 'end'] + output_file: /users/Home/configure.cfg + output_file_format: plain-text + +- name: Show running-config and show interface mgmt, and pass only if all (both) results match + aoscx_command: + commands: + - 'show run' + - 'show int mgmt' + wait_for: + - result[0] contains "vlan " + - result[1] contains "127.0.0.1" + match: all + retries: 5 + interval: 5 + +- name: Show all available commands and output them to a file (as JSON) + aoscx_command: + commands: ['list'] + output_file: /users/Home/config_list.cfg + +- name: Run ping command with increased command timeout + vars: + - ansible_command_timeout: 60 + aoscx_command: + commands: + - ping 10.80.2.120 vrf mgmt repetitions 100 +''' # NOQA + +RETURN = r''' +stdout: + description: The set of responses from the commands + returned: always apart from low level errors (such as action plugin) + type: list + sample: ['...', '...'] +stdout_lines: + description: The value of stdout split into a list + returned: always apart from low level errors (such as action plugin) + type: list + sample: [['...', '...'], ['...'], ['...']] +''' + +import time +import json +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.parsing import Conditional +from ansible.module_utils.network.common.utils import to_lines, ComplexList +from ansible.module_utils.aoscx import run_commands, aoscx_argument_spec + + +def transform_commands(module): + ''' + Transform the command to a complex list + ''' + transform = ComplexList(dict( + command=dict(key=True), + prompt=dict(type='list'), + answer=dict(type='list'), + newline=dict(type='bool', default=True), + sendonly=dict(type='bool', default=False), + check_all=dict(type='bool', default=False), + ), module) + + return transform(module.params['commands']) + + +def parse_commands(module, warnings): + ''' + Parse the command + ''' + commands = transform_commands(module) + + return commands + + +def main(): + ''' + Main entry point to the module + ''' + + argument_spec = dict( + commands=dict(type='list', required=True), + wait_for=dict(type='list', aliases=['waitfor']), + match=dict(default='all', choices=['all', 'any']), + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int'), + output_file=dict(type='str', default=None), + output_file_format=dict(type='str', default='json', + choices=['json', 'plain-text']) + ) + + argument_spec.update(aoscx_argument_spec) + + warnings = list() + + result = {'changed': False, 'warnings': warnings} + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + commands = parse_commands(module, warnings) + wait_for = module.params['wait_for'] or list() + + try: + conditionals = [Conditional(c) for c in wait_for] + except AttributeError as exc: + module.fail_json(msg=to_text(exc)) + + retries = module.params['retries'] + interval = module.params['interval'] + match = module.params['match'] + + while retries >= 0: + responses = run_commands(module, commands) + + for item in list(conditionals): + if item(responses): + if match == 'any': + conditionals = list() + break + conditionals.remove(item) + + if not conditionals: + break + + time.sleep(interval) + retries -= 1 + + if conditionals: + failed_conditions = [item.raw for item in conditionals] + msg = 'One or more conditional statements have not been satisfied' + module.fail_json(msg=msg, failed_conditions=failed_conditions) + + commands_list = [] + for command in commands: + commands_list.append(command['command']) + + if module.params['output_file'] is not None: + output_file_format = str(module.params['output_file_format']) + if output_file_format == 'json': + output_list = [] + for i, command in enumerate(commands_list): + output_dict = {} + output_dict['command'] = command + output_dict['response'] = responses[i] + output_list.append(output_dict) + output_file = str(module.params['output_file']) + with open(output_file, 'w') as output: + json.dump(output_list, output, indent=4) + output.write("\n") + else: + output_file = str(module.params['output_file']) + with open(output_file, 'w') as output: + for i, command in enumerate(commands_list): + output.write("command: ") + output.write(command) + output.write("\n") + output.write("response: ") + output.write(str(responses[i])) + output.write("\n") + output.write("------------------------------------------") + output.write("\n") + + result.update({ + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/library/aoscx_config.py b/library/aoscx_config.py new file mode 100644 index 0000000..8426622 --- /dev/null +++ b/library/aoscx_config.py @@ -0,0 +1,466 @@ +#!/usr/bin/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 + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'certified' +} + +DOCUMENTATION = ''' +--- +module: aoscx_config +version_added: "2.9" +short_description: Logs in and executes configuration commands on AOS-CX device via SSH connection +description: + - This module allows configuration of running-configs on AOS-CX devices via SSH connection +author: Aruba Networks (@ArubaNetworks) +options: + + lines: + description: + - List of configuration commands to be executed. If "parents" is specified, these + are the child lines contained under/within the parent entry. If "parents" is not + specified, these lines will be checked and/or placed under the global config level. + These commands must correspond with what would be found in the device's running-config. + required: False + type: list + + parents: + description: + - Parent lines that identify the configuration section or context under which the + "lines" lines should be checked and/or placed. + required: False + type: list + + src: + description: + - Path to the file containing the configuration to load into the device. The path can + be either a full system path to the configuration file if the value starts with "/" + or a path relative to the directory containing the playbook. This argument is mutually + exclusive with the "lines" and "parents" arguments. This src file must have same + 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 + + before: + description: + - Commands to be executed prior to execution of the parent and child lines. This option + can be used to guarantee idempotency. + required: False + type: list + + after: + description: + - Commands to be executed following the execution of the parent and child lines. This + option can be used to guarantee idempotency. + required: False + type: list + + match: + description: + - Specifies the method of matching. Matching is the comparison against the existing + running-config to determine whether changes need to be applied. + If "match" is set to "line," commands are matched line by line. + If "match" is set to "strict," command lines are matched with respect to position. + If "match" is set to "exact," command lines must be an equal match. + If "match" is set to "none," the module will not attempt to compare the source + configuration with the running-config on the remote device. + default: line + choices: ['line', 'strict', 'exact', 'none'] + required: False + type: str + + replace: + description: + - Specifies the approach the module will take when performing configuration on the + device. + If "replace" is set to "line," then only the differing and missing configuration lines + are pushed to the device. + If "replace" is set to "block," then the entire command block is pushed to the device + if there is any differing or missing line at all. + default: line + choices: ['line', 'block'] + required: False + type: str + + backup: + description: + - Specifies whether a full backup of the existing running-config on the device will be + performed before any changes are potentially made. If the "backup_options" value is not + specified, the backup file is written to the "backup" folder in the playbook root + directory. If the directory does not exist, it is created. + required: False + type: bool + default: False + + backup_options: + description: + - File path and name options for backing up the existing running-config. + To be used with "backup." + suboptions: + filename: + description: + - Name of file in which the running-config will be saved. + required: False + type: str + dir_path: + description: + - Path to directory in which the backup file should reside. + required: False + type: str + type: dict + + running_config: + description: + - Specifies an alternative running-config to be used as the base config for matching. The + module, by default, will connect to the device and retrieve the current running-config + to use as the basis for comparison against the source. This argument is handy for times + when it is not desirable to have the task get the current running-config, and instead use + another config for matching. + aliases: ['config'] + required: False + type: str + + save_when: + description: + - Specifies when to copy the running-config to the startup-config. When changes are made to + the device running-configuration, the changes are not copied to non-volatile storage by default. + If "save_when" is set to "always," the running-config will unconditionally be copied to + startup-config. + If "save_when" is set to "never," the running-config will never be copied to startup-config. + If "save_when" is set to "modified," the running-config will be copied to startup-config + if the two differ. + If "save_when" is set to "changed," the running-config will be copied to startup-config + if the task modified the running-config. + default: never + choices: ['always', 'never', 'modified', 'changed'] + required: False + type: str + + diff_against: + description: + - When using the "ansible-playbook --diff" command line argument this module can generate + diffs against different sources. This argument specifies the particular config against + which a diff of the running-config will be performed. + If "diff_against" is set to "startup," the module will return the diff of the running-config + against the startup configuration. + If "diff_against" is set to "intended," the module will return the diff of the running-config + against the configuration provided in the "intended_config" argument. + If "diff_against" is set to "running," the module will return before and after diff of the + running-config with respect to any changes made to the device configuration. + choices: ['startup', 'intended', 'running'] + required: False + type: str + + diff_ignore_lines: + description: + - Specifies one or more lines that should be ignored during the diff. This is used to + ignore lines in the configuration that are automatically updated by the system. This + argument takes a list of regular expressions or exact commands. + required: False + type: list + + intended_config: + description: + - Path to file containing the intended configuration that the device should conform to, and + that is used to check the final running-config against. To be used with "diff_against," + which should be set to "intended." + required: False + type: str +''' # NOQA + +EXAMPLES = ''' +- name: First delete VLAN 44, then configure VLAN 45, and lastly create VLAN 46 + aoscx_config: + before: + - no vlan 44 + parents: + - vlan 45 + lines: + - name testvlan + - description test_vlan + after: + - vlan 46 + +- name: Back up running-config, then create VLAN 100, and save running-config to startup-config if change was made + aoscx_config: + backup: True + lines: + - vlan 100 + backup_options: + filename: backup.cfg + dir_path: /users/Home/ + save_when: changed + +- name: Compare running-config with saved config + aoscx_config: + diff_against: intended + intended_config: /users/Home/backup.cfg + +- name: Configure VLAN 2345 and compare resulting running-config with previous running-config + aoscx_config: + lines: + - vlan 2345 + diff_against: running + +- name: Upload a config from local system file onto device + aoscx_config: + src: /users/Home/golden.cfg + +- name: Update interface 1/1/4, matching only if both "parents" and "lines" are present + aoscx_config: + lines: + - ip address 4.4.4.5/24 + parents: interface 1/1/4 + match: strict + +- name: Configure a multi-line banner + aoscx_config: + lines: + - hello this is a banner_motd + - this is banner line 2 banner_motd + - this is banner line 3 banner_motd + before: "banner motd `" + after: "`" +''' # NOQA + +RETURN = r''' # ''' + +from ansible.module_utils.aoscx import run_commands, get_config, load_config +from ansible.module_utils.aoscx import aoscx_argument_spec +from ansible.module_utils.aoscx import check_args as aoscx_check_args +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.config import NetworkConfig, dumps + + +def get_running_config(module, config=None): + ''' + Gets the running-config from the switch + ''' + contents = module.params['running_config'] + if not contents: + if config: + contents = config + else: + contents = get_config(module) + return NetworkConfig(contents=contents) + + +def get_candidate(module): + ''' + Gets config candidate + ''' + candidate = NetworkConfig() + + if module.params['src']: + candidate.loadfp(module.params['src']) + elif module.params['lines']: + parents = module.params['parents'] or list() + candidate.add(module.params['lines'], parents=parents) + return candidate + + +def save_config(module, result): + ''' + Saves config to memory + ''' + result['changed'] = True + if not module.check_mode: + run_commands(module, 'write memory') + else: + module.warn('Skipping command `write memory` ' + 'due to check_mode. Configuration not copied to ' + 'non-volatile storage') + + +def main(): + """ main entry point for module execution + """ + backup_spec = dict( + filename=dict(), + dir_path=dict(type='path') + ) + argument_spec = dict( + src=dict(type='path'), + + lines=dict(aliases=['commands'], type='list'), + parents=dict(type='list'), + + before=dict(type='list'), + after=dict(type='list'), + + match=dict(default='line', + choices=['line', 'strict', 'exact', 'none']), + replace=dict(default='line', choices=['line', 'block']), + + running_config=dict(aliases=['config']), + intended_config=dict(), + + backup=dict(type='bool', default=False), + backup_options=dict(type='dict', options=backup_spec), + + save_when=dict(choices=['always', 'never', 'modified', 'changed'], + default='never'), + + diff_against=dict(choices=['running', 'startup', 'intended']), + diff_ignore_lines=dict(type='list'), + ) + + argument_spec.update(aoscx_argument_spec) + + mutually_exclusive = [('lines', 'src'), + ('parents', 'src')] + + required_if = [('match', 'strict', ['lines']), + ('match', 'exact', ['lines']), + ('replace', 'block', ['lines']), + ('diff_against', 'intended', ['intended_config'])] + + module = AnsibleModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + required_if=required_if, + supports_check_mode=True) + + warnings = list() + aoscx_check_args(module, warnings) + result = {'changed': False, 'warnings': warnings} + + config = None + + if module.params['diff_against'] is not None: + module._diff = True + + if module.params['backup'] or (module._diff and + module.params['diff_against'] == 'running'): + contents = get_config(module) + config = NetworkConfig(contents=contents) + if module.params['backup']: + result['__backup__'] = contents + result['backup_options'] = module.params['backup_options'] + if module.params['backup_options']: + if 'dir_path' in module.params['backup_options']: + dir_path = module.params['backup_options']['dir_path'] + else: + dir_path = "" + if 'filename' in module.params['backup_options']: + filename = module.params['backup_options']['filename'] + else: + filename = "backup.cfg" + + with open(dir_path+'/'+filename, 'w') as backupfile: + backupfile.write(contents) + backupfile.write("\n") + + if any((module.params['src'], module.params['lines'])): + match = module.params['match'] + replace = module.params['replace'] + + candidate = get_candidate(module) + + if match != 'none': + config = get_running_config(module, config) + path = module.params['parents'] + configobjs = candidate.difference( + config, match=match, replace=replace, path=path) + else: + configobjs = candidate.items + + if configobjs: + commands = dumps(configobjs, 'commands').split('\n') + + if module.params['before']: + commands[:0] = module.params['before'] + + if module.params['after']: + commands.extend(module.params['after']) + + result['commands'] = commands + result['updates'] = commands + + if not module.check_mode: + load_config(module, commands) + + result['changed'] = True + + running_config = None + startup_config = None + + diff_ignore_lines = module.params['diff_ignore_lines'] + if diff_ignore_lines is None: + diff_ignore_lines = [] + + diff_ignore_lines.append("Current configuration:") + diff_ignore_lines.append("Startup configuration:") + + if module.params['save_when'] == 'always': + save_config(module, result) + elif module.params['save_when'] == 'modified': + output = run_commands(module, + ['show running-config', 'show startup-config']) + + running_config = NetworkConfig( + contents=output[0], ignore_lines=diff_ignore_lines) + startup_config = NetworkConfig( + contents=output[1], ignore_lines=diff_ignore_lines) + + if running_config.sha1 != startup_config.sha1: + save_config(module, result) + elif module.params['save_when'] == 'changed': + if result['changed']: + save_config(module, result) + + if module._diff: + if not running_config: + output = run_commands(module, 'show running-config') + contents = output[0] + else: + contents = running_config.config_text + + # recreate the object in order to process diff_ignore_lines + running_config = NetworkConfig( + contents=contents, ignore_lines=diff_ignore_lines) + + if module.params['diff_against'] == 'running': + if module.check_mode: + module.warn("unable to perform diff against " + "running-config due to check mode") + contents = None + else: + contents = config.config_text + + elif module.params['diff_against'] == 'startup': + if not startup_config: + output = run_commands(module, 'show startup-config') + contents = output[0] + else: + contents = startup_config.config_text + + elif module.params['diff_against'] == 'intended': + with open(module.params['intended_config'], 'r') as intended_file: + contents = intended_file.read() + if contents is not None: + base_config = NetworkConfig( + contents=contents, ignore_lines=diff_ignore_lines) + + if running_config.sha1 != base_config.sha1: + result.update({ + 'changed': True, + 'diff': {'before': str(base_config), + 'after': str(running_config)} + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/library/aoscx_facts.py b/library/aoscx_facts.py new file mode 100644 index 0000000..54c6fa7 --- /dev/null +++ b/library/aoscx_facts.py @@ -0,0 +1,162 @@ +#!/usr/bin/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) + + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'certified' +} + +DOCUMENTATION = ''' +--- +module: aoscx_facts +version_added: "2.9" +short_description: Collects facts from remote AOS-CX device +description: + - This module retrieves facts from Aruba devices running the AOS-CX operating system. + Facts will be printed out when the playbook execution is done with increased verbosity. +author: Aruba Networks (@ArubaNetworks) +options: + + 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 + returns all facts, which may be an intensive process. + options: ['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' + 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'] + required: False + type: list +''' # NOQA + +EXAMPLES = ''' +- name: Retrieve all information from the device and save into a variable "facts_output" + aoscx_facts: + register: facts_output + +- name: Retrieve power supply and domain name info from the device + aoscx_facts: + gather_subset: ['power_supplies', 'domain_name'] + +- name: Retrieve VRF info, host name, and fan info from the device and save into a variable + aoscx_facts: + gather_subset: ['host_name', 'fans'] + gather_network_resources: ['vrfs'] + register: facts_subset_output +''' # NOQA + +RETURN = r''' +ansible_net_gather_subset: + description: The list of fact subsets collected from the device + returned: always + type: list +ansible_net_gather_network_resources: + description: The list of fact for network resource subsets collected from the device + returned: when the resource is configured + type: list +# default +ansible_net_domain_name: + description: The domain name returned from the device + returned: always + type: str +ansible_net_hostname: + description: The configured hostname of the device + returned: always + type: str +ansible_net_platform_name: + description: The platform name returned from the device + returned: always + type: str +ansible_net_product_info: + description: The product system information returned from the device + returned: always + type: dict +ansible_net_resource_utilization: + description: The resource utilization of the remote device + returned: always + type: dict +ansible_net_software_images: + description: The software images on the remote device + returned: always + type: dict +ansible_net_software_info: + description: The details of the software image running on the remote device + returned: always + type: dict +ansible_net_software_version: + description: The software version running on the remote device + returned: always + type: str +# hardware +ansible_net_fans: + description: The fan information returned from the device + returned: always + type: dict +ansible_net_power_supplies: + description: All power supplies available on the device + returned: always + type: dict +# interfaces +ansible_net_interfaces: + description: A dictionary of all interfaces running on the system + returned: always + type: dict +ansible_net_mgmt_intf_status: + description: A dictionary of management interfaces running on the system + returned: always + 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 + + +def main(): + """ + Main entry point for module execution + :returns: ansible_facts + """ + argument_spec = { + 'gather_subset': dict(default=['!config'], type='list'), + 'gather_network_resources': dict(type='list'), + } + argument_spec.update(aoscx_http_argument_spec) + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + module._connection = get_connection(module) #noqa + + warnings = [] + if module.params["gather_subset"] == "!config": + warnings.append( + 'default value for `gather_subset` will be changed ' + 'to `min` from `!config` v2.11 onwards') + + result = Facts(module).get_facts() + + ansible_facts, additional_warnings = result + warnings.extend(additional_warnings) + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == '__main__': + main() diff --git a/module_utils/aoscx.py b/module_utils/aoscx.py index 7618ac5..08a71d8 100644 --- a/module_utils/aoscx.py +++ b/module_utils/aoscx.py @@ -12,33 +12,138 @@ import copy import json import requests +import re 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.connection import Connection, ConnectionError +from ansible.module_utils.network.common.utils import to_list, ComplexList +from ansible.module_utils.connection import exec_command, Connection, ConnectionError _DEVICE_CONNECTION = None +_DEVICE_CONFIGS = {} + +aoscx_provider_spec = { + 'host': dict(), + 'port': dict(type='int'), + 'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True), + 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + 'authorize': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']), type='bool'), + 'auth_pass': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS']), no_log=True), + 'timeout': dict(type='int') +} + +aoscx_http_provider_spec = { + 'host': dict(), + 'port': dict(type='int'), + 'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True) +} + +aoscx_argument_spec = { + 'provider': dict(type='dict', options=aoscx_provider_spec, removed_in_version=2.14), +} + +aoscx_http_argument_spec = { + '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 + ''' + flags = [] if flags is None else flags + + cmd = 'show running-config ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + try: + return _DEVICE_CONFIGS[cmd] + 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')) + 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')) + + 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) + + exec_command(module, 'end') + + +def sanitize(resp): + ''' + Sanitizes the string to remove additiona white spaces + ''' + # Takes response from device and adjusts leading whitespace to just 1 space + cleaned = [] + for line in resp.splitlines(): + 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 @property def _connection(self): + ''' + Creates HTTP API connection + ''' if not self._connection_obj: self._connection_obj = Connection(self._module._socket_path) return self._connection_obj def get(self, url, data=None): + ''' + GET REST call + ''' res = self._connection.send_request(data=data, method='GET', path=url) return res def put(self, url, data=None, headers={}): + ''' + PUT REST call + ''' return self._connection.send_request(data=data, method='PUT', path=url, headers=headers) def post(self, url, data=None, headers={}): + ''' + POST REST call + ''' return self._connection.send_request(data=data, method='POST', path=url, headers=headers) @@ -73,67 +178,96 @@ def file_upload(self, url, files, headers={}): raise ConnectionError(error_text, code=res.status_code) return res - -def get_connection(module): +def get_connection(module, is_cli=False): + ''' + Returns the connection plugin + ''' global _DEVICE_CONNECTION if not _DEVICE_CONNECTION: - conn = HttpApi(module) + if is_cli: + if hasattr(module, '_aoscx_connection'): + _DEVICE_CONNECTION = module._aoscx_connection + return module._aoscx_connection + module._aoscx_connection = Connection(module._socket_path) + _DEVICE_CONNECTION = module._aoscx_connection + return module._aoscx_connection + else: + conn = HttpApi(module) _DEVICE_CONNECTION = conn return _DEVICE_CONNECTION def get(module, url, data=None): + ''' + Perform GET REST call + ''' conn = get_connection(module) res = conn.get(url, data) return res def put(module, url, data=None, headers={}): + ''' + Perform PUT REST call + ''' conn = get_connection(module) res = conn.put(url, data, headers) return res def post(module, url, data=None, headers={}): + ''' + Perform POST REST call + ''' conn = get_connection(module) res = conn.post(url, data, headers) return res def file_upload(module, url, files, headers={}): + ''' + Upload File through REST + ''' conn = get_connection(module) res = conn.file_upload(url, files, headers) return res - -class Cli: - def __init__(self, module): - self._module = module - self._connection_obj = None - - @property - def _connection(self): - if not self._connection_obj: - self._connection_obj = Connection(self._module._socket_path) - return self._connection_obj - - def run_commands(self, commands, check_rc=True): - """Run list of commands on remote device and return results - """ - connection = self._connection_obj - try: - response = connection.run_commands(commands=commands, - check_rc=check_rc) - except ConnectionError as exc: - self._module.fail_json(msg=to_text(exc, - errors='surrogate_then_replace') - ) - return response +def to_command(module, commands): + + ''' + Convert command to ComplexList + ''' + transform = ComplexList(dict( + command=dict(key=True), + prompt=dict(type='list'), + answer=dict(type='list'), + newline=dict(type='bool', default=True), + sendonly=dict(type='bool', default=False), + check_all=dict(type='bool', default=False), + ), module) + + return transform(to_list(commands)) + +def run_commands(module, commands, check_rc=False): + ''' + Execute command on the switch + ''' + conn = get_connection(module, True) + try: + return conn.run_commands(commands=commands, check_rc=check_rc) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc)) class ArubaAnsibleModule: + ''' + Aruba ansible mdule wrapper class + ''' def __init__(self, module_args, store_config=True): + ''' + module init function + ''' self.module = AnsibleModule( argument_spec=module_args, @@ -163,23 +297,34 @@ def __init__(self, module_args, store_config=True): "firmware version is 10.03") def get_switch_platform(self): + ''' + Returns the switch platform + ''' platform_url = '/rest/v1/system?attributes=platform_name' platform = get(self.module, platform_url) self.switch_platform = platform["platform_name"] def get_switch_firmware_version(self): + ''' + Returns the switch firmware + ''' firmware_url = '/rest/v1/firmware' firmware_versions = get(self.module, firmware_url) self.switch_current_firmware = firmware_versions["current_version"] def get_firmware_upgrade_status(self): + ''' + Returns the firmware upgrade status + ''' fimrware_status_url = '/rest/v1/firmware/status' firmware_update_status = get(self.module, fimrware_status_url) return firmware_update_status def get_switch_config(self, config_name='running-config', store_config=True): - + ''' + Returns the switch config + ''' config_url = '/rest/v1/fullconfigs/{cfg}'.format(cfg=config_name) running_config = get(self.module, config_url) @@ -192,7 +337,9 @@ def get_switch_config(self, config_name='running-config', def copy_switch_config_to_remote_location(self, config_name, config_type, destination, vrf): - + ''' + TFTP switch config to TFTP server + ''' config_url = ('/rest/v1/fullconfigs/' '{cfg}?to={dest}&type={type}' '&vrf={vrf}'.format(cfg=config_name, @@ -205,7 +352,9 @@ def copy_switch_config_to_remote_location(self, config_name, config_type, def tftp_switch_config_from_remote_location(self, config_file_location, config_name, vrf): - + ''' + TFTP switch config from TFTP server + ''' config_url = ('/rest/v1/fullconfigs/' '{cfg}?from={dest}&vrf={vrf}' ''.format(cfg=config_name, @@ -216,14 +365,18 @@ def tftp_switch_config_from_remote_location(self, config_file_location, return def upload_switch_config(self, config, config_name='running-config'): - + ''' + Upload switch config + ''' config_url = '/rest/v1/fullconfigs/{cfg}'.format(cfg=config_name) config_json = json.dumps(config) put(self.module, config_url, config_json) return def update_switch_config(self): - + ''' + Update switch config + ''' self.result = dict(changed=self.changed, warnings=self.warnings) if self.original_config != self.running_config: diff --git a/module_utils/argspec/facts/facts.py b/module_utils/argspec/facts/facts.py new file mode 100644 index 0000000..9824f39 --- /dev/null +++ b/module_utils/argspec/facts/facts.py @@ -0,0 +1,18 @@ +#!/usr/bin/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) + +class FactsArgs(object): + """ The arg spec for the aoscx facts module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'gather_subset': dict(default=['!config'], type='list'), + 'gather_network_resources': dict(type='list'), + } \ No newline at end of file diff --git a/module_utils/facts/facts.py b/module_utils/facts/facts.py new file mode 100644 index 0000000..00592d9 --- /dev/null +++ b/module_utils/facts/facts.py @@ -0,0 +1,76 @@ +#!/usr/bin/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 ansible.module_utils.aoscx import get +from ansible.module_utils.facts.interfaces import InterfacesFacts +from ansible.module_utils.facts.legacy import Default, SoftwareInfo, \ + SoftwareImages, HostName, PlatformName, ManagementInterface, \ + SoftwareVersion, Config, ProductInfo, PowerSupplies, PhysicalInterfaces, \ + Fans, ResourceUtilization, DomainName +from ansible.module_utils.facts.vlans import VlansFacts +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, + software_images=SoftwareImages, + host_name=HostName, + platform_name=PlatformName, + management_interface=ManagementInterface, + software_version=SoftwareVersion, + config=Config, + fans=Fans, + power_supplies=PowerSupplies, + product_info=ProductInfo, + physical_interfaces=PhysicalInterfaces, + resource_utilization=ResourceUtilization, + domain_name=DomainName, +) + +FACT_RESOURCE_SUBSETS = dict( + vlans=VlansFacts, + interfaces=InterfacesFacts, + vrfs=VrfsFacts +) + + +class Facts(FactsBase): + ''' + Base class for AOS-CX Facts + ''' + VALID_LEGACY_GATHER_SUBSETS = frozenset(FACT_LEGACY_SUBSETS.keys()) + VALID_RESOURCE_SUBSETS = frozenset(FACT_RESOURCE_SUBSETS.keys()) + + 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: + self.get_network_resources_facts(FACT_RESOURCE_SUBSETS, + resource_facts_type, data) + + if self.VALID_LEGACY_GATHER_SUBSETS: + self.get_network_legacy_facts(FACT_LEGACY_SUBSETS, + legacy_facts_type) + + return self.ansible_facts, self._warnings + +def get_switch_running_config(module): + + ''' + Gets the switch running-config + ''' + config_url = '/rest/v1/fullconfigs/running-config' + running_config = get(module, config_url) + return running_config diff --git a/module_utils/facts/interfaces.py b/module_utils/facts/interfaces.py new file mode 100644 index 0000000..cc2e761 --- /dev/null +++ b/module_utils/facts/interfaces.py @@ -0,0 +1,31 @@ +#!/usr/bin/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 ansible.module_utils.aoscx import get + + +class InterfacesFacts(object): + ''' + Class for AOS-CX Interface facts + ''' + def __init__(self, module, subspec='config', options='options'): + ''' + init function + ''' + self._module = module + + def populate_facts(self, connection, ansible_facts, data=None): + ''' + Obtain and return interfaces facts + ''' + interfaces_url = '/rest/v10.04/system/interfaces?depth=2' + data = get(self._module, interfaces_url) + facts = { + 'interfaces': data + } + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts diff --git a/module_utils/facts/legacy.py b/module_utils/facts/legacy.py new file mode 100644 index 0000000..71b4911 --- /dev/null +++ b/module_utils/facts/legacy.py @@ -0,0 +1,255 @@ +#!/usr/bin/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 ansible.module_utils.aoscx import get + +class FactsBase(object): + ''' + FactsBase class + ''' + def __init__(self, module): + self._module = module + self.warnings = list() + self.facts = dict() + self.responses = None + self._url = "/rest/v1/fullconfigs/running-config" + self._fact_name = "config" + self.data = None + + def populate(self): + ''' + Obtain and populate the facts + ''' + self.data = get(self._module, self._url) + + if self._fact_name == 'config': + self.facts['config'] = self.data + return + + if self._fact_name in self.data.keys(): + self.facts[self._fact_name] = self.data[self._fact_name] + + +class SoftwareInfo(FactsBase): + ''' + Software Info facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'software_info' + self._url = '/rest/v10.04/system?attributes=software_info' + super(SoftwareInfo, self).populate() + + +class SoftwareImages(FactsBase): + ''' + Software Images facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'software_images' + self._url = '/rest/v10.04/system?attributes=software_images' + super(SoftwareImages, self).populate() + + +class HostName(FactsBase): + ''' + Host Name facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'hostname' + self._url = '/rest/v10.04/system?attributes=hostname' + super(HostName, self).populate() + + +class PlatformName(FactsBase): + ''' + Platform Name facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'platform_name' + self._url = '/rest/v10.04/system?attributes=platform_name' + super(PlatformName, self).populate() + + +class ManagementInterface(FactsBase): + ''' + Management Interface facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'mgmt_intf_status' + self._url = '/rest/v10.04/system?attributes=mgmt_intf_status' + super(ManagementInterface, self).populate() + + +class SoftwareVersion(FactsBase): + ''' + Software Version facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'software_version' + self._url = '/rest/v10.04/system?attributes=software_version' + super(SoftwareVersion, self).populate() + +class Config(FactsBase): + ''' + Config facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'config' + super(Config, self).populate() + + +class Default(FactsBase): + ''' + Default facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'mgmt_intf_status' + self._url = '/rest/v10.04/system?attributes=mgmt_intf_status' + super(Default, self).populate() + + self._fact_name = 'software_version' + self._url = '/rest/v10.04/system?attributes=software_version' + super(Default, self).populate() + + +class DomainName(FactsBase): + ''' + Domain Name facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'domain_name' + self._url = '/rest/v10.04/system?attributes=domain_name' + super(DomainName, self).populate() + + +class SubSystemFactsBase(FactsBase): + ''' + SubSystem Base facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._url = '/rest/v10.04/system/subsystems?depth=4' + self.data = get(self._module, self._url) + output_data = {} + + for sub_system in self.data.keys(): + sub_system_details = self.data[sub_system] + + if self._fact_name in sub_system_details.keys(): + + output_data[sub_system] = sub_system_details[self._fact_name] + + self.facts[self._fact_name] = output_data + + +class ProductInfo(SubSystemFactsBase): + + ''' + Product Info facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'product_info' + super(ProductInfo, self).populate() + + +class PowerSupplies(SubSystemFactsBase): + + ''' + Power supplies facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'power_supplies' + super(PowerSupplies, self).populate() + + +class PhysicalInterfaces(SubSystemFactsBase): + + ''' + Physical Interfaces facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'interfaces' + super(PhysicalInterfaces, self).populate() + + +class Fans(SubSystemFactsBase): + + ''' + Fans facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'fans' + super(Fans, self).populate() + + +class ResourceUtilization(SubSystemFactsBase): + + ''' + Resource utilization facts class + ''' + + def populate(self): + ''' + Obtain and populate the facts + ''' + self._fact_name = 'resource_utilization' + super(ResourceUtilization, self).populate() diff --git a/module_utils/facts/vlans.py b/module_utils/facts/vlans.py new file mode 100644 index 0000000..4b92e3a --- /dev/null +++ b/module_utils/facts/vlans.py @@ -0,0 +1,41 @@ +#!/usr/bin/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 ansible.module_utils.aoscx import get + + +class VlansFacts(object): + ''' + VLANs Facts Class + ''' + def __init__(self, module, subspec='config', options='options'): + ''' + init function + ''' + self._module = module + + def populate_facts(self, connection, ansible_facts, data=None): + ''' + Obtain and return VLAN facts + ''' + vlans_url = '/rest/v10.04/system/vlans?depth=2' + data = get(self._module, vlans_url) + + internal_vlan_list = [] + for vlan in data.keys(): + if 'type' in data[vlan].keys(): + if data[vlan]['type'] == 'internal': + internal_vlan_list.append(vlan) + + for vlan in internal_vlan_list: + data.pop(vlan) + + facts = { + 'vlans': data + } + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts diff --git a/module_utils/facts/vrfs.py b/module_utils/facts/vrfs.py new file mode 100644 index 0000000..6d3e83c --- /dev/null +++ b/module_utils/facts/vrfs.py @@ -0,0 +1,31 @@ +#!/usr/bin/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 ansible.module_utils.aoscx import get + + +class VrfsFacts(object): + ''' + VRFs facts class + ''' + def __init__(self, module, subspec='config', options='options'): + ''' + init function + ''' + self._module = module + + def populate_facts(self, connection, ansible_facts, data=None): + ''' + Obtain and return VRFs Facts + ''' + vrfs_url = '/rest/v10.04/system/vrfs?depth=2' + data = get(self._module, vrfs_url) + facts = { + 'vrfs': data + } + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts diff --git a/module_utils/providers.py b/module_utils/providers.py new file mode 100644 index 0000000..c893609 --- /dev/null +++ b/module_utils/providers.py @@ -0,0 +1,104 @@ +#!/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) + +import json +from threading import RLock +from ansible.module_utils.six import itervalues +from ansible.module_utils.network.common.utils import to_list + +_registered_providers = {} +_provider_lock = RLock() + + +def register_provider(network_os, module_name): + def wrapper(cls): + _provider_lock.acquire() + try: + if network_os not in _registered_providers: + _registered_providers[network_os] = {} + for ct in cls.supported_connections: + if ct not in _registered_providers[network_os]: + _registered_providers[network_os][ct] = {} + for item in to_list(module_name): + for entry in itervalues(_registered_providers[network_os]): + entry[item] = cls + finally: + _provider_lock.release() + return cls + return wrapper + + +def get(network_os, module_name, connection_type): + network_os_providers = _registered_providers.get(network_os) + if network_os_providers is None: + raise ValueError('unable to find a suitable provider for this module') + if connection_type not in network_os_providers: + raise ValueError('provider does not support this connection type') + elif module_name not in network_os_providers[connection_type]: + raise ValueError('could not find a suitable provider for this module') + return network_os_providers[connection_type][module_name] + + +class ProviderBase(object): + + supported_connections = () + + def __init__(self, params, connection=None, check_mode=False): + self.params = params + self.connection = connection + self.check_mode = check_mode + + @property + def capabilities(self): + pass + + def get_value(self, path): + params = self.params.copy() + for key in path.split('.'): + params = params[key] + return params + + def get_facts(self, subset=None): + raise NotImplementedError(self.__class__.__name__) + + def edit_config(self): + raise NotImplementedError(self.__class__.__name__) + + +class CliProvider(ProviderBase): + + supported_connections = ('network_cli',) + + @property + def capabilities(self): + pass + + def get_config_context(self, config, path, indent=1): + pass + + def render(self, config=None): + raise NotImplementedError(self.__class__.__name__) + + def cli(self, command): + try: + if not hasattr(self, '_command_output'): + setattr(self, '_command_output', {}) + return self._command_output[command] + except KeyError: + out = self.connection.get(command) + try: + out = json.loads(out) + except ValueError: + pass + self._command_output[command] = out + return out + + def get_facts(self, subset=None): + pass + + def edit_config(self, config=None): + pass diff --git a/terminal_plugins/aoscx.py b/terminal_plugins/aoscx.py new file mode 100644 index 0000000..b023910 --- /dev/null +++ b/terminal_plugins/aoscx.py @@ -0,0 +1,68 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (C) Copyright 2019 Hewlett Packard Enterprise Development LP. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from ansible.plugins.terminal import TerminalBase +from ansible.errors import AnsibleConnectionFailure +from ansible.utils.display import Display +import re + +display = Display() + +class TerminalModule(TerminalBase): + ''' + Terminal Module class for AOS-CX + ''' + + terminal_stdout_re = [re.compile( + br"[\r\n]?[\w\+\-\.:\/\[\]]+(?:\([^\)]+\)){0,3}(?:[>#]) ?$")] + terminal_stderr_re = [] + terminal_initial_prompt = [] + terminal_initial_answer = [] + terminal_inital_prompt_newline = [] + + def on_open_shell(self): + ''' + Tasks to be executed immediately after connecting to switch. + ''' + try: + self._exec_cli_command(b'terminal length 0') + except AnsibleConnectionFailure: + raise AnsibleConnectionFailure('unable to set terminal parameters') + + try: + self._exec_cli_command(b'terminal width 512') + try: + self._exec_cli_command(b'terminal width 0') + except AnsibleConnectionFailure: + pass + except AnsibleConnectionFailure: + display.display('WARNING: Unable to set terminal width, ' + 'command responses may be truncated') + + + def on_become(self, passwd=None): + ''' + Priveleged mode + ''' + return + + def on_unbecome(self): + ''' + Come out of priveleged mode + ''' + return