def login(username, password, scope, client_id, client_secret, verbose): """ Retrieves and stores an OAuth2 personal auth token. """ if not supports_oauth(): raise exc.TowerCLIError( 'This version of Tower does not support OAuth2.0. Set credentials using tower-cli config.' ) # Explicitly set a basic auth header for PAT acquisition (so that we don't # try to auth w/ an existing user+pass or oauth2 token in a config file) req = collections.namedtuple('req', 'headers')({}) if client_id and client_secret: HTTPBasicAuth(client_id, client_secret)(req) req.headers['Content-Type'] = 'application/x-www-form-urlencoded' r = client.post( '/o/token/', data={ "grant_type": "password", "username": username, "password": password, "scope": scope }, headers=req.headers ) elif client_id: req.headers['Content-Type'] = 'application/x-www-form-urlencoded' r = client.post( '/o/token/', data={ "grant_type": "password", "username": username, "password": password, "client_id": client_id, "scope": scope }, headers=req.headers ) else: HTTPBasicAuth(username, password)(req) r = client.post( '/users/{}/personal_tokens/'.format(username), data={"description": "Tower CLI", "application": None, "scope": scope}, headers=req.headers ) if r.ok: result = r.json() result.pop('summary_fields', None) result.pop('related', None) if client_id: token = result.pop('access_token', None) else: token = result.pop('token', None) if settings.verbose: # only print the actual token if -v result['token'] = token secho(json.dumps(result, indent=1), fg='blue', bold=True) config.main(['oauth_token', token, '--scope=user'])
def login(username, password, scope, client_id, client_secret, verbose): """ Retrieves and stores an OAuth2 personal auth token. """ if not supports_oauth(): raise exc.TowerCLIError( 'This version of Tower does not support OAuth2.0. Set credentials using tower-cli config.' ) # Explicitly set a basic auth header for PAT acquisition (so that we don't # try to auth w/ an existing user+pass or oauth2 token in a config file) req = collections.namedtuple('req', 'headers')({}) if client_id and client_secret: HTTPBasicAuth(client_id, client_secret)(req) req.headers['Content-Type'] = 'application/x-www-form-urlencoded' r = client.post('/o/token/', data={ "grant_type": "password", "username": username, "password": password, "scope": scope }, headers=req.headers) elif client_id: req.headers['Content-Type'] = 'application/x-www-form-urlencoded' r = client.post('/o/token/', data={ "grant_type": "password", "username": username, "password": password, "client_id": client_id, "scope": scope }, headers=req.headers) else: HTTPBasicAuth(username, password)(req) r = client.post('/users/{}/personal_tokens/'.format(username), data={ "description": "Tower CLI", "application": None, "scope": scope }, headers=req.headers) if r.ok: result = r.json() result.pop('summary_fields', None) result.pop('related', None) if client_id: token = result.pop('access_token', None) else: token = result.pop('token', None) if settings.verbose: # only print the actual token if -v result['token'] = token secho(json.dumps(result, indent=1), fg='blue', bold=True) config.main(['oauth_token', token, '--scope=user'])
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) data['content_type__model'] = res_type.replace('_', '') 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/' % (grammar.pluralize(obj_type), obj), data=post_data) role_data['changed'] = True return role_data
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) data['content_type__model'] = res_type.replace('_', '') 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/' % (grammar.pluralize(obj_type), obj), data=post_data) role_data['changed'] = True return role_data
def relaunch(self, pk=None, **kwargs): """Relaunch a stopped job. Fails with a non-zero exit status if the job cannot be relaunched. You must provide either a pk or parameters in the job's identity. """ # Search for the record if pk not given if not pk: existing_data = self.get(**kwargs) pk = existing_data['id'] relaunch_endpoint = '%s%s/relaunch/' % (self.endpoint, pk) data = {} # Attempt to relaunch the job. answer = {} try: result = client.post(relaunch_endpoint, data=data).json() if 'id' in result: answer.update(result) answer['changed'] = True except exc.MethodNotAllowed: answer['changed'] = False # Return the answer. return answer
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 cancel(self, pk, fail_if_not_running=False): """Cancel a currently running job. Fails with a non-zero exit status if the job cannot be canceled. """ # Attempt to cancel the job. try: client.post('/jobs/%d/cancel/' % pk) changed = True except exc.MethodNotAllowed: changed = False if fail_if_not_running: raise exc.TowerCLIError('Job not running.') # Return a success. return adict({'status': 'canceled', 'changed': changed})
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 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 modify(self, setting, value): """Modify an already existing object.""" prev_value = new_value = self.get(setting)['value'] answer = OrderedDict() encrypted = '$encrypted$' in six.text_type(prev_value) if encrypted or six.text_type(prev_value) != six.text_type(value): if setting == 'LICENSE': r = client.post('/config/', data=self.coerce_type(setting, value)) new_value = r.json() else: r = client.patch( self.endpoint, data={setting: self.coerce_type(setting, value)}) new_value = r.json()[setting] answer.update(r.json()) changed = encrypted or (prev_value != new_value) answer.update({ 'changed': changed, 'id': setting, 'value': new_value, }) return answer
def modify(self, setting, value): """Modify an already existing object. Positional argument SETTING is the setting name and VALUE is its value, which can be provided directly or obtained from a file name if prefixed with '@'. """ prev_value = new_value = self.get(setting)['value'] answer = OrderedDict() encrypted = '$encrypted$' in six.text_type(prev_value) if encrypted or six.text_type(prev_value) != six.text_type(value): if setting == 'LICENSE': r = client.post('/config/', data=self.coerce_type(setting, value)) new_value = r.json() else: r = client.patch( self.endpoint, data={setting: self.coerce_type(setting, value)}) new_value = r.json()[setting] answer.update(r.json()) changed = encrypted or (prev_value != new_value) answer.update({ 'changed': changed, 'id': setting, 'value': new_value, }) return answer
def launch(self, workflow_job_template=None, monitor=False, wait=False, timeout=None, extra_vars=None, **kwargs): """Launch a new workflow job based on a workflow job template. Creates a new workflow job in Ansible Tower, starts it, and returns back an ID in order for its status to be monitored. """ if len(extra_vars) > 0: kwargs['extra_vars'] = parser.process_extra_vars(extra_vars) debug.log('Launching the workflow job.', header='details') self._pop_none(kwargs) post_response = client.post( 'workflow_job_templates/{}/launch/'.format(workflow_job_template), data=kwargs).json() workflow_job_id = post_response['id'] post_response['changed'] = True if monitor: return self.monitor(workflow_job_id, timeout=timeout) elif wait: return self.wait(workflow_job_id, timeout=timeout) return post_response
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 batch_update(self, pk=None, **kwargs): """Update all related inventory sources of the given inventory. 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'], 'update_inventory_sources') return client.post(url, data={}).json()
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 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 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 launch(self, workflow_job_template=None, monitor=False, wait=False, timeout=None, extra_vars=None, **kwargs): """Launch a new workflow job based on a workflow job template. Creates a new workflow job in Ansible Tower, starts it, and returns back an ID in order for its status to be monitored. =====API DOCS===== Launch a new workflow job based on a workflow job template. :param workflow_job_template: Primary key or name of the workflow job template to launch new job. :type workflow_job_template: str :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched workflow job rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the workflow 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 extra_vars: yaml formatted texts that contains extra variables to pass on. :type extra_vars: array of strings :param `**kwargs`: Fields needed to create and launch a workflow job. :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait`` call if ``wait`` flag is on; loaded JSON output of the job launch if none of the two flags are on. :rtype: dict =====API DOCS===== """ if extra_vars is not None and len(extra_vars) > 0: kwargs['extra_vars'] = parser.process_extra_vars(extra_vars) debug.log('Launching the workflow job.', header='details') self._pop_none(kwargs) post_response = client.post( 'workflow_job_templates/{0}/launch/'.format(workflow_job_template), data=kwargs).json() workflow_job_id = post_response['id'] post_response['changed'] = True if monitor: return self.monitor(workflow_job_id, timeout=timeout) elif wait: return self.wait(workflow_job_id, timeout=timeout) return post_response
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 test_request_post(self): """Establish that on a POST request, we encode the provided data to JSON automatically. """ with client.test_mode as t: t.register_json('/ping/', {'status': 'ok'}, method='POST') r = client.post('/ping/', {'payload': 'this is my payload.'}) # Establish that our request has the expected payload, and # is sent using an application/json content type. headers = r.request.headers self.assertEqual(headers['Content-Type'], 'application/json') self.assertEqual(r.request.body, '{"payload": "this is my payload."}')
def cancel(self, pk=None, fail_if_not_running=False, **kwargs): """Cancel a currently running job. Fails with a non-zero exit status if the job cannot be canceled. You must provide either a pk or parameters in the job's identity. """ # Search for the record if pk not given if not pk: existing_data = self.get(**kwargs) pk = existing_data['id'] cancel_endpoint = '%s%d/cancel/' % (self.endpoint, pk) # Attempt to cancel the job. try: client.post(cancel_endpoint) changed = True except exc.MethodNotAllowed: changed = False if fail_if_not_running: raise exc.TowerCLIError('Job not running.') # Return a success. return adict({'status': 'canceled', 'changed': changed})
def cancel(self, pk=None, fail_if_not_running=False, **kwargs): """Cancel a currently running job. Fails with a non-zero exit status if the job cannot be canceled. You must provide either a pk or parameters in the job's identity. """ # Search for the record if pk not given if not pk: existing_data = self.get(**kwargs) pk = existing_data['id'] cancel_endpoint = '%s%d/cancel/' % (self.endpoint, pk) # Attempt to cancel the job. try: client.post(cancel_endpoint) changed = True except exc.MethodNotAllowed: changed = False if fail_if_not_running: raise exc.TowerCLIError('Job not running.') # Return a success. return {'status': 'canceled', 'changed': changed}
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 _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 _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 launch(self, workflow_job_template=None, monitor=False, wait=False, timeout=None, extra_vars=None, **kwargs): """Launch a new workflow job based on a workflow job template. Creates a new workflow job in Ansible Tower, starts it, and returns back an ID in order for its status to be monitored. =====API DOCS===== Launch a new workflow job based on a workflow job template. :param workflow_job_template: Primary key or name of the workflow job template to launch new job. :type workflow_job_template: str :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched workflow job rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the workflow 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 extra_vars: yaml formatted texts that contains extra variables to pass on. :type extra_vars: array of strings :param `**kwargs`: Fields needed to create and launch a workflow job. :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait`` call if ``wait`` flag is on; loaded JSON output of the job launch if none of the two flags are on. :rtype: dict =====API DOCS===== """ if extra_vars is not None and len(extra_vars) > 0: kwargs['extra_vars'] = parser.process_extra_vars(extra_vars) debug.log('Launching the workflow job.', header='details') self._pop_none(kwargs) post_response = client.post('workflow_job_templates/{0}/launch/'.format( workflow_job_template), data=kwargs).json() workflow_job_id = post_response['id'] post_response['changed'] = True if monitor: return self.monitor(workflow_job_id, timeout=timeout) elif wait: return self.wait(workflow_job_id, timeout=timeout) return post_response
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 modify(self, setting, value): """Modify an already existing object. Positional argument SETTING is the setting name and VALUE is its value, which can be provided directly or obtained from a file name if prefixed with '@'. =====API DOCS===== Modify an already existing Tower setting. :param setting: The name of the Tower setting to be modified. :type setting: str :param value: The new value of the Tower setting. :type value: str :returns: A dictionary combining the JSON output of the modified resource, as well as two extra fields: "changed", a flag indicating if the resource is successfully updated; "id", an integer which is the primary key of the updated object. :rtype: dict =====API DOCS===== """ prev_value = new_value = self.get(setting)['value'] answer = OrderedDict() encrypted = '$encrypted$' in six.text_type(prev_value) if encrypted or six.text_type(prev_value) != six.text_type(value): if setting == 'LICENSE': r = client.post('/config/', data=self.coerce_type(setting, value)) new_value = r.json() else: r = client.patch( self.endpoint, data={setting: self.coerce_type(setting, value)} ) new_value = r.json()[setting] answer.update(r.json()) changed = encrypted or (prev_value != new_value) answer.update({ 'changed': changed, 'id': setting, 'value': new_value, }) return answer
def batch_update(self, pk=None, **kwargs): """Update all related inventory sources of the given inventory. Note global option --format is not available here, as the output would always be JSON-formatted. =====API DOCS===== Update all related inventory sources of the given inventory. :param pk: Primary key of the given inventory. :type pk: int :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object of update status of the given inventory. :rtype: dict =====API DOCS===== """ res = self.get(pk=pk, **kwargs) url = self.endpoint + '%d/%s/' % (res['id'], 'update_inventory_sources') return client.post(url, data={}).json()
def launch(self, monitor=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) # Return the command ID and other response data answer = OrderedDict(( ('changed', True), ('id', command_id), )) answer.update(result.json()) return answer
def write(self, pk=None, **kwargs): survey_input = kwargs.pop('survey_spec', None) if kwargs.get('extra_vars', None): kwargs['extra_vars'] = parser.process_extra_vars( kwargs['extra_vars']) ret = super(SurveyResource, self).write(pk=pk, **kwargs) if survey_input is not None and ret.get('id', None): if not isinstance(survey_input, dict): survey_input = json.loads(survey_input.strip(' ')) if survey_input == {}: debug.log('Deleting the survey_spec.', header='details') r = client.delete(self._survey_endpoint(ret['id'])) else: debug.log('Saving the survey_spec.', header='details') r = client.post(self._survey_endpoint(ret['id']), data=survey_input) if r.status_code == 200: ret['changed'] = True if survey_input and not ret['survey_enabled']: debug.log('For survey to take effect, set survey_enabled' ' field to True.', header='warning') return ret
def launch(self, job_template=None, monitor=False, timeout=None, no_input=True, extra_vars=None, **kwargs): """Launch a new job based on a job template. Creates a new job in Ansible Tower, immediately starts it, and returns back an ID in order for its status to be monitored. """ # Get the job template from Ansible Tower. # This is used as the baseline for starting the job. tags = kwargs.get('tags', None) use_job_endpoint = kwargs.pop('use_job_endpoint', False) jt_resource = get_resource('job_template') jt = jt_resource.get(job_template) # Update the job data by adding an automatically-generated job name, # and removing the ID. data = copy(jt) data['job_template'] = data.pop('id') data['name'] = '%s [invoked via. Tower CLI]' % data['name'] if tags: data['job_tags'] = tags # Initialize an extra_vars list that starts with the job template # preferences first, if they exist extra_vars_list = [] if 'extra_vars' in data and len(data['extra_vars']) > 0: # But only do this for versions before 2.3 debug.log('Getting version of Tower.', header='details') r = client.get('/config/') if LooseVersion(r.json()['version']) < LooseVersion('2.4'): extra_vars_list = [data['extra_vars']] # Add the runtime extra_vars to this list if extra_vars: extra_vars_list += list(extra_vars) # accept tuples # If the job template requires prompting for extra variables, # do so (unless --no-input is set). if data.pop('ask_variables_on_launch', False) and not no_input \ and not extra_vars: # If JT extra_vars are JSON, echo them to user as YAML initial = parser.process_extra_vars([data['extra_vars']], force_json=False) initial = '\n'.join(( '# Specify extra variables (if any) here as YAML.', '# Lines beginning with "#" denote comments.', initial, )) extra_vars = click.edit(initial) or '' if extra_vars != initial: extra_vars_list = [extra_vars] # Data is starting out with JT variables, and we only want to # include extra_vars that come from the algorithm here. data.pop('extra_vars', None) # Replace/populate data fields if prompted. modified = set() for resource in PROMPT_LIST: if data.pop('ask_' + resource + '_on_launch', False) \ and not no_input or use_job_endpoint: resource_object = kwargs.get(resource, None) if type(resource_object) == types.Related: resource_class = get_resource(resource) resource_object = resource_class.get(resource).\ pop('id', None) if resource_object is None: if not use_job_endpoint: debug.log( '{0} is asked at launch but not provided'.format( resource), header='warning') elif resource != 'tags': data[resource] = resource_object modified.add(resource) # Dump extra_vars into JSON string for launching job if len(extra_vars_list) > 0: data['extra_vars'] = parser.process_extra_vars(extra_vars_list, force_json=True) # In Tower 2.1 and later, we create the new job with # /job_templates/N/launch/; in Tower 2.0 and before, there is a two # step process of posting to /jobs/ and then /jobs/N/start/. supports_job_template_launch = False if 'launch' in jt['related']: supports_job_template_launch = True # Create the new job in Ansible Tower. start_data = {} if supports_job_template_launch and not use_job_endpoint: endpoint = '/job_templates/%d/launch/' % jt['id'] if 'extra_vars' in data and len(data['extra_vars']) > 0: start_data['extra_vars'] = data['extra_vars'] if tags: start_data['job_tags'] = data['job_tags'] for resource in PROMPT_LIST: if resource in modified: start_data[resource] = data[resource] else: debug.log('Creating the job.', header='details') job = client.post('/jobs/', data=data).json() job_id = job['id'] endpoint = '/jobs/%d/start/' % job_id # There's a non-trivial chance that we are going to need some # additional information to start the job; in particular, many jobs # rely on passwords entered at run-time. # # If there are any such passwords on this job, ask for them now. debug.log('Asking for information necessary to start the job.', header='details') job_start_info = client.get(endpoint).json() for password in job_start_info.get('passwords_needed_to_start', []): start_data[password] = getpass('Password for %s: ' % password) # Actually start the job. debug.log('Launching the job.', header='details') self._pop_none(kwargs) kwargs.update(start_data) job_started = client.post(endpoint, data=kwargs) # If this used the /job_template/N/launch/ route, get the job # ID from the result. if supports_job_template_launch and not use_job_endpoint: job_id = job_started.json()['job'] # Get some information about the running job to print result = self.status(pk=job_id, detail=True) result['changed'] = True # If we were told to monitor the job once it started, then call # monitor from here. if monitor: return self.monitor(job_id, timeout=timeout) return result
def launch( self, job_template=None, tags=None, monitor=False, timeout=None, no_input=True, extra_vars=None, **kwargs ): """Launch a new job based on a job template. Creates a new job in Ansible Tower, immediately starts it, and returns back an ID in order for its status to be monitored. """ # Get the job template from Ansible Tower. # This is used as the baseline for starting the job. jt_resource = get_resource("job_template") jt = jt_resource.get(job_template) # Update the job data by adding an automatically-generated job name, # and removing the ID. data = copy(jt) data["job_template"] = data.pop("id") data["name"] = "%s [invoked via. Tower CLI]" % data["name"] if tags: data["job_tags"] = tags # Initialize an extra_vars list that starts with the job template # preferences first, if they exist extra_vars_list = [] if "extra_vars" in data and len(data["extra_vars"]) > 0: # But only do this for versions before 2.3 debug.log("Getting version of Tower.", header="details") r = client.get("/config/") if LooseVersion(r.json()["version"]) < LooseVersion("2.4"): extra_vars_list = [data["extra_vars"]] # Add the runtime extra_vars to this list if extra_vars: extra_vars_list += list(extra_vars) # accept tuples # If the job template requires prompting for extra variables, # do so (unless --no-input is set). if data.pop("ask_variables_on_launch", False) and not no_input and not extra_vars: # If JT extra_vars are JSON, echo them to user as YAML initial = parser.process_extra_vars([data["extra_vars"]], force_json=False) initial = "\n".join( ( "# Specify extra variables (if any) here as YAML.", '# Lines beginning with "#" denote comments.', initial, ) ) extra_vars = click.edit(initial) or "" if extra_vars != initial: extra_vars_list = [extra_vars] # Data is starting out with JT variables, and we only want to # include extra_vars that come from the algorithm here. data.pop("extra_vars", None) # Dump extra_vars into JSON string for launching job if len(extra_vars_list) > 0: data["extra_vars"] = parser.process_extra_vars(extra_vars_list, force_json=True) # In Tower 2.1 and later, we create the new job with # /job_templates/N/launch/; in Tower 2.0 and before, there is a two # step process of posting to /jobs/ and then /jobs/N/start/. supports_job_template_launch = False if "launch" in jt["related"]: supports_job_template_launch = True # Create the new job in Ansible Tower. start_data = {} if supports_job_template_launch: endpoint = "/job_templates/%d/launch/" % jt["id"] if "extra_vars" in data and len(data["extra_vars"]) > 0: start_data["extra_vars"] = data["extra_vars"] if tags: start_data["job_tags"] = data["job_tags"] else: debug.log("Creating the job.", header="details") job = client.post("/jobs/", data=data).json() job_id = job["id"] endpoint = "/jobs/%d/start/" % job_id # There's a non-trivial chance that we are going to need some # additional information to start the job; in particular, many jobs # rely on passwords entered at run-time. # # If there are any such passwords on this job, ask for them now. debug.log("Asking for information necessary to start the job.", header="details") job_start_info = client.get(endpoint).json() for password in job_start_info.get("passwords_needed_to_start", []): start_data[password] = getpass("Password for %s: " % password) # Actually start the job. debug.log("Launching the job.", header="details") self._pop_none(kwargs) kwargs.update(start_data) job_started = client.post(endpoint, data=kwargs) # If this used the /job_template/N/launch/ route, get the job # ID from the result. if supports_job_template_launch: job_id = job_started.json()["job"] # Get some information about the running job to print result = self.status(pk=job_id, detail=True) result["changed"] = True # If we were told to monitor the job once it started, then call # monitor from here. if monitor: return self.monitor(job_id, timeout=timeout) return result
def launch(self, job_template=None, tags=None, monitor=False, timeout=None, no_input=True, extra_vars=None, **kwargs): """Launch a new job based on a job template. Creates a new job in Ansible Tower, immediately starts it, and returns back an ID in order for its status to be monitored. """ # Get the job template from Ansible Tower. # This is used as the baseline for starting the job. jt_resource = get_resource('job_template') jt = jt_resource.get(job_template) # Update the job data by adding an automatically-generated job name, # and removing the ID. data = copy(jt) data['job_template'] = data.pop('id') data['name'] = '%s [invoked via. Tower CLI]' % data['name'] if tags: data['job_tags'] = tags # Initialize an extra_vars list that starts with the job template # preferences first, if they exist extra_vars_list = [] if 'extra_vars' in data and len(data['extra_vars']) > 0: # But only do this for versions before 2.3 r = client.get('/config/') if LooseVersion(r.json()['version']) < LooseVersion('2.4'): extra_vars_list = [data['extra_vars']] # Add the runtime extra_vars to this list if extra_vars: extra_vars_list += extra_vars # Call parser utility to process extra_vars, if any are present if len(extra_vars_list) > 0: data['extra_vars'] = parser. \ extra_vars_loader_wrapper(extra_vars_list) # If the job template requires prompting for extra variables, # do so (unless --no-input is set). if data.pop('ask_variables_on_launch', False) and not no_input \ and not extra_vars: initial = data['extra_vars'] initial = '\n'.join(( '# Specify extra variables (if any) here.', '# Lines beginning with "#" are ignored.', initial, )) extra_vars = click.edit(initial) or '' extra_vars = '\n'.join([i for i in extra_vars.split('\n') if not i.startswith('#')]) data['extra_vars'] = extra_vars # In Tower 2.1 and later, we create the new job with # /job_templates/N/launch/; in Tower 2.0 and before, there is a two # step process of posting to /jobs/ and then /jobs/N/start/. supports_job_template_launch = False if 'launch' in jt['related']: supports_job_template_launch = True # Create the new job in Ansible Tower. start_data = {} if supports_job_template_launch: endpoint = '/job_templates/%d/launch/' % jt['id'] if 'extra_vars' in data and len(data['extra_vars']) > 0: start_data['extra_vars'] = data['extra_vars'] if tags: start_data['job_tags'] = data['job_tags'] else: debug.log('Creating the job.', header='details') job = client.post('/jobs/', data=data).json() job_id = job['id'] endpoint = '/jobs/%d/start/' % job_id # There's a non-trivial chance that we are going to need some # additional information to start the job; in particular, many jobs # rely on passwords entered at run-time. # # If there are any such passwords on this job, ask for them now. debug.log('Asking for information necessary to start the job.', header='details') job_start_info = client.get(endpoint).json() for password in job_start_info.get('passwords_needed_to_start', []): start_data[password] = getpass('Password for %s: ' % password) # Actually start the job. debug.log('Launching the job.', header='details') self._pop_none(kwargs) kwargs.update(start_data) job_started = client.post(endpoint, data=kwargs) # If this used the /job_template/N/launch/ route, get the job # ID from the result. if supports_job_template_launch: job_id = job_started.json()['job'] # Get some information about the running job to print result = self.status(pk=job_id, detail=True) result['changed'] = True # If we were told to monitor the job once it started, then call # monitor from here. if monitor: return self.monitor(job_id, timeout=timeout) return result
def launch(self, job_template=None, monitor=False, timeout=None, no_input=True, extra_vars=None, **kwargs): """Launch a new job based on a job template. Creates a new job in Ansible Tower, immediately starts it, and returns back an ID in order for its status to be monitored. """ # Get the job template from Ansible Tower. # This is used as the baseline for starting the job. tags = kwargs.get('tags', None) use_job_endpoint = kwargs.pop('use_job_endpoint', False) jt_resource = get_resource('job_template') jt = jt_resource.get(job_template) # Update the job data by adding an automatically-generated job name, # and removing the ID. data = copy(jt) data['job_template'] = data.pop('id') data['name'] = '%s [invoked via. Tower CLI]' % data['name'] if tags: data['job_tags'] = tags # Initialize an extra_vars list that starts with the job template # preferences first, if they exist extra_vars_list = [] if 'extra_vars' in data and len(data['extra_vars']) > 0: # But only do this for versions before 2.3 debug.log('Getting version of Tower.', header='details') r = client.get('/config/') if LooseVersion(r.json()['version']) < LooseVersion('2.4'): extra_vars_list = [data['extra_vars']] # Add the runtime extra_vars to this list if extra_vars: extra_vars_list += list(extra_vars) # accept tuples # If the job template requires prompting for extra variables, # do so (unless --no-input is set). if data.pop('ask_variables_on_launch', False) and not no_input \ and not extra_vars: # If JT extra_vars are JSON, echo them to user as YAML initial = parser.process_extra_vars( [data['extra_vars']], force_json=False ) initial = '\n'.join(( '# Specify extra variables (if any) here as YAML.', '# Lines beginning with "#" denote comments.', initial, )) extra_vars = click.edit(initial) or '' if extra_vars != initial: extra_vars_list = [extra_vars] # Data is starting out with JT variables, and we only want to # include extra_vars that come from the algorithm here. data.pop('extra_vars', None) # Replace/populate data fields if prompted. modified = set() for resource in PROMPT_LIST: if data.pop('ask_' + resource + '_on_launch', False) \ and not no_input or use_job_endpoint: resource_object = kwargs.get(resource, None) if type(resource_object) == types.Related: resource_class = get_resource(resource) resource_object = resource_class.get(resource).\ pop('id', None) if resource_object is None: if not use_job_endpoint: debug.log('{0} is asked at launch but not provided'. format(resource), header='warning') elif resource != 'tags': data[resource] = resource_object modified.add(resource) # Dump extra_vars into JSON string for launching job if len(extra_vars_list) > 0: data['extra_vars'] = parser.process_extra_vars( extra_vars_list, force_json=True ) # In Tower 2.1 and later, we create the new job with # /job_templates/N/launch/; in Tower 2.0 and before, there is a two # step process of posting to /jobs/ and then /jobs/N/start/. supports_job_template_launch = False if 'launch' in jt['related']: supports_job_template_launch = True # Create the new job in Ansible Tower. start_data = {} if supports_job_template_launch and not use_job_endpoint: endpoint = '/job_templates/%d/launch/' % jt['id'] if 'extra_vars' in data and len(data['extra_vars']) > 0: start_data['extra_vars'] = data['extra_vars'] if tags: start_data['job_tags'] = data['job_tags'] for resource in PROMPT_LIST: if resource in modified: start_data[resource] = data[resource] else: debug.log('Creating the job.', header='details') job = client.post('/jobs/', data=data).json() job_id = job['id'] endpoint = '/jobs/%d/start/' % job_id # There's a non-trivial chance that we are going to need some # additional information to start the job; in particular, many jobs # rely on passwords entered at run-time. # # If there are any such passwords on this job, ask for them now. debug.log('Asking for information necessary to start the job.', header='details') job_start_info = client.get(endpoint).json() for password in job_start_info.get('passwords_needed_to_start', []): start_data[password] = getpass('Password for %s: ' % password) # Actually start the job. debug.log('Launching the job.', header='details') self._pop_none(kwargs) kwargs.update(start_data) job_started = client.post(endpoint, data=kwargs) # If this used the /job_template/N/launch/ route, get the job # ID from the result. if supports_job_template_launch and not use_job_endpoint: job_id = job_started.json()['job'] # If returning json indicates any ignored fields, display it in # verbose mode. ignored_fields = job_started.json().get('ignored_fields', {}) has_ignored_fields = False for key, value in ignored_fields.items(): if value and value != '{}': if not has_ignored_fields: debug.log('List of ignored fields on the server side:', header='detail') has_ignored_fields = True debug.log('{0}: {1}'.format(key, value)) # Get some information about the running job to print result = self.status(pk=job_id, detail=True) result['changed'] = True # If we were told to monitor the job once it started, then call # monitor from here. if monitor: return self.monitor(job_id, timeout=timeout) return result
def launch(self, job_template, monitor=False, timeout=None, no_input=True, extra_vars=None): """Launch a new job based on a job template. Creates a new job in Ansible Tower, immediately stats it, and returns back an ID in order for its status to be monitored. """ # Get the job template from Ansible Tower. # This is used as the baseline for starting the job. jt_resource = get_resource('job_template') jt = jt_resource.get(job_template) # Update the job data by adding an automatically-generated job name, # and removing the ID. data = copy(jt) data.pop('id') data['name'] = '%s [invoked via. Tower CLI]' % data['name'] # If the job template requires prompting for extra variables, # do so (unless --no-input is set). if extra_vars: data['extra_vars'] = extra_vars.read() elif data.pop('ask_variables_on_launch', False) and not no_input: initial = data['extra_vars'] initial = '\n'.join(( '# Specify extra variables (if any) here.', '# Lines beginning with "#" are ignored.', initial, )) extra_vars = click.edit(initial) or '' extra_vars = '\n'.join([i for i in extra_vars.split('\n') if not i.startswith('#')]) data['extra_vars'] = extra_vars # Create the new job in Ansible Tower. debug.log('Creating the job.', header='details') job = client.post('/jobs/', data=data).json() # There's a non-trivial chance that we are going to need some # additional information to start the job; in particular, many jobs # rely on passwords entered at run-time. # # If there are any such passwords on this job, ask for them now. debug.log('Asking for information necessary to start the job.', header='details') job_start_info = client.get('/jobs/%d/start/' % job['id']).json() start_data = {} for password in job_start_info.get('passwords_needed_to_start', []): start_data[password] = getpass('Password for %s: ' % password) # Actually start the job. debug.log('Launching the job.', header='details') result = client.post('/jobs/%d/start/' % job['id'], start_data) # If we were told to monitor the job once it started, then call # monitor from here. if monitor: return self.monitor(job['id'], timeout=timeout) # Return the job ID. return { 'changed': True, 'id': job['id'], }
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. =====API DOCS===== Update the given project. :param pk: Primary key of the project to be updated. :type pk: int :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched project update rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the project 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 name: Name of the project to be updated if ``pk`` is not set. :type name: str :param organization: Primary key or name of the organization the project to be updated belonging to if ``pk`` is not set. :type organization: str :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.CannotStartJob: When the project cannot be updated. =====API DOCS===== """ # 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) project_update_id = result.json()['project_update'] # If we were told to monitor the project update's status, do so. if monitor: return self.monitor(project_update_id, parent_pk=pk, timeout=timeout) elif wait: return self.wait(project_update_id, parent_pk=pk, timeout=timeout) # Return the project update ID. return { 'id': project_update_id, 'changed': True, }
def launch(self, job_template=None, monitor=False, wait=False, timeout=None, no_input=True, extra_vars=None, **kwargs): """Launch a new job based on a job template. Creates a new job in Ansible Tower, immediately starts it, and returns back an ID in order for its status to be monitored. =====API DOCS===== Launch a new job based on a job template. :param job_template: Primary key or name of the job template to launch new job. :type job_template: str :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched job 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 no_input: Flag that if set, suppress any requests for input. :type no_input: bool :param extra_vars: yaml formatted texts that contains extra variables to pass on. :type extra_vars: array of strings :param diff_mode: Specify diff mode for job template to run. :type diff_mode: bool :param limit: Specify host limit for job template to run. :type limit: str :param tags: Specify tagged actions in the playbook to run. :type tags: str :param skip_tags: Specify tagged actions in the playbook to omit. :type skip_tags: str :param job_type: Specify job type for job template to run. :type job_type: str :param verbosity: Specify verbosity of the playbook run. :type verbosity: int :param inventory: Specify machine credential for job template to run. :type inventory: str :param credential: Specify machine credential for job template to run. :type credential: str :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait`` call if ``wait`` flag is on; Result of subsequent ``status`` call if none of the two flags are on. :rtype: dict =====API DOCS===== """ # Get the job template from Ansible Tower. # This is used as the baseline for starting the job. tags = kwargs.get('tags', None) jt_resource = get_resource('job_template') jt = jt_resource.get(job_template) # Update the job data by adding an automatically-generated job name, # and removing the ID. data = {} if tags: data['job_tags'] = tags # Initialize an extra_vars list that starts with the job template # preferences first, if they exist extra_vars_list = [] if 'extra_vars' in data and len(data['extra_vars']) > 0: # But only do this for versions before 2.3 debug.log('Getting version of Tower.', header='details') r = client.get('/config/') if LooseVersion(r.json()['version']) < LooseVersion('2.4'): extra_vars_list = [data['extra_vars']] # Add the runtime extra_vars to this list if extra_vars: extra_vars_list += list(extra_vars) # accept tuples # If the job template requires prompting for extra variables, # do so (unless --no-input is set). if jt.get('ask_variables_on_launch', False) and not no_input \ and not extra_vars: # If JT extra_vars are JSON, echo them to user as YAML initial = parser.process_extra_vars([jt['extra_vars']], force_json=False) initial = '\n'.join(( '# Specify extra variables (if any) here as YAML.', '# Lines beginning with "#" denote comments.', initial, )) extra_vars = click.edit(initial) or '' if extra_vars != initial: extra_vars_list = [extra_vars] # Data is starting out with JT variables, and we only want to # include extra_vars that come from the algorithm here. data.pop('extra_vars', None) # Replace/populate data fields if prompted. modified = set() for resource in PROMPT_LIST: if jt.pop('ask_' + resource + '_on_launch', False) and not no_input: resource_object = kwargs.get(resource, None) if type(resource_object) == types.Related: resource_class = get_resource(resource) resource_object = resource_class.get(resource).pop( 'id', None) if resource_object is None: debug.log('{0} is asked at launch but not provided'.format( resource), header='warning') elif resource != 'tags': data[resource] = resource_object modified.add(resource) # Dump extra_vars into JSON string for launching job if len(extra_vars_list) > 0: data['extra_vars'] = parser.process_extra_vars(extra_vars_list, force_json=True) # Create the new job in Ansible Tower. start_data = {} endpoint = '/job_templates/%d/launch/' % jt['id'] if 'extra_vars' in data and len(data['extra_vars']) > 0: start_data['extra_vars'] = data['extra_vars'] if tags: start_data['job_tags'] = data['job_tags'] for resource in PROMPT_LIST: if resource in modified: start_data[resource] = data[resource] # There's a non-trivial chance that we are going to need some # additional information to start the job; in particular, many jobs # rely on passwords entered at run-time. # # If there are any such passwords on this job, ask for them now. debug.log('Asking for information necessary to start the job.', header='details') job_start_info = client.get(endpoint).json() for password in job_start_info.get('passwords_needed_to_start', []): start_data[password] = getpass('Password for %s: ' % password) # Actually start the job. debug.log('Launching the job.', header='details') self._pop_none(kwargs) kwargs.update(start_data) job_started = client.post(endpoint, data=kwargs) # Get the job ID from the result. job_id = job_started.json()['id'] # If returning json indicates any ignored fields, display it in # verbose mode. if job_started.text == '': ignored_fields = {} else: ignored_fields = job_started.json().get('ignored_fields', {}) has_ignored_fields = False for key, value in ignored_fields.items(): if value and value != '{}': if not has_ignored_fields: debug.log('List of ignored fields on the server side:', header='detail') has_ignored_fields = True debug.log('{0}: {1}'.format(key, value)) # Get some information about the running job to print result = self.status(pk=job_id, detail=True) result['changed'] = True # If we were told to monitor the job once it started, then call # monitor from here. if monitor: return self.monitor(job_id, timeout=timeout) elif wait: return self.wait(job_id, timeout=timeout) return result
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'}
#job_data['extra_vars'] = json_extra_vars # TODO: nagios is breaking quotes, so don't try to interpret JSON for now if args.extra_vars: job_data['extra_vars'] = args.extra_vars else: job_data['extra_vars'] = "{nagios_no_extra_var: true }" except ValueError: error("The extra_vars parameter is not valid JSON.") if(job_check.json()['ask_limit_on_launch'] and not args.limit): log_run("ERROR: job requires --limit") error("The job requires a list of hosts to limit the run.") else: job_data['limit'] = args.limit try: job_started = client.post('/job_templates/%s/launch/' % template_number, data=job_data) #print json.dumps(job_started.json(), indent=2) if(job_started.json()['id'] and job_started.json()['job']): job_number = job_started.json()['id'] job_status = "STARTED" log_run("OK: job started") info("Tower job %s started." % job_number) else: job_status = "FAILED" log_run("ERROR: API call to start job failed") error("Could not start tower job: %s" % job_started['result_stdout']) except exceptions.BadRequest: log_run("ERROR: bad request on API call -- URI[/job_templates/%s/launch/] DATA[%s]" % (template_number, job_data)) error("There was a bad request on the API call -- URI[/job_templates/%s/launch/] DATA[%s]" % (template_number, job_data))
def launch(self, job_template, tags=None, monitor=False, timeout=None, no_input=True, extra_vars=None): """Launch a new job based on a job template. Creates a new job in Ansible Tower, immediately stats it, and returns back an ID in order for its status to be monitored. """ # Get the job template from Ansible Tower. # This is used as the baseline for starting the job. jt_resource = get_resource('job_template') jt = jt_resource.get(job_template) # Update the job data by adding an automatically-generated job name, # and removing the ID. data = copy(jt) data['job_template'] = data.pop('id') data['name'] = '%s [invoked via. Tower CLI]' % data['name'] if tags: data['job_tags'] = tags # If the job template requires prompting for extra variables, # do so (unless --no-input is set). if extra_vars: if hasattr(extra_vars, 'read'): extra_vars = extra_vars.read() data['extra_vars'] = extra_vars elif data.pop('ask_variables_on_launch', False) and not no_input: initial = data['extra_vars'] initial = '\n'.join(( '# Specify extra variables (if any) here.', '# Lines beginning with "#" are ignored.', initial, )) extra_vars = click.edit(initial) or '' extra_vars = '\n'.join( [i for i in extra_vars.split('\n') if not i.startswith('#')]) data['extra_vars'] = extra_vars # In Tower 2.1 and later, we create the new job with # /job_templates/N/launch/; in Tower 2.0 and before, there is a two # step process of posting to /jobs/ and then /jobs/N/start/. supports_job_template_launch = False if 'launch' in jt['related']: supports_job_template_launch = True # Create the new job in Ansible Tower. start_data = {} if supports_job_template_launch: endpoint = '/job_templates/%d/launch/' % jt['id'] if 'extra_vars' in data: start_data['extra_vars'] = data['extra_vars'] if tags: start_data['job_tags'] = data['job_tags'] else: debug.log('Creating the job.', header='details') job = client.post('/jobs/', data=data).json() job_id = job['id'] endpoint = '/jobs/%d/start/' % job_id # There's a non-trivial chance that we are going to need some # additional information to start the job; in particular, many jobs # rely on passwords entered at run-time. # # If there are any such passwords on this job, ask for them now. debug.log('Asking for information necessary to start the job.', header='details') job_start_info = client.get(endpoint).json() for password in job_start_info.get('passwords_needed_to_start', []): start_data[password] = getpass('Password for %s: ' % password) # Actually start the job. debug.log('Launching the job.', header='details') result = client.post(endpoint, start_data) # If this used the /job_template/N/launch/ route, get the job # ID from the result. if supports_job_template_launch: job_id = result.json()['job'] # If we were told to monitor the job once it started, then call # monitor from here. if monitor: return self.monitor(job_id, timeout=timeout) # Return the job ID. return { 'changed': True, 'id': job_id, }