Beispiel #1
0
    def main(args):
        # Retrieve the intersphinx information from the sphinx config file
        conf_dir = pathlib.Path(args.conf_file).parent

        conf_module_spec = importlib.util.spec_from_file_location(
            'sphinxconf', args.conf_file)
        conf_module = importlib.util.module_from_spec(conf_module_spec)
        conf_module_spec.loader.exec_module(conf_module)
        intersphinx_mapping = conf_module.intersphinx_mapping

        for intersphinx_name, inventory in intersphinx_mapping.items():
            if not is_iterable(inventory) or len(inventory) != 2:
                print('WARNING: The intersphinx entry for {0} must be'
                      ' a two-tuple.\n{1}'.format(intersphinx_name,
                                                  EXAMPLE_CONF))
                continue

            url = cache_file = None
            for inv_source in inventory:
                if isinstance(inv_source, str) and url is None:
                    url = inv_source
                elif is_iterable(inv_source) and cache_file is None:
                    if len(inv_source) != 2:
                        print(
                            'WARNING: The fallback entry for {0} should be a tuple of (None,'
                            ' filename).\n{1}'.format(intersphinx_name,
                                                      EXAMPLE_CONF))
                        continue
                    cache_file = inv_source[1]
                else:
                    print(
                        'WARNING: The configuration for {0} should be a tuple of one url and one'
                        ' tuple for a fallback filename.\n{1}'.format(
                            intersphinx_name, EXAMPLE_CONF))
                    continue

            if url is None or cache_file is None:
                print('WARNING: Could not figure out the url or fallback'
                      ' filename for {0}.\n{1}'.format(intersphinx_name,
                                                       EXAMPLE_CONF))
                continue

            url = urllib.parse.urljoin(url, 'objects.inv')
            # Resolve any relative cache files to be relative to the conf file
            cache_file = conf_dir / cache_file

            # Retrieve the inventory and cache it
            # The jinja CDN seems to be blocking the default urllib User-Agent
            requestor = Request(
                headers={'User-Agent': 'Definitely Not Python ;-)'})
            with requestor.open('GET', url) as source_file:
                with open(cache_file, 'wb') as f:
                    f.write(source_file.read())

        print(
            'Download of new cache files complete.  Remember to git commit -a the changes'
        )

        return 0
Beispiel #2
0
def test_Request_fallback(urlopen_mock, install_opener_mock, mocker):
    cookies = cookiejar.CookieJar()
    request = Request(
        headers={'foo': 'bar'},
        use_proxy=False,
        force=True,
        timeout=100,
        validate_certs=False,
        url_username='******',
        url_password='******',
        http_agent='ansible-tests',
        force_basic_auth=True,
        follow_redirects='all',
        client_cert='/tmp/client.pem',
        client_key='/tmp/client.key',
        cookies=cookies,
        unix_socket='/foo/bar/baz.sock',
        ca_path='/foo/bar/baz.pem',
    )
    fallback_mock = mocker.spy(request, '_fallback')

    r = request.open('GET', 'https://ansible.com')

    calls = [
        call(None, False),  # use_proxy
        call(None, True),  # force
        call(None, 100),  # timeout
        call(None, False),  # validate_certs
        call(None, 'user'),  # url_username
        call(None, 'passwd'),  # url_password
        call(None, 'ansible-tests'),  # http_agent
        call(None, True),  # force_basic_auth
        call(None, 'all'),  # follow_redirects
        call(None, '/tmp/client.pem'),  # client_cert
        call(None, '/tmp/client.key'),  # client_key
        call(None, cookies),  # cookies
        call(None, '/foo/bar/baz.sock'),  # unix_socket
        call(None, '/foo/bar/baz.pem'),  # ca_path
        call(None, None),  # unredirected_headers
        call(None, True),  # auto_decompress
    ]
    fallback_mock.assert_has_calls(calls)

    assert fallback_mock.call_count == 16  # All but headers use fallback

    args = urlopen_mock.call_args[0]
    assert args[1] is None  # data, this is handled in the Request not urlopen
    assert args[2] == 100  # timeout

    req = args[0]
    assert req.headers == {
        'Authorization': b'Basic dXNlcjpwYXNzd2Q=',
        'Cache-control': 'no-cache',
        'Foo': 'bar',
        'User-agent': 'ansible-tests'
    }
    assert req.data is None
    assert req.get_method() == 'GET'
Beispiel #3
0
class Connection:
    def __init__(self, address):
        self._address = address.rstrip("/")
        self._headers = {}
        self._client = Request()

    def _request(self, method, path, payload=None):
        headers = self._headers.copy()
        data = None
        if payload:
            data = json.dumps(payload)
            headers["Content-Type"] = "application/json"

        url = self._address + path
        try:
            r = self._client.open(method, url, data=data, headers=headers)
            r_status = r.getcode()
            r_headers = dict(r.headers)
            data = r.read().decode("utf-8")
            r_data = json.loads(data) if data else {}
        except HTTPError as e:
            r_status = e.code
            r_headers = {}
            r_data = dict(msg=str(e.reason))
        except (ConnectionError, URLError) as e:
            raise AnsibleConnectionFailure(
                "Could not connect to {0}: {1}".format(url, e.reason))
        return r_status, r_headers, r_data

    def get(self, path):
        return self._request("GET", path)

    def post(self, path, payload=None):
        return self._request("POST", path, payload)

    def delete(self, path):
        return self._request("DELETE", path)

    def login(self, username, password):
        status, headers, _ = self.post(
            "/tokens",
            dict(username=username, password=password),
        )
        self._headers["x-auth-token"] = headers["x-auth-token"]

    def logout(self):
        if "x-auth-token" in self._headers:
            self.delete("/tokens/" + self._headers["x-auth-token"])
            del self._headers["x-auth-token"]
Beispiel #4
0
class GalaxyModule(AnsibleModule):
    url = None
    session = None
    AUTH_ARGSPEC = dict(
        galaxy_server=dict(required=False, fallback=(env_fallback, ['GALAXY_SERVER'])),
        validate_certs=dict(type='bool', aliases=['galaxy_verify_ssl'], required=False, fallback=(env_fallback, ['GALAXY_VERIFY_SSL'])),
        galaxy_token=dict(type='str', no_log=True, required=False, fallback=(env_fallback, ['GALAXY_API_TOKEN'])),
    )
    ENCRYPTED_STRING = "$encrypted$"
    short_params = {
        'host': 'galaxy_server',
        'verify_ssl': 'validate_certs',
        'oauth_token': 'galaxy_token',
    }
    IDENTITY_FIELDS = {
    }
    ENCRYPTED_STRING = "$encrypted$"
    host = '127.0.0.1'
    verify_ssl = True
    oauth_token = None
    error_callback = None
    warn_callback = None

    def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, **kwargs):
        full_argspec = {}
        full_argspec.update(GalaxyModule.AUTH_ARGSPEC)
        full_argspec.update(argument_spec)
        kwargs['supports_check_mode'] = True

        self.error_callback = error_callback
        self.warn_callback = warn_callback

        self.json_output = {'changed': False}

        if direct_params is not None:
            self.params = direct_params
#        else:
        super(GalaxyModule, self).__init__(argument_spec=full_argspec, **kwargs)
        self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl)

        # Parameters specified on command line will override settings in any config
        for short_param, long_param in self.short_params.items():
            direct_value = self.params.get(long_param)
            if direct_value is not None:
                setattr(self, short_param, direct_value)

        # Perform magic checking whether galaxy_token is a string
        if self.params.get('galaxy_token'):
            token_param = self.params.get('galaxy_token')
            if isinstance(token_param, string_types):
                self.oauth_token = self.params.get('galaxy_token')
            else:
                error_msg = "The provided galaxy_token type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__)
                self.fail_json(msg=error_msg)

        # Perform some basic validation
        if not re.match('^https{0,1}://', self.host):
            self.host = "https://{0}".format(self.host)

        # Try to parse the hostname as a url
        try:
            self.url = urlparse(self.host)
        except Exception as e:
            self.fail_json(msg="Unable to parse galaxy host as a URL ({1}): {0}".format(self.host, e))

        # Try to resolve the hostname
        hostname = self.url.netloc.split(':')[0]
        try:
            gethostbyname(hostname)
        except Exception as e:
            self.fail_json(msg="Unable to resolve galaxy host ({1}): {0}".format(hostname, e))

        if 'update_secrets' in self.params:
            self.update_secrets = self.params.pop('update_secrets')
        else:
            self.update_secrets = True

    def build_url(self, endpoint, query_params=None):
        # Make sure we start with /api/vX
        if not endpoint.startswith("/"):
            endpoint = "/{0}".format(endpoint)
        if not endpoint.startswith("/api/"):
            endpoint = "api/automation-hub/v3{0}".format(endpoint)
        if not endpoint.endswith('/') and '?' not in endpoint:
            endpoint = "{0}/".format(endpoint)

        # Update the URL path with the endpoint
        url = self.url._replace(path=endpoint)

        if query_params:
            url = url._replace(query=urlencode(query_params))

        return url

    def fail_json(self, **kwargs):
        # Try to log out if we are authenticated
        if self.error_callback:
            self.error_callback(**kwargs)
        else:
            super(GalaxyModule, self).fail_json(**kwargs)

    def exit_json(self, **kwargs):
        # Try to log out if we are authenticated
        super(GalaxyModule, self).exit_json(**kwargs)

    def warn(self, warning):
        if self.warn_callback is not None:
            self.warn_callback(warning)
        else:
            super(GalaxyModule, self).warn(warning)

    @staticmethod
    def get_name_field_from_endpoint(endpoint):
        return GalaxyModule.IDENTITY_FIELDS.get(endpoint, 'name')

    def get_endpoint(self, endpoint, *args, **kwargs):
        return self.make_request('GET', endpoint, **kwargs)

    def make_request(self, method, endpoint, *args, **kwargs):
        # In case someone is calling us directly; make sure we were given a method, let's not just assume a GET
        if not method:
            raise Exception("The HTTP method must be defined")

        if method in ['POST', 'PUT', 'PATCH']:
            url = self.build_url(endpoint)
        else:
            url = self.build_url(endpoint, query_params=kwargs.get('data'))

        # Extract the headers, this will be used in a couple of places
        headers = kwargs.get('headers', {})

        # Authenticate to Automation Hub
        if self.oauth_token:
            # If we have a oauth token, we just use a token header
            headers['Authorization'] = 'token {0}'.format(self.oauth_token)

        if method in ['POST', 'PUT', 'PATCH']:
            headers.setdefault('Content-Type', 'application/json')
            kwargs['headers'] = headers

        data = None  # Important, if content type is not JSON, this should not be dict type
        if headers.get('Content-Type', '') == 'application/json':
            data = dumps(kwargs.get('data', {}))

        try:
            response = self.session.open(method, url.geturl(), headers=headers, validate_certs=self.verify_ssl, follow_redirects=True, data=data)
        except(SSLValidationError) as ssl_err:
            self.fail_json(msg="Could not establish a secure connection to your host ({1}): {0}.".format(url.netloc, ssl_err))
        except(ConnectionError) as con_err:
            self.fail_json(msg="There was a network error of some kind trying to connect to your host ({1}): {0}.".format(url.netloc, con_err))
        except(HTTPError) as he:
            # Sanity check: Did the server send back some kind of internal error?
            if he.code >= 500:
                self.fail_json(msg='The host sent back a server error ({1}): {0}. Please check the logs and try again later'.format(url.path, he))
            # Sanity check: Did we fail to authenticate properly?  If so, fail out now; this is always a failure.
            elif he.code == 401:
                self.fail_json(msg='Invalid Automation Hub authentication credentials for {0} (HTTP 401).'.format(url.path))
            # Sanity check: Did we get a forbidden response, which means that the user isn't allowed to do this? Report that.
            elif he.code == 403:
                self.fail_json(msg="You don't have permission to {1} to {0} (HTTP 403).".format(url.path, method))
            # Sanity check: Did we get a 404 response?
            # Requests with primary keys will return a 404 if there is no response, and we want to consistently trap these.
            elif he.code == 404:
                if kwargs.get('return_none_on_404', False):
                    return None
                self.fail_json(msg='The requested object could not be found at {0}.'.format(url.path))
            # Sanity check: Did we get a 405 response?
            # A 405 means we used a method that isn't allowed. Usually this is a bad request, but it requires special treatment because the
            # API sends it as a logic error in a few situations (e.g. trying to cancel a job that isn't running).
            elif he.code == 405:
                self.fail_json(msg="The Automation Hub server says you can't make a request with the {0} method to this endpoing {1}".format(method, url.path))
            # Sanity check: Did we get some other kind of error?  If so, write an appropriate error message.
            elif he.code >= 400:
                # We are going to return a 400 so the module can decide what to do with it
                page_data = he.read()
                try:
                    return {'status_code': he.code, 'json': loads(page_data)}
                # JSONDecodeError only available on Python 3.5+
                except ValueError:
                    return {'status_code': he.code, 'text': page_data}
            elif he.code == 204 and method == 'DELETE':
                # A 204 is a normal response for a delete function
                pass
            else:
                self.fail_json(msg="Unexpected return code when calling {0}: {1}".format(url.geturl(), he))
        except(Exception) as e:
            self.fail_json(msg="There was an unknown error when trying to connect to {2}: {0} {1}".format(type(e).__name__, e, url.geturl()))

        response_body = ''
        try:
            response_body = response.read()
        except(Exception) as e:
            self.fail_json(msg="Failed to read response body: {0}".format(e))

        response_json = {}
        if response_body and response_body != '':
            try:
                response_json = loads(response_body)
            except(Exception) as e:
                self.fail_json(msg="Failed to parse the response json: {0}".format(e))

        if PY2:
            status_code = response.getcode()
        else:
            status_code = response.status
        return {'status_code': status_code, 'json': response_json}

    def get_one(self, endpoint, name_or_id=None, allow_none=True, **kwargs):
        new_kwargs = kwargs.copy()
        if name_or_id:
            name_field = self.get_name_field_from_endpoint(endpoint)
            new_data = kwargs.get('data', {}).copy()
            if name_field in new_data:
                self.fail_json(msg="You can't specify the field {0} in your search data if using the name_or_id field".format(name_field))

            try:
                new_data['or__id'] = int(name_or_id)
                new_data['or__{0}'.format(name_field)] = name_or_id
            except ValueError:
                # If we get a value error, then we didn't have an integer so we can just pass and fall down to the fail
                new_data[name_field] = name_or_id
            new_kwargs['data'] = new_data

        response = self.get_endpoint(endpoint, **new_kwargs)
        if response['status_code'] != 200:
            fail_msg = "Got a {0} response when trying to get one from {1}".format(response['status_code'], endpoint)
            if 'detail' in response.get('json', {}):
                fail_msg += ', detail: {0}'.format(response['json']['detail'])
            self.fail_json(msg=fail_msg)

        if 'count' not in response['json']['meta'] or 'data' not in response['json']:
            self.fail_json(msg="The endpoint did not provide count and results.")

        if response['json']['meta']['count'] == 0:
            if allow_none:
                return None
            else:
                self.fail_wanted_one(response, endpoint, new_kwargs.get('data'))
        elif response['json']['meta']['count'] > 1:
            if name_or_id:
                # Since we did a name or ID search and got > 1 return something if the id matches
                for asset in response['json']['data']:
                    if str(asset['id']) == name_or_id:
                        return self.existing_item_add_url(asset, endpoint)

            # We got > 1 and either didn't find something by ID (which means multiple names)
            # Or we weren't running with a or search and just got back too many to begin with.
            self.fail_wanted_one(response, endpoint, new_kwargs.get('data'))

        return self.existing_item_add_url(response['json']['data'][0], endpoint)

    def existing_item_add_url(self, existing_item, endpoint):
        # Add url and type to response as its missing in current iteration of Automation Hub.
        existing_item['url'] = "{0}{1}/".format(self.build_url(endpoint).geturl()[len(self.host):], existing_item['name'])
        existing_item['type'] = endpoint
        return existing_item

    def delete_if_needed(self, existing_item, on_delete=None, auto_exit=True):
        # This will exit from the module on its own.
        # If the method successfully deletes an item and on_delete param is defined,
        #   the on_delete parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #   1. None if the existing_item is not defined (so no delete needs to happen)
        #   2. The response from Tower from calling the delete on the endpont. It's up to you to process the response and exit from the module
        # Note: common error codes from the Tower API can cause the module to fail
        if existing_item:
            # If we have an item, we can try to delete it
            try:
                item_url = existing_item['url']
                item_type = existing_item['type']
                item_id = existing_item['id']
                item_name = self.get_item_name(existing_item, allow_unknown=True)
            except KeyError as ke:
                self.fail_json(msg="Unable to process delete of item due to missing data {0}".format(ke))

            response = self.delete_endpoint(item_url)

            if response['status_code'] in [202, 204]:
                if on_delete:
                    on_delete(self, response['json'])
                self.json_output['changed'] = True
                self.json_output['id'] = item_id
                self.exit_json(**self.json_output)
                if auto_exit:
                    self.exit_json(**self.json_output)
                else:
                    return self.json_output
            else:
                if 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0]))
                elif 'json' in response:
                    # This is from a project delete (if there is an active job against it)
                    if 'error' in response['json']:
                        self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['json']['error']))
                    else:
                        self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['json']))
                else:
                    self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code']))
        else:
            if auto_exit:
                self.exit_json(**self.json_output)
            else:
                return self.json_output

    def get_item_name(self, item, allow_unknown=False):
        if item:
            if 'name' in item:
                return item['name']

        if allow_unknown:
            return 'unknown'

        if item:
            self.exit_json(msg='Cannot determine identity field for {0} object.'.format(item.get('type', 'unknown')))
        else:
            self.exit_json(msg='Cannot determine identity field for Undefined object.')

    def delete_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('DELETE', endpoint, **kwargs)

    def create_or_update_if_needed(
        self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, auto_exit=True, associations=None
    ):
        if existing_item:
            return self.update_if_needed(existing_item, new_item, on_update=on_update, auto_exit=auto_exit, associations=associations)
        else:
            return self.create_if_needed(
                existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, auto_exit=auto_exit, associations=associations)

    def create_if_needed(self, existing_item, new_item, endpoint, on_create=None, auto_exit=True, item_type='unknown', associations=None):

        # This will exit from the module on its own
        # If the method successfully creates an item and on_create param is defined,
        #    the on_create parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #    1. None if the existing_item is already defined (so no create needs to happen)
        #    2. The response from Tower from calling the patch on the endpont. It's up to you to process the response and exit from the module
        # Note: common error codes from the Galaxy API can cause the module to fail

        if not endpoint:
            self.fail_json(msg="Unable to create new {0} due to missing endpoint".format(item_type))

        item_url = None
        if existing_item:
            try:
                item_url = existing_item['url']
            except KeyError as ke:
                self.fail_json(msg="Unable to process create of item due to missing data {0}".format(ke))
        else:
            # If we don't have an exisitng_item, we can try to create it

            # We have to rely on item_type being passed in since we don't have an existing item that declares its type
            # We will pull the item_name out from the new_item, if it exists
            item_name = self.get_item_name(new_item, allow_unknown=True)

            response = self.post_endpoint(endpoint, **{'data': new_item})

            if response['status_code'] in [201]:
                self.json_output['name'] = 'unknown'
                for key in ('name', 'username', 'identifier', 'hostname'):
                    if key in response['json']:
                        self.json_output['name'] = response['json'][key]
                self.json_output['id'] = response['json']['id']
                self.json_output['changed'] = True
                item_url = "{0}{1}/".format(self.build_url(endpoint).geturl()[len(self.host):], new_item['name'])
            else:
                if 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0]))
                elif 'json' in response:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json']))
                else:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['status_code']))

        # Process any associations with this item
        if associations is not None:
            for association_type in associations:
                sub_endpoint = '{0}{1}/'.format(item_url, association_type)
                self.modify_associations(sub_endpoint, associations[association_type])

        # If we have an on_create method and we actually changed something we can call on_create
        if on_create is not None and self.json_output['changed']:
            on_create(self, response['json'])
        elif auto_exit:
            self.exit_json(**self.json_output)
        else:
            last_data = response['json']
            return last_data

    def update_if_needed(self, existing_item, new_item, on_update=None, auto_exit=True, associations=None):
        # This will exit from the module on its own
        # If the method successfully updates an item and on_update param is defined,
        #   the on_update parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #    1. None if the existing_item does not need to be updated
        #    2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module.
        # Note: common error codes from the Tower API can cause the module to fail
        response = None
        if existing_item:
            # If we have an item, we can see if it needs an update
            try:
                item_url = existing_item['url']
                item_type = existing_item['type']
                item_name = existing_item['name']
                item_id = existing_item['id']
            except KeyError as ke:
                self.fail_json(msg="Unable to process update of item due to missing data {0}".format(ke))

            # Check to see if anything within the item requires the item to be updated
            needs_patch = self.objects_could_be_different(existing_item, new_item)

            # If we decided the item needs to be updated, update it
            self.json_output['id'] = item_id
            self.json_output['name'] = item_name
            self.json_output['type'] = item_type
            if needs_patch:
                response = self.put_endpoint(item_url, **{'data': new_item})
                if response['status_code'] == 200:
                    # compare apples-to-apples, old API data to new API data
                    # but do so considering the fields given in parameters
                    self.json_output['changed'] = self.objects_could_be_different(
                        existing_item, response['json'], field_set=new_item.keys(), warning=True)
                elif 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg=response['json']['__all__'])
                else:
                    self.fail_json(**{'msg': "Unable to update {0} {1}, see response".format(item_type, item_name), 'response': response})

        else:
            raise RuntimeError('update_if_needed called incorrectly without existing_item')

        # Process any associations with this item
        if associations is not None:
            for association_type, id_list in associations.items():
                endpoint = '{0}{1}/'.format(item_url, association_type)
                self.modify_associations(endpoint, id_list)

        # If we change something and have an on_change call it
        if on_update is not None and self.json_output['changed']:
            if response is None:
                last_data = existing_item
            else:
                last_data = response['json']
            on_update(self, last_data)
        elif auto_exit:
            self.exit_json(**self.json_output)
        else:
            if response is None:
                last_data = existing_item
            else:
                last_data = response['json']
            return last_data

    def modify_associations(self, association_endpoint, new_association_list):
        # if we got None instead of [] we are not modifying the association_list
        if new_association_list is None:
            return

        # First get the existing associations
        response = self.get_all_endpoint(association_endpoint)
        existing_associated_ids = [association['id'] for association in response['json']['results']]

        # Disassociate anything that is in existing_associated_ids but not in new_association_list
        ids_to_remove = list(set(existing_associated_ids) - set(new_association_list))
        for an_id in ids_to_remove:
            response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id), 'disassociate': True}})
            if response['status_code'] == 204:
                self.json_output['changed'] = True
            else:
                self.fail_json(msg="Failed to disassociate item {0}".format(response['json'].get('detail', response['json'])))

        # Associate anything that is in new_association_list but not in `association`
        for an_id in list(set(new_association_list) - set(existing_associated_ids)):
            response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id)}})
            if response['status_code'] == 204:
                self.json_output['changed'] = True
            else:
                self.fail_json(msg="Failed to associate item {0}".format(response['json'].get('detail', response['json'])))

    def post_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('POST', endpoint, **kwargs)

    def patch_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('PATCH', endpoint, **kwargs)

    def put_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('PUT', endpoint, **kwargs)

    def get_all_endpoint(self, endpoint, *args, **kwargs):
        response = self.get_endpoint(endpoint, *args, **kwargs)
        if 'next' not in response['json']:
            raise RuntimeError('Expected list from API at {0}, got: {1}'.format(endpoint, response))
        next_page = response['json']['next']

        if response['json']['count'] > 10000:
            self.fail_json(msg='The number of items being queried for is higher than 10,000.')

        while next_page is not None:
            next_response = self.get_endpoint(next_page)
            response['json']['results'] = response['json']['results'] + next_response['json']['results']
            next_page = next_response['json']['next']
            response['json']['next'] = next_page
        return response

    def fail_wanted_one(self, response, endpoint, query_params):
        sample = response.copy()
        if len(sample['json']['data']) > 1:
            sample['json']['data'] = sample['json']['data'][:2] + ['...more results snipped...']
        url = self.build_url(endpoint, query_params)
        display_endpoint = url.geturl()[len(self.host):]  # truncate to not include the base URL
        self.fail_json(
            msg="Request to {0} returned {1} items, expected 1".format(
                display_endpoint, response['json']['meta']['count']
            ),
            query=query_params,
            response=sample,
            total_results=response['json']['meta']['count']
        )

    def objects_could_be_different(self, old, new, field_set=None, warning=False):
        if field_set is None:
            field_set = set(fd for fd in new.keys() if fd not in ('modified', 'related', 'summary_fields'))
        for field in field_set:
            new_field = new.get(field, None)
            old_field = old.get(field, None)
            if old_field != new_field:
                if self.update_secrets or (not self.fields_could_be_same(old_field, new_field)):
                    return True  # Something doesn't match, or something might not match
            elif self.has_encrypted_values(new_field) or field not in new:
                if self.update_secrets or (not self.fields_could_be_same(old_field, new_field)):
                    # case of 'field not in new' - user password write-only field that API will not display
                    self._encrypted_changed_warning(field, old, warning=warning)
                    return True
        return False

    @staticmethod
    def has_encrypted_values(obj):
        """Returns True if JSON-like python content in obj has $encrypted$
        anywhere in the data as a value
        """
        if isinstance(obj, dict):
            for val in obj.values():
                if GalaxyModule.has_encrypted_values(val):
                    return True
        elif isinstance(obj, list):
            for val in obj:
                if GalaxyModule.has_encrypted_values(val):
                    return True
        elif obj == GalaxyModule.ENCRYPTED_STRING:
            return True
        return False

    def _encrypted_changed_warning(self, field, old, warning=False):
        if not warning:
            return
        self.warn(
            'The field {0} of {1} {2} has encrypted data and may inaccurately report task is changed.'.format(
                field, old.get('type', 'unknown'), old.get('id', 'unknown')
            ))
