class Resource(models.Resource): """A resource for inventories.""" cli_help = 'Manage inventory within Ansible Tower.' endpoint = '/inventories/' identity = ('organization', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) organization = models.Field(type=types.Related('organization')) variables = models.Field(type=types.Variables(), required=False, display=False, help_text='Inventory variables, use "@" to get from file.') kind = models.Field(type=click.Choice(['', 'smart']), required=False, display=False, help_text='The kind field. Cannot be modified after created.') host_filter = models.Field(required=False, display=False, help_text='The host_filter field. Only useful when kind=smart.') insights_credential = models.Field(display=False, required=False, type=types.Related('credential')) @resources.command(ignore_defaults=True) def batch_update(self, pk=None, **kwargs): """Update all related inventory sources of the given inventory. Note global option --format is not available here, as the output would always be JSON-formatted. """ res = self.get(pk=pk, **kwargs) url = self.endpoint + '%d/%s/' % (res['id'], 'update_inventory_sources') return client.post(url, data={}).json() batch_update.format_freezer = 'json'
def test_variables_file(self): """Establish that file with variables is opened in this type.""" f = types.Variables() with mock.patch.object(click.File, 'convert') as convert: convert.return_value = "foo: bar" foo_converted = f.convert('@foobar.yml', 'myfile', None) convert.assert_called_once_with("foobar.yml", 'myfile', None) self.assertEqual(foo_converted, 'foo: bar')
class Resource(models.Resource): cli_help = 'Manage inventory within Ansible Tower.' endpoint = '/inventories/' identity = ('organization', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) organization = models.Field(type=types.Related('organization')) variables = models.Field( type=types.Variables(), required=False, display=False, help_text='Inventory variables, use "@" to get from file.')
class Resource(models.Resource): cli_help = 'Manage inventory scripts within Ansible Tower.' endpoint = '/inventory_scripts/' name = models.Field(unique=True) description = models.Field(required=False, display=False) script = models.Field( type=types.Variables(), display=False, help_text='Script code to fetch inventory, prefix with "@" to ' 'use contents of file for this field.') organization = models.Field(type=types.Related('organization'), display=False)
class Resource(models.Resource): """A resource for inventories.""" cli_help = 'Manage inventory within Ansible Tower.' endpoint = '/inventories/' identity = ('organization', 'name') dependencies = ['organization'] related = ['host', 'group', 'inventory_source'] name = models.Field(unique=True) description = models.Field(required=False, display=False) organization = models.Field(type=types.Related('organization')) variables = models.Field(type=types.Variables(), required=False, display=False, help_text='Inventory variables, use "@" to get from file.') kind = models.Field(type=click.Choice(['', 'smart']), required=False, display=False, help_text='The kind field. Cannot be modified after created.') host_filter = models.Field(required=False, display=False, help_text='The host_filter field. Only useful when kind=smart.') insights_credential = models.Field(display=False, required=False, type=types.Related('credential')) instance_groups = models.ManyToManyField('instance_group', method_name='ig') @resources.command(ignore_defaults=True) def batch_update(self, pk=None, **kwargs): """Update all related inventory sources of the given inventory. Note global option --format is not available here, as the output would always be JSON-formatted. =====API DOCS===== Update all related inventory sources of the given inventory. :param pk: Primary key of the given inventory. :type pk: int :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object of update status of the given inventory. :rtype: dict =====API DOCS===== """ res = self.get(pk=pk, **kwargs) url = self.endpoint + '%d/%s/' % (res['id'], 'update_inventory_sources') return client.post(url, data={}).json() batch_update.format_freezer = 'json'
class Resource(models.Resource): cli_help = 'Manage schedules within Ansible Tower.' endpoint = '/schedules/' # General fields. name = models.Field(unique=True) description = models.Field(required=False, display=False) # Unified jt fields. note these fields will only be used during creation. # Plus, one and only one field should be provided. job_template = models.Field(type=types.Related('job_template'), required=False, display=False) inventory_source = models.Field(type=types.Related('inventory_source'), required=False, display=False) project = models.Field(type=types.Related('project'), required=False, display=False) # Schedule-specific fields. unified_job_template = models.Field(required=False, type=int, help_text='Integer used to display' ' unified job template in result, ' 'Please don\'t use it for create/' 'modify.') enabled = models.Field(required=False, type=click.BOOL, default=True, help_text='Whether this schedule will be used', show_default=True) rrule = models.Field(required=False, display=False, help_text='Schedule rules specifications which is' ' less than 255 characters.') 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.') def _get_patch_url(self, url, pk): urlTokens = url.split('/') if len(urlTokens) > 3: # reconstruct url to prevent a rare corner case where resources # cannot be constructed independently. Open to modification if # API convention changes. url = '/'.join(urlTokens[:1] + urlTokens[-2:]) return super(Resource, self)._get_patch_url(url, pk)
class Resource(models.Resource): cli_help = 'Manage hosts belonging to a group within an inventory.' endpoint = '/hosts/' identity = ('inventory', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) inventory = models.Field(type=types.Related('inventory')) enabled = models.Field(type=bool, required=False) variables = models.Field( type=types.Variables(), required=False, display=False, help_text='Host variables, use "@" to get from file.') @resources.command(use_fields_as_options=False) @click.option('--host', type=types.Related('host')) @click.option('--group', type=types.Related('group')) def associate(self, host, group): """Associate a group with this host.""" return self._assoc('groups', host, group) @resources.command(use_fields_as_options=False) @click.option('--host', type=types.Related('host')) @click.option('--group', type=types.Related('group')) def disassociate(self, host, group): """Disassociate a group from this host.""" return self._disassoc('groups', host, group) @resources.command(ignore_defaults=True, no_args_is_help=False) @click.option('--group', type=types.Related('group'), help='List hosts that are children of this group.') def list(self, group=None, **kwargs): """Return a list of hosts. """ if group: kwargs['query'] = (kwargs.get('query', ()) + (('groups__in', group), )) return super(Resource, self).list(**kwargs)
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') @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 Resource(models.Resource): """A resource for credentials.""" cli_help = 'Manage hosts belonging to a group within an inventory.' endpoint = '/hosts/' identity = ('inventory', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) inventory = models.Field(type=types.Related('inventory')) enabled = models.Field(type=bool, required=False) variables = models.Field( type=types.Variables(), required=False, display=False, help_text='Host variables, use "@" to get from file.') insights_system_id = models.Field(required=False, display=False) @resources.command(use_fields_as_options=False) @click.option('--host', type=types.Related('host')) @click.option('--group', type=types.Related('group')) def associate(self, host, group): """Associate a group with this host. =====API DOCS===== Associate a group with this host. :param host: Primary key or name of the host to be associated. :type host: str :param group: Primary key or name of the group to associate. :type group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('groups', host, group) @resources.command(use_fields_as_options=False) @click.option('--host', type=types.Related('host')) @click.option('--group', type=types.Related('group')) def disassociate(self, host, group): """Disassociate a group from this host. =====API DOCS===== Disassociate a group from this host. :param host: Primary key or name of the host to be disassociated. :type host: str :param group: Primary key or name of the group to disassociate. :type group: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('groups', host, group) @resources.command(ignore_defaults=True, no_args_is_help=False) @click.option('--group', type=types.Related('group'), help='List hosts that are children of this group.') @click.option('--host-filter', help='List hosts filtered by this fact search query string.') def list(self, group=None, host_filter=None, **kwargs): """Return a list of hosts. =====API DOCS===== Retrieve a list of hosts. :param group: Primary key or name of the group whose hosts will be listed. :type group: str :param all_pages: Flag that if set, collect all pages of content from the API when returning results. :type all_pages: bool :param page: The page to show. Ignored if all_pages is set. :type page: int :param query: Contains 2-tuples used as query parameters to filter resulting resource objects. :type query: list :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict =====API DOCS===== """ if group: kwargs['query'] = kwargs.get('query', ()) + (('groups__in', group), ) if host_filter: kwargs['query'] = kwargs.get('query', ()) + (('host_filter', host_filter), ) return super(Resource, self).list(**kwargs) @resources.command(ignore_defaults=True) def list_facts(self, pk=None, **kwargs): """Return a JSON object of all available facts of the given host. Note global option --format is not available here, as the output would always be JSON-formatted. =====API DOCS===== List all available facts of the given host. :param pk: Primary key of the target host. :type pk: int :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object of all available facts of the given host. :rtype: dict =====API DOCS===== """ res = self.get(pk=pk, **kwargs) url = self.endpoint + '%d/%s/' % (res['id'], 'ansible_facts') return client.get(url, params={}).json() list_facts.format_freezer = 'json' @resources.command(ignore_defaults=True) def insights(self, pk=None, **kwargs): """Return a JSON object of host insights. Note global option --format is not available here, as the output would always be JSON-formatted. =====API DOCS===== List host insights. :param pk: Primary key of the target host. :type pk: int :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object of host insights. :rtype: dict =====API DOCS===== """ res = self.get(pk=pk, **kwargs) url = self.endpoint + '%d/%s/' % (res['id'], 'insights') return client.get(url, params={}).json() insights.format_freezer = 'json'
class Resource(models.Resource): """A resource for schedules.""" cli_help = 'Manage schedules within Ansible Tower.' endpoint = '/schedules/' # General fields. name = models.Field(unique=True) description = models.Field(required=False, display=False) # Unified jt fields. note these fields will only be used during creation. # Plus, one and only one field should be provided. job_template = models.Field(type=types.Related('job_template'), required=False, display=False) inventory_source = models.Field(type=types.Related('inventory_source'), required=False, display=False) project = models.Field(type=types.Related('project'), required=False, display=False) # Schedule-specific fields. unified_job_template = models.Field(required=False, type=int, help_text='Integer used to display' ' unified job template in result, ' 'Do not use it for create/' 'modify.') enabled = models.Field(required=False, type=click.BOOL, default=True, help_text='Whether this schedule will be used.', show_default=True) rrule = models.Field(required=False, display=False, help_text='Schedule rules specifications which is' ' less than 255 characters.') # Prompts extra_data = models.Field(type=types.Variables(), required=False, display=False, help_text='Extra data for ' 'schedule rules in the form of a .json file.') inventory = models.Field(type=types.Related('inventory'), required=False, display=False) credentials = models.ManyToManyField('credential') job_type = models.Field(required=False, display=False) job_tags = models.Field(required=False, display=False) skip_tags = models.Field(required=False, display=False) limit = models.Field(required=False, display=False) diff_mode = models.Field(type=bool, required=False, display=False) verbosity = models.Field( display=False, type=types.MappedChoice([ (0, 'default'), (1, 'verbose'), (2, 'more_verbose'), (3, 'debug'), (4, 'connection'), (5, 'winrm'), ]), required=False, ) def _get_patch_url(self, url, pk): urlTokens = url.split('/') if len(urlTokens) > 3: # reconstruct url to prevent a rare corner case where resources # cannot be constructed independently. Open to modification if # API convention changes. url = '/'.join(urlTokens[:1] + urlTokens[-2:]) return super(Resource, self)._get_patch_url(url, pk)
class Resource(models.Resource): """A resource for workflow nodes.""" cli_help = 'Manage nodes inside of a workflow job template.' endpoint = '/workflow_job_template_nodes/' identity = ('id', ) workflow_job_template = models.Field(key='-W', type=types.Related('workflow')) unified_job_template = models.Field(required=False) # Prompts extra_data = models.Field(type=types.Variables(), required=False, display=False, help_text='Extra data for ' 'schedule rules in the form of a .json file.') inventory = models.Field(type=types.Related('inventory'), required=False, display=False) credential = models.Field(type=types.Related('credential'), required=False, display=False) credentials = models.ManyToManyField('credential') job_type = models.Field(required=False, display=False) job_tags = models.Field(required=False, display=False) skip_tags = models.Field(required=False, display=False) limit = models.Field(required=False, display=False) diff_mode = models.Field(type=bool, required=False, display=False) verbosity = models.Field( display=False, type=types.MappedChoice([ (0, 'default'), (1, 'verbose'), (2, 'more_verbose'), (3, 'debug'), (4, 'connection'), (5, 'winrm'), ]), required=False, ) def __new__(cls, *args, **kwargs): for attr in ['create', 'modify', 'list']: setattr(cls, attr, unified_job_template_options(getattr(cls, attr))) return super(Resource, cls).__new__(cls, *args, **kwargs) @staticmethod def _forward_rel_name(rel): return '{0}_nodes'.format(rel) @staticmethod def _reverse_rel_name(rel): return 'workflowjobtemplatenodes_{0}'.format(rel) def _parent_filter(self, parent, relationship, **kwargs): """ Returns filtering parameters to limit a search to the children of a particular node by a particular relationship. """ if parent is None or relationship is None: return {} parent_filter_kwargs = {} query_params = ((self._reverse_rel_name(relationship), parent), ) parent_filter_kwargs['query'] = query_params if kwargs.get('workflow_job_template', None) is None: parent_data = self.read(pk=parent)['results'][0] parent_filter_kwargs['workflow_job_template'] = parent_data[ 'workflow_job_template'] return parent_filter_kwargs @unified_job_template_options def _get_or_create_child(self, parent, relationship, **kwargs): ujt_pk = kwargs.get('unified_job_template', None) if ujt_pk is None: raise exceptions.BadRequest( 'A child node must be specified by one of the options ' 'unified-job-template, job-template, project, or ' 'inventory-source') kwargs.update(self._parent_filter(parent, relationship, **kwargs)) response = self.read(fail_on_no_results=False, fail_on_multiple_results=False, **kwargs) if len(response['results']) == 0: debug.log('Creating new workflow node.', header='details') return client.post(self.endpoint, data=kwargs).json() else: return response['results'][0] def _assoc_or_create(self, relationship, parent, child, **kwargs): if child is None: child_data = self._get_or_create_child(parent, relationship, **kwargs) return child_data return self._assoc(self._forward_rel_name(relationship), parent, child) @resources.command @unified_job_template_options @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node'), required=False) def associate_success_node(self, parent, child=None, **kwargs): """Add a node to run on success. =====API DOCS===== Add a node to run on success. :param parent: Primary key of parent node to associate success node to. :type parent: int :param child: Primary key of child node to be associated. :type child: int :param `**kwargs`: Fields used to create child node if ``child`` is not provided. :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc_or_create('success', parent, child, **kwargs) @resources.command(use_fields_as_options=False) @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node')) def disassociate_success_node(self, parent, child): """Remove success node. The resulatant 2 nodes will both become root nodes. =====API DOCS===== Remove success node. :param parent: Primary key of parent node to disassociate success node from. :type parent: int :param child: Primary key of child node to be disassociated. :type child: int :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc(self._forward_rel_name('success'), parent, child) @resources.command @unified_job_template_options @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node'), required=False) def associate_failure_node(self, parent, child=None, **kwargs): """Add a node to run on failure. =====API DOCS===== Add a node to run on failure. :param parent: Primary key of parent node to associate failure node to. :type parent: int :param child: Primary key of child node to be associated. :type child: int :param `**kwargs`: Fields used to create child node if ``child`` is not provided. :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc_or_create('failure', parent, child, **kwargs) @resources.command(use_fields_as_options=False) @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node')) def disassociate_failure_node(self, parent, child): """Remove a failure node link. The resulatant 2 nodes will both become root nodes. =====API DOCS===== Remove a failure node link. :param parent: Primary key of parent node to disassociate failure node from. :type parent: int :param child: Primary key of child node to be disassociated. :type child: int :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc(self._forward_rel_name('failure'), parent, child) @resources.command @unified_job_template_options @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node'), required=False) def associate_always_node(self, parent, child=None, **kwargs): """Add a node to always run after the parent is finished. =====API DOCS===== Add a node to always run after the parent is finished. :param parent: Primary key of parent node to associate always node to. :type parent: int :param child: Primary key of child node to be associated. :type child: int :param `**kwargs`: Fields used to create child node if ``child`` is not provided. :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc_or_create('always', parent, child, **kwargs) @resources.command(use_fields_as_options=False) @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node')) def disassociate_always_node(self, parent, child): """Remove an always node link. The resultant 2 nodes will both become root nodes. =====API DOCS===== Remove an always node link. :param parent: Primary key of parent node to disassociate always node from. :type parent: int :param child: Primary key of child node to be disassociated. :type child: int :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc(self._forward_rel_name('always'), parent, child)
class Resource(models.Resource): cli_help = 'Manage groups belonging to an inventory.' endpoint = '/groups/' identity = ('inventory', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) inventory = models.Field(type=types.Related('inventory')) variables = models.Field( type=types.Variables(), required=False, display=False, help_text='Group variables, use "@" to get from file.') def lookup_with_inventory(self, group, inventory=None): group_res = get_resource('group') if isinstance(group, int) or group.isdigit(): return group_res.get(int(group)) else: return group_res.get(name=group, inventory=inventory) def set_child_endpoint(self, parent, inventory=None): parent_data = self.lookup_with_inventory(parent, inventory) self.endpoint = '/groups/' + str(parent_data['id']) + '/children/' return parent_data # Basic options for the source @resources.command @click.option('--credential', type=types.Related('credential'), required=False, help='The cloud credential to use.') @click.option('--source', type=click.Choice(INVENTORY_SOURCES), default='manual', help='The source to use for this group.') @click.option('--source-regions', help='Regions for your cloud provider.') # Options may not be valid for certain types of cloud servers @click.option('--source-vars', help='Override variables found on source ' 'with variables defined in this field.') @click.option('--instance-filters', help='A comma-separated list of ' 'filter expressions for matching hosts to be imported to ' 'Tower.') @click.option('--group-by', help='Limit groups automatically created from' ' inventory source.') @click.option('--source-script', type=types.Related('inventory_script'), help='Inventory script to be used when group type is ' '"custom".') @click.option('--overwrite', type=bool, help='Delete child groups and hosts not found in source.') @click.option('--overwrite-vars', type=bool, help='Override vars in child groups and hosts with those ' 'from the external source.') @click.option('--update-on-launch', type=bool, help='Refresh inventory ' 'data from its source each time a job is run.') @click.option('--parent', help='Parent group to nest this one inside of.') @click.option('--job-timeout', type=int, help='Timeout value (in seconds) ' 'for underlying inventory source.') def create(self, fail_on_found=False, force_on_exists=False, **kwargs): """Create a group and, if necessary, modify the inventory source within the group. """ group_fields = [f.name for f in self.fields] if kwargs.get('parent', None): parent_data = self.set_child_endpoint(parent=kwargs['parent'], inventory=kwargs.get( 'inventory', None)) kwargs['inventory'] = parent_data['inventory'] group_fields.append('group') elif 'inventory' not in kwargs: raise exc.UsageError('To create a group, you must provide a ' 'parent inventory or parent group.') # Break out the options for the group vs its inventory_source is_kwargs = {} for field in kwargs.copy(): if field not in group_fields: if field == 'job_timeout': is_kwargs['timeout'] = kwargs.pop(field) else: is_kwargs[field] = kwargs.pop(field) # Handle alias for "manual" source if is_kwargs.get('source', None) == 'manual': is_kwargs.pop('source') # First, create the group. answer = super(Resource, self).create(fail_on_found=fail_on_found, force_on_exists=force_on_exists, **kwargs) # If the group already exists and we aren't supposed to make changes, # then we're done. if not force_on_exists and not answer['changed']: return answer # Sanity check: A group was created, but do we need to do anything # with the inventory source at all? If no credential or source # was specified, then we'd just be updating the inventory source # with an effective no-op. if len(is_kwargs) == 0: return answer # Get the inventory source ID ("isid"). # Inventory sources are not created directly; rather, one was created # automatically when the group was created. isid = self._get_inventory_source_id(answer) # We now have our inventory source ID; modify it according to the # provided parameters. isrc = get_resource('inventory_source') is_answer = isrc.write(pk=isid, force_on_exists=True, **is_kwargs) # If either the inventory_source or the group objects were modified # then refelect this in the output to avoid confusing the user. if is_answer['changed']: answer['changed'] = True return answer @resources.command @click.option('--credential', type=types.Related('credential'), required=False, help='The cloud credential to use.') @click.option('--source', type=click.Choice(INVENTORY_SOURCES), help='The source to use for this group.') @click.option('--source-regions', help='Regions for your cloud provider.') # Options may not be valid for certain types of cloud servers @click.option('--source-vars', help='Override variables found on source ' 'with variables defined in this field.') @click.option('--instance-filters', help='A comma-separated list of ' 'filter expressions for matching hosts to be imported to ' 'Tower.') @click.option('--group-by', help='Limit groups automatically created from' ' inventory source.') @click.option('--source-script', type=types.Related('inventory_script'), help='Inventory script to be used when group type is ' '"custom".') @click.option('--overwrite', type=bool, help='Delete child groups and hosts not found in source.') @click.option('--overwrite-vars', type=bool, help='Override vars in child groups and hosts with those ' 'from the external source.') @click.option('--update-on-launch', type=bool, help='Refersh inventory ' 'data from its source each time a job is run.') @click.option('--job-timeout', type=int, help='Timeout value (in seconds) ' 'for underlying inventory source.') def modify(self, pk=None, create_on_missing=False, **kwargs): """Modify a group and, if necessary, the inventory source within the group. """ # Break out the options for the group vs its inventory_source group_fields = [f.name for f in self.fields] is_kwargs = {} for field in kwargs.copy(): if field not in group_fields: if field == 'job_timeout': is_kwargs['timeout'] = kwargs.pop(field) else: is_kwargs[field] = kwargs.pop(field) # Handle alias for "manual" source if is_kwargs.get('source', None) == 'manual': is_kwargs['source'] = '' # First, modify the group. answer = super(Resource, self).modify(pk=pk, create_on_missing=create_on_missing, **kwargs) # If the group already exists and we aren't supposed to make changes, # then we're done. if len(is_kwargs) == 0: return answer # Get the inventory source ID ("isid"). # Inventory sources are not created directly; rather, one was created # automatically when the group was created. isid = self._get_inventory_source_id(answer) # We now have our inventory source ID; modify it according to the # provided parameters. # # Note: Any fields that were part of the group modification need # to be expunged from kwargs before making this call. isrc = get_resource('inventory_source') is_answer = isrc.write(pk=isid, force_on_exists=True, **is_kwargs) # If either the inventory_source or the group objects were modified # then refelect this in the output to avoid confusing the user. if is_answer['changed']: answer['changed'] = True return answer @resources.command(ignore_defaults=True, no_args_is_help=False) @click.option('--root', is_flag=True, default=False, help='Show only root groups (groups with no parent groups) ' 'within the given inventory.') @click.option('--parent', help='Parent group to nest this one inside of.') def list(self, root=False, **kwargs): """Return a list of groups.""" # Option to list children of a parent group if kwargs.get('parent', None): self.set_child_endpoint(parent=kwargs['parent'], inventory=kwargs.get('inventory', None)) kwargs.pop('parent') # Sanity check: If we got `--root` and no inventory, that's an # error. if root and not kwargs.get('inventory', None): raise exc.UsageError('The --root option requires specifying an ' 'inventory also.') # If we are tasked with getting root groups, do that. if root: inventory_id = kwargs['inventory'] r = client.get('/inventories/%d/root_groups/' % inventory_id) return r.json() # Return the superclass implementation. return super(Resource, self).list(**kwargs) @click.argument('group', required=False, type=types.Related('group')) @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(no_args_is_help=True) def sync(self, group, monitor=False, wait=False, timeout=None, **kwargs): """Update the given group's inventory source.""" isrc = get_resource('inventory_source') isid = self._get_inventory_source_id(group, kwargs) return isrc.update(isid, monitor=monitor, timeout=timeout, wait=wait, **kwargs) @resources.command @click.argument('group', required=False, type=types.Related('group')) @click.option('--start-line', required=False, type=int, help='Line at which to start printing the standard out.') @click.option('--end-line', required=False, type=int, help='Line at which to end printing the standard out.') def stdout(self, group, start_line=None, end_line=None, **kwargs): """Print the standard out of the last group update.""" isrc = get_resource('inventory_source') isid = self._get_inventory_source_id(group, kwargs) return isrc.stdout(isid) @resources.command(use_fields_as_options=False) @click.option('--group', help='The group to move.') @click.option('--parent', help='Destination group to move into.') @click.option('--inventory', type=types.Related('inventory')) def associate(self, group, parent, **kwargs): """Associate this group with the specified group.""" parent_id = self.lookup_with_inventory(parent, kwargs.get('inventory', None))['id'] group_id = self.lookup_with_inventory(group, kwargs.get('inventory', None))['id'] return self._assoc('children', parent_id, group_id) @resources.command(use_fields_as_options=False) @click.option('--group', help='The group to move.') @click.option('--parent', help='Destination group to move into.') @click.option('--inventory', type=types.Related('inventory')) def disassociate(self, group, parent, **kwargs): """Disassociate this group from the specified group.""" parent_id = self.lookup_with_inventory(parent, kwargs.get('inventory', None))['id'] group_id = self.lookup_with_inventory(group, kwargs.get('inventory', None))['id'] return self._disassoc('children', parent_id, group_id) def _get_inventory_source_id(self, group, data=None): """Return the inventory source ID given a group dictionary returned from the Tower API. Alternatively, get it from a group's identity set in data. """ if group is None: group = self.get(**data) # If we got a group ID rather than a group, get the group. elif isinstance(group, int): group = self.get(group) # Return the inventory source ID. return int(group['related']['inventory_source'].split('/')[-2])
def test_variables_backup_option(self): """Establish that non-string input is protected against.""" f = types.Variables() foo_converted = f.convert(54, 'myfile', None) self.assertEqual(foo_converted, 54)
def test_variables_no_file(self): """Establish that plain variables are passed as-is.""" f = types.Variables() foo_converted = f.convert('foo: barz', 'myfile', None) self.assertEqual(foo_converted, 'foo: barz')
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.SurveyResource): """A resource for job templates.""" cli_help = 'Manage job templates.' endpoint = '/job_templates/' name = models.Field(unique=True) description = models.Field(required=False, display=False) job_type = models.Field( required=False, display=False, type=click.Choice(['run', 'check']), ) inventory = models.Field(type=types.Related('inventory'), required=False) project = models.Field(type=types.Related('project')) playbook = models.Field() credential = models.Field(display=False, required=False, type=types.Related('credential')) vault_credential = models.Field(type=types.Related('credential'), required=False, display=False) forks = models.Field(type=int, required=False, display=False) limit = models.Field(required=False, display=False) verbosity = models.Field( display=False, type=types.MappedChoice([ (0, 'default'), (1, 'verbose'), (2, 'more_verbose'), (3, 'debug'), (4, 'connection'), (5, 'winrm'), ]), required=False, ) extra_vars = models.Field( type=types.Variables(), required=False, display=False, multiple=True, help_text='Extra variables used by Ansible in YAML or key=value ' 'format. Use @ to get YAML from a file.') job_tags = models.Field(required=False, display=False) force_handlers = models.Field(type=bool, required=False, display=False) skip_tags = models.Field(required=False, display=False) start_at_task = models.Field(required=False, display=False) timeout = models.Field( type=int, required=False, display=False, help_text= 'The amount of time (in seconds) to run before the task is canceled.') use_fact_cache = models.Field( type=bool, required=False, display=False, help_text='If enabled, Tower will act as an Ansible Fact Cache Plugin;' ' persisting facts at the end of a playbook run to the database' ' and caching facts for use by Ansible.') host_config_key = models.Field( required=False, display=False, help_text='Allow Provisioning Callbacks using this host config key') ask_diff_mode_on_launch = models.Field( type=bool, required=False, display=False, help_text='Ask diff mode on launch.') ask_variables_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for extra_vars on launch.') ask_limit_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for host limits on launch.') ask_tags_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job tags on launch.') ask_skip_tags_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for tags to skip on launch.') ask_job_type_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') ask_verbosity_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for verbosity on launch.') ask_inventory_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for inventory on launch.') ask_credential_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for machine credential on launch.') survey_enabled = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') become_enabled = models.Field(type=bool, required=False, display=False) diff_mode = models.Field( type=bool, required=False, display=False, help_text='If enabled, textual changes made to any templated files on' ' the host are shown in the standard output.') allow_simultaneous = models.Field(type=bool, required=False, display=False) survey_spec = models.Field( type=types.Variables(), required=False, display=False, help_text='On write commands, perform extra POST to the ' 'survey_spec endpoint.') def write(self, *args, **kwargs): # Provide a default value for job_type, but only in creation of JT if (kwargs.get('create_on_missing', False) and (not kwargs.get('job_type', None))): kwargs['job_type'] = 'run' return super(Resource, self).write(*args, **kwargs) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--label', type=types.Related('label')) def associate_label(self, job_template, label): """Associate an label with this job template. =====API DOCS===== Associate an label with this job template. :param job_template: The job template to associate to. :type job_template: str :param label: The label to be associated. :type label: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('labels', job_template, label) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--label', type=types.Related('label')) def disassociate_label(self, job_template, label): """Disassociate an label from this job template. =====API DOCS===== Disassociate an label from this job template. :param job_template: The job template to disassociate from. :type job_template: str :param label: The label to be disassociated. :type label: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('labels', job_template, label) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--credential', type=types.Related('credential')) def associate_credential(self, job_template, credential): """Associate a credential with this job template. =====API DOCS===== Associate a credential with this job template. :param job_template: The job template to associate to. :type job_template: str :param credential: The credential to be associated. :type credential: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('extra_credentials', job_template, credential) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--credential', type=types.Related('credential')) def disassociate_credential(self, job_template, credential): """Disassociate a credential with this job template. =====API DOCS===== Disassociate a credential from this job template. :param job_template: The job template to disassociate fom. :type job_template: str :param credential: The credential to be disassociated. :type credential: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('extra_credentials', job_template, credential) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def associate_notification_template(self, job_template, notification_template, status): """Associate a notification template from this job template. =====API DOCS===== Associate a notification template from this job template. :param job_template: The job template to associate to. :type job_template: str :param notification_template: The notification template to be associated. :type notification_template: str :param status: type of notification this notification template should be associated to. :type status: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('notification_templates_%s' % status, job_template, notification_template) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def disassociate_notification_template(self, job_template, notification_template, status): """Disassociate a notification template from this job template. =====API DOCS===== Disassociate a notification template from this job template. :param job_template: The job template to disassociate from. :type job_template: str :param notification_template: The notification template to be disassociated. :type notification_template: str :param status: type of notification this notification template should be disassociated from. :type status: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('notification_templates_%s' % status, job_template, notification_template) @resources.command(use_fields_as_options=('extra_vars')) @click.option('--host-config-key', help='Job-template-specific string used to authenticate ' 'host during provisioning callback.') def callback(self, pk=None, host_config_key='', extra_vars=None): """Contact Tower and request a configuration update using this job template. =====API DOCS===== Contact Tower and request a provisioning callback using this job template. :param pk: Primary key of the job template to run provisioning callback against. :type pk: int :param host_config_key: Key string used to authenticate the callback host. :type host_config_key: str :param extra_vars: Extra variables that are passed to provisioning callback. :type extra_vars: array of str :returns: A dictionary of a single key "changed", which indicates whether the provisioning callback is successful. :rtype: dict =====API DOCS===== """ url = self.endpoint + '%s/callback/' % pk if not host_config_key: host_config_key = client.get(url).json()['host_config_key'] post_data = {'host_config_key': host_config_key} if extra_vars: post_data['extra_vars'] = parser.process_extra_vars( list(extra_vars), force_json=True) r = client.post(url, data=post_data, auth=None) if r.status_code == 201: return {'changed': True} @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def associate_ig(self, job_template, instance_group): """Associate an instance group with this job_template. The instance group will be used to run jobs within the job_template. =====API DOCS===== Associate an instance group with this job_template. :param job_template: Primary key or name of the job_template to associate to. :type job_template: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('instance_groups', job_template, instance_group) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def disassociate_ig(self, job_template, instance_group): """Disassociate an instance group from this job_template. =====API DOCS===== Disassociate an instance group with this job_template. :param job_template: Primary key or name of the job_template to associate to. :type job_template: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('instance_groups', job_template, instance_group)
class Resource(models.Resource): """A resource for 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) 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 Resource(models.Resource): """A resource for inventories.""" cli_help = 'Manage inventory within Ansible Tower.' endpoint = '/inventories/' identity = ('organization', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) organization = models.Field(type=types.Related('organization')) variables = models.Field( type=types.Variables(), required=False, display=False, help_text='Inventory variables, use "@" to get from file.') kind = models.Field( type=click.Choice(['', 'smart']), required=False, display=False, help_text='The kind field. Cannot be modified after created.') host_filter = models.Field( required=False, display=False, help_text='The host_filter field. Only useful when kind=smart.') insights_credential = models.Field(display=False, required=False, type=types.Related('credential')) @resources.command(ignore_defaults=True) def batch_update(self, pk=None, **kwargs): """Update all related inventory sources of the given inventory. Note global option --format is not available here, as the output would always be JSON-formatted. =====API DOCS===== Update all related inventory sources of the given inventory. :param pk: Primary key of the given inventory. :type pk: int :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object of update status of the given inventory. :rtype: dict =====API DOCS===== """ res = self.get(pk=pk, **kwargs) url = self.endpoint + '%d/%s/' % (res['id'], 'update_inventory_sources') return client.post(url, data={}).json() batch_update.format_freezer = 'json' @resources.command(use_fields_as_options=False) @click.option('--inventory', type=types.Related('inventory'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def associate_ig(self, inventory, instance_group): """Associate an instance group with this inventory. The instance group will be used to run jobs within the inventory. =====API DOCS===== Associate an instance group with this inventory. :param inventory: Primary key or name of the inventory to associate to. :type inventory: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('instance_groups', inventory, instance_group) @resources.command(use_fields_as_options=False) @click.option('--inventory', type=types.Related('inventory'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def disassociate_ig(self, inventory, instance_group): """Disassociate an instance group from this inventory. =====API DOCS===== Disassociate an instance group with this inventory. :param inventory: Primary key or name of the inventory to associate to. :type inventory: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('instance_groups', inventory, instance_group)
class Resource(models.SurveyResource): cli_help = 'Manage job templates.' endpoint = '/job_templates/' name = models.Field(unique=True) description = models.Field(required=False, display=False) job_type = models.Field( required=False, display=False, type=click.Choice(['run', 'check', 'scan']), ) inventory = models.Field(type=types.Related('inventory'), required=False) project = models.Field(type=types.Related('project')) playbook = models.Field() machine_credential = models.Field( 'credential', display=False, required=False, type=types.Related('credential'), ) cloud_credential = models.Field(type=types.Related('credential'), required=False, display=False) network_credential = models.Field(type=types.Related('credential'), required=False, display=False) forks = models.Field(type=int, required=False, display=False) limit = models.Field(required=False, display=False) verbosity = models.Field( display=False, type=types.MappedChoice([ (0, 'default'), (1, 'verbose'), (2, 'more_verbose'), (3, 'debug'), (4, 'connection'), (5, 'winrm'), ]), required=False, ) job_tags = models.Field(required=False, display=False) skip_tags = models.Field(required=False, display=False) extra_vars = models.Field( type=types.Variables(), required=False, display=False, multiple=True, help_text='Extra variables used by Ansible in YAML or key=value ' 'format. Use @ to get YAML from a file.') host_config_key = models.Field( required=False, display=False, help_text='Allow Provisioning Callbacks using this host config key') ask_variables_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for extra_vars on launch.') ask_limit_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for host limits on launch.') ask_tags_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job tags on launch.') ask_skip_tags_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for tags to skip on launch.') ask_job_type_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') ask_inventory_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for inventory on launch.') ask_credential_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for machine credential on launch.') become_enabled = models.Field(type=bool, required=False, display=False) allow_simultaneous = models.Field(type=bool, required=False, display=False) timeout = models.Field(type=int, required=False, display=False, help_text='The timeout field (in seconds).') survey_enabled = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') survey_spec = models.Field( type=types.Variables(), required=False, display=False, help_text='On write commands, perform extra POST to the ' 'survey_spec endpoint.') def write(self, *args, **kwargs): # Provide a default value for job_type, but only in creation of JT if (kwargs.get('create_on_missing', False) and (not kwargs.get('job_type', None))): kwargs['job_type'] = 'run' return super(Resource, self).write(*args, **kwargs) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--label', type=types.Related('label')) def associate_label(self, job_template, label): """Associate an label with this job template.""" return self._assoc('labels', job_template, label) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--label', type=types.Related('label')) def disassociate_label(self, job_template, label): """Disassociate an label from this job template.""" return self._disassoc('labels', job_template, label) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def associate_notification_template(self, job_template, notification_template, status): """Associate a notification template from this job template.""" return self._assoc('notification_templates_%s' % status, job_template, notification_template) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def disassociate_notification_template(self, job_template, notification_template, status): """Disassociate a notification template from this job template.""" return self._disassoc('notification_templates_%s' % status, job_template, notification_template) @resources.command(use_fields_as_options=('extra_vars')) @click.option('--host-config-key', help='Job-template-specific string used to authenticate ' 'host during provisioning callback.') def callback(self, pk=None, host_config_key='', extra_vars=None): """Contact Tower and request a configuration update using this job template.""" url = self.endpoint + '%s/callback/' % pk if not host_config_key: host_config_key = client.get(url).json()['host_config_key'] post_data = {'host_config_key': host_config_key} if extra_vars: post_data['extra_vars'] = parser.process_extra_vars(list(extra_vars), force_json=True) r = client.post(url, data=post_data, auth=None) if r.status_code == 201: return {'changed': True}
class Resource(models.Resource): """A resource for groups.""" cli_help = 'Manage groups belonging to an inventory.' endpoint = '/groups/' identity = ('inventory', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) inventory = models.Field(type=types.Related('inventory')) variables = models.Field( type=types.Variables(), required=False, display=False, help_text='Group variables, use "@" to get from file.') def lookup_with_inventory(self, group, inventory=None): group_res = get_resource('group') if isinstance(group, int) or group.isdigit(): return group_res.get(int(group)) else: return group_res.get(name=group, inventory=inventory) def set_child_endpoint(self, parent, inventory=None): parent_data = self.lookup_with_inventory(parent, inventory) self.endpoint = '/groups/' + str(parent_data['id']) + '/children/' return parent_data @resources.command @click.option('--parent', help='Parent group to nest this one inside of.') def create(self, fail_on_found=False, force_on_exists=False, **kwargs): """Create a group. =====API DOCS===== Create a group. :param parent: Primary key or name of the group which will be the parent of created group. :type parent: 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.UsageError: When inventory is not provided in ``**kwargs`` and ``parent`` is not provided. =====API DOCS===== """ if kwargs.get('parent', None): parent_data = self.set_child_endpoint(parent=kwargs['parent'], inventory=kwargs.get( 'inventory', None)) kwargs['inventory'] = parent_data['inventory'] elif 'inventory' not in kwargs: raise exc.UsageError( 'To create a group, you must provide a parent inventory or parent group.' ) return super(Resource, self).create(fail_on_found=fail_on_found, force_on_exists=force_on_exists, **kwargs) @resources.command(ignore_defaults=True, no_args_is_help=False) @click.option( '--root', is_flag=True, default=False, help= 'Show only root groups (groups with no parent groups) within the given inventory.' ) @click.option('--parent', help='Parent group to nest this one inside of.') def list(self, root=False, **kwargs): """Return a list of groups. =====API DOCS===== Retrieve a list of groups. :param root: Flag that if set, only root groups of a specific inventory will be listed. :type root: bool :param parent: Primary key or name of the group whose child groups will be listed. :type parent: str :param all_pages: Flag that if set, collect all pages of content from the API when returning results. :type all_pages: bool :param page: The page to show. Ignored if all_pages is set. :type page: int :param query: Contains 2-tuples used as query parameters to filter resulting resource objects. :type query: list :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict :raises tower_cli.exceptions.UsageError: When ``root`` flag is on and ``inventory`` is not present in ``**kwargs``. =====API DOCS===== """ # Option to list children of a parent group if kwargs.get('parent', None): self.set_child_endpoint(parent=kwargs['parent'], inventory=kwargs.get('inventory', None)) kwargs.pop('parent') # Sanity check: If we got `--root` and no inventory, that's an error. if root and not kwargs.get('inventory', None): raise exc.UsageError( 'The --root option requires specifying an inventory also.') # If we are tasked with getting root groups, do that. if root: inventory_id = kwargs['inventory'] r = client.get('/inventories/%d/root_groups/' % inventory_id) return r.json() # Return the superclass implementation. return super(Resource, self).list(**kwargs) @resources.command(use_fields_as_options=False) @click.option('--group', help='The group to move.') @click.option('--parent', help='Destination group to move into.') @click.option('--inventory', type=types.Related('inventory')) def associate(self, group, parent, **kwargs): """Associate this group with the specified group. =====API DOCS===== Associate this group with the specified group. :param group: Primary key or name of the child group to associate. :type group: str :param parent: Primary key or name of the parent group to associate to. :type parent: str :param inventory: Primary key or name of the inventory the association should happen in. :type inventory: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ parent_id = self.lookup_with_inventory(parent, kwargs.get('inventory', None))['id'] group_id = self.lookup_with_inventory(group, kwargs.get('inventory', None))['id'] return self._assoc('children', parent_id, group_id) @resources.command(use_fields_as_options=False) @click.option('--group', help='The group to move.') @click.option('--parent', help='Destination group to move into.') @click.option('--inventory', type=types.Related('inventory')) def disassociate(self, group, parent, **kwargs): """Disassociate this group from the specified group. =====API DOCS===== Disassociate this group with the specified group. :param group: Primary key or name of the child group to disassociate. :type group: str :param parent: Primary key or name of the parent group to disassociate from. :type parent: str :param inventory: Primary key or name of the inventory the disassociation should happen in. :type inventory: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ parent_id = self.lookup_with_inventory(parent, kwargs.get('inventory', None))['id'] group_id = self.lookup_with_inventory(group, kwargs.get('inventory', None))['id'] return self._disassoc('children', parent_id, group_id)