class Resource(models.Resource): cli_help = 'Manage credentials within Ansible Tower.' endpoint = '/credentials/' identity = ('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, ) # What type of credential is this (machine, SCM, etc.)? kind = models.Field( help_text='The type of credential being added. ' 'Valid options are: ssh, scm, aws, rax, gce, azure.', type=click.Choice(['ssh', 'scm', 'aws', 'rax', 'gce', 'azure']), ) # SSH and SCM fields. username = models.Field( help_text='The username. For AWS credentials, the access key.', required=False, ) password = models.Field( help_text='The password. For AWS credentials, the secret key. ' 'For Rackspace credentials, the API key.', password=True, required=False, ) private_key = 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'), ) private_key_password = models.Field('ssh_key_unlock', password=True, required=False) # SSH specific fields. sudo_username = models.Field(required=False, display=False) sudo_password = models.Field(password=True, required=False) vault_password = models.Field(password=True, required=False)
class Resource(models.Resource): """A resource for notification templates.""" cli_help = 'Manage notification templates within Ansible Tower.' endpoint = '/notification_templates/' dependencies = ['organization'] # Actual fields name = models.Field(unique=True) description = models.Field(required=False, display=False) organization = models.Field(type=types.Related('organization'), required=False, display=False) notification_type = models.Field(type=click.Choice([ 'email', 'slack', 'twilio', 'pagerduty', 'hipchat', 'webhook', 'irc' ])) notification_configuration = models.Field( type=models.File('r', lazy=True), required=False, display=False, help_text='The notification configuration field. Note providing this' ' field would disable all notification-configuration-related' ' fields.') # Fields that are part of notification_configuration config_fields = [ 'notification_configuration', 'channels', 'token', 'username', 'sender', 'recipients', 'use_tls', 'host', 'use_ssl', 'password', 'port', 'account_token', 'from_number', 'to_numbers', 'account_sid', 'subdomain', 'service_key', 'client_name', 'message_from', 'api_url', 'color', 'notify', 'rooms', 'url', 'headers', 'server', 'nickname', 'targets' ] # Fields that are part of notification_configuration which are categorized # according to notification_type configuration = { 'slack': ['channels', 'token'], 'email': [ 'username', 'sender', 'recipients', 'use_tls', 'host', 'use_ssl', 'password', 'port' ], 'twilio': ['account_token', 'from_number', 'to_numbers', 'account_sid'], 'pagerduty': ['token', 'subdomain', 'service_key', 'client_name'], 'hipchat': ['message_from', 'api_url', 'color', 'token', 'notify', 'rooms'], 'webhook': ['url', 'headers'], 'irc': ['server', 'port', 'use_ssl', 'password', 'nickname', 'targets'] } # Fields which are expected to be json files. json_fields = ['notification_configuration', 'headers'] encrypted_fields = ['password', 'token', 'account_token'] # notification_configuration-related fields. fields with default values # are optional. username = models.Field(required=False, display=False, help_text='[{0}]The username.'.format('email')) sender = models.Field(required=False, display=False, help_text='[{0}]The sender.'.format('email')) recipients = models.Field(required=False, display=False, multiple=True, help_text='[{0}]The recipients.'.format('email')) use_tls = models.Field(required=False, display=False, type=click.BOOL, default=False, help_text='[{0}]The tls trigger.'.format('email')) host = models.Field(required=False, display=False, help_text='[{0}]The host.'.format('email')) use_ssl = models.Field( required=False, display=False, type=click.BOOL, default=False, help_text='[{0}]The ssl trigger.'.format('email/irc')) password = models.Field(required=False, display=False, password=True, help_text='[{0}]The password.'.format('email/irc')) port = models.Field(required=False, display=False, type=click.INT, help_text='[{0}]The email port.'.format('email/irc')) channels = models.Field(required=False, display=False, multiple=True, help_text='[{0}]The channel.'.format('slack')) token = models.Field( required=False, display=False, password=True, help_text='[{0}]The token.'.format('slack/pagerduty/hipchat')) account_token = models.Field( required=False, display=False, password=True, help_text='[{0}]The account token.'.format('twilio')) from_number = models.Field( required=False, display=False, help_text='[{0}]The source phone number.'.format('twilio')) to_numbers = models.Field( required=False, display=False, multiple=True, help_text='[{0}]The destination SMS numbers.'.format('twilio')) account_sid = models.Field( required=False, display=False, help_text='[{0}The account sid.'.format('twilio')) subdomain = models.Field( required=False, display=False, help_text='[{0}]The subdomain.'.format('pagerduty')) service_key = models.Field(required=False, display=False, help_text='[{0}]The API service/integration' ' key.'.format('pagerduty')) client_name = models.Field( required=False, display=False, help_text='[{0}]The client identifier.'.format('pagerduty')) message_from = models.Field(required=False, display=False, help_text='[{0}]The label to be shown with ' 'notification.'.format('hipchat')) api_url = models.Field(required=False, display=False, help_text='[{0}]The api url.'.format('hipchat')) color = models.Field( required=False, display=False, type=click.Choice( ['yellow', 'green', 'red', 'purple', 'gray', 'random']), help_text='[{0}]The notification color.'.format('hipchat')) rooms = models.Field(required=False, display=False, default=False, help_text='[{0}]Rooms to send notification to. ' 'Use multiple flags to send to multiple rooms, ex ' '--rooms=A --rooms=B'.format('hipchat'), multiple=True) notify = models.Field( required=False, display=False, type=click.BOOL, default=False, help_text='[{0}]The notify channel trigger.'.format('hipchat')) url = models.Field(required=False, display=False, help_text='[{0}]The target URL.'.format('webhook')) headers = models.Field( required=False, display=False, type=models.File('r', lazy=True), help_text='[{0}]The http headers.'.format('webhook')) server = models.Field(required=False, display=False, help_text='[{0}]Server address.'.format('irc')) nickname = models.Field(required=False, display=False, help_text='[{0}]The irc nick.'.format('irc')) target = models.Field( required=False, display=False, help_text='[{0}]The distination channels or users.'.format('irc')) def _separate(self, kwargs): """Remove None-valued and configuration-related keyworded arguments """ self._pop_none(kwargs) result = {} for field in Resource.config_fields: if field in kwargs: result[field] = kwargs.pop(field) if field in Resource.json_fields: # If result[field] is not a string we can continue on if not isinstance(result[field], six.string_types): continue try: data = json.loads(result[field]) result[field] = data except ValueError: raise exc.TowerCLIError('Provided json file format ' 'invalid. Please recheck.') return result def _configuration(self, kwargs, config_item): """Combine configuration-related keyworded arguments into notification_configuration. """ if 'notification_configuration' not in config_item: if 'notification_type' not in kwargs: return nc = kwargs['notification_configuration'] = {} for field in Resource.configuration[kwargs['notification_type']]: if field not in config_item: raise exc.TowerCLIError('Required config field %s not' ' provided.' % field) else: nc[field] = config_item[field] else: kwargs['notification_configuration'] = \ config_item['notification_configuration'] @resources.command @click.option('--job-template', type=types.Related('job_template'), required=False, help='The job template to relate to.') @click.option('--status', type=click.Choice(['error', 'success']), required=False, help='Specify job run status of job ' 'template to relate to.') def create(self, fail_on_found=False, force_on_exists=False, **kwargs): """Create a notification template. All required configuration-related fields (required according to notification_type) must be provided. There are two types of notification template creation: isolatedly creating a new notification template and creating a new notification template under a job template. Here the two types are discriminated by whether to provide --job-template option. --status option controls more specific, job-run-status-related association. Fields in the resource's `identity` tuple are used for a lookup; if a match is found, then no-op (unless `force_on_exists` is set) but do not fail (unless `fail_on_found` is set). =====API DOCS===== Create an object. :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===== """ config_item = self._separate(kwargs) jt_id = kwargs.pop('job_template', None) status = kwargs.pop('status', 'any') old_endpoint = self.endpoint if jt_id is not None: jt = get_resource('job_template') jt.get(pk=jt_id) try: nt_id = self.get(**copy.deepcopy(kwargs))['id'] except exc.NotFound: pass else: if fail_on_found: raise exc.TowerCLIError('Notification template already ' 'exists and fail-on-found is ' 'switched on. Please use' ' "associate_notification" method' ' of job_template instead.') else: debug.log( 'Notification template already exists, ' 'associating with job template.', header='details') return jt.associate_notification_template(jt_id, nt_id, status=status) self.endpoint = '/job_templates/%d/notification_templates_%s/' %\ (jt_id, status) self._configuration(kwargs, config_item) result = super(Resource, self).create(**kwargs) self.endpoint = old_endpoint return result @resources.command def modify(self, pk=None, create_on_missing=False, **kwargs): """Modify an existing notification template. Not all required configuration-related fields (required according to notification_type) should be provided. 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 object. :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===== """ # Create the resource if needed. if pk is None and create_on_missing: try: self.get(**copy.deepcopy(kwargs)) except exc.NotFound: return self.create(**kwargs) # Modify everything except notification type and configuration config_item = self._separate(kwargs) notification_type = kwargs.pop('notification_type', None) debug.log( 'Modify everything except notification type and' ' configuration', header='details') part_result = super(Resource, self).\ modify(pk=pk, create_on_missing=create_on_missing, **kwargs) # Modify notification type and configuration if notification_type is None or \ notification_type == part_result['notification_type']: for item in part_result['notification_configuration']: if item not in config_item or not config_item[item]: to_add = part_result['notification_configuration'][item] if not (to_add == '$encrypted$' and item in Resource.encrypted_fields): config_item[item] = to_add if notification_type is None: kwargs['notification_type'] = part_result['notification_type'] else: kwargs['notification_type'] = notification_type self._configuration(kwargs, config_item) debug.log('Modify notification type and configuration', header='details') result = super(Resource, self).\ modify(pk=pk, create_on_missing=create_on_missing, **kwargs) # Update 'changed' field to give general changed info if 'changed' in result and 'changed' in part_result: result['changed'] = result['changed'] or part_result['changed'] return result @resources.command def delete(self, pk=None, fail_on_missing=False, **kwargs): """Remove the given notification template. Note here configuration-related fields like 'notification_configuration' and 'channels' will not be used even provided. If `fail_on_missing` is True, then the object's not being found is considered a failure; otherwise, a success with no change is reported. =====API DOCS===== Remove the given object. :param pk: Primary key of the resource to be deleted. :type pk: int :param fail_on_missing: Flag that if set, the object's not being found is considered a failure; otherwise, a success with no change is reported. :type fail_on_missing: bool :param `**kwargs`: Keyword arguments used to look up resource object to delete if ``pk`` is not provided. :returns: dictionary of only one field "changed", which is a flag indicating whether the specified resource is successfully deleted. :rtype: dict =====API DOCS===== """ self._separate(kwargs) return super(Resource, self).\ delete(pk=pk, fail_on_missing=fail_on_missing, **kwargs) @resources.command def list(self, all_pages=False, **kwargs): """Return a list of notification templates. Note here configuration-related fields like 'notification_configuration' and 'channels' will not be used even provided. If one or more filters are provided through keyword arguments, filter the results accordingly. If no filters are provided, return all results. =====API DOCS===== Retrieve a list of objects. :param all_pages: Flag that if set, collect all pages of content from the API when returning results. :type all_pages: bool :param page: The page to show. Ignored if all_pages is set. :type page: int :param query: Contains 2-tuples used as query parameters to filter resulting resource objects. :type query: list :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict =====API DOCS===== """ self._separate(kwargs) return super(Resource, self).list(all_pages=all_pages, **kwargs) @resources.command def get(self, pk=None, **kwargs): """Return one and exactly one notification template. Note here configuration-related fields like 'notification_configuration' and 'channels' will not be used even provided. Lookups may be through a primary key, specified as a positional argument, and/or through filters specified through keyword arguments. If the number of results does not equal one, raise an exception. =====API DOCS===== Retrieve one and exactly one object. :param pk: Primary key of the resource to be read. Tower CLI will only attempt to read *that* object if ``pk`` is provided (not ``None``). :type pk: int :param `**kwargs`: Keyword arguments used to look up resource object to retrieve if ``pk`` is not provided. :returns: loaded JSON of the retrieved resource object. :rtype: dict =====API DOCS===== """ self._separate(kwargs) return super(Resource, self).get(pk=pk, **kwargs)
class Resource(models.Resource): """A resource for labels.""" cli_help = 'Manage labels within Ansible Tower.' endpoint = '/labels/' name = models.Field(unique=True) organization = models.Field(type=types.Related('organization'), display=False) def __getattribute__(self, name): """Disable inherited methods that cannot be applied to this particular resource. """ if name in ['delete']: raise AttributeError else: return object.__getattribute__(self, name) @resources.command @click.option('--job-template', type=types.Related('job_template'), required=False, help='The job template to relate to.') def create(self, fail_on_found=False, force_on_exists=False, **kwargs): """Create a new label. There are two types of label creation: isolatedly creating a new label and creating a new label under a job template. Here the two types are discriminated by whether to provide --job-template option. Fields in the resource's `identity` tuple are used for a lookup; if a match is found, then no-op (unless `force_on_exists` is set) but do not fail (unless `fail_on_found` is set). =====API DOCS===== Create a label. :param job_template: Primary key or name of the job template for the created label to associate to. :type job_template: str :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 arguements 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 :raises tower_cli.exceptions.TowerCLIError: When the label already exists and ``fail_on_found`` flag is on. =====API DOCS===== """ jt_id = kwargs.pop('job_template', None) old_endpoint = self.endpoint if jt_id is not None: jt = get_resource('job_template') jt.get(pk=jt_id) try: label_id = self.get(name=kwargs.get('name', None), organization=kwargs.get('organization', None))['id'] except exc.NotFound: pass else: if fail_on_found: raise exc.TowerCLIError('Label already exists and fail-on-found is switched on. Please use' ' "associate_label" method of job_template instead.') else: debug.log('Label already exists, associating with job template.', header='details') return jt.associate_label(jt_id, label_id) self.endpoint = '/job_templates/%d/labels/' % jt_id result = super(Resource, self).create(fail_on_found=fail_on_found, force_on_exists=force_on_exists, **kwargs) self.endpoint = old_endpoint return result
class Resource(models.SurveyResource): """A resource for job templates.""" cli_help = 'Manage job templates.' endpoint = '/job_templates/' dependencies = ['inventory', 'credential', 'project', 'vault_credential'] related = [ 'survey_spec', 'notification_templates', 'schedules', 'labels', 'credentials' ] 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) custom_virtualenv = models.Field(required=False, display=False) job_slice_count = models.Field(type=int, 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.') labels = models.ManyToManyField('label') instance_groups = models.ManyToManyField('instance_group', method_name='ig') def write(self, pk=None, *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' mcred = kwargs.get('credential', None) ret = super(Resource, self).write(pk=pk, **kwargs) cred_ids = [ c['id'] for c in ret.get('summary_fields', {}).get('credentials', []) ] if mcred and mcred not in cred_ids: new_pk = ret['id'] debug.log( 'Processing deprecated credential field via another request.', header='details') self._assoc('credentials', new_pk, mcred) ret = self.read(new_pk) ret['id'] = new_pk ret['changed'] = True return ret @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===== """ try: # Tower 3.3 behavior, allows all types of credentials return self._assoc('credentials', job_template, credential) except NotFound: debug.log( 'Attempting to use extra_credential as fallback in ' 'case server is older version.', header='details') 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===== """ try: return self._disassoc('credentials', job_template, credential) except NotFound: debug.log( 'Attempting to use extra_credential as fallback in ' 'case server is older version.', header='details') 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}
class Resource(models.SurveyResource): """A resource for workflow job templates.""" cli_help = 'Manage workflow job templates.' endpoint = '/workflow_job_templates/' unified_job_type = '/workflow_jobs/' dependencies = ['organization'] related = ['survey_spec', 'workflow_nodes', 'schedules', 'labels'] workflow_node_types = ['success_nodes', 'failure_nodes', 'always_nodes'] name = models.Field(unique=True) description = 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. Use the option ' 'multiple times to add multiple extra variables.') organization = models.Field(type=types.Related('organization'), required=False) survey_enabled = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') 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.') labels = models.ManyToManyField('label', res_name='workflow') ask_variables_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for extra_vars on launch.') ask_inventory_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for inventory on launch.') @staticmethod def _workflow_node_structure(node_results): ''' Takes the list results from the API in `node_results` and translates this data into a dictionary organized in a human-readable heirarchial structure ''' # Build list address translation, and create backlink lists node_list_pos = {} for i, node_result in enumerate(node_results): for rel in ['success', 'failure', 'always']: node_result['{0}_backlinks'.format(rel)] = [] node_list_pos[node_result['id']] = i # Populate backlink lists for node_result in node_results: for rel in ['success', 'failure', 'always']: for sub_node_id in node_result['{0}_nodes'.format(rel)]: j = node_list_pos[sub_node_id] node_results[j]['{0}_backlinks'.format(rel)].append( node_result['id']) # Find the root nodes root_nodes = [] for node_result in node_results: is_root = True for rel in ['success', 'failure', 'always']: if node_result['{0}_backlinks'.format(rel)] != []: is_root = False break if is_root: root_nodes.append(node_result['id']) # Create network dictionary recursively from root nodes def branch_schema(node_id): i = node_list_pos[node_id] node_dict = node_results[i] ret_dict = {"id": node_id} for fd in NODE_STANDARD_FIELDS: val = node_dict.get(fd, None) if val is not None: if fd == 'unified_job_template': job_type = node_dict['summary_fields'][ 'unified_job_template']['unified_job_type'] ujt_key = JOB_TYPES[job_type] ret_dict[ujt_key] = val else: ret_dict[fd] = val for rel in ['success', 'failure', 'always']: sub_node_id_list = node_dict['{0}_nodes'.format(rel)] if len(sub_node_id_list) == 0: continue relationship_name = '{0}_nodes'.format(rel) ret_dict[relationship_name] = [] for sub_node_id in sub_node_id_list: ret_dict[relationship_name].append( branch_schema(sub_node_id)) return ret_dict schema_dict = [] for root_node_id in root_nodes: schema_dict.append(branch_schema(root_node_id)) return schema_dict def _get_schema(self, wfjt_id): """ Returns a dictionary that represents the node network of the workflow job template """ node_res = get_resource('node') node_results = node_res.list(workflow_job_template=wfjt_id, all_pages=True)['results'] return self._workflow_node_structure(node_results) @resources.command(use_fields_as_options=False) @click.argument('wfjt', type=types.Related('workflow')) @click.argument('node_network', type=types.Variables(), required=False) def schema(self, wfjt, node_network=None): """ Convert YAML/JSON content into workflow node objects if node_network param is given. If not, print a YAML representation of the node network. =====API DOCS===== Convert YAML/JSON content into workflow node objects if ``node_network`` param is given. If not, print a YAML representation of the node network. :param wfjt: Primary key or name of the workflow job template to run schema against. :type wfjt: str :param node_network: JSON- or YAML-formatted string representing the topology of the workflow job template be updated to. :type node_network: str :returns: The latest topology (possibly after modification) of the workflow job template. :rtype: dict =====API DOCS===== """ existing_network = self._get_schema(wfjt) if not isinstance(existing_network, list): existing_network = [] if node_network is None: if settings.format == 'human': settings.format = 'yaml' return existing_network if hasattr(node_network, 'read'): node_network = node_network.read() node_network = string_to_dict(node_network, allow_kv=False, require_dict=False) if not isinstance(node_network, list): node_network = [] _update_workflow( [TreeNode(x, wfjt, include_id=True) for x in existing_network], [TreeNode(x, wfjt) for x in node_network]) if settings.format == 'human': settings.format = 'yaml' return self._get_schema(wfjt) @resources.command(use_fields_as_options=False) @click.option('--workflow', type=types.Related('workflow')) @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, workflow, notification_template, status): """Associate a notification template from this workflow. =====API DOCS===== Associate a notification template from this workflow job template. :param workflow: The workflow job template to associate to. :type workflow: 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, workflow, notification_template) @resources.command(use_fields_as_options=False) @click.option('--workflow', type=types.Related('workflow')) @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, workflow, notification_template, status): """Disassociate a notification template from this workflow. =====API DOCS===== Disassociate a notification template from this workflow job template. :param job_template: The workflow 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, workflow, notification_template)
class FooResource(models.Resource): endpoint = '/foo/' name = models.Field(unique=True) description = models.Field(required=False)
class Resource(models.Resource): cli_help = ( 'Manage permissions within Ansible Tower in versions prior to 3.0. \n' 'Starting with Ansible Tower 3.0, use the role resource to manage ' 'access controls. \n' 'All commands must specify either a user or a team to operate on.') endpoint = '/permissions/' identity = ('name', ) no_lookup_flag = False # Permissions must be created for either a user or a team name = models.Field(unique=True, required=False, display=True) user = models.Field(type=types.Related('user'), required=False, display=True, help_text='User to grant permission to.') team = models.Field(type=types.Related('team'), required=False, display=True, help_text='Team to grant permission to ' '(will apply to all members).') # Descriptive fields - not in identity or a parent resource description = models.Field(required=False, display=False) project = models.Field( type=types.Related('project'), required=False, display=True, help_text='Allows team/user access to this project.') inventory = models.Field( type=types.Related('inventory'), required=False, display=True, help_text='Allows team/user access to this inventory.') permission_type = models.Field(help_text='The level of access granted.', type=click.Choice([ "read", "write", "admin", "run", "check", "scan", "create" ])) run_ad_hoc_commands = models.Field( type=bool, required=False, display=False, help_text='If "true", includes permission to run ad hoc commands') def set_base_url(self, user, team): """Assure that endpoint is nested under a user or team""" if self.no_lookup_flag: return if user: self.endpoint = '/users/%d/permissions/' % user elif team: self.endpoint = '/teams/%d/permissions/' % team else: raise exc.TowerCLIError('Specify either a user or a team.') self.no_lookup_flag = True def get_permission_pk(self, pk, user, team, **kwargs): """Return the pk with a search method specific to permissions.""" if not pk: self.set_base_url(user, team) debug.log('Checking for existing permission.', header='details') existing_data = self._lookup(fail_on_found=False, fail_on_missing=True, include_debug_header=False, **kwargs) return existing_data['id'] else: self.no_lookup_flag = True return pk @resources.command def create(self, user=None, team=None, **kwargs): """Create a permission. Provide one of each: Permission granted to: user or team. Permission to: inventory or project.""" self.set_base_url(user, team) # Apply default specific to creation if not kwargs.get('permission_type', None): kwargs['permission_type'] = 'read' return super(Resource, self).create(**kwargs) @resources.command def modify(self, pk=None, user=None, team=None, **kwargs): """Modify an already existing permission. Provide pk for permission. Alternatively, provide name and the parent user/team. To modify unique fields, you must use the primary key for the lookup. """ # Use the user-based or team-based endpoint to search for record pk = self.get_permission_pk(pk, user, team, **kwargs) # Now use the permission-based endpoint to modify the record self.endpoint = '/permissions/' return super(Resource, self).modify(pk=pk, **kwargs) @resources.command def delete(self, pk=None, user=None, team=None, **kwargs): """Remove the given permission. Provide pk for permission. Alternatively, provide name and the parent user/team. If `fail_on_missing` is True, then the permission's not being found is considered a failure; otherwise, a success with no change is reported. """ # Use the user-based or team-based endpoint to search for record pk = self.get_permission_pk(pk, user, team, **kwargs) # Now use the permission-based endpoint to delete the record self.endpoint = '/permissions/' return super(Resource, self).delete(pk=pk, **kwargs) @resources.command(ignore_defaults=True) def get(self, pk=None, user=None, team=None, **kwargs): """Return one and exactly one permission. Provide pk for permission. Alternatively, provide name and the parent user/team. """ self.set_base_url(user, team) return super(Resource, self).get(pk=pk, **kwargs) @resources.command(ignore_defaults=True, no_args_is_help=False) def list(self, user=None, team=None, all_pages=False, **kwargs): """Return a list of permissions, specific to given user or team. If one or more filters are provided through keyword arguments, filter the results accordingly. If no filters are provided, return all results. But you still must give a user or team because a global listing is not allowed. """ self.set_base_url(user, team) return super(Resource, self).list(all_pages=all_pages, **kwargs)
class Resource(models.Resource): """A resource for Tower configurations.""" cli_help = 'Manage settings within Ansible Tower.' custom_category = None value = models.Field(required=True, type=types.Variables()) @resources.command(ignore_defaults=True, no_args_is_help=False) @click.option('category', '-c', '--category', help='If set, filter settings by a specific category') def list(self, **kwargs): """Return a list of objects. =====API DOCS===== Retrieve a list of Tower settings. :param category: The category slug in which to look up indevidual settings. :type category: str :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict =====API DOCS===== """ self.custom_category = kwargs.get('category', 'all') try: result = super(Resource, self).list(**kwargs) except exc.NotFound as e: categories = map( lambda category: category['slug'], client.get('/settings/').json()['results'] ) e.message = '%s is not a valid category. Choose from [%s]' % ( kwargs['category'], ', '.join(categories) ) raise e finally: self.custom_category = None return { 'results': [{'id': k, 'value': v} for k, v in result.items()] } @resources.command(use_fields_as_options=False) def get(self, pk): """Return one and exactly one object =====API DOCS===== Return one and exactly one Tower setting. :param pk: Primary key of the Tower setting to retrieve :type pk: int :returns: loaded JSON of the retrieved Tower setting object. :rtype: dict :raises tower_cli.exceptions.NotFound: When no specified Tower setting exists. =====API DOCS===== """ # The Tower API doesn't provide a mechanism for retrieving a single # setting value at a time, so fetch them all and filter try: return next(s for s in self.list()['results'] if s['id'] == pk) except StopIteration: raise exc.NotFound('The requested object could not be found.') @resources.command(use_fields_as_options=False) @click.argument('setting') @click.argument('value', default=None, required=False, type=types.Variables()) 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 @property def endpoint(self): return '/settings/%s/' % (self.custom_category or 'all') def coerce_type(self, key, value): if key == 'LICENSE': return json.loads(value) r = client.options(self.endpoint) if key not in r.json()['actions']['PUT']: raise exc.TowerCLIError('You are trying to modify value of a ' 'Read-Only field, which is not allowed') to_type = r.json()['actions']['PUT'].get(key, {}).get('type') if to_type == 'integer': if value != 'null': return int(value) else: return None elif to_type == 'boolean': return bool(strtobool(value)) elif to_type in ('list', 'nested object'): try: return json.loads(value) except Exception: debug.log('Could not parse value as JSON, trying as python.', header='details') return ast.literal_eval(value) return value def __getattribute__(self, name): """Disable inherited methods that cannot be applied to this particular resource. """ if name in ['create', 'delete']: raise AttributeError else: return object.__getattribute__(self, name)
class Resource(models.Resource, models.MonitorableResource): cli_help = 'Manage projects within Ansible Tower.' endpoint = '/projects/' 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) @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('--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, 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. """ 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(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')) 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. 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('--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, 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, } @resources.command @click.option('--detail', is_flag=True, default=False, help='Print more detail.') def status(self, pk=None, detail=False, **kwargs): """Print the current job status.""" # Get the job from Ansible Tower. debug.log('Asking for project update status.', header='details') project = client.get('/projects/%d/' % pk).json() # Determine the appropriate project update. if 'current_update' in project['related']: debug.log('A current update exists; retrieving it.', header='details') job = client.get(project['related']['current_update'][7:]).json() elif project['related'].get('last_update', None): debug.log( 'No current update exists; retrieving the most ' 'recent update.', header='details') job = client.get(project['related']['last_update'][7:]).json() else: raise exc.NotFound('No project updates exist.') # In most cases, we probably only want to know the status of the job # and the amount of time elapsed. However, if we were asked for # verbose information, provide it. if detail: return job # Print just the information we need. return { 'elapsed': job['elapsed'], 'failed': job['failed'], 'status': job['status'], }
class Resource(models.SurveyResource): cli_help = 'Manage workflow job templates.' endpoint = '/workflow_job_templates/' unified_job_type = '/workflow_jobs/' name = models.Field(unique=True) description = 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. Use the option ' 'multiple times to add multiple extra variables') organization = models.Field(type=types.Related('organization'), required=False) 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.') @staticmethod def _workflow_node_structure(node_results): ''' Takes the list results from the API in `node_results` and translates this data into a dictionary organized in a human-readable heirarchial structure ''' # Build list address translation, and create backlink lists node_list_pos = {} for i, node_result in enumerate(node_results): for rel in ['success', 'failure', 'always']: node_result['{0}_backlinks'.format(rel)] = [] node_list_pos[node_result['id']] = i # Populate backlink lists for node_result in node_results: for rel in ['success', 'failure', 'always']: for sub_node_id in node_result['{0}_nodes'.format(rel)]: j = node_list_pos[sub_node_id] node_results[j]['{0}_backlinks'.format(rel)].append( node_result['id']) # Find the root nodes root_nodes = [] for node_result in node_results: is_root = True for rel in ['success', 'failure', 'always']: if node_result['{0}_backlinks'.format(rel)] != []: is_root = False break if is_root: root_nodes.append(node_result['id']) # Create network dictionary recursively from root nodes def branch_schema(node_id): i = node_list_pos[node_id] node_dict = node_results[i] ret_dict = {} for fd in NODE_STANDARD_FIELDS: val = node_dict.get(fd, None) if val is not None: if fd == 'unified_job_template': job_type = node_dict['summary_fields'][ 'unified_job_template']['unified_job_type'] ujt_key = JOB_TYPES[job_type] ret_dict[ujt_key] = val else: ret_dict[fd] = val for rel in ['success', 'failure', 'always']: sub_node_id_list = node_dict['{0}_nodes'.format(rel)] if len(sub_node_id_list) == 0: continue relationship_name = '{0}_nodes'.format(rel) ret_dict[relationship_name] = [] for sub_node_id in sub_node_id_list: ret_dict[relationship_name].append( branch_schema(sub_node_id)) return ret_dict schema_dict = [] for root_node_id in root_nodes: schema_dict.append(branch_schema(root_node_id)) return schema_dict def _get_schema(self, wfjt_id): """ Returns a dictionary that represents the node network of the workflow job template """ node_res = get_resource('node') node_results = node_res.list(workflow_job_template=wfjt_id, all_pages=True)['results'] return self._workflow_node_structure(node_results) @resources.command(use_fields_as_options=False) @click.argument('wfjt', type=types.Related('workflow')) @click.argument('node_network', type=types.Variables(), required=False) def schema(self, wfjt, node_network=None): """ Convert YAML/JSON content into workflow node objects if node_network param is given. If not, print a YAML representation of the node network. """ if node_network is None: if settings.format == 'human': settings.format = 'yaml' return self._get_schema(wfjt) node_res = get_resource('node') def create_node(node_branch, parent, relationship): # Create node with data specified by top-level keys create_data = {} FK_FIELDS = JOB_TYPES.values() + ['inventory', 'credential'] for fd in NODE_STANDARD_FIELDS + JOB_TYPES.values(): if fd in node_branch: if (fd in FK_FIELDS and not isinstance(node_branch[fd], int)): # Node's template was given by name, do lookup ujt_res = get_resource(fd) ujt_data = ujt_res.get(name=node_branch[fd]) create_data[fd] = ujt_data['id'] else: create_data[fd] = node_branch[fd] create_data['workflow_job_template'] = wfjt return node_res._get_or_create_child(parent, relationship, **create_data) def get_adj_list(node_branch): ret = {} for fd in node_branch: for rel in ['success', 'failure', 'always']: if fd.startswith(rel): sub_branch_list = node_branch[fd] if not isinstance(sub_branch_list, list): raise BadRequest( 'Sublists in spec must be lists.' 'Encountered in {0} at {1}'.format( fd, sub_branch_list)) ret[rel] = sub_branch_list break return ret def create_node_recursive(node_network): queue = deque() id_queue = deque() for base_node in node_network: queue.append(base_node) id_queue.append(create_node(base_node, None, None)['id']) while (len(queue) != 0): to_expand = queue.popleft() parent_id = id_queue.popleft() adj_list = get_adj_list(to_expand) for rel in adj_list: for sub_node in adj_list[rel]: id_queue.append( create_node(sub_node, parent_id, rel)['id']) queue.append(sub_node) node_res._assoc(node_res._forward_rel_name(rel), parent_id, id_queue[-1]) if hasattr(node_network, 'read'): node_network = node_network.read() node_network = string_to_dict(node_network, allow_kv=False, require_dict=False) create_node_recursive(node_network) if settings.format == 'human': settings.format = 'yaml' return self._get_schema(wfjt)
class Resource(models.Resource, models.MonitorableResource): """A resource for inventory sources.""" cli_help = 'Manage inventory sources within Ansible Tower.' endpoint = '/inventory_sources/' unified_job_type = '/inventory_updates/' identity = ('inventory', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) inventory = models.Field(type=types.Related('inventory')) source = models.Field( default=None, help_text='The type of inventory source in use.', type=click.Choice(INVENTORY_SOURCE_CHOICES), ) credential = models.Field(type=types.Related('credential'), required=False, display=False) source_vars = models.Field(required=False, display=False) timeout = models.Field(type=int, required=False, display=False, help_text='The timeout field (in seconds).') # Variables not shared by all cloud providers source_project = models.Field( type=types.Related('project'), required=False, display=False, help_text='Use project files as source for inventory.') source_path = models.Field( required=False, display=False, help_text='File in SCM Project to use as source.') update_on_project_update = models.Field(type=bool, required=False, display=False) source_regions = models.Field(required=False, display=False) instance_filters = models.Field(required=False, display=False) group_by = models.Field(required=False, display=False) source_script = models.Field(type=types.Related('inventory_script'), required=False, display=False) # Boolean variables overwrite = models.Field(type=bool, required=False, display=False) overwrite_vars = models.Field(type=bool, required=False, display=False) update_on_launch = models.Field(type=bool, required=False, display=False) # Only used if update_on_launch is used update_cache_timeout = models.Field(type=int, required=False, display=False) @click.argument('inventory_source', type=types.Related('inventory_source')) @click.option('--monitor', is_flag=True, default=False, help='If sent, immediately calls `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.') @resources.command(use_fields_as_options=False, no_args_is_help=True) 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'} @resources.command @click.option('--detail', is_flag=True, default=False, help='Print more detail.') def status(self, pk, detail=False, **kwargs): """Print the status of the most recent sync. =====API DOCS===== Retrieve the current inventory update status. :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 inventory sync 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.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_name in ['create', 'modify', 'list']: attr = getattr(cls, attr_name) if getattr(attr, '__decorator__', None) == 'unified_job_template_options': continue wrapped_func = unified_job_template_options(attr) wrapped_func.__decorator__ = 'unified_job_template_options' setattr(cls, attr_name, wrapped_func) 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): 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 BarResource(models.Resource): endpoint = '/bar/' name = models.Field(unique=True) variables = models.Field(multiple=True)
class NoOptionResource(models.Resource): endpoint = '/nor/' yes = models.Field() no = models.Field(is_option=False)
class Resource(models.Resource): cli_help = 'Manage settings within Ansible Tower.' custom_category = None value = models.Field(required=True, type=types.Variables()) @resources.command(ignore_defaults=True, no_args_is_help=False) @click.option('category', '-c', '--category', help='If set, filter settings by a specific category') def list(self, **kwargs): """Return a list of objects.""" self.custom_category = kwargs.get('category', 'all') try: result = super(Resource, self).list(**kwargs) except exc.NotFound as e: categories = map(lambda category: category['slug'], client.get('/settings/').json()['results']) e.message = '%s is not a valid category. Choose from [%s]' % ( kwargs['category'], ', '.join(categories)) raise e finally: self.custom_category = None return {'results': [{'id': k, 'value': v} for k, v in result.items()]} @resources.command(use_fields_as_options=False) def get(self, pk): """Return one and exactly one object""" # The Tower API doesn't provide a mechanism for retrieving a single # setting value at a time, so fetch them all and filter try: return next(s for s in self.list()['results'] if s['id'] == pk) except StopIteration: raise exc.NotFound('The requested object could not be found.') @resources.command(use_fields_as_options=False) @click.argument('setting') @click.argument('value', default=None, required=False, type=types.Variables()) 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 @property def endpoint(self): return '/settings/%s/' % (self.custom_category or 'all') def coerce_type(self, key, value): if key == 'LICENSE': return json.loads(value) r = client.options(self.endpoint) to_type = r.json()['actions']['PUT'].get(key, {}).get('type') if to_type == 'integer': return int(value) elif to_type == 'boolean': return bool(strtobool(value)) elif to_type in ('list', 'nested object'): return ast.literal_eval(value) return value def __getattribute__(self, name): """Disable inherited methods that cannot be applied to this particular resource. """ if name in ['create', 'delete']: raise AttributeError else: return object.__getattribute__(self, name)
class ExplicitKeyResource(models.Resource): endpoint = '/ekr/' option_name = models.Field('internal_name')
class Resource(models.Resource): cli_help = 'Manage organizations within Ansible Tower.' endpoint = '/organizations/' deprecated_methods = ['associate_project', 'disassociate_project'] name = models.Field(unique=True) description = models.Field(required=False, display=False) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--user', type=types.Related('user'), required=True) def associate(self, organization, user): """Associate a user with this organization. =====API DOCS===== Associate a user with this organization. :param organization: Primary key or name of the organization to associate to. :type organization: str :param user: Primary key or name of the user to be associated. :type user: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('users', organization, user) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--user', type=types.Related('user'), required=True) def associate_admin(self, organization, user): """Associate an admin with this organization. =====API DOCS===== Associate an admin with this organization. :param organization: Primary key or name of the organization to associate to. :type organization: str :param user: Primary key or name of the user to be associated. :type user: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('admins', organization, user) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--user', type=types.Related('user'), required=True) def disassociate(self, organization, user): """Disassociate a user from this organization. =====API DOCS===== Disassociate a user from this organization. :param organization: Primary key or name of the organization to disassociate from. :type organization: str :param user: Primary key or name of the user to be disassociated. :type user: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('users', organization, user) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--user', type=types.Related('user'), required=True) def disassociate_admin(self, organization, user): """Disassociate an admin from this organization. =====API DOCS===== Disassociate an admin from this organization. :param organization: Primary key or name of the organization to disassociate from. :type organization: str :param user: Primary key or name of the user to be disassociated. :type user: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('admins', organization, user) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--project', type=types.Related('project'), required=True) def associate_project(self, organization, project): """Associate a project with this organization. =====API DOCS===== Associate a project with this organization. :param organization: Primary key or name of the organization to associate to. :type organization: str :param project: Primary key or name of the project to be associated. :type project: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('projects', organization, project) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--project', type=types.Related('project'), required=True) def disassociate_project(self, organization, project): """Disassociate a project from this organization. =====API DOCS===== Disassociate a project from this organization. :param organization: Primary key or name of the organization to disassociate from. :type organization: str :param project: Primary key or name of the project to be disassociated. :type project: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('projects', organization, project)
class BarResource(models.Resource): endpoint = '/bar/' name = models.Field(unique=True) required = models.Field()
class Resource(models.ExeResource): """A resource for workflow jobs.""" cli_help = 'Launch or monitor workflow jobs.' endpoint = '/workflow_jobs/' workflow_job_template = models.Field(key='-W', type=types.Related('workflow'), display=True) extra_vars = models.Field(type=types.Variables(), required=False, display=False, multiple=True) created = models.Field(required=False, display=True) status = models.Field(required=False, display=True) def __getattribute__(self, attr): """Alias the stdout to `summary` specially for workflow""" if attr == 'summary': return object.__getattribute__(self, 'stdout') elif attr == 'stdout': raise AttributeError return super(Resource, self).__getattribute__(attr) def lookup_stdout(self, pk=None, start_line=None, end_line=None, full=True): """ Internal method that lies to our `monitor` method by returning a scorecard for the workflow job where the standard out would have been expected. """ uj_res = get_resource('unified_job') # Filters # - limit search to jobs spawned as part of this workflow job # - order in the order in which they should add to the list # - only include final job states query_params = (('unified_job_node__workflow_job', pk), ('order_by', 'finished'), ('status__in', 'successful,failed,error')) jobs_list = uj_res.list(all_pages=True, query=query_params) if jobs_list['count'] == 0: return '' return_content = ResSubcommand(uj_res)._format_human(jobs_list) lines = return_content.split('\n') if not full: lines = lines[:-1] N = len(lines) start_range = start_line if start_line is None: start_range = 0 elif start_line > N: start_range = N end_range = end_line if end_line is None or end_line > N: end_range = N lines = lines[start_range:end_range] return_content = '\n'.join(lines) if len(lines) > 0: return_content += '\n' return return_content @resources.command def summary(self): """Placeholder to get swapped out for `stdout`. =====API DOCS===== foobar =====API DOCS===== """ pass @resources.command(use_fields_as_options=('workflow_job_template', 'extra_vars')) @click.option('--monitor', is_flag=True, default=False, help='If used, immediately calls monitor on the newly ' 'launched workflow job rather than exiting.') @click.option('--wait', is_flag=True, default=False, help='Wait until completion to exit, displaying ' 'placeholder text while in progress.') @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 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
class Resource(models.Resource): cli_help = 'Manage job templates.' endpoint = '/job_templates/' name = models.Field(unique=True) description = models.Field(required=False, display=False) job_type = models.Field( 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, 'debug'), ]), required=False, ) job_tags = models.Field(required=False, display=False) skip_tags = models.Field(required=False, display=False) extra_vars = models.Field(required=False, display=False) 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_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) @resources.command @click.option('--extra-vars', required=False, multiple=True, help='Extra variables used by Ansible in YAML or key=value ' 'format. Use @ to get YAML from a file.') def create(self, fail_on_found=False, force_on_exists=False, extra_vars=None, **kwargs): """Create a job template. You may include multiple --extra-vars flags in order to combine different sources of extra variables. Start this with @ in order to indicate a filename.""" if extra_vars: # combine sources of extra variables, if given kwargs['extra_vars'] = parser.process_extra_vars( extra_vars, force_json=False ) # Provide a default value for job_type, but only in creation of JT if not kwargs.get('job_type', False): kwargs['job_type'] = 'run' return super(Resource, self).create( fail_on_found=fail_on_found, force_on_exists=force_on_exists, **kwargs ) @resources.command @click.option('--extra-vars', required=False, multiple=True, help='Extra variables used by Ansible in YAML or key=value ' 'format. Use @ to get YAML from a file.') def modify(self, pk=None, create_on_missing=False, extra_vars=None, **kwargs): """Modify a job template. You may include multiple --extra-vars flags in order to combine different sources of extra variables. Start this with @ in order to indicate a filename.""" if extra_vars: # combine sources of extra variables, if given kwargs['extra_vars'] = parser.process_extra_vars( extra_vars, force_json=False ) return super(Resource, self).modify( pk=pk, create_on_missing=create_on_missing, **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)
class Resource(models.Resource): cli_help = 'Manage job templates.' endpoint = '/job_templates/' name = models.Field(unique=True) description = models.Field(required=False, display=False) job_type = models.Field( default='run', display=False, show_default=True, type=click.Choice(['run', 'check']), ) inventory = models.Field(type=types.Related('inventory')) project = models.Field(type=types.Related('project')) playbook = models.Field() machine_credential = models.Field('credential', display=False, type=types.Related('credential'), ) cloud_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, 'debug'), ]), ) job_tags = models.Field(required=False, display=False) extra_vars = models.Field(type=models.File('r'), required=False, display=False)
class Resource(models.Resource): """A resource for managing roles. This resource has ordinary list and get methods, but it roles can not be created or edited, instead, they are automatically generated along with the connected resource. """ cli_help = 'Add and remove users/teams from roles.' endpoint = '/roles/' user = models.Field(type=types.Related('user'), required=False, display=True) team = models.Field(type=types.Related('team'), required=False, display=True, help_text='The team that receives the permissions ' 'specified by the role') type = models.Field( required=False, display=True, type=click.Choice(ROLE_TYPES), help_text='The type of permission that the role controls.') # These fields are never valid input arguments, # they are only used as columns in output resource_name = models.Field(required=False, display=False) resource_type = models.Field(required=False, display=False) # These are purely resource fields, and are always inputs, # but are only selectively set as output columns target_team = models.Field(type=types.Related('team'), required=False, display=False, help_text='The team that the role acts on.') credential = models.Field(type=types.Related('credential'), required=False, display=False) inventory = models.Field(type=types.Related('inventory'), required=False, display=False) job_template = models.Field(type=types.Related('job_template'), required=False, display=False) credential = models.Field(type=types.Related('credential'), required=False, display=False) organization = models.Field(type=types.Related('organization'), required=False, display=False) project = models.Field(type=types.Related('project'), required=False, display=False) workflow = models.Field(type=types.Related('workflow'), required=False, display=False) def __getattribute__(self, name): """Disable inherited methods that cannot be applied to this particular resource. """ if name in ['create', 'delete', 'modify']: raise AttributeError else: return object.__getattribute__(self, name) @staticmethod def pluralize(kind): if kind == 'inventory': return 'inventories' elif kind == 'workflow': return 'workflow_job_templates' else: return '%ss' % kind @staticmethod def obj_res(data, fail_on=['type', 'obj', 'res']): """ Given some CLI input data, Returns the following and their types: obj - the role grantee res - the resource that the role applies to """ errors = [] if not data.get('type', None) and 'type' in fail_on: errors += ['You must provide a role type to use this command.'] # Find the grantee, and remove them from resource_list obj = None obj_type = None for fd in ACTOR_FIELDS: if data.get(fd, False): if not obj: obj = data[fd] obj_type = fd else: errors += [ 'You can not give a role to a user ' 'and team at the same time.' ] break if not obj and 'obj' in fail_on: errors += [ 'You must specify either user or ' 'team to use this command.' ] # Out of the resource list, pick out available valid resource field res = None res_type = None for fd in RESOURCE_FIELDS: if data.get(fd, False): if not res: res = data[fd] res_type = fd if res_type == 'target_team': res_type = 'team' else: errors += [ 'You can only give a role to one ' 'type of resource at a time.' ] break if not res and 'res' in fail_on: errors += [ 'You must specify a target resource ' 'to use this command.' ] if errors: raise exc.UsageError("\n".join(errors)) return obj, obj_type, res, res_type @classmethod def data_endpoint(cls, in_data, ignore=[]): """ Converts a set of CLI input arguments, `in_data`, into request data and an endpoint that can be used to look up a role or list of roles. Also changes the format of `type` in data to what the server expects for the role model, as it exists in the database. """ obj, obj_type, res, res_type = cls.obj_res(in_data, fail_on=[]) data = {} if 'obj' in ignore: obj = None if 'res' in ignore: res = None # Input fields are not actually present on role model, and all have # to be managed as individual special-cases if obj and obj_type == 'user': data['members__in'] = obj if obj and obj_type == 'team': endpoint = '%s/%s/roles/' % (cls.pluralize(obj_type), obj) if res is not None: # For teams, this is the best lookup we can do # without making the addional request for its member_role data['object_id'] = res elif res: endpoint = '%s/%s/object_roles/' % (cls.pluralize(res_type), res) else: endpoint = '/roles/' if in_data.get('type', False): data['role_field'] = '%s_role' % in_data['type'].lower() return data, endpoint @staticmethod def populate_resource_columns(item_dict): """Operates on item_dict Promotes the resource_name and resource_type fields to the top-level of the serialization so they can be printed as columns. Also makes a copies name field to type, which is a default column.""" item_dict['type'] = item_dict['name'] if len(item_dict['summary_fields']) == 0: # Singleton roles ommit these fields item_dict['resource_name'] = None item_dict['resource_type'] = None else: item_dict['resource_name'] = item_dict['summary_fields'][ 'resource_name'] item_dict['resource_type'] = item_dict['summary_fields'][ 'resource_type'] def set_display_columns(self, set_true=[], set_false=[]): """Add or remove columns from the output.""" for i in range(len(self.fields)): if self.fields[i].name in set_true: self.fields[i].display = True elif self.fields[i].name in set_false: self.fields[i].display = False def configure_display(self, data, kwargs=None, write=False): """Populates columns and sets display attribute as needed. Operates on data.""" if settings.format != 'human': return # This is only used for human format if write: obj, obj_type, res, res_type = self.obj_res(kwargs) data['type'] = kwargs['type'] data[obj_type] = obj data[res_type] = res self.set_display_columns( set_false=['team' if obj_type == 'user' else 'user'], set_true=['target_team' if res_type == 'team' else res_type]) else: self.set_display_columns( set_false=['user', 'team'], set_true=['resource_name', 'resource_type']) if 'results' in data: for i in range(len(data['results'])): self.populate_resource_columns(data['results'][i]) else: self.populate_resource_columns(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) 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/' % (self.pluralize(obj_type), obj), data=post_data) role_data['changed'] = True return role_data # Command method for roles # TODO: write commands to see access_list for resource @resources.command(use_fields_as_options=ACTOR_FIELDS + RESOURCE_FIELDS + ['type']) def list(self, **kwargs): """Return a list of roles.""" data, self.endpoint = self.data_endpoint(kwargs) r = super(Resource, self).list(**data) # Change display settings and data format for human consumption self.configure_display(r) return r @resources.command(use_fields_as_options=ACTOR_FIELDS + RESOURCE_FIELDS + ['type']) def get(self, pk=None, **kwargs): """Get information about a role.""" if kwargs.pop('include_debug_header', True): debug.log('Getting the role record.', header='details') data, self.endpoint = self.data_endpoint(kwargs) response = self.read(pk=pk, fail_on_no_results=True, fail_on_multiple_results=True, **data) item_dict = response['results'][0] self.configure_display(item_dict) return item_dict @resources.command(use_fields_as_options=ACTOR_FIELDS + RESOURCE_FIELDS + ['type']) @click.option('--fail-on-found', default=False, show_default=True, type=bool, is_flag=True, help='If used, return an error if the user already has the ' 'role.') def grant(self, fail_on_found=False, **kwargs): """Add a user or a team to a role. Required information: 1) Type of the role 2) Resource of the role, inventory, credential, or any other 3) A user or a team to add to the role""" return self.role_write(fail_on_found=fail_on_found, **kwargs) @resources.command(use_fields_as_options=ACTOR_FIELDS + RESOURCE_FIELDS + ['type']) @click.option('--fail-on-found', default=False, show_default=True, type=bool, is_flag=True, help='If used, return an error if the user is already ' 'not a member of the role.') def revoke(self, fail_on_found=False, **kwargs): """Remove a user or a team from a role. Required information: 1) Type of the role 2) Resource of the role, inventory, credential, or any other 3) A user or a team to add to the role""" return self.role_write(fail_on_found=fail_on_found, disassociate=True, **kwargs)
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) # 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')) machine_credential = models.Field( 'credential', display=False, type=types.Related('credential'), ) cloud_credential = models.Field(type=types.Related('credential'), required=False, display=False) 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) 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, ) @resources.command( use_fields_as_options=( 'job_explanation', 'job_type', 'inventory', 'machine_credential', 'cloud_credential', 'module_name', 'module_args', 'forks', 'limit', 'verbosity', 'become_enabled', ) ) @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.') @click.option('--become', required=False, is_flag=True, help='If used, privilege escalation will be enabled for ' 'this command.') 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
class Resource(models.ExeResource): """A resource for jobs. This resource has ordinary list and get methods, but it does not have create or modify. Instead of being created, a job is launched. """ cli_help = 'Launch or monitor jobs.' endpoint = '/jobs/' job_template = models.Field(key='-J', type=types.Related('job_template'), required=False, display=True) 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) @resources.command(use_fields_as_options=('job_template', 'job_explanation')) @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='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 command (not the job)' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') @click.option('--no-input', is_flag=True, default=False, help='Suppress any requests for input.') @click.option('-e', '--extra-vars', required=False, multiple=True, help='yaml format text that contains extra variables ' 'to pass on. Use @ to get these from a file.') @click.option('--limit', required=False, help='Specify host limit for job template to run.') @click.option('--tags', required=False, help='Specify tagged actions in the playbook to run.') @click.option('--skip-tags', required=False, help='Specify tagged actions in the playbook to ommit.') @click.option('--job-type', required=False, type=click.Choice(['run', 'check', 'scan']), help='Specify job type for job template' ' to run.') @click.option('--inventory', required=False, type=types.Related('inventory'), help='Specify inventory for job template to run.') @click.option('--credential', required=False, type=types.Related('credential'), help='Specify machine credential for job template to run.') @click.option('--use-job-endpoint', required=False, default=False, is_flag=True, help='A flag that disable launching jobs' ' from job template when set.') 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. """ # 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. 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
class MyResource(models.Resource): endpoint = '/bogus/' foo = models.Field(unique=True) bar = models.Field()
class Resource(models.Resource, models.MonitorableResource): """A resource for projects.""" 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'), ('insights', 'insights'), ]), ) 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).') @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()['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. =====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 BasicResource(models.Resource): endpoint = '/basic/' name = models.Field(unique=True)
class Resource(models.Resource): cli_help = 'Manage job templates.' endpoint = '/job_templates/' name = models.Field(unique=True) description = models.Field(required=False, display=False) job_type = models.Field( display=False, type=click.Choice(['run', 'check']), ) inventory = models.Field(type=types.Related('inventory')) project = models.Field(type=types.Related('project')) playbook = models.Field() machine_credential = models.Field( 'credential', display=False, type=types.Related('credential'), ) cloud_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, 'debug'), ]), required=False, ) job_tags = models.Field(required=False, display=False) skip_tags = models.Field(required=False, display=False) extra_vars = models.Field(required=False, display=False) ask_variables_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for extra_vars on launch.') become_enabled = models.Field(type=bool, required=False, display=False) @click.option('--extra-vars', required=False, multiple=True, help='Extra variables used by Ansible in YAML or key=value ' 'format. Use @ to get YAML from a file.') def create(self, fail_on_found=False, force_on_exists=False, extra_vars=None, **kwargs): """Create a job template. You may include multiple --extra-vars flags in order to combine different sources of extra variables. Start this with @ in order to indicate a filename.""" if extra_vars: # combine sources of extra variables, if given kwargs['extra_vars'] = parser.process_extra_vars(extra_vars, force_json=False) # Provide a default value for job_type, but only in creation of JT if not kwargs.get('job_type', False): kwargs['job_type'] = 'run' return super(Resource, self).create(fail_on_found=fail_on_found, force_on_exists=force_on_exists, **kwargs) @click.option('--extra-vars', required=False, multiple=True, help='Extra variables used by Ansible in YAML or key=value ' 'format. Use @ to get YAML from a file.') def modify(self, pk=None, create_on_missing=False, extra_vars=None, **kwargs): """Modify a job template. You may include multiple --extra-vars flags in order to combine different sources of extra variables. Start this with @ in order to indicate a filename.""" if extra_vars: # combine sources of extra variables, if given kwargs['extra_vars'] = parser.process_extra_vars(extra_vars, force_json=False) return super(Resource, self).modify(pk=pk, create_on_missing=create_on_missing, **kwargs)
class FieldHelpTextResource(models.Resource): endpoint = '/foobar/' option_name = models.Field('internal_name', help_text='foobar', required=False)