Beispiel #5
0
class iControlRestSession(object):
    """Represents a session that communicates with a BigIP.

    This acts as a loose wrapper around Ansible's ``Request`` class. We're doing
    this as interim work until we move to the httpapi connector.
    """
    def __init__(self,
                 headers=None,
                 use_proxy=True,
                 force=False,
                 timeout=120,
                 validate_certs=True,
                 url_username=None,
                 url_password=None,
                 http_agent=None,
                 force_basic_auth=False,
                 follow_redirects='urllib2',
                 client_cert=None,
                 client_key=None,
                 cookies=None):
        self.request = Request(headers=headers,
                               use_proxy=use_proxy,
                               force=force,
                               timeout=timeout,
                               validate_certs=validate_certs,
                               url_username=url_username,
                               url_password=url_password,
                               http_agent=http_agent,
                               force_basic_auth=force_basic_auth,
                               follow_redirects=follow_redirects,
                               client_cert=client_cert,
                               client_key=client_key,
                               cookies=cookies)
        self.last_url = None

    def get_headers(self, result):
        try:
            return dict(result.getheaders())
        except AttributeError:
            return result.headers

    def update_response(self, response, result):
        response.headers = self.get_headers(result)
        response._content = result.read()
        response.status = result.getcode()
        response.url = result.geturl()
        response.msg = "OK (%s bytes)" % response.headers.get(
            'Content-Length', 'unknown')

    def send(self, method, url, **kwargs):
        response = Response()

        # Set the last_url called
        #
        # This is used by the object destructor to erase the token when the
        # ModuleManager exits and destroys the iControlRestSession object
        self.last_url = url

        body = None
        data = kwargs.pop('data', None)
        json = kwargs.pop('json', None)

        if not data and json is not None:
            self.request.headers['Content-Type'] = 'application/json'
            body = _json.dumps(json)
            if not isinstance(body, bytes):
                body = body.encode('utf-8')
        if data:
            body = data
        if body:
            kwargs['data'] = body

        try:
            result = self.request.open(method, url, **kwargs)
        except HTTPError as e:
            # Catch HTTPError delivered from Ansible
            #
            # The structure of this object, in Ansible 2.8 is
            #
            # HttpError {
            #   args
            #   characters_written
            #   close
            #   code
            #   delete
            #   errno
            #   file
            #   filename
            #   filename2
            #   fp
            #   getcode
            #   geturl
            #   hdrs
            #   headers
            #   info
            #   msg
            #   name
            #   reason
            #   strerror
            #   url
            #   with_traceback
            # }
            self.update_response(response, e)
            return response

        self.update_response(response, result)
        return response

    def delete(self, url, **kwargs):
        return self.send('DELETE', url, **kwargs)

    def get(self, url, **kwargs):
        return self.send('GET', url, **kwargs)

    def patch(self, url, data=None, **kwargs):
        return self.send('PATCH', url, data=data, **kwargs)

    def post(self, url, data=None, **kwargs):
        return self.send('POST', url, data=data, **kwargs)

    def put(self, url, data=None, **kwargs):
        return self.send('PUT', url, data=data, **kwargs)

    def __del__(self):
        if self.last_url is None:
            return
        token = self.request.headers.get('X-F5-Auth-Token', None)
        if not token:
            return
        try:
            p = generic_urlparse(urlparse(self.last_url))
            uri = "https://{0}:{1}/mgmt/shared/authz/tokens/{2}".format(
                p['hostname'], p['port'], token)
            self.delete(uri)
        except ValueError:
            pass
Beispiel #6
0
class TowerAPIModule(TowerModule):
    # TODO: Move the collection version check into tower_module.py
    # This gets set by the make process so whatever is in here is irrelevant
    _COLLECTION_VERSION = "0.0.1-devel"
    _COLLECTION_TYPE = "awx"
    # This maps the collections type (awx/tower) to the values returned by the API
    # Those values can be found in awx/api/generics.py line 204
    collection_to_version = {
        'awx': 'AWX',
        'tower': 'Red Hat Ansible Tower',
    }
    session = None
    cookie_jar = CookieJar()

    def __init__(self,
                 argument_spec,
                 direct_params=None,
                 error_callback=None,
                 warn_callback=None,
                 **kwargs):
        kwargs['supports_check_mode'] = True

        super(TowerAPIModule, self).__init__(argument_spec=argument_spec,
                                             direct_params=direct_params,
                                             error_callback=error_callback,
                                             warn_callback=warn_callback,
                                             **kwargs)
        self.session = Request(cookies=CookieJar(),
                               validate_certs=self.verify_ssl)

    @staticmethod
    def param_to_endpoint(name):
        exceptions = {
            'inventory': 'inventories',
            'target_team': 'teams',
            'workflow': 'workflow_job_templates'
        }
        return exceptions.get(name, '{0}s'.format(name))

    def head_endpoint(self, endpoint, *args, **kwargs):
        return self.make_request('HEAD', endpoint, **kwargs)

    def get_endpoint(self, endpoint, *args, **kwargs):
        return self.make_request('GET', endpoint, **kwargs)

    def patch_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('PATCH', endpoint, **kwargs)

    def post_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('POST', endpoint, **kwargs)

    def delete_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('DELETE', endpoint, **kwargs)

    def get_all_endpoint(self, endpoint, *args, **kwargs):
        response = self.get_endpoint(endpoint, *args, **kwargs)
        if 'next' not in response['json']:
            raise RuntimeError(
                'Expected list from API at {0}, got: {1}'.format(
                    endpoint, response))
        next_page = response['json']['next']

        if response['json']['count'] > 10000:
            self.fail_json(
                msg=
                'The number of items being queried for is higher than 10,000.')

        while next_page is not None:
            next_response = self.get_endpoint(next_page)
            response['json']['results'] = response['json'][
                'results'] + next_response['json']['results']
            next_page = next_response['json']['next']
            response['json']['next'] = next_page
        return response

    def get_one(self, endpoint, *args, **kwargs):
        response = self.get_endpoint(endpoint, *args, **kwargs)
        if response['status_code'] != 200:
            fail_msg = "Got a {0} response when trying to get one from {1}".format(
                response['status_code'], endpoint)
            if 'detail' in response.get('json', {}):
                fail_msg += ', detail: {0}'.format(response['json']['detail'])
            self.fail_json(msg=fail_msg)

        if 'count' not in response['json'] or 'results' not in response['json']:
            self.fail_json(
                msg="The endpoint did not provide count and results")

        if response['json']['count'] == 0:
            return None
        elif response['json']['count'] > 1:
            self.fail_json(
                msg=
                "An unexpected number of items was returned from the API ({0})"
                .format(response['json']['count']))

        return response['json']['results'][0]

    def get_one_by_name_or_id(self, endpoint, name_or_id):
        name_field = 'name'
        if endpoint == 'users':
            name_field = 'username'

        query_params = {'or__{0}'.format(name_field): name_or_id}
        try:
            query_params['or__id'] = int(name_or_id)
        except ValueError:
            # If we get a value error, then we didn't have an integer so we can just pass and fall down to the fail
            pass

        response = self.get_endpoint(endpoint, **{'data': query_params})
        if response['status_code'] != 200:
            self.fail_json(
                msg="Failed to query endpoint {0} for {1} {2} ({3}), see results"
                .format(endpoint, name_field, name_or_id,
                        response['status_code']),
                resuls=response)

        if response['json']['count'] == 1:
            return response['json']['results'][0]
        elif response['json']['count'] > 1:
            for tower_object in response['json']['results']:
                # ID takes priority, so we match on that first
                if str(tower_object['id']) == name_or_id:
                    return tower_object
            # We didn't match on an ID but we found more than 1 object, therefore the results are ambiguous
            self.fail_json(
                msg=
                "The requested name or id was ambiguous and resulted in too many items"
            )
        elif response['json']['count'] == 0:
            self.fail_json(
                msg="The {0} {1} was not found on the Tower server".format(
                    endpoint, name_or_id))

    def resolve_name_to_id(self, endpoint, name_or_id):
        return self.get_one_by_name_or_id(endpoint, name_or_id)['id']

    def make_request(self, method, endpoint, *args, **kwargs):
        # In case someone is calling us directly; make sure we were given a method, let's not just assume a GET
        if not method:
            raise Exception("The HTTP method must be defined")

        # Make sure we start with /api/vX
        if not endpoint.startswith("/"):
            endpoint = "/{0}".format(endpoint)
        if not endpoint.startswith("/api/"):
            endpoint = "/api/v2{0}".format(endpoint)
        if not endpoint.endswith('/') and '?' not in endpoint:
            endpoint = "{0}/".format(endpoint)

        # Extract the headers, this will be used in a couple of places
        headers = kwargs.get('headers', {})

        # Authenticate to Tower (if we don't have a token and if not already done so)
        if not self.oauth_token and not self.authenticated:
            # This method will set a cookie in the cookie jar for us and also an oauth_token
            self.authenticate(**kwargs)
        if self.oauth_token:
            # If we have a oauth token, we just use a bearer header
            headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token)

        # Update the URL path with the endpoint
        self.url = self.url._replace(path=endpoint)

        if method in ['POST', 'PUT', 'PATCH']:
            headers.setdefault('Content-Type', 'application/json')
            kwargs['headers'] = headers
        elif kwargs.get('data'):
            self.url = self.url._replace(query=urlencode(kwargs.get('data')))

        data = None  # Important, if content type is not JSON, this should not be dict type
        if headers.get('Content-Type', '') == 'application/json':
            data = dumps(kwargs.get('data', {}))

        try:
            response = self.session.open(method,
                                         self.url.geturl(),
                                         headers=headers,
                                         validate_certs=self.verify_ssl,
                                         follow_redirects=True,
                                         data=data)
        except (SSLValidationError) as ssl_err:
            self.fail_json(
                msg=
                "Could not establish a secure connection to your host ({1}): {0}."
                .format(self.url.netloc, ssl_err))
        except (ConnectionError) as con_err:
            self.fail_json(
                msg=
                "There was a network error of some kind trying to connect to your host ({1}): {0}."
                .format(self.url.netloc, con_err))
        except (HTTPError) as he:
            # Sanity check: Did the server send back some kind of internal error?
            if he.code >= 500:
                self.fail_json(
                    msg=
                    'The host sent back a server error ({1}): {0}. Please check the logs and try again later'
                    .format(self.url.path, he))
            # Sanity check: Did we fail to authenticate properly?  If so, fail out now; this is always a failure.
            elif he.code == 401:
                self.fail_json(
                    msg=
                    'Invalid Tower authentication credentials for {0} (HTTP 401).'
                    .format(self.url.path))
            # Sanity check: Did we get a forbidden response, which means that the user isn't allowed to do this? Report that.
            elif he.code == 403:
                self.fail_json(
                    msg="You don't have permission to {1} to {0} (HTTP 403).".
                    format(self.url.path, method))
            # Sanity check: Did we get a 404 response?
            # Requests with primary keys will return a 404 if there is no response, and we want to consistently trap these.
            elif he.code == 404:
                if kwargs.get('return_none_on_404', False):
                    return None
                self.fail_json(
                    msg='The requested object could not be found at {0}.'.
                    format(self.url.path))
            # Sanity check: Did we get a 405 response?
            # A 405 means we used a method that isn't allowed. Usually this is a bad request, but it requires special treatment because the
            # API sends it as a logic error in a few situations (e.g. trying to cancel a job that isn't running).
            elif he.code == 405:
                self.fail_json(
                    msg=
                    "The Tower server says you can't make a request with the {0} method to this endpoing {1}"
                    .format(method, self.url.path))
            # Sanity check: Did we get some other kind of error?  If so, write an appropriate error message.
            elif he.code >= 400:
                # We are going to return a 400 so the module can decide what to do with it
                page_data = he.read()
                try:
                    return {'status_code': he.code, 'json': loads(page_data)}
                # JSONDecodeError only available on Python 3.5+
                except ValueError:
                    return {'status_code': he.code, 'text': page_data}
            elif he.code == 204 and method == 'DELETE':
                # A 204 is a normal response for a delete function
                pass
            else:
                self.fail_json(
                    msg="Unexpected return code when calling {0}: {1}".format(
                        self.url.geturl(), he))
        except (Exception) as e:
            self.fail_json(
                msg=
                "There was an unknown error when trying to connect to {2}: {0} {1}"
                .format(type(e).__name__, e, self.url.geturl()))
        finally:
            self.url = self.url._replace(query=None)

        if not self.version_checked:
            # In PY2 we get back an HTTPResponse object but PY2 is returning an addinfourl
            # First try to get the headers in PY3 format and then drop down to PY2.
            try:
                tower_type = response.getheader('X-API-Product-Name', None)
                tower_version = response.getheader('X-API-Product-Version',
                                                   None)
            except Exception:
                tower_type = response.info().getheader('X-API-Product-Name',
                                                       None)
                tower_version = response.info().getheader(
                    'X-API-Product-Version', None)

            if self._COLLECTION_TYPE not in self.collection_to_version or self.collection_to_version[
                    self._COLLECTION_TYPE] != tower_type:
                self.warn(
                    "You are using the {0} version of this collection but connecting to {1}"
                    .format(self._COLLECTION_TYPE, tower_type))
            elif self._COLLECTION_VERSION != tower_version:
                self.warn(
                    "You are running collection version {0} but connecting to tower version {1}"
                    .format(self._COLLECTION_VERSION, tower_version))
            self.version_checked = True

        response_body = ''
        try:
            response_body = response.read()
        except (Exception) as e:
            self.fail_json(msg="Failed to read response body: {0}".format(e))

        response_json = {}
        if response_body and response_body != '':
            try:
                response_json = loads(response_body)
            except (Exception) as e:
                self.fail_json(
                    msg="Failed to parse the response json: {0}".format(e))

        if PY2:
            status_code = response.getcode()
        else:
            status_code = response.status
        return {'status_code': status_code, 'json': response_json}

    def authenticate(self, **kwargs):
        if self.username and self.password:
            # Attempt to get a token from /api/v2/tokens/ by giving it our username/password combo
            # If we have a username and password, we need to get a session cookie
            login_data = {
                "description": "Ansible Tower Module Token",
                "application": None,
                "scope": "write",
            }
            # Post to the tokens endpoint with baisc auth to try and get a token
            api_token_url = (self.url._replace(
                path='/api/v2/tokens/')).geturl()

            try:
                response = self.session.open(
                    'POST',
                    api_token_url,
                    validate_certs=self.verify_ssl,
                    follow_redirects=True,
                    force_basic_auth=True,
                    url_username=self.username,
                    url_password=self.password,
                    data=dumps(login_data),
                    headers={'Content-Type': 'application/json'})
            except HTTPError as he:
                try:
                    resp = he.read()
                except Exception as e:
                    resp = 'unknown {0}'.format(e)
                self.fail_json(msg='Failed to get token: {0}'.format(he),
                               response=resp)
            except (Exception) as e:
                # Sanity check: Did the server send back some kind of internal error?
                self.fail_json(msg='Failed to get token: {0}'.format(e))

            token_response = None
            try:
                token_response = response.read()
                response_json = loads(token_response)
                self.oauth_token_id = response_json['id']
                self.oauth_token = response_json['token']
            except (Exception) as e:
                self.fail_json(
                    msg=
                    "Failed to extract token information from login response: {0}"
                    .format(e),
                    **{'response': token_response})

        # If we have neither of these, then we can try un-authenticated access
        self.authenticated = True

    def delete_if_needed(self, existing_item, on_delete=None):
        # This will exit from the module on its own.
        # If the method successfully deletes an item and on_delete param is defined,
        #   the on_delete parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #   1. None if the existing_item is not defined (so no delete needs to happen)
        #   2. The response from Tower from calling the delete on the endpont. It's up to you to process the response and exit from the module
        # Note: common error codes from the Tower API can cause the module to fail
        if existing_item:
            # If we have an item, we can try to delete it
            try:
                item_url = existing_item['url']
                item_type = existing_item['type']
                item_id = existing_item['id']
            except KeyError as ke:
                self.fail_json(
                    msg=
                    "Unable to process delete of item due to missing data {0}".
                    format(ke))

            if 'name' in existing_item:
                item_name = existing_item['name']
            elif 'username' in existing_item:
                item_name = existing_item['username']
            elif 'identifier' in existing_item:
                item_name = existing_item['identifier']
            elif item_type == 'o_auth2_access_token':
                # An oauth2 token has no name, instead we will use its id for any of the messages
                item_name = existing_item['id']
            elif item_type == 'credential_input_source':
                # An credential_input_source has no name, instead we will use its id for any of the messages
                item_name = existing_item['id']
            else:
                self.fail_json(
                    msg="Unable to process delete of {0} due to missing name".
                    format(item_type))

            response = self.delete_endpoint(item_url)

            if response['status_code'] in [202, 204]:
                if on_delete:
                    on_delete(self, response['json'])
                self.json_output['changed'] = True
                self.json_output['id'] = item_id
                self.exit_json(**self.json_output)
            else:
                if 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg="Unable to delete {0} {1}: {2}".format(
                        item_type, item_name, response['json']['__all__'][0]))
                elif 'json' in response:
                    # This is from a project delete (if there is an active job against it)
                    if 'error' in response['json']:
                        self.fail_json(
                            msg="Unable to delete {0} {1}: {2}".format(
                                item_type, item_name, response['json']
                                ['error']))
                    else:
                        self.fail_json(
                            msg="Unable to delete {0} {1}: {2}".format(
                                item_type, item_name, response['json']))
                else:
                    self.fail_json(msg="Unable to delete {0} {1}: {2}".format(
                        item_type, item_name, response['status_code']))
        else:
            self.exit_json(**self.json_output)

    def modify_associations(self, association_endpoint, new_association_list):
        # if we got None instead of [] we are not modifying the association_list
        if new_association_list is None:
            return

        # First get the existing associations
        response = self.get_all_endpoint(association_endpoint)
        existing_associated_ids = [
            association['id'] for association in response['json']['results']
        ]

        # Disassociate anything that is in existing_associated_ids but not in new_association_list
        ids_to_remove = list(
            set(existing_associated_ids) - set(new_association_list))
        for an_id in ids_to_remove:
            response = self.post_endpoint(
                association_endpoint,
                **{'data': {
                    'id': int(an_id),
                    'disassociate': True
                }})
            if response['status_code'] == 204:
                self.json_output['changed'] = True
            else:
                self.fail_json(msg="Failed to disassociate item {0}".format(
                    response['json']['detail']))

        # Associate anything that is in new_association_list but not in `association`
        for an_id in list(
                set(new_association_list) - set(existing_associated_ids)):
            response = self.post_endpoint(association_endpoint,
                                          **{'data': {
                                              'id': int(an_id)
                                          }})
            if response['status_code'] == 204:
                self.json_output['changed'] = True
            else:
                self.fail_json(msg="Failed to associate item {0}".format(
                    response['json']['detail']))

    def create_if_needed(self,
                         existing_item,
                         new_item,
                         endpoint,
                         on_create=None,
                         item_type='unknown',
                         associations=None):

        # This will exit from the module on its own
        # If the method successfully creates an item and on_create param is defined,
        #    the on_create parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #    1. None if the existing_item is already defined (so no create needs to happen)
        #    2. The response from Tower from calling the patch on the endpont. It's up to you to process the response and exit from the module
        # Note: common error codes from the Tower API can cause the module to fail

        if not endpoint:
            self.fail_json(
                msg="Unable to create new {0} due to missing endpoint".format(
                    item_type))

        item_url = None
        if existing_item:
            try:
                item_url = existing_item['url']
            except KeyError as ke:
                self.fail_json(
                    msg=
                    "Unable to process create of item due to missing data {0}".
                    format(ke))
        else:
            # If we don't have an exisitng_item, we can try to create it

            # We have to rely on item_type being passed in since we don't have an existing item that declares its type
            # We will pull the item_name out from the new_item, if it exists
            for key in ('name', 'username', 'identifier', 'hostname'):
                if key in new_item:
                    item_name = new_item[key]
                    break
            else:
                item_name = 'unknown'

            response = self.post_endpoint(endpoint, **{'data': new_item})
            if response['status_code'] == 201:
                self.json_output['name'] = 'unknown'
                for key in ('name', 'username', 'identifier', 'hostname'):
                    if key in response['json']:
                        self.json_output['name'] = response['json'][key]
                self.json_output['id'] = response['json']['id']
                self.json_output['changed'] = True
                item_url = response['json']['url']
            else:
                if 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                        item_type, item_name, response['json']['__all__'][0]))
                elif 'json' in response:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                        item_type, item_name, response['json']))
                else:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                        item_type, item_name, response['status_code']))

        # Process any associations with this item
        if associations is not None:
            for association_type in associations:
                sub_endpoint = '{0}{1}/'.format(item_url, association_type)
                self.modify_associations(sub_endpoint,
                                         associations[association_type])

        # If we have an on_create method and we actually changed something we can call on_create
        if on_create is not None and self.json_output['changed']:
            on_create(self, response['json'])
        else:
            self.exit_json(**self.json_output)

    def _encrypted_changed_warning(self, field, old, warning=False):
        if not warning:
            return
        self.warn(
            'The field {0} of {1} {2} has encrypted data and may inaccurately report task is changed.'
            .format(field, old.get('type', 'unknown'),
                    old.get('id', 'unknown')))

    @staticmethod
    def has_encrypted_values(obj):
        """Returns True if JSON-like python content in obj has $encrypted$
        anywhere in the data as a value
        """
        if isinstance(obj, dict):
            for val in obj.values():
                if TowerAPIModule.has_encrypted_values(val):
                    return True
        elif isinstance(obj, list):
            for val in obj:
                if TowerAPIModule.has_encrypted_values(val):
                    return True
        elif obj == TowerAPIModule.ENCRYPTED_STRING:
            return True
        return False

    def objects_could_be_different(self,
                                   old,
                                   new,
                                   field_set=None,
                                   warning=False):
        if field_set is None:
            field_set = set(fd for fd in new.keys()
                            if fd not in ('modified', 'related',
                                          'summary_fields'))
        for field in field_set:
            new_field = new.get(field, None)
            old_field = old.get(field, None)
            if old_field != new_field:
                return True  # Something doesn't match
            elif self.has_encrypted_values(new_field) or field not in new:
                # case of 'field not in new' - user password write-only field that API will not display
                self._encrypted_changed_warning(field, old, warning=warning)
                return True
        return False

    def update_if_needed(self,
                         existing_item,
                         new_item,
                         on_update=None,
                         associations=None):
        # This will exit from the module on its own
        # If the method successfully updates an item and on_update param is defined,
        #   the on_update parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #    1. None if the existing_item does not need to be updated
        #    2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module.
        # Note: common error codes from the Tower API can cause the module to fail
        response = None
        if existing_item:

            # If we have an item, we can see if it needs an update
            try:
                item_url = existing_item['url']
                item_type = existing_item['type']
                if item_type == 'user':
                    item_name = existing_item['username']
                elif item_type == 'workflow_job_template_node':
                    item_name = existing_item['identifier']
                elif item_type == 'credential_input_source':
                    item_name = existing_item['id']
                else:
                    item_name = existing_item['name']
                item_id = existing_item['id']
            except KeyError as ke:
                self.fail_json(
                    msg=
                    "Unable to process update of item due to missing data {0}".
                    format(ke))

            # Check to see if anything within the item requires the item to be updated
            needs_patch = self.objects_could_be_different(
                existing_item, new_item)

            # If we decided the item needs to be updated, update it
            self.json_output['id'] = item_id
            if needs_patch:
                response = self.patch_endpoint(item_url, **{'data': new_item})
                if response['status_code'] == 200:
                    # compare apples-to-apples, old API data to new API data
                    # but do so considering the fields given in parameters
                    self.json_output[
                        'changed'] = self.objects_could_be_different(
                            existing_item,
                            response['json'],
                            field_set=new_item.keys(),
                            warning=True)
                elif 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg=response['json']['__all__'])
                else:
                    self.fail_json(
                        **{
                            'msg':
                            "Unable to update {0} {1}, see response".format(
                                item_type, item_name),
                            'response':
                            response
                        })

        else:
            raise RuntimeError(
                'update_if_needed called incorrectly without existing_item')

        # Process any associations with this item
        if associations is not None:
            for association_type, id_list in associations.items():
                endpoint = '{0}{1}/'.format(item_url, association_type)
                self.modify_associations(endpoint, id_list)

        # If we change something and have an on_change call it
        if on_update is not None and self.json_output['changed']:
            if response is None:
                last_data = existing_item
            else:
                last_data = response['json']
            on_update(self, last_data)
        else:
            self.exit_json(**self.json_output)

    def create_or_update_if_needed(self,
                                   existing_item,
                                   new_item,
                                   endpoint=None,
                                   item_type='unknown',
                                   on_create=None,
                                   on_update=None,
                                   associations=None):
        if existing_item:
            return self.update_if_needed(existing_item,
                                         new_item,
                                         on_update=on_update,
                                         associations=associations)
        else:
            return self.create_if_needed(existing_item,
                                         new_item,
                                         endpoint,
                                         on_create=on_create,
                                         item_type=item_type,
                                         associations=associations)

    def logout(self):
        if self.authenticated and self.oauth_token_id:
            # Attempt to delete our current token from /api/v2/tokens/
            # Post to the tokens endpoint with baisc auth to try and get a token
            api_token_url = (
                self.url._replace(
                    path='/api/v2/tokens/{0}/'.format(self.oauth_token_id),
                    query=
                    None  # in error cases, fail_json exists before exception handling
                )).geturl()

            try:
                self.session.open('DELETE',
                                  api_token_url,
                                  validate_certs=self.verify_ssl,
                                  follow_redirects=True,
                                  force_basic_auth=True,
                                  url_username=self.username,
                                  url_password=self.password)
                self.oauth_token_id = None
                self.authenticated = False
            except HTTPError as he:
                try:
                    resp = he.read()
                except Exception as e:
                    resp = 'unknown {0}'.format(e)
                self.warn(
                    'Failed to release tower token: {0}, response: {1}'.format(
                        he, resp))
            except (Exception) as e:
                # Sanity check: Did the server send back some kind of internal error?
                self.warn('Failed to release tower token {0}: {1}'.format(
                    self.oauth_token_id, e))

    def is_job_done(self, job_status):
        if job_status in ['new', 'pending', 'waiting', 'running']:
            return False
        else:
            return True
