Ejemplo n.º 1
0
    def status(self, pk, detail=False):
        """Print the current job status."""
        # Get the job from Ansible Tower.
        debug.log('Asking for inventory source status.', header='details')
        inv_src = client.get('/inventory_sources/%d/' % pk).json()

        # Determine the appropriate inventory source update.
        if 'current_update' in inv_src['related']:
            debug.log('A current update exists; retrieving it.',
                      header='details')
            job = client.get(inv_src['related']['current_update'][7:]).json()
        elif inv_src['related'].get('last_update', None):
            debug.log(
                'No current update exists; retrieving the most '
                'recent update.',
                header='details')
            job = client.get(inv_src['related']['last_update'][7:]).json()
        else:
            raise exc.NotFound('No inventory source updates exist.')

        # In most cases, we probably only want to know the status of the job
        # and the amount of time elapsed. However, if we were asked for
        # verbose information, provide it.
        if detail:
            return job

        # Print just the information we need.
        return adict({
            'elapsed': job['elapsed'],
            'failed': job['failed'],
            'status': job['status'],
        })
Ejemplo n.º 2
0
 def get(self, pk):
     """Return one and exactly one object"""
     # The Tower API doesn't provide a mechanism for retrieving a single
     # setting value at a time, so fetch them all and filter
     try:
         return next(s for s in self.list()['results'] if s['id'] == pk)
     except StopIteration:
         raise exc.NotFound('The requested object could not be found.')
Ejemplo n.º 3
0
    def role_write(self, fail_on_found=False, disassociate=False, **kwargs):
        """Re-implementation of the parent `write` method specific to roles.
        Adds a grantee (user or team) to the resource's role."""

        # Get the role, using only the resource data
        data, self.endpoint = self.data_endpoint(kwargs, ignore=['obj'])
        debug.log('Checking if role exists.', header='details')
        response = self.read(pk=None,
                             fail_on_no_results=True,
                             fail_on_multiple_results=True,
                             **data)
        role_data = response['results'][0]
        role_id = role_data['id']

        # Role exists, change display settings to output something
        self.configure_display(role_data, kwargs, write=True)

        # Check if user/team has this role
        # Implictly, force_on_exists is false for roles
        obj, obj_type, res, res_type = self.obj_res(kwargs)
        debug.log('Checking if %s already has role.' % obj_type,
                  header='details')
        data, self.endpoint = self.data_endpoint(kwargs)
        response = self.read(pk=None,
                             fail_on_no_results=False,
                             fail_on_multiple_results=False,
                             **data)

        msg = ''
        if response['count'] > 0 and not disassociate:
            msg = 'This %s is already a member of the role.' % obj_type
        elif response['count'] == 0 and disassociate:
            msg = 'This %s is already a non-member of the role.' % obj_type

        if msg:
            role_data['changed'] = False
            if fail_on_found:
                raise exc.NotFound(msg)
            else:
                debug.log(msg, header='DECISION')
                return role_data

        # Add or remove the user/team to the role
        debug.log('Attempting to %s the %s in this role.' %
                  ('remove' if disassociate else 'add', obj_type),
                  header='details')
        post_data = {'id': role_id}
        if disassociate:
            post_data['disassociate'] = True
        client.post('%s/%s/roles/' % (self.pluralize(obj_type), obj),
                    data=post_data)
        role_data['changed'] = True
        return role_data
Ejemplo n.º 4
0
    def _lookup(self,
                fail_on_missing=False,
                fail_on_found=False,
                include_debug_header=True,
                **kwargs):
        """Attempt to perform a lookup that is expected to return a single
        result, and return the record.

        This method is a wrapper around `get` that strips out non-unique
        keys, and is used internally by `write` and `delete`.
        """
        # Determine which parameters we are using to determine
        # the appropriate field.
        read_params = {}
        for field_name in self.identity:
            if field_name in kwargs:
                read_params[field_name] = kwargs[field_name]

        # Sanity check: Do we have any parameters?
        # If not, then there's no way for us to do this read.
        if not read_params:
            raise exc.BadRequest('Cannot reliably determine which record '
                                 'to write. Include an ID or unique '
                                 'fields.')

        # Get the record to write.
        try:
            existing_data = self.get(include_debug_header=include_debug_header,
                                     **read_params)
            if fail_on_found:
                raise exc.Found('A record matching %s already exists, and '
                                'you requested a failure in that case.' %
                                read_params)
            return existing_data
        except exc.NotFound:
            if fail_on_missing:
                raise exc.NotFound('A record matching %s does not exist, and '
                                   'you requested a failure in that case.' %
                                   read_params)
            return {}
