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 21__metaclass__ = type 22 23DOCUMENTATION = """ 24author: Ansible Networking Team 25netconf: junos 26short_description: Use junos netconf plugin to run netconf commands on Juniper JUNOS 27 platform 28description: 29- This junos plugin provides low level abstraction apis for sending and receiving 30 netconf commands from Juniper JUNOS network devices. 31version_added: 1.0.0 32options: 33 ncclient_device_handler: 34 type: str 35 default: junos 36 description: 37 - Specifies the ncclient device handler name for Juniper junos network os. To 38 identify the ncclient device handler name refer ncclient library documentation. 39""" 40 41import json 42import re 43 44from ansible.module_utils._text import to_text, to_native 45from ansible.module_utils.six import string_types 46from ansible.errors import AnsibleConnectionFailure 47from ansible.plugins.netconf import NetconfBase, ensure_ncclient 48 49try: 50 from ncclient import manager 51 from ncclient.operations import RPCError 52 from ncclient.transport.errors import SSHUnknownHostError 53 from ncclient.xml_ import to_ele, to_xml, new_ele, sub_ele 54 55 HAS_NCCLIENT = True 56except ( 57 ImportError, 58 AttributeError, 59): # paramiko and gssapi are incompatible and raise AttributeError not ImportError 60 HAS_NCCLIENT = False 61 62 63class Netconf(NetconfBase): 64 def get_text(self, ele, tag): 65 try: 66 return to_text( 67 ele.find(tag).text, errors="surrogate_then_replace" 68 ).strip() 69 except AttributeError: 70 pass 71 72 @ensure_ncclient 73 def get_device_info(self): 74 device_info = dict() 75 device_info["network_os"] = "junos" 76 ele = new_ele("get-software-information") 77 data = self.execute_rpc(to_xml(ele)) 78 reply = to_ele(data) 79 sw_info = reply.find(".//software-information") 80 81 device_info["network_os_version"] = self.get_text( 82 sw_info, "junos-version" 83 ) 84 device_info["network_os_hostname"] = self.get_text( 85 sw_info, "host-name" 86 ) 87 device_info["network_os_model"] = self.get_text( 88 sw_info, "product-model" 89 ) 90 91 return device_info 92 93 def execute_rpc(self, name): 94 """ 95 RPC to be execute on remote device 96 :param name: Name of rpc in string format 97 :return: Received rpc response from remote host 98 """ 99 return self.rpc(name) 100 101 @ensure_ncclient 102 def load_configuration( 103 self, format="xml", action="merge", target="candidate", config=None 104 ): 105 """ 106 Load given configuration on device 107 :param format: Format of configuration (xml, text, set) 108 :param action: Action to be performed (merge, replace, override, update) 109 :param target: The name of the configuration datastore being edited 110 :param config: The configuration to be loaded on remote host in string format 111 :return: Received rpc response from remote host in string format 112 """ 113 if config: 114 if format == "xml": 115 config = to_ele(config) 116 117 try: 118 return self.m.load_configuration( 119 format=format, action=action, target=target, config=config 120 ).data_xml 121 except RPCError as exc: 122 raise Exception(to_xml(exc.xml)) 123 124 def get_capabilities(self): 125 result = dict() 126 result["rpc"] = self.get_base_rpc() + [ 127 "commit", 128 "discard_changes", 129 "validate", 130 "lock", 131 "unlock", 132 "copy_copy", 133 "execute_rpc", 134 "load_configuration", 135 "get_configuration", 136 "command", 137 "reboot", 138 "halt", 139 ] 140 result["network_api"] = "netconf" 141 result["device_info"] = self.get_device_info() 142 result["server_capabilities"] = list(self.m.server_capabilities) 143 result["client_capabilities"] = list(self.m.client_capabilities) 144 result["session_id"] = self.m.session_id 145 result["device_operations"] = self.get_device_operations( 146 result["server_capabilities"] 147 ) 148 return json.dumps(result) 149 150 @staticmethod 151 @ensure_ncclient 152 def guess_network_os(obj): 153 """ 154 Guess the remote network os name 155 :param obj: Netconf connection class object 156 :return: Network OS name 157 """ 158 try: 159 m = manager.connect( 160 host=obj._play_context.remote_addr, 161 port=obj._play_context.port or 830, 162 username=obj._play_context.remote_user, 163 password=obj._play_context.password, 164 key_filename=obj.key_filename, 165 hostkey_verify=obj.get_option("host_key_checking"), 166 look_for_keys=obj.get_option("look_for_keys"), 167 allow_agent=obj._play_context.allow_agent, 168 timeout=obj.get_option("persistent_connect_timeout"), 169 # We need to pass in the path to the ssh_config file when guessing 170 # the network_os so that a jumphost is correctly used if defined 171 ssh_config=obj._ssh_config, 172 ) 173 except SSHUnknownHostError as exc: 174 raise AnsibleConnectionFailure(to_native(exc)) 175 176 guessed_os = None 177 for c in m.server_capabilities: 178 if re.search("junos", c): 179 guessed_os = "junos" 180 181 m.close_session() 182 return guessed_os 183 184 def get_configuration(self, format="xml", filter=None): 185 """ 186 Retrieve all or part of a specified configuration. 187 :param format: format in which configuration should be retrieved 188 :param filter: specifies the portion of the configuration to retrieve 189 as either xml string rooted in <configuration> element 190 :return: Received rpc response from remote host in string format 191 """ 192 if filter is not None: 193 if not isinstance(filter, string_types): 194 raise AnsibleConnectionFailure( 195 "get configuration filter should be of type string," 196 " received value '%s' is of type '%s'" 197 % (filter, type(filter)) 198 ) 199 filter = to_ele(filter) 200 201 return self.m.get_configuration(format=format, filter=filter).data_xml 202 203 def compare_configuration(self, rollback=0): 204 """ 205 Compare the candidate configuration with running configuration 206 by default. The candidate configuration can be compared with older 207 committed configuration by providing rollback id. 208 :param rollback: Rollback id of previously commited configuration 209 :return: Received rpc response from remote host in string format 210 """ 211 return self.m.compare_configuration(rollback=rollback).data_xml 212 213 def halt(self): 214 """reboot the device""" 215 return self.m.halt().data_xml 216 217 def reboot(self): 218 """reboot the device""" 219 return self.m.reboot().data_xml 220 221 # Due to issue in ncclient commit() method for Juniper (https://github.com/ncclient/ncclient/issues/238) 222 # below commit() is a workaround which build's raw `commit-configuration` xml with required tags and uses 223 # ncclient generic rpc() method to execute rpc on remote host. 224 # Remove below method after the issue in ncclient is fixed. 225 @ensure_ncclient 226 def commit( 227 self, 228 confirmed=False, 229 check=False, 230 timeout=None, 231 comment=None, 232 synchronize=False, 233 at_time=None, 234 ): 235 """ 236 Commit the candidate configuration as the device's new current configuration. 237 Depends on the `:candidate` capability. 238 A confirmed commit (i.e. if *confirmed* is `True`) is reverted if there is no 239 followup commit within the *timeout* interval. If no timeout is specified the 240 confirm timeout defaults to 600 seconds (10 minutes). 241 A confirming commit may have the *confirmed* parameter but this is not required. 242 Depends on the `:confirmed-commit` capability. 243 :param confirmed: whether this is a confirmed commit 244 :param check: Check correctness of syntax 245 :param timeout: specifies the confirm timeout in seconds 246 :param comment: Message to write to commit log 247 :param synchronize: Synchronize commit on remote peers 248 :param at_time: Time at which to activate configuration changes 249 :return: Received rpc response from remote host 250 """ 251 obj = new_ele("commit-configuration") 252 if confirmed: 253 sub_ele(obj, "confirmed") 254 if check: 255 sub_ele(obj, "check") 256 if synchronize: 257 sub_ele(obj, "synchronize") 258 if at_time: 259 subele = sub_ele(obj, "at-time") 260 subele.text = str(at_time) 261 if comment: 262 subele = sub_ele(obj, "log") 263 subele.text = str(comment) 264 if timeout: 265 subele = sub_ele(obj, "confirm-timeout") 266 subele.text = str(timeout) 267 return self.rpc(obj) 268