From 94043849855d4c4f573c4844aa7ac3e797b387d7 Mon Sep 17 00:00:00 2001 From: Daniel Mellado Date: Wed, 4 Dec 2019 12:59:56 +0100 Subject: [PATCH] Add junos_static_routes module (#65239) This commit adds a new network resource module for static routes on junos devices. Signed-off-by: Daniel Mellado --- .../junos/argspec/static_routes/__init__.py | 0 .../argspec/static_routes/static_routes.py | 73 +++++ .../junos/config/static_routes/__init__.py | 0 .../config/static_routes/static_routes.py | 235 +++++++++++++++ .../module_utils/network/junos/facts/facts.py | 2 + .../junos/facts/static_routes/__init__.py | 0 .../facts/static_routes/static_routes.py | 152 ++++++++++ ...static_route.py => _junos_static_route.py} | 6 +- .../network/junos/junos_static_routes.py | 282 ++++++++++++++++++ .../network-integration.requirements.txt | 1 + .../junos_static_routes/defaults/main.yaml | 3 + .../junos_static_routes/meta/main.yaml | 2 + .../junos_static_routes/tasks/main.yaml | 2 + .../junos_static_routes/tasks/netconf.yaml | 17 ++ .../tests/netconf/_base_config.yaml | 21 ++ .../tests/netconf/_remove_config.yaml | 14 + .../tests/netconf/deleted.yaml | 37 +++ .../tests/netconf/merged.yaml | 72 +++++ .../tests/netconf/overridden.yaml | 51 ++++ .../tests/netconf/replaced.yaml | 54 ++++ test/sanity/ignore.txt | 14 +- 21 files changed, 1030 insertions(+), 8 deletions(-) create mode 100644 lib/ansible/module_utils/network/junos/argspec/static_routes/__init__.py create mode 100644 lib/ansible/module_utils/network/junos/argspec/static_routes/static_routes.py create mode 100644 lib/ansible/module_utils/network/junos/config/static_routes/__init__.py create mode 100644 lib/ansible/module_utils/network/junos/config/static_routes/static_routes.py create mode 100644 lib/ansible/module_utils/network/junos/facts/static_routes/__init__.py create mode 100644 lib/ansible/module_utils/network/junos/facts/static_routes/static_routes.py rename lib/ansible/modules/network/junos/{junos_static_route.py => _junos_static_route.py} (97%) create mode 100644 lib/ansible/modules/network/junos/junos_static_routes.py create mode 100644 test/integration/targets/junos_static_routes/defaults/main.yaml create mode 100644 test/integration/targets/junos_static_routes/meta/main.yaml create mode 100644 test/integration/targets/junos_static_routes/tasks/main.yaml create mode 100644 test/integration/targets/junos_static_routes/tasks/netconf.yaml create mode 100644 test/integration/targets/junos_static_routes/tests/netconf/_base_config.yaml create mode 100644 test/integration/targets/junos_static_routes/tests/netconf/_remove_config.yaml create mode 100644 test/integration/targets/junos_static_routes/tests/netconf/deleted.yaml create mode 100644 test/integration/targets/junos_static_routes/tests/netconf/merged.yaml create mode 100644 test/integration/targets/junos_static_routes/tests/netconf/overridden.yaml create mode 100644 test/integration/targets/junos_static_routes/tests/netconf/replaced.yaml diff --git a/lib/ansible/module_utils/network/junos/argspec/static_routes/__init__.py b/lib/ansible/module_utils/network/junos/argspec/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/junos/argspec/static_routes/static_routes.py b/lib/ansible/module_utils/network/junos/argspec/static_routes/static_routes.py new file mode 100644 index 00000000000..f4179024e5f --- /dev/null +++ b/lib/ansible/module_utils/network/junos/argspec/static_routes/static_routes.py @@ -0,0 +1,73 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The arg spec for the junos_static_routes module +""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class Static_routesArgs(object): # pylint: disable=R0903 + """The arg spec for the junos_static_routes module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'address_families': { + 'elements': 'dict', + 'options': { + 'afi': { + 'choices': ['ipv4', 'ipv6'], + 'required': True, + 'type': 'str'}, + 'routes': {'elements': 'dict', + 'options': { + 'dest': { + 'type': 'str'}, + 'metric': { + 'type': 'int'}, + 'next_hop': { + 'elements': 'dict', + 'options': { + 'forward_router_address': { + 'type': 'str'}}, + 'type': 'list'}}, + 'type': 'list'}}, + 'type': 'list'}, + 'vrf': { + 'type': 'str'}}, + 'type': 'list'}, + 'state': { + 'choices': ['merged', + 'replaced', + 'overridden', + 'deleted'], + 'default': 'merged', + 'type': 'str'}} # pylint: disable=C0301 diff --git a/lib/ansible/module_utils/network/junos/config/static_routes/__init__.py b/lib/ansible/module_utils/network/junos/config/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/junos/config/static_routes/static_routes.py b/lib/ansible/module_utils/network/junos/config/static_routes/static_routes.py new file mode 100644 index 00000000000..07cce2ff63c --- /dev/null +++ b/lib/ansible/module_utils/network/junos/config/static_routes/static_routes.py @@ -0,0 +1,235 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The junos_static_routes class +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to it's desired end-state is +created +""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils.network.common.cfg.base import ConfigBase +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.network.junos.facts.facts import Facts +from ansible.module_utils.network.junos.junos import (locked_config, + load_config, + commit_configuration, + discard_changes, + tostring) +from ansible.module_utils.network.common.netconf import (build_root_xml_node, + build_child_xml_node) + + +class Static_routes(ConfigBase): + """ + The junos_static_routes class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'static_routes', + ] + + def __init__(self, module): + super(Static_routes, self).__init__(module) + + def get_static_routes_facts(self): + """ Get the 'facts' (the current configuration) + + :rtype: A dictionary + :returns: The current configuration as a dictionary + """ + facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources) + static_routes_facts = facts['ansible_network_resources'].get('static_routes') + if not static_routes_facts: + return [] + return static_routes_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + + existing_static_routes_facts = self.get_static_routes_facts() + config_xmls = self.set_config(existing_static_routes_facts) + + with locked_config(self._module): + for config_xml in to_list(config_xmls): + diff = load_config(self._module, config_xml, []) + + commit = not self._module.check_mode + if diff: + if commit: + commit_configuration(self._module) + else: + discard_changes(self._module) + result['changed'] = True + + if self._module._diff: + result['diff'] = {'prepared': diff} + + result['xml'] = config_xmls + changed_static_routes_facts = self.get_static_routes_facts() + + result['before'] = existing_static_routes_facts + if result['changed']: + result['after'] = changed_static_routes_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_static_routes_facts): + """ Collect the configuration from the args passed to the module, + collect the current configuration (as a dict from facts) + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + want = self._module.params['config'] + have = existing_static_routes_facts + resp = self.set_state(want, have) + return to_list(resp) + + def set_state(self, want, have): + """ Select the appropriate function based on the state provided + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + state = self._module.params['state'] + root = build_root_xml_node('configuration') + routing_options = build_child_xml_node(root, 'routing-options') + routing_instances = build_child_xml_node(root, 'routing-instances') + if state == 'overridden': + config_xmls = self._state_overridden(want, have) + elif state == 'deleted': + config_xmls = self._state_deleted(want, have) + elif state == 'merged': + config_xmls = self._state_merged(want, have) + elif state == 'replaced': + config_xmls = self._state_replaced(want, have) + + for xml in config_xmls: + if xml['root_type'] == 'routing-options': + routing_options.append(xml['static_route_xml']) + elif xml['root_type'] == 'routing-instances': + routing_instances.append(xml['static_route_xml']) + + return [tostring(xml) for xml in root.getchildren()] + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the xml necessary to migrate the current configuration + to the desired configuration + """ + static_route_xml = [] + static_route_xml.extend(self._state_deleted(want, have)) + static_route_xml.extend(self._state_merged(want, have)) + return static_route_xml + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :rtype: A list + :returns: the xml necessary to migrate the current configuration + to the desired configuration + """ + static_route_xml = [] + static_route_xml.extend(self._state_deleted(have, have)) + static_route_xml.extend(self._state_merged(want, have)) + return static_route_xml + + def _state_deleted(self, want, have): + """ The command generator when state is deleted + + :rtype: A list + :returns: the xml necessary to migrate the current configuration + to the desired configuration + """ + if not want: + want = have + static_route_xml = self._state_merged(want, have, delete={'delete': 'delete'}) + return static_route_xml + + def _state_merged(self, want, have, delete=None): + """ The command generator when state is merged + + :rtype: A list + :returns: the xml necessary to migrate the current configuration + to the desired configuration + """ + static_route_xml = [] + root_type = "" + vrf_name = None + for config in want: + if config.get('vrf'): + vrf_name = config['vrf'] + if vrf_name: + root_type = 'routing-instances' + instance = build_root_xml_node('instance') + build_child_xml_node(instance, 'name', vrf_name) + routing_options = build_child_xml_node(instance, 'routing-options') + else: + root_type = 'routing-options' + + for afi in config['address_families']: + protocol = afi['afi'] + if protocol == 'ipv6': + if vrf_name: + rib_route_root = build_child_xml_node(routing_options, 'rib') + build_child_xml_node(rib_route_root, 'name', vrf_name + '.inet6.0') + else: + rib_route_root = build_root_xml_node('rib') + build_child_xml_node(rib_route_root, 'name', 'inet6.0') + static_route_root = build_child_xml_node(rib_route_root, 'static') + elif protocol == 'ipv4': + if vrf_name: + static_route_root = build_child_xml_node(routing_options, 'static') + else: + static_route_root = build_root_xml_node('static') + + if afi.get('routes'): + for route in afi['routes']: + route_node = build_child_xml_node(static_route_root, 'route') + if delete: + route_node.attrib.update(delete) + if route.get('dest'): + build_child_xml_node(route_node, 'name', route['dest']) + if not delete: + if route.get('metric'): + build_child_xml_node(route_node, 'metric', route['metric']) + if route.get('next_hop'): + for hop in route['next_hop']: + build_child_xml_node(route_node, 'next-hop', hop['forward_router_address']) + elif delete: + if vrf_name: + instance.attrib.update(delete) + static_route_root.attrib.update(delete) + + if vrf_name: + static_route_xml.append({'root_type': root_type, 'static_route_xml': instance}) + else: + if protocol == 'ipv6': + static_route_xml.append({'root_type': root_type, 'static_route_xml': rib_route_root}) + else: + static_route_xml.append({'root_type': root_type, 'static_route_xml': static_route_root}) + return static_route_xml diff --git a/lib/ansible/module_utils/network/junos/facts/facts.py b/lib/ansible/module_utils/network/junos/facts/facts.py index 4c9417e61f4..28aa11cf32f 100644 --- a/lib/ansible/module_utils/network/junos/facts/facts.py +++ b/lib/ansible/module_utils/network/junos/facts/facts.py @@ -21,6 +21,7 @@ from ansible.module_utils.network.junos.facts.lldp_global.lldp_global import Lld from ansible.module_utils.network.junos.facts.lldp_interfaces.lldp_interfaces import Lldp_interfacesFacts from ansible.module_utils.network.junos.facts.vlans.vlans import VlansFacts from ansible.module_utils.network.junos.facts.l2_interfaces.l2_interfaces import L2_interfacesFacts +from ansible.module_utils.network.junos.facts.static_routes.static_routes import Static_routesFacts FACT_LEGACY_SUBSETS = dict( default=Default, @@ -38,6 +39,7 @@ FACT_RESOURCE_SUBSETS = dict( lldp_global=Lldp_globalFacts, lldp_interfaces=Lldp_interfacesFacts, vlans=VlansFacts, + static_routes=Static_routesFacts ) diff --git a/lib/ansible/module_utils/network/junos/facts/static_routes/__init__.py b/lib/ansible/module_utils/network/junos/facts/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/junos/facts/static_routes/static_routes.py b/lib/ansible/module_utils/network/junos/facts/static_routes/static_routes.py new file mode 100644 index 00000000000..9f8019a7162 --- /dev/null +++ b/lib/ansible/module_utils/network/junos/facts/static_routes/static_routes.py @@ -0,0 +1,152 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The junos static_routes fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from copy import deepcopy + +from ansible.module_utils._text import to_bytes +from ansible.module_utils.network.common import utils +from ansible.module_utils.network.junos.argspec.static_routes.static_routes import Static_routesArgs +from ansible.module_utils.six import string_types +try: + from lxml import etree + HAS_LXML = True +except ImportError: + HAS_LXML = False +try: + import xmltodict + HAS_XMLTODICT = True +except ImportError: + HAS_XMLTODICT = False + + +class Static_routesFacts(object): + """ The junos static_routes fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Static_routesArgs.argument_spec + spec = deepcopy(self.argument_spec) + if subspec: + if options: + facts_argument_spec = spec[subspec][options] + else: + facts_argument_spec = spec[subspec] + else: + facts_argument_spec = spec + + self.generated_spec = utils.generate_dict(facts_argument_spec) + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for static_routes + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not HAS_LXML: + self._module.fail_json(msg='lxml is not installed.') + if not HAS_XMLTODICT: + self._module.fail_json(msg='xmltodict is not installed.') + + if not data: + config_filter = """ + + + + + """ + data = connection.get_configuration(filter=config_filter) + + if isinstance(data, string_types): + data = etree.fromstring(to_bytes(data, + errors='surrogate_then_replace')) + + resources = data.xpath('configuration/routing-options') + vrf_resources = data.xpath('configuration/routing-instances') + resources.extend(vrf_resources) + + objs = [] + for resource in resources: + if resource is not None: + xml = self._get_xml_dict(resource) + obj = self.render_config(self.generated_spec, xml) + if obj: + objs.append(obj) + + facts = {} + if objs: + facts['static_routes'] = [] + params = utils.validate_config(self.argument_spec, + {'config': objs}) + for cfg in params['config']: + facts['static_routes'].append(utils.remove_empties(cfg)) + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def _get_xml_dict(self, xml_root): + xml_dict = xmltodict.parse(etree.tostring(xml_root), dict_constructor=dict) + return xml_dict + + def _create_route_dict(self, afi, route_path): + routes_dict = {'afi': afi, + 'routes': []} + if isinstance(route_path, dict): + route_path = [route_path] + for route in route_path: + route_dict = {} + route_dict['dest'] = route['name'] + if route.get('metric'): + route_dict['metric'] = route['metric']['metric-value'] + route_dict['next_hop'] = [] + route_dict['next_hop'].append({'forward_router_address': route['next-hop']}) + routes_dict['routes'].append(route_dict) + return routes_dict + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = deepcopy(spec) + routes = [] + config['address_families'] = [] + + if conf.get('routing-options'): + if conf['routing-options'].get('rib'): + if conf['routing-options'].get('rib').get('name') == 'inet6.0': + if conf['routing-options'].get('rib').get('static'): + route_path = conf['routing-options']['rib']['static'].get('route') + routes.append(self._create_route_dict('ipv6', route_path)) + if conf['routing-options'].get('static'): + route_path = conf['routing-options']['static'].get('route') + routes.append(self._create_route_dict('ipv4', route_path)) + + if conf.get('routing-instances'): + config['vrf'] = conf['routing-instances']['instance']['name'] + if conf['routing-instances'].get('instance').get('routing-options').get('rib').get('name') == config['vrf'] + '.inet6.0': + if conf['routing-instances']['instance']['routing-options']['rib'].get('static'): + route_path = conf['routing-instances']['instance']['routing-options']['rib']['static'].get('route') + routes.append(self._create_route_dict('ipv6', route_path)) + if conf['routing-instances'].get('instance').get('routing-options').get('static'): + route_path = conf['routing-instances']['instance']['routing-options']['static'].get('route') + routes.append(self._create_route_dict('ipv4', route_path)) + config['address_families'].extend(routes) + return utils.remove_empties(config) diff --git a/lib/ansible/modules/network/junos/junos_static_route.py b/lib/ansible/modules/network/junos/_junos_static_route.py similarity index 97% rename from lib/ansible/modules/network/junos/junos_static_route.py rename to lib/ansible/modules/network/junos/_junos_static_route.py index 59516a08d25..d12832b96b0 100644 --- a/lib/ansible/modules/network/junos/junos_static_route.py +++ b/lib/ansible/modules/network/junos/_junos_static_route.py @@ -9,7 +9,7 @@ __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], + 'status': ['deprecated'], 'supported_by': 'network'} @@ -22,6 +22,10 @@ short_description: Manage static IP routes on Juniper JUNOS network devices description: - This module provides declarative management of static IP routes on Juniper JUNOS network devices. +deprecated: + removed_in: "2.13" + why: Updated modules released with more functionality + alternative: Use M(junos_static_routes) instead. options: address: description: diff --git a/lib/ansible/modules/network/junos/junos_static_routes.py b/lib/ansible/modules/network/junos/junos_static_routes.py new file mode 100644 index 00000000000..c7971bccf5c --- /dev/null +++ b/lib/ansible/modules/network/junos/junos_static_routes.py @@ -0,0 +1,282 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for junos_static_routes +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network' +} + +DOCUMENTATION = """ +--- +module: junos_static_routes +version_added: '2.10' +short_description: Manage static routes on Juniper JUNOS devices +description: This module provides declarative management of static routes on Juniper JUNOS devices +author: Daniel Mellado (@dmellado) +requirements: + - ncclient (>=v0.6.4) + - xmltodict (>=0.12) +notes: + - This module requires the netconf system service be enabled on the device being managed. + - This module works with connection C(netconf). See L(the Junos OS Platform Options,../network/user_guide/platform_junos.html). + - Tested against JunOS v18.4R1 +options: + config: + description: A dictionary of static routes options + type: list + elements: dict + suboptions: + vrf: + description: + - Virtual Routing and Forwarding (VRF) name + type: str + address_families: + description: + - Address family to use for the static routes + elements: dict + type: list + suboptions: + afi: + description: + - afi to use for the static routes + type: str + required: true + choices: + - ipv4 + - ipv6 + routes: + description: + - Static route configuration + elements: dict + type: list + suboptions: + dest: + description: + - Static route destination including prefix + type: str + next_hop: + elements: dict + type: list + description: + - Next hop to destination + suboptions: + forward_router_address: + description: + - List of next hops + type: str + metric: + description: + - Metric value for the static route + type: int + state: + description: + - The state the configuration should be left in + type: str + choices: + - merged + - replaced + - overridden + - deleted + default: merged +""" + +EXAMPLES = """ +--- +# Using deleted + +# Before state +# ------------ +# +# admin# show routing-options +# static { +# route 192.168.47.0/24 next-hop 172.16.1.2; +# route 192.168.16.0/24 next-hop 172.16.1.2; +# route 10.200.16.75/24 next-hop 10.200.16.2; +# } + +- name: Delete provided configuration (default operation is merge) + junos_static_routes: + config: + - address_families: + - afi: 'ipv4' + routes: + - dest: 10.200.16.75/24 + next_hop: + - forward_router_address: 10.200.16.2 + state: deleted + +# After state: +# ------------ +# +# admin# show routing-options +# static { +# route 192.168.47.0/24 next-hop 172.16.1.2; +# route 192.168.16.0/24 next-hop 172.16.1.2; +# } + +# Using merged + +# Before state +# ------------ +# +# admin# show routing-options +# static { +# route 192.168.47.0/24 next-hop 172.16.1.2; +# route 192.168.16.0/24 next-hop 172.16.1.2; +# } + +- name: Merge provided configuration with device configuration (default operation is merge) + junos_static_routes: + config: + - address_families: + - afi: 'ipv4' + routes: + - dest: 10.200.16.75/24 + next_hop: + - forward_router_address: 10.200.16.2 + state: merged + +# After state: +# ------------ +# +# admin# show routing-options +# static { +# route 192.168.47.0/24 next-hop 172.16.1.2; +# route 192.168.16.0/24 next-hop 172.16.1.2; +# route 10.200.16.75/24 next-hop 10.200.16.2; +# } + +# Using overridden + +# Before state +# ------------ +# +# admin# show routing-options +# static { +# route 192.168.47.0/24 next-hop 172.16.1.2; +# route 192.168.16.0/24 next-hop 172.16.0.1; +# } + +- name: Override provided configuration with device configuration (default operation is merge) + junos_static_routes: + config: + - address_families: + - afi: 'ipv4' + routes: + - dest: 10.200.16.75/24 + next_hop: + - forward_router_address: 10.200.16.2 + state: overridden + +# After state: +# ------------ +# +# admin# show routing-options +# static { +# route 10.200.16.75/24 next-hop 10.200.16.2; +# } + +# Using replaced + +# Before state +# ------------ +# +# admin# show routing-options +# static { +# route 192.168.47.0/24 next-hop 172.16.1.2; +# route 192.168.16.0/24 next-hop 172.16.1.2; +# } + +- name: Replace provided configuration with device configuration (default operation is merge) + junos_static_routes: + config: + - address_families: + - afi: 'ipv4' + routes: + - dest: 192.168.47.0/24 + next_hop: + - forward_router_address: 10.200.16.2 + state: replaced + +# After state: +# ------------ +# +# admin# show routing-options +# static { +# route 192.168.47.0/24 next-hop 10.200.16.2; +# route 192.168.16.0/24 next-hop 172.16.1.2; +# } + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: str + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: str + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['command 1', 'command 2', 'command 3'] +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.junos.argspec.static_routes.static_routes import Static_routesArgs +from ansible.module_utils.network.junos.config.static_routes.static_routes import Static_routes + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Static_routesArgs.argument_spec, + supports_check_mode=True) + + result = Static_routes(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/network-integration.requirements.txt b/test/integration/network-integration.requirements.txt index e08e0d81bd9..e8c81df12a6 100644 --- a/test/integration/network-integration.requirements.txt +++ b/test/integration/network-integration.requirements.txt @@ -3,3 +3,4 @@ scp # for Cisco ios selectors2 # for ncclient ncclient # for Junos jxmlease # for Junos +xmltodict # for Junos diff --git a/test/integration/targets/junos_static_routes/defaults/main.yaml b/test/integration/targets/junos_static_routes/defaults/main.yaml new file mode 100644 index 00000000000..164afead284 --- /dev/null +++ b/test/integration/targets/junos_static_routes/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/junos_static_routes/meta/main.yaml b/test/integration/targets/junos_static_routes/meta/main.yaml new file mode 100644 index 00000000000..874017d64b1 --- /dev/null +++ b/test/integration/targets/junos_static_routes/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: +# - prepare_junos_tests diff --git a/test/integration/targets/junos_static_routes/tasks/main.yaml b/test/integration/targets/junos_static_routes/tasks/main.yaml new file mode 100644 index 00000000000..cc27f174fd8 --- /dev/null +++ b/test/integration/targets/junos_static_routes/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: netconf.yaml, tags: ['netconf'] } diff --git a/test/integration/targets/junos_static_routes/tasks/netconf.yaml b/test/integration/targets/junos_static_routes/tasks/netconf.yaml new file mode 100644 index 00000000000..73b91adfaa2 --- /dev/null +++ b/test/integration/targets/junos_static_routes/tasks/netconf.yaml @@ -0,0 +1,17 @@ +--- +- name: collect all netconf test cases + find: + paths: "{{ role_path }}/tests/netconf" + patterns: "{{ testcase }}.yaml" + use_regex: true + connection: local + register: test_cases + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test case (connection=netconf) + include: "{{ test_case_to_run }} ansible_connection=netconf" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/junos_static_routes/tests/netconf/_base_config.yaml b/test/integration/targets/junos_static_routes/tests/netconf/_base_config.yaml new file mode 100644 index 00000000000..83b7e3a7534 --- /dev/null +++ b/test/integration/targets/junos_static_routes/tests/netconf/_base_config.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: "Start junos_static_routes base config ansible_connection={{ ansible_connection }}" + +- name: Configure base static_routes + junos_static_routes: + config: + - address_families: + - afi: 'ipv4' + routes: + - dest: 192.168.0.0/24 + next_hop: + - forward_router_address: 192.168.0.1 + - afi: 'ipv6' + routes: + - dest: 2001:db8::5/128 + next_hop: + - forward_router_address: 2001:db8:0:1:2a0:a502:0:19da + +- debug: + msg: "End junos_static_routes base config ansible_connection={{ ansible_connection }}" diff --git a/test/integration/targets/junos_static_routes/tests/netconf/_remove_config.yaml b/test/integration/targets/junos_static_routes/tests/netconf/_remove_config.yaml new file mode 100644 index 00000000000..20d76a9420f --- /dev/null +++ b/test/integration/targets/junos_static_routes/tests/netconf/_remove_config.yaml @@ -0,0 +1,14 @@ +--- +- debug: + msg: "Start junos_static_routes teardown ansible_connection={{ ansible_connection }}" + +- name: Remove static route config + junos_static_routes: + config: + - address_families: + - afi: 'ipv4' + - afi: 'ipv6' + state: deleted + +- debug: + msg: "End junos_static_routes teardown ansible_connection={{ ansible_connection }}" diff --git a/test/integration/targets/junos_static_routes/tests/netconf/deleted.yaml b/test/integration/targets/junos_static_routes/tests/netconf/deleted.yaml new file mode 100644 index 00000000000..78a9431b690 --- /dev/null +++ b/test/integration/targets/junos_static_routes/tests/netconf/deleted.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: "START junos_static_routes deleted integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml +- include_tasks: _base_config.yaml + +- block: + - name: Delete the provided configuration with the exisiting running configuration + junos_static_routes: &deleted + config: + - address_families: + - afi: 'ipv4' + - afi: 'ipv6' + state: deleted + register: result + + - name: Assert the configuration is reflected on host + assert: + that: + - not result.after + debugger: on_failed + + - name: Delete the provided configuration with the existing running configuration (IDEMPOTENT) + junos_static_routes: *deleted + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + + always: + - include_tasks: _remove_config.yaml + +- debug: + msg: "END junos_static_routes deleted integration tests on connection={{ ansible_connection }}" diff --git a/test/integration/targets/junos_static_routes/tests/netconf/merged.yaml b/test/integration/targets/junos_static_routes/tests/netconf/merged.yaml new file mode 100644 index 00000000000..5630266b512 --- /dev/null +++ b/test/integration/targets/junos_static_routes/tests/netconf/merged.yaml @@ -0,0 +1,72 @@ +--- +- debug: + msg: "START junos_static_routes merged integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- set_fact: + expected_merged_output: + - address_families: + - afi: 'ipv6' + routes: + - dest: 2001:db8::5/128 + next_hop: + - forward_router_address: 2001:db8:0:1:2a0:a502:0:19da + - dest: ::/0 + next_hop: + - forward_router_address: 2001:db8:0:1:2a0:a502:0:19da + - afi: 'ipv4' + routes: + - dest: 192.168.0.0/24 + next_hop: + - forward_router_address: 192.168.0.1 + - dest: 192.168.1.0/24 + metric: 2 + next_hop: + - forward_router_address: 192.168.1.1 + +- block: + - name: Merge the provided configuration with the exisiting running configuration + junos_static_routes: &merged + config: + - address_families: + - afi: 'ipv4' + routes: + - dest: 192.168.0.0/24 + next_hop: + - forward_router_address: 192.168.0.1 + - dest: 192.168.1.0/24 + next_hop: + - forward_router_address: 192.168.1.1 + metric: 2 + - afi: 'ipv6' + routes: + - dest: 2001:db8::5/128 + next_hop: + - forward_router_address: 2001:db8:0:1:2a0:a502:0:19da + - dest: ::/0 + next_hop: + - forward_router_address: 2001:db8:0:1:2a0:a502:0:19da + state: merged + register: result + + - name: Assert the configuration is reflected on host + assert: + that: + - "{{ expected_merged_output | symmetric_difference(result['after']) |length == 0 }}" + debugger: on_failed + + - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT) + junos_static_routes: *merged + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + + always: + - include_tasks: _remove_config.yaml + +- debug: + msg: "END junos_static_routes merged integration tests on connection={{ ansible_connection }}" diff --git a/test/integration/targets/junos_static_routes/tests/netconf/overridden.yaml b/test/integration/targets/junos_static_routes/tests/netconf/overridden.yaml new file mode 100644 index 00000000000..6f5ea6b32d9 --- /dev/null +++ b/test/integration/targets/junos_static_routes/tests/netconf/overridden.yaml @@ -0,0 +1,51 @@ +--- +- debug: + msg: "START junos_static_routes overridden integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml +- include_tasks: _base_config.yaml + +- set_fact: + expected_overridden_output: + - address_families: + - afi: 'ipv4' + routes: + - dest: 192.168.20.0/24 + next_hop: + - forward_router_address: 192.168.20.1 + metric: 10 + +- block: + - name: Override the provided configuration with the exisiting running configuration + junos_static_routes: &overridden + config: + - address_families: + - afi: 'ipv4' + routes: + - dest: 192.168.20.0/24 + next_hop: + - forward_router_address: 192.168.20.1 + metric: 10 + state: overridden + register: result + + - name: Assert the configuration is reflected on host + assert: + that: + - "{{ expected_overridden_output | symmetric_difference(result['after']) |length == 0 }}" + debugger: on_failed + + - name: Override the provided configuration with the existing running configuration (IDEMPOTENT) + junos_static_routes: *overridden + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + + always: + - include_tasks: _remove_config.yaml + +- debug: + msg: "END junos_static_routes overridden integration tests on connection={{ ansible_connection }}" diff --git a/test/integration/targets/junos_static_routes/tests/netconf/replaced.yaml b/test/integration/targets/junos_static_routes/tests/netconf/replaced.yaml new file mode 100644 index 00000000000..e2715a413b5 --- /dev/null +++ b/test/integration/targets/junos_static_routes/tests/netconf/replaced.yaml @@ -0,0 +1,54 @@ +--- +- debug: + msg: "START junos_static_routes overridden integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml +- include_tasks: _base_config.yaml + +- set_fact: + expected_overridden_output: + - address_families: + - afi: 'ipv6' + routes: + - dest: 2001:db8::5/128 + next_hop: + - forward_router_address: 2001:db8:0:1:2a0:a502:0:19da + - afi: 'ipv4' + routes: + - dest: 192.168.0.0/24 + next_hop: + - forward_router_address: 192.168.20.1 + +- block: + - name: Replace the provided configuration with the exisiting running configuration + junos_static_routes: &overridden + config: + - address_families: + - afi: 'ipv4' + routes: + - dest: 192.168.0.0/24 + next_hop: + - forward_router_address: 192.168.20.1 + state: replaced + register: result + + - name: Assert the configuration is reflected on host + assert: + that: + - "{{ expected_overridden_output | symmetric_difference(result['after']) |length == 0 }}" + debugger: on_failed + + - name: Override the provided configuration with the existing running configuration (IDEMPOTENT) + junos_static_routes: *overridden + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + + always: + - include_tasks: _remove_config.yaml + +- debug: + msg: "END junos_static_routes overridden integration tests on connection={{ ansible_connection }}" diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index 723f633a9d7..aafd0c7b0b9 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -3994,13 +3994,13 @@ lib/ansible/modules/network/junos/junos_scp.py validate-modules:doc-missing-type lib/ansible/modules/network/junos/junos_scp.py validate-modules:doc-required-mismatch lib/ansible/modules/network/junos/junos_scp.py validate-modules:parameter-type-not-in-doc lib/ansible/modules/network/junos/junos_scp.py validate-modules:undocumented-parameter -lib/ansible/modules/network/junos/junos_static_route.py validate-modules:doc-choices-do-not-match-spec -lib/ansible/modules/network/junos/junos_static_route.py validate-modules:doc-default-does-not-match-spec -lib/ansible/modules/network/junos/junos_static_route.py validate-modules:doc-missing-type -lib/ansible/modules/network/junos/junos_static_route.py validate-modules:doc-required-mismatch -lib/ansible/modules/network/junos/junos_static_route.py validate-modules:missing-suboption-docs -lib/ansible/modules/network/junos/junos_static_route.py validate-modules:parameter-type-not-in-doc -lib/ansible/modules/network/junos/junos_static_route.py validate-modules:undocumented-parameter +lib/ansible/modules/network/junos/_junos_static_route.py validate-modules:doc-choices-do-not-match-spec +lib/ansible/modules/network/junos/_junos_static_route.py validate-modules:doc-default-does-not-match-spec +lib/ansible/modules/network/junos/_junos_static_route.py validate-modules:doc-missing-type +lib/ansible/modules/network/junos/_junos_static_route.py validate-modules:doc-required-mismatch +lib/ansible/modules/network/junos/_junos_static_route.py validate-modules:missing-suboption-docs +lib/ansible/modules/network/junos/_junos_static_route.py validate-modules:parameter-type-not-in-doc +lib/ansible/modules/network/junos/_junos_static_route.py validate-modules:undocumented-parameter lib/ansible/modules/network/junos/junos_system.py validate-modules:doc-choices-do-not-match-spec lib/ansible/modules/network/junos/junos_system.py validate-modules:doc-default-does-not-match-spec lib/ansible/modules/network/junos/junos_system.py validate-modules:doc-missing-type