diff --git a/lib/ansible/plugins/connection/httpapi.py b/lib/ansible/plugins/connection/httpapi.py index a029580d6d..5fae66b3ce 100644 --- a/lib/ansible/plugins/connection/httpapi.py +++ b/lib/ansible/plugins/connection/httpapi.py @@ -181,25 +181,17 @@ class Connection(ConnectionBase): self._local = connection_loader.get('local', play_context, '/dev/null') self._local.set_options() - self._cliconf = None + self._implementation_plugins = [] self._ansible_playbook_pid = kwargs.get('ansible_playbook_pid') - network_os = self._play_context.network_os - if not network_os: + self._network_os = self._play_context.network_os + if not self._network_os: raise AnsibleConnectionFailure( 'Unable to automatically determine host network os. Please ' 'manually configure ansible_network_os value for this host' ) - self._httpapi = httpapi_loader.get(network_os, self) - if self._httpapi: - if hasattr(self._httpapi, 'set_become'): - self._httpapi.set_become(play_context) - display.vvvv('loaded API plugin for network_os %s' % network_os, host=self._play_context.remote_addr) - else: - raise AnsibleConnectionFailure('unable to load API plugin for network_os %s' % network_os) - self._url = None self._auth = None @@ -211,7 +203,7 @@ class Connection(ConnectionBase): return self.__dict__[name] except KeyError: if not name.startswith('_'): - for plugin in (self._httpapi, self._cliconf): + for plugin in self._implementation_plugins: method = getattr(plugin, name, None) if method: return method @@ -244,22 +236,29 @@ class Connection(ConnectionBase): return messages def _connect(self): - if self.connected: - return - network_os = self._play_context.network_os + if not self.connected: + protocol = 'https' if self.get_option('use_ssl') else 'http' + host = self.get_option('host') + port = self.get_option('port') or (443 if protocol == 'https' else 80) + self._url = '%s://%s:%s' % (protocol, host, port) - protocol = 'https' if self.get_option('use_ssl') else 'http' - host = self.get_option('host') - port = self.get_option('port') or (443 if protocol == 'https' else 80) - self._url = '%s://%s:%s' % (protocol, host, port) + httpapi = httpapi_loader.get(self._network_os, self) + if httpapi: + httpapi.set_become(self._play_context) + httpapi.login(self.get_option('remote_user'), self.get_option('password')) + display.vvvv('loaded API plugin for network_os %s' % self._network_os, host=self._play_context.remote_addr) + else: + raise AnsibleConnectionFailure('unable to load API plugin for network_os %s' % self._network_os) + self._implementation_plugins.append(httpapi) - self._cliconf = cliconf_loader.get(network_os, self) - if self._cliconf: - display.vvvv('loaded cliconf plugin for network_os %s' % network_os, host=host) - else: - display.vvvv('unable to load cliconf for network_os %s' % network_os) + cliconf = cliconf_loader.get(self._network_os, self) + if cliconf: + display.vvvv('loaded cliconf plugin for network_os %s' % self._network_os, host=host) + self._implementation_plugins.append(cliconf) + else: + display.vvvv('unable to load cliconf for network_os %s' % self._network_os) - self._connected = True + self._connected = True def _update_connection_state(self): ''' @@ -294,6 +293,7 @@ class Connection(ConnectionBase): display.vvvv('reset call on connection instance', host=self._play_context.remote_addr) def close(self): + self._implementation_plugins = [] if self._connected: self._connected = False @@ -302,14 +302,23 @@ class Connection(ConnectionBase): Sends the command to the device over api ''' url_kwargs = dict( - url_username=self.get_option('remote_user'), url_password=self.get_option('password'), timeout=self.get_option('timeout'), validate_certs=self.get_option('validate_certs'), ) url_kwargs.update(kwargs) + if self._auth: + url_kwargs['headers']['Cookie'] = self._auth + else: + url_kwargs['url_username'] = self.get_option('remote_user') + url_kwargs['url_password'] = self.get_option('password') try: response = open_url(self._url + path, data=data, **url_kwargs) except URLError as exc: + if exc.reason == 'Unauthorized' and self._auth: + # Stored auth appears to be invalid, clear and retry + self._auth = None + self.login(self.get_option('remote_user'), self.get_option('password')) + return self.send(path, data, **kwargs) raise AnsibleConnectionFailure('Could not connect to {0}: {1}'.format(self._url, exc.reason)) self._auth = response.info().get('Set-Cookie') diff --git a/lib/ansible/plugins/httpapi/__init__.py b/lib/ansible/plugins/httpapi/__init__.py index e69de29bb2..d678b9bbbf 100644 --- a/lib/ansible/plugins/httpapi/__init__.py +++ b/lib/ansible/plugins/httpapi/__init__.py @@ -0,0 +1,34 @@ +# (c) 2018 Red Hat Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from abc import abstractmethod + +from ansible.plugins import AnsiblePlugin + + +class HttpApiBase(AnsiblePlugin): + def __init__(self, connection): + self.connection = connection + self._become = False + self._become_pass = '' + + def set_become(self, become_context): + self._become = become_context.become + self._become_pass = getattr(become_context, 'become_pass') or '' + + def login(self, username, password): + """Call a defined login endpoint to receive an authentication token. + + This should only be implemented if the API has a single endpoint which + can turn HTTP basic auth into a token which can be reused for the rest + of the calls for the session. + """ + pass + + @abstractmethod + def send_request(self, data, **message_kwargs): + """Prepares and sends request(s) to device.""" + pass diff --git a/lib/ansible/plugins/httpapi/eos.py b/lib/ansible/plugins/httpapi/eos.py index dc5d2059b6..5a699e23fc 100644 --- a/lib/ansible/plugins/httpapi/eos.py +++ b/lib/ansible/plugins/httpapi/eos.py @@ -8,8 +8,9 @@ import json import time from ansible.module_utils._text import to_text -from ansible.module_utils.network.common.utils import to_list from ansible.module_utils.connection import ConnectionError +from ansible.module_utils.network.common.utils import to_list +from ansible.plugins.httpapi import HttpApiBase try: from __main__ import display @@ -18,11 +19,7 @@ except ImportError: display = Display() -class HttpApi: - def __init__(self, connection): - self.connection = connection - self._become = False - +class HttpApi(HttpApiBase): def send_request(self, data, **message_kwargs): data = to_list(data) if self._become: @@ -39,6 +36,7 @@ class HttpApi: response = json.loads(response_text) except ValueError: raise ConnectionError('Response was not valid JSON, got {0}'.format(response_text)) + results = handle_response(response) if self._become: @@ -55,10 +53,6 @@ class HttpApi: else: return '>' - def set_become(self, play_context): - self._become = play_context.become - self._become_pass = getattr(play_context, 'become_pass') or '' - # Imported from module_utils def edit_config(self, config, commit=False, replace=False): """Loads the configuration onto the remote devices @@ -120,23 +114,24 @@ class HttpApi: return response for item in to_list(commands): - cmd_output = None + cmd_output = 'text' if isinstance(item, dict): command = item['command'] - if command.endswith('| json'): - command = command.replace('| json', '') - cmd_output = 'json' - elif 'output' in item: + if 'output' in item: cmd_output = item['output'] else: command = item + + # Emulate '| json' from CLI + if command.endswith('| json'): + command = command.rsplit('|', 1)[0] cmd_output = 'json' if output and output != cmd_output: responses.extend(run_queue(queue, output)) queue = list() - output = cmd_output or 'json' + output = cmd_output queue.append(command) if queue: diff --git a/lib/ansible/plugins/httpapi/nxos.py b/lib/ansible/plugins/httpapi/nxos.py index 2aee30ccfb..4d03d7725d 100644 --- a/lib/ansible/plugins/httpapi/nxos.py +++ b/lib/ansible/plugins/httpapi/nxos.py @@ -9,6 +9,7 @@ import json from ansible.module_utils._text import to_text from ansible.module_utils.connection import ConnectionError from ansible.module_utils.network.common.utils import to_list +from ansible.plugins.httpapi import HttpApiBase try: from __main__ import display @@ -17,14 +18,12 @@ except ImportError: display = Display() -class HttpApi: - def __init__(self, connection): - self.connection = connection - +class HttpApi(HttpApiBase): def _run_queue(self, queue, output): if self._become: display.vvvv('firing event: on_become') queue.insert(0, 'enable') + request = request_builder(queue, output) headers = {'Content-Type': 'application/json'} @@ -74,10 +73,6 @@ class HttpApi: return responses[0] return responses - def set_become(self, play_context): - self._become = play_context.become - self._become_pass = getattr(play_context, 'become_pass') or '' - # Migrated from module_utils def edit_config(self, command): resp = list()