1# 2# (c) 2017 Red Hat Inc. 3# 4# This file is part of Ansible 5# 6# Ansible is free software: you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation, either version 3 of the License, or 9# (at your option) any later version. 10# 11# Ansible is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 18# 19from __future__ import (absolute_import, division, print_function) 20__metaclass__ = type 21 22from abc import abstractmethod 23from functools import wraps 24 25from ansible.errors import AnsibleError 26from ansible.plugins import AnsiblePlugin 27from ansible.module_utils._text import to_native 28from ansible.module_utils.basic import missing_required_lib 29 30try: 31 from ncclient.operations import RPCError 32 from ncclient.xml_ import to_xml, to_ele, NCElement 33 HAS_NCCLIENT = True 34 NCCLIENT_IMP_ERR = None 35# paramiko and gssapi are incompatible and raise AttributeError not ImportError 36# When running in FIPS mode, cryptography raises InternalError 37# https://bugzilla.redhat.com/show_bug.cgi?id=1778939 38except Exception as err: 39 HAS_NCCLIENT = False 40 NCCLIENT_IMP_ERR = err 41 42try: 43 from lxml.etree import Element, SubElement, tostring, fromstring 44except ImportError: 45 from xml.etree.ElementTree import Element, SubElement, tostring, fromstring 46 47 48def ensure_ncclient(func): 49 @wraps(func) 50 def wrapped(self, *args, **kwargs): 51 if not HAS_NCCLIENT: 52 raise AnsibleError("%s: %s" % (missing_required_lib('ncclient'), to_native(NCCLIENT_IMP_ERR))) 53 return func(self, *args, **kwargs) 54 return wrapped 55 56 57class NetconfBase(AnsiblePlugin): 58 """ 59 A base class for implementing Netconf connections 60 61 .. note:: Unlike most of Ansible, nearly all strings in 62 :class:`TerminalBase` plugins are byte strings. This is because of 63 how close to the underlying platform these plugins operate. Remember 64 to mark literal strings as byte string (``b"string"``) and to use 65 :func:`~ansible.module_utils._text.to_bytes` and 66 :func:`~ansible.module_utils._text.to_text` to avoid unexpected 67 problems. 68 69 List of supported rpc's: 70 :get: Retrieves running configuration and device state information 71 :get_config: Retrieves the specified configuration from the device 72 :edit_config: Loads the specified commands into the remote device 73 :commit: Load configuration from candidate to running 74 :discard_changes: Discard changes to candidate datastore 75 :validate: Validate the contents of the specified configuration. 76 :lock: Allows the client to lock the configuration system of a device. 77 :unlock: Release a configuration lock, previously obtained with the lock operation. 78 :copy_config: create or replace an entire configuration datastore with the contents of another complete 79 configuration datastore. 80 :get-schema: Retrieves the required schema from the device 81 :get_capabilities: Retrieves device information and supported rpc methods 82 83 For JUNOS: 84 :execute_rpc: RPC to be execute on remote device 85 :load_configuration: Loads given configuration on device 86 87 Note: rpc support depends on the capabilites of remote device. 88 89 :returns: Returns output received from remote device as byte string 90 Note: the 'result' or 'error' from response should to be converted to object 91 of ElementTree using 'fromstring' to parse output as xml doc 92 93 'get_capabilities()' returns 'result' as a json string. 94 95 Usage: 96 from ansible.module_utils.connection import Connection 97 98 conn = Connection() 99 data = conn.execute_rpc(rpc) 100 reply = fromstring(reply) 101 102 data = conn.get_capabilities() 103 json.loads(data) 104 105 conn.load_configuration(config=[''set system ntp server 1.1.1.1''], action='set', format='text') 106 """ 107 108 __rpc__ = ['rpc', 'get_config', 'get', 'edit_config', 'validate', 'copy_config', 'dispatch', 'lock', 'unlock', 109 'discard_changes', 'commit', 'get_schema', 'delete_config', 'get_device_operations'] 110 111 def __init__(self, connection): 112 super(NetconfBase, self).__init__() 113 self._connection = connection 114 115 @property 116 def m(self): 117 return self._connection.manager 118 119 def rpc(self, name): 120 """ 121 RPC to be execute on remote device 122 :param name: Name of rpc in string format 123 :return: Received rpc response from remote host 124 """ 125 try: 126 obj = to_ele(name) 127 resp = self.m.rpc(obj) 128 return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 129 except RPCError as exc: 130 msg = exc.xml 131 raise Exception(to_xml(msg)) 132 133 def get_config(self, source=None, filter=None): 134 """ 135 Retrieve all or part of a specified configuration 136 (by default entire configuration is retrieved). 137 :param source: Name of the configuration datastore being queried, defaults to running datastore 138 :param filter: This argument specifies the portion of the configuration data to retrieve 139 :return: Returns xml string containing the RPC response received from remote host 140 """ 141 if isinstance(filter, list): 142 filter = tuple(filter) 143 144 if not source: 145 source = 'running' 146 resp = self.m.get_config(source=source, filter=filter) 147 return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 148 149 def get(self, filter=None, with_defaults=None): 150 """ 151 Retrieve device configuration and state information. 152 :param filter: This argument specifies the portion of the state data to retrieve 153 (by default entire state data is retrieved) 154 :param with_defaults: defines an explicit method of retrieving default values 155 from the configuration 156 :return: Returns xml string containing the RPC response received from remote host 157 """ 158 if isinstance(filter, list): 159 filter = tuple(filter) 160 resp = self.m.get(filter=filter, with_defaults=with_defaults) 161 response = resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 162 return response 163 164 def edit_config(self, config=None, format='xml', target='candidate', default_operation=None, test_option=None, error_option=None): 165 """ 166 Loads all or part of the specified *config* to the *target* configuration datastore. 167 :param config: Is the configuration, which must be rooted in the `config` element. 168 It can be specified either as a string or an :class:`~xml.etree.ElementTree.Element`. 169 :param format: The format of configuration eg. xml, text 170 :param target: Is the name of the configuration datastore being edited 171 :param default_operation: If specified must be one of { `"merge"`, `"replace"`, or `"none"` } 172 :param test_option: If specified must be one of { `"test_then_set"`, `"set"` } 173 :param error_option: If specified must be one of { `"stop-on-error"`, `"continue-on-error"`, `"rollback-on-error"` } 174 The `"rollback-on-error"` *error_option* depends on the `:rollback-on-error` capability. 175 :return: Returns xml string containing the RPC response received from remote host 176 """ 177 if config is None: 178 raise ValueError('config value must be provided') 179 resp = self.m.edit_config(config, format=format, target=target, default_operation=default_operation, test_option=test_option, 180 error_option=error_option) 181 return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 182 183 def validate(self, source='candidate'): 184 """ 185 Validate the contents of the specified configuration. 186 :param source: Is the name of the configuration datastore being validated or `config` element 187 containing the configuration subtree to be validated 188 :return: Returns xml string containing the RPC response received from remote host 189 """ 190 resp = self.m.validate(source=source) 191 return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 192 193 def copy_config(self, source, target): 194 """ 195 Create or replace an entire configuration datastore with the contents of another complete configuration datastore. 196 :param source: Is the name of the configuration datastore to use as the source of the copy operation or `config` 197 element containing the configuration subtree to copy 198 :param target: Is the name of the configuration datastore to use as the destination of the copy operation 199 :return: Returns xml string containing the RPC response received from remote host 200 """ 201 resp = self.m.copy_config(source, target) 202 return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 203 204 def dispatch(self, rpc_command=None, source=None, filter=None): 205 """ 206 Execute rpc on the remote device eg. dispatch('clear-arp-table') 207 :param rpc_command: specifies rpc command to be dispatched either in plain text or in xml element format (depending on command) 208 :param source: name of the configuration datastore being queried 209 :param filter: specifies the portion of the configuration to retrieve (by default entire configuration is retrieved) 210 :return: Returns xml string containing the RPC response received from remote host 211 """ 212 if rpc_command is None: 213 raise ValueError('rpc_command value must be provided') 214 215 resp = self.m.dispatch(fromstring(rpc_command), source=source, filter=filter) 216 217 if isinstance(resp, NCElement): 218 # In case xml reply is transformed or namespace is removed in 219 # ncclient device specific handler return modified xml response 220 result = resp.data_xml 221 elif hasattr(resp, 'data_ele') and resp.data_ele: 222 # if data node is present in xml response return the xml string 223 # with data node as root 224 result = resp.data_xml 225 else: 226 # return raw xml string received from host with rpc-reply as the root node 227 result = resp.xml 228 229 return result 230 231 def lock(self, target="candidate"): 232 """ 233 Allows the client to lock the configuration system of a device. 234 :param target: is the name of the configuration datastore to lock, 235 defaults to candidate datastore 236 :return: Returns xml string containing the RPC response received from remote host 237 """ 238 resp = self.m.lock(target=target) 239 return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 240 241 def unlock(self, target="candidate"): 242 """ 243 Release a configuration lock, previously obtained with the lock operation. 244 :param target: is the name of the configuration datastore to unlock, 245 defaults to candidate datastore 246 :return: Returns xml string containing the RPC response received from remote host 247 """ 248 resp = self.m.unlock(target=target) 249 return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 250 251 def discard_changes(self): 252 """ 253 Revert the candidate configuration to the currently running configuration. 254 Any uncommitted changes are discarded. 255 :return: Returns xml string containing the RPC response received from remote host 256 """ 257 resp = self.m.discard_changes() 258 return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 259 260 def commit(self, confirmed=False, timeout=None, persist=None): 261 """ 262 Commit the candidate configuration as the device's new current configuration. 263 Depends on the `:candidate` capability. 264 A confirmed commit (i.e. if *confirmed* is `True`) is reverted if there is no 265 followup commit within the *timeout* interval. If no timeout is specified the 266 confirm timeout defaults to 600 seconds (10 minutes). 267 A confirming commit may have the *confirmed* parameter but this is not required. 268 Depends on the `:confirmed-commit` capability. 269 :param confirmed: whether this is a confirmed commit 270 :param timeout: specifies the confirm timeout in seconds 271 :param persist: make the confirmed commit survive a session termination, 272 and set a token on the ongoing confirmed commit 273 :return: Returns xml string containing the RPC response received from remote host 274 """ 275 resp = self.m.commit(confirmed=confirmed, timeout=timeout, persist=persist) 276 return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 277 278 def get_schema(self, identifier=None, version=None, format=None): 279 """ 280 Retrieve a named schema, with optional revision and type. 281 :param identifier: name of the schema to be retrieved 282 :param version: version of schema to get 283 :param format: format of the schema to be retrieved, yang is the default 284 :return: Returns xml string containing the RPC response received from remote host 285 """ 286 resp = self.m.get_schema(identifier, version=version, format=format) 287 return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 288 289 def delete_config(self, target): 290 """ 291 delete a configuration datastore 292 :param target: specifies the name or URL of configuration datastore to delete 293 :return: Returns xml string containing the RPC response received from remote host 294 """ 295 resp = self.m.delete_config(target) 296 return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml 297 298 def locked(self, target): 299 return self.m.locked(target) 300 301 @abstractmethod 302 def get_capabilities(self): 303 """ 304 Retrieves device information and supported 305 rpc methods by device platform and return result 306 as a string 307 :return: Netconf session capability 308 """ 309 pass 310 311 @staticmethod 312 def guess_network_os(obj): 313 """ 314 Identifies the operating system of network device. 315 :param obj: ncclient manager connection instance 316 :return: The name of network operating system. 317 """ 318 pass 319 320 def get_base_rpc(self): 321 """ 322 Returns list of base rpc method supported by remote device 323 :return: List of RPC supported 324 """ 325 return self.__rpc__ 326 327 def put_file(self, source, destination): 328 """ 329 Copies file to remote host 330 :param source: Source location of file 331 :param destination: Destination file path 332 :return: Returns xml string containing the RPC response received from remote host 333 """ 334 pass 335 336 def fetch_file(self, source, destination): 337 """ 338 Fetch file from remote host 339 :param source: Source location of file 340 :param destination: Source location of file 341 :return: Returns xml string containing the RPC response received from remote host 342 """ 343 pass 344 345 def get_device_operations(self, server_capabilities): 346 """ 347 Retrieve remote host capability from Netconf server hello message. 348 :param server_capabilities: Server capabilities received during Netconf session initialization 349 :return: Remote host capabilities in dictionary format 350 """ 351 operations = {} 352 capabilities = '\n'.join(server_capabilities) 353 operations['supports_commit'] = ':candidate' in capabilities 354 operations['supports_defaults'] = ':with-defaults' in capabilities 355 operations['supports_confirm_commit'] = ':confirmed-commit' in capabilities 356 operations['supports_startup'] = ':startup' in capabilities 357 operations['supports_xpath'] = ':xpath' in capabilities 358 operations['supports_writable_running'] = ':writable-running' in capabilities 359 operations['supports_validate'] = ':validate' in capabilities 360 361 operations['lock_datastore'] = [] 362 if operations['supports_writable_running']: 363 operations['lock_datastore'].append('running') 364 365 if operations['supports_commit']: 366 operations['lock_datastore'].append('candidate') 367 368 if operations['supports_startup']: 369 operations['lock_datastore'].append('startup') 370 371 operations['supports_lock'] = bool(operations['lock_datastore']) 372 373 return operations 374 375# TODO Restore .xml, when ncclient supports it for all platforms 376