From 2bc4c4f1565ecc0736f9fe8b17f511d9463cc565 Mon Sep 17 00:00:00 2001 From: Kedar Kekan <4506537+kedarX@users.noreply.github.com> Date: Wed, 6 Dec 2017 22:37:31 +0530 Subject: [PATCH] IOS-XR NetConf and Cliconf plugin work (#33332) * - Netconf plugin addition for iosxr - Utilities refactoring to support netconf and cliconf - iosx_banner refactoring for netconf and cliconf - Integration testcases changes to accomodate above changes * Fix sanity failures, shippable errors and review comments * fix pep8 issue * changes run_command method to send specific command args * - Review comment fixes - iosxr_command changes to remove ComplexDict based command_spec * - Move namespaces removal method from utils to netconf plugin * Minor refactoring in utils and change in deprecation message * rewrite build_xml logic and import changes for new utils dir structure * - Review comment changes and minor changes to documentation * * refactor common code and docs updates --- .../module_utils/network/iosxr/iosxr.py | 423 ++++++++++++++---- .../modules/network/iosxr/iosxr_banner.py | 194 +++++--- .../modules/network/iosxr/iosxr_command.py | 31 +- .../modules/network/iosxr/iosxr_config.py | 8 +- .../modules/network/iosxr/iosxr_facts.py | 9 +- .../modules/network/iosxr/iosxr_interface.py | 7 +- .../modules/network/iosxr/iosxr_logging.py | 6 +- .../modules/network/iosxr/iosxr_netconf.py | 31 +- .../modules/network/iosxr/iosxr_system.py | 15 +- .../modules/network/iosxr/iosxr_user.py | 7 +- lib/ansible/plugins/action/iosxr.py | 27 +- lib/ansible/plugins/cliconf/iosxr.py | 11 +- lib/ansible/plugins/netconf/__init__.py | 38 +- lib/ansible/plugins/netconf/iosxr.py | 209 +++++++++ lib/ansible/plugins/netconf/junos.py | 36 ++ .../utils/module_docs_fragments/iosxr.py | 3 + .../targets/iosxr_banner/tasks/main.yaml | 1 + .../targets/iosxr_banner/tasks/netconf.yaml | 16 + .../iosxr_banner/tests/cli/basic-login.yaml | 3 + .../iosxr_banner/tests/cli/basic-motd.yaml | 3 + .../tests/cli/basic-no-login.yaml | 3 + .../tests/netconf/basic-login.yaml | 49 ++ .../tests/netconf/basic-motd.yaml | 49 ++ .../tests/netconf/basic-no-login.yaml | 43 ++ .../iosxr_command/tests/cli/invalid.yaml | 4 +- .../iosxr_facts/tests/cli/all_facts.yaml | 1 + .../iosxr_facts/tests/cli/default_facts.yaml | 1 + .../iosxr_facts/tests/cli/invalid_subset.yaml | 2 + .../iosxr_facts/tests/cli/not_hardware.yaml | 1 + .../iosxr_interface/tests/cli/basic.yaml | 18 + .../iosxr_interface/tests/cli/intent.yaml | 6 + .../iosxr_logging/tests/cli/basic.yaml | 11 + .../tests/cli/set_domain_list.yaml | 10 + .../tests/cli/set_domain_name.yaml | 4 + .../iosxr_system/tests/cli/set_hostname.yaml | 4 + .../tests/cli/set_lookup_source.yaml | 4 + .../tests/cli/set_name_servers.yaml | 4 + .../targets/iosxr_user/tests/cli/auth.yaml | 6 + .../targets/iosxr_user/tests/cli/basic.yaml | 11 + test/sanity/pep8/legacy-files.txt | 4 - .../network/iosxr/test_iosxr_command.py | 14 +- .../modules/network/iosxr/test_iosxr_facts.py | 10 +- 42 files changed, 1090 insertions(+), 247 deletions(-) create mode 100644 lib/ansible/plugins/netconf/iosxr.py create mode 100644 test/integration/targets/iosxr_banner/tasks/netconf.yaml create mode 100644 test/integration/targets/iosxr_banner/tests/netconf/basic-login.yaml create mode 100644 test/integration/targets/iosxr_banner/tests/netconf/basic-motd.yaml create mode 100644 test/integration/targets/iosxr_banner/tests/netconf/basic-no-login.yaml diff --git a/lib/ansible/module_utils/network/iosxr/iosxr.py b/lib/ansible/module_utils/network/iosxr/iosxr.py index 5645475656d..39119eba2b2 100644 --- a/lib/ansible/module_utils/network/iosxr/iosxr.py +++ b/lib/ansible/module_utils/network/iosxr/iosxr.py @@ -26,12 +26,44 @@ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # -from ansible.module_utils._text import to_text -from ansible.module_utils.basic import env_fallback, return_values -from ansible.module_utils.network.common.utils import to_list, ComplexList -from ansible.module_utils.connection import exec_command +import json +from difflib import Differ +from copy import deepcopy + +from ansible.module_utils._text import to_text, to_bytes +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.connection import Connection +from ansible.module_utils.network.common.netconf import NetconfConnection + +try: + from ncclient.xml_ import to_xml + HAS_NCCLIENT = True +except ImportError: + HAS_NCCLIENT = False + +try: + from lxml import etree + HAS_XML = True +except ImportError: + HAS_XML = False _DEVICE_CONFIGS = {} +_EDIT_OPS = frozenset(['merge', 'create', 'replace', 'delete']) + +BASE_1_0 = "{urn:ietf:params:xml:ns:netconf:base:1.0}" + +NS_DICT = { + 'BASE_NSMAP': {"xc": "urn:ietf:params:xml:ns:netconf:base:1.0"}, + 'BANNERS_NSMAP': {None: "http://cisco.com/ns/yang/Cisco-IOS-XR-infra-infra-cfg"}, + 'INTERFACES_NSMAP': {None: "http://openconfig.net/yang/interfaces"}, + 'INSTALL_NSMAP': {None: "http://cisco.com/ns/yang/Cisco-IOS-XR-installmgr-admin-oper"}, + 'HOST-NAMES_NSMAP': {None: "http://cisco.com/ns/yang/Cisco-IOS-XR-shellutil-cfg"}, + 'M:TYPE_NSMAP': {"idx": "urn:ietf:params:xml:ns:yang:iana-if-type"}, + 'ETHERNET_NSMAP': {None: "http://openconfig.net/yang/interfaces/ethernet"}, + 'CETHERNET_NSMAP': {None: "http://cisco.com/ns/yang/Cisco-IOS-XR-drivers-media-eth-cfg"}, + 'INTERFACE-CONFIGURATIONS_NSMAP': {None: "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"} +} iosxr_provider_spec = { 'host': dict(), @@ -40,10 +72,19 @@ iosxr_provider_spec = { 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True), 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), 'timeout': dict(type='int'), + 'transport': dict(), } + iosxr_argument_spec = { 'provider': dict(type='dict', options=iosxr_provider_spec) } + +command_spec = { + 'command': dict(), + 'prompt': dict(default=None), + 'answer': dict(default=None) +} + iosxr_top_spec = { 'host': dict(removed_in_version=2.9), 'port': dict(removed_in_version=2.9, type='int'), @@ -59,91 +100,317 @@ def get_provider_argspec(): return iosxr_provider_spec -def check_args(module, warnings): - pass +def get_connection(module): + if hasattr(module, 'connection'): + return module.connection + + capabilities = get_device_capabilities(module) + network_api = capabilities.get('network_api') + if network_api == 'cliconf': + module.connection = Connection(module._socket_path) + elif network_api == 'netconf': + module.connection = NetconfConnection(module._socket_path) + else: + module.fail_json(msg='Invalid connection type {!s}'.format(network_api)) + + return module.connection -def get_config(module, flags=None): - flags = [] if flags is None else flags +def get_device_capabilities(module): + if hasattr(module, 'capabilities'): + return module.capabilities - cmd = 'show running-config ' - cmd += ' '.join(flags) - cmd = cmd.strip() + capabilities = Connection(module._socket_path).get_capabilities() + module.capabilities = json.loads(capabilities) - 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_or_strict')) - cfg = to_text(out, errors='surrogate_or_strict').strip() - _DEVICE_CONFIGS[cmd] = cfg + return module.capabilities + + +def build_xml_subtree(container_ele, xmap, param=None, opcode=None): + sub_root = container_ele + meta_subtree = list() + + for key, meta in xmap.items(): + + candidates = meta.get('xpath', "").split("/") + + if container_ele.tag == candidates[-2]: + parent = container_ele + elif sub_root.tag == candidates[-2]: + parent = sub_root + else: + parent = sub_root.find(".//" + meta.get('xpath', "").split(sub_root.tag + '/', 1)[1].rsplit('/', 1)[0]) + + if ((opcode in ('delete', 'merge') and meta.get('operation', 'unknown') == 'edit') or + meta.get('operation', None) is None): + + if meta.get('tag', False): + if parent.tag == container_ele.tag: + if meta.get('ns', None) is True: + child = etree.Element(candidates[-1], nsmap=NS_DICT[key.upper() + "_NSMAP"]) + else: + child = etree.Element(candidates[-1]) + meta_subtree.append(child) + sub_root = child + else: + if meta.get('ns', None) is True: + child = etree.SubElement(parent, candidates[-1], nsmap=NS_DICT[key.upper() + "_NSMAP"]) + else: + child = etree.SubElement(parent, candidates[-1]) + + if meta.get('attrib', None) and opcode in ('delete', 'merge'): + child.set(BASE_1_0 + meta.get('attrib'), opcode) + + continue + + text = None + param_key = key.split(":") + if param_key[0] == 'a': + if param.get(param_key[1], None): + text = param.get(param_key[1]) + elif param_key[0] == 'm': + if meta.get('value', None): + text = meta.get('value') + + if text: + if meta.get('ns', None) is True: + child = etree.SubElement(parent, candidates[-1], nsmap=NS_DICT[key.upper() + "_NSMAP"]) + else: + child = etree.SubElement(parent, candidates[-1]) + child.text = text + + if len(meta_subtree) > 1: + for item in meta_subtree: + container_ele.append(item) + + return sub_root + + +def build_xml(container, xmap=None, params=None, opcode=None): + + ''' + Builds netconf xml rpc document from meta-data + + Args: + container: the YANG container within the namespace + xmap: meta-data map to build xml tree + params: Input params that feed xml tree values + opcode: operation to be performed (merge, delete etc.) + + Example: + Module inputs: + banner_params = [{'banner':'motd', 'text':'Ansible banner example', 'state':'present'}] + + Meta-data definition: + bannermap = collections.OrderedDict() + bannermap.update([ + ('banner', {'xpath' : 'banners/banner', 'tag' : True, 'attrib' : "operation"}), + ('a:banner', {'xpath' : 'banner/banner-name'}), + ('a:text', {'xpath' : 'banner/banner-text', 'operation' : 'edit'}) + ]) + + Fields: + key: exact match to the key in arg_spec for a parameter + (prefixes --> a: value fetched from arg_spec, m: value fetched from meta-data) + xpath: xpath of the element (based on YANG model) + tag: True if no text on the element + attrib: attribute to be embedded in the element (e.g. xc:operation="merge") + operation: if edit --> includes the element in edit_config() query else ignores for get() queries + value: if key is prefixed with "m:", value is required in meta-data + + Output: + + + + motd + Ansible banner example + + + + :returns: xml rpc document as a string + ''' + + if opcode == 'filter': + root = etree.Element("filter", type="subtree") + elif opcode in ('delete', 'merge'): + root = etree.Element("config", nsmap=NS_DICT['BASE_NSMAP']) + + container_ele = etree.SubElement(root, container, nsmap=NS_DICT[container.upper() + "_NSMAP"]) + + if xmap: + if not params: + build_xml_subtree(container_ele, xmap) + else: + subtree_list = list() + + for param in to_list(params): + subtree_list.append(build_xml_subtree(container_ele, xmap, param, opcode=opcode)) + + for item in subtree_list: + container_ele.append(item) + + return etree.tostring(root) + + +def etree_find(root, node): + element = etree.fromstring(root).find('.//' + to_bytes(node, errors='surrogate_then_replace').strip()) + if element is not None: + return element + + return None + + +def etree_findall(root, node): + element = etree.fromstring(root).findall('.//' + to_bytes(node, errors='surrogate_then_replace').strip()) + if element is not None: + return element + + return None + + +def is_cliconf(module): + capabilities = get_device_capabilities(module) + network_api = capabilities.get('network_api') + if network_api not in ('cliconf', 'netconf'): + module.fail_json(msg=('unsupported network_api: {!s}'.format(network_api))) + return False + + if network_api == 'cliconf': + return True + + return False + + +def is_netconf(module): + capabilities = get_device_capabilities(module) + network_api = capabilities.get('network_api') + if network_api not in ('cliconf', 'netconf'): + module.fail_json(msg=('unsupported network_api: {!s}'.format(network_api))) + return False + + if network_api == 'netconf': + if not HAS_NCCLIENT: + module.fail_json(msg=('ncclient is not installed')) + if not HAS_XML: + module.fail_json(msg=('lxml is not installed')) + + return True + + return False + + +def get_config_diff(module, running=None, candidate=None): + conn = get_connection(module) + + if is_cliconf(module): + return conn.get('show commit changes diff') + elif is_netconf(module): + if running and candidate: + running_data = running.split("\n", 1)[1].rsplit("\n", 1)[0] + candidate_data = candidate.split("\n", 1)[1].rsplit("\n", 1)[0] + if running_data != candidate_data: + d = Differ() + diff = list(d.compare(running_data.splitlines(), candidate_data.splitlines())) + return '\n'.join(diff).strip() + + return None + + +def discard_config(module): + conn = get_connection(module) + conn.discard_changes() + + +def commit_config(module, comment=None, confirmed=False, confirm_timeout=None, persist=False, check=False): + conn = get_connection(module) + reply = None + + if check: + reply = conn.validate() + else: + if is_netconf(module): + reply = conn.commit(confirmed=confirmed, timeout=confirm_timeout, persist=persist) + elif is_cliconf(module): + reply = conn.commit(comment=comment) + + return reply + + +def get_config(module, source='running', config_filter=None): + global _DEVICE_CONFIGS + + conn = get_connection(module) + + if config_filter is not None: + key = (source + ' ' + ' '.join(config_filter)).strip().rstrip() + else: + key = source + config = _DEVICE_CONFIGS.get(key) + if config: + return config + else: + out = conn.get_config(source=source, filter=config_filter) + if is_netconf(module): + out = to_xml(conn.get_config(source=source, filter=config_filter)) + + cfg = to_bytes(out, errors='surrogate_then_replace').strip() + _DEVICE_CONFIGS.update({key: cfg}) return cfg -def to_commands(module, commands): - spec = { - 'command': dict(key=True), - 'prompt': dict(), - 'answer': dict() - } - transform = ComplexList(spec, module) - return transform(commands) +def load_config(module, command_filter, warnings, replace=False, admin=False, commit=False, comment=None): + conn = get_connection(module) + if is_netconf(module): + # FIXME: check for platform behaviour and restore this + # ret = conn.lock(target = 'candidate') + # ret = conn.discard_changes() + try: + ret = conn.edit_config(command_filter) + finally: + # ret = conn.unlock(target = 'candidate') + pass -def run_commands(module, commands, check_rc=True): - responses = list() - commands = to_commands(module, to_list(commands)) - for cmd in to_list(commands): - cmd = module.jsonify(cmd) - rc, out, err = exec_command(module, cmd) - if check_rc and rc != 0: - module.fail_json(msg=to_text(err, errors='surrogate_or_strict'), rc=rc) - responses.append(to_text(out, errors='surrogate_or_strict')) - return responses + return ret - -def load_config(module, commands, warnings, commit=False, replace=False, comment=None, admin=False): - cmd = 'configure terminal' - if admin: - cmd = 'admin ' + cmd - - rc, out, err = exec_command(module, cmd) - if rc != 0: - module.fail_json(msg='unable to enter configuration mode', err=to_text(err, errors='surrogate_or_strict')) - - failed = False - for command in to_list(commands): - if command == 'end': - continue - - rc, out, err = exec_command(module, command) - if rc != 0: - failed = True - break - - if failed: - exec_command(module, 'abort') - module.fail_json(msg=to_text(err, errors='surrogate_or_strict'), commands=commands, rc=rc) - - rc, diff, err = exec_command(module, 'show commit changes diff') - if rc != 0: - # If we failed, maybe we are in an old version so - # we run show configuration instead - rc, diff, err = exec_command(module, 'show configuration') + elif is_cliconf(module): + # to keep the pre-cliconf behaviour, make a copy, avoid adding commands to input list + cmd_filter = deepcopy(command_filter) + cmd_filter.insert(0, 'configure terminal') + if admin: + cmd_filter.insert(0, 'admin') + conn.edit_config(cmd_filter) + diff = get_config_diff(module) if module._diff: - warnings.append('device platform does not support config diff') + if diff: + module['diff'] = to_text(diff, errors='surrogate_or_strict') + if commit: + commit_config(module, comment=comment) + conn.edit_config('end') + else: + conn.discard_changes() - if commit: - cmd = 'commit' - if comment: - cmd += ' comment {0}'.format(comment) - else: - cmd = 'abort' + return diff - rc, out, err = exec_command(module, cmd) - if rc != 0: - exec_command(module, 'abort') - module.fail_json(msg=err, commands=commands, rc=rc) - return to_text(diff, errors='surrogate_or_strict') +def run_command(module, commands): + conn = get_connection(module) + responses = list() + for cmd in to_list(commands): + try: + cmd = json.loads(cmd) + command = cmd['command'] + prompt = cmd['prompt'] + answer = cmd['answer'] + except: + command = cmd + prompt = None + answer = None + + out = conn.get(command, prompt, answer) + + try: + responses.append(to_text(out, errors='surrogate_or_strict')) + except UnicodeError: + module.fail_json(msg=u'failed to decode output from {0}:{1}'.format(cmd, to_text(out))) + return responses diff --git a/lib/ansible/modules/network/iosxr/iosxr_banner.py b/lib/ansible/modules/network/iosxr/iosxr_banner.py index c97f2d02744..264bdb7fd56 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_banner.py +++ b/lib/ansible/modules/network/iosxr/iosxr_banner.py @@ -16,32 +16,32 @@ DOCUMENTATION = """ --- module: iosxr_banner version_added: "2.4" -author: "Trishna Guha (@trishnaguha)" +author: + - Trishna Guha (@trishnaguha) + - Kedar Kekan (@kedarX) short_description: Manage multiline banners on Cisco IOS XR devices description: - - This will configure both exec and motd banners on remote devices - running Cisco IOS XR. It allows playbooks to add or remote - banner text from the active running configuration. + - This module will configure both exec and motd banners on remote device + running Cisco IOS XR. It allows playbooks to add or remove + banner text from the running configuration. +extends_documentation_fragment: iosxr notes: - - Tested against IOS XR 6.1.2 + - Tested against IOS XRv 6.1.2 options: banner: description: - - Specifies which banner that should be - configured on the remote device. + - Specifies the type of banner to configure on remote device. required: true default: null choices: ['login', 'motd'] text: description: - - The banner text that should be - present in the remote device running configuration. This argument - accepts a multiline string, with no empty lines. Requires I(state=present). + - Banner text to be configured. Accepts multiline string, + without empty lines. Requires I(state=present). default: null state: description: - - Specifies whether or not the configuration is present in the current - devices active running configuration. + - Existential state of the configuration on the device. default: present choices: ['present', 'absent'] """ @@ -79,60 +79,130 @@ commands: """ import re +import collections from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.network.iosxr.iosxr import get_config, load_config -from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec, check_args +from ansible.module_utils.network.iosxr.iosxr import get_config_diff, commit_config +from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec, discard_config +from ansible.module_utils.network.iosxr.iosxr import build_xml, is_cliconf, is_netconf +from ansible.module_utils.network.iosxr.iosxr import etree_find, etree_findall -def map_obj_to_commands(updates, module): - commands = list() - want, have = updates - state = module.params['state'] +class ConfigBase(object): + def __init__(self, module): + self._module = module + self._result = {'changed': False, 'warnings': []} + self._want = {} + self._have = {} - if state == 'absent': - if have.get('state') != 'absent' and ('text' in have.keys() and have['text']): - commands.append('no banner %s' % module.params['banner']) - - elif state == 'present': - if (want['text'] and - want['text'].encode().decode('unicode_escape').strip("'") != have.get('text')): - banner_cmd = 'banner %s ' % module.params['banner'] - banner_cmd += want['text'].strip() - commands.append(banner_cmd) - - return commands + def map_params_to_obj(self): + text = self._module.params['text'] + if text: + text = "{!r}".format(str(text).strip()) + self._want.update({ + 'banner': self._module.params['banner'], + 'text': text, + 'state': self._module.params['state'] + }) -def map_config_to_obj(module): - flags = 'banner %s' % module.params['banner'] - output = get_config(module, flags=[flags]) +class CliConfiguration(ConfigBase): + def __init__(self, module): + super(CliConfiguration, self).__init__(module) - match = re.search(r'banner (\S+) (.*)', output, re.DOTALL) - if match: - text = match.group(2).strip("'") - else: - text = None + def map_obj_to_commands(self): + commands = list() + state = self._module.params['state'] + if state == 'absent': + if self._have.get('state') != 'absent' and ('text' in self._have.keys() and self._have['text']): + commands.append('no banner {!s}'.format(self._module.params['banner'])) + elif state == 'present': + if (self._want['text'] and + self._want['text'].encode().decode('unicode_escape').strip("'") != self._have.get('text')): + banner_cmd = 'banner {!s} '.format(self._module.params['banner']) + banner_cmd += self._want['text'].strip() + commands.append(banner_cmd) + self._result['commands'] = commands + if commands: + if not self._module.check_mode: + load_config(self._module, commands, self._result['warnings'], commit=True) + self._result['changed'] = True - obj = {'banner': module.params['banner'], 'state': 'absent'} + def map_config_to_obj(self): + cli_filter = 'banner {!s}'.format(self._module.params['banner']) + output = get_config(self._module, config_filter=cli_filter) + match = re.search(r'banner (\S+) (.*)', output, re.DOTALL) + if match: + text = match.group(2).strip("'") + else: + text = None + obj = {'banner': self._module.params['banner'], 'state': 'absent'} + if output: + obj['text'] = text + obj['state'] = 'present' + self._have.update(obj) - if output: - obj['text'] = text - obj['state'] = 'present' + def run(self): + self.map_params_to_obj() + self.map_config_to_obj() + self.map_obj_to_commands() - return obj + return self._result -def map_params_to_obj(module): - text = module.params['text'] - if text: - text = "%r" % (str(text).strip()) +class NCConfiguration(ConfigBase): + def __init__(self, module): + super(NCConfiguration, self).__init__(module) + self._banners_meta = collections.OrderedDict() + self._banners_meta.update([ + ('banner', {'xpath': 'banners/banner', 'tag': True, 'attrib': "operation"}), + ('a:banner', {'xpath': 'banner/banner-name'}), + ('a:text', {'xpath': 'banner/banner-text', 'operation': 'edit'}) + ]) - return { - 'banner': module.params['banner'], - 'text': text, - 'state': module.params['state'] - } + def map_obj_to_commands(self): + state = self._module.params['state'] + _get_filter = build_xml('banners', xmap=self._banners_meta, params=self._module.params, opcode="filter") + + running = get_config(self._module, source='running', config_filter=_get_filter) + + banner_name = None + banner_text = None + if etree_find(running, 'banner-text') is not None: + banner_name = etree_find(running, 'banner-name').text + banner_text = etree_find(running, 'banner-text').text + + opcode = None + if state == 'absent' and banner_name == self._module.params['banner'] and len(banner_text): + opcode = "delete" + elif state == 'present': + opcode = 'merge' + + self._result['commands'] = [] + if opcode: + _edit_filter = build_xml('banners', xmap=self._banners_meta, params=self._module.params, opcode=opcode) + + if _edit_filter is not None: + if not self._module.check_mode: + load_config(self._module, _edit_filter, self._result['warnings']) + candidate = get_config(self._module, source='candidate', config_filter=_get_filter) + + diff = get_config_diff(self._module, running, candidate) + if diff: + commit_config(self._module) + self._result['changed'] = True + self._result['commands'] = _edit_filter + if self._module._diff: + self._result['diff'] = {'prepared': diff} + else: + discard_config(self._module) + + def run(self): + self.map_params_to_obj() + self.map_obj_to_commands() + + return self._result def main(): @@ -152,23 +222,13 @@ def main(): required_if=required_if, supports_check_mode=True) - warnings = list() - check_args(module, warnings) - - result = {'changed': False} - result['warnings'] = warnings - - want = map_params_to_obj(module) - have = map_config_to_obj(module) - - commands = map_obj_to_commands((want, have), module) - result['commands'] = commands - - if commands: - if not module.check_mode: - load_config(module, commands, result['warnings'], commit=True) - result['changed'] = True + if is_cliconf(module): + module.deprecate("cli support for 'iosxr_banner' is deprecated. Use transport netconf instead', version='4 releases from v2.5") + config_object = CliConfiguration(module) + elif is_netconf(module): + config_object = NCConfiguration(module) + result = config_object.run() module.exit_json(**result) diff --git a/lib/ansible/modules/network/iosxr/iosxr_command.py b/lib/ansible/modules/network/iosxr/iosxr_command.py index 33d4d4c8453..5fca0fdb262 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_command.py +++ b/lib/ansible/modules/network/iosxr/iosxr_command.py @@ -94,6 +94,7 @@ tasks: commands: - show version - show interfaces + - [{ command: example command that prompts, prompt: expected prompt, answer: yes}] - name: run multiple commands and evaluate the output iosxr_command: @@ -125,9 +126,9 @@ failed_conditions: import time from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.network.iosxr.iosxr import run_commands, iosxr_argument_spec, check_args +from ansible.module_utils.network.iosxr.iosxr import run_command, iosxr_argument_spec +from ansible.module_utils.network.iosxr.iosxr import command_spec from ansible.module_utils.network.common.parsing import Conditional -from ansible.module_utils.network.common.utils import ComplexList from ansible.module_utils.six import string_types from ansible.module_utils._text import to_native @@ -140,27 +141,27 @@ def to_lines(stdout): def parse_commands(module, warnings): - command = ComplexList(dict( - command=dict(key=True), - prompt=dict(), - answer=dict() - ), module) - commands = command(module.params['commands']) - + commands = module.params['commands'] for item in list(commands): - if module.check_mode and not item['command'].startswith('show'): + try: + command = item['command'] + except Exception: + command = item + if module.check_mode and not command.startswith('show'): warnings.append( 'only show commands are supported when using check mode, not ' - 'executing `%s`' % item['command'] + 'executing `%s`' % command ) commands.remove(item) - elif item['command'].startswith('conf'): + elif command.startswith('conf'): module.fail_json( msg='iosxr_command does not support running config mode ' 'commands. Please use iosxr_config instead' ) + return commands + def main(): spec = dict( commands=dict(type='list', required=True), @@ -174,11 +175,12 @@ def main(): spec.update(iosxr_argument_spec) + spec.update(command_spec) + module = AnsibleModule(argument_spec=spec, supports_check_mode=True) warnings = list() - check_args(module, warnings) commands = parse_commands(module, warnings) @@ -190,7 +192,7 @@ def main(): match = module.params['match'] while retries > 0: - responses = run_commands(module, commands) + responses = run_command(module, commands) for item in list(conditionals): if item(responses): @@ -210,7 +212,6 @@ def main(): msg = 'One or more conditional statements have not be satisfied' module.fail_json(msg=msg, failed_conditions=failed_conditions) - result = { 'changed': False, 'stdout': responses, diff --git a/lib/ansible/modules/network/iosxr/iosxr_config.py b/lib/ansible/modules/network/iosxr/iosxr_config.py index 9b34f02dc06..d886a7cc7b2 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_config.py +++ b/lib/ansible/modules/network/iosxr/iosxr_config.py @@ -180,9 +180,8 @@ backup_path: sample: /playbooks/ansible/backup/iosxr01.2016-07-16@22:28:34 """ from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.network.iosxr.iosxr import load_config,get_config +from ansible.module_utils.network.iosxr.iosxr import load_config, get_config from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec -from ansible.module_utils.network.iosxr.iosxr import check_args as iosxr_check_args from ansible.module_utils.network.common.config import NetworkConfig, dumps @@ -190,7 +189,6 @@ DEFAULT_COMMIT_COMMENT = 'configured by iosxr_config' def check_args(module, warnings): - iosxr_check_args(module, warnings) if module.params['comment']: if len(module.params['comment']) > 60: module.fail_json(msg='comment argument cannot be more than 60 characters') @@ -216,6 +214,7 @@ def get_candidate(module): candidate.add(module.params['lines'], parents=parents) return candidate + def run(module, result): match = module.params['match'] replace = module.params['replace'] @@ -231,7 +230,7 @@ def run(module, result): contents = get_running_config(module) configobj = NetworkConfig(contents=contents, indent=1) commands = candidate.difference(configobj, path=path, match=match, - replace=replace) + replace=replace) else: commands = candidate.items @@ -253,6 +252,7 @@ def run(module, result): result['diff'] = dict(prepared=diff) result['changed'] = True + def main(): """main entry point for module execution """ diff --git a/lib/ansible/modules/network/iosxr/iosxr_facts.py b/lib/ansible/modules/network/iosxr/iosxr_facts.py index cd37987fb52..74a114fec7b 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_facts.py +++ b/lib/ansible/modules/network/iosxr/iosxr_facts.py @@ -115,7 +115,7 @@ ansible_net_neighbors: import re from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec, check_args, run_commands +from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec, run_command from ansible.module_utils.six import iteritems from ansible.module_utils.six.moves import zip @@ -166,7 +166,7 @@ class Hardware(FactsBase): results['dir /all']) match = re.search(r'Physical Memory: (\d+)M total \((\d+)', - results['show memory summary']) + results['show memory summary']) if match: self.facts['memtotal_mb'] = match.group(1) self.facts['memfree_mb'] = match.group(2) @@ -188,7 +188,7 @@ class Interfaces(FactsBase): def commands(self): return(['show interfaces', 'show ipv6 interface', - 'show lldp', 'show lldp neighbors detail']) + 'show lldp', 'show lldp neighbors detail']) def populate(self, results): self.facts['all_ipv4_addresses'] = list() @@ -360,7 +360,6 @@ def main(): supports_check_mode=True) warnings = list() - check_args(module, warnings) gather_subset = module.params['gather_subset'] @@ -405,7 +404,7 @@ def main(): try: for inst in instances: commands = inst.commands() - responses = run_commands(module, commands) + responses = run_command(module, commands) results = dict(zip(commands, responses)) inst.populate(results) facts.update(inst.facts) diff --git a/lib/ansible/modules/network/iosxr/iosxr_interface.py b/lib/ansible/modules/network/iosxr/iosxr_interface.py index 50aa0012cf9..7404ff5ccff 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_interface.py +++ b/lib/ansible/modules/network/iosxr/iosxr_interface.py @@ -22,6 +22,7 @@ short_description: Manage Interface on Cisco IOS XR network devices description: - This module provides declarative management of Interfaces on Cisco IOS XR network devices. +extends_documentation_fragment: iosxr notes: - Tested against IOS XR 6.1.2 options: @@ -142,7 +143,7 @@ from ansible.module_utils._text import to_text from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.connection import exec_command from ansible.module_utils.network.iosxr.iosxr import get_config, load_config -from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec, check_args +from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec from ansible.module_utils.network.common.utils import conditional, remove_default_spec @@ -228,7 +229,7 @@ def map_params_to_obj(module): def map_config_to_obj(module): - data = get_config(module, flags=['interface']) + data = get_config(module, config_filter='interface') interfaces = data.strip().rstrip('!').split('!') if not interfaces: @@ -387,7 +388,6 @@ def main(): supports_check_mode=True) warnings = list() - check_args(module, warnings) result = {'changed': False} @@ -402,7 +402,6 @@ def main(): if commands: if not module.check_mode: load_config(module, commands, result['warnings'], commit=True) - exec_command(module, 'exit') result['changed'] = True failed_conditions = check_declarative_intent_params(module, want, result) diff --git a/lib/ansible/modules/network/iosxr/iosxr_logging.py b/lib/ansible/modules/network/iosxr/iosxr_logging.py index 04f37d6a169..9a5c0a672f8 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_logging.py +++ b/lib/ansible/modules/network/iosxr/iosxr_logging.py @@ -21,6 +21,7 @@ short_description: Manage logging on network devices description: - This module provides declarative management of logging on Cisco IOS XR devices. +extends_documentation_fragment: iosxr notes: - Tested against IOS XR 6.1.2 options: @@ -114,7 +115,7 @@ from copy import deepcopy from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.network.iosxr.iosxr import get_config, load_config -from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec, check_args +from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec from ansible.module_utils.network.common.utils import remove_default_spec @@ -237,7 +238,7 @@ def map_config_to_obj(module): obj = [] dest_group = ('console', 'hostnameprefix', 'monitor', 'buffered', 'on') - data = get_config(module, flags=['logging']) + data = get_config(module, config_filter='logging') lines = data.split("\n") for line in lines: @@ -349,7 +350,6 @@ def main(): supports_check_mode=True) warnings = list() - check_args(module, warnings) result = {'changed': False} diff --git a/lib/ansible/modules/network/iosxr/iosxr_netconf.py b/lib/ansible/modules/network/iosxr/iosxr_netconf.py index 3ecbcdf22e1..4ba17299c61 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_netconf.py +++ b/lib/ansible/modules/network/iosxr/iosxr_netconf.py @@ -38,7 +38,7 @@ options: description: - netconf vrf name required: false - default: none + default: default state: description: - Specifies the state of the C(iosxr_netconf) resource on @@ -75,7 +75,7 @@ import re from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.connection import exec_command -from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec, check_args +from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec from ansible.module_utils.network.iosxr.iosxr import get_config, load_config from ansible.module_utils.six import iteritems @@ -90,16 +90,12 @@ def map_obj_to_commands(updates, module): if have['state'] == 'present': commands.append('no netconf-yang agent ssh') - if 'netconf_port' in have: - commands.append('no ssh server netconf port %s' % have['netconf_port']) + if 'netconf_port' in have: + commands.append('no ssh server netconf port %s' % have['netconf_port']) - if want['netconf_vrf']: - for vrf in have['netconf_vrf']: - if vrf == want['netconf_vrf']: - commands.append('no ssh server netconf vrf %s' % vrf) - else: - for vrf in have['netconf_vrf']: - commands.append('no ssh server netconf vrf %s' % vrf) + if have['netconf_vrf']: + for vrf in have['netconf_vrf']: + commands.append('no ssh server netconf vrf %s' % vrf) else: if have['state'] == 'absent': commands.append('netconf-yang agent ssh') @@ -131,9 +127,9 @@ def parse_port(config): def map_config_to_obj(module): obj = {'state': 'absent'} - netconf_config = get_config(module, flags=['netconf-yang agent']) + netconf_config = get_config(module, config_filter='netconf-yang agent') - ssh_config = get_config(module, flags=['ssh server']) + ssh_config = get_config(module, config_filter='ssh server') ssh_config = [config_line for config_line in (line.strip() for line in ssh_config.splitlines()) if config_line] obj['netconf_vrf'] = [] for config in ssh_config: @@ -141,7 +137,7 @@ def map_config_to_obj(module): obj.update({'netconf_port': parse_port(config)}) if 'netconf vrf' in config: obj['netconf_vrf'].append(parse_vrf(config)) - if 'ssh' in netconf_config or 'netconf_port' in obj or obj['netconf_vrf']: + if 'ssh' in netconf_config and ('netconf_port' in obj or obj['netconf_vrf']): obj.update({'state': 'present'}) if 'ssh' in netconf_config and 'netconf_port' not in obj: @@ -176,7 +172,7 @@ def main(): """ argument_spec = dict( netconf_port=dict(type='int', default=830, aliases=['listens_on']), - netconf_vrf=dict(aliases=['vrf']), + netconf_vrf=dict(aliases=['vrf'], default='default'), state=dict(default='present', choices=['present', 'absent']), ) argument_spec.update(iosxr_argument_spec) @@ -185,7 +181,6 @@ def main(): supports_check_mode=True) warnings = list() - check_args(module, warnings) result = {'changed': False, 'warnings': warnings} @@ -197,10 +192,6 @@ def main(): if commands: if not module.check_mode: diff = load_config(module, commands, result['warnings'], commit=True) - if diff: - if module._diff: - result['diff'] = {'prepared': diff} - exec_command(module, 'exit') result['changed'] = True module.exit_json(**result) diff --git a/lib/ansible/modules/network/iosxr/iosxr_system.py b/lib/ansible/modules/network/iosxr/iosxr_system.py index 4de4fa8b3ae..3043156e222 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_system.py +++ b/lib/ansible/modules/network/iosxr/iosxr_system.py @@ -108,18 +108,21 @@ import re from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.network.iosxr.iosxr import get_config, load_config -from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec, check_args +from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec + def diff_list(want, have): adds = set(want).difference(have) removes = set(have).difference(want) return (adds, removes) + def map_obj_to_commands(want, have, module): commands = list() state = module.params['state'] - needs_update = lambda x: want.get(x) and (want.get(x) != have.get(x)) + def needs_update(x): + return want.get(x) and (want.get(x) != have.get(x)) if state == 'absent': if have['hostname'] != 'ios': @@ -167,20 +170,24 @@ def map_obj_to_commands(want, have, module): return commands + def parse_hostname(config): match = re.search(r'^hostname (\S+)', config, re.M) return match.group(1) + def parse_domain_name(config): match = re.search(r'^domain name (\S+)', config, re.M) if match: return match.group(1) + def parse_lookup_source(config): match = re.search(r'^domain lookup source-interface (\S+)', config, re.M) if match: return match.group(1) + def map_config_to_obj(module): config = get_config(module) return { @@ -192,6 +199,7 @@ def map_config_to_obj(module): 'name_servers': re.findall(r'^domain name-server (\S+)', config, re.M) } + def map_params_to_obj(module): return { 'hostname': module.params['hostname'], @@ -202,6 +210,7 @@ def map_params_to_obj(module): 'name_servers': module.params['name_servers'] } + def main(): """ Main entry point for Ansible module execution """ @@ -223,7 +232,6 @@ def main(): supports_check_mode=True) warnings = list() - check_args(module, warnings) result = {'changed': False, 'warnings': warnings} @@ -240,5 +248,6 @@ def main(): module.exit_json(**result) + if __name__ == "__main__": main() diff --git a/lib/ansible/modules/network/iosxr/iosxr_user.py b/lib/ansible/modules/network/iosxr/iosxr_user.py index e4f0b16637e..0f514774db7 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_user.py +++ b/lib/ansible/modules/network/iosxr/iosxr_user.py @@ -26,6 +26,7 @@ description: either individual usernames or the aggregate of usernames in the current running config. It also supports purging usernames from the configuration that are not explicitly defined. +extends_documentation_fragment: iosxr notes: - Tested against IOS XR 6.1.2 options: @@ -166,7 +167,7 @@ from copy import deepcopy from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.network.common.utils import remove_default_spec from ansible.module_utils.network.iosxr.iosxr import get_config, load_config -from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec, check_args +from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec try: from base64 import b64decode @@ -228,7 +229,7 @@ def map_obj_to_commands(updates, module): def map_config_to_obj(module): - data = get_config(module, flags=['username']) + data = get_config(module, config_filter='username') users = data.strip().rstrip('!').split('!') if not users: @@ -453,8 +454,6 @@ def main(): 'To set a user password use "configured_password" instead.' ) - check_args(module, warnings) - result = {'changed': False} want = map_params_to_obj(module) diff --git a/lib/ansible/plugins/action/iosxr.py b/lib/ansible/plugins/action/iosxr.py index 77c865e9220..96140b195a0 100644 --- a/lib/ansible/plugins/action/iosxr.py +++ b/lib/ansible/plugins/action/iosxr.py @@ -48,7 +48,17 @@ class ActionModule(_ActionModule): elif self._play_context.connection == 'local': provider = load_provider(iosxr_provider_spec, self._task.args) pc = copy.deepcopy(self._play_context) - pc.connection = 'network_cli' + if self._task.action in ['iosxr_netconf', 'iosxr_config', 'iosxr_command'] or \ + (provider['transport'] == 'cli' and (self._task.action == 'iosxr_banner' or + self._task.action == 'iosxr_facts' or self._task.action == 'iosxr_logging' or + self._task.action == 'iosxr_system' or self._task.action == 'iosxr_user' or + self._task.action == 'iosxr_interface')): + pc.connection = 'network_cli' + pc.port = int(provider['port'] or self._play_context.port or 22) + else: + pc.connection = 'netconf' + pc.port = int(provider['port'] or self._play_context.port or 830) + pc.network_os = 'iosxr' pc.remote_addr = provider['host'] or self._play_context.remote_addr pc.port = int(provider['port'] or self._play_context.port or 22) @@ -70,15 +80,16 @@ class ActionModule(_ActionModule): # make sure we are in the right cli context which should be # enable mode and not config module - if socket_path is None: - socket_path = self._connection.socket_path + if pc.connection == 'network_cli': + if socket_path is None: + socket_path = self._connection.socket_path - conn = Connection(socket_path) - out = conn.get_prompt() - while to_text(out, errors='surrogate_then_replace').strip().endswith(')#'): - display.vvvv('wrong context, sending exit to device', self._play_context.remote_addr) - conn.send_command('abort') + conn = Connection(socket_path) out = conn.get_prompt() + while to_text(out, errors='surrogate_then_replace').strip().endswith(')#'): + display.vvvv('wrong context, sending exit to device', self._play_context.remote_addr) + conn.send_command('abort') + out = conn.get_prompt() result = super(ActionModule, self).run(tmp, task_vars) return result diff --git a/lib/ansible/plugins/cliconf/iosxr.py b/lib/ansible/plugins/cliconf/iosxr.py index f4196867ed5..0c602fe260d 100644 --- a/lib/ansible/plugins/cliconf/iosxr.py +++ b/lib/ansible/plugins/cliconf/iosxr.py @@ -56,14 +56,19 @@ class Cliconf(CliconfBase): return device_info - def get_config(self, source='running'): + def get_config(self, source='running', filter=None): lookup = {'running': 'running-config'} if source not in lookup: return self.invalid_params("fetching configuration from %s is not supported" % source) - return self.send_command(to_bytes(b'show %s' % lookup[source], errors='surrogate_or_strict')) + if filter: + cmd = to_bytes(b'show {0} {1}'.format(lookup[source], filter), errors='surrogate_or_strict') + else: + cmd = to_bytes(b'show {0}'.format(lookup[source]), errors='surrogate_or_strict') + + return self.send_command(cmd) def edit_config(self, command): - for cmd in chain([b'configure'], to_list(command), [b'end']): + for cmd in chain(to_list(command)): self.send_command(cmd) def get(self, command, prompt=None, answer=None, sendonly=False): diff --git a/lib/ansible/plugins/netconf/__init__.py b/lib/ansible/plugins/netconf/__init__.py index f9febb5482b..e7558c46710 100644 --- a/lib/ansible/plugins/netconf/__init__.py +++ b/lib/ansible/plugins/netconf/__init__.py @@ -54,10 +54,9 @@ class NetconfBase(with_metaclass(ABCMeta, object)): problems. List of supported rpc's: + :get: Retrieves running configuration and device state information :get_config: Retrieves the specified configuration from the device :edit_config: Loads the specified commands into the remote device - :get: Execute specified command on remote device - :get_capabilities: Retrieves device information and supported rpc methods :commit: Load configuration from candidate to running :discard_changes: Discard changes to candidate datastore :validate: Validate the contents of the specified configuration. @@ -65,6 +64,9 @@ class NetconfBase(with_metaclass(ABCMeta, object)): :unlock: Release a configuration lock, previously obtained with the lock operation. :copy_config: create or replace an entire configuration datastore with the contents of another complete configuration datastore. + :get-schema: Retrieves the required schema from the device + :get_capabilities: Retrieves device information and supported rpc methods + For JUNOS: :execute_rpc: RPC to be execute on remote device :load_configuration: Loads given configuration on device @@ -100,7 +102,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)): :source: name of the configuration datastore being queried :filter: specifies the portion of the configuration to retrieve (by default entire configuration is retrieved)""" - return self.m.get_config(*args, **kwargs).data_xml + pass @ensure_connected def get(self, *args, **kwargs): @@ -108,7 +110,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)): *filter* specifies the portion of the configuration to retrieve (by default entire configuration is retrieved) """ - return self.m.get(*args, **kwargs).data_xml + pass @ensure_connected def edit_config(self, *args, **kwargs): @@ -122,10 +124,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)): :error_option: if specified must be one of { `"stop-on-error"`, `"continue-on-error"`, `"rollback-on-error"` } The `"rollback-on-error"` *error_option* depends on the `:rollback-on-error` capability. """ - try: - return self.m.edit_config(*args, **kwargs).data_xml - except RPCError as exc: - raise Exception(to_xml(exc.xml)) + pass @ensure_connected def validate(self, *args, **kwargs): @@ -133,7 +132,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)): :source: is the name of the configuration datastore being validated or `config` element containing the configuration subtree to be validated """ - return self.m.validate(*args, **kwargs).data_xml + pass @ensure_connected def copy_config(self, *args, **kwargs): @@ -162,7 +161,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)): def discard_changes(self, *args, **kwargs): """Revert the candidate configuration to the currently running configuration. Any uncommitted changes are discarded.""" - return self.m.discard_changes(*args, **kwargs).data_xml + pass @ensure_connected def commit(self, *args, **kwargs): @@ -176,10 +175,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)): :confirmed: whether this is a confirmed commit :timeout: specifies the confirm timeout in seconds """ - try: - return self.m.commit(*args, **kwargs).data_xml - except RPCError as exc: - raise Exception(to_xml(exc.xml)) + pass @ensure_connected def validate(self, *args, **kwargs): @@ -187,8 +183,18 @@ class NetconfBase(with_metaclass(ABCMeta, object)): :source: name of configuration data store""" return self.m.validate(*args, **kwargs).data_xml + @ensure_connected + def get_schema(self, *args, **kwargs): + """Retrieves the required schema from the device + """ + return self.m.get_schema(*args, **kwargs) + + @ensure_connected + def locked(self, *args, **kwargs): + return self.m.locked(*args, **kwargs) + @abstractmethod - def get_capabilities(self, commands): + def get_capabilities(self): """Retrieves device information and supported rpc methods by device platform and return result as a string @@ -213,3 +219,5 @@ class NetconfBase(with_metaclass(ABCMeta, object)): def fetch_file(self, source, destination): """Fetch file over scp from remote device""" pass + +# TODO Restore .data_xml, when ncclient supports it for all platforms diff --git a/lib/ansible/plugins/netconf/iosxr.py b/lib/ansible/plugins/netconf/iosxr.py new file mode 100644 index 00000000000..e97a7dc3bc2 --- /dev/null +++ b/lib/ansible/plugins/netconf/iosxr.py @@ -0,0 +1,209 @@ +# +# (c) 2017 Red Hat Inc. +# (c) 2017 Kedar Kekan (kkekan@redhat.com) +# +# 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 . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json +import re +import sys +import collections +from io import BytesIO +from ansible.module_utils.six import StringIO + +from ansible import constants as C +from ansible.module_utils.network.iosxr.iosxr import build_xml +from ansible.errors import AnsibleConnectionFailure, AnsibleError +from ansible.plugins.netconf import NetconfBase +from ansible.plugins.netconf import ensure_connected + +try: + from ncclient import manager + from ncclient.operations import RPCError + from ncclient.transport.errors import SSHUnknownHostError + from ncclient.xml_ import to_ele, to_xml, new_ele +except ImportError: + raise AnsibleError("ncclient is not installed") + +try: + from lxml import etree +except ImportError: + raise AnsibleError("lxml is not installed") + + +def transform_reply(): + reply = ''' + + + + + + + + + + + + + + + + + + + + + ''' + if sys.version < '3': + return reply + else: + print("utf8") + return reply.encode('UTF-8') + + +# Note: Workaround for ncclient 0.5.3 +def remove_namespaces(rpc_reply): + xslt = transform_reply() + parser = etree.XMLParser(remove_blank_text=True) + xslt_doc = etree.parse(BytesIO(xslt), parser) + transform = etree.XSLT(xslt_doc) + + return etree.fromstring(str(transform(etree.parse(StringIO(str(rpc_reply)))))) + + +class Netconf(NetconfBase): + + @ensure_connected + def get_device_info(self): + device_info = {} + device_info['network_os'] = 'iosxr' + install_meta = collections.OrderedDict() + install_meta.update([ + ('boot-variables', {'xpath': 'install/boot-variables', 'tag': True}), + ('boot-variable', {'xpath': 'install/boot-variables/boot-variable', 'tag': True, 'lead': True}), + ('software', {'xpath': 'install/software', 'tag': True}), + ('alias-devices', {'xpath': 'install/software/alias-devices', 'tag': True}), + ('alias-device', {'xpath': 'install/software/alias-devices/alias-device', 'tag': True}), + ('m:device-name', {'xpath': 'install/software/alias-devices/alias-device/device-name', 'value': 'disk0:'}), + ]) + + install_filter = build_xml('install', install_meta, opcode='filter') + + reply = self.get(install_filter) + ele_boot_variable = etree.fromstring(reply).find('.//boot-variable/boot-variable') + if ele_boot_variable: + device_info['network_os_image'] = re.split('[:|,]', ele_boot_variable.text)[1] + ele_package_name = etree.fromstring(reply).find('.//package-name') + if ele_package_name: + device_info['network_os_package'] = ele_package_name.text + device_info['network_os_version'] = re.split('-', ele_package_name.text)[-1] + + hostname_filter = build_xml('host-names', opcode='filter') + + reply = self.get(hostname_filter) + device_info['network_os_hostname'] = etree.fromstring(reply).find('.//host-name').text + + return device_info + + def get_capabilities(self): + result = dict() + result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes', 'validate', 'lock', 'unlock', 'get-schema'] + result['network_api'] = 'netconf' + result['device_info'] = self.get_device_info() + result['server_capabilities'] = [c for c in self.m.server_capabilities] + result['client_capabilities'] = [c for c in self.m.client_capabilities] + result['session_id'] = self.m.session_id + + return json.dumps(result) + + @staticmethod + def guess_network_os(obj): + + try: + m = manager.connect( + host=obj._play_context.remote_addr, + port=obj._play_context.port or 830, + username=obj._play_context.remote_user, + password=obj._play_context.password, + key_filename=str(obj.key_filename), + hostkey_verify=C.HOST_KEY_CHECKING, + look_for_keys=C.PARAMIKO_LOOK_FOR_KEYS, + allow_agent=obj.allow_agent, + timeout=obj._play_context.timeout + ) + except SSHUnknownHostError as exc: + raise AnsibleConnectionFailure(str(exc)) + + guessed_os = None + for c in m.server_capabilities: + if re.search('IOS-XR', c): + guessed_os = 'iosxr' + break + + m.close_session() + return guessed_os + + # TODO: change .xml to .data_xml, when ncclient supports data_xml on all platforms + @ensure_connected + def get(self, *args, **kwargs): + try: + response = self.m.get(*args, **kwargs) + return to_xml(remove_namespaces(response)) + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_connected + def get_config(self, *args, **kwargs): + try: + response = self.m.get_config(*args, **kwargs) + return to_xml(remove_namespaces(response)) + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_connected + def edit_config(self, *args, **kwargs): + try: + response = self.m.edit_config(*args, **kwargs) + return to_xml(remove_namespaces(response)) + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_connected + def commit(self, *args, **kwargs): + try: + response = self.m.commit(*args, **kwargs) + return to_xml(remove_namespaces(response)) + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_connected + def validate(self, *args, **kwargs): + try: + response = self.m.validate(*args, **kwargs) + return to_xml(remove_namespaces(response)) + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_connected + def discard_changes(self, *args, **kwargs): + try: + response = self.m.discard_changes(*args, **kwargs) + return to_xml(remove_namespaces(response)) + except RPCError as exc: + raise Exception(to_xml(exc.xml)) diff --git a/lib/ansible/plugins/netconf/junos.py b/lib/ansible/plugins/netconf/junos.py index afb57777f64..cb61e62869e 100644 --- a/lib/ansible/plugins/netconf/junos.py +++ b/lib/ansible/plugins/netconf/junos.py @@ -152,3 +152,39 @@ class Netconf(NetconfBase): def halt(self): """reboot the device""" return self.m.halt().data_xml + + @ensure_connected + def get(self, *args, **kwargs): + try: + return self.m.get(*args, **kwargs).data_xml + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_connected + def get_config(self, *args, **kwargs): + try: + return self.m.get_config(*args, **kwargs).data_xml + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_connected + def edit_config(self, *args, **kwargs): + try: + self.m.edit_config(*args, **kwargs).data_xml + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_connected + def commit(self, *args, **kwargs): + try: + return self.m.commit(*args, **kwargs).data_xml + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_connected + def validate(self, *args, **kwargs): + return self.m.validate(*args, **kwargs).data_xml + + @ensure_connected + def discard_changes(self, *args, **kwargs): + return self.m.discard_changes(*args, **kwargs).data_xml diff --git a/lib/ansible/utils/module_docs_fragments/iosxr.py b/lib/ansible/utils/module_docs_fragments/iosxr.py index abe0d41b6ca..775c9e3b3c7 100644 --- a/lib/ansible/utils/module_docs_fragments/iosxr.py +++ b/lib/ansible/utils/module_docs_fragments/iosxr.py @@ -64,6 +64,9 @@ options: key used to authenticate the SSH session. If the value is not specified in the task, the value of environment variable C(ANSIBLE_NET_SSH_KEYFILE) will be used instead. +requirements: + - "ncclient >= 0.5.3 when using netconf" + - "lxml >= 4.1.1 when using netconf" notes: - For more information on using Ansible to manage Cisco devices see U(https://www.ansible.com/ansible-cisco). """ diff --git a/test/integration/targets/iosxr_banner/tasks/main.yaml b/test/integration/targets/iosxr_banner/tasks/main.yaml index 415c99d8b12..af08869c922 100644 --- a/test/integration/targets/iosxr_banner/tasks/main.yaml +++ b/test/integration/targets/iosxr_banner/tasks/main.yaml @@ -1,2 +1,3 @@ --- - { include: cli.yaml, tags: ['cli'] } +- { include: netconf.yaml, tags: ['netconf'] } diff --git a/test/integration/targets/iosxr_banner/tasks/netconf.yaml b/test/integration/targets/iosxr_banner/tasks/netconf.yaml new file mode 100644 index 00000000000..1286b354228 --- /dev/null +++ b/test/integration/targets/iosxr_banner/tasks/netconf.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all netconf test cases + find: + paths: "{{ role_path }}/tests/netconf" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test case + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/iosxr_banner/tests/cli/basic-login.yaml b/test/integration/targets/iosxr_banner/tests/cli/basic-login.yaml index 1f6749f2f94..032fa1f4eaf 100644 --- a/test/integration/targets/iosxr_banner/tests/cli/basic-login.yaml +++ b/test/integration/targets/iosxr_banner/tests/cli/basic-login.yaml @@ -2,6 +2,7 @@ - name: setup - remove login iosxr_banner: banner: login + provider: "{{ cli }}" state: absent - name: Set login @@ -11,6 +12,7 @@ this is my login banner that has a multiline string + provider: "{{ cli }}" state: present register: result @@ -30,6 +32,7 @@ this is my login banner that has a multiline string + provider: "{{ cli }}" state: present register: result diff --git a/test/integration/targets/iosxr_banner/tests/cli/basic-motd.yaml b/test/integration/targets/iosxr_banner/tests/cli/basic-motd.yaml index 877097fc891..67b80eb0238 100644 --- a/test/integration/targets/iosxr_banner/tests/cli/basic-motd.yaml +++ b/test/integration/targets/iosxr_banner/tests/cli/basic-motd.yaml @@ -3,6 +3,7 @@ iosxr_banner: banner: motd state: absent + provider: "{{ cli }}" - name: Set motd iosxr_banner: @@ -11,6 +12,7 @@ this is my motd banner that has a multiline string + provider: "{{ cli }}" state: present register: result @@ -30,6 +32,7 @@ this is my motd banner that has a multiline string + provider: "{{ cli }}" state: present register: result diff --git a/test/integration/targets/iosxr_banner/tests/cli/basic-no-login.yaml b/test/integration/targets/iosxr_banner/tests/cli/basic-no-login.yaml index f2784e86491..539baf23f50 100644 --- a/test/integration/targets/iosxr_banner/tests/cli/basic-no-login.yaml +++ b/test/integration/targets/iosxr_banner/tests/cli/basic-no-login.yaml @@ -5,12 +5,14 @@ text: | Junk login banner over multiple lines + provider: "{{ cli }}" state: present - name: remove login iosxr_banner: banner: login state: absent + provider: "{{ cli }}" register: result - debug: @@ -25,6 +27,7 @@ iosxr_banner: banner: login state: absent + provider: "{{ cli }}" register: result - assert: diff --git a/test/integration/targets/iosxr_banner/tests/netconf/basic-login.yaml b/test/integration/targets/iosxr_banner/tests/netconf/basic-login.yaml new file mode 100644 index 00000000000..cb78654e9dc --- /dev/null +++ b/test/integration/targets/iosxr_banner/tests/netconf/basic-login.yaml @@ -0,0 +1,49 @@ +--- +- name: Enable Netconf service + iosxr_netconf: + netconf_port: 830 + netconf_vrf: 'default' + state: present + register: result + +- name: setup - remove login + iosxr_banner: + banner: login + provider: "{{ netconf }}" + state: absent + +- name: Set login + iosxr_banner: + banner: login + text: | + this is my login banner + that has a multiline + string + provider: "{{ netconf }}" + state: present + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" + - "'this is my login banner' in result.commands" + - "'that has a multiline' in result.commands" + +- name: Set login again (idempotent) + iosxr_banner: + banner: login + text: | + this is my login banner + that has a multiline + string + provider: "{{ netconf }}" + state: present + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands | length == 0" diff --git a/test/integration/targets/iosxr_banner/tests/netconf/basic-motd.yaml b/test/integration/targets/iosxr_banner/tests/netconf/basic-motd.yaml new file mode 100644 index 00000000000..b50147c9750 --- /dev/null +++ b/test/integration/targets/iosxr_banner/tests/netconf/basic-motd.yaml @@ -0,0 +1,49 @@ +--- +- name: Enable Netconf service + iosxr_netconf: + netconf_port: 830 + netconf_vrf: 'default' + state: present + register: result + +- name: setup - remove motd + iosxr_banner: + banner: motd + state: absent + provider: "{{ netconf }}" + +- name: Set motd + iosxr_banner: + banner: motd + text: | + this is my motd banner + that has a multiline + string + provider: "{{ netconf }}" + state: present + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" + - "'this is my motd banner' in result.commands" + - "'that has a multiline' in result.commands" + +- name: Set motd again (idempotent) + iosxr_banner: + banner: motd + text: | + this is my motd banner + that has a multiline + string + provider: "{{ netconf }}" + state: present + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands | length == 0" diff --git a/test/integration/targets/iosxr_banner/tests/netconf/basic-no-login.yaml b/test/integration/targets/iosxr_banner/tests/netconf/basic-no-login.yaml new file mode 100644 index 00000000000..e74a0e628d4 --- /dev/null +++ b/test/integration/targets/iosxr_banner/tests/netconf/basic-no-login.yaml @@ -0,0 +1,43 @@ +--- +- name: Enable Netconf service + iosxr_netconf: + netconf_port: 830 + netconf_vrf: 'default' + state: present + register: result + +- name: Setup + iosxr_banner: + banner: login + text: | + Junk login banner + over multiple lines + provider: "{{ netconf }}" + state: present + +- name: remove login + iosxr_banner: + banner: login + state: absent + provider: "{{ netconf }}" + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" + - "'xc:operation=\"delete\"' in result.commands" + +- name: remove login (idempotent) + iosxr_banner: + banner: login + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands | length == 0" diff --git a/test/integration/targets/iosxr_command/tests/cli/invalid.yaml b/test/integration/targets/iosxr_command/tests/cli/invalid.yaml index 9b88f566c20..c128c0c1001 100644 --- a/test/integration/targets/iosxr_command/tests/cli/invalid.yaml +++ b/test/integration/targets/iosxr_command/tests/cli/invalid.yaml @@ -3,7 +3,7 @@ - name: run invalid command iosxr_command: - commands: ['show foo'] + commands: [{command: 'show foo', prompt: 'fooprompt', answer: 'yes'}] register: result ignore_errors: yes @@ -15,7 +15,7 @@ iosxr_command: commands: - show version - - show foo + - [{command: 'show foo', prompt: 'fooprompt', answer: 'yes'}] register: result ignore_errors: yes diff --git a/test/integration/targets/iosxr_facts/tests/cli/all_facts.yaml b/test/integration/targets/iosxr_facts/tests/cli/all_facts.yaml index 806657e7e2a..3eeb8c0ed35 100644 --- a/test/integration/targets/iosxr_facts/tests/cli/all_facts.yaml +++ b/test/integration/targets/iosxr_facts/tests/cli/all_facts.yaml @@ -6,6 +6,7 @@ iosxr_facts: gather_subset: - all + provider: "{{ cli }}" register: result diff --git a/test/integration/targets/iosxr_facts/tests/cli/default_facts.yaml b/test/integration/targets/iosxr_facts/tests/cli/default_facts.yaml index 118637cfff7..ebf9aa26523 100644 --- a/test/integration/targets/iosxr_facts/tests/cli/default_facts.yaml +++ b/test/integration/targets/iosxr_facts/tests/cli/default_facts.yaml @@ -4,6 +4,7 @@ - name: test getting default facts iosxr_facts: + provider: "{{ cli }}" register: result - assert: diff --git a/test/integration/targets/iosxr_facts/tests/cli/invalid_subset.yaml b/test/integration/targets/iosxr_facts/tests/cli/invalid_subset.yaml index 1fd902e55e3..5bfd68708b5 100644 --- a/test/integration/targets/iosxr_facts/tests/cli/invalid_subset.yaml +++ b/test/integration/targets/iosxr_facts/tests/cli/invalid_subset.yaml @@ -6,6 +6,7 @@ iosxr_facts: gather_subset: - "foobar" + provider: "{{ cli }}" register: result ignore_errors: true @@ -28,6 +29,7 @@ gather_subset: - "!hardware" - "hardware" + provider: "{{ cli }}" register: result ignore_errors: true diff --git a/test/integration/targets/iosxr_facts/tests/cli/not_hardware.yaml b/test/integration/targets/iosxr_facts/tests/cli/not_hardware.yaml index 1019c22dd38..cd9c60e29c6 100644 --- a/test/integration/targets/iosxr_facts/tests/cli/not_hardware.yaml +++ b/test/integration/targets/iosxr_facts/tests/cli/not_hardware.yaml @@ -6,6 +6,7 @@ iosxr_facts: gather_subset: - "!hardware" + provider: "{{ cli }}" register: result - assert: diff --git a/test/integration/targets/iosxr_interface/tests/cli/basic.yaml b/test/integration/targets/iosxr_interface/tests/cli/basic.yaml index 015f9718b2c..a1cb2b63013 100644 --- a/test/integration/targets/iosxr_interface/tests/cli/basic.yaml +++ b/test/integration/targets/iosxr_interface/tests/cli/basic.yaml @@ -5,6 +5,7 @@ iosxr_interface: name: GigabitEthernet0/0/0/2 state: absent + provider: "{{ cli }}" register: result @@ -13,6 +14,7 @@ name: GigabitEthernet0/0/0/2 description: test-interface-initial state: present + provider: "{{ cli }}" register: result - assert: @@ -25,6 +27,7 @@ name: GigabitEthernet0/0/0/2 description: test-interface-initial state: present + provider: "{{ cli }}" register: result - assert: @@ -39,6 +42,7 @@ duplex: half mtu: 512 state: present + provider: "{{ cli }}" register: result - assert: @@ -57,6 +61,7 @@ duplex: full mtu: 256 state: present + provider: "{{ cli }}" register: result - assert: @@ -75,6 +80,7 @@ duplex: full mtu: 256 state: present + provider: "{{ cli }}" register: result - assert: that: @@ -84,6 +90,7 @@ iosxr_interface: name: GigabitEthernet0/0/0/2 enabled: False + provider: "{{ cli }}" register: result - assert: @@ -95,6 +102,7 @@ iosxr_interface: name: GigabitEthernet0/0/0/2 enabled: True + provider: "{{ cli }}" register: result - assert: @@ -107,6 +115,7 @@ name: GigabitEthernet0/0/0/3 description: test-interface-initial state: present + provider: "{{ cli }}" register: result - assert: @@ -120,6 +129,7 @@ - name: GigabitEthernet0/0/0/3 - name: GigabitEthernet0/0/0/2 state: absent + provider: "{{ cli }}" - name: Add interface aggregate iosxr_interface: @@ -129,6 +139,7 @@ speed: 100 duplex: full state: present + provider: "{{ cli }}" register: result - assert: @@ -152,6 +163,7 @@ speed: 100 duplex: full state: present + provider: "{{ cli }}" register: result - assert: @@ -165,6 +177,7 @@ - name: GigabitEthernet0/0/0/2 enabled: False state: present + provider: "{{ cli }}" register: result - assert: @@ -180,6 +193,7 @@ - name: GigabitEthernet0/0/0/2 enabled: True state: present + provider: "{{ cli }}" register: result - assert: @@ -194,6 +208,7 @@ - name: GigabitEthernet0/0/0/4 - name: GigabitEthernet0/0/0/5 description: test-interface-initial + provider: "{{ cli }}" register: result - name: Create interface aggregate @@ -204,6 +219,7 @@ - name: GigabitEthernet0/0/0/5 description: test_interface_2 state: present + provider: "{{ cli }}" register: result - assert: @@ -218,6 +234,7 @@ - name: GigabitEthernet0/0/0/4 - name: GigabitEthernet0/0/0/5 state: absent + provider: "{{ cli }}" register: result - assert: @@ -232,6 +249,7 @@ - name: GigabitEthernet0/0/0/4 - name: GigabitEthernet0/0/0/5 state: absent + provider: "{{ cli }}" register: result - assert: diff --git a/test/integration/targets/iosxr_interface/tests/cli/intent.yaml b/test/integration/targets/iosxr_interface/tests/cli/intent.yaml index bbf96d1f069..c134428b4ca 100644 --- a/test/integration/targets/iosxr_interface/tests/cli/intent.yaml +++ b/test/integration/targets/iosxr_interface/tests/cli/intent.yaml @@ -7,6 +7,7 @@ description: test_interface_1 enabled: True state: present + provider: "{{ cli }}" register: result - name: Check intent arguments @@ -14,6 +15,7 @@ name: GigabitEthernet0/0/0/1 state: up delay: 20 + provider: "{{ cli }}" register: result - assert: @@ -24,6 +26,7 @@ iosxr_interface: name: GigabitEthernet0/0/0/1 state: down + provider: "{{ cli }}" ignore_errors: yes register: result @@ -38,6 +41,7 @@ enabled: False state: down delay: 20 + provider: "{{ cli }}" register: result - assert: @@ -49,6 +53,7 @@ name: GigabitEthernet0/0/0/1 enabled: False state: up + provider: "{{ cli }}" ignore_errors: yes register: result @@ -64,6 +69,7 @@ enabled: True state: up delay: 20 + provider: "{{ cli }}" ignore_errors: yes register: result diff --git a/test/integration/targets/iosxr_logging/tests/cli/basic.yaml b/test/integration/targets/iosxr_logging/tests/cli/basic.yaml index f608ad927ab..5ef97cd9ab8 100644 --- a/test/integration/targets/iosxr_logging/tests/cli/basic.yaml +++ b/test/integration/targets/iosxr_logging/tests/cli/basic.yaml @@ -5,12 +5,14 @@ dest: hostnameprefix name: 172.16.0.1 state: absent + provider: "{{ cli }}" - name: Remove console logging iosxr_logging: dest: console level: warning state: absent + provider: "{{ cli }}" register: result - name: Remove buffer @@ -18,6 +20,7 @@ dest: buffered size: 4800000 state: absent + provider: "{{ cli }}" register: result # Start tests @@ -26,6 +29,7 @@ dest: hostnameprefix name: 172.16.0.1 state: present + provider: "{{ cli }}" register: result - assert: @@ -39,6 +43,7 @@ dest: hostnameprefix name: 172.16.0.1 state: present + provider: "{{ cli }}" register: result - assert: @@ -50,6 +55,7 @@ dest: hostnameprefix name: 172.16.0.1 state: absent + provider: "{{ cli }}" register: result - assert: @@ -62,6 +68,7 @@ dest: hostnameprefix name: 172.16.0.1 state: absent + provider: "{{ cli }}" register: result - assert: @@ -73,6 +80,7 @@ dest: console level: warning state: present + provider: "{{ cli }}" register: result - assert: @@ -84,6 +92,7 @@ iosxr_logging: dest: buffered size: 4800000 + provider: "{{ cli }}" register: result - assert: @@ -96,6 +105,7 @@ aggregate: - { dest: console, level: notifications } - { dest: buffered, size: 4700000 } + provider: "{{ cli }}" register: result - assert: @@ -110,6 +120,7 @@ - { dest: console, level: notifications } - { dest: buffered, size: 4700000 } state: absent + provider: "{{ cli }}" register: result - assert: diff --git a/test/integration/targets/iosxr_system/tests/cli/set_domain_list.yaml b/test/integration/targets/iosxr_system/tests/cli/set_domain_list.yaml index acbe9b303e8..6b3db147c5a 100644 --- a/test/integration/targets/iosxr_system/tests/cli/set_domain_list.yaml +++ b/test/integration/targets/iosxr_system/tests/cli/set_domain_list.yaml @@ -7,12 +7,14 @@ - no ip domain-list ansible.com - no ip domain-list redhat.com match: none + provider: "{{ cli }}" - name: configure domain_search iosxr_system: domain_search: - ansible.com - redhat.com + provider: "{{ cli }}" register: result - assert: @@ -26,6 +28,7 @@ domain_search: - ansible.com - redhat.com + provider: "{{ cli }}" register: result - assert: @@ -36,6 +39,7 @@ iosxr_system: domain_search: - ansible.com + provider: "{{ cli }}" register: result - assert: @@ -47,6 +51,7 @@ iosxr_system: domain_search: - ansible.com + provider: "{{ cli }}" register: result - assert: @@ -58,6 +63,7 @@ domain_search: - ansible.com - redhat.com + provider: "{{ cli }}" register: result - assert: @@ -70,6 +76,7 @@ domain_search: - ansible.com - redhat.com + provider: "{{ cli }}" register: result - assert: @@ -81,6 +88,7 @@ domain_search: - ansible.com - eng.ansible.com + provider: "{{ cli }}" register: result - assert: @@ -95,6 +103,7 @@ domain_search: - ansible.com - eng.ansible.com + provider: "{{ cli }}" register: result - assert: @@ -108,5 +117,6 @@ - no domain list redhat.com - no domain list eng.ansible.com match: none + provider: "{{ cli }}" - debug: msg="END cli/set_domain_search.yaml" diff --git a/test/integration/targets/iosxr_system/tests/cli/set_domain_name.yaml b/test/integration/targets/iosxr_system/tests/cli/set_domain_name.yaml index 5c6a0e2d7ed..fc14aaaa006 100644 --- a/test/integration/targets/iosxr_system/tests/cli/set_domain_name.yaml +++ b/test/integration/targets/iosxr_system/tests/cli/set_domain_name.yaml @@ -5,10 +5,12 @@ iosxr_config: lines: no domain name match: none + provider: "{{ cli }}" - name: configure domain_name iosxr_system: domain_name: eng.ansible.com + provider: "{{ cli }}" register: result - assert: @@ -18,6 +20,7 @@ - name: verify domain_name iosxr_system: domain_name: eng.ansible.com + provider: "{{ cli }}" register: result - assert: @@ -28,5 +31,6 @@ iosxr_config: lines: no domain name match: none + provider: "{{ cli }}" - debug: msg="END cli/set_domain_name.yaml" diff --git a/test/integration/targets/iosxr_system/tests/cli/set_hostname.yaml b/test/integration/targets/iosxr_system/tests/cli/set_hostname.yaml index 02b5f23e068..d9fd02212b6 100644 --- a/test/integration/targets/iosxr_system/tests/cli/set_hostname.yaml +++ b/test/integration/targets/iosxr_system/tests/cli/set_hostname.yaml @@ -5,10 +5,12 @@ iosxr_config: lines: hostname switch match: none + provider: "{{ cli }}" - name: configure hostname iosxr_system: hostname: foo + provider: "{{ cli }}" register: result - assert: @@ -18,6 +20,7 @@ - name: verify hostname iosxr_system: hostname: foo + provider: "{{ cli }}" register: result - assert: @@ -28,5 +31,6 @@ iosxr_config: lines: "hostname {{ inventory_hostname }}" match: none + provider: "{{ cli }}" - debug: msg="END cli/set_hostname.yaml" diff --git a/test/integration/targets/iosxr_system/tests/cli/set_lookup_source.yaml b/test/integration/targets/iosxr_system/tests/cli/set_lookup_source.yaml index 56b6b1fb706..dffe1373584 100644 --- a/test/integration/targets/iosxr_system/tests/cli/set_lookup_source.yaml +++ b/test/integration/targets/iosxr_system/tests/cli/set_lookup_source.yaml @@ -7,10 +7,12 @@ - no domain lookup source-interface Loopback10 # - vrf ansible match: none + provider: "{{ cli }}" - name: configure lookup_source iosxr_system: lookup_source: Loopback10 + provider: "{{ cli }}" register: result - assert: @@ -21,6 +23,7 @@ - name: verify lookup_source iosxr_system: lookup_source: Loopback10 + provider: "{{ cli }}" register: result - assert: @@ -58,5 +61,6 @@ - no domain lookup source-interface Loopback10 - no vrf ansible match: none + provider: "{{ cli }}" - debug: msg="END cli/set_lookup_source.yaml" diff --git a/test/integration/targets/iosxr_system/tests/cli/set_name_servers.yaml b/test/integration/targets/iosxr_system/tests/cli/set_name_servers.yaml index e0219be9cf8..bed28f63943 100644 --- a/test/integration/targets/iosxr_system/tests/cli/set_name_servers.yaml +++ b/test/integration/targets/iosxr_system/tests/cli/set_name_servers.yaml @@ -8,6 +8,7 @@ - no ip name-server 2.2.2.2 - no ip name-server 3.3.3.3 match: none + provider: "{{ cli }}" - name: configure name_servers iosxr_system: @@ -15,6 +16,7 @@ - 1.1.1.1 - 2.2.2.2 - 3.3.3.3 + provider: "{{ cli }}" register: result - assert: @@ -31,6 +33,7 @@ - 1.1.1.1 - 2.2.2.2 - 3.3.3.3 + provider: "{{ cli }}" register: result - assert: @@ -69,6 +72,7 @@ name_servers: - 1.1.1.1 - 2.2.2.2 + provider: "{{ cli }}" register: result - assert: diff --git a/test/integration/targets/iosxr_user/tests/cli/auth.yaml b/test/integration/targets/iosxr_user/tests/cli/auth.yaml index 46ed0e16698..4aa4adfe25e 100644 --- a/test/integration/targets/iosxr_user/tests/cli/auth.yaml +++ b/test/integration/targets/iosxr_user/tests/cli/auth.yaml @@ -5,6 +5,7 @@ name: auth_user state: present configured_password: pass123 + provider: "{{ cli }}" - name: test login expect: @@ -30,6 +31,7 @@ name: auth_user state: present public_key_contents: "{{ lookup('file', \"{{ role_path }}/files/public.pub\") }}" + provider: "{{ cli }}" - name: test login with private key expect: @@ -40,6 +42,7 @@ - name: remove user and key iosxr_user: name: auth_user + provider: "{{ cli }}" state: absent - name: test login with private key (should fail, no user) @@ -55,6 +58,7 @@ name: auth_user state: present public_key: "{{ role_path }}/files/public.pub" + provider: "{{ cli }}" - name: test login with private key expect: @@ -68,6 +72,7 @@ name: auth_user state: present public_key_contents: "{{ lookup('file', \"{{ role_path }}/files/public2.pub\") }}" + provider: "{{ cli }}" # FIXME: pexpect fails with OSError: [Errno 5] Input/output error - name: test login with invalid private key (should fail) @@ -88,4 +93,5 @@ iosxr_user: name: auth_user state: absent + provider: "{{ cli }}" register: result diff --git a/test/integration/targets/iosxr_user/tests/cli/basic.yaml b/test/integration/targets/iosxr_user/tests/cli/basic.yaml index eba431b1308..f1c614d82be 100644 --- a/test/integration/targets/iosxr_user/tests/cli/basic.yaml +++ b/test/integration/targets/iosxr_user/tests/cli/basic.yaml @@ -5,12 +5,14 @@ - no username ansibletest1 - no username ansibletest2 - no username ansibletest3 + provider: "{{ cli }}" - name: Create user (SetUp) iosxr_user: name: ansibletest1 configured_password: test state: present + provider: "{{ cli }}" register: result - assert: @@ -25,6 +27,7 @@ configured_password: test update_password: always state: present + provider: "{{ cli }}" register: result - assert: @@ -39,6 +42,7 @@ configured_password: test update_password: on_create state: present + provider: "{{ cli }}" register: result - assert: @@ -53,6 +57,7 @@ update_password: on_create group: sysadmin state: present + provider: "{{ cli }}" register: result - assert: @@ -68,6 +73,7 @@ update_password: on_create group: sysadmin state: present + provider: "{{ cli }}" register: result - assert: @@ -83,6 +89,7 @@ configured_password: test state: present group: sysadmin + provider: "{{ cli }}" register: result - assert: @@ -103,6 +110,7 @@ configured_password: test state: present group: sysadmin + provider: "{{ cli }}" register: result - assert: @@ -122,6 +130,7 @@ update_password: on_create state: present group: sysadmin + provider: "{{ cli }}" register: result - assert: @@ -136,6 +145,7 @@ - name: ansibletest2 - name: ansibletest3 state: absent + provider: "{{ cli }}" register: result - assert: @@ -150,6 +160,7 @@ - name: ansibletest2 - name: ansibletest3 state: absent + provider: "{{ cli }}" register: result - assert: diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt index c9cd855ad09..53c281892a2 100644 --- a/test/sanity/pep8/legacy-files.txt +++ b/test/sanity/pep8/legacy-files.txt @@ -182,10 +182,6 @@ lib/ansible/modules/network/ios/ios_command.py lib/ansible/modules/network/ios/ios_facts.py lib/ansible/modules/network/ios/ios_system.py lib/ansible/modules/network/ios/ios_vrf.py -lib/ansible/modules/network/iosxr/iosxr_command.py -lib/ansible/modules/network/iosxr/iosxr_config.py -lib/ansible/modules/network/iosxr/iosxr_facts.py -lib/ansible/modules/network/iosxr/iosxr_system.py lib/ansible/modules/network/netvisor/pn_cluster.py lib/ansible/modules/network/netvisor/pn_ospfarea.py lib/ansible/modules/network/netvisor/pn_vlag.py diff --git a/test/units/modules/network/iosxr/test_iosxr_command.py b/test/units/modules/network/iosxr/test_iosxr_command.py index 3f1a0e6aa41..a9393e86718 100644 --- a/test/units/modules/network/iosxr/test_iosxr_command.py +++ b/test/units/modules/network/iosxr/test_iosxr_command.py @@ -32,13 +32,13 @@ class TestIosxrCommandModule(TestIosxrModule): def setUp(self): super(TestIosxrCommandModule, self).setUp() - self.mock_run_commands = patch('ansible.modules.network.iosxr.iosxr_command.run_commands') - self.run_commands = self.mock_run_commands.start() + self.mock_run_command = patch('ansible.modules.network.iosxr.iosxr_command.run_command') + self.run_command = self.mock_run_command.start() def tearDown(self): super(TestIosxrCommandModule, self).tearDown() - self.mock_run_commands.stop() + self.mock_run_command.stop() def load_fixtures(self, commands=None): @@ -49,13 +49,13 @@ class TestIosxrCommandModule(TestIosxrModule): for item in commands: try: command = item['command'] - except ValueError: + except Exception: command = item filename = str(command).replace(' ', '_') output.append(load_fixture(filename)) return output - self.run_commands.side_effect = load_from_file + self.run_command.side_effect = load_from_file def test_iosxr_command_simple(self): set_module_args(dict(commands=['show version'])) @@ -78,13 +78,13 @@ class TestIosxrCommandModule(TestIosxrModule): wait_for = 'result[0] contains "test string"' set_module_args(dict(commands=['show version'], wait_for=wait_for)) self.execute_module(failed=True) - self.assertEqual(self.run_commands.call_count, 10) + self.assertEqual(self.run_command.call_count, 10) def test_iosxr_command_retries(self): wait_for = 'result[0] contains "test string"' set_module_args(dict(commands=['show version'], wait_for=wait_for, retries=2)) self.execute_module(failed=True) - self.assertEqual(self.run_commands.call_count, 2) + self.assertEqual(self.run_command.call_count, 2) def test_iosxr_command_match_any(self): wait_for = ['result[0] contains "Cisco IOS"', diff --git a/test/units/modules/network/iosxr/test_iosxr_facts.py b/test/units/modules/network/iosxr/test_iosxr_facts.py index 717f403009f..8b3233d0cf2 100644 --- a/test/units/modules/network/iosxr/test_iosxr_facts.py +++ b/test/units/modules/network/iosxr/test_iosxr_facts.py @@ -34,14 +34,14 @@ class TestIosxrFacts(TestIosxrModule): def setUp(self): super(TestIosxrFacts, self).setUp() - self.mock_run_commands = patch( - 'ansible.modules.network.iosxr.iosxr_facts.run_commands') - self.run_commands = self.mock_run_commands.start() + self.mock_run_command = patch( + 'ansible.modules.network.iosxr.iosxr_facts.run_command') + self.run_command = self.mock_run_command.start() def tearDown(self): super(TestIosxrFacts, self).tearDown() - self.mock_run_commands.stop() + self.mock_run_command.stop() def load_fixtures(self, commands=None): @@ -60,7 +60,7 @@ class TestIosxrFacts(TestIosxrModule): output.append(load_fixture(filename)) return output - self.run_commands.side_effect = load_from_file + self.run_command.side_effect = load_from_file def test_iosxr_facts_gather_subset_default(self): set_module_args(dict())