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 status(self, pk=None, detail=False, **kwargs): """Print the current job status.""" # Get the job from Ansible Tower. debug.log('Asking for project update status.', header='details') project = client.get('/projects/%d/' % pk).json() # Determine the appropriate project update. if 'current_update' in project['related']: debug.log('A current update exists; retrieving it.', header='details') job = client.get(project['related']['current_update'][7:]).json() elif project['related'].get('last_update', None): debug.log('No current update exists; retrieving the most ' 'recent update.', header='details') job = client.get(project['related']['last_update'][7:]).json() else: raise exc.NotFound('No project 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 { 'elapsed': job['elapsed'], 'failed': job['failed'], 'status': job['status'], }
def test_method_not_allowed_error(self): """Establish that authentication errors raise the MethodNotAllowed exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=405) with self.assertRaises(exc.MethodNotAllowed): client.get('/ping/')
def test_server_error(self): """Establish that server errors raise the ServerError exception as expected. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=500) with self.assertRaises(exc.ServerError): client.get('/ping/')
def test_bad_request_error(self): """Establish that other errors not covered above raise the BadRequest exception. """ with client.test_mode as t: t.register('/ping/', "I'm a teapot!", status_code=418) with self.assertRaises(exc.BadRequest): client.get('/ping/')
def test_not_found_error(self): """Establish that authentication errors raise the NotFound exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=404) with self.assertRaises(exc.NotFound): client.get('/ping/')
def test_forbidden_error(self): """Establish that forbidden errors raise the ForbiddenError exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=403) with self.assertRaises(exc.Forbidden): client.get('/ping/')
def test_auth_error(self): """Establish that authentication errors raise the AuthError exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=401) with self.assertRaises(exc.AuthError): client.get('/ping/')
def test_connection_ssl_error(self): """Establish that if we get a ConnectionError or an SSLError back from requests, that we deal with it nicely. """ for ErrorType in REQUESTS_ERRORS: with settings.runtime_values(verbose=False, host='https://foo.co'): with mock.patch.object(Session, 'request') as req: req.side_effect = ErrorType with self.assertRaises(exc.ConnectionError): client.get('/ping/')
def test_failed_suggestion_protocol(self): """Establish that if connection fails and protocol not given, tower-cli suggests that to the user.""" with settings.runtime_values(verbose=False, host='foo.co'): with mock.patch.object(Session, 'request') as req: req.side_effect = requests.exceptions.SSLError with mock.patch.object(click, 'secho') as secho: with self.assertRaises(exc.ConnectionError): client.get('/ping/') self.assertTrue(secho.called)
def test_disable_connection_warning(self): """Establish that the --insecure flag will cause the program to call disable_warnings in the urllib3 package. """ with mock.patch('requests.packages.urllib3.disable_warnings') as g: with client.test_mode as t: t.register('/ping/', "I'm a teapot!", status_code=200) with settings.runtime_values(insecure=False): client.get('/ping/') assert g.called
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={}) inventory_update_id = r.json()['inventory_update'] # If we were told to monitor the project update's status, do so. if monitor or wait: 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 { 'id': inventory_update_id, 'status': 'ok' }
def test_connection_ssl(self): with client.test_mode as t: t.register_json('/ping/', {'status': 'ok'}) https_adapter = client.adapters['https://'] with mock.patch.object(FauxAdapter, 'send', wraps=https_adapter.send) as mock_send: client.get('/ping/') mock_send.assert_called_once_with( mock.ANY, cert=None, proxies=mock.ANY, stream=mock.ANY, timeout=mock.ANY, verify=True ) self.assertTrue(mock_send.call_args[1]['verify'])
def test_connection_ssl_error_verbose(self): """Establish that if we get a ConnectionError or an SSLError back from requests, that we deal with it nicely, and additionally print the internal error if verbose is True. """ for ErrorType in REQUESTS_ERRORS: with settings.runtime_values(verbose=True, host='https://foo.co'): with mock.patch.object(Session, 'request') as req: req.side_effect = ErrorType with mock.patch.object(debug, 'log') as dlog: with self.assertRaises(exc.ConnectionError): client.get('/ping/') self.assertEqual(dlog.call_count, 5)
def list(self, **kwargs): """Return a list of objects. =====API DOCS===== Retrieve a list of Tower settings. :param category: The category slug in which to look up indevidual settings. :type category: str :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict =====API DOCS===== """ self.custom_category = kwargs.get('category', 'all') try: result = super(Resource, self).list(**kwargs) except exc.NotFound as e: categories = map( lambda category: category['slug'], client.get('/settings/').json()['results'] ) e.message = '%s is not a valid category. Choose from [%s]' % ( kwargs['category'], ', '.join(categories) ) raise e finally: self.custom_category = None return { 'results': [{'id': k, 'value': v} for k, v in result.items()] }
def list(self, root=False, **kwargs): """Return a list of groups.""" # Option to list children of a parent group if kwargs.get('parent', None): self.set_child_endpoint( parent=kwargs['parent'], inventory=kwargs.get('inventory', None) ) kwargs.pop('parent') # Sanity check: If we got `--root` and no inventory, that's an # error. if root and not kwargs.get('inventory', None): raise exc.UsageError('The --root option requires specifying an ' 'inventory also.') # If we are tasked with getting root groups, do that. if root: inventory_id = kwargs['inventory'] r = client.get('/inventories/%d/root_groups/' % inventory_id) return r.json() # Return the superclass implementation. return super(Resource, self).list(**kwargs)
def update(self, pk=None, create_on_missing=False, monitor=False, timeout=None, name=None, organization=None): """Trigger a project update job within Ansible Tower. Only meaningful on non-manual projects. """ # First, get the appropriate project. # This should be uniquely identified at this point, and if not, then # we just want the error that `get` will throw to bubble up. project = self.get(pk, name=name, organization=organization) pk = project['id'] # Determine whether this project is able to be updated. debug.log('Asking whether the project can be updated.', header='details') result = client.get('/projects/%d/update/' % pk) if not result.json()['can_update']: raise exc.CannotStartJob('Cannot update project.') # Okay, this project can be updated, according to Tower. # Commence the update. debug.log('Updating the project.', header='details') result = client.post('/projects/%d/update/' % pk) # If we were told to monitor the project update's status, do so. if monitor: return self.monitor(pk, timeout=timeout) # Return the project update ID. return { 'changed': True, }
def status(self, pk=None, detail=False, **kwargs): """Print the current job status. This is used to check a running job. You can look up the job with the same parameters used for a get request.""" # Remove default values (anything where the value is None). self._pop_none(kwargs) # Search for the record if pk not given if not pk: job = self.get(include_debug_header=True, **kwargs) # Get the job from Ansible Tower if pk given else: debug.log('Asking for job status.', header='details') finished_endpoint = '%s%d/' % (self.endpoint, pk) job = client.get(finished_endpoint).json() # 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 version(): """Display full version information.""" # Print out the current version of Tower CLI. click.echo('Tower CLI %s' % __version__) # Print out the current API version of the current code base. click.echo('API %s' % CUR_API_VERSION) # Attempt to connect to the Ansible Tower server. # If we succeed, print a version; if not, generate a failure. try: r = client.get('/config/') except RequestException as ex: raise exc.TowerCLIError('Could not connect to Ansible Tower.\n%s' % six.text_type(ex)) config = r.json() license = config.get('license_info', {}).get('license_type', 'open') if license == 'open': server_type = 'AWX' else: server_type = 'Ansible Tower' click.echo('%s %s' % (server_type, config['version'])) # Print out Ansible version of server click.echo('Ansible %s' % config['ansible_version'])
def callback(self, pk=None, host_config_key='', extra_vars=None): """Contact Tower and request a configuration update using this job template. =====API DOCS===== Contact Tower and request a provisioning callback using this job template. :param pk: Primary key of the job template to run provisioning callback against. :type pk: int :param host_config_key: Key string used to authenticate the callback host. :type host_config_key: str :param extra_vars: Extra variables that are passed to provisioning callback. :type extra_vars: array of str :returns: A dictionary of a single key "changed", which indicates whether the provisioning callback is successful. :rtype: dict =====API DOCS===== """ url = self.endpoint + '%s/callback/' % pk if not host_config_key: host_config_key = client.get(url).json()['host_config_key'] post_data = {'host_config_key': host_config_key} if extra_vars: post_data['extra_vars'] = parser.process_extra_vars(list(extra_vars), force_json=True) r = client.post(url, data=post_data, auth=None) if r.status_code == 201: return {'changed': True}
def insights(self, pk=None, **kwargs): """Return a JSON object of host insights. Note global option --format is not available here, as the output would always be JSON-formatted. """ res = self.get(pk=pk, **kwargs) url = self.endpoint + '%d/%s/' % (res['id'], 'insights') return client.get(url, params={}).json()
def last_job_data(self, pk=None, **kwargs): """ Internal utility function for Unified Job Templates Returns data about the last job run off of that UJT """ ujt = self.get(pk, include_debug_header=True, **kwargs) # Determine the appropriate inventory source update. if 'current_update' in ujt['related']: debug.log('A current job; retrieving it.', header='details') return client.get(ujt['related']['current_update'][7:]).json() elif ujt['related'].get('last_update', None): debug.log('No current job or update exists; retrieving the most ' 'recent.', header='details') return client.get(ujt['related']['last_update'][7:]).json() else: raise exc.NotFound('No related jobs or updates exist.')
def test_server_error(self): """Establish that server errors raise the ServerError exception as expected. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=500) with self.assertRaises(exc.ServerError): r = client.get('/ping/')
def survey(self, pk=None, **kwargs): """Get the survey_spec for the job template. To write a survey, use the modify command with the --survey-spec parameter.""" job_template = self.get(pk=pk, **kwargs) if settings.format == 'human': settings.format = 'json' return client.get(self._survey_endpoint(job_template['id'])).json()
def test_forbidden_error(self): """Establish that forbidden errors raise the ForbiddenError exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=403) with self.assertRaises(exc.Forbidden): r = client.get('/ping/')
def test_bad_request_error(self): """Establish that other errors not covered above raise the BadRequest exception. """ with client.test_mode as t: t.register('/ping/', "I'm a teapot!", status_code=418) with self.assertRaises(exc.BadRequest): r = client.get('/ping/')
def test_insecure_connection(self): """Establish that the --insecure flag will cause the program to call request with verify=False. """ with mock.patch('requests.sessions.Session.request') as g: mock_response = type('statobj', (), {})() # placeholder object mock_response.status_code = 200 g.return_value = mock_response with client.test_mode as t: t.register('/ping/', "I'm a teapot!", status_code=200) with settings.runtime_values(verify_ssl=False): client.get('/ping/') g.assert_called_once_with( # The point is to assure verify=False below 'GET', mock.ANY, allow_redirects=True, auth=mock.ANY, verify=False )
def tower_check_mode(module): '''Execute check mode logic for Ansible Tower modules''' if module.check_mode: try: result = client.get('/ping').json() module.exit_json(changed=True, tower_version='{0}'.format(result['version'])) except (exc.ServerError, exc.ConnectionError, exc.BadRequest) as excinfo: module.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo))
def test_auth_error(self): """Establish that authentication errors raise the AuthError exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=401) with self.assertRaises(exc.AuthError): r = client.get('/ping/')
def test_method_not_allowed_error(self): """Establish that authentication errors raise the MethodNotAllowed exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=405) with self.assertRaises(exc.MethodNotAllowed): r = client.get('/ping/')
def test_not_found_error(self): """Establish that authentication errors raise the NotFound exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=404) with self.assertRaises(exc.NotFound): r = client.get('/ping/')
def test_connection_error(self): """Establish that if we get a ConnectionError back from requests, that we deal with it nicely. """ with settings.runtime_values(verbose=False): with mock.patch.object(Session, 'request') as req: req.side_effect = requests.exceptions.ConnectionError with self.assertRaises(exc.ConnectionError): r = client.get('/ping/')
def launch(self, monitor=False, wait=False, timeout=None, **kwargs): """Launch a new ad-hoc command. Runs a user-defined command from Ansible Tower, immediately starts it, and returns back an ID in order for its status to be monitored. =====API DOCS===== Launch a new ad-hoc command. :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched command rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the job, but do not print while job 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 needed to create and launch an ad hoc command. :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait`` call if ``wait`` flag is on; dictionary of "id" and "changed" if none of the two flags are on. :rtype: dict :raises tower_cli.exceptions.TowerCLIError: When ad hoc commands are not available in Tower backend. =====API DOCS===== """ # This feature only exists for versions 2.2 and up r = client.get('/') if 'ad_hoc_commands' not in r.json(): raise exc.TowerCLIError('Your host is running an outdated version' 'of Ansible Tower that can not run ' 'ad-hoc commands (2.2 or earlier)') # Pop the None arguments because we have no .write() method in # inheritance chain for this type of resource. This is needed self._pop_none(kwargs) # Actually start the command. debug.log('Launching the ad-hoc command.', header='details') result = client.post(self.endpoint, data=kwargs) command = result.json() command_id = command['id'] # If we were told to monitor the command once it started, then call # monitor from here. if monitor: return self.monitor(command_id, timeout=timeout) elif wait: return self.wait(command_id, timeout=timeout) # Return the command ID and other response data answer = OrderedDict(( ('changed', True), ('id', command_id), )) answer.update(result.json()) return answer
def callback(self, pk=None, host_config_key='', extra_vars=None): """Contact Tower and request a configuration update using this job template.""" url = self.endpoint + '%s/callback/' % pk if not host_config_key: host_config_key = client.get(url).json()['host_config_key'] post_data = {'host_config_key': host_config_key} if extra_vars: post_data['extra_vars'] = parser.process_extra_vars(list(extra_vars), force_json=True) r = client.post(url, data=post_data, auth=None) if r.status_code == 201: return {'changed': True}
def test_connection_error_verbose(self): """Establish that if we get a ConnectionError back from requests, that we deal with it nicely, and additionally print the internal error if verbose is True. """ with settings.runtime_values(verbose=True): with mock.patch.object(Session, 'request') as req: req.side_effect = requests.exceptions.ConnectionError with mock.patch.object(debug, 'log') as dlog: with self.assertRaises(exc.ConnectionError): r = client.get('/ping/') self.assertEqual(dlog.call_count, 5)
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 sync(self, inventory_source, **kwargs): """Update the given inventory source.""" # Establish that we are able to update this inventory source # at all. 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. r = client.post('%s%d/update/' % (self.endpoint, inventory_source)) return {'status': 'ok'}
def list(self, **kwargs): """Return a list of objects.""" self.custom_category = kwargs.get('category', 'all') try: result = super(Resource, self).list(**kwargs) except exc.NotFound as e: categories = map(lambda category: category['slug'], client.get('/settings/').json()['results']) e.message = '%s is not a valid category. Choose from [%s]' % ( kwargs['category'], ', '.join(categories)) raise e finally: self.custom_category = None return {'results': [{'id': k, 'value': v} for k, v in result.items()]}
def version(**kwargs): """Display version information.""" # Print out the current version of Tower CLI. click.echo('Tower CLI %s' % __version__) # Attempt to connect to the Ansible Tower server. # If we succeed, print a version; if not, generate a failure. try: r = client.get('/config/') click.echo('Ansible Tower %s' % r.json()['version']) except RequestException as ex: raise TowerCLIError('Could not connect to Ansible Tower.\n%s' % six.text_type(ex))
def version(): """Display version information.""" # Print out the current version of Tower CLI. click.echo('Tower CLI %s' % __version__) # Attempt to connect to the Ansible Tower server. # If we succeed, print a version; if not, generate a failure. try: r = client.get('/config/') click.echo('Ansible Tower %s' % r.json()['version']) except RequestException as ex: raise TowerCLIError('Could not connect to Ansible Tower.\n%s' % six.text_type(ex))
def _assoc(self, url_fragment, me, other): """Associate the `other` record with the `me` record.""" # Get the endpoint for foreign records within this object. url = self.endpoint + '%d/%s/' % (me, url_fragment) # Attempt to determine whether the other record already exists here, # for the "changed" moniker. r = client.get(url, params={'id': other}).json() if r['count'] > 0: return {'changed': False} # Send a request adding the other record to this one. r = client.post(url, data={'associate': True, 'id': other}) return {'changed': True}
def _disassoc(self, url_fragment, me, other): """Disassociate the `other` record from the `me` record.""" # Get the endpoint for foreign records within this object. url = self.endpoint + '%d/%s/' % (me, url_fragment) # Attempt to determine whether the other record already is absent, for # the "changed" moniker. r = client.get(url, params={'id': other}).json() if r['count'] == 0: return {'changed': False} # Send a request removing the foreign record from this one. r = client.post(url, data={'disassociate': True, 'id': other}) return {'changed': True}
def launch(self, monitor=False, wait=False, timeout=None, become=False, **kwargs): """Launch a new ad-hoc command. Runs a user-defined command from Ansible Tower, immediately starts it, and returns back an ID in order for its status to be monitored. """ # This feature only exists for versions 2.2 and up r = client.get('/') if 'ad_hoc_commands' not in r.json(): raise exc.TowerCLIError('Your host is running an outdated version' 'of Ansible Tower that can not run ' 'ad-hoc commands (2.2 or earlier)') # Pop the None arguments because we have no .write() method in # inheritance chain for this type of resource. This is needed self._pop_none(kwargs) # Change the flag to the dictionary format if become: kwargs['become_enabled'] = True # Actually start the command. debug.log('Launching the ad-hoc command.', header='details') result = client.post(self.endpoint, data=kwargs) command = result.json() command_id = command['id'] # If we were told to monitor the command once it started, then call # monitor from here. if monitor: return self.monitor(command_id, timeout=timeout) elif wait: return self.wait(command_id, timeout=timeout) # Return the command ID and other response data answer = OrderedDict(( ('changed', True), ('id', command_id), )) answer.update(result.json()) return answer
def list(self, root=False, **kwargs): """Return a list of groups.""" # Sanity check: If we got `--root` and no inventory, that's an # error. if root and not kwargs.get('inventory', None): raise exc.UsageError('The --root option requires specifying an ' 'inventory also.') # If we are tasked with getting root groups, do that. if root: inventory_id = kwargs['inventory'] r = client.get('/inventories/%d/root_groups/' % inventory_id) return r.json() # Return the superclass implementation. return super(Resource, self).list(**kwargs)
def insights(self, pk=None, **kwargs): """Return a JSON object of host insights. Note global option --format is not available here, as the output would always be JSON-formatted. =====API DOCS===== List host insights. :param pk: Primary key of the target host. :type pk: int :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object of host insights. :rtype: dict =====API DOCS===== """ res = self.get(pk=pk, **kwargs) url = self.endpoint + '%d/%s/' % (res['id'], 'insights') return client.get(url, params={}).json()
def status(self, pk, detail=False): """Print the current job status.""" # Get the job from Ansible Tower. debug.log('Asking for job status.', header='details') job = client.get('/jobs/%d/' % pk).json() # 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 update(self, pk=None, create_on_missing=False, monitor=False, wait=False, timeout=None, name=None, organization=None): """Trigger a project update job within Ansible Tower. Only meaningful on non-manual projects. """ # First, get the appropriate project. # This should be uniquely identified at this point, and if not, then # we just want the error that `get` will throw to bubble up. project = self.get(pk, name=name, organization=organization) pk = project['id'] # Determine whether this project is able to be updated. debug.log('Asking whether the project can be updated.', header='details') result = client.get('/projects/%d/update/' % pk) if not result.json()['can_update']: raise exc.CannotStartJob('Cannot update project.') # Okay, this project can be updated, according to Tower. # Commence the update. debug.log('Updating the project.', header='details') result = client.post('/projects/%d/update/' % pk) # If we were told to monitor the project update's status, do so. if monitor: project_update_id = result.json()['project_update'] return self.monitor(project_update_id, parent_pk=pk, timeout=timeout) elif wait: project_update_id = result.json()['project_update'] return self.wait(project_update_id, parent_pk=pk, timeout=timeout) # Return the project update ID. return { 'changed': True, }
def lookup_stdout(self, pk=None, start_line=None, end_line=None, full=True): """ Internal utility function to return standard out requires the pk of a unified job """ stdout_url = '%s%s/stdout/' % (self.unified_job_type, pk) payload = { 'format': 'json', 'content_encoding': 'base64', 'content_format': 'ansi'} if start_line: payload['start_line'] = start_line if end_line: payload['end_line'] = end_line debug.log('Requesting a copy of job standard output', header='details') resp = client.get(stdout_url, params=payload).json() content = b64decode(resp['content']) return content
def test_request_ok(self): """Establish that a request that returns a valid JSON response returns without incident and comes back as an APIResponse. """ with client.test_mode as t: t.register_json('/ping/', {'status': 'ok'}) r = client.get('/ping/') # Establish that our response is an APIResponse and that our # JSONification method returns back an ordered dict. self.assertIsInstance(r, APIResponse) self.assertIsInstance(r.json(), OrderedDict) # Establish that our headers have expected auth. request = r.request self.assertEqual(request.headers['Authorization'], 'Basic bWVhZ2FuOlRoaXMgaXMgdGhlIGJlc3Qgd2luZS4=') # Make sure the content matches what we expect. self.assertEqual(r.json(), {'status': 'ok'})
def list(self, root=False, **kwargs): """Return a list of groups. =====API DOCS===== Retrieve a list of groups. :param root: Flag that if set, only root groups of a specific inventory will be listed. :type root: bool :param parent: Primary key or name of the group whose child groups will be listed. :type parent: str :param all_pages: Flag that if set, collect all pages of content from the API when returning results. :type all_pages: bool :param page: The page to show. Ignored if all_pages is set. :type page: int :param query: Contains 2-tuples used as query parameters to filter resulting resource objects. :type query: list :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict :raises tower_cli.exceptions.UsageError: When ``root`` flag is on and ``inventory`` is not present in ``**kwargs``. =====API DOCS===== """ # Option to list children of a parent group if kwargs.get('parent', None): self.set_child_endpoint(parent=kwargs['parent'], inventory=kwargs.get('inventory', None)) kwargs.pop('parent') # Sanity check: If we got `--root` and no inventory, that's an error. if root and not kwargs.get('inventory', None): raise exc.UsageError( 'The --root option requires specifying an inventory also.') # If we are tasked with getting root groups, do that. if root: inventory_id = kwargs['inventory'] r = client.get('/inventories/%d/root_groups/' % inventory_id) return r.json() # Return the superclass implementation. return super(Resource, self).list(**kwargs)
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 main(): argument_spec = dict( workflow_template=dict(required=True), extra_vars=dict(required=False), wait=dict(required=False, default=True, type='bool'), timeout=dict(required=False, default=None, type='int'), ) module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) workflow_template = module.params.get('workflow_template') extra_vars = module.params.get('extra_vars') wait = module.params.get('wait') timeout = module.params.get('timeout') # If we are going to use this result to return we can consider ourselfs changed result = dict(changed=False, msg='initial message') tower_auth = tower_auth_config(module) with settings.runtime_values(**tower_auth): # First we will test the connection. This will be a test for both check and run mode # Note, we are not using the tower_check_mode method here because we want to do more than just a ping test # If we are in check mode we also want to validate that we can find the workflow try: ping_result = client.get('/ping').json() # Stuff the version into the results as an FYI result['tower_version'] = ping_result['version'] except (ServerError, ConnectionError, BadRequest) as excinfo: result['msg'] = "Failed to reach Tower: {0}".format(excinfo) module.fail_json(**result) # Now that we know we can connect, lets verify that we can resolve the workflow_template try: workflow = tower_cli.get_resource("workflow").get( **{'name': workflow_template}) except TowerCLIError as e: result['msg'] = "Failed to find workflow: {0}".format(e) module.fail_json(**result) # Since we were able to find the workflow, if we are in check mode we can return now if module.check_mode: result['msg'] = "Check mode passed" module.exit_json(**result) # We are no ready to run the workflow try: result['job_info'] = tower_cli.get_resource('workflow_job').launch( workflow_job_template=workflow['id'], monitor=False, wait=wait, timeout=timeout, extra_vars=extra_vars) if wait: # If we were waiting for a result we will fail if the workflow failed if result['job_info']['failed']: result['msg'] = "Workflow execution failed" module.fail_json(**result) else: module.exit_json(**result) # We were not waiting and there should be no way we can make it here without the workflow fired off so we can return a success module.exit_json(**result) except TowerCLIError as e: result['msg'] = "Failed to execute workflow: {0}".format(e) module.fail_json(**result)