class BorgBaseClient:
    LOGIN = '''
mutation login(
	$email: String!
	$password: String!
	$otp: String
	) {
		login(
			username: $email
			password: $password
			otp: $otp
		) {
			user {
				id
			}
		}
}
'''

    SSH_LIST = '''
query data {
	sshList {
		id
		name
		keyData
	}
}
'''

    SSH_ADD = '''
mutation sshAdd(
	$name: String!
	$keyData: String!
	) {
		sshAdd(
			name: $name
			keyData: $keyData
		) {
			keyAdded {
				id
				name
				hashMd5
				keyType
				bits
			}
		}
}
'''

    SSH_DELETE = '''
mutation sshDelete($id: Int!) {
  sshDelete(id: $id) {
    ok
  }
}
'''

    REPO_LIST = '''
query repoList {
	repoList {
		id
		name
		quota
		quotaEnabled
		alertDays
		region
		borgVersion
		appendOnly
		appendOnlyKeys
		fullAccessKeys
	}
}
'''

    REPO_ADD = '''
mutation repoAdd(
  $name: String
  $quota: Int
  $quotaEnabled: Boolean
  $appendOnlyKeys: [String]
  $fullAccessKeys: [String]
  $alertDays: Int
  $region: String
  $borgVersion: String
  ) {
    repoAdd(
      name: $name
      quota: $quota
      quotaEnabled: $quotaEnabled
      appendOnlyKeys: $appendOnlyKeys
      fullAccessKeys: $fullAccessKeys
      alertDays: $alertDays
      region: $region
      borgVersion: $borgVersion
    ) {
      repoAdded {
        id
        name
      }
    }
}
'''

    REPO_EDIT = '''
mutation repoEdit(
  $id: String
  $name: String
  $quota: Int
  $quotaEnabled: Boolean
  $appendOnlyKeys: [String]
  $fullAccessKeys: [String]
  $alertDays: Int
  $region: String
  $borgVersion: String
  ) {
    repoEdit(
      id: $id
      name: $name
      quota: $quota
      quotaEnabled: $quotaEnabled
      appendOnlyKeys: $appendOnlyKeys
      fullAccessKeys: $fullAccessKeys
      alertDays: $alertDays
      region: $region
      borgVersion: $borgVersion
    ) {
      repoEdited {
        id
        name
      }
    }
}
'''

    REPO_DELETE = '''
mutation repoDelete($id: String!) {
  repoDelete(id: $id) {
    ok
  }
}
'''

    def __init__(self, endpoint='https://api.borgbase.com/graphql'):
        self.endpoint = endpoint
        self.session = Request()

    def login(self, **kwargs):
        return self._send(self.LOGIN, kwargs)

    def execute(self, query, variables=None):
        return self._send(query, variables)

    def _send(self, query, variables):
        data = {'query': query, 'variables': variables}

        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        }

        request = self.session.open('POST',
                                    self.endpoint,
                                    data=json.dumps(data),
                                    headers=headers)

        if request.getcode() != 200:
            raise Exception(
                "Query failed to run by returning code of {}. {}".format(
                    request.getcode(), query))

        return json.loads(request.read())
Beispiel #8
0
class Client:
    def __init__(self,
                 host,
                 username,
                 password,
                 client_id=None,
                 client_secret=None,
                 timeout=None):
        self.host = host
        self.username = username
        self.password = password
        self.client_id = client_id
        self.client_secret = client_secret
        self.timeout = timeout

        self._auth_header = None
        self._client = Request()

    @property
    def auth_header(self):
        if not self._auth_header:
            self._auth_header = self._login()
        return self._auth_header

    def _login(self):
        if self.client_id and self.client_secret:
            return self._login_oauth()
        return self._login_username_password()

    def _login_username_password(self):
        return dict(
            Authorization=basic_auth_header(self.username, self.password))

    def _login_oauth(self):
        auth_data = urlencode(
            dict(
                grant_type="password",
                username=self.username,
                password=self.password,
                client_id=self.client_id,
                client_secret=self.client_secret,
            ))
        resp = self._request(
            "POST",
            "{0}/oauth_token.do".format(self.host),
            data=auth_data,
            headers=dict(Accept="application/json"),
        )
        if resp.status != 200:
            raise UnexpectedAPIResponse(resp.status, resp.data)

        access_token = resp.json["access_token"]
        return dict(Authorization="Bearer {0}".format(access_token))

    def _request(self, method, path, data=None, headers=None):
        try:
            raw_resp = self._client.open(method,
                                         path,
                                         data=data,
                                         headers=headers,
                                         timeout=self.timeout)
        except HTTPError as e:
            # Wrong username/password, or expired access token
            if e.code == 401:
                raise AuthError(
                    "Failed to authenticate with the instance: {0} {1}".format(
                        e.code, e.reason), )
            # Other HTTP error codes do not necessarily mean errors.
            # This is for the caller to decide.
            return Response(e.code, e.read(), e.headers)
        except URLError as e:
            raise ServiceNowError(e.reason)

        if PY2:
            return Response(raw_resp.getcode(), raw_resp.read(),
                            raw_resp.info())
        return Response(raw_resp.status, raw_resp.read(), raw_resp.headers)

    def request(self, method, path, query=None, data=None):
        escaped_path = quote(path.rstrip("/"))
        url = "{0}/api/now/{1}".format(self.host, escaped_path)
        if query:
            url = "{0}?{1}".format(url, urlencode(query))
        headers = dict(Accept="application/json", **self.auth_header)
        if data is not None:
            data = json.dumps(data, separators=(",", ":"))
            headers["Content-type"] = "application/json"
        return self._request(method, url, data=data, headers=headers)

    def get(self, path, query=None):
        resp = self.request("GET", path, query=query)
        if resp.status in (200, 404):
            return resp
        raise UnexpectedAPIResponse(resp.status, resp.data)

    def post(self, path, data, query=None):
        resp = self.request("POST", path, data=data, query=query)
        if resp.status == 201:
            return resp
        raise UnexpectedAPIResponse(resp.status, resp.data)

    def patch(self, path, data, query=None):
        resp = self.request("PATCH", path, data=data, query=query)
        if resp.status == 200:
            return resp
        raise UnexpectedAPIResponse(resp.status, resp.data)

    def put(self, path, data, query=None):
        resp = self.request("PUT", path, data=data, query=query)
        if resp.status == 200:
            return resp
        raise UnexpectedAPIResponse(resp.status, resp.data)

    def delete(self, path, query=None):
        resp = self.request("DELETE", path, query=query)
        if resp.status != 204:
            raise UnexpectedAPIResponse(resp.status, resp.data)
Beispiel #9
0
class TowerModule(AnsibleModule):
    url = None
    honorred_settings = ('host', 'username', 'password', 'verify_ssl',
                         'oauth_token')
    host = '127.0.0.1'
    username = None
    password = None
    verify_ssl = True
    oauth_token = None
    oauth_token_id = None
    session = None
    cookie_jar = CookieJar()
    authenticated = False
    config_name = 'tower_cli.cfg'

    def __init__(self, argument_spec, **kwargs):
        args = dict(
            tower_host=dict(required=False,
                            fallback=(env_fallback, ['TOWER_HOST'])),
            tower_username=dict(required=False,
                                fallback=(env_fallback, ['TOWER_USERNAME'])),
            tower_password=dict(no_log=True,
                                required=False,
                                fallback=(env_fallback, ['TOWER_PASSWORD'])),
            validate_certs=dict(type='bool',
                                aliases=['tower_verify_ssl'],
                                required=False,
                                fallback=(env_fallback, ['TOWER_VERIFY_SSL'])),
            tower_oauthtoken=dict(type='str',
                                  no_log=True,
                                  required=False,
                                  fallback=(env_fallback,
                                            ['TOWER_OAUTH_TOKEN'])),
            tower_config_file=dict(type='path', required=False, default=None),
        )
        args.update(argument_spec)
        kwargs['supports_check_mode'] = True

        self.json_output = {'changed': False}

        super(TowerModule, self).__init__(argument_spec=args, **kwargs)

        self.load_config_files()

        # Parameters specified on command line will override settings in any config
        if self.params.get('tower_host'):
            self.host = self.params.get('tower_host')
        if self.params.get('tower_username'):
            self.username = self.params.get('tower_username')
        if self.params.get('tower_password'):
            self.password = self.params.get('tower_password')
        if self.params.get('validate_certs') is not None:
            self.verify_ssl = self.params.get('validate_certs')
        if self.params.get('tower_oauthtoken'):
            self.oauth_token = self.params.get('tower_oauthtoken')

        # Perform some basic validation
        if not re.match('^https{0,1}://', self.host):
            self.host = "https://{0}".format(self.host)

        # Try to parse the hostname as a url
        try:
            self.url = urlparse(self.host)
        except Exception as e:
            self.fail_json(
                msg="Unable to parse tower_host as a URL ({1}): {0}".format(
                    self.host, e))

        # Try to resolve the hostname
        hostname = self.url.netloc.split(':')[0]
        try:
            gethostbyname(hostname)
        except Exception as e:
            self.fail_json(
                msg="Unable to resolve tower_host ({1}): {0}".format(
                    hostname, e))

        self.session = Request(cookies=CookieJar(),
                               validate_certs=self.verify_ssl)

    def load_config_files(self):
        # Load configs like TowerCLI would have from least import to most
        config_files = [
            '/etc/tower/tower_cli.cfg',
            join(expanduser("~"), ".{0}".format(self.config_name))
        ]
        local_dir = getcwd()
        config_files.append(join(local_dir, self.config_name))
        while split(local_dir)[1]:
            local_dir = split(local_dir)[0]
            config_files.insert(
                2, join(local_dir, ".{0}".format(self.config_name)))

        for config_file in config_files:
            if exists(config_file) and not isdir(config_file):
                # Only throw a formatting error if the file exists and is not a directory
                try:
                    self.load_config(config_file)
                except ConfigFileException:
                    self.fail_json(
                        'The config file {0} is not properly formatted'.format(
                            config_file))

        # If we have a specified  tower config, load it
        if self.params.get('tower_config_file'):
            duplicated_params = []
            for direct_field in ('tower_host', 'tower_username',
                                 'tower_password', 'validate_certs',
                                 'tower_oauthtoken'):
                if self.params.get(direct_field):
                    duplicated_params.append(direct_field)
            if duplicated_params:
                self.warn((
                    'The parameter(s) {0} were provided at the same time as tower_config_file. '
                    'Precedence may be unstable, we suggest either using config file or params.'
                ).format(', '.join(duplicated_params)))
            try:
                # TODO: warn if there are conflicts with other params
                self.load_config(self.params.get('tower_config_file'))
            except ConfigFileException as cfe:
                # Since we were told specifically to load this we want it to fail if we have an error
                self.fail_json(msg=cfe)

    def load_config(self, config_path):
        # Validate the config file is an actual file
        if not isfile(config_path):
            raise ConfigFileException(
                'The specified config file does not exist')

        if not access(config_path, R_OK):
            raise ConfigFileException(
                "The specified config file cannot be read")

        # Read in the file contents:
        with open(config_path, 'r') as f:
            config_string = f.read()

        # First try to yaml load the content (which will also load json)
        try:
            config_data = yaml.load(config_string, Loader=yaml.SafeLoader)
            # If this is an actual ini file, yaml will return the whole thing as a string instead of a dict
            if type(config_data) is not dict:
                raise AssertionError(
                    "The yaml config file is not properly formatted as a dict."
                )

        except (AttributeError, yaml.YAMLError, AssertionError):
            # TowerCLI used to support a config file with a missing [general] section by prepending it if missing
            if '[general]' not in config_string:
                config_string = '[general]{0}'.format(config_string)

            config = ConfigParser()

            try:
                placeholder_file = StringIO(config_string)
                # py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3
                # This "if" removes the deprecation warning
                if hasattr(config, 'read_file'):
                    config.read_file(placeholder_file)
                else:
                    config.readfp(placeholder_file)

                # If we made it here then we have values from reading the ini file, so let's pull them out into a dict
                config_data = {}
                for honorred_setting in self.honorred_settings:
                    try:
                        config_data[honorred_setting] = config.get(
                            'general', honorred_setting)
                    except (NoOptionError):
                        pass

            except Exception as e:
                raise ConfigFileException(
                    "An unknown exception occured trying to ini load config file: {0}"
                    .format(e))

        except Exception as e:
            raise ConfigFileException(
                "An unknown exception occured trying to load config file: {0}".
                format(e))

        # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here
        for honorred_setting in self.honorred_settings:
            if honorred_setting in config_data:
                # Veriffy SSL must be a boolean
                if honorred_setting == 'verify_ssl':
                    if type(config_data[honorred_setting]) is str:
                        setattr(self, honorred_setting,
                                strtobool(config_data[honorred_setting]))
                    else:
                        setattr(self, honorred_setting,
                                bool(config_data[honorred_setting]))
                else:
                    setattr(self, honorred_setting,
                            config_data[honorred_setting])

    def head_endpoint(self, endpoint, *args, **kwargs):
        return self.make_request('HEAD', endpoint, **kwargs)

    def get_endpoint(self, endpoint, *args, **kwargs):
        return self.make_request('GET', endpoint, **kwargs)

    def patch_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('PATCH', endpoint, **kwargs)

    def post_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('POST', endpoint, **kwargs)

    def delete_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('DELETE', endpoint, **kwargs)

    def get_all_endpoint(self, endpoint, *args, **kwargs):
        response = self.get_endpoint(endpoint, *args, **kwargs)
        if 'next' not in response['json']:
            raise RuntimeError(
                'Expected list from API at {0}, got: {1}'.format(
                    endpoint, response))
        next_page = response['json']['next']

        if response['json']['count'] > 10000:
            self.fail_json(
                msg=
                'The number of items being queried for is higher than 10,000.')

        while next_page is not None:
            next_response = self.get_endpoint(next_page)
            response['json']['results'] = response['json'][
                'results'] + next_response['json']['results']
            next_page = next_response['json']['next']
            response['json']['next'] = next_page
        return response

    def get_one(self, endpoint, *args, **kwargs):
        response = self.get_endpoint(endpoint, *args, **kwargs)
        if response['status_code'] != 200:
            self.fail_json(
                msg="Got a {0} response when trying to get one from {1}".
                format(response['status_code'], endpoint))

        if 'count' not in response['json'] or 'results' not in response['json']:
            self.fail_json(
                msg="The endpoint did not provide count and results")

        if response['json']['count'] == 0:
            return None
        elif response['json']['count'] > 1:
            self.fail_json(
                msg=
                "An unexpected number of items was returned from the API ({0})"
                .format(response['json']['count']))

        return response['json']['results'][0]

    def resolve_name_to_id(self, endpoint, name_or_id):
        # Try to resolve the object by name
        response = self.get_endpoint(endpoint,
                                     **{'data': {
                                         'name': name_or_id
                                     }})
        if response['json']['count'] == 1:
            return response['json']['results'][0]['id']
        elif response['json']['count'] == 0:
            try:
                int(name_or_id)
                # If we got 0 items by name, maybe they gave us an ID, let's try looking it up by ID
                response = self.head_endpoint(
                    "{0}/{1}".format(endpoint, name_or_id),
                    **{'return_none_on_404': True})
                if response is not None:
                    return name_or_id
            except ValueError:
                # If we got a value error than we didn't have an integer so we can just pass and fall down to the fail
                pass

            self.fail_json(
                msg="The {0} {1} was not found on the Tower server".format(
                    endpoint, name_or_id))
        else:
            self.fail_json(
                msg=
                "Found too many names {0} at endpoint {1} try using an ID instead of a name"
                .format(name_or_id, endpoint))

    def make_request(self, method, endpoint, *args, **kwargs):
        # In case someone is calling us directly; make sure we were given a method, let's not just assume a GET
        if not method:
            raise Exception("The HTTP method must be defined")

        # Make sure we start with /api/vX
        if not endpoint.startswith("/"):
            endpoint = "/{0}".format(endpoint)
        if not endpoint.startswith("/api/"):
            endpoint = "/api/v2{0}".format(endpoint)
        if not endpoint.endswith('/') and '?' not in endpoint:
            endpoint = "{0}/".format(endpoint)

        # Extract the headers, this will be used in a couple of places
        headers = kwargs.get('headers', {})

        # Authenticate to Tower (if we've not already done so)
        if not self.authenticated:
            # This method will set a cookie in the cookie jar for us
            self.authenticate(**kwargs)
        if self.oauth_token:
            # If we have a oauth token, we just use a bearer header
            headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token)

        # Update the URL path with the endpoint
        self.url = self.url._replace(path=endpoint)

        if method in ['POST', 'PUT', 'PATCH']:
            headers.setdefault('Content-Type', 'application/json')
            kwargs['headers'] = headers
        elif kwargs.get('data'):
            self.url = self.url._replace(query=urlencode(kwargs.get('data')))

        data = {}
        if headers.get('Content-Type', '') == 'application/json':
            data = dumps(kwargs.get('data', {}))

        try:
            response = self.session.open(method,
                                         self.url.geturl(),
                                         headers=headers,
                                         validate_certs=self.verify_ssl,
                                         follow_redirects=True,
                                         data=data)
            self.url = self.url._replace(query=None)
        except (SSLValidationError) as ssl_err:
            self.fail_json(
                msg=
                "Could not establish a secure connection to your host ({1}): {0}."
                .format(self.url.netloc, ssl_err))
        except (ConnectionError) as con_err:
            self.fail_json(
                msg=
                "There was a network error of some kind trying to connect to your host ({1}): {0}."
                .format(self.url.netloc, con_err))
        except (HTTPError) as he:
            # Sanity check: Did the server send back some kind of internal error?
            if he.code >= 500:
                self.fail_json(
                    msg=
                    'The host sent back a server error ({1}): {0}. Please check the logs and try again later'
                    .format(self.url.path, he))
            # Sanity check: Did we fail to authenticate properly?  If so, fail out now; this is always a failure.
            elif he.code == 401:
                self.fail_json(
                    msg=
                    'Invalid Tower authentication credentials for {0} (HTTP 401).'
                    .format(self.url.path))
            # Sanity check: Did we get a forbidden response, which means that the user isn't allowed to do this? Report that.
            elif he.code == 403:
                self.fail_json(
                    msg="You don't have permission to {1} to {0} (HTTP 403).".
                    format(self.url.path, method))
            # Sanity check: Did we get a 404 response?
            # Requests with primary keys will return a 404 if there is no response, and we want to consistently trap these.
            elif he.code == 404:
                if kwargs.get('return_none_on_404', False):
                    return None
                self.fail_json(
                    msg='The requested object could not be found at {0}.'.
                    format(self.url.path))
            # Sanity check: Did we get a 405 response?
            # A 405 means we used a method that isn't allowed. Usually this is a bad request, but it requires special treatment because the
            # API sends it as a logic error in a few situations (e.g. trying to cancel a job that isn't running).
            elif he.code == 405:
                self.fail_json(
                    msg=
                    "The Tower server says you can't make a request with the {0} method to this endpoing {1}"
                    .format(method, self.url.path))
            # Sanity check: Did we get some other kind of error?  If so, write an appropriate error message.
            elif he.code >= 400:
                # We are going to return a 400 so the module can decide what to do with it
                page_data = he.read()
                try:
                    return {'status_code': he.code, 'json': loads(page_data)}
                # JSONDecodeError only available on Python 3.5+
                except ValueError:
                    return {'status_code': he.code, 'text': page_data}
            elif he.code == 204 and method == 'DELETE':
                # A 204 is a normal response for a delete function
                pass
            else:
                self.fail_json(
                    msg="Unexpected return code when calling {0}: {1}".format(
                        self.url.geturl(), he))
        except (Exception) as e:
            self.fail_json(
                msg=
                "There was an unknown error when trying to connect to {2}: {0} {1}"
                .format(type(e).__name__, e, self.url.geturl()))

        response_body = ''
        try:
            response_body = response.read()
        except (Exception) as e:
            self.fail_json(msg="Failed to read response body: {0}".format(e))

        response_json = {}
        if response_body and response_body != '':
            try:
                response_json = loads(response_body)
            except (Exception) as e:
                self.fail_json(
                    msg="Failed to parse the response json: {0}".format(e))

        if PY2:
            status_code = response.getcode()
        else:
            status_code = response.status
        return {'status_code': status_code, 'json': response_json}

    def authenticate(self, **kwargs):
        if self.username and self.password:
            # Attempt to get a token from /api/v2/tokens/ by giving it our username/password combo
            # If we have a username and password, we need to get a session cookie
            login_data = {
                "description": "Ansible Tower Module Token",
                "application": None,
                "scope": "write",
            }
            # Post to the tokens endpoint with baisc auth to try and get a token
            api_token_url = (self.url._replace(
                path='/api/v2/tokens/')).geturl()

            try:
                response = self.session.open(
                    'POST',
                    api_token_url,
                    validate_certs=self.verify_ssl,
                    follow_redirects=True,
                    force_basic_auth=True,
                    url_username=self.username,
                    url_password=self.password,
                    data=dumps(login_data),
                    headers={'Content-Type': 'application/json'})
            except (Exception) as e:
                # Sanity check: Did the server send back some kind of internal error?
                self.fail_json(msg='Failed to get token: {0}'.format(e))

            token_response = None
            try:
                token_response = response.read()
                response_json = loads(token_response)
                self.oauth_token_id = response_json['id']
                self.oauth_token = response_json['token']
            except (Exception) as e:
                self.fail_json(
                    msg=
                    "Failed to extract token information from login response: {0}"
                    .format(e),
                    **{'response': token_response})

        # If we have neither of these, then we can try un-authenticated access
        self.authenticated = True

    def default_check_mode(self):
        '''Execute check mode logic for Ansible Tower modules'''
        if self.check_mode:
            try:
                result = self.get_endpoint('ping')
                self.exit_json(
                    **{
                        'changed': True,
                        'tower_version': '{0}'.format(result['json']
                                                      ['version'])
                    })
            except (Exception) as excinfo:
                self.fail_json(changed=False,
                               msg='Failed check mode: {0}'.format(excinfo))

    def delete_if_needed(self, existing_item, on_delete=None):
        # This will exit from the module on its own.
        # If the method successfully deletes an item and on_delete param is defined,
        #   the on_delete parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #   1. None if the existing_item is not defined (so no delete needs to happen)
        #   2. The response from Tower from calling the delete on the endpont. It's up to you to process the response and exit from the module
        # Note: common error codes from the Tower API can cause the module to fail
        if existing_item:
            # If we have an item, we can try to delete it
            try:
                item_url = existing_item['url']
                item_type = existing_item['type']
                item_id = existing_item['id']
            except KeyError as ke:
                self.fail_json(
                    msg=
                    "Unable to process delete of item due to missing data {0}".
                    format(ke))

            if 'name' in existing_item:
                item_name = existing_item['name']
            elif 'username' in existing_item:
                item_name = existing_item['username']
            else:
                self.fail_json(
                    msg="Unable to process delete of {0} due to missing name".
                    format(item_type))

            response = self.delete_endpoint(item_url)

            if response['status_code'] in [202, 204]:
                if on_delete:
                    on_delete(self, response['json'])
                self.json_output['changed'] = True
                self.json_output['id'] = item_id
                self.exit_json(**self.json_output)
            else:
                if 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg="Unable to delete {0} {1}: {2}".format(
                        item_type, item_name, response['json']['__all__'][0]))
                elif 'json' in response:
                    # This is from a project delete (if there is an active job against it)
                    if 'error' in response['json']:
                        self.fail_json(
                            msg="Unable to delete {0} {1}: {2}".format(
                                item_type, item_name, response['json']
                                ['error']))
                    else:
                        self.fail_json(
                            msg="Unable to delete {0} {1}: {2}".format(
                                item_type, item_name, response['json']))
                else:
                    self.fail_json(msg="Unable to delete {0} {1}: {2}".format(
                        item_type, item_name, response['status_code']))
        else:
            self.exit_json(**self.json_output)

    def modify_associations(self, association_endpoint, new_association_list):
        # First get the existing associations
        response = self.get_all_endpoint(association_endpoint)
        existing_associated_ids = [
            association['id'] for association in response['json']['results']
        ]

        # Disassociate anything that is in existing_associated_ids but not in new_association_list
        ids_to_remove = list(
            set(existing_associated_ids) - set(new_association_list))
        for an_id in ids_to_remove:
            response = self.post_endpoint(
                association_endpoint,
                **{'data': {
                    'id': int(an_id),
                    'disassociate': True
                }})
            if response['status_code'] == 204:
                self.json_output['changed'] = True
            else:
                self.fail_json(msg="Failed to disassociate item {0}".format(
                    response['json']['detail']))

        # Associate anything that is in new_association_list but not in `association`
        for an_id in list(
                set(new_association_list) - set(existing_associated_ids)):
            response = self.post_endpoint(association_endpoint,
                                          **{'data': {
                                              'id': int(an_id)
                                          }})
            if response['status_code'] == 204:
                self.json_output['changed'] = True
            else:
                self.fail_json(msg="Failed to associate item {0}".format(
                    response['json']['detail']))

    def create_if_needed(self,
                         existing_item,
                         new_item,
                         endpoint,
                         on_create=None,
                         item_type='unknown',
                         associations=None):

        # This will exit from the module on its own
        # If the method successfully creates an item and on_create param is defined,
        #    the on_create parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #    1. None if the existing_item is already defined (so no create needs to happen)
        #    2. The response from Tower from calling the patch on the endpont. It's up to you to process the response and exit from the module
        # Note: common error codes from the Tower API can cause the module to fail

        if not endpoint:
            self.fail_json(
                msg="Unable to create new {0} due to missing endpoint".format(
                    item_type))

        if existing_item:
            try:
                existing_item['url']
            except KeyError as ke:
                self.fail_json(
                    msg=
                    "Unable to process create of item due to missing data {0}".
                    format(ke))
        else:
            # If we don't have an exisitng_item, we can try to create it

            # We have to rely on item_type being passed in since we don't have an existing item that declares its type
            # We will pull the item_name out from the new_item, if it exists
            item_name = new_item.get('name', 'unknown')

            response = self.post_endpoint(endpoint, **{'data': new_item})
            if response['status_code'] == 201:
                self.json_output['name'] = 'unknown'
                if 'name' in response['json']:
                    self.json_output['name'] = response['json']['name']
                elif 'username' in response['json']:
                    # User objects return username instead of name
                    self.json_output['name'] = response['json']['username']
                self.json_output['id'] = response['json']['id']
                self.json_output['changed'] = True
            else:
                if 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                        item_type, item_name, response['json']['__all__'][0]))
                elif 'json' in response:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                        item_type, item_name, response['json']))
                else:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                        item_type, item_name, response['status_code']))

        # Process any associations with this item
        if associations is not None:
            for association_type in associations:
                self.modify_associations(response,
                                         associations[association_type])

        # If we have an on_create method and we actually changed something we can call on_create
        if on_create is not None and self.json_output['changed']:
            on_create(self, response['json'])
        else:
            self.exit_json(**self.json_output)

    def update_if_needed(self,
                         existing_item,
                         new_item,
                         on_update=None,
                         associations=None):
        # This will exit from the module on its own
        # If the method successfully updates an item and on_update param is defined,
        #   the on_update parameter will be called as a method pasing in this object and the json from the response
        # This will return one of three things:
        #    1. None if the existing_item does not need to be updated
        #    2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module.
        #    3. An ItemNotDefined exception, if the existing_item does not exist
        # Note: common error codes from the Tower API can cause the module to fail
        if existing_item:

            # If we have an item, we can see if it needs an update
            try:
                item_url = existing_item['url']
                item_type = existing_item['type']
                if item_type == 'user':
                    item_name = existing_item['username']
                else:
                    item_name = existing_item['name']
                item_id = existing_item['id']
            except KeyError as ke:
                self.fail_json(
                    msg=
                    "Unable to process update of item due to missing data {0}".
                    format(ke))

            # Check to see if anything within the item requires the item to be updated
            needs_update = False
            for field in new_item:
                existing_field = existing_item.get(field, None)
                new_field = new_item.get(field, None)
                # If the two items don't match and we are not comparing '' to None
                if existing_field != new_field and not (
                        existing_field in (None, '') and new_field == ''):
                    # Something doesn't match so let's update it
                    needs_update = True
                    break

            # If we decided the item needs to be updated, update it
            self.json_output['id'] = item_id
            if needs_update:
                response = self.patch_endpoint(item_url, **{'data': new_item})
                if response['status_code'] == 200:
                    self.json_output['changed'] = True
                elif 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg=response['json']['__all__'])
                else:
                    self.fail_json(
                        **{
                            'msg':
                            "Unable to update {0} {1}, see response".format(
                                item_type, item_name),
                            'response':
                            response
                        })

        else:
            raise RuntimeError(
                'update_if_needed called incorrectly without existing_item')

        # Process any associations with this item
        if associations is not None:
            for association_type, id_list in associations.items():
                endpoint = '{0}{1}/'.format(item_url, association_type)
                self.modify_associations(endpoint, id_list)

        # If we change something and have an on_change call it
        if on_update is not None and self.json_output['changed']:
            on_update(self, response['json'])
        else:
            self.exit_json(**self.json_output)

    def create_or_update_if_needed(self,
                                   existing_item,
                                   new_item,
                                   endpoint=None,
                                   item_type='unknown',
                                   on_create=None,
                                   on_update=None,
                                   associations=None):
        if existing_item:
            return self.update_if_needed(existing_item,
                                         new_item,
                                         on_update=on_update,
                                         associations=associations)
        else:
            return self.create_if_needed(existing_item,
                                         new_item,
                                         endpoint,
                                         on_create=on_create,
                                         item_type=item_type,
                                         associations=associations)

    def logout(self):
        if self.oauth_token_id is not None and self.username and self.password:
            # Attempt to delete our current token from /api/v2/tokens/
            # Post to the tokens endpoint with baisc auth to try and get a token
            api_token_url = (self.url._replace(
                path='/api/v2/tokens/{0}/'.format(self.oauth_token_id))
                             ).geturl()

            try:
                self.session.open('DELETE',
                                  api_token_url,
                                  validate_certs=self.verify_ssl,
                                  follow_redirects=True,
                                  force_basic_auth=True,
                                  url_username=self.username,
                                  url_password=self.password)
                self.oauth_token_id = None
                self.authenticated = False
            except (Exception) as e:
                # Sanity check: Did the server send back some kind of internal error?
                self.warn('Failed to release tower token {0}: {1}'.format(
                    self.oauth_token_id, e))

    def fail_json(self, **kwargs):
        # Try to log out if we are authenticated
        self.logout()
        super(TowerModule, self).fail_json(**kwargs)

    def exit_json(self, **kwargs):
        # Try to log out if we are authenticated
        self.logout()
        super(TowerModule, self).exit_json(**kwargs)

    def is_job_done(self, job_status):
        if job_status in ['new', 'pending', 'waiting', 'running']:
            return False
        else:
            return True
