1# (c) 2018 Red Hat Inc. 2# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 4from __future__ import absolute_import, division, print_function 5 6__metaclass__ = type 7 8DOCUMENTATION = """author: Ansible Networking Team 9connection: httpapi 10short_description: Use httpapi to run command on network appliances 11description: 12- This connection plugin provides a connection to remote devices over a HTTP(S)-based 13 api. 14options: 15 host: 16 description: 17 - Specifies the remote device FQDN or IP address to establish the HTTP(S) connection 18 to. 19 default: inventory_hostname 20 vars: 21 - name: ansible_host 22 port: 23 type: int 24 description: 25 - Specifies the port on the remote device that listens for connections when establishing 26 the HTTP(S) connection. 27 - When unspecified, will pick 80 or 443 based on the value of use_ssl. 28 ini: 29 - section: defaults 30 key: remote_port 31 env: 32 - name: ANSIBLE_REMOTE_PORT 33 vars: 34 - name: ansible_httpapi_port 35 network_os: 36 description: 37 - Configures the device platform network operating system. This value is used 38 to load the correct httpapi plugin to communicate with the remote device 39 vars: 40 - name: ansible_network_os 41 remote_user: 42 description: 43 - The username used to authenticate to the remote device when the API connection 44 is first established. If the remote_user is not specified, the connection will 45 use the username of the logged in user. 46 - Can be configured from the CLI via the C(--user) or C(-u) options. 47 ini: 48 - section: defaults 49 key: remote_user 50 env: 51 - name: ANSIBLE_REMOTE_USER 52 vars: 53 - name: ansible_user 54 password: 55 description: 56 - Configures the user password used to authenticate to the remote device when 57 needed for the device API. 58 vars: 59 - name: ansible_password 60 - name: ansible_httpapi_pass 61 - name: ansible_httpapi_password 62 use_ssl: 63 type: boolean 64 description: 65 - Whether to connect using SSL (HTTPS) or not (HTTP). 66 default: false 67 vars: 68 - name: ansible_httpapi_use_ssl 69 validate_certs: 70 type: boolean 71 description: 72 - Whether to validate SSL certificates 73 default: true 74 vars: 75 - name: ansible_httpapi_validate_certs 76 use_proxy: 77 type: boolean 78 description: 79 - Whether to use https_proxy for requests. 80 default: true 81 vars: 82 - name: ansible_httpapi_use_proxy 83 become: 84 type: boolean 85 description: 86 - The become option will instruct the CLI session to attempt privilege escalation 87 on platforms that support it. Normally this means transitioning from user mode 88 to C(enable) mode in the CLI session. If become is set to True and the remote 89 device does not support privilege escalation or the privilege has already been 90 elevated, then this option is silently ignored. 91 - Can be configured from the CLI via the C(--become) or C(-b) options. 92 default: false 93 ini: 94 - section: privilege_escalation 95 key: become 96 env: 97 - name: ANSIBLE_BECOME 98 vars: 99 - name: ansible_become 100 become_method: 101 description: 102 - This option allows the become method to be specified in for handling privilege 103 escalation. Typically the become_method value is set to C(enable) but could 104 be defined as other values. 105 default: sudo 106 ini: 107 - section: privilege_escalation 108 key: become_method 109 env: 110 - name: ANSIBLE_BECOME_METHOD 111 vars: 112 - name: ansible_become_method 113 persistent_connect_timeout: 114 type: int 115 description: 116 - Configures, in seconds, the amount of time to wait when trying to initially 117 establish a persistent connection. If this value expires before the connection 118 to the remote device is completed, the connection will fail. 119 default: 30 120 ini: 121 - section: persistent_connection 122 key: connect_timeout 123 env: 124 - name: ANSIBLE_PERSISTENT_CONNECT_TIMEOUT 125 vars: 126 - name: ansible_connect_timeout 127 persistent_command_timeout: 128 type: int 129 description: 130 - Configures, in seconds, the amount of time to wait for a command to return from 131 the remote device. If this timer is exceeded before the command returns, the 132 connection plugin will raise an exception and close. 133 default: 30 134 ini: 135 - section: persistent_connection 136 key: command_timeout 137 env: 138 - name: ANSIBLE_PERSISTENT_COMMAND_TIMEOUT 139 vars: 140 - name: ansible_command_timeout 141 persistent_log_messages: 142 type: boolean 143 description: 144 - This flag will enable logging the command executed and response received from 145 target device in the ansible log file. For this option to work 'log_path' ansible 146 configuration option is required to be set to a file path with write access. 147 - Be sure to fully understand the security implications of enabling this option 148 as it could create a security vulnerability by logging sensitive information 149 in log file. 150 default: false 151 ini: 152 - section: persistent_connection 153 key: log_messages 154 env: 155 - name: ANSIBLE_PERSISTENT_LOG_MESSAGES 156 vars: 157 - name: ansible_persistent_log_messages 158""" 159 160from io import BytesIO 161 162from ansible.errors import AnsibleConnectionFailure 163from ansible.module_utils._text import to_bytes 164from ansible.module_utils.six import PY3 165from ansible.module_utils.six.moves import cPickle 166from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError 167from ansible.module_utils.urls import open_url 168from ansible.playbook.play_context import PlayContext 169from ansible.plugins.loader import httpapi_loader 170from ansible.plugins.connection import NetworkConnectionBase, ensure_connect 171 172 173class Connection(NetworkConnectionBase): 174 """Network API connection""" 175 176 transport = "ansible.netcommon.httpapi" 177 has_pipelining = True 178 179 def __init__(self, play_context, new_stdin, *args, **kwargs): 180 super(Connection, self).__init__( 181 play_context, new_stdin, *args, **kwargs 182 ) 183 184 self._url = None 185 self._auth = None 186 187 if self._network_os: 188 189 self.httpapi = httpapi_loader.get(self._network_os, self) 190 if self.httpapi: 191 self._sub_plugin = { 192 "type": "httpapi", 193 "name": self.httpapi._load_name, 194 "obj": self.httpapi, 195 } 196 self.queue_message( 197 "vvvv", 198 "loaded API plugin %s from path %s for network_os %s" 199 % ( 200 self.httpapi._load_name, 201 self.httpapi._original_path, 202 self._network_os, 203 ), 204 ) 205 else: 206 raise AnsibleConnectionFailure( 207 "unable to load API plugin for network_os %s" 208 % self._network_os 209 ) 210 211 else: 212 raise AnsibleConnectionFailure( 213 "Unable to automatically determine host network os. Please " 214 "manually configure ansible_network_os value for this host" 215 ) 216 self.queue_message("log", "network_os is set to %s" % self._network_os) 217 218 def update_play_context(self, pc_data): 219 """Updates the play context information for the connection""" 220 pc_data = to_bytes(pc_data) 221 if PY3: 222 pc_data = cPickle.loads(pc_data, encoding="bytes") 223 else: 224 pc_data = cPickle.loads(pc_data) 225 play_context = PlayContext() 226 play_context.deserialize(pc_data) 227 228 self.queue_message("vvvv", "updating play_context for connection") 229 if self._play_context.become ^ play_context.become: 230 self.set_become(play_context) 231 if play_context.become is True: 232 self.queue_message("vvvv", "authorizing connection") 233 else: 234 self.queue_message("vvvv", "deauthorizing connection") 235 236 self._play_context = play_context 237 238 def _connect(self): 239 if not self.connected: 240 protocol = "https" if self.get_option("use_ssl") else "http" 241 host = self.get_option("host") 242 port = self.get_option("port") or ( 243 443 if protocol == "https" else 80 244 ) 245 self._url = "%s://%s:%s" % (protocol, host, port) 246 247 self.queue_message( 248 "vvv", 249 "ESTABLISH HTTP(S) CONNECTFOR USER: %s TO %s" 250 % (self._play_context.remote_user, self._url), 251 ) 252 self.httpapi.set_become(self._play_context) 253 self._connected = True 254 255 self.httpapi.login( 256 self.get_option("remote_user"), self.get_option("password") 257 ) 258 259 def close(self): 260 """ 261 Close the active session to the device 262 """ 263 # only close the connection if its connected. 264 if self._connected: 265 self.queue_message("vvvv", "closing http(s) connection to device") 266 self.logout() 267 268 super(Connection, self).close() 269 270 @ensure_connect 271 def send(self, path, data, **kwargs): 272 """ 273 Sends the command to the device over api 274 """ 275 url_kwargs = dict( 276 timeout=self.get_option("persistent_command_timeout"), 277 validate_certs=self.get_option("validate_certs"), 278 use_proxy=self.get_option("use_proxy"), 279 headers={}, 280 ) 281 url_kwargs.update(kwargs) 282 if self._auth: 283 # Avoid modifying passed-in headers 284 headers = dict(kwargs.get("headers", {})) 285 headers.update(self._auth) 286 url_kwargs["headers"] = headers 287 else: 288 url_kwargs["force_basic_auth"] = True 289 url_kwargs["url_username"] = self.get_option("remote_user") 290 url_kwargs["url_password"] = self.get_option("password") 291 292 try: 293 url = self._url + path 294 self._log_messages( 295 "send url '%s' with data '%s' and kwargs '%s'" 296 % (url, data, url_kwargs) 297 ) 298 response = open_url(url, data=data, **url_kwargs) 299 except HTTPError as exc: 300 is_handled = self.handle_httperror(exc) 301 if is_handled is True: 302 return self.send(path, data, **kwargs) 303 elif is_handled is False: 304 raise 305 else: 306 response = is_handled 307 except URLError as exc: 308 raise AnsibleConnectionFailure( 309 "Could not connect to {0}: {1}".format( 310 self._url + path, exc.reason 311 ) 312 ) 313 314 response_buffer = BytesIO() 315 resp_data = response.read() 316 self._log_messages("received response: '%s'" % resp_data) 317 response_buffer.write(resp_data) 318 319 # Try to assign a new auth token if one is given 320 self._auth = self.update_auth(response, response_buffer) or self._auth 321 322 response_buffer.seek(0) 323 324 return response, response_buffer 325