ansible-collections / ansible.netcommon

Ansible Network Collection for Common Code
GNU General Public License v3.0
143 stars 102 forks source link

Logout function not being called from httpapi object context #145

Open Leovilhena opened 3 years ago

Leovilhena commented 3 years ago
SUMMARY

While developing an HttpApi plugin and instantiating HttpApiBase the logout function was never called.

According to the Ansible documentation the logout function, if implemented, will be called:

"Similarly, logout(self) can be implemented to call an endpoint to invalidate and/or release the current token, if such an endpoint exists. This will be automatically called when the connection is closed (and, by extension, when reset)."

From the code on line 280 the logout function is only being called from the connection object context.

The solution would be to check if "_netfwork_os" attribute is set, then it should called the "httpapi" object logout function as in:

if self._network_os:
    self.httpapi.logout()
ISSUE TYPE
COMPONENT NAME

httpapi.py connection plugin

ANSIBLE VERSION
ansible 2.9.6
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/leovilhena/.ansible/plugins/modu
les', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/leovilhena/.local/lib/python3.6/s
ite-packages/ansible
  executable location = /home/leovilhena/.local/bin/ansible
  python version = 3.6.9 (default, Nov  7 2019, 10:44:02) [GCC 8.3.0]
OS / ENVIRONMENT

Linux RHEL 8 and AWX

Qalthos commented 3 years ago

"Similarly, logout(self) can be implemented to call an endpoint to invalidate and/or release the current token, if such an endpoint exists. This will be automatically called when the connection is closed (and, by extension, when reset)."

From the code on line 280 the logout function is only being called from the connection object context.

Yes. You will also note that the connection plugin does not have a logout method. As such, the plugin's __getattr__ method is called to figure out what to do with this call. It looks for a logout method on the registered sub-plugin, and calls it if it finds one. This is how it presently works for all httpapi plugins even if they don't implement a logout function.

Can I get more information about what is going wrong? Is your only issue that logout isn't being called, or are there other errors being shown? How are you presenting the plugin to ansible (role, collection, playbook-adjacent directory, etc.)? I would be surprised if logout were not working, but the rest of the httpapi plugin were functional. If you can provide some or all of your plugin, that would also help figuring out what's going on.

Leovilhena commented 3 years ago

The logout function is never being called even after being implemented. The login function works as it should. We had to use a decorator to make sure that the logout is being called.

We tried to raise an error inside the logout function and even exit(1) to make sure that it was being called, but the Ansible job finished successfully, which means that the logout was never being called.


from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

import os
import json

from ansible.module_utils.basic import to_text
from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible.plugins.httpapi import HttpApiBase
from ansible.module_utils.connection import ConnectionError
from ansible.module_utils.urls import open_url

def logout_dec(send_request_function):
    def wrapper(self, *args, **kwargs):
        code, response_data = send_request_function(self, *args, **kwargs)
        if '/api/jwt/logout' not in kwargs.get('path'):
            self.logout()
        return code, response_data
    return wrapper

class HttpApi(HttpApiBase):
    def set_variables(self, **kwargs):
        os.environ.update(kwargs)

    def login(self, username, password):
        if not username or not password:
            raise AnsibleConnectionFailure('[Username and password] or api_key are required for login')

        data = 'username={}&password={}'.format(username, password).encode("ascii")
        url = '{}/api/jwt/login'.format(os.environ.get('helix_domain'))
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept-Encoding': 'application/json',
            'User-Agent': 'Ansible',
            'Content-Length': len(data),
        }
        try:
            with open_url(
                    url=url,
                    method='POST',
                    data=data,
                    use_proxy=True,
                    force=True,
                    headers=headers
            ) as response:
                code = response.getcode()
                response_data = response.read().decode('utf-8')

        except HTTPError as response:
            code = response.getcode()
            response_data = response.read()
        if code != 200 or not response_data:
            raise ConnectionError('Code: {} \n Response data: {}'.format(code, response_data))

        self.connection._auth = self.logout_headers = {'Authorization': 'AR-JWT {}'.format(response_data)}

    def logout(self):
        url = '/api/jwt/logout'
        response, dummy = self.send_request(path=url, data=None, method='POST', headers=self.logout_headers)

    @logout_dec
    def send_request(self, data, **message_kwargs):
        path = message_kwargs.get('path')
        method = message_kwargs.get('method', 'POST')
        headers = message_kwargs.get('headers', {
            'Content-Type': 'application/json',
            'User-Agent': 'Ansible',
            'Accept-Encoding': 'application/json',
        })

        data = json.dumps(data) if data else '{}'

        try:
            self._display_request()
            response, response_data = self.connection.send(path, data, method=method, headers=headers, use_proxy=True)
            value = self._get_response_value(response_data)
            return response.getcode(), self._response_to_json(value)
        except AnsibleConnectionFailure as e:
            return 418, e.message
        except HTTPError as e:
            error = json.loads(e.read())
            return e.code, error

    def _display_request(self, **message_kwargs):
        self.connection.queue_message('vvvv', 'Web Services: {} {}'.format(message_kwargs.get('method'), self.connection._url))

    def _get_response_value(self, response_data):
        return to_text(response_data.getvalue())

    def _response_to_json(self, response_text):
        try:
            return json.loads(response_text) if response_text else {}
        # JSONDecodeError only available on Python 3.5+
        except ValueError:
            raise ConnectionError('Invalid JSON response: {}'.format(response_text))```