Beispiel #10
0
class Client:
    def __init__(self, host, token, timeout=None, validate_certs=True):
        self.host = host
        self.token = token
        self.timeout = timeout
        self.validate_certs = validate_certs

        self._auth_header = None
        self._client = Request()

    @property
    def auth_header(self):
        if not self._auth_header:
            self._auth_header = self._login()
        return self._auth_header

    def _login(self):
        if self.token:
            return {"X-API-Token": self.token}

    def _request(self, method, path, data=None, headers=None):
        try:
            raw_resp = self._client.open(
                method,
                path,
                data=data,
                headers=headers,
                timeout=self.timeout,
                validate_certs=self.validate_certs,
            )
        except HTTPError as e:
            if e.code == 401:
                raise AuthError(
                    "Failed to authenticate with IPFabric: {0} {1}"
                    " (check token)".format(
                        e.code,
                        e.reason,
                    ),
                )
            elif e.code == 403:
                raise AuthError(
                    "Insufficient API Rights Check Permissions: "
                    "{0} {1}".format(
                        e.code,
                        e.reason,
                    ),
                )
            return Response(e.code, e.read(), e.headers)
        except URLError as e:
            raise IPFabricError(e.reason)

        return Response(raw_resp.status, raw_resp.read(), raw_resp.headers)

    def request(self, method, path, query=None, data=None):
        url = "{0}/api/v1/{1}".format(self.host, path)

        headers = dict(Accept="application/json", **self.auth_header)
        if data is not None:
            data = json.dumps(data, separators=(",", ":"))
            headers["Content-Type"] = "application/json"
        return self._request(method, url, data=data, headers=headers)

    def get(self, path):
        resp = self.request("GET", path)
        if resp.status in (200, 404):
            return resp
        raise UnexpectedAPIResponse(resp.status, resp.data)

    def post(self, path, data):
        resp = self.request("POST", path, data=data)
        if resp.status in (200, 201):
            return resp
        raise UnexpectedAPIResponse(resp.status, resp.data)

    def get_snapshots(self, snapshot_id=None):
        resp = self.request("GET", "snapshots")
        if resp.status == 200:
            if snapshot_id:
                single_snapshot = [
                    snapshot
                    for snapshot in resp.json
                    if snapshot_id == snapshot["id"]  # noqa E501
                ]

                if len(single_snapshot) == 0:
                    raise IPFabricError("Snapshot not found.")

                return single_snapshot
            return resp.json
        raise UnexpectedAPIResponse(resp.status, resp.data)

    def rediscover_existing_snapshot(
        self,
        snapshot_id,
        devices,
    ):
        data = {"snList": devices}
        url = "snapshots/{0}/devices".format(snapshot_id)
        resp = self.request("POST", url, data=data)
        return resp

    def rediscover_new_snapshot(self, ips):
        data = {
            "networks": {
                "include": ["{0}/32".format(ip) for ip in ips],
            },
            "seedList": ips,
        }
        resp = self.request("POST", "snapshots", data=data)
        return resp

    def create_snapshot(self, snapshot_id=None, devices=None, ips=None):
        if snapshot_id and devices:
            resp = self.rediscover_snapshot(snapshot_id, devices)
        elif ips:
            resp = self.rediscover_new_snapshot(ips)
        else:
            resp = self.request("POST", "snapshots")

        if resp.status == 200 and resp.json["success"]:
            time.sleep(1)
            iterations = 0
            snapshot = self.get_snapshots()[0]
            while snapshot["state"] != "discovering" and iterations >= 10:
                snapshot = self.get_snapshots()[0]["state"]
                iterations += 1
            return snapshot

        raise IPFabricError("Failed to create snapshot.")

    def delete_snapshot(self, snapshot_id):
        if self.get_snapshots(snapshot_id=snapshot_id):
            resp = self.request("DELETE", "snapshots/{0}".format(snapshot_id))
            if resp.status == 204:
                return True
            raise IPFabricError("Snapshot failed to delete.")

    def snapshot_load(self, snapshot_id, state):
        if self.get_snapshots(snapshot_id):
            url = "snapshots/{0}/{1}".format(snapshot_id, state)
            resp = self.request("POST", url)
            if resp.status == 204:
                return resp
            raise IPFabricError("Snapshot failed to {0}.".format(state))
Beispiel #11
0
class BaseApi(object):
    PLUGIN = None

    def __init__(self, module, module_name):
        self._module = module
        self._module_name = module_name

        self._es_url = self._module.params.get('elasticsearch_url')

        self._connect()

    def put(self, ressource, name=None, data=None):
        return self._open('PUT', self._url(ressource, name), data=data)

    def get(self, ressource, name=None):
        return self._open('GET', self._url(ressource, name))

    def patch(self, ressource, name=None, data=None):
        return self._open('PATCH', self._url(ressource, name), data=data)

    def delete(self, ressource, name=None):
        return self._open('DELETE', self._url(ressource, name))

    def _connect(self):
        self.request = Request(
            headers={'Accept': 'application/json'},
            http_agent=self._http_agent(),
            url_username=self._module.params.get('elasticsearch_user', None),
            url_password=self._module.params.get('elasticsearch_password', None),
            client_cert=self._module.params.get('elasticsearch_cert', None),
            client_key=self._module.params.get('elasticsearch_key', None),
            ca_path=self._module.params.get('elasticsearch_cacert', None),
            force_basic_auth=True,
            validate_certs=self._module.params.get('validate_certs'),
        )

        self._server_info()

    def _server_info(self):
        code, data = self._open('GET', '{0}/_nodes/_local/plugins'.format(self._es_url))

        if code != 200 or 'nodes' not in data:
            self._module.fail_json(msg='Error talking to Elasticsearch {0}'.format(self._es_url),
                                   http_code=code,
                                   http_body=data)

        self.server = {}

    def _http_agent(self):
        return 'ansible-{0}/jiuka.opendistro.{1}'.format(self._module.ansible_version,
                                                         self._module_name)

    def _url(self, ressource, name=None):
        if name:
            return '{0}/_opendistro/_{1}/api/{2}/{3}'.format(self._es_url, self.PLUGIN, ressource, name)
        return '{0}/_opendistro/_{1}/api/{2}'.format(self._es_url, self.PLUGIN, ressource)

    def _open(self, method, url, data=None):
        headers = None

        if data:
            headers = {'Content-Type': 'application/json'}
            data = json.dumps(data)

        try:
            resp = self.request.open(method, url, data=data, headers=headers)

            code = resp.code
            body = resp.read()
        except urllib_error.HTTPError as e:
            code = e.code
            try:
                body = e.read()
            except AttributeError:
                body = ''
        except urllib_error.URLError as e:
            self._module.fail_json(msg=str(e.reason),
                                   method=method,
                                   url=url,
                                   data=data)

        try:
            data = json.loads(body)
        except Exception:
            data = body

        return code, data
Beispiel #12
0
class iControlRestSession(object):
    """Represents a session that communicates with a BigIP.

    This acts as a loose wrapper around Ansible's ``Request`` class. We're doing
    this as interim work until we move to the httpapi connector.
    """
    def __init__(self,
                 headers=None,
                 use_proxy=True,
                 force=False,
                 timeout=10,
                 validate_certs=True,
                 url_username=None,
                 url_password=None,
                 http_agent=None,
                 force_basic_auth=False,
                 follow_redirects='urllib2',
                 client_cert=None,
                 client_key=None,
                 cookies=None):
        self.request = Request(headers=headers,
                               use_proxy=use_proxy,
                               force=force,
                               timeout=timeout,
                               validate_certs=validate_certs,
                               url_username=url_username,
                               url_password=url_password,
                               http_agent=http_agent,
                               force_basic_auth=force_basic_auth,
                               follow_redirects=follow_redirects,
                               client_cert=client_cert,
                               client_key=client_key,
                               cookies=cookies)
        self.last_url = None

    def send(self, method, url, **kwargs):
        response = Response()

        # Set the last_url called
        #
        # This is used by the object destructor to erase the token when the
        # ModuleManager exits and destroys the iControlRestSession object
        self.last_url = url

        body = None
        data = kwargs.pop('data', None)
        json = kwargs.pop('json', None)

        if not data and json is not None:
            self.request.headers['Content-Type'] = 'application/json'
            body = _json.dumps(json)
            if not isinstance(body, bytes):
                body = body.encode('utf-8')
        if data:
            body = data
        kwargs['data'] = body

        try:
            result = self.request.open(method, url, **kwargs)
            response._content = result.read()
            response.status = result.getcode()
            response.url = result.geturl()
            response.msg = "OK (%s bytes)" % result.headers.get(
                'Content-Length', 'unknown')
        except Exception as e:
            try:
                response._content = e.read()
                response.status_code = e.code
            except AttributeError:
                response._content = ''
                response.status_code = '-1'

            response.reason = to_native(e)
        return response

    def delete(self, url, **kwargs):
        return self.send('DELETE', url, **kwargs)

    def get(self, url, **kwargs):
        return self.send('GET', url, **kwargs)

    def patch(self, url, data=None, **kwargs):
        return self.send('PATCH', url, data=data, **kwargs)

    def post(self, url, data=None, **kwargs):
        return self.send('POST', url, data=data, **kwargs)

    def put(self, url, data=None, **kwargs):
        return self.send('PUT', url, data=data, **kwargs)

    def __del__(self):
        if self.last_url is None:
            return
        token = self.request.headers.get('X-F5-Auth-Token', None)
        if not token:
            return
        p = generic_urlparse(urlparse(self.last_url))
        uri = "https://{0}:{1}/mgmt/shared/authz/tokens/{2}".format(
            p['hostname'], p['port'], token)
        self.delete(uri)
Beispiel #13
0
class Client:
    VALID_PREFIXES = "http://", "https://", "unix:///"

    def __init__(self, endpoint, username, password, verify, ca_path):
        valid_prefix = any(endpoint.startswith(p) for p in self.VALID_PREFIXES)
        if not valid_prefix:
            raise UnitError(
                "Endpoint should start with one of the following: {0}".format(
                    ", ".join(self.VALID_PREFIXES), ))

        if endpoint.startswith("unix://"):
            self._client = Request(unix_socket=endpoint[7:])
            self._host = "http://localhost"
        else:
            self._client = Request(
                force_basic_auth=True,
                validate_certs=verify,
                ca_path=ca_path,
                url_username=username,
                url_password=password,
            )
            self._host = endpoint.rstrip("/")

    def request(self, method, path, data=None):
        url = (self._host + "/" + "/".join(quote(s, safe="")
                                           for s in path)).rstrip("/")

        if data is not None:
            data = json.dumps(data, separators=(",", ":"))

        try:
            raw_resp = self._client.open(method=method, url=url, data=data)
            return Response(raw_resp.getcode(), raw_resp.read())
        except HTTPError as e:
            # This is not an error, since client consumers might be able to
            # work around/expect non 20x codes.
            return Response(e.code, e.reason)
        except URLError as e:
            raise UnitError("{0} request failed: {1}".format(method, e.reason))

    def get(self, path):
        r = self.request("GET", path)
        if r.status == 200:
            return r.json
        if r.status == 404:
            return {}

        raise UnitError("Invalid response: ({0}) - {1}".format(
            r.status, r.data))

    def put(self, path, data):
        # Any of the parrent sections might be missing at this point, so do
        # not fail on 404. Instead, incrementally build the payload until we
        # get a non-404 response back.
        #
        # Example: If the following request
        #
        #   POST /config/listeners/127.0.0.1:80
        #     {"pass": "******"127.0.0.1:80": {"pass": "******"listeners": {"127.0.0.1:80": {"pass": "******"PUT", path, data)
            if r.status == 200:
                # Success, we managed to get our data pushed to the server.
                return

            if r.status != 404:
                # Something bad happened. Stop being smart and bail.
                raise UnitError("Invalid response: ({0}) - {1}".format(
                    r.status, r.data))

            if not path:
                # We ran out of parent path segments. Bail.
                raise UnitError(
                    "Ran out of parent path segments. This probaly indicates "
                    "a bug in NGINX Unit Ansible Collection or in the NGINX "
                    "Unit itself. Please file an issue in the collection's "
                    "issue tracker. Thank you in advance for providing as "
                    "much relevant data in the issue as possible ;)")

            # Missing parent category. Retry push with expanded data on
            # parent path.
            data = {path[-1]: data}
            path = path[:-1]

    def delete(self, path):
        r = self.request("DELETE", path)
        # Yes, unit returns 200 on DELETE ...
        if r.status != 200:
            raise UnitError("Invalid response: ({0}) - {1}".format(
                r.status, r.data))
