diff --git a/lib/ansible/module_utils/junos.py b/lib/ansible/module_utils/junos.py index e7d99c03bb..58885dc607 100644 --- a/lib/ansible/module_utils/junos.py +++ b/lib/ansible/module_utils/junos.py @@ -17,9 +17,8 @@ # along with Ansible. If not, see . # import collections - from contextlib import contextmanager -from xml.etree.ElementTree import Element, SubElement, fromstring +from copy import deepcopy from ansible.module_utils.basic import env_fallback, return_values from ansible.module_utils.netconf import send_request, children @@ -27,6 +26,13 @@ from ansible.module_utils.netconf import discard_changes, validate from ansible.module_utils.six import string_types from ansible.module_utils._text import to_text +try: + from lxml.etree import Element, SubElement, fromstring, tostring + HAS_LXML = True +except ImportError: + from xml.etree.ElementTree import Element, SubElement, fromstring, tostring + HAS_LXML = False + ACTIONS = frozenset(['merge', 'override', 'replace', 'update', 'set']) JSON_ACTIONS = frozenset(['merge', 'override', 'update']) FORMATS = frozenset(['xml', 'text', 'json']) @@ -225,17 +231,18 @@ def get_param(module, key): def map_params_to_obj(module, param_to_xpath_map): """ Creates a new dictionary with key as xpath corresponding - to param and value is a dict with metadata and value for + to param and value is a list of dict with metadata and values for the xpath. Acceptable metadata keys: - 'xpath': Relative xpath corresponding to module param. 'value': Value of param. 'tag_only': Value is indicated by tag only in xml hierarchy. 'leaf_only': If operation is to be added at leaf node only. + 'value_req': If value(text) is requried for leaf node. + 'is_key': If the field is key or not. eg: Output { - 'name': {'xpath': 'name', 'value': 'ge-0/0/1'} - 'disable': {'xpath': 'disable', 'tag_only': True} + 'name': [{'value': 'ge-0/0/1'}] + 'disable': [{'value': True, tag_only': True}] } :param module: @@ -243,17 +250,32 @@ def map_params_to_obj(module, param_to_xpath_map): :return: obj """ obj = collections.OrderedDict() - for key, attrib in param_to_xpath_map.items(): + for key, attribute in param_to_xpath_map.items(): if key in module.params: - if isinstance(attrib, dict): - xpath = attrib.get('xpath') - del attrib['xpath'] + is_attribute_dict = False - attrib.update({'value': module.params[key]}) - obj.update({xpath: attrib}) + value = module.params[key] + if not isinstance(value, (list, tuple)): + value = [value] + + if isinstance(attribute, dict): + xpath = attribute.get('xpath') + is_attribute_dict = True else: - xpath = attrib - obj.update({xpath: {'value': module.params[key]}}) + xpath = attribute + + if not obj.get(xpath): + obj[xpath] = list() + + for val in value: + if is_attribute_dict: + attr = deepcopy(attribute) + del attr['xpath'] + + attr.update({'value': val}) + obj[xpath].append(attr) + else: + obj[xpath].append({'value': val}) return obj @@ -261,7 +283,7 @@ def map_obj_to_ele(module, want, top, value_map=None): top_ele = top.split('/') root = Element(top_ele[0]) ele = root - oper = None + if len(top_ele) > 1: for item in top_ele[1:-1]: ele = SubElement(ele, item) @@ -270,41 +292,58 @@ def map_obj_to_ele(module, want, top, value_map=None): # build xml subtree for obj in want: - node = SubElement(container, top_ele[-1]) + oper = None + if container.tag != top_ele[-1]: + node = SubElement(container, top_ele[-1]) + else: + node = container + if state and state != 'present': oper = OPERATION_LOOK_UP.get(state) - node.set(oper, oper) - for xpath, attrib in obj.items(): - tag_only = attrib.get('tag_only', False) - leaf_only = attrib.get('leaf_only', False) - value = attrib.get('value') + for xpath, attributes in obj.items(): + for attr in attributes: + tag_only = attr.get('tag_only', False) + leaf_only = attr.get('leaf_only', False) + is_value = attr.get('value_req', False) + is_key = attr.get('is_key', False) + value = attr.get('value') - # convert param value to device specific value - if value_map and xpath in value_map: - value = value_map[xpath].get(value) + # operation (delete/active/inactive) is added as element attribute + # only if it is key or tag only or leaf only node + if oper and not (is_key or tag_only or leaf_only): + continue - # for leaf only fields operation attributes should be at leaf level - # and not at node level. - if leaf_only and node.attrib.get(oper): - node.attrib.pop(oper) + # convert param value to device specific value + if value_map and xpath in value_map: + value = value_map[xpath].get(value) - if value or tag_only or leaf_only: - ele = node - tags = xpath.split('/') + if value or tag_only or (leaf_only and value): + ele = node + tags = xpath.split('/') + if value: + value = to_text(value, errors='surrogate_then_replace') - for item in tags: - ele = SubElement(ele, item) + for item in tags: + ele = SubElement(ele, item) - if tag_only: - if not value: - ele.set('delete', 'delete') - elif leaf_only and oper: - ele.set(oper, oper) - else: - ele.text = to_text(value, errors='surrogate_then_replace') - - if state != 'present': - break + if tag_only: + if not value: + ele.set('delete', 'delete') + elif leaf_only: + if oper: + ele.set(oper, oper) + if is_value: + ele.text = value + else: + ele.text = value + else: + ele.text = value + if HAS_LXML: + par = ele.getparent() + else: + module.fail_json(msg='lxml is not installed.') + if is_key and oper and not par.attrib.get(oper): + par.set(oper, oper) return root diff --git a/lib/ansible/module_utils/netconf.py b/lib/ansible/module_utils/netconf.py index 26ae60fed5..225ad103b3 100644 --- a/lib/ansible/module_utils/netconf.py +++ b/lib/ansible/module_utils/netconf.py @@ -26,10 +26,14 @@ # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from contextlib import contextmanager -from xml.etree.ElementTree import Element, SubElement, fromstring, tostring from ansible.module_utils.connection import exec_command +try: + from lxml.etree import Element, SubElement, fromstring, tostring +except ImportError: + from xml.etree.ElementTree import Element, SubElement, fromstring, tostring + NS_MAP = {'nc': "urn:ietf:params:xml:ns:netconf:base:1.0"} diff --git a/lib/ansible/modules/network/junos/_junos_template.py b/lib/ansible/modules/network/junos/_junos_template.py index 9f01d5e496..c058770a48 100644 --- a/lib/ansible/modules/network/junos/_junos_template.py +++ b/lib/ansible/modules/network/junos/_junos_template.py @@ -21,7 +21,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.0', 'supported_by': 'community'} - DOCUMENTATION = """ --- module: junos_template @@ -87,6 +86,8 @@ options: required: false default: null choices: ['text', 'xml', 'set'] +requirements: + - ncclient (>=v0.5.2) notes: - This module requires the netconf system service be enabled on the remote device being managed @@ -111,11 +112,11 @@ EXAMPLES = """ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.junos import check_args, junos_argument_spec from ansible.module_utils.junos import get_configuration, load_config -from ansible.module_utils.six import text_type USE_PERSISTENT_CONNECTION = True DEFAULT_COMMENT = 'configured by junos_template' + def main(): argument_spec = dict( @@ -137,16 +138,13 @@ def main(): result = {'changed': False, 'warnings': warnings} - comment = module.params['comment'] - confirm = module.params['confirm'] commit = not module.check_mode action = module.params['action'] src = module.params['src'] fmt = module.params['config_format'] if action == 'overwrite' and fmt == 'set': - module.fail_json(msg="overwrite cannot be used when format is " - "set per junos-pyez documentation") + module.fail_json(msg="overwrite cannot be used when format is set per junos-pyez documentation") if module.params['backup']: reply = get_configuration(module, format='set') diff --git a/lib/ansible/modules/network/junos/junos_banner.py b/lib/ansible/modules/network/junos/junos_banner.py index 9c11819fcc..a00b699651 100644 --- a/lib/ansible/modules/network/junos/junos_banner.py +++ b/lib/ansible/modules/network/junos/junos_banner.py @@ -54,6 +54,11 @@ options: present in the current devices active running configuration. default: present choices: ['present', 'absent', 'active', 'suspend'] +requirements: + - ncclient (>=v0.5.2) +notes: + - This module requires the netconf system service be enabled on + the remote device being managed """ EXAMPLES = """ @@ -102,12 +107,15 @@ rpc: """ import collections -from xml.etree.ElementTree import tostring - from ansible.module_utils.junos import junos_argument_spec, check_args from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.junos import load_config, map_params_to_obj, map_obj_to_ele +try: + from lxml.etree import tostring +except ImportError: + from xml.etree.ElementTree import tostring + USE_PERSISTENT_CONNECTION = True diff --git a/lib/ansible/modules/network/junos/junos_command.py b/lib/ansible/modules/network/junos/junos_command.py index 7e75b1d7f3..53633b5401 100644 --- a/lib/ansible/modules/network/junos/junos_command.py +++ b/lib/ansible/modules/network/junos/junos_command.py @@ -104,6 +104,10 @@ options: version_added: "2.3" requirements: - jxmlease + - ncclient (>=v0.5.2) +notes: + - This module requires the netconf system service be enabled on + the remote device being managed """ EXAMPLES = """ @@ -163,17 +167,17 @@ import time import re import shlex -from functools import partial -from xml.etree import ElementTree as etree -from xml.etree.ElementTree import Element, SubElement, tostring - from ansible.module_utils.junos import junos_argument_spec, check_args from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.netcli import Conditional, FailedConditionalError from ansible.module_utils.netconf import send_request -from ansible.module_utils.network_common import ComplexList, to_list from ansible.module_utils.six import string_types, iteritems +try: + from lxml.etree import Element, SubElement, tostring +except ImportError: + from xml.etree.ElementTree import Element, SubElement, tostring + try: import jxmlease HAS_JXMLEASE = True @@ -182,6 +186,7 @@ except ImportError: USE_PERSISTENT_CONNECTION = True + def to_lines(stdout): lines = list() for item in stdout: @@ -190,6 +195,7 @@ def to_lines(stdout): lines.append(item) return lines + def rpc(module, items): responses = list() @@ -238,6 +244,7 @@ def rpc(module, items): return responses + def split(value): lex = shlex.shlex(value) lex.quotes = '"' @@ -245,6 +252,7 @@ def split(value): lex.commenters = '' return list(lex) + def parse_rpcs(module): items = list() @@ -270,6 +278,7 @@ def parse_rpcs(module): return items + def parse_commands(module, warnings): items = list() @@ -329,7 +338,6 @@ def main(): items.extend(parse_rpcs(module)) wait_for = module.params['wait_for'] or list() - display = module.params['display'] conditionals = [Conditional(c) for c in wait_for] retries = module.params['retries'] @@ -344,8 +352,8 @@ def main(): for item, resp in zip(items, responses): if item['xattrs']['format'] == 'xml': if not HAS_JXMLEASE: - module.fail_json(msg='jxmlease is required but does not appear to ' - 'be installed. It can be installed using `pip install jxmlease`') + module.fail_json(msg='jxmlease is required but does not appear to be installed. ' + 'It can be installed using `pip install jxmlease`') try: transformed.append(jxmlease.parse(resp)) @@ -382,9 +390,7 @@ def main(): 'stdout_lines': to_lines(responses) } - module.exit_json(**result) - if __name__ == '__main__': main() diff --git a/lib/ansible/modules/network/junos/junos_config.py b/lib/ansible/modules/network/junos/junos_config.py index b2cc7f2e2a..48d0ad5952 100644 --- a/lib/ansible/modules/network/junos/junos_config.py +++ b/lib/ansible/modules/network/junos/junos_config.py @@ -137,6 +137,8 @@ options: default: merge choices: ['merge', 'override', 'replace'] version_added: "2.3" +requirements: + - ncclient (>=v0.5.2) notes: - This module requires the netconf system service be enabled on the remote device being managed. @@ -185,33 +187,47 @@ import re import json import sys -from xml.etree import ElementTree - from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.junos import get_diff, load_config, get_configuration from ansible.module_utils.junos import junos_argument_spec from ansible.module_utils.junos import check_args as junos_check_args from ansible.module_utils.netconf import send_request from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_text, to_native +from ansible.module_utils._text import to_native -if sys.version_info < (2, 7): - from xml.parsers.expat import ExpatError - ParseError = ExpatError -else: - ParseError = ElementTree.ParseError +try: + from lxml.etree import Element, fromstring +except ImportError: + from xml.etree.ElementTree import Element, fromstring + +try: + from lxml.etree import ParseError +except ImportError: + try: + from xml.etree.ElementTree import ParseError + except ImportError: + # for Python < 2.7 + from xml.parsers.expat import ExpatError + ParseError = ExpatError USE_PERSISTENT_CONNECTION = True DEFAULT_COMMENT = 'configured by junos_config' + def check_args(module, warnings): junos_check_args(module, warnings) if module.params['replace'] is not None: module.fail_json(msg='argument replace is deprecated, use update') -zeroize = lambda x: send_request(x, ElementTree.Element('request-system-zeroize')) -rollback = lambda x: get_diff(x) + +def zeroize(ele): + return send_request(ele, Element('request-system-zeroize')) + + +def rollback(ele): + return get_diff(ele) + def guess_format(config): try: @@ -221,7 +237,7 @@ def guess_format(config): pass try: - ElementTree.fromstring(config) + fromstring(config) return 'xml' except ParseError: pass @@ -231,6 +247,7 @@ def guess_format(config): return 'text' + def filter_delete_statements(module, candidate): reply = get_configuration(module, format='set') match = reply.find('.//configuration-set') @@ -248,6 +265,7 @@ def filter_delete_statements(module, candidate): return modified_candidate + def configure_device(module, warnings): candidate = module.params['lines'] or module.params['src'] @@ -283,6 +301,7 @@ def configure_device(module, warnings): return load_config(module, candidate, warnings, **kwargs) + def main(): """ main entry point for module execution """ diff --git a/lib/ansible/modules/network/junos/junos_facts.py b/lib/ansible/modules/network/junos/junos_facts.py index 7dff91c1d3..d079f2502c 100644 --- a/lib/ansible/modules/network/junos/junos_facts.py +++ b/lib/ansible/modules/network/junos/junos_facts.py @@ -59,9 +59,13 @@ options: default: text choices: ['xml', 'set', 'text', 'json'] version_added: "2.3" +requirements: + - ncclient (>=v0.5.2) notes: - Ensure I(config_format) used to retrieve configuration from device is supported by junos version running on device. + - This module requires the netconf system service be enabled on + the remote device being managed """ EXAMPLES = """ @@ -79,16 +83,18 @@ ansible_facts: returned: always type: dict """ - -from xml.etree.ElementTree import Element, SubElement, tostring - from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception from ansible.module_utils.six import iteritems from ansible.module_utils.junos import junos_argument_spec, check_args, get_param -from ansible.module_utils.junos import command, get_configuration +from ansible.module_utils.junos import get_configuration from ansible.module_utils.netconf import send_request +try: + from lxml.etree import Element, SubElement, tostring +except ImportError: + from xml.etree.ElementTree import Element, SubElement, tostring + try: from jnpr.junos import Device from jnpr.junos.exception import ConnectError diff --git a/lib/ansible/modules/network/junos/junos_interface.py b/lib/ansible/modules/network/junos/junos_interface.py index 224c9375a9..23bcb4304f 100644 --- a/lib/ansible/modules/network/junos/junos_interface.py +++ b/lib/ansible/modules/network/junos/junos_interface.py @@ -76,6 +76,11 @@ options: - State of the Interface configuration. default: present choices: ['present', 'absent', 'active', 'suspend'] +requirements: + - ncclient (>=v0.5.2) +notes: + - This module requires the netconf system service be enabled on + the remote device being managed """ EXAMPLES = """ @@ -136,12 +141,15 @@ rpc: """ import collections -from xml.etree.ElementTree import tostring - from ansible.module_utils.junos import junos_argument_spec, check_args from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.junos import load_config, map_params_to_obj, map_obj_to_ele +try: + from lxml.etree import tostring +except ImportError: + from xml.etree.ElementTree import tostring + USE_PERSISTENT_CONNECTION = True @@ -193,7 +201,7 @@ def main(): param_to_xpath_map = collections.OrderedDict() param_to_xpath_map.update({ - 'name': 'name', + 'name': {'xpath': 'name', 'is_key': True}, 'description': 'description', 'speed': 'speed', 'mtu': 'mtu', diff --git a/lib/ansible/modules/network/junos/junos_netconf.py b/lib/ansible/modules/network/junos/junos_netconf.py index 90680b3f26..7fbca62c00 100644 --- a/lib/ansible/modules/network/junos/junos_netconf.py +++ b/lib/ansible/modules/network/junos/junos_netconf.py @@ -105,11 +105,13 @@ def map_obj_to_commands(updates, module): return commands + def parse_port(config): match = re.search(r'port (\d+)', config) if match: return int(match.group(1)) + def map_config_to_obj(module): cmd = 'show configuration system services netconf' rc, out, err = exec_command(module, cmd) @@ -130,6 +132,7 @@ def validate_netconf_port(value, module): if not 1 <= value <= 65535: module.fail_json(msg='netconf_port must be between 1 and 65535') + def map_params_to_obj(module): obj = { 'netconf_port': module.params['netconf_port'], @@ -144,6 +147,7 @@ def map_params_to_obj(module): return obj + def load_config(module, config, commit=False): exec_command(module, 'configure') @@ -164,6 +168,7 @@ def load_config(module, config, commit=False): return str(diff).strip() + def main(): """main entry point for module execution """ diff --git a/lib/ansible/modules/network/junos/junos_package.py b/lib/ansible/modules/network/junos/junos_package.py index 9dba38b244..0ba1db7ea7 100644 --- a/lib/ansible/modules/network/junos/junos_package.py +++ b/lib/ansible/modules/network/junos/junos_package.py @@ -79,6 +79,7 @@ options: choices: ['true', 'false'] requirements: - junos-eznc + - ncclient (>=v0.5.2) notes: - This module requires the netconf system service be enabled on the remote device being managed @@ -142,7 +143,8 @@ def install_package(module, device): package = module.params['src'] no_copy = module.params['no_copy'] - progress_log = lambda x, y: module.log(y) + def progress_log(dev, report): + module.log(report) module.log('installing package') result = junos.install(package, progress=progress_log, no_copy=no_copy) diff --git a/lib/ansible/modules/network/junos/junos_rpc.py b/lib/ansible/modules/network/junos/junos_rpc.py index 7cfde47500..09930ee667 100644 --- a/lib/ansible/modules/network/junos/junos_rpc.py +++ b/lib/ansible/modules/network/junos/junos_rpc.py @@ -55,6 +55,11 @@ options: version of software that supports native JSON output. required: false default: xml +requirements: + - ncclient (>=v0.5.2) +notes: + - This module requires the netconf system service be enabled on + the remote device being managed """ EXAMPLES = """ @@ -84,8 +89,6 @@ output_lines: returned: always type: list """ -from xml.etree.ElementTree import Element, SubElement, tostring - from ansible.module_utils.junos import junos_argument_spec, check_args from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.netconf import send_request @@ -93,6 +96,11 @@ from ansible.module_utils.six import iteritems USE_PERSISTENT_CONNECTION = True +try: + from lxml.etree import Element, SubElement, tostring +except ImportError: + from xml.etree.ElementTree import Element, SubElement, tostring + def main(): """main entry point for Ansible module diff --git a/lib/ansible/modules/network/junos/junos_system.py b/lib/ansible/modules/network/junos/junos_system.py new file mode 100644 index 0000000000..d29315102b --- /dev/null +++ b/lib/ansible/modules/network/junos/junos_system.py @@ -0,0 +1,195 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'core'} + + +DOCUMENTATION = """ +--- +module: junos_system +version_added: "2.4" +author: "Ganesh Nalawade (@ganeshrn)" +short_description: Manage the system attributes on Juniper JUNOS devices +description: + - This module provides declarative management of node system attributes + on Juniper JUNOS devices. It provides an option to configure host system + parameters or remove those parameters from the device active + configuration. +options: + hostname: + description: + - Configure the device hostname parameter. This option takes an ASCII string value. + domain_name: + description: + - Configure the IP domain name + on the remote device to the provided value. Value + should be in the dotted name form and will be + appended to the C(hostname) to create a fully-qualified + domain name. + domain_search: + description: + - Provides the list of domain suffixes to + append to the hostname for the purpose of doing name resolution. + This argument accepts a list of names and will be reconciled + with the current active configuration on the running node. + name_servers: + description: + - List of DNS name servers by IP address to use to perform name resolution + lookups. This argument accepts either a list of DNS servers See + examples. + state: + description: + - State of the configuration + values in the device's current active configuration. When set + to I(present), the values should be configured in the device active + configuration and when set to I(absent) the values should not be + in the device active configuration + default: present + choices: ['present', 'absent', 'active', 'suspend'] +requirements: + - ncclient (>=v0.5.2) +notes: + - This module requires the netconf system service be enabled on + the remote device being managed +""" + +EXAMPLES = """ +- name: configure hostname and domain name + junos_system: + hostname: junos01 + domain_name: test.example.com + domain-search: + - ansible.com + - redhat.com + - juniper.com + +- name: remove configuration + junos_system: + state: absent + +- name: configure name servers + junos_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 +""" + +RETURN = """ +rpc: + description: load-configuration RPC send to the device + returned: when configuration is changed on device + type: string + sample: > + + + ge-0/0/0 + test interface + + +""" +import collections + +from ansible.module_utils.junos import junos_argument_spec, check_args +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.junos import load_config, map_params_to_obj, map_obj_to_ele + +try: + from lxml.etree import tostring +except ImportError: + from xml.etree.ElementTree import tostring + +USE_PERSISTENT_CONNECTION = True + + +def validate_param_values(module, obj): + for key in obj: + # validate the param value (if validator func exists) + validator = globals().get('validate_%s' % key) + if callable(validator): + validator(module.params.get(key), module) + + +def main(): + """ main entry point for module execution + """ + argument_spec = dict( + hostname=dict(), + domain_name=dict(), + domain_search=dict(type='list'), + name_servers=dict(type='list'), + + state=dict(choices=['present', 'absent', 'active', 'suspend'], default='present') + ) + + argument_spec.update(junos_argument_spec) + + params = ['hostname', 'domain_name', 'domain_search', 'name_servers'] + required_if = [('state', 'present', params, True), + ('state', 'absent', params, True), + ('state', 'active', params, True), + ('state', 'suspend', params, True)] + + module = AnsibleModule(argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True) + + warnings = list() + check_args(module, warnings) + + result = {'changed': False} + + if warnings: + result['warnings'] = warnings + + top = 'system' + + param_to_xpath_map = collections.OrderedDict() + param_to_xpath_map.update({ + 'hostname': {'xpath': 'host-name', 'leaf_only': True}, + 'domain_name': {'xpath': 'domain-name', 'leaf_only': True}, + 'domain_search': {'xpath': 'domain-search', 'leaf_only': True, 'value_req': True}, + 'name_servers': {'xpath': 'name-server/name', 'is_key': True} + }) + + validate_param_values(module, param_to_xpath_map) + + want = list() + want.append(map_params_to_obj(module, param_to_xpath_map)) + ele = map_obj_to_ele(module, want, top) + + kwargs = {'commit': not module.check_mode} + kwargs['action'] = 'replace' + + diff = load_config(module, tostring(ele), warnings, **kwargs) + + if diff: + result.update({ + 'changed': True, + 'diff': {'prepared': diff}, + 'rpc': tostring(ele) + }) + + module.exit_json(**result) + +if __name__ == "__main__": + main() diff --git a/lib/ansible/modules/network/junos/junos_user.py b/lib/ansible/modules/network/junos/junos_user.py index 3738e88e34..e0daf5fb2c 100644 --- a/lib/ansible/modules/network/junos/junos_user.py +++ b/lib/ansible/modules/network/junos/junos_user.py @@ -91,6 +91,11 @@ options: required: false default: present choices: ['present', 'absent'] +requirements: + - ncclient (>=v0.5.2) +notes: + - This module requires the netconf system service be enabled on + the remote device being managed """ EXAMPLES = """ @@ -116,13 +121,16 @@ RETURN = """ """ from functools import partial -from xml.etree.ElementTree import Element, SubElement, tostring - from ansible.module_utils.junos import junos_argument_spec, check_args from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.junos import load_config from ansible.module_utils.six import iteritems +try: + from lxml.etree import Element, SubElement, tostring +except ImportError: + from xml.etree.ElementTree import Element, SubElement, tostring + ROLES = ['operator', 'read-only', 'super-user', 'unauthorized'] USE_PERSISTENT_CONNECTION = True diff --git a/lib/ansible/modules/network/junos/junos_vlan.py b/lib/ansible/modules/network/junos/junos_vlan.py index 0763a24b6a..25beeeb3a7 100644 --- a/lib/ansible/modules/network/junos/junos_vlan.py +++ b/lib/ansible/modules/network/junos/junos_vlan.py @@ -60,6 +60,11 @@ options: - State of the VLAN configuration. default: present choices: ['present', 'absent', 'active', 'suspend'] +requirements: + - ncclient (>=v0.5.2) +notes: + - This module requires the netconf system service be enabled on + the remote device being managed """ EXAMPLES = """ @@ -94,12 +99,15 @@ rpc: """ import collections -from xml.etree.ElementTree import tostring - from ansible.module_utils.junos import junos_argument_spec, check_args from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.junos import load_config, map_params_to_obj, map_obj_to_ele +try: + from lxml.etree import tostring +except ImportError: + from xml.etree.ElementTree import tostring + USE_PERSISTENT_CONNECTION = True @@ -147,7 +155,7 @@ def main(): param_to_xpath_map = collections.OrderedDict() param_to_xpath_map.update({ - 'name': 'name', + 'name': {'xpath': 'name', 'is_key': True}, 'vlan_id': 'vlan-id', 'description': 'description' }) diff --git a/lib/ansible/modules/network/net_interface.py b/lib/ansible/modules/network/net_interface.py index 19e304e9e9..780ddf5719 100644 --- a/lib/ansible/modules/network/net_interface.py +++ b/lib/ansible/modules/network/net_interface.py @@ -112,6 +112,7 @@ commands: sample: - interface 20 - name test-interface + rpc: description: load-configuration RPC send to the device returned: C(rpc) is returned only for junos device @@ -124,5 +125,4 @@ rpc: test interface - """ diff --git a/lib/ansible/modules/network/net_system.py b/lib/ansible/modules/network/net_system.py index 5776f6967c..4534483be8 100644 --- a/lib/ansible/modules/network/net_system.py +++ b/lib/ansible/modules/network/net_system.py @@ -107,4 +107,17 @@ commands: sample: - hostname ios01 - ip domain name test.example.com + +rpc: + description: load-configuration RPC send to the device + returned: C(rpc) is returned only for junos device + when configuration is changed on device + type: string + sample: > + + + ge-0/0/0 + test interface + + """ diff --git a/lib/ansible/modules/network/net_vlan.py b/lib/ansible/modules/network/net_vlan.py index 52217ec631..ea39dcb79c 100644 --- a/lib/ansible/modules/network/net_vlan.py +++ b/lib/ansible/modules/network/net_vlan.py @@ -81,6 +81,7 @@ commands: sample: - vlan 20 - name test-vlan + rpc: description: load-configuration RPC send to the device returned: C(rpc) is returned only for junos device diff --git a/lib/ansible/utils/jsonrpc.py b/lib/ansible/utils/jsonrpc.py index 70c35427a4..5a4843e656 100644 --- a/lib/ansible/utils/jsonrpc.py +++ b/lib/ansible/utils/jsonrpc.py @@ -65,7 +65,6 @@ class Rpc: else: try: result = rpc_method(*args, **kwargs) - display.display(" -- result -- %s" % result, log_only=True) except Exception as exc: display.display(traceback.format_exc(), log_only=True) error = self.internal_error(data=to_text(exc, errors='surrogate_then_replace')) @@ -78,7 +77,6 @@ class Rpc: response = json.dumps(response) - display.display(" -- response -- %s" % response, log_only=True) delattr(self, '_identifier') return response diff --git a/test/integration/junos.yaml b/test/integration/junos.yaml index 81ae5c5cda..e0a8509e0b 100644 --- a/test/integration/junos.yaml +++ b/test/integration/junos.yaml @@ -17,3 +17,4 @@ - { role: junos_vlan, when: "limit_to in ['*', 'junos_vlan']" } - { role: junos_interface, when: "limit_to in ['*', 'junos_interface']" } - { role: junos_banner, when: "limit_to in ['*', 'junos_banner']" } + - { role: junos_system, when: "limit_to in ['*', 'junos_system']" } diff --git a/test/integration/targets/junos_system/aliases b/test/integration/targets/junos_system/aliases new file mode 100644 index 0000000000..93151a8d9d --- /dev/null +++ b/test/integration/targets/junos_system/aliases @@ -0,0 +1 @@ +network/ci diff --git a/test/integration/targets/junos_system/defaults/main.yaml b/test/integration/targets/junos_system/defaults/main.yaml new file mode 100644 index 0000000000..5f709c5aac --- /dev/null +++ b/test/integration/targets/junos_system/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/junos_system/tasks/main.yaml b/test/integration/targets/junos_system/tasks/main.yaml new file mode 100644 index 0000000000..cc27f174fd --- /dev/null +++ b/test/integration/targets/junos_system/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: netconf.yaml, tags: ['netconf'] } diff --git a/test/integration/targets/junos_system/tasks/netconf.yaml b/test/integration/targets/junos_system/tasks/netconf.yaml new file mode 100644 index 0000000000..1286b35422 --- /dev/null +++ b/test/integration/targets/junos_system/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/junos_system/tests/netconf/basic.yaml b/test/integration/targets/junos_system/tests/netconf/basic.yaml new file mode 100644 index 0000000000..090a76467c --- /dev/null +++ b/test/integration/targets/junos_system/tests/netconf/basic.yaml @@ -0,0 +1,308 @@ +--- +- debug: msg="START junos_system netconf/basic.yaml" + +- name: setup - remove hostname + junos_system: + hostname: vsrx01 + state: absent + provider: "{{ netconf }}" + +- name: Set hostname + junos_system: + hostname: vsrx01 + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'vsrx01' in result.rpc" + +- name: Set hostname (idempotent) + junos_system: + hostname: vsrx01 + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate hostname configuration + junos_system: + hostname: vsrx01 + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Activate hostname configuration + junos_system: + hostname: vsrx01 + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Delete hostname configuration + junos_system: + hostname: vsrx01 + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Teardown - set hostname + junos_system: + hostname: vsrx01 + state: present + provider: "{{ netconf }}" + +- name: setup - remove domain name + junos_system: + domain_name: ansible.com + state: absent + provider: "{{ netconf }}" + +- name: Set domain name + junos_system: + domain_name: ansible.com + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'ansible.com' in result.rpc" + +- name: Set domain name (idempotent) + junos_system: + domain_name: ansible.com + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate domain name + junos_system: + domain_name: ansible.com + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Activate domain name + junos_system: + domain_name: ansible.com + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Delete domain name + junos_system: + domain_name: ansible.com + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Teardown - set domain name + junos_system: + domain_name: ansible.com + state: present + provider: "{{ netconf }}" + +- name: Setup - delete domain search + junos_system: + domain_search: + - test.com + - sample.com + state: absent + provider: "{{ netconf }}" + register: result + +- name: Set domain search + junos_system: + domain_search: + - test.com + - sample.com + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'test.com' in result.rpc" + - "'sample.com' in result.rpc" + +- name: Set domain search + junos_system: + domain_search: + - test.com + - sample.com + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate domain search + junos_system: + domain_search: + - test.com + - sample.com + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'test.com' in result.rpc" + - "'sample.com' in result.rpc" + +- name: Activate domain search + junos_system: + domain_search: + - test.com + - sample.com + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'test.com' in result.rpc" + - "'sample.com' in result.rpc" + +- name: Delete domain search + junos_system: + domain_search: + - test.com + - sample.com + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'test.com' in result.rpc" + - "'sample.com' in result.rpc" + +- name: Setup - delete name servers + junos_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: absent + provider: "{{ netconf }}" + register: result + +- name: Set name servers + junos_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'8.8.8.8' in result.rpc" + - "'8.8.4.4' in result.rpc" + +- name: Set name servers (idempotent) + junos_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate name servers + junos_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'8.8.8.8' in result.rpc" + - "'8.8.4.4' in result.rpc" + +- name: Activate name servers + junos_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'8.8.8.8' in result.rpc" + - "'8.8.4.4' in result.rpc" + +- name: Delete name servers + junos_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'8.8.8.8' in result.rpc" + - "'8.8.4.4' in result.rpc" diff --git a/test/integration/targets/net_system/tasks/main.yaml b/test/integration/targets/net_system/tasks/main.yaml index 415c99d8b1..af08869c92 100644 --- a/test/integration/targets/net_system/tasks/main.yaml +++ b/test/integration/targets/net_system/tasks/main.yaml @@ -1,2 +1,3 @@ --- - { include: cli.yaml, tags: ['cli'] } +- { include: netconf.yaml, tags: ['netconf'] } diff --git a/test/integration/targets/net_system/tasks/netconf.yaml b/test/integration/targets/net_system/tasks/netconf.yaml new file mode 100644 index 0000000000..1286b35422 --- /dev/null +++ b/test/integration/targets/net_system/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/net_system/tests/junos/basic.yaml b/test/integration/targets/net_system/tests/junos/basic.yaml new file mode 100644 index 0000000000..0b0c1bec08 --- /dev/null +++ b/test/integration/targets/net_system/tests/junos/basic.yaml @@ -0,0 +1,308 @@ +--- +- debug: msg="START net_system junos/basic.yaml" + +- name: setup - remove hostname + net_system: + hostname: vsrx01 + state: absent + provider: "{{ netconf }}" + +- name: Set hostname + net_system: + hostname: vsrx01 + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'vsrx01' in result.rpc" + +- name: Set hostname (idempotent) + net_system: + hostname: vsrx01 + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate hostname configuration + net_system: + hostname: vsrx01 + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Activate hostname configuration + net_system: + hostname: vsrx01 + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Delete hostname configuration + net_system: + hostname: vsrx01 + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Teardown - set hostname + net_system: + hostname: vsrx01 + state: present + provider: "{{ netconf }}" + +- name: setup - remove domain name + net_system: + domain_name: ansible.com + state: absent + provider: "{{ netconf }}" + +- name: Set domain name + net_system: + domain_name: ansible.com + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'ansible.com' in result.rpc" + +- name: Set domain name (idempotent) + net_system: + domain_name: ansible.com + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate domain name + net_system: + domain_name: ansible.com + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Activate domain name + net_system: + domain_name: ansible.com + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Delete domain name + net_system: + domain_name: ansible.com + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Teardown - set domain name + net_system: + domain_name: ansible.com + state: present + provider: "{{ netconf }}" + +- name: Setup - delete domain search + net_system: + domain_search: + - test.com + - sample.com + state: absent + provider: "{{ netconf }}" + register: result + +- name: Set domain search + net_system: + domain_search: + - test.com + - sample.com + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'test.com' in result.rpc" + - "'sample.com' in result.rpc" + +- name: Set domain search + net_system: + domain_search: + - test.com + - sample.com + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate domain search + net_system: + domain_search: + - test.com + - sample.com + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'test.com' in result.rpc" + - "'sample.com' in result.rpc" + +- name: Activate domain search + net_system: + domain_search: + - test.com + - sample.com + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'test.com' in result.rpc" + - "'sample.com' in result.rpc" + +- name: Delete domain search + net_system: + domain_search: + - test.com + - sample.com + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'test.com' in result.rpc" + - "'sample.com' in result.rpc" + +- name: Setup - delete name servers + net_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: absent + provider: "{{ netconf }}" + register: result + +- name: Set name servers + net_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'8.8.8.8' in result.rpc" + - "'8.8.4.4' in result.rpc" + +- name: Set name servers (idempotent) + net_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate name servers + net_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'8.8.8.8' in result.rpc" + - "'8.8.4.4' in result.rpc" + +- name: Activate name servers + net_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'8.8.8.8' in result.rpc" + - "'8.8.4.4' in result.rpc" + +- name: Delete name servers + net_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'8.8.8.8' in result.rpc" + - "'8.8.4.4' in result.rpc" diff --git a/test/integration/targets/net_system/tests/netconf/basic.yaml b/test/integration/targets/net_system/tests/netconf/basic.yaml new file mode 100644 index 0000000000..5ff7cf5af8 --- /dev/null +++ b/test/integration/targets/net_system/tests/netconf/basic.yaml @@ -0,0 +1,3 @@ +--- +- include: "{{ role_path }}/tests/junos/basic.yaml" + when: hostvars[inventory_hostname]['ansible_network_os'] == 'junos' diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt index da46e7a65e..000dc66dd6 100644 --- a/test/sanity/pep8/legacy-files.txt +++ b/test/sanity/pep8/legacy-files.txt @@ -340,11 +340,6 @@ lib/ansible/modules/network/iosxr/iosxr_facts.py lib/ansible/modules/network/iosxr/iosxr_system.py lib/ansible/modules/net_tools/ipify_facts.py lib/ansible/modules/net_tools/ipinfoio_facts.py -lib/ansible/modules/network/junos/_junos_template.py -lib/ansible/modules/network/junos/junos_command.py -lib/ansible/modules/network/junos/junos_config.py -lib/ansible/modules/network/junos/junos_netconf.py -lib/ansible/modules/network/junos/junos_package.py lib/ansible/modules/network/lenovo/cnos_conditional_template.py lib/ansible/modules/network/lenovo/cnos_template.py lib/ansible/modules/network/lenovo/cnos_vlan.py