Ejemplo n.º 5
0
    def read(self,
             pk=None,
             fail_on_no_results=False,
             fail_on_multiple_results=False,
             **kwargs):
        """Retrieve and return objects from the Ansible Tower API.

        If an `object_id` is provided, only attempt to read that object,
        rather than the list at large.

        If `fail_on_no_results` is True, then zero results is considered
        a failure case and raises an exception; otherwise, empty list is
        returned. (Note: This is always True if a primary key is included.)

        If `fail_on_multiple_results` is True, then at most one result is
        expected, and more results constitutes a failure case.
        (Note: This is meaningless if a primary key is included, as there can
        never be multiple results.)
        """
        # Piece together the URL we will be hitting.
        url = self.endpoint
        if pk:
            url += '%d/' % pk

        # Pop the query parameter off of the keyword arguments; it will
        # require special handling (below).
        queries = kwargs.pop('query', [])

        # Remove default values (anything where the value is None).
        # click is unfortunately bad at the way it sends through unspecified
        # defaults.
        for key, value in copy(kwargs).items():
            if value is None:
                kwargs.pop(key)
            if hasattr(value, 'read'):
                kwargs[key] = value.read()

        # If queries were provided, process them.
        for query in queries:
            if query[0] in kwargs:
                raise exc.BadRequest('Attempted to set %s twice.' % query[0])
            kwargs[query[0]] = query[1]

        # Make the request to the Ansible Tower API.
        r = client.get(url, params=kwargs)
        resp = r.json()

        # If this was a request with a primary key included, then at the
        # point that we got a good result, we know that we're done and can
        # return the result.
        if pk:
            # Make the results all look the same, for easier parsing
            # by other methods.
            #
            # Note that the `get` method will effectively undo this operation,
            # but that's a good thing, because we might use `get` without a
            # primary key.
            return {'count': 1, 'results': [resp]}

        # Did we get zero results back when we shouldn't?
        # If so, this is an error, and we need to complain.
        if fail_on_no_results and resp['count'] == 0:
            raise exc.NotFound('The requested object could not be found.')

        # Did we get more than one result back?
        # If so, this is also an error, and we need to complain.
        if fail_on_multiple_results and resp['count'] >= 2:
            raise exc.MultipleResults('Expected one result, got %d. Tighten '
                                      'your criteria.' % resp['count'])

        # Return the response.
        return resp
Ejemplo n.º 6
0
    def request(self, method, url, *args, **kwargs):
        """Make a request to the Ansible Tower API, and return the
        response.
        """
        # Piece together the full URL.
        url = '%s%s' % (self.prefix, url.lstrip('/'))

        # Ansible Tower expects authenticated requests; add the authentication
        # from settings if it's provided.
        kwargs.setdefault('auth', (settings.username, settings.password))

        # POST and PUT requests will send JSON by default; make this
        # the content_type by default.  This makes it such that we don't have
        # to constantly write that in our code, which gets repetitive.
        headers = kwargs.get('headers', {})
        if method.upper() in ('PATCH', 'POST', 'PUT'):
            headers.setdefault('Content-Type', 'application/json')
            kwargs['headers'] = headers

        # If debugging is on, print the URL and data being sent.
        debug.log('%s %s' % (method, url), fg='blue', bold=True)
        if method in ('POST', 'PUT', 'PATCH'):
            debug.log('Data: %s' % kwargs.get('data', {}),
                      fg='blue', bold=True)
        if method == 'GET' or kwargs.get('params', None):
            debug.log('Params: %s' % kwargs.get('params', {}),
                      fg='blue', bold=True)
        debug.log('')

        # If this is a JSON request, encode the data value.
        if headers.get('Content-Type', '') == 'application/json':
            kwargs['data'] = json.dumps(kwargs.get('data', {}))

        # Call the superclass method.
        try:
            r = super(Client, self).request(method, url, *args,
                                            verify=False, **kwargs)
        except ConnectionError as ex:
            if settings.verbose:
                debug.log('Cannot connect to Tower:', fg='yellow', bold=True)
                debug.log(str(ex), fg='yellow', bold=True, nl=2)
            raise exc.ConnectionError(
                'There was a network error of some kind trying to connect '
                'to Tower.\n\nThe most common  reason for this is a settings '
                'issue; is your "host" value in `tower-cli config` correct?\n'
                'Right now it is: "%s".' % settings.host
            )

        # Sanity check: Did the server send back some kind of internal error?
        # If so, bubble this up.
        if r.status_code >= 500:
            raise exc.ServerError('The Tower server sent back a server error. '
                                  'Please try again later.')

        # Sanity check: Did we fail to authenticate properly?
        # If so, fail out now; this is always a failure.
        if r.status_code == 401:
            raise exc.AuthError('Invalid Tower authentication credentials.')

        # Sanity check: Did we get a forbidden response, which means that
        # the user isn't allowed to do this? Report that.
        if r.status_code == 403:
            raise exc.Forbidden("You don't have permission to do that.")

        # 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.
        if r.status_code == 404:
            raise exc.NotFound('The requested object could not be found.')

        # 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).
        if r.status_code == 405:
            raise exc.MethodNotAllowed(
                "The Tower server says you can't make a request with the "
                "%s method to that URL (%s)." % (method, url),
            )

        # Sanity check: Did we get some other kind of error?
        # If so, write an appropriate error message.
        if r.status_code >= 400:
            raise exc.BadRequest(
                'The Tower server claims it was sent a bad request.\n\n'
                '%s %s\nParams: %s\nData: %s\n\nResponse: %s' %
                (method, url, kwargs.get('params', None),
                 kwargs.get('data', None), r.content.decode('utf8'))
            )

        # Django REST Framework intelligently prints API keys in the
        # order that they are defined in the models and serializer.
        #
        # We want to preserve this behavior when it is possible to do so
        # with minimal effort, because while the order has no explicit meaning,
        # we make some effort to order keys in a convenient manner.
        #
        # To this end, make this response into an APIResponse subclass
        # (defined below), which has a `json` method that doesn't lose key
        # order.
        r.__class__ = APIResponse

        # Return the response object.
        return r