Beispiel #14
0
class vsz_api:

    api_endpoint_url = ''
    __api_user = ''
    __api_password = ''

    __request = None
    __ignore_ssl_validation = True

    __ansible_module = None

    def __init__(self, server, user, password, server_port=8443, use_ssl=True, ignore_ssl_validation=True):

        # Set api endpoint url
        if use_ssl:
            self.api_endpoint_url = self.api_endpoint_url + 'https://'
        else:
            self.api_endpoint_url = self.api_endpoint_url + 'http://'

        self.__api_password = password
        self.__api_user = user

        self.__ignore_ssl_validation = ignore_ssl_validation

        self.__request = Request()

        api_fetch_result, api_version_string = self.get_latest_api_version
        if not api_fetch_result:
            # TODO:: What we do if we got an error on the API Request
            # should we throw exception, but then it will not be pretty
        else:
            self.api_endpoint_url = self.api_endpoint_url + \
                server + ':' + str(server_port) + "/wsg/api/public/" + api_version_string

    def get_latest_api_version(self):
        
        apiInfo_result, apiInfo_response = self.__api_call('GET', '/wsg/api/public/apiInfo')
        if apiInfo_result:
            current_version_string = ""
            current_version_number = 0
            for version in apiInfo_response.apiSupportVersion:
                if current_version_number == 0:
                    current_version_string = version
                    current_version_number = version.split('_')[0].replace('v','')
                else:
                    version_number = version.split('_')[0].replace('v','')
                    if version_number > current_version_number:
                        current_version_string = version
                        current_version_number = version_number
            return True, current_version_string
        else:
            # Error on apiInfo request
            return False, apiInfo_response       

    def get_domain_id(self, domain_name):

        fetch_domain_result, data = self.get_domains()
        if not fetch_domain_result:
            return False, data
        
        for domain in data:
            if domain_name == domain['name']:
                return True, domain['id']
        
        # No domain found
        return False, "domain with name '" + domain_name + "' not found!"


    def get_domains(self):
        return self.__return_api_list(
            *(self.__api_call('GET','/domains?listSize=9999&recusively=True'))
        )

    def get_rkszone_id(self, zone_name, domain_id):
        fetch_zone_result, data = self.get_rkszones(domain_id)
        if not fetch_zone_result:
            return False, data
        
        for zone in data:
            if zone_name == zone['name']:
                return True, zone['id']
        
        # Zone not found
        return False, "zone with name '" + zone_name + "' not found!"

    def get_rkszones(self, domain_id):
        return self.__return_api_list(
            *(self.__api_call('GET','/rkszones?listSize=9999&domainId=' + domain_id))
        )

    def set_wlan_encryption(self, zone_id, wlan_id, wlan_encryption_settings):
        data = {
            "method" : wlan_encryption_settings['method'],
            "passphrase" : wlan_encryption_settings['passphrase']
        }
        return self.__api_call('PATCH','/rkszones/' + zone_id + '/wlans/' + wlan_id  + '/encryption', data=json.dumps(data))
        

    def get_wlan_settings(self, zone_id, wlan_id):
        result, data = self.__api_call('GET','/rkszones/' + zone_id + '/wlans/' + wlan_id)
        return result, data

    def get_wlan_id(self, zone_id, wlan_name):
        fetch_wlan_result, data = self.get_wlans(zone_id)
        if not fetch_wlan_result:
            return False, data
        
        for wlan in data:
            if wlan_name == wlan['name']:
                return True, wlan['id']

        # Wlan not found
        return False, "wlan with name '" + wlan_name + "' not found!"
    

    def get_wlans(self, zone_id):
        return self.__return_api_list(
            *(self.__api_call('GET','/rkszones/' + zone_id + '/wlans?listSize=99999'))
        )


    def login(self):
        data = dict(
            username=self.__api_user,
            password=self.__api_password,
            timeZoneUtcOffset="+01:00"
        )

        return self.__api_call(
            'POST','/session', 
            data=json.dumps(data),
        )

    def logout(self):
        return self.__api_call(
                'DELETE','/session'
            )
    

    # return list object of json response or pass error
    def __return_api_list(self,result,data):
        if not result:
            return result, data
        return result, data['list']

    # execute api calls
    def __api_call(self, http_method, api_method, data=None):
        if(type(data) is dict):
            data = json.dumps(data)
        try:
            request_result = self.__request.open(
                http_method, 
                (self.api_endpoint_url + api_method), 
                data = data,
                headers= {
                    "Content-Type": "application/json",
                    "Accept":"application/json"
                },
                validate_certs = not self.__ignore_ssl_validation
            )
        except urllib2.HTTPError as err:
            return False, err.msg
        except urllib2.URLError as err:
            return False, err.reason.strerror
        except SSLValidationError as err:
            return False, err.message
        except:
            return False, "unknown urllib2 exception"

        data = request_result.read()
        if data:
            return True, json.loads(data)

        return True, request_result.msg
Beispiel #15
0
class TowerRestClient:
    def __init__(self,
                 address,
                 username,
                 password,
                 validate_certs=False,
                 force_basic_auth=True):
        self._address = address
        self._username = username
        self._password = password
        self._validate_certs = validate_certs
        self._force_basic_auth = force_basic_auth
        self._headers = {}
        self._client = Request()

    def _request(self, method, path, payload=None):
        headers = self._headers.copy()
        data = None
        if payload:
            data = json.dumps(payload)
            headers["Content-Type"] = "application/json"

        url = self._address + path
        try:
            r = self._client.open(method,
                                  url,
                                  data=data,
                                  headers=headers,
                                  validate_certs=self._validate_certs,
                                  url_username=self._username,
                                  url_password=self._password,
                                  force_basic_auth=self._force_basic_auth)
            r_status = r.getcode()
            r_headers = dict(r.headers)
            data = r.read().decode("utf-8")
            r_data = json.loads(data) if data else {}
        except HTTPError as e:
            r_status = e.code
            r_headers = {}
            r_data = dict(msg=str(e.reason))
        except (ConnectionError, URLError) as e:
            raise AnsibleConnectionFailure(
                "Could not connect to {0}: {1}".format(url, e.reason))
        return r_status, r_headers, r_data

    def get(self, path):
        return self._request("GET", path)

    def post(self, path, payload=None):
        return self._request("POST", path, payload)

    def patch(self, path, payload=None):
        return self._request("PATCH", path, payload)

    def delete(self, path):
        return self._request("DELETE", path)

    def asset_exists(self, asset_name, asset_type):
        _status, _headers, _data = self.get('/api/v2/{}/?name={}'.format(
            asset_type, asset_name))

        if _data.get('count') != 0:
            return _data
        else:
            return None

    # EXPORTS
    def _export_project(self, name, resolve_dependencies):
        _status, _headers, _data = self.get(
            '/api/v2/projects/?name={}'.format(name))

        # Check HTTP status code for request
        if _status != 200:
            raise TowerConnectionError(
                "Ansible expected an HTTP response code of {} but got {}".
                format(200, _status))
        else:
            # Check if we found an asset matching provided name
            if _data["count"] == 0:
                raise TowerResourceNotFound(
                    "Project named {} was not found".format(name))
            else:
                # Store local copy of project json
                project = _data["results"][0]

                # Remove fields that cannot be imported
                for field in PROJECT_FIELDS_TO_REMOVE:
                    del (project[field])

                return project

    def _export_job_template(self, name, resolve_dependencies):
        pass

    def _export_workflow_job_template(self, name, resolve_dependencies):
        pass

    def export_asset(self, asset_type, asset_name, resolve_dependencies):

        if asset_type == "project":
            return self._export_project(asset_name, resolve_dependencies)

        elif asset_type == "job_template":
            return self._export_job_template(asset_name, resolve_dependencies)

        elif asset_type == "workflow_job_template":
            return self._export_workflow_job_template(asset_name,
                                                      resolve_dependencies)

    # IMPORTS
    def _import_project(self, project, update_asset):

        result = dict(imported=False, request=None)

        success_codes = [200, 201, 202, 203]

        asset_exists = self.asset_exists(project["name"], 'projects')

        if asset_exists:
            asset_id = asset_exists["results"][0]["id"]
            if update_asset:
                _status, _headers, _data = self.patch(
                    '/api/v2/projects/{}/'.format(asset_id), project)
            else:
                # raise error that asset already exists, try using update_asset if you want to override it
                raise TowerAssetExists(
                    message=
                    "An asset named '{}' already exists. If you'd like to overwrite this assets variables, set 'update_asset: true' in your playbook"
                    .format(project["name"]))
        else:
            _status, _headers, _data = self.post('/api/v2/projects/', project)

        result["request"] = _status, _headers, _data

        if _status in success_codes:  # Successful import
            result["imported"] = True
            return result
        else:
            return result  # return error here

    def import_asset(self, asset, update_asset):

        if asset["type"] == "project":
            return self._import_project(asset, update_asset)
class AHModule(AnsibleModule):
    url = None
    session = None
    AUTH_ARGSPEC = dict(
        ah_host=dict(required=False, aliases=["ah_hostname"], fallback=(env_fallback, ["AH_HOST"])),
        ah_username=dict(required=False, fallback=(env_fallback, ["AH_USERNAME"])),
        ah_password=dict(no_log=True, required=False, fallback=(env_fallback, ["AH_PASSWORD"])),
        ah_path_prefix=dict(required=False, fallback=(env_fallback, ["GALAXY_API_PATH_PREFIX"])),
        validate_certs=dict(type="bool", aliases=["ah_verify_ssl"], required=False, fallback=(env_fallback, ["AH_VERIFY_SSL"])),
        ah_token=dict(type="raw", no_log=True, required=False, fallback=(env_fallback, ["AH_API_TOKEN"])),
    )
    ENCRYPTED_STRING = "$encrypted$"
    short_params = {
        "host": "ah_host",
        "username": "******",
        "password": "******",
        "verify_ssl": "validate_certs",
        "path_prefix": "ah_path_prefix",
        "oauth_token": "ah_token",
    }
    IDENTITY_FIELDS = {}
    ENCRYPTED_STRING = "$encrypted$"
    host = "127.0.0.1"
    path_prefix = "galaxy"
    username = None
    password = None
    verify_ssl = True
    oauth_token = None
    authenticated = False
    error_callback = None
    warn_callback = None

    def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, require_auth=True, **kwargs):
        full_argspec = {}
        if require_auth:
            full_argspec.update(AHModule.AUTH_ARGSPEC)
        full_argspec.update(argument_spec)
        kwargs["supports_check_mode"] = True

        self.error_callback = error_callback
        self.warn_callback = warn_callback

        self.json_output = {"changed": False}

        if direct_params is not None:
            self.params = direct_params
        #        else:
        super(AHModule, self).__init__(argument_spec=full_argspec, **kwargs)
        self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl)

        # Parameters specified on command line will override settings in any config
        for short_param, long_param in self.short_params.items():
            direct_value = self.params.get(long_param)
            if direct_value is not None:
                setattr(self, short_param, direct_value)

        # Perform magic depending on whether ah_token is a string or a dict
        if self.params.get("ah_token"):
            token_param = self.params.get("ah_token")
            if type(token_param) is dict:
                if "token" in token_param:
                    self.oauth_token = self.params.get("ah_token")["token"]
                else:
                    self.fail_json(msg="The provided dict in ah_token did not properly contain the token entry")
            elif isinstance(token_param, string_types):
                self.oauth_token = self.params.get("ah_token")
            else:
                error_msg = "The provided ah_token type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__)
                self.fail_json(msg=error_msg)

        # Perform some basic validation
        if not re.match("^https{0,1}://", self.host):
            self.host = "https://{0}".format(self.host)

        # Try to parse the hostname as a url
        try:
            self.url = urlparse(self.host)
        except Exception as e:
            self.fail_json(msg="Unable to parse ah host as a URL ({1}): {0}".format(self.host, e))

        # Try to resolve the hostname
        hostname = self.url.netloc.split(":")[0]
        try:
            gethostbyname(hostname)
        except Exception as e:
            self.fail_json(msg="Unable to resolve ah host ({1}): {0}".format(hostname, e))

        if "update_secrets" in self.params:
            self.update_secrets = self.params.pop("update_secrets")
        else:
            self.update_secrets = True

    def build_url(self, endpoint, query_params=None):
        # Make sure we start with /api/vX
        if not endpoint.startswith("/"):
            endpoint = "/{0}".format(endpoint)
        if not endpoint.startswith("/api/"):
            if self.path_prefix == "galaxy":
                endpoint = "api/galaxy/v3{0}".format(endpoint)
            elif self.path_prefix == "galaxy":
                endpoint = "api/automation-hub/v3{0}".format(endpoint)
            else:
                endpoint = "api/{0}/v3{1}".format(self.path_prefix, endpoint)
        if not endpoint.endswith("/") and "?" not in endpoint:
            endpoint = "{0}/".format(endpoint)

        # Update the URL path with the endpoint
        url = self.url._replace(path=endpoint)

        if query_params:
            url = url._replace(query=urlencode(query_params))

        return url

    def fail_json(self, **kwargs):
        # Try to log out if we are authenticated
        if self.error_callback:
            self.error_callback(**kwargs)
        else:
            super(AHModule, self).fail_json(**kwargs)

    def exit_json(self, **kwargs):
        # Try to log out if we are authenticated
        super(AHModule, self).exit_json(**kwargs)

    def warn(self, warning):
        if self.warn_callback is not None:
            self.warn_callback(warning)
        else:
            super(AHModule, self).warn(warning)

    @staticmethod
    def get_name_field_from_endpoint(endpoint):
        return AHModule.IDENTITY_FIELDS.get(endpoint, "name")

    def get_endpoint(self, endpoint, *args, **kwargs):
        return self.make_request("GET", endpoint, **kwargs)

    def make_request(self, method, endpoint, *args, **kwargs):
        # In case someone is calling us directly; make sure we were given a method, let's not just assume a GET
        if not method:
            raise Exception("The HTTP method must be defined")

        # Extract the headers, this will be used in a couple of places
        headers = kwargs.get("headers", {})

        # Authenticate to Automation Hub (if we don't have a token and if not already done so)
        if not self.oauth_token and not self.authenticated:
            # This method will set a cookie in the cookie jar for us and also an oauth_token
            self.authenticate(**kwargs)
        if self.oauth_token:
            # If we have a oauth token, we just use a bearer header
            headers["Authorization"] = "Token {0}".format(self.oauth_token)
        if method in ["POST", "PUT", "PATCH"]:
            headers.setdefault("Content-Type", "application/json")
            kwargs["headers"] = headers
            url = self.build_url(endpoint)
        else:
            url = self.build_url(endpoint, query_params=kwargs.get("data"))

        data = None  # Important, if content type is not JSON, this should not be dict type
        if headers.get("Content-Type", "") == "application/json":
            data = dumps(kwargs.get("data", {}))
        elif kwargs.get("binary", False):
            data = kwargs.get("data", None)

        try:
            response = self.session.open(method, url.geturl(), headers=headers, validate_certs=self.verify_ssl, follow_redirects=True, data=data)
        except (SSLValidationError) as ssl_err:
            self.fail_json(msg="Could not establish a secure connection to your host ({1}): {0}.".format(url.netloc, ssl_err))
        except (ConnectionError) as con_err:
            self.fail_json(msg="There was a network error of some kind trying to connect to your host ({1}): {0}.".format(url.netloc, con_err))
        except (HTTPError) as he:
            # Sanity check: Did the server send back some kind of internal error?
            if he.code >= 500:
                self.fail_json(msg="The host sent back a server error ({1}): {0}. Please check the logs and try again later".format(url.path, he))
            # Sanity check: Did we fail to authenticate properly?  If so, fail out now; this is always a failure.
            elif he.code == 401:
                self.fail_json(msg="Invalid Automation Hub authentication credentials for {0} (HTTP 401).".format(url.path))
            # Sanity check: Did we get a forbidden response, which means that the user isn't allowed to do this? Report that.
            elif he.code == 403:
                self.fail_json(msg="You don't have permission to {1} to {0} (HTTP 403).".format(url.path, method))
            # Sanity check: Did we get a 404 response?
            # Requests with primary keys will return a 404 if there is no response, and we want to consistently trap these.
            elif he.code == 404:
                if kwargs.get("return_none_on_404", False):
                    return None
                if kwargs.get("return_errors_on_404", False):
                    page_data = he.read()
                    try:
                        return {"status_code": he.code, "json": loads(page_data)}
                    # JSONDecodeError only available on Python 3.5+
                    except ValueError:
                        return {"status_code": he.code, "text": page_data}
                self.fail_json(msg="The requested object could not be found at {0}.".format(url.path), response=he)
            # Sanity check: Did we get a 405 response?
            # A 405 means we used a method that isn't allowed. Usually this is a bad request, but it requires special treatment because the
            # API sends it as a logic error in a few situations (e.g. trying to cancel a job that isn't running).
            elif he.code == 405:
                self.fail_json(msg="The Automation Hub server says you can't make a request with the {0} method to this endpoing {1}".format(method, url.path))
            # Sanity check: Did we get some other kind of error?  If so, write an appropriate error message.
            elif he.code >= 400:
                # We are going to return a 400 so the module can decide what to do with it
                page_data = he.read()
                try:
                    return {"status_code": he.code, "json": loads(page_data)}
                # JSONDecodeError only available on Python 3.5+
                except ValueError:
                    return {"status_code": he.code, "text": page_data}
            elif he.code == 204 and method == "DELETE":
                # A 204 is a normal response for a delete function
                pass
            else:
                self.fail_json(msg="Unexpected return code when calling {0}: {1}".format(url.geturl(), he))
        except (Exception) as e:
            self.fail_json(msg="There was an unknown error when trying to connect to {2}: {0} {1}".format(type(e).__name__, e, url.geturl()))

        response_body = ""
        try:
            response_body = response.read()
        except (Exception) as e:
            self.fail_json(msg="Failed to read response body: {0}".format(e))

        response_json = {}
        if response_body and response_body != "":
            try:
                response_json = loads(response_body)
            except (Exception) as e:
                self.fail_json(msg="Failed to parse the response json: {0}".format(e))

        if PY2:
            status_code = response.getcode()
        else:
            status_code = response.status
        return {"status_code": status_code, "json": response_json}

    def get_one(self, endpoint, name_or_id=None, allow_none=True, **kwargs):
        new_kwargs = kwargs.copy()
        if name_or_id:
            name_field = self.get_name_field_from_endpoint(endpoint)
            new_data = kwargs.get("data", {}).copy()
            if name_field in new_data:
                self.fail_json(msg="You can't specify the field {0} in your search data if using the name_or_id field".format(name_field))

            try:
                new_data["or__id"] = int(name_or_id)
                new_data["or__{0}".format(name_field)] = name_or_id
            except ValueError:
                # If we get a value error, then we didn't have an integer so we can just pass and fall down to the fail
                new_data[name_field] = name_or_id
            new_kwargs["data"] = new_data

        response = self.get_endpoint(endpoint, **new_kwargs)
        if response["status_code"] != 200:
            fail_msg = "Got a {0} response when trying to get one from {1}".format(response["status_code"], endpoint)
            if "detail" in response.get("json", {}):
                fail_msg += ", detail: {0}".format(response["json"]["detail"])
            self.fail_json(msg=fail_msg)

        if "count" not in response["json"]["meta"] or "data" not in response["json"]:
            self.fail_json(msg="The endpoint did not provide count and results.")

        if response["json"]["meta"]["count"] == 0:
            if allow_none:
                return None
            else:
                self.fail_wanted_one(response, endpoint, new_kwargs.get("data"))
        elif response["json"]["meta"]["count"] > 1:
            if name_or_id:
                # Since we did a name or ID search and got > 1 return something if the id matches
                for asset in response["json"]["data"]:
                    if str(asset["id"]) == name_or_id:
                        return self.existing_item_add_url(asset, endpoint)

            # We got > 1 and either didn't find something by ID (which means multiple names)
            # Or we weren't running with a or search and just got back too many to begin with.
            self.fail_wanted_one(response, endpoint, new_kwargs.get("data"))

        return self.existing_item_add_url(response["json"]["data"][0], endpoint)

    def get_only(self, endpoint, name_or_id=None, allow_none=True, key="url", **kwargs):
        new_kwargs = kwargs.copy()
        if name_or_id:
            name_field = self.get_name_field_from_endpoint(endpoint)
            new_data = kwargs.get("data", {}).copy()
            if name_field in new_data:
                self.fail_json(msg="You can't specify the field {0} in your search data if using the name_or_id field".format(name_field))

            try:
                new_data["or__id"] = int(name_or_id)
                new_data["or__{0}".format(name_field)] = name_or_id
            except ValueError:
                # If we get a value error, then we didn't have an integer so we can just pass and fall down to the fail
                new_data[name_field] = name_or_id
            new_kwargs["data"] = new_data

        response = self.get_endpoint(endpoint, **new_kwargs)
        if response["status_code"] != 200:
            fail_msg = "Got a {0} response when trying to get from {1}".format(response["status_code"], endpoint)
            if "detail" in response.get("json", {}):
                fail_msg += ", detail: {0}".format(response["json"]["detail"])
            self.fail_json(msg=fail_msg)

        return self.existing_item_add_url(response["json"], endpoint, key=key)

    def authenticate(self, **kwargs):
        if self.username and self.password:
            # Attempt to get a token from /v3/auth/token/ by giving it our username/password combo
            # If we have a username and password, we need to get a session cookie

            # Post to the tokens endpoint with baisc auth to try and get a token
            if self.path_prefix == "galaxy":
                api_token_url = (self.url._replace(path="/api/galaxy/v3/auth/token/")).geturl()
            elif self.path_prefix == "automation-hub":
                api_token_url = (self.url._replace(path="/api/automation-hub/v3/auth/token/")).geturl()
            else:
                token_path = "api/{0}/v3/auth/token/".format(self.path_prefix)
                api_token_url = (self.url._replace(path=token_path)).geturl()

            try:
                response = self.session.open(
                    "POST",
                    api_token_url,
                    validate_certs=self.verify_ssl,
                    follow_redirects=True,
                    force_basic_auth=True,
                    url_username=self.username,
                    url_password=self.password,
                    headers={"Content-Type": "application/json"},
                )
            except HTTPError as he:
                try:
                    resp = he.read()
                except Exception as e:
                    resp = "unknown {0}".format(e)
                self.fail_json(msg="Failed to get token: {0}".format(he), response=resp)
            except (Exception) as e:
                # Sanity check: Did the server send back some kind of internal error?
                self.fail_json(msg="Failed to get token: {0}".format(e))

            token_response = None
            try:
                token_response = response.read()
                response_json = loads(token_response)
                self.oauth_token = response_json["token"]
            except (Exception) as e:
                self.fail_json(msg="Failed to extract token information from login response: {0}".format(e), **{"response": token_response})

        # If we have neither of these, then we can try un-authenticated access
        self.authenticated = True

    def existing_item_add_url(self, existing_item, endpoint, key="url"):
        # Add url and type to response as its missing in current iteration of Automation Hub.
        existing_item[key] = "{0}{1}/".format(self.build_url(endpoint).geturl()[len(self.host) :], existing_item["name"])
        existing_item["type"] = endpoint
        return existing_item

    def delete_if_needed(self, existing_item, on_delete=None, auto_exit=True):
        # This will exit from the module on its own.
        # If the method successfully deletes an item and on_delete param is defined,
        #   the on_delete parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #   1. None if the existing_item is not defined (so no delete needs to happen)
        #   2. The response from Automation Hub from calling the delete on the endpont. It's up to you to process the response and exit from the module
        # Note: common error codes from the Automation Hub API can cause the module to fail
        if existing_item:
            if existing_item["type"] == "token":
                response = self.delete_endpoint(existing_item["endpoint"])
        elif existing_item:
            # If we have an item, we can try to delete it
            try:
                item_url = existing_item["url"]
                item_type = existing_item["type"]
                item_id = existing_item["id"]
                item_name = self.get_item_name(existing_item, allow_unknown=True)
            except KeyError as ke:
                self.fail_json(msg="Unable to process delete of item due to missing data {0}".format(ke))

            response = self.delete_endpoint(item_url)
        else:
            if auto_exit:
                self.exit_json(**self.json_output)
            else:
                return self.json_output

        if response["status_code"] in [202, 204]:
            if on_delete:
                on_delete(self, response["json"])
            self.json_output["changed"] = True
            if existing_item["type"] == "token":
                self.json_output["msg"] = "Token Revoked"
                self.exit_json(**self.json_output)
            else:
                self.json_output["id"] = item_id
                self.exit_json(**self.json_output)
            if auto_exit:
                self.exit_json(**self.json_output)
            else:
                return self.json_output
        else:
            if "json" in response and "__all__" in response["json"]:
                self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response["json"]["__all__"][0]))
            elif "json" in response:
                # This is from a project delete (if there is an active job against it)
                if "error" in response["json"]:
                    self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response["json"]["error"]))
                else:
                    self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response["json"]))
            else:
                self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response["status_code"]))

    def get_item_name(self, item, allow_unknown=False):
        if item:
            if "name" in item:
                return item["name"]

        if allow_unknown:
            return "unknown"

        if item:
            self.exit_json(msg="Cannot determine identity field for {0} object.".format(item.get("type", "unknown")))
        else:
            self.exit_json(msg="Cannot determine identity field for Undefined object.")

    def delete_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output["changed"] = True
            self.exit_json(**self.json_output)

        return self.make_request("DELETE", endpoint, **kwargs)

    def create_or_update_if_needed(
        self,
        existing_item,
        new_item,
        endpoint=None,
        item_type="unknown",
        on_create=None,
        on_update=None,
        auto_exit=True,
        associations=None,
        require_id=True,
        fixed_url=None,
    ):
        if existing_item:
            return self.update_if_needed(
                existing_item, new_item, on_update=on_update, auto_exit=auto_exit, associations=associations, require_id=require_id, fixed_url=fixed_url
            )
        else:
            return self.create_if_needed(
                existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, auto_exit=auto_exit, associations=associations
            )

    def create_if_needed(self, existing_item, new_item, endpoint, on_create=None, auto_exit=True, item_type="unknown", associations=None):

        # This will exit from the module on its own
        # If the method successfully creates an item and on_create param is defined,
        #    the on_create parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #    1. None if the existing_item is already defined (so no create needs to happen)
        #    2. The response from Automation Hub from calling the patch on the endpont. It's up to you to process the response and exit from the module
        # Note: common error codes from the Automation Hub API can cause the module to fail

        if not endpoint:
            self.fail_json(msg="Unable to create new {0} due to missing endpoint".format(item_type))

        item_url = None
        if existing_item:
            try:
                item_url = existing_item["url"]
            except KeyError as ke:
                self.fail_json(msg="Unable to process create of item due to missing data {0}".format(ke))
        else:
            # If we don't have an exisitng_item, we can try to create it

            # We have to rely on item_type being passed in since we don't have an existing item that declares its type
            # We will pull the item_name out from the new_item, if it exists
            item_name = self.get_item_name(new_item, allow_unknown=True)

            response = self.post_endpoint(endpoint, **{"data": new_item})

            if response["status_code"] in [200, 201]:
                self.json_output["name"] = "unknown"
                for key in ("name", "username", "identifier", "hostname"):
                    if key in response["json"]:
                        self.json_output["name"] = response["json"][key]
                if item_type != "token":
                    self.json_output["id"] = response["json"]["id"]
                    item_url = "{0}{1}/".format(self.build_url(endpoint).geturl()[len(self.host) :], new_item["name"])
                self.json_output["changed"] = True
            else:
                if "json" in response and "__all__" in response["json"]:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response["json"]["__all__"][0]))
                elif "json" in response:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response["json"]))
                else:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response["status_code"]))

        # Process any associations with this item
        if associations is not None:
            for association_type in associations:
                sub_endpoint = "{0}{1}/".format(item_url, association_type)
                self.modify_associations(sub_endpoint, associations[association_type])

        # If we have an on_create method and we actually changed something we can call on_create
        if on_create is not None and self.json_output["changed"]:
            on_create(self, response["json"])
        elif auto_exit:
            self.exit_json(**self.json_output)
        else:
            last_data = response["json"]
            return last_data

    def approve(self, endpoint, auto_exit=True):

        approvalEndpoint = "move/staging/published"

        if not endpoint:
            self.fail_json(msg="Unable to approve due to missing endpoint")

        response = self.post_endpoint("{0}/{1}".format(endpoint, approvalEndpoint), None, **{"return_none_on_404": True})

        i = 0
        while i < 5:
            if not response:
                time.sleep(1)
                response = self.post_endpoint("{0}/{1}".format(endpoint, approvalEndpoint), None, **{"return_none_on_404": True})
                i += 1
            else:
                break

        if response and response["status_code"] in [202]:
            self.json_output["changed"] = True
        else:
            # Do a check to see if the version exists
            if not response:
                self.fail_json(msg="Unable to approve at {0}: Awaiting approval not found".format(endpoint))
            elif "json" in response and "__all__" in response["json"]:
                self.fail_json(msg="Unable to approve at {0}: {1}".format(endpoint, response["json"]["__all__"][0]))
            elif "json" in response:
                self.fail_json(msg="Unable to create {0}: {1}".format(endpoint, response["json"]))
            else:
                self.fail_json(msg="Unable to create {0}: {1}".format(endpoint, response["status_code"]))

        if auto_exit:
            self.exit_json(**self.json_output)
        else:
            last_data = response["json"]
            return last_data

    def prepare_multipart(self, filename):
        mime = "application/x-gzip"
        m = email.mime.multipart.MIMEMultipart("form-data")

        main_type, sep, sub_type = mime.partition("/")

        with open(to_bytes(filename, errors="surrogate_or_strict"), "rb") as f:
            part = email.mime.application.MIMEApplication(f.read())
            del part["Content-Type"]
            part.add_header("Content-Type", "%s/%s" % (main_type, sub_type))

        part.add_header("Content-Disposition", "form-data")
        del part["MIME-Version"]
        part.set_param("name", "file", header="Content-Disposition")
        if filename:
            part.set_param("filename", to_native(os.path.basename(filename)), header="Content-Disposition")

        m.attach(part)

        if PY3:
            # Ensure headers are not split over multiple lines
            # The HTTP policy also uses CRLF by default
            b_data = m.as_bytes(policy=email.policy.HTTP)
        else:
            # Py2
            # We cannot just call ``as_string`` since it provides no way
            # to specify ``maxheaderlen``
            # cStringIO seems to be required here
            fp = cStringIO()  # noqa: F821
            # Ensure headers are not split over multiple lines
            g = email.generator.Generator(fp, maxheaderlen=0)
            g.flatten(m)
            # ``fix_eols`` switches from ``\n`` to ``\r\n``
            b_data = email.utils.fix_eols(fp.getvalue())
        del m

        headers, sep, b_content = b_data.partition(b"\r\n\r\n")
        del b_data

        if PY3:
            parser = email.parser.BytesHeaderParser().parsebytes
        else:
            # Py2
            parser = email.parser.HeaderParser().parsestr

        return (parser(headers)["content-type"], b_content)  # Message converts to native strings

    def getFileContent(self, path):
        with open(to_bytes(path, errors="surrogate_or_strict"), "rb") as f:
            b_file_data = f.read()
        return to_text(b_file_data)

    def wait_for_complete(self, task_url):
        endpoint = task_url
        state = "running"
        while state == "running":
            response = self.get_endpoint(endpoint)
            state = response["json"]["state"]
            time.sleep(1)
        self.json_output["state"] = state
        if state == "failed":
            self.fail_json(msg="Upload of collection failed: {0}".format(response["json"]["error"]["description"]))
        else:
            time.sleep(1)
            return

    def upload(self, path, endpoint, wait=True, item_type="unknown"):
        if "://" in path:
            tmppath = fetch_file(self, path)
            path = ".".join(tmppath.split(".")[:-2]) + ".tar.gz"
            os.rename(tmppath, path)
            self.add_cleanup_file(path)
        ct, body = self.prepare_multipart(path)
        response = self.make_request("POST", endpoint, **{"data": body, "headers": {"Content-Type": str(ct)}, "binary": True, "return_errors_on_404": True})
        if response["status_code"] in [202]:
            self.json_output["path"] = path
            self.json_output["changed"] = True
            if wait:
                self.wait_for_complete(response["json"]["task"])
            return
        else:
            if "json" in response and "__all__" in response["json"]:
                self.fail_json(msg="Unable to create {0} from {1}: {2}".format(item_type, path, response["json"]["__all__"][0]))
            elif "json" in response and "errors" in response["json"] and "detail" in response["json"]["errors"][0]:
                self.fail_json(msg="Unable to create {0} from {1}: {2}".format(item_type, path, response["json"]["errors"][0]["detail"]))
            elif "json" in response:
                self.fail_json(msg="Unable to create {0} from {1}: {2}".format(item_type, path, response["json"]))
            else:
                self.fail_json(msg="Unable to create {0} from {1}: {2}".format(item_type, path, response["status_code"]))

    def update_if_needed(self, existing_item, new_item, on_update=None, auto_exit=True, associations=None, require_id=True, fixed_url=None):
        # This will exit from the module on its own
        # If the method successfully updates an item and on_update param is defined,
        #   the on_update parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #    1. None if the existing_item does not need to be updated
        #    2. The response from Automation Hub from patching to the endpoint. It's up to you to process the response and exit from the module.
        # Note: common error codes from the Automation Hub API can cause the module to fail
        response = None
        if existing_item:
            # If we have an item, we can see if it needs an update
            try:
                item_url = fixed_url or existing_item["url"]
                item_type = existing_item["type"]
                item_name = existing_item["name"]
                item_id = require_id and existing_item["id"]
            except KeyError as ke:
                self.fail_json(msg="Unable to process update of item due to missing data {0}".format(ke))

            # Check to see if anything within the item requires the item to be updated
            needs_patch = self.objects_could_be_different(existing_item, new_item)

            # If we decided the item needs to be updated, update it
            self.json_output["id"] = item_id
            self.json_output["name"] = item_name
            self.json_output["type"] = item_type
            if needs_patch:
                response = self.put_endpoint(item_url, **{"data": new_item})
                if response["status_code"] == 200:
                    # compare apples-to-apples, old API data to new API data
                    # but do so considering the fields given in parameters
                    self.json_output["changed"] = self.objects_could_be_different(existing_item, response["json"], field_set=new_item.keys(), warning=True)
                elif "json" in response and "__all__" in response["json"]:
                    self.fail_json(msg=response["json"]["__all__"])
                else:
                    self.fail_json(**{"msg": "Unable to update {0} {1}, see response".format(item_type, item_name), "response": response, "input": new_item})

        else:
            raise RuntimeError("update_if_needed called incorrectly without existing_item")

        # Process any associations with this item
        if associations is not None:
            for association_type, id_list in associations.items():
                endpoint = "{0}{1}/".format(item_url, association_type)
                self.modify_associations(endpoint, id_list)

        # If we change something and have an on_change call it
        if on_update is not None and self.json_output["changed"]:
            if response is None:
                last_data = existing_item
            else:
                last_data = response["json"]
            on_update(self, last_data)
        elif auto_exit:
            self.exit_json(**self.json_output)
        else:
            if response is None:
                last_data = existing_item
            else:
                last_data = response["json"]
            return last_data

    def modify_associations(self, association_endpoint, new_association_list):
        # if we got None instead of [] we are not modifying the association_list
        if new_association_list is None:
            return

        # First get the existing associations
        response = self.get_all_endpoint(association_endpoint)
        existing_associated_ids = [association["id"] for association in response["json"]["results"]]

        # Disassociate anything that is in existing_associated_ids but not in new_association_list
        ids_to_remove = list(set(existing_associated_ids) - set(new_association_list))
        for an_id in ids_to_remove:
            response = self.post_endpoint(association_endpoint, **{"data": {"id": int(an_id), "disassociate": True}})
            if response["status_code"] == 204:
                self.json_output["changed"] = True
            else:
                self.fail_json(msg="Failed to disassociate item {0}".format(response["json"].get("detail", response["json"])))

        # Associate anything that is in new_association_list but not in `association`
        for an_id in list(set(new_association_list) - set(existing_associated_ids)):
            response = self.post_endpoint(association_endpoint, **{"data": {"id": int(an_id)}})
            if response["status_code"] == 204:
                self.json_output["changed"] = True
            else:
                self.fail_json(msg="Failed to associate item {0}".format(response["json"].get("detail", response["json"])))

    def post_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output["changed"] = True
            self.exit_json(**self.json_output)

        return self.make_request("POST", endpoint, **kwargs)

    def patch_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output["changed"] = True
            self.exit_json(**self.json_output)

        return self.make_request("PATCH", endpoint, **kwargs)

    def put_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output["changed"] = True
            self.exit_json(**self.json_output)

        return self.make_request("PUT", endpoint, **kwargs)

    def get_all_endpoint(self, endpoint, *args, **kwargs):
        response = self.get_endpoint(endpoint, *args, **kwargs)
        if "next" not in response["json"]:
            raise RuntimeError("Expected list from API at {0}, got: {1}".format(endpoint, response))
        next_page = response["json"]["next"]

        if response["json"]["count"] > 10000:
            self.fail_json(msg="The number of items being queried for is higher than 10,000.")

        while next_page is not None:
            next_response = self.get_endpoint(next_page)
            response["json"]["results"] = response["json"]["results"] + next_response["json"]["results"]
            next_page = next_response["json"]["next"]
            response["json"]["next"] = next_page
        return response

    def fail_wanted_one(self, response, endpoint, query_params):
        sample = response.copy()
        if len(sample["json"]["data"]) > 1:
            sample["json"]["data"] = sample["json"]["data"][:2] + ["...more results snipped..."]
        url = self.build_url(endpoint, query_params)
        display_endpoint = url.geturl()[len(self.host) :]  # truncate to not include the base URL
        self.fail_json(
            msg="Request to {0} returned {1} items, expected 1".format(display_endpoint, response["json"]["meta"]["count"]),
            query=query_params,
            response=sample,
            total_results=response["json"]["meta"]["count"],
        )

    def get_exactly_one(self, endpoint, name_or_id=None, **kwargs):
        return self.get_one(endpoint, name_or_id=name_or_id, allow_none=False, **kwargs)

    def resolve_name_to_id(self, endpoint, name_or_id):
        return self.get_exactly_one(endpoint, name_or_id)["id"]

    def objects_could_be_different(self, old, new, field_set=None, warning=False):
        if field_set is None:
            field_set = set(fd for fd in new.keys() if fd not in ("modified", "related", "summary_fields"))
        for field in field_set:
            new_field = new.get(field, None)
            old_field = old.get(field, None)
            if old_field != new_field:
                if self.update_secrets or (not self.fields_could_be_same(old_field, new_field)):
                    return True  # Something doesn't match, or something might not match
            elif self.has_encrypted_values(new_field) or field not in new:
                if self.update_secrets or (not self.fields_could_be_same(old_field, new_field)):
                    # case of 'field not in new' - user password write-only field that API will not display
                    self._encrypted_changed_warning(field, old, warning=warning)
                    return True
        return False

    def execute_build(self, path, force, output_path):
        path = self._resolve_path(path)
        output_path = self._resolve_path(output_path)
        b_output_path = to_bytes(output_path, errors="surrogate_or_strict")

        if not os.path.exists(b_output_path):
            os.makedirs(b_output_path)
        elif os.path.isfile(b_output_path):
            self.fail_json(msg="the output collection directory {0} is a file - aborting".format(to_native(output_path)))

        output_build = self.run_command(["ansible-galaxy", "collection", "build", path, "--output-path", output_path, (None, "--force")[force]])
        if output_build[0] == 0:
            self.json_output["path"] = "/" + "/".join(output_build[1].split("/")[1:])[:-1]
            self.json_output["changed"] = True
            self.exit_json(**self.json_output)
        else:
            self.fail_json(msg=output_build[2])

    def wait_sync_output(self, response):
        for k in ("task_id", "state", "started_at", "finished_at"):
            self.json_output[k] = response["last_sync_task"].get(k)

    @staticmethod
    def _resolve_path(path):
        return os.path.abspath(os.path.expanduser(os.path.expandvars(path)))

    @staticmethod
    def has_encrypted_values(obj):
        """Returns True if JSON-like python content in obj has $encrypted$
        anywhere in the data as a value
        """
        if isinstance(obj, dict):
            for val in obj.values():
                if AHModule.has_encrypted_values(val):
                    return True
        elif isinstance(obj, list):
            for val in obj:
                if AHModule.has_encrypted_values(val):
                    return True
        elif obj == AHModule.ENCRYPTED_STRING:
            return True
        return False

    def _encrypted_changed_warning(self, field, old, warning=False):
        if not warning:
            return
        self.warn(
            "The field {0} of {1} {2} has encrypted data and may inaccurately report task is changed.".format(
                field, old.get("type", "unknown"), old.get("id", "unknown")
            )
        )
