diff --git a/.gitignore b/.gitignore index 8230afb21b522e60708e4f3985e205e170f7ab46..10ad04846cf7da272a86ddfd5db14ed8054ff278 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__/ .DS_Store .vscode geant/gap_ansible/.ansible-lint +.venv +geant-gap_ansible-*.tar.gz diff --git a/geant/gap_ansible/plugins/modules/junos_config.py b/geant/gap_ansible/plugins/modules/junos_config.py new file mode 100644 index 0000000000000000000000000000000000000000..c2581ce6dc7554a5203a0f3c68f65cab90e97c79 --- /dev/null +++ b/geant/gap_ansible/plugins/modules/junos_config.py @@ -0,0 +1,592 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from contextlib import contextmanager + +DOCUMENTATION = """ +module: junos_config +author: Peter Sprygada (@privateip) +short_description: Manage configuration on devices running Juniper JUNOS +description: +- This module provides an implementation for working with the active configuration + running on Juniper JUNOS devices. It provides a set of arguments for loading configuration, + performing rollback operations and zeroing the active configuration on the device. +version_added: 1.0.0 +extends_documentation_fragment: +- junipernetworks.junos.junos +options: + lines: + description: + - This argument takes a list of C(set) or C(delete) configuration lines to push + into the remote device. Each line must start with either C(set) or C(delete). This + argument is mutually exclusive with the I(src) argument. + type: list + aliases: + - commands + elements: str + src: + description: + - The I(src) argument provides a path to the configuration file to load into the + remote system. The path can either be a full system path to the configuration + file if the value starts with / or relative to the root of the implemented role + or playbook. This argument is mutually exclusive with the I(lines) argument. + type: path + src_format: + description: + - The I(src_format) argument specifies the format of the configuration found int + I(src). If the I(src_format) argument is not provided, the module will attempt + to determine the format of the configuration file specified in I(src). + type: str + choices: + - xml + - set + - text + - json + rollback: + description: + - The C(rollback) argument instructs the module to rollback the current configuration + to the identifier specified in the argument. If the specified rollback identifier + does not exist on the remote device, the module will fail. To rollback to the + most recent commit, set the C(rollback) argument to 0. + type: int + zeroize: + description: + - The C(zeroize) argument is used to completely sanitize the remote device configuration + back to initial defaults. This argument will effectively remove all current + configuration statements on the remote device. + type: bool + default: false + confirm: + description: + - The C(confirm) argument will configure a time out value in minutes for the commit + to be confirmed before it is automatically rolled back. If the C(confirm) argument + is set to False, this argument is silently ignored. If the value for this argument + is set to 0, the commit is confirmed immediately. + type: int + default: 0 + comment: + description: + - The C(comment) argument specifies a text string to be used when committing the + configuration. If the C(confirm) argument is set to False, this argument is + silently ignored. + default: configured by junos_config + type: str + replace: + description: + - The C(replace) argument will instruct the remote device to replace the current + configuration hierarchy with the one specified in the corresponding hierarchy + of the source configuration loaded from this module. + - Note this argument should be considered deprecated. To achieve the equivalent, + set the I(update) argument to C(replace). This argument will be removed in a + future release. The C(replace) and C(update) argument is mutually exclusive. + type: bool + backup: + description: + - This argument will cause the module to create a full backup of the current C(running-config) + from the remote device before any changes are made. If the C(backup_options) + value is not given, the backup file is written to the C(backup) folder in the + playbook root directory or role root directory, if playbook is part of an ansible + role. If the directory does not exist, it is created. + type: bool + default: false + update: + description: + - This argument will decide how to load the configuration data particularly when + the candidate configuration and loaded configuration contain conflicting statements. + Following are accepted values. C(merge) combines the data in the loaded configuration + with the candidate configuration. If statements in the loaded configuration + conflict with statements in the candidate configuration, the loaded statements + replace the candidate ones. C(override) discards the entire candidate configuration + and replaces it with the loaded configuration. C(replace) substitutes each hierarchy + level in the loaded configuration for the corresponding level. C(update) is + similar to the override option. The new configuration completely replaces the + existing configuration. The difference comes when the configuration is later + committed. This option performs a 'diff' between the new candidate configuration + and the existing committed configuration. It then only notifies system processes + responsible for the changed portions of the configuration, and only marks the + actual configuration changes as 'changed'. + type: str + default: merge + choices: + - merge + - override + - replace + - update + confirm_commit: + description: + - This argument will execute commit operation on remote device. It can be used + to confirm a previous commit. + type: bool + default: false + check_commit: + description: + - This argument will check correctness of syntax; do not apply changes. + - Note that this argument can be used to confirm verified configuration done via + commit confirmed operation + type: bool + default: false + backup_options: + description: + - This is a dict object containing configurable options related to backup file + path. The value of this option is read only when C(backup) is set to I(true), + if C(backup) is set to I(false) this option will be silently ignored. + suboptions: + filename: + description: + - The filename to be used to store the backup configuration. If the filename + is not given it will be generated based on the hostname, current time and + date in format defined by <hostname>_config.<current-date>@<current-time> + type: str + dir_path: + description: + - This option provides the path ending with directory name in which the backup + configuration file will be stored. If the directory does not exist it will + be first created and the filename is either the value of C(filename) or + default filename as described in C(filename) options description. If the + path value is not given in that case a I(backup) directory will be created + in the current working directory and backup configuration will be copied + in C(filename) within I(backup) directory. + type: path + backup_format: + description: + - This argument specifies the format of the configuration the backup file will + be stored as. If the argument is not specified, the module will use the 'set' + format. + type: str + default: set + choices: + - xml + - set + - text + - json + type: dict +config_mode: + description: + - If set to “private”, open a private candidate datastore instead of the global one. + type: str + choices: [ private ] + required: false + default: null +requirements: +- ncclient (>=v0.5.2) +notes: +- This module requires the netconf system service be enabled on the remote device + being managed. +- Abbreviated commands are NOT idempotent, see L(Network FAQ,../network/user_guide/faq.html) +- Loading JSON-formatted configuration I(json) is supported starting in Junos OS Release + 16.1 onwards. +- Update C(override) not currently compatible with C(set) notation. +- Tested against vSRX JUNOS version 15.1X49-D15.4, vqfx-10000 JUNOS Version 15.1X53-D60.4. +- Recommended connection is C(netconf). See L(the Junos OS Platform Options,../network/user_guide/platform_junos.html). +- This module also works with C(local) connections for legacy playbooks. +""" + +EXAMPLES = """ +- name: load configure file into device + junipernetworks.junos.junos_config: + src: srx.cfg + comment: update config + +- name: load configure lines into device + junipernetworks.junos.junos_config: + lines: + - set interfaces ge-0/0/1 unit 0 description "Test interface" + - set vlans vlan01 description "Test vlan" + comment: update config + +- name: Set routed VLAN interface (RVI) IPv4 address + junipernetworks.junos.junos_config: + lines: + - set vlans vlan01 vlan-id 1 + - set interfaces irb unit 10 family inet address 10.0.0.1/24 + - set vlans vlan01 l3-interface irb.10 + +- name: Check correctness of commit configuration + junipernetworks.junos.junos_config: + check_commit: true + +- name: rollback the configuration to id 10 + junipernetworks.junos.junos_config: + rollback: 10 + +- name: zero out the current configuration + junipernetworks.junos.junos_config: + zeroize: true + +- name: Set VLAN access and trunking + junipernetworks.junos.junos_config: + lines: + - set vlans vlan02 vlan-id 6 + - set interfaces ge-0/0/6.0 family ethernet-switching interface-mode access vlan + members vlan02 + - set interfaces ge-0/0/6.0 family ethernet-switching interface-mode trunk vlan + members vlan02 + +- name: confirm a previous commit + junipernetworks.junos.junos_config: + confirm_commit: true + +- name: for idempotency, use full-form commands + junipernetworks.junos.junos_config: + lines: + # - set int ge-0/0/1 unit 0 desc "Test interface" + - set interfaces ge-0/0/1 unit 0 description "Test interface" + +- name: configurable backup path + junipernetworks.junos.junos_config: + src: srx.cfg + backup: true + backup_options: + filename: backup.cfg + dir_path: /home/user +""" + +RETURN = """ +backup_path: + description: The full path to the backup file + returned: when backup is true + type: str + sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34 +filename: + description: The name of the backup file + returned: when backup is true and filename is not specified in backup options + type: str + sample: junos01_config.2016-07-16@22:28:34 +shortname: + description: The full path to the backup file excluding the timestamp + returned: when backup is true and filename is not specified in backup options + type: str + sample: /playbooks/ansible/backup/junos01_config +date: + description: The date extracted from the backup file name + returned: when backup is true + type: str + sample: "2016-07-16" +time: + description: The time extracted from the backup file name + returned: when backup is true + type: str + sample: "22:28:34" +""" +import json +import re + +from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import string_types +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.netconf import ( + exec_rpc, +) +from ansible.module_utils.connection import ConnectionError +from ansible_collections.junipernetworks.junos.plugins.module_utils.network.junos.junos import ( + commit_configuration, + discard_changes, + get_configuration, + get_diff, + load_config, + load_configuration, + tostring, + get_connection, + lock_configuration, + unlock_configuration, +) + +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): + if module.params["replace"] is not None: + module.fail_json(msg="argument replace is deprecated, use update") + + +def zeroize(module): + return exec_rpc( + module, + tostring(Element("request-system-zeroize")), + ignore_warning=False, + ) + + +def rollback(ele, id="0"): + return get_diff(ele, id) + + +def guess_format(config): + try: + json.loads(config) + return "json" + except ValueError: + pass + + try: + fromstring(config) + return "xml" + except ParseError: + pass + + if config.startswith("set") or config.startswith("delete"): + return "set" + + return "text" + + +def filter_delete_statements(module, candidate): + reply = get_configuration(module, format="set") + match = reply.find(".//configuration-set") + if match is None: + # Could not find configuration-set in reply, perhaps device does not support it? + return candidate + config = to_native(match.text, encoding="latin-1") + + modified_candidate = candidate[:] + for index, line in reversed(list(enumerate(candidate))): + if line.startswith("delete"): + newline = re.sub("^delete", "set", line) + if newline not in config: + del modified_candidate[index] + + return modified_candidate + + +def configure_device(module, warnings, candidate): + kwargs = {} + config_format = None + + if module.params["src"]: + config_format = module.params["src_format"] or guess_format( + str(candidate), + ) + if config_format == "set": + kwargs.update({"format": "text", "action": "set"}) + else: + kwargs.update( + {"format": config_format, "action": module.params["update"]}, + ) + + if isinstance(candidate, string_types): + candidate = candidate.split("\n") + + # this is done to filter out `delete ...` statements which map to + # nothing in the config as that will cause an exception to be raised + if any((module.params["lines"], config_format == "set")): + candidate = filter_delete_statements(module, candidate) + kwargs["format"] = "text" + kwargs["action"] = "set" + + return load_config(module, candidate, warnings, **kwargs) + + +@contextmanager +def safe_locked_config(module): + """Lock the candidate—either shared or private—depending on config_mode.""" + conn = get_connection(module) + private = module.params.get("config_mode") == "private" + + try: + if private: + # Try to clean up any stale private first + try: + conn.execute_rpc( + tostring( + Element("close-configuration", + {"xmlns": "http://xml.juniper.net/xnm/1.1/xnm"} + ) + ) + ) + except ConnectionError: + pass + + # Open your own private sandbox + conn.execute_rpc( + tostring( + Element("open-configuration", + {"xmlns": "http://xml.juniper.net/xnm/1.1/xnm", + "private": "true"} + ) + ) + ) + + else: + # shared candidate: standard lock + lock_configuration(module) + except ConnectionError as exc: + module.fail_json(msg=f"Could not acquire {'private' if private else 'shared'} lock: {to_text(exc)}") + + # Hand control back to the caller + try: + yield + finally: + # Teardown / unlock + try: + if private: + conn.execute_rpc( + tostring(Element("close-configuration", + {"xmlns": "http://xml.juniper.net/xnm/1.1/xnm"} + )) + ) + else: + unlock_configuration(module) + except ConnectionError as exc: + module.warn(f"Ignoring failure closing {'private' if private else 'shared'} lock: {to_text(exc)}") + + +def main(): + """main entry point for module execution""" + backup_spec = dict( + filename=dict(), + dir_path=dict(type="path"), + backup_format=dict( + default="set", + choices=["xml", "text", "set", "json"], + ), + ) + argument_spec = dict( + lines=dict(aliases=["commands"], type="list", elements="str"), + src=dict(type="path"), + src_format=dict(choices=["xml", "text", "set", "json"]), + # update operations + update=dict( + default="merge", + choices=["merge", "override", "replace", "update"], + ), + # deprecated replace in Ansible 2.3 + replace=dict(type="bool"), + confirm=dict(default=0, type="int"), + comment=dict(default=DEFAULT_COMMENT), + confirm_commit=dict(type="bool", default=False), + check_commit=dict(type="bool", default=False), + # config operations + backup=dict(type="bool", default=False), + backup_options=dict(type="dict", options=backup_spec), + rollback=dict(type="int"), + zeroize=dict(default=False, type="bool"), + config_mode=dict(type='str', choices=['private'], required=False, default=None), + ) + + mutually_exclusive = [("lines", "src", "rollback", "zeroize")] + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + warnings = list() + check_args(module, warnings) + + candidate = module.params["lines"] or module.params["src"] + commit = not module.check_mode + + result = {"changed": False, "warnings": warnings} + + if module.params["backup"]: + if module.params["backup_options"] is not None: + conf_format = module.params["backup_options"]["backup_format"] + else: + conf_format = "set" + reply = get_configuration(module, format=conf_format) + if reply is None: + module.fail_json(msg="unable to retrieve device configuration") + else: + if conf_format in ["set", "text"]: + reply = reply.find( + ".//configuration-%s" % conf_format, + ).text.strip() + elif conf_format in "xml": + reply = str( + tostring(reply.find(".//configuration"), pretty_print=True), + ).strip() + elif conf_format in "json": + reply = str(reply.xpath("//rpc-reply/text()")[0]).strip() + if not isinstance(reply, str): + module.fail_json( + msg="unable to format retrieved device configuration", + ) + result["__backup__"] = reply + + rollback_id = module.params["rollback"] + if isinstance(rollback_id, int) and rollback_id >= 0: + diff = rollback(module, rollback_id) + if commit: + kwargs = {"comment": module.params["comment"]} + with safe_locked_config(module): + load_configuration(module, rollback=rollback_id) + commit_configuration(module, **kwargs) + if module._diff: + result["diff"] = {"prepared": diff} + result["changed"] = True + + elif module.params["zeroize"]: + if commit: + zeroize(module) + result["changed"] = True + + else: + if candidate: + with safe_locked_config(module): + diff = configure_device(module, warnings, candidate) + if diff: + if commit: + kwargs = { + "comment": module.params["comment"], + "check": module.params["check_commit"], + } + + confirm = module.params["confirm"] + if confirm > 0: + kwargs.update( + { + "confirm": True, + "confirm_timeout": to_text( + confirm, + errors="surrogate_then_replace", + ), + }, + ) + commit_configuration(module, **kwargs) + else: + discard_changes(module) + result["changed"] = True + + if module._diff: + result["diff"] = {"prepared": diff} + + elif module.params["check_commit"]: + commit_configuration(module, check=True) + + elif module.params["confirm_commit"]: + with safe_locked_config(module): + # confirm a previous commit + commit_configuration(module) + + result["changed"] = True + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/geant/gap_ansible/plugins/modules/nokia_netconf_config.py b/geant/gap_ansible/plugins/modules/nokia_netconf_config.py index ca63f799765ee5e9bfbe54fda1b3174ff23b237e..20736e57fdc60e73bbae34d0095c6c5780d4f710 100644 --- a/geant/gap_ansible/plugins/modules/nokia_netconf_config.py +++ b/geant/gap_ansible/plugins/modules/nokia_netconf_config.py @@ -396,7 +396,6 @@ EXAMPLES = """ # "interfaces.interface.config.enabled": "true", # "interfaces.interface.config.mtu": "0", # "interfaces.interface.config.name": "Ethernet2", - """ RETURN = """