class Resource(models.ExeResource): """A resource for project updates. """ cli_help = 'Launch or monitor project updates.' endpoint = '/project_updates/' project = models.Field(key='-P', type=types.Related('project'), required=True, display=True) name = models.Field(required=False, display=True, read_only=True) launch_type = models.Field(type=click.Choice(LAUNCH_TYPE_CHOICES), read_only=True, display=False) status = models.Field(type=click.Choice(STATUS_CHOICES), read_only=True) job_type = models.Field(type=click.Choice(['run', 'check']), read_only=True) job_explanation = models.Field(required=False, display=False, read_only=True) created = models.Field(required=False, display=True, read_only=True) elapsed = models.Field(required=False, display=True, read_only=True, type=float) scm_type = models.Field(type=types.MappedChoice([ ('', 'manual'), ('git', 'git'), ('hg', 'hg'), ('svn', 'svn'), ('insights', 'insights'), ]), display=False)
def test_convert(self): """Establish that the convert method converts from the value provided to the user to the internal value, and calls the superclass method. """ mc = types.MappedChoice({'foo': 'bar', 'spam': 'eggs'}) self.assertEqual(mc.convert('bar', 'myopt', None), 'foo') self.assertEqual(mc.convert('eggs', 'myopt', None), 'spam')
class Resource(models.ExeResource): """A resource for ad hoc commands.""" cli_help = 'Launch commands based on playbook given at runtime.' endpoint = '/ad_hoc_commands/' # Parameters similar to job job_explanation = models.Field(required=False, display=False) created = models.Field(required=False, display=True) status = models.Field(required=False, display=True) elapsed = models.Field(required=False, display=True, type=float) # Parameters similar to job_template job_type = models.Field( default='run', display=False, show_default=True, type=click.Choice(['run', 'check']), ) inventory = models.Field(type=types.Related('inventory')) limit = models.Field(required=False, display=False) credential = models.Field(display=False, type=types.Related('credential')) module_name = models.Field(required=False, display=True, default="command", show_default=True) module_args = models.Field(required=False, display=False) forks = models.Field(type=int, required=False, display=False) verbosity = models.Field( display=False, type=types.MappedChoice([ (0, 'default'), (1, 'verbose'), (2, 'more_verbose'), (3, 'debug'), (4, 'connection'), (5, 'winrm'), ]), required=False, ) become_enabled = models.Field(type=bool, required=False, display=False) diff_mode = models.Field(type=bool, required=False, display=False) @resources.command( use_fields_as_options=( 'job_explanation', 'job_type', 'inventory', 'credential', 'module_name', 'module_args', 'forks', 'limit', 'verbosity', 'become_enabled', 'diff_mode', ) ) @click.option('--monitor', is_flag=True, default=False, help='If sent, immediately calls `monitor` on the newly ' 'launched command rather than exiting with a success.') @click.option('--wait', is_flag=True, default=False, help='Monitor the status of the job, but do not print ' 'while job is in progress.') @click.option('--timeout', required=False, type=int, help='If provided with --monitor, this attempt' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') 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
class Resource(models.SurveyResource): """A resource for job templates.""" cli_help = 'Manage job templates.' endpoint = '/job_templates/' name = models.Field(unique=True) description = models.Field(required=False, display=False) job_type = models.Field( required=False, display=False, type=click.Choice(['run', 'check']), ) inventory = models.Field(type=types.Related('inventory'), required=False) project = models.Field(type=types.Related('project')) playbook = models.Field() credential = models.Field(display=False, required=False, type=types.Related('credential')) vault_credential = models.Field(type=types.Related('credential'), required=False, display=False) forks = models.Field(type=int, required=False, display=False) limit = models.Field(required=False, display=False) verbosity = models.Field( display=False, type=types.MappedChoice([ (0, 'default'), (1, 'verbose'), (2, 'more_verbose'), (3, 'debug'), (4, 'connection'), (5, 'winrm'), ]), required=False, ) extra_vars = models.Field( type=types.Variables(), required=False, display=False, multiple=True, help_text='Extra variables used by Ansible in YAML or key=value ' 'format. Use @ to get YAML from a file.') job_tags = models.Field(required=False, display=False) force_handlers = models.Field(type=bool, required=False, display=False) skip_tags = models.Field(required=False, display=False) start_at_task = models.Field(required=False, display=False) timeout = models.Field( type=int, required=False, display=False, help_text= 'The amount of time (in seconds) to run before the task is canceled.') use_fact_cache = models.Field( type=bool, required=False, display=False, help_text='If enabled, Tower will act as an Ansible Fact Cache Plugin;' ' persisting facts at the end of a playbook run to the database' ' and caching facts for use by Ansible.') host_config_key = models.Field( required=False, display=False, help_text='Allow Provisioning Callbacks using this host config key') ask_diff_mode_on_launch = models.Field( type=bool, required=False, display=False, help_text='Ask diff mode on launch.') ask_variables_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for extra_vars on launch.') ask_limit_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for host limits on launch.') ask_tags_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job tags on launch.') ask_skip_tags_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for tags to skip on launch.') ask_job_type_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') ask_verbosity_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for verbosity on launch.') ask_inventory_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for inventory on launch.') ask_credential_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for machine credential on launch.') survey_enabled = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') become_enabled = models.Field(type=bool, required=False, display=False) diff_mode = models.Field( type=bool, required=False, display=False, help_text='If enabled, textual changes made to any templated files on' ' the host are shown in the standard output.') allow_simultaneous = models.Field(type=bool, required=False, display=False) survey_spec = models.Field( type=types.Variables(), required=False, display=False, help_text='On write commands, perform extra POST to the ' 'survey_spec endpoint.') def write(self, *args, **kwargs): # Provide a default value for job_type, but only in creation of JT if (kwargs.get('create_on_missing', False) and (not kwargs.get('job_type', None))): kwargs['job_type'] = 'run' return super(Resource, self).write(*args, **kwargs) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--label', type=types.Related('label')) def associate_label(self, job_template, label): """Associate an label with this job template. =====API DOCS===== Associate an label with this job template. :param job_template: The job template to associate to. :type job_template: str :param label: The label to be associated. :type label: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('labels', job_template, label) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--label', type=types.Related('label')) def disassociate_label(self, job_template, label): """Disassociate an label from this job template. =====API DOCS===== Disassociate an label from this job template. :param job_template: The job template to disassociate from. :type job_template: str :param label: The label to be disassociated. :type label: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('labels', job_template, label) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--credential', type=types.Related('credential')) def associate_credential(self, job_template, credential): """Associate a credential with this job template. =====API DOCS===== Associate a credential with this job template. :param job_template: The job template to associate to. :type job_template: str :param credential: The credential to be associated. :type credential: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('extra_credentials', job_template, credential) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--credential', type=types.Related('credential')) def disassociate_credential(self, job_template, credential): """Disassociate a credential with this job template. =====API DOCS===== Disassociate a credential from this job template. :param job_template: The job template to disassociate fom. :type job_template: str :param credential: The credential to be disassociated. :type credential: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('extra_credentials', job_template, credential) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def associate_notification_template(self, job_template, notification_template, status): """Associate a notification template from this job template. =====API DOCS===== Associate a notification template from this job template. :param job_template: The job template to associate to. :type job_template: str :param notification_template: The notification template to be associated. :type notification_template: str :param status: type of notification this notification template should be associated to. :type status: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('notification_templates_%s' % status, job_template, notification_template) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def disassociate_notification_template(self, job_template, notification_template, status): """Disassociate a notification template from this job template. =====API DOCS===== Disassociate a notification template from this job template. :param job_template: The job template to disassociate from. :type job_template: str :param notification_template: The notification template to be disassociated. :type notification_template: str :param status: type of notification this notification template should be disassociated from. :type status: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('notification_templates_%s' % status, job_template, notification_template) @resources.command(use_fields_as_options=('extra_vars')) @click.option('--host-config-key', help='Job-template-specific string used to authenticate ' 'host during provisioning callback.') 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} @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def associate_ig(self, job_template, instance_group): """Associate an instance group with this job_template. The instance group will be used to run jobs within the job_template. =====API DOCS===== Associate an instance group with this job_template. :param job_template: Primary key or name of the job_template to associate to. :type job_template: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('instance_groups', job_template, instance_group) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def disassociate_ig(self, job_template, instance_group): """Disassociate an instance group from this job_template. =====API DOCS===== Disassociate an instance group with this job_template. :param job_template: Primary key or name of the job_template to associate to. :type job_template: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('instance_groups', job_template, instance_group)
class Resource(models.Resource): """A resource for schedules.""" cli_help = 'Manage schedules within Ansible Tower.' endpoint = '/schedules/' # General fields. name = models.Field(unique=True) description = models.Field(required=False, display=False) # Unified jt fields. note these fields will only be used during creation. # Plus, one and only one field should be provided. job_template = models.Field(type=types.Related('job_template'), required=False, display=False) inventory_source = models.Field(type=types.Related('inventory_source'), required=False, display=False) project = models.Field(type=types.Related('project'), required=False, display=False) # Schedule-specific fields. unified_job_template = models.Field(required=False, type=int, help_text='Integer used to display' ' unified job template in result, ' 'Do not use it for create/' 'modify.') enabled = models.Field(required=False, type=click.BOOL, default=True, help_text='Whether this schedule will be used.', show_default=True) rrule = models.Field(required=False, display=False, help_text='Schedule rules specifications which is' ' less than 255 characters.') # Prompts extra_data = models.Field(type=types.Variables(), required=False, display=False, help_text='Extra data for ' 'schedule rules in the form of a .json file.') inventory = models.Field(type=types.Related('inventory'), required=False, display=False) credentials = models.ManyToManyField('credential') job_type = models.Field(required=False, display=False) job_tags = models.Field(required=False, display=False) skip_tags = models.Field(required=False, display=False) limit = models.Field(required=False, display=False) diff_mode = models.Field(type=bool, required=False, display=False) verbosity = models.Field( display=False, type=types.MappedChoice([ (0, 'default'), (1, 'verbose'), (2, 'more_verbose'), (3, 'debug'), (4, 'connection'), (5, 'winrm'), ]), required=False, ) def _get_patch_url(self, url, pk): urlTokens = url.split('/') if len(urlTokens) > 3: # reconstruct url to prevent a rare corner case where resources # cannot be constructed independently. Open to modification if # API convention changes. url = '/'.join(urlTokens[:1] + urlTokens[-2:]) return super(Resource, self)._get_patch_url(url, pk)
class Resource(models.Resource): """A resource for workflow nodes.""" cli_help = 'Manage nodes inside of a workflow job template.' endpoint = '/workflow_job_template_nodes/' identity = ('id', ) workflow_job_template = models.Field(key='-W', type=types.Related('workflow')) unified_job_template = models.Field(required=False) # Prompts extra_data = models.Field(type=types.Variables(), required=False, display=False, help_text='Extra data for ' 'schedule rules in the form of a .json file.') inventory = models.Field(type=types.Related('inventory'), required=False, display=False) credential = models.Field(type=types.Related('credential'), required=False, display=False) credentials = models.ManyToManyField('credential') job_type = models.Field(required=False, display=False) job_tags = models.Field(required=False, display=False) skip_tags = models.Field(required=False, display=False) limit = models.Field(required=False, display=False) diff_mode = models.Field(type=bool, required=False, display=False) verbosity = models.Field( display=False, type=types.MappedChoice([ (0, 'default'), (1, 'verbose'), (2, 'more_verbose'), (3, 'debug'), (4, 'connection'), (5, 'winrm'), ]), required=False, ) def __new__(cls, *args, **kwargs): for attr in ['create', 'modify', 'list']: setattr(cls, attr, unified_job_template_options(getattr(cls, attr))) return super(Resource, cls).__new__(cls, *args, **kwargs) @staticmethod def _forward_rel_name(rel): return '{0}_nodes'.format(rel) @staticmethod def _reverse_rel_name(rel): return 'workflowjobtemplatenodes_{0}'.format(rel) def _parent_filter(self, parent, relationship, **kwargs): """ Returns filtering parameters to limit a search to the children of a particular node by a particular relationship. """ if parent is None or relationship is None: return {} parent_filter_kwargs = {} query_params = ((self._reverse_rel_name(relationship), parent), ) parent_filter_kwargs['query'] = query_params if kwargs.get('workflow_job_template', None) is None: parent_data = self.read(pk=parent)['results'][0] parent_filter_kwargs['workflow_job_template'] = parent_data[ 'workflow_job_template'] return parent_filter_kwargs @unified_job_template_options 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 _assoc_or_create(self, relationship, parent, child, **kwargs): if child is None: child_data = self._get_or_create_child(parent, relationship, **kwargs) return child_data return self._assoc(self._forward_rel_name(relationship), parent, child) @resources.command @unified_job_template_options @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node'), required=False) def associate_success_node(self, parent, child=None, **kwargs): """Add a node to run on success. =====API DOCS===== Add a node to run on success. :param parent: Primary key of parent node to associate success node to. :type parent: int :param child: Primary key of child node to be associated. :type child: int :param `**kwargs`: Fields used to create child node if ``child`` is not provided. :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc_or_create('success', parent, child, **kwargs) @resources.command(use_fields_as_options=False) @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node')) def disassociate_success_node(self, parent, child): """Remove success node. The resulatant 2 nodes will both become root nodes. =====API DOCS===== Remove success node. :param parent: Primary key of parent node to disassociate success node from. :type parent: int :param child: Primary key of child node to be disassociated. :type child: int :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc(self._forward_rel_name('success'), parent, child) @resources.command @unified_job_template_options @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node'), required=False) def associate_failure_node(self, parent, child=None, **kwargs): """Add a node to run on failure. =====API DOCS===== Add a node to run on failure. :param parent: Primary key of parent node to associate failure node to. :type parent: int :param child: Primary key of child node to be associated. :type child: int :param `**kwargs`: Fields used to create child node if ``child`` is not provided. :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc_or_create('failure', parent, child, **kwargs) @resources.command(use_fields_as_options=False) @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node')) def disassociate_failure_node(self, parent, child): """Remove a failure node link. The resulatant 2 nodes will both become root nodes. =====API DOCS===== Remove a failure node link. :param parent: Primary key of parent node to disassociate failure node from. :type parent: int :param child: Primary key of child node to be disassociated. :type child: int :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc(self._forward_rel_name('failure'), parent, child) @resources.command @unified_job_template_options @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node'), required=False) def associate_always_node(self, parent, child=None, **kwargs): """Add a node to always run after the parent is finished. =====API DOCS===== Add a node to always run after the parent is finished. :param parent: Primary key of parent node to associate always node to. :type parent: int :param child: Primary key of child node to be associated. :type child: int :param `**kwargs`: Fields used to create child node if ``child`` is not provided. :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc_or_create('always', parent, child, **kwargs) @resources.command(use_fields_as_options=False) @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node')) def disassociate_always_node(self, parent, child): """Remove an always node link. The resultant 2 nodes will both become root nodes. =====API DOCS===== Remove an always node link. :param parent: Primary key of parent node to disassociate always node from. :type parent: int :param child: Primary key of child node to be disassociated. :type child: int :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc(self._forward_rel_name('always'), parent, child)
class Resource(models.Resource, models.MonitorableResource): """A resource for projects.""" cli_help = 'Manage projects within Ansible Tower.' endpoint = '/projects/' unified_job_type = '/project_updates/' dependencies = ['organization', 'credential'] related = ['notification_templates', 'schedules'] name = models.Field(unique=True) description = models.Field(required=False, display=False) organization = models.Field(type=types.Related('organization'), display=False, required=False) scm_type = models.Field(type=types.MappedChoice([ ('', 'manual'), ('git', 'git'), ('hg', 'hg'), ('svn', 'svn'), ('insights', 'insights'), ]), required=False) scm_url = models.Field(required=False) local_path = models.Field( help_text='For manual projects, the server playbook directory name.', required=False) scm_branch = models.Field(required=False, display=False) scm_credential = models.Field( 'credential', display=False, required=False, type=types.Related('credential'), ) scm_clean = models.Field(type=bool, required=False, display=False) scm_delete_on_update = models.Field(type=bool, required=False, display=False) scm_update_on_launch = models.Field(type=bool, required=False, display=False) scm_update_cache_timeout = models.Field(type=int, required=False, display=False) job_timeout = models.Field(type=int, required=False, display=False, help_text='The timeout field (in seconds).') custom_virtualenv = models.Field(required=False, display=False) @resources.command @click.option('--monitor', is_flag=True, default=False, help='If sent, immediately calls `project monitor` on the ' 'project rather than exiting with a success.' 'It polls for status until the SCM is updated.') @click.option('--wait', is_flag=True, default=False, help='Polls server for status, exists when finished.') @click.option('--timeout', required=False, type=int, help='If provided with --monitor, the SCM update' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') def create(self, organization=None, monitor=False, wait=False, timeout=None, fail_on_found=False, force_on_exists=False, **kwargs): """Create a new item of resource, with or w/o org. This would be a shared class with user, but it needs the ability to monitor if the flag is set. =====API DOCS===== Create a project and, if related flags are set, monitor or wait the triggered initial project update. :param monitor: Flag that if set, immediately calls ``monitor`` on the newly triggered project update rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the triggered 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: bool :param fail_on_found: Flag that if set, the operation fails if an object matching the unique criteria already exists. :type fail_on_found: bool :param force_on_exists: Flag that if set, then if a match is found on unique fields, other fields will be updated to the provided values.; If unset, a match causes the request to be a no-op. :type force_on_exists: bool :param `**kwargs`: Keyword arguments which, all together, will be used as POST body to create the resource object. :returns: A dictionary combining the JSON output of the created resource, as well as two extra fields: "changed", a flag indicating if the resource is created successfully; "id", an integer which is the primary key of the created object. :rtype: dict =====API DOCS===== """ if 'job_timeout' in kwargs and 'timeout' not in kwargs: kwargs['timeout'] = kwargs.pop('job_timeout') post_associate = False if organization: # Processing the organization flag depends on version debug.log('Checking Organization Relationship.', header='details') r = client.options('/projects/') if 'organization' in r.json().get('actions', {}).get('POST', {}): kwargs['organization'] = organization else: post_associate = True # First, run the create method, ignoring the organization given answer = super(Resource, self).write(create_on_missing=True, fail_on_found=fail_on_found, force_on_exists=force_on_exists, **kwargs) project_id = answer['id'] # If an organization is given, associate it here if post_associate: # Get the organization from Tower, will lookup name if needed org_resource = get_resource('organization') org_data = org_resource.get(organization) org_pk = org_data['id'] debug.log("associating the project with its organization", header='details', nl=1) org_resource._assoc('projects', org_pk, project_id) # if the monitor flag is set, wait for the SCM to update if monitor and answer.get('changed', False): return self.monitor(pk=None, parent_pk=project_id, timeout=timeout) elif wait and answer.get('changed', False): return self.wait(pk=None, parent_pk=project_id, timeout=timeout) return answer @resources.command( use_fields_as_options=('name', 'description', 'scm_type', 'scm_url', 'local_path', 'scm_branch', 'scm_credential', 'scm_clean', 'scm_delete_on_update', 'scm_update_on_launch', 'job_timeout', 'custom_virtualenv')) def modify(self, pk=None, create_on_missing=False, **kwargs): """Modify an already existing. To edit the project's organizations, see help for organizations. Fields in the resource's `identity` tuple can be used in lieu of a primary key for a lookup; in such a case, only other fields are written. To modify unique fields, you must use the primary key for the lookup. =====API DOCS===== Modify an already existing project. :param pk: Primary key of the resource to be modified. :type pk: int :param create_on_missing: Flag that if set, a new object is created if ``pk`` is not set and objects matching the appropriate unique criteria is not found. :type create_on_missing: bool :param `**kwargs`: Keyword arguments which, all together, will be used as PATCH body to modify the resource object. if ``pk`` is not set, key-value pairs of ``**kwargs`` which are also in resource's identity will be used to lookup existing reosource. :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===== """ # Associated with issue #52, the organization can't be modified # with the 'modify' command. This would create confusion about # whether its flag is an identifier versus a field to modify. if 'job_timeout' in kwargs and 'timeout' not in kwargs: kwargs['timeout'] = kwargs.pop('job_timeout') return super(Resource, self).write(pk, create_on_missing=create_on_missing, force_on_exists=True, **kwargs) @resources.command(use_fields_as_options=('name', 'organization')) @click.option('--monitor', is_flag=True, default=False, help='If sent, immediately calls `job monitor` on the newly ' 'launched job rather than exiting with a success.') @click.option('--wait', is_flag=True, default=False, help='Polls server for status, exists when finished.') @click.option('--timeout', required=False, type=int, help='If provided with --monitor, this command (not the job)' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') 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, } @resources.command @click.option('--detail', is_flag=True, default=False, help='Print more detail.') def status(self, pk=None, detail=False, **kwargs): """Print the status of the most recent update. =====API DOCS===== Print the status of the most recent update. :param pk: Primary key of the resource to retrieve status from. :type pk: int :param detail: Flag that if set, return the full JSON of the job resource rather than a status summary. :type detail: bool :param `**kwargs`: Keyword arguments used to look up resource object to retrieve status from if ``pk`` is not provided. :returns: full loaded JSON of the specified unified job if ``detail`` flag is on; trimed JSON containing only "elapsed", "failed" and "status" fields of the unified job if ``detail`` flag is off. :rtype: dict =====API DOCS===== """ # Obtain the most recent project update job = self.last_job_data(pk, **kwargs) # 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'], }
class Resource(models.SurveyResource): cli_help = 'Manage job templates.' endpoint = '/job_templates/' name = models.Field(unique=True) description = models.Field(required=False, display=False) job_type = models.Field( required=False, display=False, type=click.Choice(['run', 'check', 'scan']), ) inventory = models.Field(type=types.Related('inventory'), required=False) project = models.Field(type=types.Related('project')) playbook = models.Field() machine_credential = models.Field( 'credential', display=False, required=False, type=types.Related('credential'), ) cloud_credential = models.Field(type=types.Related('credential'), required=False, display=False) network_credential = models.Field(type=types.Related('credential'), required=False, display=False) forks = models.Field(type=int, required=False, display=False) limit = models.Field(required=False, display=False) verbosity = models.Field( display=False, type=types.MappedChoice([ (0, 'default'), (1, 'verbose'), (2, 'more_verbose'), (3, 'debug'), (4, 'connection'), (5, 'winrm'), ]), required=False, ) job_tags = models.Field(required=False, display=False) skip_tags = models.Field(required=False, display=False) extra_vars = models.Field( type=types.Variables(), required=False, display=False, multiple=True, help_text='Extra variables used by Ansible in YAML or key=value ' 'format. Use @ to get YAML from a file.') host_config_key = models.Field( required=False, display=False, help_text='Allow Provisioning Callbacks using this host config key') ask_variables_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for extra_vars on launch.') ask_limit_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for host limits on launch.') ask_tags_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job tags on launch.') ask_skip_tags_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for tags to skip on launch.') ask_job_type_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') ask_inventory_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for inventory on launch.') ask_credential_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for machine credential on launch.') become_enabled = models.Field(type=bool, required=False, display=False) allow_simultaneous = models.Field(type=bool, required=False, display=False) timeout = models.Field(type=int, required=False, display=False, help_text='The timeout field (in seconds).') survey_enabled = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') survey_spec = models.Field( type=types.Variables(), required=False, display=False, help_text='On write commands, perform extra POST to the ' 'survey_spec endpoint.') def write(self, *args, **kwargs): # Provide a default value for job_type, but only in creation of JT if (kwargs.get('create_on_missing', False) and (not kwargs.get('job_type', None))): kwargs['job_type'] = 'run' return super(Resource, self).write(*args, **kwargs) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--label', type=types.Related('label')) def associate_label(self, job_template, label): """Associate an label with this job template.""" return self._assoc('labels', job_template, label) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--label', type=types.Related('label')) def disassociate_label(self, job_template, label): """Disassociate an label from this job template.""" return self._disassoc('labels', job_template, label) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def associate_notification_template(self, job_template, notification_template, status): """Associate a notification template from this job template.""" return self._assoc('notification_templates_%s' % status, job_template, notification_template) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def disassociate_notification_template(self, job_template, notification_template, status): """Disassociate a notification template from this job template.""" return self._disassoc('notification_templates_%s' % status, job_template, notification_template) @resources.command(use_fields_as_options=('extra_vars')) @click.option('--host-config-key', help='Job-template-specific string used to authenticate ' 'host during provisioning callback.') 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}
class Resource(models.Resource): cli_help = 'Manage credentials within Ansible Tower.' endpoint = '/credentials/' identity = ('organization', 'user', 'team', 'kind', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) # Who owns this credential? user = models.Field( display=False, type=types.Related('user'), required=False, ) team = models.Field( display=False, type=types.Related('team'), required=False, ) organization = models.Field( display=False, type=types.Related('organization'), required=False, ) # What type of credential is this (machine, SCM, etc.)? kind = models.Field( display=True, help_text='The type of credential being added. ' 'Valid options are: ssh, net, scm, aws, rax, vmware,' ' satellite6, cloudforms, gce, azure, azure_rm, openstack.', type=click.Choice([ 'ssh', 'net', 'scm', 'aws', 'rax', 'vmware', 'satellite6', 'cloudforms', 'gce', 'azure', 'azure_rm', 'openstack' ]), ) # need host in order to use VMware host = models.Field(help_text='The hostname or IP address to use.', required=False, display=False) # need project to use openstack project = models.Field(help_text='The identifier for the project.', required=False, display=False) # SSH and SCM fields. username = models.Field( help_text='The username. For AWS credentials, the access key.', required=False, ) password = models.Field( help_text='%sThe password. For AWS credentials, the secret key. ' 'For Rackspace credentials, the API key.' % PROMPT, password=True, required=False, ) ssh_key_data = models.Field( 'ssh_key_data', display=False, help_text="The full path to the SSH private key to store. " "(Don't worry; it's encrypted.)", required=False, type=models.File('r'), ) ssh_key_unlock = models.Field(help_text='%sssh_key_unlock' % PROMPT, password=True, required=False) # Extra fields in 3.0 authorize = models.Field( help_text='Whether to use the authorize mechanism when type is "net".', required=False, display=False, type=click.BOOL, ) authorize_password = models.Field( help_text='Password used by the authorize mechanism when type is ' '"net".', password=True, required=False, display=False, ) client = models.Field( help_text='Client Id or Application Id for the credential when type ' 'is "azure_rm".', required=False, display=False, ) secret = models.Field( help_text='Secret Token for this credential when type is "azure_rm".', required=False, display=False) subscription = models.Field( help_text='Subscription identifier for this credential when type is ' '"azure_rm".', required=False, display=False, ) tenant = models.Field( help_text='Tenant identifier for this credential when type is ' '"azure_rm"', required=False, display=False, ) domain = models.Field( help_text='Domain name for this credential when type is "openstack".', required=False, display=False, ) # Method with which to escalate become_method = models.Field( display=False, help_text='Privilege escalation method. ', type=types.MappedChoice([ ('', 'None'), ('sudo', 'sudo'), ('su', 'su'), ('pbrun', 'pbrun'), ('pfexec', 'pfexec'), ]), required=False, ) # SSH specific fields. become_username = models.Field(required=False, display=False) become_password = models.Field(password=True, required=False, help_text='%sThe become_password field' % PROMPT) vault_password = models.Field(password=True, required=False, help_text='%sThe vault_password field' % PROMPT) @resources.command def create(self, **kwargs): """Create a credential. """ if (kwargs.get('user', False) or kwargs.get('team', False) or kwargs.get('organization', False)): debug.log('Checking Project API Details.', header='details') r = client.options('/credentials/') if 'organization' in r.json()['actions']['POST']: for i in range(len(self.fields)): if self.fields[i].name in ('user', 'team'): self.fields[i].no_lookup = True return super(Resource, self).create(**kwargs)
class Resource(models.Resource, models.MonitorableResource): cli_help = 'Manage projects within Ansible Tower.' endpoint = '/projects/' unified_job_type = '/project_updates/' name = models.Field(unique=True) description = models.Field(required=False, display=False) organization = models.Field(type=types.Related('organization'), display=False, required=False) scm_type = models.Field(type=types.MappedChoice([ ('', 'manual'), ('git', 'git'), ('hg', 'hg'), ('svn', 'svn'), ]), ) scm_url = models.Field(required=False) local_path = models.Field( help_text='For manual projects, the server playbook directory name', required=False) scm_branch = models.Field(required=False, display=False) scm_credential = models.Field( 'credential', display=False, required=False, type=types.Related('credential'), ) scm_clean = models.Field(type=bool, required=False, display=False) scm_delete_on_update = models.Field(type=bool, required=False, display=False) scm_update_on_launch = models.Field(type=bool, required=False, display=False) job_timeout = models.Field(type=int, required=False, display=False, help_text='The timeout field (in seconds).') @resources.command @click.option('--monitor', is_flag=True, default=False, help='If sent, immediately calls `project monitor` on the ' 'project rather than exiting with a success.' 'It polls for status until the SCM is updated.') @click.option('--wait', is_flag=True, default=False, help='Polls server for status, exists when finished.') @click.option('--timeout', required=False, type=int, help='If provided with --monitor, the SCM update' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') def create(self, organization=None, monitor=False, wait=False, timeout=None, fail_on_found=False, force_on_exists=False, **kwargs): """Create a new item of resource, with or w/o org. This would be a shared class with user, but it needs the ability to monitor if the flag is set. """ if 'job_timeout' in kwargs and 'timeout' not in kwargs: kwargs['timeout'] = kwargs.pop('job_timeout') post_associate = False if organization: # Processing the organization flag depends on version debug.log('Checking Organization Relationship.', header='details') r = client.options('/projects/') if 'organization' in r.json()['actions']['POST']: kwargs['organization'] = organization else: post_associate = True # First, run the create method, ignoring the organization given answer = super(Resource, self).write(create_on_missing=True, fail_on_found=fail_on_found, force_on_exists=force_on_exists, **kwargs) project_id = answer['id'] # If an organization is given, associate it here if post_associate: # Get the organization from Tower, will lookup name if needed org_resource = get_resource('organization') org_data = org_resource.get(organization) org_pk = org_data['id'] debug.log("associating the project with its organization", header='details', nl=1) org_resource._assoc('projects', org_pk, project_id) # if the monitor flag is set, wait for the SCM to update if monitor and answer.get('changed', False): return self.monitor(pk=None, parent_pk=project_id, timeout=timeout) elif wait and answer.get('changed', False): return self.wait(pk=None, parent_pk=project_id, timeout=timeout) return answer @resources.command( use_fields_as_options=('name', 'description', 'scm_type', 'scm_url', 'local_path', 'scm_branch', 'scm_credential', 'scm_clean', 'scm_delete_on_update', 'scm_update_on_launch', 'job_timeout')) def modify(self, pk=None, create_on_missing=False, **kwargs): """Modify an already existing. To edit the project's organizations, see help for organizations. Fields in the resource's `identity` tuple can be used in lieu of a primary key for a lookup; in such a case, only other fields are written. To modify unique fields, you must use the primary key for the lookup. """ # Associated with issue #52, the organization can't be modified # with the 'modify' command. This would create confusion about # whether its flag is an identifier versus a field to modify. if 'job_timeout' in kwargs and 'timeout' not in kwargs: kwargs['timeout'] = kwargs.pop('job_timeout') return super(Resource, self).write(pk, create_on_missing=create_on_missing, force_on_exists=True, **kwargs) @resources.command(use_fields_as_options=('name', 'organization')) @click.option('--monitor', is_flag=True, default=False, help='If sent, immediately calls `job monitor` on the newly ' 'launched job rather than exiting with a success.') @click.option('--wait', is_flag=True, default=False, help='Polls server for status, exists when finished.') @click.option('--timeout', required=False, type=int, help='If provided with --monitor, this command (not the job)' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') 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, } @resources.command @click.option('--detail', is_flag=True, default=False, help='Print more detail.') def status(self, pk=None, detail=False, **kwargs): """Print the status of the most recent update.""" # Obtain the most recent project update job = self.last_job_data(pk, **kwargs) # 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'], }