From 8a754e0858d922e955e71b253c139e071ecec432 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 18:04:21 +0200 Subject: Adding upstream version 2.14.3. Signed-off-by: Daniel Baumann --- lib/ansible/plugins/netconf/__init__.py | 375 ++++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 lib/ansible/plugins/netconf/__init__.py (limited to 'lib/ansible/plugins/netconf') diff --git a/lib/ansible/plugins/netconf/__init__.py b/lib/ansible/plugins/netconf/__init__.py new file mode 100644 index 0000000..e99efbd --- /dev/null +++ b/lib/ansible/plugins/netconf/__init__.py @@ -0,0 +1,375 @@ +# +# (c) 2017 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from abc import abstractmethod +from functools import wraps + +from ansible.errors import AnsibleError +from ansible.plugins import AnsiblePlugin +from ansible.module_utils._text import to_native +from ansible.module_utils.basic import missing_required_lib + +try: + from ncclient.operations import RPCError + from ncclient.xml_ import to_xml, to_ele, NCElement + HAS_NCCLIENT = True + NCCLIENT_IMP_ERR = None +# paramiko and gssapi are incompatible and raise AttributeError not ImportError +# When running in FIPS mode, cryptography raises InternalError +# https://bugzilla.redhat.com/show_bug.cgi?id=1778939 +except Exception as err: + HAS_NCCLIENT = False + NCCLIENT_IMP_ERR = err + +try: + from lxml.etree import Element, SubElement, tostring, fromstring +except ImportError: + from xml.etree.ElementTree import Element, SubElement, tostring, fromstring + + +def ensure_ncclient(func): + @wraps(func) + def wrapped(self, *args, **kwargs): + if not HAS_NCCLIENT: + raise AnsibleError("%s: %s" % (missing_required_lib('ncclient'), to_native(NCCLIENT_IMP_ERR))) + return func(self, *args, **kwargs) + return wrapped + + +class NetconfBase(AnsiblePlugin): + """ + A base class for implementing Netconf connections + + .. note:: Unlike most of Ansible, nearly all strings in + :class:`TerminalBase` plugins are byte strings. This is because of + how close to the underlying platform these plugins operate. Remember + to mark literal strings as byte string (``b"string"``) and to use + :func:`~ansible.module_utils._text.to_bytes` and + :func:`~ansible.module_utils._text.to_text` to avoid unexpected + problems. + + List of supported rpc's: + :get: Retrieves running configuration and device state information + :get_config: Retrieves the specified configuration from the device + :edit_config: Loads the specified commands into the remote device + :commit: Load configuration from candidate to running + :discard_changes: Discard changes to candidate datastore + :validate: Validate the contents of the specified configuration. + :lock: Allows the client to lock the configuration system of a device. + :unlock: Release a configuration lock, previously obtained with the lock operation. + :copy_config: create or replace an entire configuration datastore with the contents of another complete + configuration datastore. + :get-schema: Retrieves the required schema from the device + :get_capabilities: Retrieves device information and supported rpc methods + + For JUNOS: + :execute_rpc: RPC to be execute on remote device + :load_configuration: Loads given configuration on device + + Note: rpc support depends on the capabilities of remote device. + + :returns: Returns output received from remote device as byte string + Note: the 'result' or 'error' from response should to be converted to object + of ElementTree using 'fromstring' to parse output as xml doc + + 'get_capabilities()' returns 'result' as a json string. + + Usage: + from ansible.module_utils.connection import Connection + + conn = Connection() + data = conn.execute_rpc(rpc) + reply = fromstring(reply) + + data = conn.get_capabilities() + json.loads(data) + + conn.load_configuration(config=[''set system ntp server 1.1.1.1''], action='set', format='text') + """ + + __rpc__ = ['rpc', 'get_config', 'get', 'edit_config', 'validate', 'copy_config', 'dispatch', 'lock', 'unlock', + 'discard_changes', 'commit', 'get_schema', 'delete_config', 'get_device_operations'] + + def __init__(self, connection): + super(NetconfBase, self).__init__() + self._connection = connection + + @property + def m(self): + return self._connection.manager + + def rpc(self, name): + """ + RPC to be execute on remote device + :param name: Name of rpc in string format + :return: Received rpc response from remote host + """ + try: + obj = to_ele(name) + resp = self.m.rpc(obj) + return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + except RPCError as exc: + msg = exc.xml + raise Exception(to_xml(msg)) + + def get_config(self, source=None, filter=None): + """ + Retrieve all or part of a specified configuration + (by default entire configuration is retrieved). + :param source: Name of the configuration datastore being queried, defaults to running datastore + :param filter: This argument specifies the portion of the configuration data to retrieve + :return: Returns xml string containing the RPC response received from remote host + """ + if isinstance(filter, list): + filter = tuple(filter) + + if not source: + source = 'running' + resp = self.m.get_config(source=source, filter=filter) + return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + + def get(self, filter=None, with_defaults=None): + """ + Retrieve device configuration and state information. + :param filter: This argument specifies the portion of the state data to retrieve + (by default entire state data is retrieved) + :param with_defaults: defines an explicit method of retrieving default values + from the configuration + :return: Returns xml string containing the RPC response received from remote host + """ + if isinstance(filter, list): + filter = tuple(filter) + resp = self.m.get(filter=filter, with_defaults=with_defaults) + response = resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + return response + + def edit_config(self, config=None, format='xml', target='candidate', default_operation=None, test_option=None, error_option=None): + """ + Loads all or part of the specified *config* to the *target* configuration datastore. + :param config: Is the configuration, which must be rooted in the `config` element. + It can be specified either as a string or an :class:`~xml.etree.ElementTree.Element`. + :param format: The format of configuration eg. xml, text + :param target: Is the name of the configuration datastore being edited + :param default_operation: If specified must be one of { `"merge"`, `"replace"`, or `"none"` } + :param test_option: If specified must be one of { `"test_then_set"`, `"set"` } + :param error_option: If specified must be one of { `"stop-on-error"`, `"continue-on-error"`, `"rollback-on-error"` } + The `"rollback-on-error"` *error_option* depends on the `:rollback-on-error` capability. + :return: Returns xml string containing the RPC response received from remote host + """ + if config is None: + raise ValueError('config value must be provided') + resp = self.m.edit_config(config, format=format, target=target, default_operation=default_operation, test_option=test_option, + error_option=error_option) + return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + + def validate(self, source='candidate'): + """ + Validate the contents of the specified configuration. + :param source: Is the name of the configuration datastore being validated or `config` element + containing the configuration subtree to be validated + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.validate(source=source) + return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + + def copy_config(self, source, target): + """ + Create or replace an entire configuration datastore with the contents of another complete configuration datastore. + :param source: Is the name of the configuration datastore to use as the source of the copy operation or `config` + element containing the configuration subtree to copy + :param target: Is the name of the configuration datastore to use as the destination of the copy operation + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.copy_config(source, target) + return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + + def dispatch(self, rpc_command=None, source=None, filter=None): + """ + Execute rpc on the remote device eg. dispatch('clear-arp-table') + :param rpc_command: specifies rpc command to be dispatched either in plain text or in xml element format (depending on command) + :param source: name of the configuration datastore being queried + :param filter: specifies the portion of the configuration to retrieve (by default entire configuration is retrieved) + :return: Returns xml string containing the RPC response received from remote host + """ + if rpc_command is None: + raise ValueError('rpc_command value must be provided') + + resp = self.m.dispatch(fromstring(rpc_command), source=source, filter=filter) + + if isinstance(resp, NCElement): + # In case xml reply is transformed or namespace is removed in + # ncclient device specific handler return modified xml response + result = resp.data_xml + elif hasattr(resp, 'data_ele') and resp.data_ele: + # if data node is present in xml response return the xml string + # with data node as root + result = resp.data_xml + else: + # return raw xml string received from host with rpc-reply as the root node + result = resp.xml + + return result + + def lock(self, target="candidate"): + """ + Allows the client to lock the configuration system of a device. + :param target: is the name of the configuration datastore to lock, + defaults to candidate datastore + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.lock(target=target) + return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + + def unlock(self, target="candidate"): + """ + Release a configuration lock, previously obtained with the lock operation. + :param target: is the name of the configuration datastore to unlock, + defaults to candidate datastore + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.unlock(target=target) + return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + + def discard_changes(self): + """ + Revert the candidate configuration to the currently running configuration. + Any uncommitted changes are discarded. + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.discard_changes() + return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + + def commit(self, confirmed=False, timeout=None, persist=None): + """ + Commit the candidate configuration as the device's new current configuration. + Depends on the `:candidate` capability. + A confirmed commit (i.e. if *confirmed* is `True`) is reverted if there is no + followup commit within the *timeout* interval. If no timeout is specified the + confirm timeout defaults to 600 seconds (10 minutes). + A confirming commit may have the *confirmed* parameter but this is not required. + Depends on the `:confirmed-commit` capability. + :param confirmed: whether this is a confirmed commit + :param timeout: specifies the confirm timeout in seconds + :param persist: make the confirmed commit survive a session termination, + and set a token on the ongoing confirmed commit + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.commit(confirmed=confirmed, timeout=timeout, persist=persist) + return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + + def get_schema(self, identifier=None, version=None, format=None): + """ + Retrieve a named schema, with optional revision and type. + :param identifier: name of the schema to be retrieved + :param version: version of schema to get + :param format: format of the schema to be retrieved, yang is the default + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.get_schema(identifier, version=version, format=format) + return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + + def delete_config(self, target): + """ + delete a configuration datastore + :param target: specifies the name or URL of configuration datastore to delete + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.delete_config(target) + return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml + + def locked(self, target): + return self.m.locked(target) + + @abstractmethod + def get_capabilities(self): + """ + Retrieves device information and supported + rpc methods by device platform and return result + as a string + :return: Netconf session capability + """ + pass + + @staticmethod + def guess_network_os(obj): + """ + Identifies the operating system of network device. + :param obj: ncclient manager connection instance + :return: The name of network operating system. + """ + pass + + def get_base_rpc(self): + """ + Returns list of base rpc method supported by remote device + :return: List of RPC supported + """ + return self.__rpc__ + + def put_file(self, source, destination): + """ + Copies file to remote host + :param source: Source location of file + :param destination: Destination file path + :return: Returns xml string containing the RPC response received from remote host + """ + pass + + def fetch_file(self, source, destination): + """ + Fetch file from remote host + :param source: Source location of file + :param destination: Source location of file + :return: Returns xml string containing the RPC response received from remote host + """ + pass + + def get_device_operations(self, server_capabilities): + """ + Retrieve remote host capability from Netconf server hello message. + :param server_capabilities: Server capabilities received during Netconf session initialization + :return: Remote host capabilities in dictionary format + """ + operations = {} + capabilities = '\n'.join(server_capabilities) + operations['supports_commit'] = ':candidate' in capabilities + operations['supports_defaults'] = ':with-defaults' in capabilities + operations['supports_confirm_commit'] = ':confirmed-commit' in capabilities + operations['supports_startup'] = ':startup' in capabilities + operations['supports_xpath'] = ':xpath' in capabilities + operations['supports_writable_running'] = ':writable-running' in capabilities + operations['supports_validate'] = ':validate' in capabilities + + operations['lock_datastore'] = [] + if operations['supports_writable_running']: + operations['lock_datastore'].append('running') + + if operations['supports_commit']: + operations['lock_datastore'].append('candidate') + + if operations['supports_startup']: + operations['lock_datastore'].append('startup') + + operations['supports_lock'] = bool(operations['lock_datastore']) + + return operations + +# TODO Restore .xml, when ncclient supports it for all platforms -- cgit v1.2.3