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 _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] # Special case of resources that only only addressable by id if 'id' in self.identity and len(self.identity) == 1: return {} # 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 update(self, inventory_source, monitor=False, wait=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 or wait: inventory_update_id = r.json()['inventory_update'] if monitor: result = self.monitor( inventory_update_id, parent_pk=inventory_source, timeout=timeout) elif wait: result = self.wait( inventory_update_id, parent_pk=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 request(self, method, url, *args, **kwargs): """Make a request to the Ansible Tower API, and return the response. """ # If the URL has the api/vX at the front strip it off # This is common to have if you are extracting a URL from an existing object. # For example, any of the 'related' fields of an object will have this import re url = re.sub("^/?api/v[0-9]+/", "", url) # Piece together the full URL. use_version = not url.startswith('/o/') url = '%s%s' % (self.get_prefix(use_version), url.lstrip('/')) # Ansible Tower expects authenticated requests; add the authentication # from settings if it's provided. kwargs.setdefault( 'auth', BasicTowerAuth(settings.username, settings.password, self)) # 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', {})) r = self._make_request(method, url, args, kwargs) # 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 (HTTP 401).') # 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 (HTTP 403).") # 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
def update(self, inventory_source, monitor=False, wait=False, timeout=None, **kwargs): """Update the given inventory source. =====API DOCS===== Update the given inventory source. :param inventory_source: Primary key or name of the inventory source to be updated. :type inventory_source: str :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched inventory update rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the inventory update, but do not print while it is in progress. :type wait: bool :param timeout: If provided with ``monitor`` flag set, this attempt will time out after the given number of seconds. :type timeout: int :param `**kwargs`: Fields used to override underlyingl inventory source fields when creating and launching an inventory update. :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait`` call if ``wait`` flag is on; dictionary of "status" if none of the two flags are on. :rtype: dict :raises tower_cli.exceptions.BadRequest: When the inventory source cannot be updated. =====API DOCS===== """ # 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), data={}) # If we were told to monitor the project update's status, do so. if monitor or wait: inventory_update_id = r.json()['inventory_update'] if monitor: result = self.monitor(inventory_update_id, parent_pk=inventory_source, timeout=timeout) elif wait: result = self.wait(inventory_update_id, parent_pk=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 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). self._pop_none(kwargs) # 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. missing_fields = [] for i in self.fields: if i.key not in kwargs and i.name not in kwargs and i.required: missing_fields.append(i.key or i.name) if missing_fields and not pk: raise exc.BadRequest('Missing required fields: %s' % ', '.join(missing_fields).replace('_', '-')) # 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 # Reinsert None for special case of null association for key in kwargs: if kwargs[key] == 'null': kwargs[key] = None # Get the URL and method to use for the write. url = self.endpoint method = 'POST' if pk: url = self._get_patch_url(url, 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 += '%s/' % 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). self._pop_none(kwargs) # Remove fields that are specifically excluded from lookup for field in self.fields: if field.no_lookup and field.name in kwargs: kwargs.pop(field.name) # 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].replace('_', '-')) 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. Possibly ' 'caused by not providing required ' 'fields. Please tighten ' 'your criteria.' % resp['count']) # Return the response. return resp