Beispiel #17
0
class Client:
    def __init__(
        self,
        host,
        username=None,
        password=None,
        grant_type=None,
        refresh_token=None,
        client_id=None,
        client_secret=None,
        timeout=None,
    ):
        if not (host or "").startswith(("https://", "http://")):
            raise ServiceNowError(
                "Invalid instance host value: '{0}'. "
                "Value must start with 'https://' or 'http://'".format(host))

        self.host = host
        self.username = username
        self.password = password
        self.grant_type = grant_type
        self.client_id = client_id
        self.client_secret = client_secret
        self.refresh_token = refresh_token
        self.timeout = timeout

        self._auth_header = None
        self._client = Request()

    @property
    def auth_header(self):
        if not self._auth_header:
            self._auth_header = self._login()
        return self._auth_header

    def _login(self):
        if self.client_id and self.client_secret:
            return self._login_oauth()
        return self._login_username_password()

    def _login_username_password(self):
        return dict(
            Authorization=basic_auth_header(self.username, self.password))

    def _login_oauth(self):
        if self.grant_type == "refresh_token":
            auth_data = urlencode(
                dict(
                    grant_type=self.grant_type,
                    refresh_token=self.refresh_token,
                    client_id=self.client_id,
                    client_secret=self.client_secret,
                ))
        # Only other possible value for grant_type is "password"
        else:
            auth_data = urlencode(
                dict(
                    grant_type=self.grant_type,
                    username=self.username,
                    password=self.password,
                    client_id=self.client_id,
                    client_secret=self.client_secret,
                ))
        resp = self._request(
            "POST",
            "{0}/oauth_token.do".format(self.host),
            data=auth_data,
            headers=dict(Accept="application/json"),
        )
        if resp.status != 200:
            raise UnexpectedAPIResponse(resp.status, resp.data)

        access_token = resp.json["access_token"]
        return dict(Authorization="Bearer {0}".format(access_token))

    def _request(self, method, path, data=None, headers=None):
        try:
            raw_resp = self._client.open(method,
                                         path,
                                         data=data,
                                         headers=headers,
                                         timeout=self.timeout)
        except HTTPError as e:
            # Wrong username/password, or expired access token
            if e.code == 401:
                raise AuthError(
                    "Failed to authenticate with the instance: {0} {1}".format(
                        e.code, e.reason), )
            # Other HTTP error codes do not necessarily mean errors.
            # This is for the caller to decide.
            return Response(e.code, e.read(), e.headers)
        except URLError as e:
            raise ServiceNowError(e.reason)

        if PY2:
            return Response(raw_resp.getcode(), raw_resp.read(),
                            raw_resp.info())
        return Response(raw_resp.status, raw_resp.read(), raw_resp.headers)

    def request(self,
                method,
                path,
                query=None,
                data=None,
                headers=None,
                bytes=None):
        # Make sure we only have one kind of payload
        if data is not None and bytes is not None:
            raise AssertionError(
                "Cannot have JSON and binary payload in a single request.")

        escaped_path = quote(path.strip("/"))
        if escaped_path:
            escaped_path = "/" + escaped_path
        url = "{0}{1}".format(self.host, escaped_path)
        if query:
            url = "{0}?{1}".format(url, urlencode(query))
        headers = dict(headers or DEFAULT_HEADERS, **self.auth_header)
        if data is not None:
            data = json.dumps(data, separators=(",", ":"))
            headers["Content-type"] = "application/json"
        elif bytes is not None:
            data = bytes
        return self._request(method, url, data=data, headers=headers)

    def get(self, path, query=None):
        resp = self.request("GET", path, query=query)
        if resp.status in (200, 404):
            return resp
        raise UnexpectedAPIResponse(resp.status, resp.data)

    def post(self, path, data, query=None):
        resp = self.request("POST", path, data=data, query=query)
        if resp.status == 201:
            return resp
        raise UnexpectedAPIResponse(resp.status, resp.data)

    def patch(self, path, data, query=None):
        resp = self.request("PATCH", path, data=data, query=query)
        if resp.status == 200:
            return resp
        raise UnexpectedAPIResponse(resp.status, resp.data)

    def put(self, path, data, query=None):
        resp = self.request("PUT", path, data=data, query=query)
        if resp.status == 200:
            return resp
        raise UnexpectedAPIResponse(resp.status, resp.data)

    def delete(self, path, query=None):
        resp = self.request("DELETE", path, query=query)
        if resp.status != 204:
            raise UnexpectedAPIResponse(resp.status, resp.data)
