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 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): 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 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 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.')
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)
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.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.""" 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.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) 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.') @resources.command def create(self, fail_on_found=False, force_on_exists=False, extra_vars=None, **kwargs): """Create a job template.""" # 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(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.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.ExeResource): 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 = uj_res.as_command()._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`.""" 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. """ if len(extra_vars) > 0: kwargs['extra_vars'] = parser.process_extra_vars(extra_vars) debug.log('Launching the workflow job.', header='details') self._pop_none(kwargs) post_response = client.post( 'workflow_job_templates/{}/launch/'.format(workflow_job_template), data=kwargs).json() workflow_job_id = post_response['id'] post_response['changed'] = True if monitor: return self.monitor(workflow_job_id, timeout=timeout) elif wait: return self.wait(workflow_job_id, timeout=timeout) return post_response
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])