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