Beispiel #18
0
class TowerAPIModule(TowerModule):
    # TODO: Move the collection version check into tower_module.py
    # This gets set by the make process so whatever is in here is irrelevant
    _COLLECTION_VERSION = "0.0.1-devel"
    _COLLECTION_TYPE = "awx"
    # This maps the collections type (awx/tower) to the values returned by the API
    # Those values can be found in awx/api/generics.py line 204
    collection_to_version = {
        'awx': 'AWX',
        'tower': 'Red Hat Ansible Tower',
    }
    session = None
    IDENTITY_FIELDS = {
        'users': 'username',
        'workflow_job_template_nodes': 'identifier',
        'instances': 'hostname'
    }
    ENCRYPTED_STRING = "$encrypted$"

    def __init__(self,
                 argument_spec,
                 direct_params=None,
                 error_callback=None,
                 warn_callback=None,
                 **kwargs):
        kwargs['supports_check_mode'] = True

        super(TowerAPIModule, self).__init__(argument_spec=argument_spec,
                                             direct_params=direct_params,
                                             error_callback=error_callback,
                                             warn_callback=warn_callback,
                                             **kwargs)
        self.session = Request(cookies=CookieJar(),
                               validate_certs=self.verify_ssl)

        if 'update_secrets' in self.params:
            self.update_secrets = self.params.pop('update_secrets')
        else:
            self.update_secrets = True

    @staticmethod
    def param_to_endpoint(name):
        exceptions = {
            'inventory': 'inventories',
            'target_team': 'teams',
            'workflow': 'workflow_job_templates'
        }
        return exceptions.get(name, '{0}s'.format(name))

    @staticmethod
    def get_name_field_from_endpoint(endpoint):
        return TowerAPIModule.IDENTITY_FIELDS.get(endpoint, 'name')

    def get_item_name(self, item, allow_unknown=False):
        if item:
            if 'name' in item:
                return item['name']

            for field_name in TowerAPIModule.IDENTITY_FIELDS.values():
                if field_name in item:
                    return item[field_name]

            if item.get('type', None) in ('o_auth2_access_token',
                                          'credential_input_source'):
                return item['id']

        if allow_unknown:
            return 'unknown'

        if item:
            self.exit_json(
                msg='Cannot determine identity field for {0} object.'.format(
                    item.get('type', 'unknown')))
        else:
            self.exit_json(
                msg='Cannot determine identity field for Undefined object.')

    def head_endpoint(self, endpoint, *args, **kwargs):
        return self.make_request('HEAD', endpoint, **kwargs)

    def get_endpoint(self, endpoint, *args, **kwargs):
        return self.make_request('GET', endpoint, **kwargs)

    def patch_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('PATCH', endpoint, **kwargs)

    def post_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('POST', endpoint, **kwargs)

    def delete_endpoint(self, endpoint, *args, **kwargs):
        # Handle check mode
        if self.check_mode:
            self.json_output['changed'] = True
            self.exit_json(**self.json_output)

        return self.make_request('DELETE', endpoint, **kwargs)

    def get_all_endpoint(self, endpoint, *args, **kwargs):
        response = self.get_endpoint(endpoint, *args, **kwargs)
        if 'next' not in response['json']:
            raise RuntimeError(
                'Expected list from API at {0}, got: {1}'.format(
                    endpoint, response))
        next_page = response['json']['next']

        if response['json']['count'] > 10000:
            self.fail_json(
                msg=
                'The number of items being queried for is higher than 10,000.')

        while next_page is not None:
            next_response = self.get_endpoint(next_page)
            response['json']['results'] = response['json'][
                'results'] + next_response['json']['results']
            next_page = next_response['json']['next']
            response['json']['next'] = next_page
        return response

    def get_one(self, endpoint, name_or_id=None, allow_none=True, **kwargs):
        new_kwargs = kwargs.copy()
        if name_or_id:
            name_field = self.get_name_field_from_endpoint(endpoint)
            new_data = kwargs.get('data', {}).copy()
            if name_field in new_data:
                self.fail_json(
                    msg=
                    "You can't specify the field {0} in your search data if using the name_or_id field"
                    .format(name_field))

            try:
                new_data['or__id'] = int(name_or_id)
                new_data['or__{0}'.format(name_field)] = name_or_id
            except ValueError:
                # If we get a value error, then we didn't have an integer so we can just pass and fall down to the fail
                new_data[name_field] = name_or_id
            new_kwargs['data'] = new_data

        response = self.get_endpoint(endpoint, **new_kwargs)
        if response['status_code'] != 200:
            fail_msg = "Got a {0} response when trying to get one from {1}".format(
                response['status_code'], endpoint)
            if 'detail' in response.get('json', {}):
                fail_msg += ', detail: {0}'.format(response['json']['detail'])
            self.fail_json(msg=fail_msg)

        if 'count' not in response['json'] or 'results' not in response['json']:
            self.fail_json(
                msg="The endpoint did not provide count and results")

        if response['json']['count'] == 0:
            if allow_none:
                return None
            else:
                self.fail_wanted_one(response, endpoint,
                                     new_kwargs.get('data'))
        elif response['json']['count'] > 1:
            if name_or_id:
                # Since we did a name or ID search and got > 1 return something if the id matches
                for asset in response['json']['results']:
                    if str(asset['id']) == name_or_id:
                        return asset
            # We got > 1 and either didn't find something by ID (which means multiple names)
            # Or we weren't running with a or search and just got back too many to begin with.
            self.fail_wanted_one(response, endpoint, new_kwargs.get('data'))

        return response['json']['results'][0]

    def fail_wanted_one(self, response, endpoint, query_params):
        sample = response.copy()
        if len(sample['json']['results']) > 1:
            sample['json']['results'] = sample['json']['results'][:2] + [
                '...more results snipped...'
            ]
        url = self.build_url(endpoint, query_params)
        display_endpoint = url.geturl()[len(
            self.host):]  # truncate to not include the base URL
        self.fail_json(
            msg="Request to {0} returned {1} items, expected 1".format(
                display_endpoint, response['json']['count']),
            query=query_params,
            response=sample,
            total_results=response['json']['count'],
        )

    def get_exactly_one(self, endpoint, name_or_id=None, **kwargs):
        return self.get_one(endpoint,
                            name_or_id=name_or_id,
                            allow_none=False,
                            **kwargs)

    def resolve_name_to_id(self, endpoint, name_or_id):
        return self.get_exactly_one(endpoint, name_or_id)['id']

    def make_request(self, method, endpoint, *args, **kwargs):
        # In case someone is calling us directly; make sure we were given a method, let's not just assume a GET
        if not method:
            raise Exception("The HTTP method must be defined")

        if method in ['POST', 'PUT', 'PATCH']:
            url = self.build_url(endpoint)
        else:
            url = self.build_url(endpoint, query_params=kwargs.get('data'))

        # Extract the headers, this will be used in a couple of places
        headers = kwargs.get('headers', {})

        # Authenticate to Tower (if we don't have a token and if not already done so)
        if not self.oauth_token and not self.authenticated:
            # This method will set a cookie in the cookie jar for us and also an oauth_token
            self.authenticate(**kwargs)
        if self.oauth_token:
            # If we have a oauth token, we just use a bearer header
            headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token)

        if method in ['POST', 'PUT', 'PATCH']:
            headers.setdefault('Content-Type', 'application/json')
            kwargs['headers'] = headers

        data = None  # Important, if content type is not JSON, this should not be dict type
        if headers.get('Content-Type', '') == 'application/json':
            data = dumps(kwargs.get('data', {}))

        try:
            response = self.session.open(method,
                                         url.geturl(),
                                         headers=headers,
                                         validate_certs=self.verify_ssl,
                                         follow_redirects=True,
                                         data=data)
        except (SSLValidationError) as ssl_err:
            self.fail_json(
                msg=
                "Could not establish a secure connection to your host ({1}): {0}."
                .format(url.netloc, ssl_err))
        except (ConnectionError) as con_err:
            self.fail_json(
                msg=
                "There was a network error of some kind trying to connect to your host ({1}): {0}."
                .format(url.netloc, con_err))
        except (HTTPError) as he:
            # Sanity check: Did the server send back some kind of internal error?
            if he.code >= 500:
                self.fail_json(
                    msg=
                    'The host sent back a server error ({1}): {0}. Please check the logs and try again later'
                    .format(url.path, he))
            # Sanity check: Did we fail to authenticate properly?  If so, fail out now; this is always a failure.
            elif he.code == 401:
                self.fail_json(
                    msg=
                    'Invalid Tower authentication credentials for {0} (HTTP 401).'
                    .format(url.path))
            # Sanity check: Did we get a forbidden response, which means that the user isn't allowed to do this? Report that.
            elif he.code == 403:
                self.fail_json(
                    msg="You don't have permission to {1} to {0} (HTTP 403).".
                    format(url.path, method))
            # Sanity check: Did we get a 404 response?
            # Requests with primary keys will return a 404 if there is no response, and we want to consistently trap these.
            elif he.code == 404:
                if kwargs.get('return_none_on_404', False):
                    return None
                self.fail_json(
                    msg='The requested object could not be found at {0}.'.
                    format(url.path))
            # Sanity check: Did we get a 405 response?
            # A 405 means we used a method that isn't allowed. Usually this is a bad request, but it requires special treatment because the
            # API sends it as a logic error in a few situations (e.g. trying to cancel a job that isn't running).
            elif he.code == 405:
                self.fail_json(
                    msg=
                    "The Tower server says you can't make a request with the {0} method to this endpoing {1}"
                    .format(method, url.path))
            # Sanity check: Did we get some other kind of error?  If so, write an appropriate error message.
            elif he.code >= 400:
                # We are going to return a 400 so the module can decide what to do with it
                page_data = he.read()
                try:
                    return {'status_code': he.code, 'json': loads(page_data)}
                # JSONDecodeError only available on Python 3.5+
                except ValueError:
                    return {'status_code': he.code, 'text': page_data}
            elif he.code == 204 and method == 'DELETE':
                # A 204 is a normal response for a delete function
                pass
            else:
                self.fail_json(
                    msg="Unexpected return code when calling {0}: {1}".format(
                        url.geturl(), he))
        except (Exception) as e:
            self.fail_json(
                msg=
                "There was an unknown error when trying to connect to {2}: {0} {1}"
                .format(type(e).__name__, e, url.geturl()))

        if not self.version_checked:
            # In PY2 we get back an HTTPResponse object but PY2 is returning an addinfourl
            # First try to get the headers in PY3 format and then drop down to PY2.
            try:
                tower_type = response.getheader('X-API-Product-Name', None)
                tower_version = response.getheader('X-API-Product-Version',
                                                   None)
            except Exception:
                tower_type = response.info().getheader('X-API-Product-Name',
                                                       None)
                tower_version = response.info().getheader(
                    'X-API-Product-Version', None)

            parsed_collection_version = Version(
                self._COLLECTION_VERSION).version
            parsed_tower_version = Version(tower_version).version
            if tower_type == 'AWX':
                collection_compare_ver = parsed_collection_version[0]
                tower_compare_ver = parsed_tower_version[0]
            else:
                collection_compare_ver = "{}.{}".format(
                    parsed_collection_version[0], parsed_collection_version[1])
                tower_compare_ver = '{}.{}'.format(parsed_tower_version[0],
                                                   parsed_tower_version[1])

            if self._COLLECTION_TYPE not in self.collection_to_version or self.collection_to_version[
                    self._COLLECTION_TYPE] != tower_type:
                self.warn(
                    "You are using the {0} version of this collection but connecting to {1}"
                    .format(self._COLLECTION_TYPE, tower_type))
            elif collection_compare_ver != tower_compare_ver:
                self.warn(
                    "You are running collection version {0} but connecting to tower version {1}"
                    .format(self._COLLECTION_VERSION, tower_version))

            self.version_checked = True

        response_body = ''
        try:
            response_body = response.read()
        except (Exception) as e:
            self.fail_json(msg="Failed to read response body: {0}".format(e))

        response_json = {}
        if response_body and response_body != '':
            try:
                response_json = loads(response_body)
            except (Exception) as e:
                self.fail_json(
                    msg="Failed to parse the response json: {0}".format(e))

        if PY2:
            status_code = response.getcode()
        else:
            status_code = response.status
        return {'status_code': status_code, 'json': response_json}

    def authenticate(self, **kwargs):
        if self.username and self.password:
            # Attempt to get a token from /api/v2/tokens/ by giving it our username/password combo
            # If we have a username and password, we need to get a session cookie
            login_data = {
                "description": "Ansible Tower Module Token",
                "application": None,
                "scope": "write",
            }
            # Post to the tokens endpoint with baisc auth to try and get a token
            api_token_url = (self.url._replace(
                path='/api/v2/tokens/')).geturl()

            try:
                response = self.session.open(
                    'POST',
                    api_token_url,
                    validate_certs=self.verify_ssl,
                    follow_redirects=True,
                    force_basic_auth=True,
                    url_username=self.username,
                    url_password=self.password,
                    data=dumps(login_data),
                    headers={'Content-Type': 'application/json'},
                )
            except HTTPError as he:
                try:
                    resp = he.read()
                except Exception as e:
                    resp = 'unknown {0}'.format(e)
                self.fail_json(msg='Failed to get token: {0}'.format(he),
                               response=resp)
            except (Exception) as e:
                # Sanity check: Did the server send back some kind of internal error?
                self.fail_json(msg='Failed to get token: {0}'.format(e))

            token_response = None
            try:
                token_response = response.read()
                response_json = loads(token_response)
                self.oauth_token_id = response_json['id']
                self.oauth_token = response_json['token']
            except (Exception) as e:
                self.fail_json(
                    msg=
                    "Failed to extract token information from login response: {0}"
                    .format(e),
                    **{'response': token_response})

        # If we have neither of these, then we can try un-authenticated access
        self.authenticated = True

    def delete_if_needed(self, existing_item, on_delete=None, auto_exit=True):
        # This will exit from the module on its own.
        # If the method successfully deletes an item and on_delete param is defined,
        #   the on_delete parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #   1. None if the existing_item is not defined (so no delete needs to happen)
        #   2. The response from Tower from calling the delete on the endpont. It's up to you to process the response and exit from the module
        # Note: common error codes from the Tower API can cause the module to fail
        if existing_item:
            # If we have an item, we can try to delete it
            try:
                item_url = existing_item['url']
                item_type = existing_item['type']
                item_id = existing_item['id']
                item_name = self.get_item_name(existing_item,
                                               allow_unknown=True)
            except KeyError as ke:
                self.fail_json(
                    msg=
                    "Unable to process delete of item due to missing data {0}".
                    format(ke))

            response = self.delete_endpoint(item_url)

            if response['status_code'] in [202, 204]:
                if on_delete:
                    on_delete(self, response['json'])
                self.json_output['changed'] = True
                self.json_output['id'] = item_id
                self.exit_json(**self.json_output)
                if auto_exit:
                    self.exit_json(**self.json_output)
                else:
                    return self.json_output
            else:
                if 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg="Unable to delete {0} {1}: {2}".format(
                        item_type, item_name, response['json']['__all__'][0]))
                elif 'json' in response:
                    # This is from a project delete (if there is an active job against it)
                    if 'error' in response['json']:
                        self.fail_json(
                            msg="Unable to delete {0} {1}: {2}".format(
                                item_type, item_name, response['json']
                                ['error']))
                    else:
                        self.fail_json(
                            msg="Unable to delete {0} {1}: {2}".format(
                                item_type, item_name, response['json']))
                else:
                    self.fail_json(msg="Unable to delete {0} {1}: {2}".format(
                        item_type, item_name, response['status_code']))
        else:
            if auto_exit:
                self.exit_json(**self.json_output)
            else:
                return self.json_output

    def modify_associations(self, association_endpoint, new_association_list):
        # if we got None instead of [] we are not modifying the association_list
        if new_association_list is None:
            return

        # First get the existing associations
        response = self.get_all_endpoint(association_endpoint)
        existing_associated_ids = [
            association['id'] for association in response['json']['results']
        ]

        # Disassociate anything that is in existing_associated_ids but not in new_association_list
        ids_to_remove = list(
            set(existing_associated_ids) - set(new_association_list))
        for an_id in ids_to_remove:
            response = self.post_endpoint(
                association_endpoint,
                **{'data': {
                    'id': int(an_id),
                    'disassociate': True
                }})
            if response['status_code'] == 204:
                self.json_output['changed'] = True
            else:
                self.fail_json(msg="Failed to disassociate item {0}".format(
                    response['json'].get('detail', response['json'])))

        # Associate anything that is in new_association_list but not in `association`
        for an_id in list(
                set(new_association_list) - set(existing_associated_ids)):
            response = self.post_endpoint(association_endpoint,
                                          **{'data': {
                                              'id': int(an_id)
                                          }})
            if response['status_code'] == 204:
                self.json_output['changed'] = True
            else:
                self.fail_json(msg="Failed to associate item {0}".format(
                    response['json'].get('detail', response['json'])))

    def copy_item(self,
                  existing_item,
                  copy_from_name_or_id,
                  new_item_name,
                  endpoint=None,
                  item_type='unknown',
                  copy_lookup_data=None):

        if existing_item is not None:
            self.warn(msg="A {0} with the name {1} already exists.".format(
                item_type, new_item_name))
            self.json_output['changed'] = False
            self.json_output['copied'] = False
            return existing_item

        # Lookup existing item to copy from
        copy_from_lookup = self.get_one(endpoint,
                                        name_or_id=copy_from_name_or_id,
                                        **{'data': copy_lookup_data})

        # Fail if the copy_from_lookup is empty
        if copy_from_lookup is None:
            self.fail_json(
                msg="A {0} with the name {1} was not able to be found.".format(
                    item_type, copy_from_name_or_id))

        # Do checks for copy permisions if warrented
        if item_type == 'workflow_job_template':
            copy_get_check = self.get_endpoint(
                copy_from_lookup['related']['copy'])
            if copy_get_check['status_code'] in [200]:
                if (copy_get_check['json']['can_copy'] and
                        copy_get_check['json']['can_copy_without_user_input']
                        and
                        not copy_get_check['json']['templates_unable_to_copy']
                        and not copy_get_check['json']
                    ['credentials_unable_to_copy']
                        and not copy_get_check['json']
                    ['inventories_unable_to_copy']):
                    # Because checks have passed
                    self.json_output['copy_checks'] = 'passed'
                else:
                    self.fail_json(
                        msg="Unable to copy {0} {1} error: {2}".format(
                            item_type, copy_from_name_or_id, copy_get_check))
            else:
                self.fail_json(
                    msg="Error accessing {0} {1} error: {2} ".format(
                        item_type, copy_from_name_or_id, copy_get_check))

        response = self.post_endpoint(copy_from_lookup['related']['copy'],
                                      **{'data': {
                                          'name': new_item_name
                                      }})

        if response['status_code'] in [201]:
            self.json_output['id'] = response['json']['id']
            self.json_output['changed'] = True
            self.json_output['copied'] = True
            new_existing_item = response['json']
        else:
            if 'json' in response and '__all__' in response['json']:
                self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                    item_type, new_item_name, response['json']['__all__'][0]))
            elif 'json' in response:
                self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                    item_type, new_item_name, response['json']))
            else:
                self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                    item_type, new_item_name, response['status_code']))
        return new_existing_item

    def create_if_needed(self,
                         existing_item,
                         new_item,
                         endpoint,
                         on_create=None,
                         auto_exit=True,
                         item_type='unknown',
                         associations=None):

        # This will exit from the module on its own
        # If the method successfully creates an item and on_create param is defined,
        #    the on_create parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #    1. None if the existing_item is already defined (so no create needs to happen)
        #    2. The response from Tower from calling the patch on the endpont. It's up to you to process the response and exit from the module
        # Note: common error codes from the Tower API can cause the module to fail

        if not endpoint:
            self.fail_json(
                msg="Unable to create new {0} due to missing endpoint".format(
                    item_type))

        item_url = None
        if existing_item:
            try:
                item_url = existing_item['url']
            except KeyError as ke:
                self.fail_json(
                    msg=
                    "Unable to process create of item due to missing data {0}".
                    format(ke))
        else:
            # If we don't have an exisitng_item, we can try to create it

            # We have to rely on item_type being passed in since we don't have an existing item that declares its type
            # We will pull the item_name out from the new_item, if it exists
            item_name = self.get_item_name(new_item, allow_unknown=True)

            response = self.post_endpoint(endpoint, **{'data': new_item})

            # 200 is response from approval node creation on tower 3.7.3 or awx 15.0.0 or earlier.
            if response['status_code'] in [200, 201]:
                self.json_output['name'] = 'unknown'
                for key in ('name', 'username', 'identifier', 'hostname'):
                    if key in response['json']:
                        self.json_output['name'] = response['json'][key]
                self.json_output['id'] = response['json']['id']
                self.json_output['changed'] = True
                item_url = response['json']['url']
            else:
                if 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                        item_type, item_name, response['json']['__all__'][0]))
                elif 'json' in response:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                        item_type, item_name, response['json']))
                else:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(
                        item_type, item_name, response['status_code']))

        # Process any associations with this item
        if associations is not None:
            for association_type in associations:
                sub_endpoint = '{0}{1}/'.format(item_url, association_type)
                self.modify_associations(sub_endpoint,
                                         associations[association_type])

        # If we have an on_create method and we actually changed something we can call on_create
        if on_create is not None and self.json_output['changed']:
            on_create(self, response['json'])
        elif auto_exit:
            self.exit_json(**self.json_output)
        else:
            last_data = response['json']
            return last_data

    def _encrypted_changed_warning(self, field, old, warning=False):
        if not warning:
            return
        self.warn(
            'The field {0} of {1} {2} has encrypted data and may inaccurately report task is changed.'
            .format(field, old.get('type', 'unknown'),
                    old.get('id', 'unknown')))

    @staticmethod
    def has_encrypted_values(obj):
        """Returns True if JSON-like python content in obj has $encrypted$
        anywhere in the data as a value
        """
        if isinstance(obj, dict):
            for val in obj.values():
                if TowerAPIModule.has_encrypted_values(val):
                    return True
        elif isinstance(obj, list):
            for val in obj:
                if TowerAPIModule.has_encrypted_values(val):
                    return True
        elif obj == TowerAPIModule.ENCRYPTED_STRING:
            return True
        return False

    @staticmethod
    def fields_could_be_same(old_field, new_field):
        """Treating $encrypted$ as a wild card,
        return False if the two values are KNOWN to be different
        return True if the two values are the same, or could potentially be the same,
        depending on the unknown $encrypted$ value or sub-values
        """
        if isinstance(old_field, dict) and isinstance(new_field, dict):
            if set(old_field.keys()) != set(new_field.keys()):
                return False
            for key in new_field.keys():
                if not TowerAPIModule.fields_could_be_same(
                        old_field[key], new_field[key]):
                    return False
            return True  # all sub-fields are either equal or could be equal
        else:
            if old_field == TowerAPIModule.ENCRYPTED_STRING:
                return True
            return bool(new_field == old_field)

    def objects_could_be_different(self,
                                   old,
                                   new,
                                   field_set=None,
                                   warning=False):
        if field_set is None:
            field_set = set(fd for fd in new.keys()
                            if fd not in ('modified', 'related',
                                          'summary_fields'))
        for field in field_set:
            new_field = new.get(field, None)
            old_field = old.get(field, None)
            if old_field != new_field:
                if self.update_secrets or (not self.fields_could_be_same(
                        old_field, new_field)):
                    return True  # Something doesn't match, or something might not match
            elif self.has_encrypted_values(new_field) or field not in new:
                if self.update_secrets or (not self.fields_could_be_same(
                        old_field, new_field)):
                    # case of 'field not in new' - user password write-only field that API will not display
                    self._encrypted_changed_warning(field,
                                                    old,
                                                    warning=warning)
                    return True
        return False

    def update_if_needed(self,
                         existing_item,
                         new_item,
                         on_update=None,
                         auto_exit=True,
                         associations=None):
        # This will exit from the module on its own
        # If the method successfully updates an item and on_update param is defined,
        #   the on_update parameter will be called as a method pasing in this object and the json from the response
        # This will return one of two things:
        #    1. None if the existing_item does not need to be updated
        #    2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module.
        # Note: common error codes from the Tower API can cause the module to fail
        response = None
        if existing_item:

            # If we have an item, we can see if it needs an update
            try:
                item_url = existing_item['url']
                item_type = existing_item['type']
                if item_type == 'user':
                    item_name = existing_item['username']
                elif item_type == 'workflow_job_template_node':
                    item_name = existing_item['identifier']
                elif item_type == 'credential_input_source':
                    item_name = existing_item['id']
                else:
                    item_name = existing_item['name']
                item_id = existing_item['id']
            except KeyError as ke:
                self.fail_json(
                    msg=
                    "Unable to process update of item due to missing data {0}".
                    format(ke))

            # Check to see if anything within the item requires the item to be updated
            needs_patch = self.objects_could_be_different(
                existing_item, new_item)

            # If we decided the item needs to be updated, update it
            self.json_output['id'] = item_id
            if needs_patch:
                response = self.patch_endpoint(item_url, **{'data': new_item})
                if response['status_code'] == 200:
                    # compare apples-to-apples, old API data to new API data
                    # but do so considering the fields given in parameters
                    self.json_output[
                        'changed'] = self.objects_could_be_different(
                            existing_item,
                            response['json'],
                            field_set=new_item.keys(),
                            warning=True)
                elif 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg=response['json']['__all__'])
                else:
                    self.fail_json(
                        **{
                            'msg':
                            "Unable to update {0} {1}, see response".format(
                                item_type, item_name),
                            'response':
                            response
                        })

        else:
            raise RuntimeError(
                'update_if_needed called incorrectly without existing_item')

        # Process any associations with this item
        if associations is not None:
            for association_type, id_list in associations.items():
                endpoint = '{0}{1}/'.format(item_url, association_type)
                self.modify_associations(endpoint, id_list)

        # If we change something and have an on_change call it
        if on_update is not None and self.json_output['changed']:
            if response is None:
                last_data = existing_item
            else:
                last_data = response['json']
            on_update(self, last_data)
        elif auto_exit:
            self.exit_json(**self.json_output)
        else:
            if response is None:
                last_data = existing_item
            else:
                last_data = response['json']
            return last_data

    def create_or_update_if_needed(self,
                                   existing_item,
                                   new_item,
                                   endpoint=None,
                                   item_type='unknown',
                                   on_create=None,
                                   on_update=None,
                                   auto_exit=True,
                                   associations=None):
        if existing_item:
            return self.update_if_needed(existing_item,
                                         new_item,
                                         on_update=on_update,
                                         auto_exit=auto_exit,
                                         associations=associations)
        else:
            return self.create_if_needed(existing_item,
                                         new_item,
                                         endpoint,
                                         on_create=on_create,
                                         item_type=item_type,
                                         auto_exit=auto_exit,
                                         associations=associations)

    def logout(self):
        if self.authenticated and self.oauth_token_id:
            # Attempt to delete our current token from /api/v2/tokens/
            # Post to the tokens endpoint with baisc auth to try and get a token
            api_token_url = (
                self.url._replace(
                    path='/api/v2/tokens/{0}/'.format(self.oauth_token_id),
                    query=
                    None  # in error cases, fail_json exists before exception handling
                )).geturl()

            try:
                self.session.open(
                    'DELETE',
                    api_token_url,
                    validate_certs=self.verify_ssl,
                    follow_redirects=True,
                    force_basic_auth=True,
                    url_username=self.username,
                    url_password=self.password,
                )
                self.oauth_token_id = None
                self.authenticated = False
            except HTTPError as he:
                try:
                    resp = he.read()
                except Exception as e:
                    resp = 'unknown {0}'.format(e)
                self.warn(
                    'Failed to release tower token: {0}, response: {1}'.format(
                        he, resp))
            except (Exception) as e:
                # Sanity check: Did the server send back some kind of internal error?
                self.warn('Failed to release tower token {0}: {1}'.format(
                    self.oauth_token_id, e))

    def is_job_done(self, job_status):
        if job_status in ['new', 'pending', 'waiting', 'running']:
            return False
        else:
            return True

    def wait_on_url(self,
                    url,
                    object_name,
                    object_type,
                    timeout=30,
                    interval=10):
        # Grab our start time to compare against for the timeout
        start = time.time()
        result = self.get_endpoint(url)
        while not result['json']['finished']:
            # If we are past our time out fail with a message
            if timeout and timeout < time.time() - start:
                # Account for Legacy messages
                if object_type == 'legacy_job_wait':
                    self.json_output[
                        'msg'] = 'Monitoring of Job - {0} aborted due to timeout'.format(
                            object_name)
                else:
                    self.json_output[
                        'msg'] = 'Monitoring of {0} - {1} aborted due to timeout'.format(
                            object_type, object_name)
                self.wait_output(result)
                self.fail_json(**self.json_output)

            # Put the process to sleep for our interval
            time.sleep(interval)

            result = self.get_endpoint(url)
            self.json_output['status'] = result['json']['status']

        # If the job has failed, we want to raise a task failure for that so we get a non-zero response.
        if result['json']['failed']:
            # Account for Legacy messages
            if object_type == 'legacy_job_wait':
                self.json_output['msg'] = 'Job with id {0} failed'.format(
                    object_name)
            else:
                self.json_output['msg'] = 'The {0} - {1}, failed'.format(
                    object_type, object_name)
            self.wait_output(result)
            self.fail_json(**self.json_output)

        self.wait_output(result)

        return result

    def wait_output(self, response):
        for k in ('id', 'status', 'elapsed', 'started', 'finished'):
            self.json_output[k] = response['json'].get(k)
