Beispiel #1
0
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')
Beispiel #3
0
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.')
Beispiel #4
0
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'
Beispiel #6
0
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)
Beispiel #7
0
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)
Beispiel #8
0
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)
Beispiel #9
0
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)
Beispiel #11
0
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)
Beispiel #12
0
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')
Beispiel #15
0
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
Beispiel #16
0
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)
Beispiel #18
0
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)
Beispiel #19
0
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}
Beispiel #20
0
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)