def update(self, inventory_source, monitor=False, timeout=None, **kwargs): """Update the given inventory source.""" # Establish that we are able to update this inventory source # at all. debug.log('Asking whether the inventory source can be updated.', header='details') r = client.get('%s%d/update/' % (self.endpoint, inventory_source)) if not r.json()['can_update']: raise exc.BadRequest('Tower says it cannot run an update against ' 'this inventory source.') # Run the update. debug.log('Updating the inventory source.', header='details') r = client.post('%s%d/update/' % (self.endpoint, inventory_source)) # If we were told to monitor the project update's status, do so. if monitor: result = self.monitor(inventory_source, timeout=timeout) inventory = client.get('/inventory_sources/%d/' % result['inventory_source'])\ .json()['inventory'] result['inventory'] = int(inventory) return result # Done. return {'status': 'ok'}
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 as ex: if fail_on_missing: raise return {}
def _get_or_create_child(self, parent, relationship, **kwargs): ujt_pk = kwargs.get('unified_job_template', None) if ujt_pk is None: raise exceptions.BadRequest( 'A child node must be specified by one of the options ' 'unified-job-template, job-template, project, or ' 'inventory-source') kwargs.update(self._parent_filter(parent, relationship, **kwargs)) response = self.read(fail_on_no_results=False, fail_on_multiple_results=False, **kwargs) if len(response['results']) == 0: debug.log('Creating new workflow node.', header='details') return client.post(self.endpoint, data=kwargs).json() else: return response['results'][0]
def write(self, pk=None, create_on_missing=False, fail_on_found=False, force_on_exists=True, **kwargs): """Modify the given object using the Ansible Tower API. Return the object and a boolean value informing us whether or not the record was changed. If `create_on_missing` is True, then an object matching the appropriate unique criteria is not found, then a new object is created. If there are no unique criteria on the model (other than the primary key), then this will always constitute a creation (even if a match exists) unless the primary key is sent. If `fail_on_found` is True, then if an object matching the unique criteria already exists, the operation fails. If `force_on_exists` is True, then if an object is modified based on matching via. unique fields (as opposed to the primary key), other fields are updated based on data sent. If `force_on_exists` is set to False, then the non-unique values are only written in a creation case. """ existing_data = {} # 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() # Determine which record we are writing, if we weren't given a # primary key. if not pk: debug.log('Checking for an existing record.', header='details') existing_data = self._lookup(fail_on_found=fail_on_found, fail_on_missing=not create_on_missing, include_debug_header=False, **kwargs) if existing_data: pk = existing_data['id'] else: # We already know the primary key, but get the existing data. # This allows us to know whether the write made any changes. debug.log('Getting existing record.', header='details') existing_data = self.get(pk) # Sanity check: Are we missing required values? # If we don't have a primary key, then all required values must be # set, and if they're not, it's an error. required_fields = [i.key or i.name for i in self.fields if i.required] missing_fields = [i for i in required_fields if i not in kwargs] if missing_fields and not pk: raise exc.BadRequest('Missing required fields: %s' % ', '.join(missing_fields)) # Sanity check: Do we need to do a write at all? # If `force_on_exists` is False and the record was, in fact, found, # then no action is required. if pk and not force_on_exists: debug.log( 'Record already exists, and --force-on-exists is off; ' 'do nothing.', header='decision', nl=2) answer = OrderedDict(( ('changed', False), ('id', pk), )) answer.update(existing_data) return answer # Similarly, if all existing data matches our write parameters, # there's no need to do anything. if all( [kwargs[k] == existing_data.get(k, None) for k in kwargs.keys()]): debug.log('All provided fields match existing data; do nothing.', header='decision', nl=2) answer = OrderedDict(( ('changed', False), ('id', pk), )) answer.update(existing_data) return answer # Get the URL and method to use for the write. url = self.endpoint method = 'POST' if pk: url += '%d/' % pk method = 'PATCH' # If debugging is on, print the URL and data being sent. debug.log('Writing the record.', header='details') # Actually perform the write. r = getattr(client, method.lower())(url, data=kwargs) # At this point, we know the write succeeded, and we know that data # was changed in the process. answer = OrderedDict(( ('changed', True), ('id', r.json()['id']), )) answer.update(r.json()) return answer
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