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
This commit is contained in:
Kedar Kekan 2017-12-06 22:37:31 +05:30 committed by GitHub
parent 4b6061ce3e
commit 2bc4c4f156
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1090 additions and 247 deletions

View file

@ -26,12 +26,44 @@
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # 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. # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# #
from ansible.module_utils._text import to_text import json
from ansible.module_utils.basic import env_fallback, return_values from difflib import Differ
from ansible.module_utils.network.common.utils import to_list, ComplexList from copy import deepcopy
from ansible.module_utils.connection import exec_command
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 = {} _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 = { iosxr_provider_spec = {
'host': dict(), 'host': dict(),
@ -40,10 +72,19 @@ iosxr_provider_spec = {
'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True), 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True),
'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
'timeout': dict(type='int'), 'timeout': dict(type='int'),
'transport': dict(),
} }
iosxr_argument_spec = { iosxr_argument_spec = {
'provider': dict(type='dict', options=iosxr_provider_spec) 'provider': dict(type='dict', options=iosxr_provider_spec)
} }
command_spec = {
'command': dict(),
'prompt': dict(default=None),
'answer': dict(default=None)
}
iosxr_top_spec = { iosxr_top_spec = {
'host': dict(removed_in_version=2.9), 'host': dict(removed_in_version=2.9),
'port': dict(removed_in_version=2.9, type='int'), 'port': dict(removed_in_version=2.9, type='int'),
@ -59,91 +100,317 @@ def get_provider_argspec():
return iosxr_provider_spec return iosxr_provider_spec
def check_args(module, warnings): def get_connection(module):
pass 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): def get_device_capabilities(module):
flags = [] if flags is None else flags if hasattr(module, 'capabilities'):
return module.capabilities
cmd = 'show running-config ' capabilities = Connection(module._socket_path).get_capabilities()
cmd += ' '.join(flags) module.capabilities = json.loads(capabilities)
cmd = cmd.strip()
try: return module.capabilities
return _DEVICE_CONFIGS[cmd]
except KeyError:
rc, out, err = exec_command(module, cmd) def build_xml_subtree(container_ele, xmap, param=None, opcode=None):
if rc != 0: sub_root = container_ele
module.fail_json(msg='unable to retrieve current config', stderr=to_text(err, errors='surrogate_or_strict')) meta_subtree = list()
cfg = to_text(out, errors='surrogate_or_strict').strip()
_DEVICE_CONFIGS[cmd] = cfg 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:
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
<banners xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-infra-infra-cfg">
<banner xc:operation="merge">
<banner-name>motd</banner-name>
<banner-text>Ansible banner example</banner-text>
</banner>
</banners>
</config>
: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 return cfg
def to_commands(module, commands): def load_config(module, command_filter, warnings, replace=False, admin=False, commit=False, comment=None):
spec = { conn = get_connection(module)
'command': dict(key=True),
'prompt': dict(),
'answer': dict()
}
transform = ComplexList(spec, module)
return transform(commands)
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): return ret
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
elif is_cliconf(module):
def load_config(module, commands, warnings, commit=False, replace=False, comment=None, admin=False): # to keep the pre-cliconf behaviour, make a copy, avoid adding commands to input list
cmd = 'configure terminal' cmd_filter = deepcopy(command_filter)
if admin: cmd_filter.insert(0, 'configure terminal')
cmd = 'admin ' + cmd if admin:
cmd_filter.insert(0, 'admin')
rc, out, err = exec_command(module, cmd) conn.edit_config(cmd_filter)
if rc != 0: diff = get_config_diff(module)
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')
if module._diff: 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: return diff
cmd = 'commit'
if comment:
cmd += ' comment {0}'.format(comment)
else:
cmd = 'abort'
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

View file

@ -16,32 +16,32 @@ DOCUMENTATION = """
--- ---
module: iosxr_banner module: iosxr_banner
version_added: "2.4" 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 short_description: Manage multiline banners on Cisco IOS XR devices
description: description:
- This will configure both exec and motd banners on remote devices - This module will configure both exec and motd banners on remote device
running Cisco IOS XR. It allows playbooks to add or remote running Cisco IOS XR. It allows playbooks to add or remove
banner text from the active running configuration. banner text from the running configuration.
extends_documentation_fragment: iosxr
notes: notes:
- Tested against IOS XR 6.1.2 - Tested against IOS XRv 6.1.2
options: options:
banner: banner:
description: description:
- Specifies which banner that should be - Specifies the type of banner to configure on remote device.
configured on the remote device.
required: true required: true
default: null default: null
choices: ['login', 'motd'] choices: ['login', 'motd']
text: text:
description: description:
- The banner text that should be - Banner text to be configured. Accepts multiline string,
present in the remote device running configuration. This argument without empty lines. Requires I(state=present).
accepts a multiline string, with no empty lines. Requires I(state=present).
default: null default: null
state: state:
description: description:
- Specifies whether or not the configuration is present in the current - Existential state of the configuration on the device.
devices active running configuration.
default: present default: present
choices: ['present', 'absent'] choices: ['present', 'absent']
""" """
@ -79,60 +79,130 @@ commands:
""" """
import re import re
import collections
from ansible.module_utils.basic import AnsibleModule 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 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): class ConfigBase(object):
commands = list() def __init__(self, module):
want, have = updates self._module = module
state = module.params['state'] self._result = {'changed': False, 'warnings': []}
self._want = {}
self._have = {}
if state == 'absent': def map_params_to_obj(self):
if have.get('state') != 'absent' and ('text' in have.keys() and have['text']): text = self._module.params['text']
commands.append('no banner %s' % module.params['banner']) if text:
text = "{!r}".format(str(text).strip())
elif state == 'present': self._want.update({
if (want['text'] and 'banner': self._module.params['banner'],
want['text'].encode().decode('unicode_escape').strip("'") != have.get('text')): 'text': text,
banner_cmd = 'banner %s ' % module.params['banner'] 'state': self._module.params['state']
banner_cmd += want['text'].strip() })
commands.append(banner_cmd)
return commands
def map_config_to_obj(module): class CliConfiguration(ConfigBase):
flags = 'banner %s' % module.params['banner'] def __init__(self, module):
output = get_config(module, flags=[flags]) super(CliConfiguration, self).__init__(module)
match = re.search(r'banner (\S+) (.*)', output, re.DOTALL) def map_obj_to_commands(self):
if match: commands = list()
text = match.group(2).strip("'") state = self._module.params['state']
else: if state == 'absent':
text = None 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: def run(self):
obj['text'] = text self.map_params_to_obj()
obj['state'] = 'present' self.map_config_to_obj()
self.map_obj_to_commands()
return obj return self._result
def map_params_to_obj(module): class NCConfiguration(ConfigBase):
text = module.params['text'] def __init__(self, module):
if text: super(NCConfiguration, self).__init__(module)
text = "%r" % (str(text).strip()) 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 { def map_obj_to_commands(self):
'banner': module.params['banner'], state = self._module.params['state']
'text': text, _get_filter = build_xml('banners', xmap=self._banners_meta, params=self._module.params, opcode="filter")
'state': module.params['state']
} 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(): def main():
@ -152,23 +222,13 @@ def main():
required_if=required_if, required_if=required_if,
supports_check_mode=True) supports_check_mode=True)
warnings = list() if is_cliconf(module):
check_args(module, warnings) module.deprecate("cli support for 'iosxr_banner' is deprecated. Use transport netconf instead', version='4 releases from v2.5")
config_object = CliConfiguration(module)
result = {'changed': False} elif is_netconf(module):
result['warnings'] = warnings config_object = NCConfiguration(module)
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
result = config_object.run()
module.exit_json(**result) module.exit_json(**result)

View file

@ -94,6 +94,7 @@ tasks:
commands: commands:
- show version - show version
- show interfaces - show interfaces
- [{ command: example command that prompts, prompt: expected prompt, answer: yes}]
- name: run multiple commands and evaluate the output - name: run multiple commands and evaluate the output
iosxr_command: iosxr_command:
@ -125,9 +126,9 @@ failed_conditions:
import time import time
from ansible.module_utils.basic import AnsibleModule 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.parsing import Conditional
from ansible.module_utils.network.common.utils import ComplexList
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
@ -140,27 +141,27 @@ def to_lines(stdout):
def parse_commands(module, warnings): def parse_commands(module, warnings):
command = ComplexList(dict( commands = module.params['commands']
command=dict(key=True),
prompt=dict(),
answer=dict()
), module)
commands = command(module.params['commands'])
for item in list(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( warnings.append(
'only show commands are supported when using check mode, not ' 'only show commands are supported when using check mode, not '
'executing `%s`' % item['command'] 'executing `%s`' % command
) )
commands.remove(item) commands.remove(item)
elif item['command'].startswith('conf'): elif command.startswith('conf'):
module.fail_json( module.fail_json(
msg='iosxr_command does not support running config mode ' msg='iosxr_command does not support running config mode '
'commands. Please use iosxr_config instead' 'commands. Please use iosxr_config instead'
) )
return commands return commands
def main(): def main():
spec = dict( spec = dict(
commands=dict(type='list', required=True), commands=dict(type='list', required=True),
@ -174,11 +175,12 @@ def main():
spec.update(iosxr_argument_spec) spec.update(iosxr_argument_spec)
spec.update(command_spec)
module = AnsibleModule(argument_spec=spec, module = AnsibleModule(argument_spec=spec,
supports_check_mode=True) supports_check_mode=True)
warnings = list() warnings = list()
check_args(module, warnings)
commands = parse_commands(module, warnings) commands = parse_commands(module, warnings)
@ -190,7 +192,7 @@ def main():
match = module.params['match'] match = module.params['match']
while retries > 0: while retries > 0:
responses = run_commands(module, commands) responses = run_command(module, commands)
for item in list(conditionals): for item in list(conditionals):
if item(responses): if item(responses):
@ -210,7 +212,6 @@ def main():
msg = 'One or more conditional statements have not be satisfied' msg = 'One or more conditional statements have not be satisfied'
module.fail_json(msg=msg, failed_conditions=failed_conditions) module.fail_json(msg=msg, failed_conditions=failed_conditions)
result = { result = {
'changed': False, 'changed': False,
'stdout': responses, 'stdout': responses,

View file

@ -180,9 +180,8 @@ backup_path:
sample: /playbooks/ansible/backup/iosxr01.2016-07-16@22:28:34 sample: /playbooks/ansible/backup/iosxr01.2016-07-16@22:28:34
""" """
from ansible.module_utils.basic import AnsibleModule 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 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 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): def check_args(module, warnings):
iosxr_check_args(module, warnings)
if module.params['comment']: if module.params['comment']:
if len(module.params['comment']) > 60: if len(module.params['comment']) > 60:
module.fail_json(msg='comment argument cannot be more than 60 characters') 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) candidate.add(module.params['lines'], parents=parents)
return candidate return candidate
def run(module, result): def run(module, result):
match = module.params['match'] match = module.params['match']
replace = module.params['replace'] replace = module.params['replace']
@ -231,7 +230,7 @@ def run(module, result):
contents = get_running_config(module) contents = get_running_config(module)
configobj = NetworkConfig(contents=contents, indent=1) configobj = NetworkConfig(contents=contents, indent=1)
commands = candidate.difference(configobj, path=path, match=match, commands = candidate.difference(configobj, path=path, match=match,
replace=replace) replace=replace)
else: else:
commands = candidate.items commands = candidate.items
@ -253,6 +252,7 @@ def run(module, result):
result['diff'] = dict(prepared=diff) result['diff'] = dict(prepared=diff)
result['changed'] = True result['changed'] = True
def main(): def main():
"""main entry point for module execution """main entry point for module execution
""" """

View file

@ -115,7 +115,7 @@ ansible_net_neighbors:
import re import re
from ansible.module_utils.basic import AnsibleModule 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 import iteritems
from ansible.module_utils.six.moves import zip from ansible.module_utils.six.moves import zip
@ -166,7 +166,7 @@ class Hardware(FactsBase):
results['dir /all']) results['dir /all'])
match = re.search(r'Physical Memory: (\d+)M total \((\d+)', match = re.search(r'Physical Memory: (\d+)M total \((\d+)',
results['show memory summary']) results['show memory summary'])
if match: if match:
self.facts['memtotal_mb'] = match.group(1) self.facts['memtotal_mb'] = match.group(1)
self.facts['memfree_mb'] = match.group(2) self.facts['memfree_mb'] = match.group(2)
@ -188,7 +188,7 @@ class Interfaces(FactsBase):
def commands(self): def commands(self):
return(['show interfaces', 'show ipv6 interface', return(['show interfaces', 'show ipv6 interface',
'show lldp', 'show lldp neighbors detail']) 'show lldp', 'show lldp neighbors detail'])
def populate(self, results): def populate(self, results):
self.facts['all_ipv4_addresses'] = list() self.facts['all_ipv4_addresses'] = list()
@ -360,7 +360,6 @@ def main():
supports_check_mode=True) supports_check_mode=True)
warnings = list() warnings = list()
check_args(module, warnings)
gather_subset = module.params['gather_subset'] gather_subset = module.params['gather_subset']
@ -405,7 +404,7 @@ def main():
try: try:
for inst in instances: for inst in instances:
commands = inst.commands() commands = inst.commands()
responses = run_commands(module, commands) responses = run_command(module, commands)
results = dict(zip(commands, responses)) results = dict(zip(commands, responses))
inst.populate(results) inst.populate(results)
facts.update(inst.facts) facts.update(inst.facts)

View file

@ -22,6 +22,7 @@ short_description: Manage Interface on Cisco IOS XR network devices
description: description:
- This module provides declarative management of Interfaces - This module provides declarative management of Interfaces
on Cisco IOS XR network devices. on Cisco IOS XR network devices.
extends_documentation_fragment: iosxr
notes: notes:
- Tested against IOS XR 6.1.2 - Tested against IOS XR 6.1.2
options: options:
@ -142,7 +143,7 @@ from ansible.module_utils._text import to_text
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.connection import exec_command 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 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 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): def map_config_to_obj(module):
data = get_config(module, flags=['interface']) data = get_config(module, config_filter='interface')
interfaces = data.strip().rstrip('!').split('!') interfaces = data.strip().rstrip('!').split('!')
if not interfaces: if not interfaces:
@ -387,7 +388,6 @@ def main():
supports_check_mode=True) supports_check_mode=True)
warnings = list() warnings = list()
check_args(module, warnings)
result = {'changed': False} result = {'changed': False}
@ -402,7 +402,6 @@ def main():
if commands: if commands:
if not module.check_mode: if not module.check_mode:
load_config(module, commands, result['warnings'], commit=True) load_config(module, commands, result['warnings'], commit=True)
exec_command(module, 'exit')
result['changed'] = True result['changed'] = True
failed_conditions = check_declarative_intent_params(module, want, result) failed_conditions = check_declarative_intent_params(module, want, result)

View file

@ -21,6 +21,7 @@ short_description: Manage logging on network devices
description: description:
- This module provides declarative management of logging - This module provides declarative management of logging
on Cisco IOS XR devices. on Cisco IOS XR devices.
extends_documentation_fragment: iosxr
notes: notes:
- Tested against IOS XR 6.1.2 - Tested against IOS XR 6.1.2
options: options:
@ -114,7 +115,7 @@ from copy import deepcopy
from ansible.module_utils.basic import AnsibleModule 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 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 from ansible.module_utils.network.common.utils import remove_default_spec
@ -237,7 +238,7 @@ def map_config_to_obj(module):
obj = [] obj = []
dest_group = ('console', 'hostnameprefix', 'monitor', 'buffered', 'on') dest_group = ('console', 'hostnameprefix', 'monitor', 'buffered', 'on')
data = get_config(module, flags=['logging']) data = get_config(module, config_filter='logging')
lines = data.split("\n") lines = data.split("\n")
for line in lines: for line in lines:
@ -349,7 +350,6 @@ def main():
supports_check_mode=True) supports_check_mode=True)
warnings = list() warnings = list()
check_args(module, warnings)
result = {'changed': False} result = {'changed': False}

View file

@ -38,7 +38,7 @@ options:
description: description:
- netconf vrf name - netconf vrf name
required: false required: false
default: none default: default
state: state:
description: description:
- Specifies the state of the C(iosxr_netconf) resource on - 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.basic import AnsibleModule
from ansible.module_utils.connection import exec_command 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.network.iosxr.iosxr import get_config, load_config
from ansible.module_utils.six import iteritems from ansible.module_utils.six import iteritems
@ -90,16 +90,12 @@ def map_obj_to_commands(updates, module):
if have['state'] == 'present': if have['state'] == 'present':
commands.append('no netconf-yang agent ssh') commands.append('no netconf-yang agent ssh')
if 'netconf_port' in have: if 'netconf_port' in have:
commands.append('no ssh server netconf port %s' % have['netconf_port']) commands.append('no ssh server netconf port %s' % have['netconf_port'])
if want['netconf_vrf']: if have['netconf_vrf']:
for vrf in have['netconf_vrf']: for vrf in have['netconf_vrf']:
if vrf == want['netconf_vrf']: commands.append('no ssh server netconf vrf %s' % 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)
else: else:
if have['state'] == 'absent': if have['state'] == 'absent':
commands.append('netconf-yang agent ssh') commands.append('netconf-yang agent ssh')
@ -131,9 +127,9 @@ def parse_port(config):
def map_config_to_obj(module): def map_config_to_obj(module):
obj = {'state': 'absent'} 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] ssh_config = [config_line for config_line in (line.strip() for line in ssh_config.splitlines()) if config_line]
obj['netconf_vrf'] = [] obj['netconf_vrf'] = []
for config in ssh_config: for config in ssh_config:
@ -141,7 +137,7 @@ def map_config_to_obj(module):
obj.update({'netconf_port': parse_port(config)}) obj.update({'netconf_port': parse_port(config)})
if 'netconf vrf' in config: if 'netconf vrf' in config:
obj['netconf_vrf'].append(parse_vrf(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'}) obj.update({'state': 'present'})
if 'ssh' in netconf_config and 'netconf_port' not in obj: if 'ssh' in netconf_config and 'netconf_port' not in obj:
@ -176,7 +172,7 @@ def main():
""" """
argument_spec = dict( argument_spec = dict(
netconf_port=dict(type='int', default=830, aliases=['listens_on']), 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']), state=dict(default='present', choices=['present', 'absent']),
) )
argument_spec.update(iosxr_argument_spec) argument_spec.update(iosxr_argument_spec)
@ -185,7 +181,6 @@ def main():
supports_check_mode=True) supports_check_mode=True)
warnings = list() warnings = list()
check_args(module, warnings)
result = {'changed': False, 'warnings': warnings} result = {'changed': False, 'warnings': warnings}
@ -197,10 +192,6 @@ def main():
if commands: if commands:
if not module.check_mode: if not module.check_mode:
diff = load_config(module, commands, result['warnings'], commit=True) 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 result['changed'] = True
module.exit_json(**result) module.exit_json(**result)

View file

@ -108,18 +108,21 @@ import re
from ansible.module_utils.basic import AnsibleModule 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 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): def diff_list(want, have):
adds = set(want).difference(have) adds = set(want).difference(have)
removes = set(have).difference(want) removes = set(have).difference(want)
return (adds, removes) return (adds, removes)
def map_obj_to_commands(want, have, module): def map_obj_to_commands(want, have, module):
commands = list() commands = list()
state = module.params['state'] 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 state == 'absent':
if have['hostname'] != 'ios': if have['hostname'] != 'ios':
@ -167,20 +170,24 @@ def map_obj_to_commands(want, have, module):
return commands return commands
def parse_hostname(config): def parse_hostname(config):
match = re.search(r'^hostname (\S+)', config, re.M) match = re.search(r'^hostname (\S+)', config, re.M)
return match.group(1) return match.group(1)
def parse_domain_name(config): def parse_domain_name(config):
match = re.search(r'^domain name (\S+)', config, re.M) match = re.search(r'^domain name (\S+)', config, re.M)
if match: if match:
return match.group(1) return match.group(1)
def parse_lookup_source(config): def parse_lookup_source(config):
match = re.search(r'^domain lookup source-interface (\S+)', config, re.M) match = re.search(r'^domain lookup source-interface (\S+)', config, re.M)
if match: if match:
return match.group(1) return match.group(1)
def map_config_to_obj(module): def map_config_to_obj(module):
config = get_config(module) config = get_config(module)
return { return {
@ -192,6 +199,7 @@ def map_config_to_obj(module):
'name_servers': re.findall(r'^domain name-server (\S+)', config, re.M) 'name_servers': re.findall(r'^domain name-server (\S+)', config, re.M)
} }
def map_params_to_obj(module): def map_params_to_obj(module):
return { return {
'hostname': module.params['hostname'], 'hostname': module.params['hostname'],
@ -202,6 +210,7 @@ def map_params_to_obj(module):
'name_servers': module.params['name_servers'] 'name_servers': module.params['name_servers']
} }
def main(): def main():
""" Main entry point for Ansible module execution """ Main entry point for Ansible module execution
""" """
@ -223,7 +232,6 @@ def main():
supports_check_mode=True) supports_check_mode=True)
warnings = list() warnings = list()
check_args(module, warnings)
result = {'changed': False, 'warnings': warnings} result = {'changed': False, 'warnings': warnings}
@ -240,5 +248,6 @@ def main():
module.exit_json(**result) module.exit_json(**result)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -26,6 +26,7 @@ description:
either individual usernames or the aggregate of usernames in the either individual usernames or the aggregate of usernames in the
current running config. It also supports purging usernames from the current running config. It also supports purging usernames from the
configuration that are not explicitly defined. configuration that are not explicitly defined.
extends_documentation_fragment: iosxr
notes: notes:
- Tested against IOS XR 6.1.2 - Tested against IOS XR 6.1.2
options: options:
@ -166,7 +167,7 @@ from copy import deepcopy
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.common.utils import remove_default_spec 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 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: try:
from base64 import b64decode from base64 import b64decode
@ -228,7 +229,7 @@ def map_obj_to_commands(updates, module):
def map_config_to_obj(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('!') users = data.strip().rstrip('!').split('!')
if not users: if not users:
@ -453,8 +454,6 @@ def main():
'To set a user password use "configured_password" instead.' 'To set a user password use "configured_password" instead.'
) )
check_args(module, warnings)
result = {'changed': False} result = {'changed': False}
want = map_params_to_obj(module) want = map_params_to_obj(module)

View file

@ -48,7 +48,17 @@ class ActionModule(_ActionModule):
elif self._play_context.connection == 'local': elif self._play_context.connection == 'local':
provider = load_provider(iosxr_provider_spec, self._task.args) provider = load_provider(iosxr_provider_spec, self._task.args)
pc = copy.deepcopy(self._play_context) 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.network_os = 'iosxr'
pc.remote_addr = provider['host'] or self._play_context.remote_addr pc.remote_addr = provider['host'] or self._play_context.remote_addr
pc.port = int(provider['port'] or self._play_context.port or 22) 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 # make sure we are in the right cli context which should be
# enable mode and not config module # enable mode and not config module
if socket_path is None: if pc.connection == 'network_cli':
socket_path = self._connection.socket_path if socket_path is None:
socket_path = self._connection.socket_path
conn = 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')
out = conn.get_prompt() 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) result = super(ActionModule, self).run(tmp, task_vars)
return result return result

View file

@ -56,14 +56,19 @@ class Cliconf(CliconfBase):
return device_info return device_info
def get_config(self, source='running'): def get_config(self, source='running', filter=None):
lookup = {'running': 'running-config'} lookup = {'running': 'running-config'}
if source not in lookup: if source not in lookup:
return self.invalid_params("fetching configuration from %s is not supported" % source) 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): 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) self.send_command(cmd)
def get(self, command, prompt=None, answer=None, sendonly=False): def get(self, command, prompt=None, answer=None, sendonly=False):

View file

@ -54,10 +54,9 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
problems. problems.
List of supported rpc's: List of supported rpc's:
:get: Retrieves running configuration and device state information
:get_config: Retrieves the specified configuration from the device :get_config: Retrieves the specified configuration from the device
:edit_config: Loads the specified commands into the remote 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 :commit: Load configuration from candidate to running
:discard_changes: Discard changes to candidate datastore :discard_changes: Discard changes to candidate datastore
:validate: Validate the contents of the specified configuration. :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. :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 :copy_config: create or replace an entire configuration datastore with the contents of another complete
configuration datastore. configuration datastore.
:get-schema: Retrieves the required schema from the device
:get_capabilities: Retrieves device information and supported rpc methods
For JUNOS: For JUNOS:
:execute_rpc: RPC to be execute on remote device :execute_rpc: RPC to be execute on remote device
:load_configuration: Loads given configuration on 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 :source: name of the configuration datastore being queried
:filter: specifies the portion of the configuration to retrieve :filter: specifies the portion of the configuration to retrieve
(by default entire configuration is retrieved)""" (by default entire configuration is retrieved)"""
return self.m.get_config(*args, **kwargs).data_xml pass
@ensure_connected @ensure_connected
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
@ -108,7 +110,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
*filter* specifies the portion of the configuration to retrieve *filter* specifies the portion of the configuration to retrieve
(by default entire configuration is retrieved) (by default entire configuration is retrieved)
""" """
return self.m.get(*args, **kwargs).data_xml pass
@ensure_connected @ensure_connected
def edit_config(self, *args, **kwargs): 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"` } :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. The `"rollback-on-error"` *error_option* depends on the `:rollback-on-error` capability.
""" """
try: pass
return self.m.edit_config(*args, **kwargs).data_xml
except RPCError as exc:
raise Exception(to_xml(exc.xml))
@ensure_connected @ensure_connected
def validate(self, *args, **kwargs): 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` :source: is the name of the configuration datastore being validated or `config`
element containing the configuration subtree to be validated element containing the configuration subtree to be validated
""" """
return self.m.validate(*args, **kwargs).data_xml pass
@ensure_connected @ensure_connected
def copy_config(self, *args, **kwargs): def copy_config(self, *args, **kwargs):
@ -162,7 +161,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
def discard_changes(self, *args, **kwargs): def discard_changes(self, *args, **kwargs):
"""Revert the candidate configuration to the currently running configuration. """Revert the candidate configuration to the currently running configuration.
Any uncommitted changes are discarded.""" Any uncommitted changes are discarded."""
return self.m.discard_changes(*args, **kwargs).data_xml pass
@ensure_connected @ensure_connected
def commit(self, *args, **kwargs): def commit(self, *args, **kwargs):
@ -176,10 +175,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
:confirmed: whether this is a confirmed commit :confirmed: whether this is a confirmed commit
:timeout: specifies the confirm timeout in seconds :timeout: specifies the confirm timeout in seconds
""" """
try: pass
return self.m.commit(*args, **kwargs).data_xml
except RPCError as exc:
raise Exception(to_xml(exc.xml))
@ensure_connected @ensure_connected
def validate(self, *args, **kwargs): def validate(self, *args, **kwargs):
@ -187,8 +183,18 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
:source: name of configuration data store""" :source: name of configuration data store"""
return self.m.validate(*args, **kwargs).data_xml 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 @abstractmethod
def get_capabilities(self, commands): def get_capabilities(self):
"""Retrieves device information and supported """Retrieves device information and supported
rpc methods by device platform and return result rpc methods by device platform and return result
as a string as a string
@ -213,3 +219,5 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
def fetch_file(self, source, destination): def fetch_file(self, source, destination):
"""Fetch file over scp from remote device""" """Fetch file over scp from remote device"""
pass pass
# TODO Restore .data_xml, when ncclient supports it for all platforms

View file

@ -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 <http://www.gnu.org/licenses/>.
#
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 = '''<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="no"/>
<xsl:template match="/|comment()|processing-instruction()">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="*">
<xsl:element name="{local-name()}">
<xsl:apply-templates select="@*|node()"/>
</xsl:element>
</xsl:template>
<xsl:template match="@*">
<xsl:attribute name="{local-name()}">
<xsl:value-of select="."/>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
'''
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))

View file

@ -152,3 +152,39 @@ class Netconf(NetconfBase):
def halt(self): def halt(self):
"""reboot the device""" """reboot the device"""
return self.m.halt().data_xml 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

View file

@ -64,6 +64,9 @@ options:
key used to authenticate the SSH session. If the value is not specified 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) in the task, the value of environment variable C(ANSIBLE_NET_SSH_KEYFILE)
will be used instead. will be used instead.
requirements:
- "ncclient >= 0.5.3 when using netconf"
- "lxml >= 4.1.1 when using netconf"
notes: notes:
- For more information on using Ansible to manage Cisco devices see U(https://www.ansible.com/ansible-cisco). - For more information on using Ansible to manage Cisco devices see U(https://www.ansible.com/ansible-cisco).
""" """

View file

@ -1,2 +1,3 @@
--- ---
- { include: cli.yaml, tags: ['cli'] } - { include: cli.yaml, tags: ['cli'] }
- { include: netconf.yaml, tags: ['netconf'] }

View file

@ -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

View file

@ -2,6 +2,7 @@
- name: setup - remove login - name: setup - remove login
iosxr_banner: iosxr_banner:
banner: login banner: login
provider: "{{ cli }}"
state: absent state: absent
- name: Set login - name: Set login
@ -11,6 +12,7 @@
this is my login banner this is my login banner
that has a multiline that has a multiline
string string
provider: "{{ cli }}"
state: present state: present
register: result register: result
@ -30,6 +32,7 @@
this is my login banner this is my login banner
that has a multiline that has a multiline
string string
provider: "{{ cli }}"
state: present state: present
register: result register: result

View file

@ -3,6 +3,7 @@
iosxr_banner: iosxr_banner:
banner: motd banner: motd
state: absent state: absent
provider: "{{ cli }}"
- name: Set motd - name: Set motd
iosxr_banner: iosxr_banner:
@ -11,6 +12,7 @@
this is my motd banner this is my motd banner
that has a multiline that has a multiline
string string
provider: "{{ cli }}"
state: present state: present
register: result register: result
@ -30,6 +32,7 @@
this is my motd banner this is my motd banner
that has a multiline that has a multiline
string string
provider: "{{ cli }}"
state: present state: present
register: result register: result

View file

@ -5,12 +5,14 @@
text: | text: |
Junk login banner Junk login banner
over multiple lines over multiple lines
provider: "{{ cli }}"
state: present state: present
- name: remove login - name: remove login
iosxr_banner: iosxr_banner:
banner: login banner: login
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
- debug: - debug:
@ -25,6 +27,7 @@
iosxr_banner: iosxr_banner:
banner: login banner: login
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
- assert: - assert:

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -3,7 +3,7 @@
- name: run invalid command - name: run invalid command
iosxr_command: iosxr_command:
commands: ['show foo'] commands: [{command: 'show foo', prompt: 'fooprompt', answer: 'yes'}]
register: result register: result
ignore_errors: yes ignore_errors: yes
@ -15,7 +15,7 @@
iosxr_command: iosxr_command:
commands: commands:
- show version - show version
- show foo - [{command: 'show foo', prompt: 'fooprompt', answer: 'yes'}]
register: result register: result
ignore_errors: yes ignore_errors: yes

View file

@ -6,6 +6,7 @@
iosxr_facts: iosxr_facts:
gather_subset: gather_subset:
- all - all
provider: "{{ cli }}"
register: result register: result

View file

@ -4,6 +4,7 @@
- name: test getting default facts - name: test getting default facts
iosxr_facts: iosxr_facts:
provider: "{{ cli }}"
register: result register: result
- assert: - assert:

View file

@ -6,6 +6,7 @@
iosxr_facts: iosxr_facts:
gather_subset: gather_subset:
- "foobar" - "foobar"
provider: "{{ cli }}"
register: result register: result
ignore_errors: true ignore_errors: true
@ -28,6 +29,7 @@
gather_subset: gather_subset:
- "!hardware" - "!hardware"
- "hardware" - "hardware"
provider: "{{ cli }}"
register: result register: result
ignore_errors: true ignore_errors: true

View file

@ -6,6 +6,7 @@
iosxr_facts: iosxr_facts:
gather_subset: gather_subset:
- "!hardware" - "!hardware"
provider: "{{ cli }}"
register: result register: result
- assert: - assert:

View file

@ -5,6 +5,7 @@
iosxr_interface: iosxr_interface:
name: GigabitEthernet0/0/0/2 name: GigabitEthernet0/0/0/2
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
@ -13,6 +14,7 @@
name: GigabitEthernet0/0/0/2 name: GigabitEthernet0/0/0/2
description: test-interface-initial description: test-interface-initial
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -25,6 +27,7 @@
name: GigabitEthernet0/0/0/2 name: GigabitEthernet0/0/0/2
description: test-interface-initial description: test-interface-initial
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -39,6 +42,7 @@
duplex: half duplex: half
mtu: 512 mtu: 512
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -57,6 +61,7 @@
duplex: full duplex: full
mtu: 256 mtu: 256
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -75,6 +80,7 @@
duplex: full duplex: full
mtu: 256 mtu: 256
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
that: that:
@ -84,6 +90,7 @@
iosxr_interface: iosxr_interface:
name: GigabitEthernet0/0/0/2 name: GigabitEthernet0/0/0/2
enabled: False enabled: False
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -95,6 +102,7 @@
iosxr_interface: iosxr_interface:
name: GigabitEthernet0/0/0/2 name: GigabitEthernet0/0/0/2
enabled: True enabled: True
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -107,6 +115,7 @@
name: GigabitEthernet0/0/0/3 name: GigabitEthernet0/0/0/3
description: test-interface-initial description: test-interface-initial
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -120,6 +129,7 @@
- name: GigabitEthernet0/0/0/3 - name: GigabitEthernet0/0/0/3
- name: GigabitEthernet0/0/0/2 - name: GigabitEthernet0/0/0/2
state: absent state: absent
provider: "{{ cli }}"
- name: Add interface aggregate - name: Add interface aggregate
iosxr_interface: iosxr_interface:
@ -129,6 +139,7 @@
speed: 100 speed: 100
duplex: full duplex: full
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -152,6 +163,7 @@
speed: 100 speed: 100
duplex: full duplex: full
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -165,6 +177,7 @@
- name: GigabitEthernet0/0/0/2 - name: GigabitEthernet0/0/0/2
enabled: False enabled: False
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -180,6 +193,7 @@
- name: GigabitEthernet0/0/0/2 - name: GigabitEthernet0/0/0/2
enabled: True enabled: True
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -194,6 +208,7 @@
- name: GigabitEthernet0/0/0/4 - name: GigabitEthernet0/0/0/4
- name: GigabitEthernet0/0/0/5 - name: GigabitEthernet0/0/0/5
description: test-interface-initial description: test-interface-initial
provider: "{{ cli }}"
register: result register: result
- name: Create interface aggregate - name: Create interface aggregate
@ -204,6 +219,7 @@
- name: GigabitEthernet0/0/0/5 - name: GigabitEthernet0/0/0/5
description: test_interface_2 description: test_interface_2
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -218,6 +234,7 @@
- name: GigabitEthernet0/0/0/4 - name: GigabitEthernet0/0/0/4
- name: GigabitEthernet0/0/0/5 - name: GigabitEthernet0/0/0/5
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -232,6 +249,7 @@
- name: GigabitEthernet0/0/0/4 - name: GigabitEthernet0/0/0/4
- name: GigabitEthernet0/0/0/5 - name: GigabitEthernet0/0/0/5
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
- assert: - assert:

View file

@ -7,6 +7,7 @@
description: test_interface_1 description: test_interface_1
enabled: True enabled: True
state: present state: present
provider: "{{ cli }}"
register: result register: result
- name: Check intent arguments - name: Check intent arguments
@ -14,6 +15,7 @@
name: GigabitEthernet0/0/0/1 name: GigabitEthernet0/0/0/1
state: up state: up
delay: 20 delay: 20
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -24,6 +26,7 @@
iosxr_interface: iosxr_interface:
name: GigabitEthernet0/0/0/1 name: GigabitEthernet0/0/0/1
state: down state: down
provider: "{{ cli }}"
ignore_errors: yes ignore_errors: yes
register: result register: result
@ -38,6 +41,7 @@
enabled: False enabled: False
state: down state: down
delay: 20 delay: 20
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -49,6 +53,7 @@
name: GigabitEthernet0/0/0/1 name: GigabitEthernet0/0/0/1
enabled: False enabled: False
state: up state: up
provider: "{{ cli }}"
ignore_errors: yes ignore_errors: yes
register: result register: result
@ -64,6 +69,7 @@
enabled: True enabled: True
state: up state: up
delay: 20 delay: 20
provider: "{{ cli }}"
ignore_errors: yes ignore_errors: yes
register: result register: result

View file

@ -5,12 +5,14 @@
dest: hostnameprefix dest: hostnameprefix
name: 172.16.0.1 name: 172.16.0.1
state: absent state: absent
provider: "{{ cli }}"
- name: Remove console logging - name: Remove console logging
iosxr_logging: iosxr_logging:
dest: console dest: console
level: warning level: warning
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
- name: Remove buffer - name: Remove buffer
@ -18,6 +20,7 @@
dest: buffered dest: buffered
size: 4800000 size: 4800000
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
# Start tests # Start tests
@ -26,6 +29,7 @@
dest: hostnameprefix dest: hostnameprefix
name: 172.16.0.1 name: 172.16.0.1
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -39,6 +43,7 @@
dest: hostnameprefix dest: hostnameprefix
name: 172.16.0.1 name: 172.16.0.1
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -50,6 +55,7 @@
dest: hostnameprefix dest: hostnameprefix
name: 172.16.0.1 name: 172.16.0.1
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -62,6 +68,7 @@
dest: hostnameprefix dest: hostnameprefix
name: 172.16.0.1 name: 172.16.0.1
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -73,6 +80,7 @@
dest: console dest: console
level: warning level: warning
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -84,6 +92,7 @@
iosxr_logging: iosxr_logging:
dest: buffered dest: buffered
size: 4800000 size: 4800000
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -96,6 +105,7 @@
aggregate: aggregate:
- { dest: console, level: notifications } - { dest: console, level: notifications }
- { dest: buffered, size: 4700000 } - { dest: buffered, size: 4700000 }
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -110,6 +120,7 @@
- { dest: console, level: notifications } - { dest: console, level: notifications }
- { dest: buffered, size: 4700000 } - { dest: buffered, size: 4700000 }
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
- assert: - assert:

View file

@ -7,12 +7,14 @@
- no ip domain-list ansible.com - no ip domain-list ansible.com
- no ip domain-list redhat.com - no ip domain-list redhat.com
match: none match: none
provider: "{{ cli }}"
- name: configure domain_search - name: configure domain_search
iosxr_system: iosxr_system:
domain_search: domain_search:
- ansible.com - ansible.com
- redhat.com - redhat.com
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -26,6 +28,7 @@
domain_search: domain_search:
- ansible.com - ansible.com
- redhat.com - redhat.com
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -36,6 +39,7 @@
iosxr_system: iosxr_system:
domain_search: domain_search:
- ansible.com - ansible.com
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -47,6 +51,7 @@
iosxr_system: iosxr_system:
domain_search: domain_search:
- ansible.com - ansible.com
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -58,6 +63,7 @@
domain_search: domain_search:
- ansible.com - ansible.com
- redhat.com - redhat.com
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -70,6 +76,7 @@
domain_search: domain_search:
- ansible.com - ansible.com
- redhat.com - redhat.com
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -81,6 +88,7 @@
domain_search: domain_search:
- ansible.com - ansible.com
- eng.ansible.com - eng.ansible.com
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -95,6 +103,7 @@
domain_search: domain_search:
- ansible.com - ansible.com
- eng.ansible.com - eng.ansible.com
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -108,5 +117,6 @@
- no domain list redhat.com - no domain list redhat.com
- no domain list eng.ansible.com - no domain list eng.ansible.com
match: none match: none
provider: "{{ cli }}"
- debug: msg="END cli/set_domain_search.yaml" - debug: msg="END cli/set_domain_search.yaml"

View file

@ -5,10 +5,12 @@
iosxr_config: iosxr_config:
lines: no domain name lines: no domain name
match: none match: none
provider: "{{ cli }}"
- name: configure domain_name - name: configure domain_name
iosxr_system: iosxr_system:
domain_name: eng.ansible.com domain_name: eng.ansible.com
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -18,6 +20,7 @@
- name: verify domain_name - name: verify domain_name
iosxr_system: iosxr_system:
domain_name: eng.ansible.com domain_name: eng.ansible.com
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -28,5 +31,6 @@
iosxr_config: iosxr_config:
lines: no domain name lines: no domain name
match: none match: none
provider: "{{ cli }}"
- debug: msg="END cli/set_domain_name.yaml" - debug: msg="END cli/set_domain_name.yaml"

View file

@ -5,10 +5,12 @@
iosxr_config: iosxr_config:
lines: hostname switch lines: hostname switch
match: none match: none
provider: "{{ cli }}"
- name: configure hostname - name: configure hostname
iosxr_system: iosxr_system:
hostname: foo hostname: foo
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -18,6 +20,7 @@
- name: verify hostname - name: verify hostname
iosxr_system: iosxr_system:
hostname: foo hostname: foo
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -28,5 +31,6 @@
iosxr_config: iosxr_config:
lines: "hostname {{ inventory_hostname }}" lines: "hostname {{ inventory_hostname }}"
match: none match: none
provider: "{{ cli }}"
- debug: msg="END cli/set_hostname.yaml" - debug: msg="END cli/set_hostname.yaml"

View file

@ -7,10 +7,12 @@
- no domain lookup source-interface Loopback10 - no domain lookup source-interface Loopback10
# - vrf ansible # - vrf ansible
match: none match: none
provider: "{{ cli }}"
- name: configure lookup_source - name: configure lookup_source
iosxr_system: iosxr_system:
lookup_source: Loopback10 lookup_source: Loopback10
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -21,6 +23,7 @@
- name: verify lookup_source - name: verify lookup_source
iosxr_system: iosxr_system:
lookup_source: Loopback10 lookup_source: Loopback10
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -58,5 +61,6 @@
- no domain lookup source-interface Loopback10 - no domain lookup source-interface Loopback10
- no vrf ansible - no vrf ansible
match: none match: none
provider: "{{ cli }}"
- debug: msg="END cli/set_lookup_source.yaml" - debug: msg="END cli/set_lookup_source.yaml"

View file

@ -8,6 +8,7 @@
- no ip name-server 2.2.2.2 - no ip name-server 2.2.2.2
- no ip name-server 3.3.3.3 - no ip name-server 3.3.3.3
match: none match: none
provider: "{{ cli }}"
- name: configure name_servers - name: configure name_servers
iosxr_system: iosxr_system:
@ -15,6 +16,7 @@
- 1.1.1.1 - 1.1.1.1
- 2.2.2.2 - 2.2.2.2
- 3.3.3.3 - 3.3.3.3
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -31,6 +33,7 @@
- 1.1.1.1 - 1.1.1.1
- 2.2.2.2 - 2.2.2.2
- 3.3.3.3 - 3.3.3.3
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -69,6 +72,7 @@
name_servers: name_servers:
- 1.1.1.1 - 1.1.1.1
- 2.2.2.2 - 2.2.2.2
provider: "{{ cli }}"
register: result register: result
- assert: - assert:

View file

@ -5,6 +5,7 @@
name: auth_user name: auth_user
state: present state: present
configured_password: pass123 configured_password: pass123
provider: "{{ cli }}"
- name: test login - name: test login
expect: expect:
@ -30,6 +31,7 @@
name: auth_user name: auth_user
state: present state: present
public_key_contents: "{{ lookup('file', \"{{ role_path }}/files/public.pub\") }}" public_key_contents: "{{ lookup('file', \"{{ role_path }}/files/public.pub\") }}"
provider: "{{ cli }}"
- name: test login with private key - name: test login with private key
expect: expect:
@ -40,6 +42,7 @@
- name: remove user and key - name: remove user and key
iosxr_user: iosxr_user:
name: auth_user name: auth_user
provider: "{{ cli }}"
state: absent state: absent
- name: test login with private key (should fail, no user) - name: test login with private key (should fail, no user)
@ -55,6 +58,7 @@
name: auth_user name: auth_user
state: present state: present
public_key: "{{ role_path }}/files/public.pub" public_key: "{{ role_path }}/files/public.pub"
provider: "{{ cli }}"
- name: test login with private key - name: test login with private key
expect: expect:
@ -68,6 +72,7 @@
name: auth_user name: auth_user
state: present state: present
public_key_contents: "{{ lookup('file', \"{{ role_path }}/files/public2.pub\") }}" public_key_contents: "{{ lookup('file', \"{{ role_path }}/files/public2.pub\") }}"
provider: "{{ cli }}"
# FIXME: pexpect fails with OSError: [Errno 5] Input/output error # FIXME: pexpect fails with OSError: [Errno 5] Input/output error
- name: test login with invalid private key (should fail) - name: test login with invalid private key (should fail)
@ -88,4 +93,5 @@
iosxr_user: iosxr_user:
name: auth_user name: auth_user
state: absent state: absent
provider: "{{ cli }}"
register: result register: result

View file

@ -5,12 +5,14 @@
- no username ansibletest1 - no username ansibletest1
- no username ansibletest2 - no username ansibletest2
- no username ansibletest3 - no username ansibletest3
provider: "{{ cli }}"
- name: Create user (SetUp) - name: Create user (SetUp)
iosxr_user: iosxr_user:
name: ansibletest1 name: ansibletest1
configured_password: test configured_password: test
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -25,6 +27,7 @@
configured_password: test configured_password: test
update_password: always update_password: always
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -39,6 +42,7 @@
configured_password: test configured_password: test
update_password: on_create update_password: on_create
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -53,6 +57,7 @@
update_password: on_create update_password: on_create
group: sysadmin group: sysadmin
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -68,6 +73,7 @@
update_password: on_create update_password: on_create
group: sysadmin group: sysadmin
state: present state: present
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -83,6 +89,7 @@
configured_password: test configured_password: test
state: present state: present
group: sysadmin group: sysadmin
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -103,6 +110,7 @@
configured_password: test configured_password: test
state: present state: present
group: sysadmin group: sysadmin
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -122,6 +130,7 @@
update_password: on_create update_password: on_create
state: present state: present
group: sysadmin group: sysadmin
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -136,6 +145,7 @@
- name: ansibletest2 - name: ansibletest2
- name: ansibletest3 - name: ansibletest3
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
- assert: - assert:
@ -150,6 +160,7 @@
- name: ansibletest2 - name: ansibletest2
- name: ansibletest3 - name: ansibletest3
state: absent state: absent
provider: "{{ cli }}"
register: result register: result
- assert: - assert:

View file

@ -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_facts.py
lib/ansible/modules/network/ios/ios_system.py lib/ansible/modules/network/ios/ios_system.py
lib/ansible/modules/network/ios/ios_vrf.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_cluster.py
lib/ansible/modules/network/netvisor/pn_ospfarea.py lib/ansible/modules/network/netvisor/pn_ospfarea.py
lib/ansible/modules/network/netvisor/pn_vlag.py lib/ansible/modules/network/netvisor/pn_vlag.py

View file

@ -32,13 +32,13 @@ class TestIosxrCommandModule(TestIosxrModule):
def setUp(self): def setUp(self):
super(TestIosxrCommandModule, self).setUp() super(TestIosxrCommandModule, self).setUp()
self.mock_run_commands = patch('ansible.modules.network.iosxr.iosxr_command.run_commands') self.mock_run_command = patch('ansible.modules.network.iosxr.iosxr_command.run_command')
self.run_commands = self.mock_run_commands.start() self.run_command = self.mock_run_command.start()
def tearDown(self): def tearDown(self):
super(TestIosxrCommandModule, self).tearDown() super(TestIosxrCommandModule, self).tearDown()
self.mock_run_commands.stop() self.mock_run_command.stop()
def load_fixtures(self, commands=None): def load_fixtures(self, commands=None):
@ -49,13 +49,13 @@ class TestIosxrCommandModule(TestIosxrModule):
for item in commands: for item in commands:
try: try:
command = item['command'] command = item['command']
except ValueError: except Exception:
command = item command = item
filename = str(command).replace(' ', '_') filename = str(command).replace(' ', '_')
output.append(load_fixture(filename)) output.append(load_fixture(filename))
return output return output
self.run_commands.side_effect = load_from_file self.run_command.side_effect = load_from_file
def test_iosxr_command_simple(self): def test_iosxr_command_simple(self):
set_module_args(dict(commands=['show version'])) set_module_args(dict(commands=['show version']))
@ -78,13 +78,13 @@ class TestIosxrCommandModule(TestIosxrModule):
wait_for = 'result[0] contains "test string"' wait_for = 'result[0] contains "test string"'
set_module_args(dict(commands=['show version'], wait_for=wait_for)) set_module_args(dict(commands=['show version'], wait_for=wait_for))
self.execute_module(failed=True) 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): def test_iosxr_command_retries(self):
wait_for = 'result[0] contains "test string"' wait_for = 'result[0] contains "test string"'
set_module_args(dict(commands=['show version'], wait_for=wait_for, retries=2)) set_module_args(dict(commands=['show version'], wait_for=wait_for, retries=2))
self.execute_module(failed=True) 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): def test_iosxr_command_match_any(self):
wait_for = ['result[0] contains "Cisco IOS"', wait_for = ['result[0] contains "Cisco IOS"',

View file

@ -34,14 +34,14 @@ class TestIosxrFacts(TestIosxrModule):
def setUp(self): def setUp(self):
super(TestIosxrFacts, self).setUp() super(TestIosxrFacts, self).setUp()
self.mock_run_commands = patch( self.mock_run_command = patch(
'ansible.modules.network.iosxr.iosxr_facts.run_commands') 'ansible.modules.network.iosxr.iosxr_facts.run_command')
self.run_commands = self.mock_run_commands.start() self.run_command = self.mock_run_command.start()
def tearDown(self): def tearDown(self):
super(TestIosxrFacts, self).tearDown() super(TestIosxrFacts, self).tearDown()
self.mock_run_commands.stop() self.mock_run_command.stop()
def load_fixtures(self, commands=None): def load_fixtures(self, commands=None):
@ -60,7 +60,7 @@ class TestIosxrFacts(TestIosxrModule):
output.append(load_fixture(filename)) output.append(load_fixture(filename))
return output 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): def test_iosxr_facts_gather_subset_default(self):
set_module_args(dict()) set_module_args(dict())