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'], })
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.')
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
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 {}
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
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