Beispiel #19
0
class OpenAPI:
    def __init__(
        self,
        base_url,
        doc_path,
        username=None,
        password=None,
        validate_certs=True,
        refresh_cache=False,
    ):
        self.doc_path = doc_path

        if base_url.startswith("unix:"):
            self.unix_socket = base_url.replace("unix:", "")
            self.base_url = "http://localhost/"
        else:
            self.unix_socket = None
            self.base_url = base_url

        headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
        }
        self._session = Request(
            url_username=username,
            url_password=password,
            headers=headers,
            validate_certs=validate_certs,
            force_basic_auth=True,
        )

        self.load_api(refresh_cache=refresh_cache)

    def load_api(self, refresh_cache=False):
        # TODO: Find a way to invalidate caches on upstream change
        xdg_cache_home = os.environ.get("XDG_CACHE_HOME") or "~/.cache"
        apidoc_cache = os.path.join(
            os.path.expanduser(xdg_cache_home),
            "squeezer",
            self.base_url.replace(":", "_").replace("/", "_"),
            "api.json",
        )
        try:
            if refresh_cache:
                raise IOError()
            with open(apidoc_cache, "rb") as f:
                data = f.read()
            self._parse_api(data)
        except Exception:
            # Try again with a freshly downloaded version
            data = self._download_api()
            self._parse_api(data)
            # Write to cache as it seems to be valid
            makedirs(os.path.dirname(apidoc_cache), exist_ok=True)
            with open(apidoc_cache, "wb") as f:
                f.write(data)

    def _parse_api(self, data):
        self.api_spec = json.loads(data)
        if self.api_spec.get("swagger") == "2.0":
            self.openapi_version = 2
        elif self.api_spec.get("openapi", "").startswith("3."):
            self.openapi_version = 3
        else:
            raise NotImplementedError("Unknown schema version")
        self.operations = {
            method_entry["operationId"]: (method, path)
            for path, path_entry in self.api_spec["paths"].items()
            for method, method_entry in path_entry.items()
            if method
            in {"get", "put", "post", "delete", "options", "head", "patch", "trace"}
        }

    def _download_api(self):
        return self._session.open(
            "GET", urljoin(self.base_url, self.doc_path), unix_socket=self.unix_socket
        ).read()

    def extract_params(self, param_type, path_spec, method_spec, params):
        param_spec = {
            entry["name"]: entry
            for entry in path_spec.get("parameters", [])
            if entry["in"] == param_type
        }
        param_spec.update(
            {
                entry["name"]: entry
                for entry in method_spec.get("parameters", [])
                if entry["in"] == param_type
            }
        )
        result = {}
        for name in list(params.keys()):
            if name in param_spec:
                param_spec.pop(name)
                result[name] = params.pop(name)
        remaining_required = [
            item["name"] for item in param_spec.values() if item.get("required", False)
        ]
        if any(remaining_required):
            raise Exception(
                "Required parameters [{0}] missing for {1}.".format(
                    ", ".join(remaining_required), param_type
                )
            )
        return result

    def render_body(self, path_spec, method_spec, headers, body=None, uploads=None):
        if not (body or uploads):
            return None
        if self.openapi_version == 2:
            content_types = (
                method_spec.get("consumes")
                or path_spec.get("consumes")
                or self.api_spec.get("consumes")
            )
        else:
            content_types = list(method_spec["requestBody"]["content"].keys())
        if uploads:
            body = body or {}
            if any(
                (
                    content_type.startswith("multipart/form-data")
                    for content_type in content_types
                )
            ):
                boundary = uuid.uuid4().hex
                part_boundary = b"--" + to_bytes(boundary, errors="surrogate_or_strict")

                form = []
                for key, value in body.items():
                    b_key = to_bytes(key, errors="surrogate_or_strict")
                    form.extend(
                        [
                            part_boundary,
                            b'Content-Disposition: form-data; name="%s"' % b_key,
                            b"",
                            to_bytes(value, errors="surrogate_or_strict"),
                        ]
                    )
                for key, file_data in uploads.items():
                    b_key = to_bytes(key, errors="surrogate_or_strict")
                    form.extend(
                        [
                            part_boundary,
                            b'Content-Disposition: file; name="%s"; filename="%s"'
                            % (b_key, b_key),
                            b"Content-Type: application/octet-stream",
                            b"",
                            file_data,
                        ]
                    )
                form.append(part_boundary + b"--")
                data = b"\r\n".join(form)
                headers[
                    "Content-Type"
                ] = "multipart/form-data; boundary={boundary}".format(boundary=boundary)
            else:
                raise Exception("No suitable content type for file upload specified.")
        elif body:
            if any(
                (
                    content_type.startswith("application/json")
                    for content_type in content_types
                )
            ):
                data = json.dumps(body)
                headers["Content-Type"] = "application/json"
            elif any(
                (
                    content_type.startswith("application/x-www-form-urlencoded")
                    for content_type in content_types
                )
            ):
                data = urlencode(body)
                headers["Content-Type"] = "application/x-www-form-urlencoded"
            else:
                raise Exception("No suitable content type for file upload specified.")
        headers["Content-Length"] = len(data)
        return data

    def call(self, operation_id, parameters=None, body=None, uploads=None):
        method, path = self.operations[operation_id]
        path_spec = self.api_spec["paths"][path]
        method_spec = path_spec[method]

        if parameters is None:
            parameters = {}
        else:
            parameters = parameters.copy()

        if any(self.extract_params("cookie", path_spec, method_spec, parameters)):
            raise NotImplementedError("Cookie parameters are not implemented.")

        headers = self.extract_params("header", path_spec, method_spec, parameters)

        for name, value in self.extract_params(
            "path", path_spec, method_spec, parameters
        ).items():
            path = path.replace("{" + name + "}", value)

        query_string = urlencode(
            self.extract_params("query", path_spec, method_spec, parameters), doseq=True
        )

        if any(parameters):
            raise Exception(
                "Parameter [{names}] not available for {operation_id}.".format(
                    names=", ".join(parameters.keys()), operation_id=operation_id
                )
            )
        url = urljoin(self.base_url, path)
        if query_string:
            url += "?" + query_string

        data = self.render_body(path_spec, method_spec, headers, body, uploads)

        result = self._session.open(
            method, url, data=data, headers=headers, unix_socket=self.unix_socket
        ).read()
        if result:
            return json.loads(result)
        return None
Beispiel #20
0
class AHAPIModule(AnsibleModule):
    """Ansible module for managing private automation hub servers."""

    AUTH_ARGSPEC = dict(
        ah_host=dict(required=False,
                     aliases=["ah_hostname"],
                     fallback=(env_fallback, ["AH_HOST"])),
        ah_username=dict(required=False,
                         fallback=(env_fallback, ["AH_USERNAME"])),
        ah_password=dict(no_log=True,
                         required=False,
                         fallback=(env_fallback, ["AH_PASSWORD"])),
        ah_path_prefix=dict(required=False,
                            fallback=(env_fallback, ["GALAXY_API_PATH_PREFIX"
                                                     ])),
        validate_certs=dict(type="bool",
                            aliases=["ah_verify_ssl"],
                            required=False,
                            fallback=(env_fallback, ["AH_VERIFY_SSL"])),
    )
    short_params = {
        "host": "ah_host",
        "username": "******",
        "password": "******",
        "verify_ssl": "validate_certs",
        "path_prefix": "ah_path_prefix",
    }

    host = "127.0.0.1"
    username = None
    password = None
    verify_ssl = True
    path_prefix = "galaxy"
    authenticated = False

    def __init__(self, argument_spec, **kwargs):
        """Initialize the object."""
        full_argspec = {}
        full_argspec.update(AHAPIModule.AUTH_ARGSPEC)
        full_argspec.update(argument_spec)

        super(AHAPIModule, self).__init__(argument_spec=full_argspec, **kwargs)

        # Update the current object with the provided parameters
        for short_param, long_param in self.short_params.items():
            direct_value = self.params.get(long_param)
            if direct_value is not None:
                setattr(self, short_param, direct_value)

        # Perform some basic validation
        if not re.match("^https{0,1}://", self.host):
            self.host = "https://{host}".format(host=self.host)

        # Try to parse the hostname as a url
        try:
            self.host_url = urlparse(self.host)
        except Exception as e:
            self.fail_json(
                msg="Unable to parse ah_host as a URL ({host}): {error}".
                format(host=self.host, error=e))

        # Try to resolve the hostname
        try:
            socket.gethostbyname(self.host_url.hostname)
        except Exception as e:
            self.fail_json(msg="Unable to resolve ah_host ({host}): {error}".
                           format(host=self.host_url.hostname, error=e))

        self.headers = {
            "referer": self.host,
            "Content-Type": "application/json",
            "Accept": "application/json"
        }
        self.session = Request(validate_certs=self.verify_ssl,
                               headers=self.headers)

        # Define the API paths
        self.galaxy_path_prefix = "/api/{prefix}".format(
            prefix=self.path_prefix.strip("/"))
        self.ui_path_prefix = "{galaxy_prefix}/_ui/v1".format(
            galaxy_prefix=self.galaxy_path_prefix)
        self.pulp_path_prefix = "/pulp/api/v3"

    def _build_url(self, prefix, endpoint=None, query_params=None):
        """Return a URL from the given prefix and endpoint.

        The URL is build as follows::

            https://<host>/<prefix>/[<endpoint>]/[?<query>]

        :param prefix: Prefix to add to the endpoint.
        :type prefix: str
        :param endpoint: Usually the API object name ("users", "groups", ...)
        :type endpoint: str
        :param query_params: The optional query to append to the URL
        :type query_params: dict

        :return: The full URL built from the given prefix and endpoint.
        :rtype: :py:class:``urllib.parse.ParseResult``
        """
        if endpoint is None:
            api_path = "/{base}/".format(base=prefix.strip("/"))
        else:
            api_path = "{base}/{endpoint}/".format(
                base=prefix, endpoint=endpoint.strip("/"))
        url = self.host_url._replace(path=api_path)
        if query_params:
            url = url._replace(query=urlencode(query_params))
        return url

    def build_ui_url(self, endpoint, query_params=None):
        """Return the URL of the given endpoint in the UI API.

        :param endpoint: Usually the API object name ("users", "groups", ...)
        :type endpoint: str
        :return: The full URL built from the given endpoint.
        :rtype: :py:class:``urllib.parse.ParseResult``
        """
        return self._build_url(self.ui_path_prefix, endpoint, query_params)

    def build_pulp_url(self, endpoint, query_params=None):
        """Return the URL of the given endpoint in the Pulp API.

        :param endpoint: Usually the API object name ("users", "groups", ...)
        :type endpoint: str
        :return: The full URL built from the given endpoint.
        :rtype: :py:class:``urllib.parse.ParseResult``
        """
        return self._build_url(self.pulp_path_prefix, endpoint, query_params)

    def make_request_raw_reponse(self, method, url, **kwargs):
        """Perform an API call and return the retrieved data.

        :param method: GET, PUT, POST, or DELETE
        :type method: str
        :param url: URL to the API endpoint
        :type url: :py:class:``urllib.parse.ParseResult``
        :param kwargs: Additionnal parameter to pass to the API (headers, data
                       for PUT and POST requests, ...)

        :raises AHAPIModuleError: The API request failed.

        :return: The reponse from the API call
        :rtype: :py:class:``http.client.HTTPResponse``
        """
        # In case someone is calling us directly; make sure we were given a method, let's not just assume a GET
        if not method:
            raise Exception("The HTTP method must be defined")

        # Extract the provided headers and data
        headers = kwargs.get("headers", {})
        data = json.dumps(kwargs.get("data", {}))

        #set default response
        response = {}

        try:
            response = self.session.open(method,
                                         url.geturl(),
                                         headers=headers,
                                         data=data)
        except SSLValidationError as ssl_err:
            raise AHAPIModuleError(
                "Could not establish a secure connection to {host}: {error}.".
                format(host=url.netloc, error=ssl_err))
        except ConnectionError as con_err:
            raise AHAPIModuleError(
                "Network error when trying to connect to {host}: {error}.".
                format(host=url.netloc, error=con_err))
        except HTTPError as he:
            # Sanity check: Did the server send back some kind of internal error?
            if he.code >= 500:
                raise AHAPIModuleError(
                    "The host sent back a server error: {path}: {error}. Please check the logs and try again later"
                    .format(path=url.path, error=he))
            # Sanity check: Did we fail to authenticate properly?  If so, fail out now; this is always a failure.
            elif he.code == 401:
                raise AHAPIModuleError(
                    "Invalid authentication credentials for {path} (HTTP 401)."
                    .format(path=url.path))
            # Sanity check: Did we get a forbidden response, which means that the user isn't allowed to do this? Report that.
            elif he.code == 403:
                raise AHAPIModuleError(
                    "You do not have permission to {method} {path} (HTTP 403)."
                    .format(method=method, path=url.path))
            # Sanity check: Did we get a 404 response?
            # Requests with primary keys will return a 404 if there is no response, and we want to consistently trap these.
            elif he.code == 404:
                raise AHAPIModuleError(
                    "The requested object could not be found at {path}.".
                    format(path=url.path))
            # Sanity check: Did we get a 405 response?
            # A 405 means we used a method that isn't allowed. Usually this is a bad request, but it requires special treatment because the
            # API sends it as a logic error in a few situations (e.g. trying to cancel a job that isn't running).
            elif he.code == 405:
                raise AHAPIModuleError(
                    "Cannot make a {method} request to this endpoint {path}".
                    format(method=method, path=url.path))
            # Sanity check: Did we get some other kind of error?  If so, write an appropriate error message.
            elif he.code >= 400:
                # We are going to return a 400 so the module can decide what to do with it
                pass
            elif he.code == 204 and method == "DELETE":
                # A 204 is a normal response for a delete function
                pass
            else:
                raise AHAPIModuleError(
                    "Unexpected return code when calling {url}: {error}".
                    format(url=url.geturl(), error=he))
        except Exception as e:
            raise AHAPIModuleError(
                "There was an unknown error when trying to connect to {name}: {error} {url}"
                .format(name=type(e).__name__, error=e, url=url.geturl()))

        return response

    def make_request(self, method, url, wait_for_task=True, **kwargs):
        """Perform an API call and return the data.

        :param method: GET, PUT, POST, or DELETE
        :type method: str
        :param url: URL to the API endpoint
        :type url: :py:class:``urllib.parse.ParseResult``
        :param kwargs: Additionnal parameter to pass to the API (headers, data
                       for PUT and POST requests, ...)

        :raises AHAPIModuleError: The API request failed.

        :return: A dictionnary with two entries: ``status_code`` provides the
                 API call returned code and ``json`` provides the returned data
                 in JSON format.
        :rtype: dict
        """
        response = self.make_request_raw_reponse(method, url, **kwargs)

        try:
            response_body = response.read()
        except Exception as e:
            raise AHAPIModuleError(
                "Failed to read response body: {error}".format(error=e))

        response_json = {}
        if response_body:
            try:
                response_json = json.loads(response_body)
            except Exception as e:
                raise AHAPIModuleError(
                    "Failed to parse the response json: {0}".format(e))

        # A background task has been triggered. Check if the task is completed
        if response.status == 202 and "task" in response_json and wait_for_task:
            url = url._replace(path=response_json["task"], query="")
            for _ in range(5):
                time.sleep(3)
                bg_task = self.make_request("GET", url)
                if "state" in bg_task["json"] and bg_task["json"][
                        "state"].lower().startswith("complete"):
                    break
            else:
                if "state" in bg_task["json"]:
                    raise AHAPIModuleError(
                        "Failed to get the status of the remote task: {task}: last status: {status}"
                        .format(task=response_json["task"],
                                status=bg_task["json"]["state"]))
                raise AHAPIModuleError(
                    "Failed to get the status of the remote task: {task}".
                    format(task=response_json["task"]))

        return {"status_code": response.status, "json": response_json}

    def extract_error_msg(self, response):
        """Return the error message provided in the API response.

        Example of messages returned by the API call:

            {
                "errors": [
                    {
                        "status":"400",
                        "code":"invalid",
                        "title":"Invalid input.",
                        "detail":"Permission matching query does not exist."
                    }
                ]
            }

            {
                "errors": [
                    {
                        "status":"404",
                        "code":"not_found",
                        "title":"Not found."
                    }
                ]
            }

            {
                "detail":"Not found."
            }

        :param response: The response message from the API. This dictionary has
                         two keys: ``status_code`` provides the API call
                         returned code and ``json`` provides the returned data
                         in JSON format.
        :type response: dict

        :return: The error message or an empty string if the reponse does not
                 provide a message.
        :rtype: str
        """
        if not response or "json" not in response:
            return ""
        if "errors" in response["json"] and len(response["json"]["errors"]):
            if "detail" in response["json"]["errors"][0]:
                return response["json"]["errors"][0]["detail"]
            if "title" in response["json"]["errors"][0]:
                return response["json"]["errors"][0]["title"]
        if "detail" in response["json"]:
            return response["json"]["detail"]
        return ""

    def authenticate(self):
        """Authenticate with the API."""
        # curl -k -i  -X GET -H "Accept: application/json" -H "Content-Type: application/json" https://hub.lab.example.com/api/galaxy/_ui/v1/auth/login/

        # HTTP/1.1 204 No Content
        # Server: nginx/1.18.0
        # Date: Tue, 10 Aug 2021 07:33:37 GMT
        # Content-Length: 0
        # Connection: keep-alive
        # Vary: Accept, Cookie
        # Allow: GET, POST, HEAD, OPTIONS
        # X-Frame-Options: SAMEORIGIN
        # Set-Cookie: csrftoken=jvdb...kKHo; expires=Tue, 09 Aug 2022 07:33:37 GMT; Max-Age=31449600; Path=/; SameSite=Lax
        # Strict-Transport-Security: max-age=15768000

        url = self.build_ui_url("auth/login")
        try:
            response = self.make_request_raw_reponse("GET", url)
        except AHAPIModuleError as e:
            self.fail_json(msg="Authentication error: {error}".format(error=e))
        # Set-Cookie: csrftoken=jvdb...kKHo; expires=Tue, 09 Aug 2022 07:33:37 GMT
        for h in response.getheaders():
            if h[0].lower() == "set-cookie":
                k, v = h[1].split("=", 1)
                if k.lower() == "csrftoken":
                    header = {"X-CSRFToken": v.split(";", 1)[0]}
                    break
        else:
            header = {}

        # curl -k -i -X POST  -H 'referer: https://hub.lab.example.com' -H "Accept: application/json" -H "Content-Type: application/json"
        #      -H 'X-CSRFToken: jvdb...kKHo' --cookie 'csrftoken=jvdb...kKHo' -d '{"username":"******","password":"******"}'
        #      https://hub.lab.example.com/api/galaxy/_ui/v1/auth/login/

        # HTTP/1.1 204 No Content
        # Server: nginx/1.18.0
        # Date: Tue, 10 Aug 2021 07:35:33 GMT
        # Content-Length: 0
        # Connection: keep-alive
        # Vary: Accept, Cookie
        # Allow: GET, POST, HEAD, OPTIONS
        # X-Frame-Options: SAMEORIGIN
        # Set-Cookie: csrftoken=6DVP...at9a; expires=Tue, 09 Aug 2022 07:35:33 GMT; Max-Age=31449600; Path=/; SameSite=Lax
        # Set-Cookie: sessionid=87b0iw12wyvy0353rk5fwci0loy5s615; expires=Tue, 24 Aug 2021 07:35:33 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
        # Strict-Transport-Security: max-age=15768000

        try:
            response = self.make_request_raw_reponse("POST",
                                                     url,
                                                     data={
                                                         "username":
                                                         self.username,
                                                         "password":
                                                         self.password
                                                     },
                                                     headers=header)
        except AHAPIModuleError as e:
            self.fail_json(msg="Authentication error: {error}".format(error=e))
        for h in response.getheaders():
            if h[0].lower() == "set-cookie":
                k, v = h[1].split("=", 1)
                if k.lower() == "csrftoken":
                    header = {"X-CSRFToken": v.split(";", 1)[0]}
                    break
        else:
            header = {}
        self.headers.update(header)
        self.authenticated = True

    def logout(self):
        if not self.authenticated:
            return

        url = self.build_ui_url("auth/logout")
        try:
            self.make_request_raw_reponse("POST", url)
        except AHAPIModuleError:
            pass
        self.headers = {
            "referer": self.host,
            "Content-Type": "application/json",
            "Accept": "application/json"
        }
        self.session = Request(validate_certs=self.verify_ssl,
                               headers=self.headers)
        self.authenticated = False

    def fail_json(self, **kwargs):
        self.logout()
        super(AHAPIModule, self).fail_json(**kwargs)

    def exit_json(self, **kwargs):
        self.logout()
        super(AHAPIModule, self).exit_json(**kwargs)

    def get_server_version(self):
        """Return the automation hub/galaxy server version.

        :return: the server version ("4.2.5" for example) or an empty string if
                 that information is not available.
        :rtype: str
        """
        url = self._build_url(self.galaxy_path_prefix)
        try:
            response = self.make_request("GET", url)
        except AHAPIModuleError as e:
            self.fail_json(
                msg="Error while getting server version: {error}".format(
                    error=e))
        if response["status_code"] != 200:
            error_msg = self.extract_error_msg(response)
            if error_msg:
                fail_msg = "Unable to get server version: {code}: {error}".format(
                    code=response["status_code"], error=error_msg)
            else:
                fail_msg = "Unable to get server version: {code}".format(
                    code=response["status_code"])
            self.fail_json(msg=fail_msg)
        return response["json"][
            "server_version"] if "server_version" in response["json"] else ""
class Connection(ConnectionBase):
    force_persistence = True
    transport = "pms"

    def __init__(self, play_context, *args, **kwargs):
        super(Connection, self).__init__(play_context, *args, **kwargs)
        self._messages = []
        self._sub_plugin = {}
        self._conn_closed = False

        # We are, for the most part, just a local connection that knows how to
        # perform HTTP requests.
        self._local = connection_loader.get("local", play_context, "/dev/null")
        self._local.set_options()

        self._headers = {}

    def _connect(self):
        if self._connected:
            return

        self._address = self.get_option("address").rstrip("/")
        self._client = Request()

        # Login
        status, headers, _ = self._request(
            "POST", "/tokens",
            dict(
                username=self.get_option("username"),
                password=self.get_option("password"),
            ))
        self._headers["x-auth-token"] = headers["x-auth-token"]

        self._local._connect()
        self._connected = True

    def exec_command(self, *args, **kwargs):
        return self._local.exec_command(*args, **kwargs)

    def put_file(self, in_path, out_path):
        return self._local.put_file(in_path, out_path)

    def fetch_file(self, in_path, out_path):
        return self._local.fetch_file(in_path, out_path)

    def close(self):
        self._conn_closed = True
        if not self._connected:
            return

        self._local.close()
        if "x-auth-token" in self._headers:
            self.delete("/tokens/" + self._headers["x-auth-token"])
            del self._headers["x-auth-token"]
        self._connected = False

    def queue_message(self, level, message):
        self._messages.append((level, message))

    def pop_messages(self):
        messages, self._messages = self._messages, []
        return messages

    def _log_messages(self, data):
        pass

    def _request(self, method, path, payload=None):
        headers = self._headers.copy()
        data = None
        if payload:
            data = json.dumps(payload)
            headers["Content-Type"] = "application/json"

        url = self._address + path
        try:
            r = self._client.open(method, url, data=data, headers=headers)
            r_status = r.getcode()
            r_headers = dict(r.headers)
            data = r.read().decode("utf-8")
            r_data = json.loads(data) if data else {}
        except HTTPError as e:
            r_status = e.code
            r_headers = {}
            r_data = dict(msg=str(e.reason))
        except (ConnectionError, URLError) as e:
            raise AnsibleConnectionFailure(
                "Could not connect to {0}: {1}".format(url, e.reason))
        return r_status, r_headers, r_data

    @ensure_connect
    def get(self, path):
        return self._request("GET", path)

    @ensure_connect
    def post(self, path, payload=None):
        return self._request("POST", path, payload)

    @ensure_connect
    def delete(self, path):
        return self._request("DELETE", path)