diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 0652005f0e..5fbdf4a860 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -470,7 +470,6 @@ files: $modules/network/netvisor/: $team_netvisor $modules/network/nuage/: pdellaert $modules/network/nxos/: $team_nxos - $modules/network/openswitch/: $team_openswitch $modules/network/openvswitch/: ignored: stygstra maintainers: $team_networking @@ -482,7 +481,7 @@ files: $modules/network/panos/panos_address.py: itdependsnetworks ivanbojer jtschichold $modules/network/protocol/: $team_networking $modules/network/routing/: $team_networking - $modules/network/sros/: $team_openswitch + $modules/network/sros/: privateip $modules/network/system/: $team_networking $modules/network/vyos/: Qalthos $modules/notification/bearychat.py: tonyseek @@ -1071,7 +1070,6 @@ macros: team_networking: Qalthos ganeshrn gundalow privateip rcarrillocruz trishnaguha team_nxos: GGabriele jedelman8 mikewiebe privateip rahushen rcarrillocruz trishnaguha team_openstack: emonty j2sol juliakreger rcarrillocruz shrews thingee - team_openswitch: Qalthos gundalow privateip team_rabbitmq: chrishoffman manuel-sousa romanek-adam team_rhn: alikins barnabycourt flossware vritant team_tower: ghjm jlaska matburt wwitzel3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 404aa05462..60fcfd9593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Ansible Changes By Release * junos_template (use junos_config instead) * nxos_template (use nxos_config instead) * ops_template (use ops_config instead) +* openswitch * Modules (scheduled for removal in 2.6) diff --git a/lib/ansible/module_utils/openswitch.py b/lib/ansible/module_utils/openswitch.py deleted file mode 100644 index 5f1925fbab..0000000000 --- a/lib/ansible/module_utils/openswitch.py +++ /dev/null @@ -1,260 +0,0 @@ -# -# (c) 2015 Peter Sprygada, -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . -# - -import re - -try: - import ovs.poller - import ops.dc - from ops.settings import settings - from opslib import restparser - HAS_OPS = True -except ImportError: - HAS_OPS = False - -from ansible.module_utils.basic import json, json_dict_bytes_to_unicode -from ansible.module_utils.network import ModuleStub, NetworkError, NetworkModule -from ansible.module_utils.network import add_argument, register_transport, to_list -from ansible.module_utils.shell import CliBase -from ansible.module_utils.urls import fetch_url, url_argument_spec - -add_argument('use_ssl', dict(default=True, type='bool')) -add_argument('validate_certs', dict(default=True, type='bool')) - - -def get_opsidl(): - extschema = restparser.parseSchema(settings.get('ext_schema')) - ovsschema = settings.get('ovs_schema') - ovsremote = settings.get('ovs_remote') - opsidl = ops.dc.register(extschema, ovsschema, ovsremote) - - init_seqno = opsidl.change_seqno - while True: - opsidl.run() - if init_seqno != opsidl.change_seqno: - break - poller = ovs.poller.Poller() - opsidl.wait(poller) - poller.block() - - return (extschema, opsidl) - - -class Response(object): - - def __init__(self, resp, hdrs): - self.body = None - self.headers = hdrs - - if resp: - self.body = resp.read() - - @property - def json(self): - if not self.body: - return None - try: - return json.loads(self.body) - except ValueError: - return None - - -class Rest(object): - - DEFAULT_HEADERS = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - def __init__(self): - self.url = None - self.url_args = ModuleStub(url_argument_spec(), self._error) - - self.headers = self.DEFAULT_HEADERS - - self._connected = False - self.default_output = 'json' - - def _error(self, msg): - raise NetworkError(msg, url=self.url) - - def connect(self, params, **kwargs): - host = params['host'] - port = params['port'] - - # sets the module_utils/urls.py req parameters - self.url_args.params['url_username'] = params['username'] - self.url_args.params['url_password'] = params['password'] - self.url_args.params['validate_certs'] = params['validate_certs'] - - if params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - baseurl = '%s://%s:%s' % (proto, host, port) - headers = {'Content-Type': 'application/x-www-form-urlencoded'} - # Get a cookie and save it the rest of the operations. - url = '%s/%s' % (baseurl, 'login') - data = 'username=%s&password=%s' % (params['username'], params['password']) - resp, hdrs = fetch_url(self.url_args, url, data=data, headers=headers, method='POST') - - # Update the base url for the rest of the operations. - self.url = '%s/rest/v1' % (baseurl) - self.headers['Cookie'] = hdrs['set-cookie'] - - def disconnect(self, **kwargs): - self.url = None - self._connected = False - - def authorize(self, params, **kwargs): - raise NotImplementedError - - # REST methods - - def _url_builder(self, path): - if path[0] == '/': - path = path[1:] - return '%s/%s' % (self.url, path) - - def request(self, method, path, data=None, headers=None): - url = self._url_builder(path) - data = self._jsonify(data) - - if headers is None: - headers = dict() - headers.update(self.headers) - - resp, hdrs = fetch_url(self.url_args, url, data=data, headers=headers, method=method) - - return Response(resp, hdrs) - - def get(self, path, data=None, headers=None): - return self.request('GET', path, data, headers) - - def put(self, path, data=None, headers=None): - return self.request('PUT', path, data, headers) - - def post(self, path, data=None, headers=None): - return self.request('POST', path, data, headers) - - def delete(self, path, data=None, headers=None): - return self.request('DELETE', path, data, headers) - - # Command methods - - def run_commands(self, commands): - raise NotImplementedError - - # Config methods - - def configure(self, commands): - path = '/system/full-configuration' - return self.put(path, data=commands) - - def load_config(self, commands, **kwargs): - return self.configure(commands) - - def get_config(self, **kwargs): - resp = self.get('/system/full-configuration') - return resp.json - - def save_config(self): - raise NotImplementedError - - def _jsonify(self, data): - for encoding in ("utf-8", "latin-1"): - try: - return json.dumps(data, encoding=encoding) - # Old systems using old simplejson module does not support encoding keyword. - except TypeError: - try: - new_data = json_dict_bytes_to_unicode(data, encoding=encoding) - except UnicodeDecodeError: - continue - return json.dumps(new_data) - except UnicodeDecodeError: - continue - self._error(msg='Invalid unicode encoding encountered') - -Rest = register_transport('rest')(Rest) - - -class Cli(CliBase): - - CLI_PROMPTS_RE = [ - re.compile(r"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), - re.compile(r"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$") - ] - - CLI_ERRORS_RE = [ - re.compile(r"(?:unknown|incomplete|ambiguous) command", re.I), - ] - - NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - - # Config methods - - def configure(self, commands, **kwargs): - cmds = ['configure terminal'] - cmds.extend(to_list(commands)) - if cmds[-1] != 'end': - cmds.append('end') - responses = self.execute(cmds) - return responses[1:] - - def get_config(self, **kwargs): - return self.execute('show running-config')[0] - - def load_config(self, commands, **kwargs): - return self.configure(commands) - - def save_config(self): - self.execute(['copy running-config startup-config']) - -Cli = register_transport('cli')(Cli) - - -class Ssh(object): - - def __init__(self): - if not HAS_OPS: - msg = 'ops.dc lib is required but does not appear to be available' - raise NetworkError(msg) - self._opsidl = None - self._extschema = None - - def configure(self, config): - if not self._opsidl: - (self._extschema, self._opsidl) = get_opsidl() - return ops.dc.write(self._extschema, self._opsidl) - - def get_config(self): - if not self._opsidl: - (self._extschema, self._opsidl) = get_opsidl() - return ops.dc.read(self._extschema, self._opsidl) - - def load_config(self, config): - return self.configure(config) - -Ssh = register_transport('ssh', default=True)(Ssh) diff --git a/lib/ansible/modules/network/openswitch/__init__.py b/lib/ansible/modules/network/openswitch/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lib/ansible/modules/network/openswitch/ops_command.py b/lib/ansible/modules/network/openswitch/ops_command.py deleted file mode 100644 index 1d66503354..0000000000 --- a/lib/ansible/modules/network/openswitch/ops_command.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/python -# -# Copyright: Ansible Project -# 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': 'community'} - - -DOCUMENTATION = """ ---- -module: ops_command -version_added: "2.1" -author: "Peter Sprygada (@privateip)" -short_description: Run arbitrary commands on OpenSwitch devices. -description: - - Sends arbitrary commands to an OpenSwitch node and returns the results - read from the device. This module includes an - argument that will cause the module to wait for a specific condition - before returning or timing out if the condition is not met. -extends_documentation_fragment: openswitch -options: - commands: - description: - - List of commands to send to the remote ops device over the - configured provider. The resulting output from the command - is returned. If the I(wait_for) argument is provided, the - module is not returned until the condition is satisfied or - the number of retires as expired. - required: true - wait_for: - description: - - List of conditions to evaluate against the output of the - command. The task will wait for each condition to be true - before moving forward. If the conditional is not true - within the configured number of retries, the task fails. - See examples. - required: false - default: null - aliases: ['waitfor'] - version_added: "2.2" - match: - description: - - The I(match) argument is used in conjunction with the - I(wait_for) argument to specify the match policy. Valid - values are C(all) or C(any). If the value is set to C(all) - then all conditionals in the I(wait_for) must be satisfied. If - the value is set to C(any) then only one of the values must be - satisfied. - required: false - default: all - choices: ['any', 'all'] - version_added: "2.2" - retries: - description: - - Specifies the number of retries a command should by tried - before it is considered failed. The command is run on the - target device every retry and evaluated against the - I(wait_for) conditions. - required: false - default: 10 - interval: - description: - - Configures the interval in seconds to wait between I(retries) - of the command. If the command does not pass the specified - conditions, the interval indicates how long to wait before - trying the command again. - required: false - default: 1 -""" - -EXAMPLES = """ -# Note: examples below use the following provider dict to handle -# transport and authentication to the node. ---- -vars: - cli: - host: "{{ inventory_hostname }}" - username: netop - password: netop - transport: cli - ---- -- ops_command: - commands: - - show version - provider: "{{ cli }}" - -- ops_command: - commands: - - show version - wait_for: - - "result[0] contains OpenSwitch" - provider: "{{ cli }}" - -- ops_command: - commands: - - show version - - show interfaces - provider: "{{ cli }}" -""" - -RETURN = """ -stdout: - description: the set of responses from the commands - returned: always - type: list - sample: ['...', '...'] - -stdout_lines: - description: The value of stdout split into a list - returned: always - type: list - sample: [['...', '...'], ['...'], ['...']] - -failed_conditions: - description: the conditionals that failed - returned: failed - type: list - sample: ['...', '...'] -""" -import traceback - -import ansible.module_utils.openswitch -from ansible.module_utils.netcli import CommandRunner -from ansible.module_utils.netcli import AddCommandError, FailedConditionsError -from ansible.module_utils.network import NetworkModule, NetworkError -from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_native - - -VALID_KEYS = ['command', 'prompt', 'response'] - -def to_lines(stdout): - for item in stdout: - if isinstance(item, string_types): - item = str(item).split('\n') - yield item - -def parse_commands(module): - for cmd in module.params['commands']: - if isinstance(cmd, string_types): - cmd = dict(command=cmd, output=None) - elif 'command' not in cmd: - module.fail_json(msg='command keyword argument is required') - elif not set(cmd.keys()).issubset(VALID_KEYS): - module.fail_json(msg='unknown keyword specified') - yield cmd - -def main(): - spec = dict( - # { command: , prompt: , response: } - 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') - ) - - module = NetworkModule(argument_spec=spec, - connect_on_load=False, - supports_check_mode=True) - - commands = list(parse_commands(module)) - conditionals = module.params['wait_for'] or list() - - warnings = list() - - runner = CommandRunner(module) - - for cmd in commands: - if module.check_mode and not cmd['command'].startswith('show'): - warnings.append('only show commands are supported when using ' - 'check mode, not executing `%s`' % cmd['command']) - else: - if cmd['command'].startswith('conf'): - module.fail_json(msg='ops_command does not support running ' - 'config mode commands. Please use ' - 'ops_config instead') - try: - runner.add_command(**cmd) - except AddCommandError: - warnings.append('duplicate command detected: %s' % cmd) - - for item in conditionals: - runner.add_conditional(item) - - runner.retries = module.params['retries'] - runner.interval = module.params['interval'] - runner.match = module.params['match'] - - try: - runner.run() - except FailedConditionsError as e: - module.fail_json(msg=to_native(e), failed_conditions=e.failed_conditions, - exception=traceback.format_exc()) - except NetworkError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - result = dict(changed=False, stdout=list()) - - for cmd in commands: - try: - output = runner.get_command(cmd['command']) - except ValueError: - output = 'command not executed due to check_mode, see warnings' - result['stdout'].append(output) - - result['warnings'] = warnings - result['stdout_lines'] = list(to_lines(result['stdout'])) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/network/openswitch/ops_config.py b/lib/ansible/modules/network/openswitch/ops_config.py deleted file mode 100644 index 00073ac67d..0000000000 --- a/lib/ansible/modules/network/openswitch/ops_config.py +++ /dev/null @@ -1,306 +0,0 @@ -#!/usr/bin/python -# -# Copyright: Ansible Project -# 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': 'community'} - - -DOCUMENTATION = """ ---- -module: ops_config -version_added: "2.1" -author: "Peter Sprygada (@privateip)" -short_description: Manage OpenSwitch configuration using CLI -description: - - OpenSwitch configurations use a simple block indent file syntax - for segmenting configuration into sections. This module provides - an implementation for working with ops configuration sections in - a deterministic way. -extends_documentation_fragment: openswitch -options: - lines: - description: - - The ordered set of commands that should be configured in the - section. The commands must be the exact same commands as found - in the device running-config. Be sure to note the configuration - command syntax as some commands are automatically modified by the - device config parser. - required: false - default: null - parents: - description: - - The ordered set of parents that uniquely identify the section - the commands should be checked against. If the parents argument - is omitted, the commands are checked against the set of top - level or global commands. - required: false - default: null - src: - description: - - The I(src) argument provides a path to the configuration file - to load into the remote system. The path can either be a full - system path to the configuration file if the value starts with / - or relative to the root of the implemented role or playbook. - This argument is mutually exclusive with the I(lines) and - I(parents) arguments. - required: false - default: null - version_added: "2.2" - before: - description: - - The ordered set of commands to push on to the command stack if - a change needs to be made. This allows the playbook designer - the opportunity to perform configuration commands prior to pushing - any changes without affecting how the set of commands are matched - against the system. - required: false - default: null - after: - description: - - The ordered set of commands to append to the end of the command - stack if a change needs to be made. Just like with I(before) this - allows the playbook designer to append a set of commands to be - executed after the command set. - required: false - default: null - match: - description: - - Instructs the module on the way to perform the matching of - the set of commands against the current device config. If - match is set to I(line), commands are matched line by line. If - match is set to I(strict), command lines are matched with respect - to position. If match is set to I(exact), command lines - must be an equal match. Finally, if match is set to I(none), the - module will not attempt to compare the source configuration with - the running configuration on the remote device. - required: false - default: line - choices: ['line', 'strict', 'exact', 'none'] - replace: - description: - - Instructs the module on the way to perform the configuration - on the device. If the replace argument is set to I(line) then - the modified lines are pushed to the device in configuration - mode. If the replace argument is set to I(block) then the entire - command block is pushed to the device in configuration mode if any - line is not correct. - required: false - default: line - choices: ['line', 'block'] - force: - description: - - The force argument instructs the module to not consider the - current devices running-config. When set to true, this will - cause the module to push the contents of I(src) into the device - without first checking if already configured. - - Note this argument should be considered deprecated. To achieve - the equivalent, set the C(match=none) which is idempotent. This argument - will be removed in a future release. - required: false - default: false - choices: ['yes', 'no'] - config: - description: - - The module, by default, will connect to the remote device and - retrieve the current running-config to use as a base for comparing - against the contents of source. There are times when it is not - desirable to have the task get the current running-config for - every task in a playbook. The I(config) argument allows the - implementer to pass in the configuration to use as the base - config for comparison. - required: false - default: null - save: - description: - - The C(save) argument instructs the module to save the running- - config to the startup-config at the conclusion of the module - running. If check mode is specified, this argument is ignored. - required: false - default: no - choices: ['yes', 'no'] - version_added: "2.2" -""" - -EXAMPLES = """ -# Note: examples below use the following provider dict to handle -# transport and authentication to the node. ---- -vars: - cli: - host: "{{ inventory_hostname }}" - username: netop - password: netop - ---- -- name: configure hostname over cli - ops_config: - lines: - - "hostname {{ inventory_hostname }}" - provider: "{{ cli }}" - - -- name: configure vlan 10 over cli - ops_config: - lines: - - no shutdown - parents: - - vlan 10 - provider: "{{ cli }}" - -- name: load config from file - ops_config: - src: ops01.cfg - backup: yes - provider: "{{ cli }}" -""" - -RETURN = """ -updates: - description: The set of commands that will be pushed to the remote device - returned: always - type: list - sample: ['...', '...'] -backup_path: - description: The full path to the backup file - returned: when backup is yes - type: string - sample: /playbooks/ansible/backup/ops_config.2016-07-16@22:28:34 -""" -import traceback - -from ansible.module_utils.openswitch import NetworkModule, NetworkError -from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils._text import to_native - - -def check_args(module, warnings): - if module.params['force']: - warnings.append('The force argument is deprecated, please use ' - 'match=none instead. This argument will be ' - 'removed in the future') - -def get_config(module, result): - contents = module.params['config'] - if not contents: - contents = module.config.get_config() - return NetworkConfig(indent=4, contents=contents) - -def get_candidate(module): - candidate = NetworkConfig(indent=4) - if module.params['src']: - candidate.load(module.params['src']) - elif module.params['lines']: - parents = module.params['parents'] or list() - candidate.add(module.params['lines'], parents=parents) - return candidate - -def load_config(module, commands, result): - if not module.check_mode: - module.config(commands) - result['changed'] = True - -def run(module, result): - match = module.params['match'] - replace = module.params['replace'] - path = module.params['parents'] - - candidate = get_candidate(module) - - if match != 'none': - config = get_config(module, result) - configobjs = candidate.difference(config, path=path, match=match, - replace=replace) - else: - configobjs = candidate.items - - if configobjs: - commands = dumps(configobjs, 'commands').split('\n') - - if module.params['lines']: - if module.params['before']: - commands[:0] = module.params['before'] - - if module.params['after']: - commands.extend(module.params['after']) - - result['updates'] = commands - - # send the configuration commands to the device and merge - # them with the current running config - if not module.check_mode: - module.config.load_config(commands) - result['changed'] = True - - if module.params['save']: - if not module.check_mode: - module.config.save_config() - result['changed'] = True - -def main(): - - 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']), - - # this argument is deprecated in favor of setting match: none - # it will be removed in a future version - force=dict(default=False, type='bool'), - - config=dict(), - - save=dict(type='bool', default=False), - backup=dict(type='bool', default=False), - - # ops_config is only supported over Cli transport so force - # the value of transport to be cli - transport=dict(default='cli', choices=['cli']) - ) - - mutually_exclusive = [('lines', 'src')] - - required_if = [('match', 'strict', ['lines']), - ('match', 'exact', ['lines']), - ('replace', 'block', ['lines'])] - - module = NetworkModule(argument_spec=argument_spec, - connect_on_load=False, - mutually_exclusive=mutually_exclusive, - required_if=required_if, - supports_check_mode=True) - - if module.params['force'] is True: - module.params['match'] = 'none' - - warnings = list() - check_args(module, warnings) - - result = dict(changed=False, warnings=warnings) - - if module.params['backup']: - result['__backup__'] = module.config.get_config() - - try: - run(module, result) - except NetworkError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/network/openswitch/ops_facts.py b/lib/ansible/modules/network/openswitch/ops_facts.py deleted file mode 100644 index da022dba60..0000000000 --- a/lib/ansible/modules/network/openswitch/ops_facts.py +++ /dev/null @@ -1,416 +0,0 @@ -#!/usr/bin/python -# -# Copyright: Ansible Project -# 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': 'community'} - - -DOCUMENTATION = """ ---- -module: ops_facts -version_added: "2.1" -author: "Peter Sprygada (@privateip)" -short_description: Collect device specific facts from OpenSwitch -description: - - Collects facts from devices running the OpenSwitch operating - system. Fact collection is supported over both Cli and Rest - transports. This module prepends all of the base network fact keys - with C(ansible_net_). The facts module will always collect a - base set of facts from the device and can enable or disable - collection of additional facts. - - The facts collected from pre Ansible 2.2 are still available and - are collected for backwards compatibility; however, these facts - should be considered deprecated and will be removed in a future - release. -extends_documentation_fragment: openswitch -options: - config: - description: - - When enabled, this argument will collect the current - running configuration from the remote device. If the - C(transport=rest) then the collected configuration will - be the full system configuration. - required: false - choices: - - true - - false - default: false - endpoints: - description: - - Accepts a list of endpoints to retrieve from the remote - device using the REST API. The endpoints should be valid - endpoints available on the device. This argument is only - valid when the C(transport=rest). - required: false - default: null - gather_subset: - description: - - When supplied, this argument will restrict the facts collected - to a given subset. Possible values for this argument include - all, hardware, config, legacy, and interfaces. Can specify a - list of values to include a larger subset. Values can also be used - with an initial C(M(!)) to specify that a specific subset should - not be collected. - required: false - default: '!config' - version_added: "2.2" -""" - -EXAMPLES = """ -# Note: examples below use the following provider dict to handle -# transport and authentication to the node. ---- -vars: - cli: - host: "{{ inventory_hostname }}" - username: netop - password: netop - transport: cli - rest: - host: "{{ inventory_hostname }}" - username: netop - password: netop - transport: rest - ---- -- ops_facts: - gather_subset: all - provider: "{{ rest }}" - -# Collect only the config and default facts -- ops_facts: - gather_subset: config - provider: "{{ cli }}" - -# Do not collect config facts -- ops_facts: - gather_subset: - - "!config" - provider: "{{ cli }}" - -- name: collect device facts - ops_facts: - provider: "{{ cli }}" - -- name: include the config - ops_facts: - config: yes - provider: "{{ rest }}" - -- name: include a set of rest endpoints - ops_facts: - endpoints: - - /system/interfaces/1 - - /system/interfaces/2 - provider: "{{ rest }}" -""" - -RETURN = """ -ansible_net_gather_subset: - description: The list of fact subsets collected from the device - returned: always - type: list - -# default -ansible_net_model: - description: The model name returned from the device - returned: when transport is cli - type: str -ansible_net_serialnum: - description: The serial number of the remote device - returned: when transport is cli - type: str -ansible_net_version: - description: The operating system version running on the remote device - returned: always - type: str -ansible_net_hostname: - description: The configured hostname of the device - returned: always - type: string -ansible_net_image: - description: The image file the device is running - returned: when transport is cli - type: string - -# config -ansible_net_config: - description: The current active config from the device - returned: when config is enabled - type: str - -# legacy (pre Ansible 2.2) -config: - description: The current system configuration - returned: when enabled - type: string - sample: '....' -hostname: - description: returns the configured hostname - returned: always - type: string - sample: ops01 -version: - description: The current version of OpenSwitch - returned: always - type: string - sample: '0.3.0' -endpoints: - description: The JSON response from the URL endpoint - returned: when endpoints argument is defined and transport is rest - type: list - sample: [{....}, {....}] -""" -import re - -import ansible.module_utils.openswitch -from ansible.module_utils.netcli import CommandRunner, AddCommandError -from ansible.module_utils.network import NetworkModule -from ansible.module_utils.six import iteritems - - -def add_command(runner, command): - try: - runner.add_command(command) - except AddCommandError: - # AddCommandError is raised for any issue adding a command to - # the runner. Silently ignore the exception in this case - pass - - -class FactsBase(object): - - def __init__(self, module, runner): - self.module = module - self.transport = module.params['transport'] - self.runner = runner - self.facts = dict() - - if self.transport == 'cli': - self.commands() - - def commands(self): - raise NotImplementedError - - def populate(self): - getattr(self, self.transport)() - - def cli(self): - pass - - def rest(self): - pass - - -class Default(FactsBase): - - def commands(self): - add_command(self.runner, 'show system') - add_command(self.runner, 'show hostname') - - def rest(self): - self.facts.update(self.get_system()) - - def cli(self): - data = self.runner.get_command('show system') - - self.facts['version'] = self.parse_version(data) - self.facts['serialnum'] = self.parse_serialnum(data) - self.facts['model'] = self.parse_model(data) - self.facts['image'] = self.parse_image(data) - - self.facts['hostname'] = self.runner.get_command('show hostname') - - def parse_version(self, data): - match = re.search(r'OpenSwitch Version\s+: (\S+)', data) - if match: - return match.group(1) - - def parse_model(self, data): - match = re.search(r'Platform\s+:\s(\S+)', data, re.M) - if match: - return match.group(1) - - def parse_image(self, data): - match = re.search(r'\(Build: (\S+)\)', data, re.M) - if match: - return match.group(1) - - def parse_serialnum(self, data): - match = re.search(r'Serial Number\s+: (\S+)', data) - if match: - return match.group(1) - - def get_system(self): - response = self.module.connection.get('/system') - return dict( - hostname=response.json['configuration']['hostname'], - version=response.json['status']['switch_version'] - ) - - -class Config(FactsBase): - - def commands(self): - add_command(self.runner, 'show running-config') - - def cli(self): - self.facts['config'] = self.runner.get_command('show running-config') - -class Legacy(FactsBase): - # facts from ops_facts 2.1 - - def commands(self): - add_command(self.runner, 'show system') - add_command(self.runner, 'show hostname') - - if self.module.params['config']: - add_command(self.runner, 'show running-config') - - def rest(self): - self.facts['_endpoints'] = self.get_endpoints() - self.facts.update(self.get_system()) - - if self.module.params['config']: - self.facts['_config'] = self.get_config() - - def cli(self): - self.facts['_hostname'] = self.runner.get_command('show hostname') - - data = self.runner.get_command('show system') - self.facts['_version'] = self.parse_version(data) - - if self.module.params['config']: - self.facts['_config'] = self.runner.get_command('show running-config') - - def parse_version(self, data): - match = re.search(r'OpenSwitch Version\s+: (\S+)', data) - if match: - return match.group(1) - - def get_endpoints(self): - responses = list() - urls = self.module.params['endpoints'] or list() - for ep in urls: - response = self.module.connection.get(ep) - if response.headers['status'] != 200: - self.module.fail_json(msg=response.headers['msg']) - responses.append(response.json) - return responses - - def get_system(self): - response = self.module.connection.get('/system') - return dict( - _hostname=response.json['configuration']['hostname'], - _version=response.json['status']['switch_version'] - ) - - def get_config(self): - response = self.module.connection.get('/system/full-configuration') - return response.json - -def check_args(module, warnings): - if module.params['transport'] != 'rest' and module.params['endpoints']: - warnings.append('Endpoints can only be collected when transport is ' - 'set to "rest". Endpoints will not be collected') - - -FACT_SUBSETS = dict( - default=Default, - config=Config, - legacy=Legacy -) - -VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) - -def main(): - spec = dict( - gather_subset=dict(default=['!config'], type='list'), - - # the next two arguments are legacy from pre 2.2 ops_facts - # these will be deprecated and ultimately removed - config=dict(default=False, type='bool'), - endpoints=dict(type='list'), - - transport=dict(default='cli', choices=['cli', 'rest']) - ) - - module = NetworkModule(argument_spec=spec, supports_check_mode=True) - - gather_subset = module.params['gather_subset'] - - warnings = list() - check_args(module, warnings) - - runable_subsets = set() - exclude_subsets = set() - - for subset in gather_subset: - if subset == 'all': - runable_subsets.update(VALID_SUBSETS) - continue - - if subset.startswith('!'): - subset = subset[1:] - if subset == 'all': - exclude_subsets.update(VALID_SUBSETS) - continue - exclude = True - else: - exclude = False - - if subset not in VALID_SUBSETS: - module.fail_json(msg='Bad subset') - - if exclude: - exclude_subsets.add(subset) - else: - runable_subsets.add(subset) - - if not runable_subsets: - runable_subsets.update(VALID_SUBSETS) - - runable_subsets.difference_update(exclude_subsets) - runable_subsets.add('default') - runable_subsets.add('legacy') - - facts = dict() - facts['gather_subset'] = list(runable_subsets) - - runner = CommandRunner(module) - - instances = list() - for key in runable_subsets: - instances.append(FACT_SUBSETS[key](module, runner)) - - if module.params['transport'] == 'cli': - runner.run() - - try: - for inst in instances: - inst.populate() - facts.update(inst.facts) - except Exception: - module.exit_json(out=module.from_json(runner.items)) - - ansible_facts = dict() - for key, value in iteritems(facts): - # this is to maintain capability with ops_facts 2.1 - if key.startswith('_'): - ansible_facts[key[1:]] = value - else: - key = 'ansible_net_%s' % key - ansible_facts[key] = value - - module.exit_json(ansible_facts=ansible_facts, warnings=warnings) - - -if __name__ == '__main__': - main()