Esempio n. 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'
class Resource(models.Resource):
    """A resource for credentials."""
    cli_help = 'Manage credentials within Ansible Tower.'
    endpoint = '/credentials/'
    identity = ('organization', 'user', 'team', 'name')
    dependencies = ['organization', 'credential_type']

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)

    # Who owns this credential?
    user = models.Field(display=False,
                        type=types.Related('user'),
                        required=False,
                        no_lookup=True)
    team = models.Field(display=False,
                        type=types.Related('team'),
                        required=False,
                        no_lookup=True)
    organization = models.Field(display=False,
                                type=types.Related('organization'),
                                required=False)

    credential_type = models.Field(type=types.Related('credential_type'))
    inputs = models.Field(type=types.StructuredInput(),
                          required=False,
                          display=False)
class Resource(models.BaseResource):
    """A resource for job events."""
    cli_help = 'View events from jobs.'
    endpoint = '/job_events/'
    internal = True

    job = models.Field(type=types.Related('job'), display=True)
    host = models.Field(type=types.Related('host'), display=True)
    parent = models.Field(type=types.Related('job_event'), display=False)
    event = models.Field()
    playbook = models.Field()
    play = models.Field()
    task = models.Field()
    role = models.Field()
    counter = models.Field(display=False)
    event_level = models.Field(display=False)
    event_data = models.Field(display=False)
    failed = models.Field(display=False, type=bool)
    changed = models.Field(type=bool)
    verbosity = models.Field(display=False)

    def __getattribute__(self, attr):
        if attr == 'delete':
            raise AttributeError
        return super(Resource, self).__getattribute__(attr)
Esempio n. 4
0
def unified_job_template_options(method):
    """
    Adds the decorators for all types of unified job templates,
    and if the non-unified type is specified, converts it into the
    unified_job_template kwarg.
    """
    jt_dec = click.option(
        '--job-template',
        type=types.Related('job_template'),
        help='Use this job template as unified_job_template field')
    prj_dec = click.option(
        '--project',
        type=types.Related('project'),
        help='Use this project as unified_job_template field')
    inv_src_dec = click.option(
        '--inventory-source',
        type=types.Related('inventory_source'),
        help='Use this inventory source as unified_job_template field')

    def ujt_translation(_method):
        def _ujt_translation(*args, **kwargs):
            for fd in ['job_template', 'project', 'inventory_source']:
                if fd in kwargs and kwargs[fd] is not None:
                    kwargs['unified_job_template'] = kwargs.pop(fd)
            return _method(*args, **kwargs)

        return functools.wraps(_method)(_ujt_translation)

    return ujt_translation(inv_src_dec(prj_dec(jt_dec(method))))
Esempio n. 5
0
    def _produce_method(self, disassociate=False):

        method = self._produce_raw_method()

        # Apply options for user to specify the 2 resources to associate
        method = click.option(
            '--{}'.format(self.other_name),
            type=types.Related(self.other_name),
            required=True
        )(method)
        method = click.option(
            '--{}'.format(self.res_name),
            type=types.Related(self.res_name),
            required=True
        )(method)

        # This does the same thing as @resources.command, but without importing
        method._cli_command = True
        method._cli_command_attrs = dict(use_fields_as_options=False)

        # Define field-specific parameters that control functionality
        method._relationship = self.relationship
        method._res_name = self.res_name
        method._other_name = self.other_name
        if disassociate:
            method._internal_name = '_disassoc'
            method.__doc__ = self._produce_doc(action='disassociate')
        else:
            method._internal_name = '_assoc'
            method.__doc__ = self._produce_doc()
        return method
Esempio n. 6
0
class Resource(models.Resource):
    """A resource for teams."""
    cli_help = 'Manage teams within Ansible Tower.'
    endpoint = '/teams/'
    identity = ('organization', 'name')

    name = models.Field(unique=True)
    organization = models.Field(type=types.Related('organization'))
    description = models.Field(required=False, display=False)

    @resources.command(use_fields_as_options=False)
    @click.option('--team', type=types.Related('team'))
    @click.option('--user', type=types.Related('user'))
    def associate(self, team, user):
        """Associate a user with this team.

        =====API DOCS=====
        Associate a user with this team.

        :param team: Primary key or name of the team to associate to.
        :type team: str
        :param user: Primary key or name of the user to be associated.
        :type user: str
        :returns: Dictionary of only one key "changed", which indicates whether the association succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._assoc('users', team, user)

    @resources.command(use_fields_as_options=False)
    @click.option('--team', type=types.Related('team'))
    @click.option('--user', type=types.Related('user'))
    def disassociate(self, team, user):
        """Disassociate a user from this team.

        =====API DOCS=====
        Disassociate a user from this team.

        :param organization: Primary key or name of the team to disassociate from.
        :type organization: str
        :param user: Primary key or name of the user to be disassociated.
        :type user: str
        :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded.
        :rtype: dict

        """
        return self._disassoc('users', team, user)
class Resource(models.BaseResource):
    """A resource for OAuth2 tokens."""
    cli_help = 'Manage OAuth2 tokens.'
    endpoint = '/tokens/'
    internal = True

    user = models.Field(type=types.Related('user'), required=True)
    application = models.Field(type=types.Related('application'),
                               required=True)

    created = models.Field(required=False)
    modified = models.Field(required=False)
    token = models.Field(required=False)
    refresh_token = models.Field(required=False)
    expires = models.Field(required=False)
    scope = models.Field(required=False)
class Resource(models.ExeResource):
    """A resource for project updates.
    """
    cli_help = 'Launch or monitor project updates.'
    endpoint = '/project_updates/'

    project = models.Field(key='-P',
                           type=types.Related('project'),
                           required=True,
                           display=True)
    name = models.Field(required=False, display=True, read_only=True)
    launch_type = models.Field(type=click.Choice(LAUNCH_TYPE_CHOICES),
                               read_only=True,
                               display=False)
    status = models.Field(type=click.Choice(STATUS_CHOICES), read_only=True)
    job_type = models.Field(type=click.Choice(['run', 'check']),
                            read_only=True)
    job_explanation = models.Field(required=False,
                                   display=False,
                                   read_only=True)
    created = models.Field(required=False, display=True, read_only=True)
    elapsed = models.Field(required=False,
                           display=True,
                           read_only=True,
                           type=float)
    scm_type = models.Field(type=types.MappedChoice([
        ('', 'manual'),
        ('git', 'git'),
        ('hg', 'hg'),
        ('svn', 'svn'),
        ('insights', 'insights'),
    ]),
                            display=False)
Esempio n. 9
0
class Resource(models.ExeResource):
    """A resource for inventory source updates.
    """
    cli_help = 'Launch or monitor inventory source updates.'
    endpoint = '/inventory_updates/'

    inventory_source = models.Field(key='-I',
                                    type=types.Related('inventory_source'),
                                    required=True,
                                    display=True)
    name = models.Field(required=False, display=True, read_only=True)
    launch_type = models.Field(type=click.Choice(LAUNCH_TYPE_CHOICES),
                               read_only=True,
                               display=True)
    status = models.Field(type=click.Choice(STATUS_CHOICES), read_only=True)
    job_explanation = models.Field(required=False,
                                   display=False,
                                   read_only=True)
    created = models.Field(required=False, display=True, read_only=True)
    elapsed = models.Field(required=False,
                           display=True,
                           read_only=True,
                           type=float)
    source = models.Field(type=click.Choice(INVENTORY_SOURCE_CHOICES),
                          display=True)
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'
Esempio n. 11
0
class Resource(models.Resource):
    """A resource for teams."""
    cli_help = 'Manage teams within Ansible Tower.'
    endpoint = '/teams/'
    identity = ('organization', 'name')

    name = models.Field(unique=True)
    organization = models.Field(type=types.Related('organization'))
    description = models.Field(required=False, display=False)

    users = models.ManyToManyField('user', method_name='')
Esempio n. 12
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.')
Esempio n. 13
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)
class Resource(models.Resource):
    """A resource for OAuth2 applications."""
    cli_help = 'Manage OAuth2 applications.'
    endpoint = '/applications/'

    user = models.Field(type=types.Related('user'), required=True)

    name = models.Field(unique=True)
    client_type = models.Field(required=True)
    redirect_uris = models.Field(required=False)
    authorization_grant_type = models.Field(required=True)
    skip_authorization = models.Field(required=False)
Esempio n. 15
0
class Resource(models.Resource):
    cli_help = 'Manage teams within Ansible Tower.'
    endpoint = '/teams/'
    identity = ('organization', 'name')

    name = models.Field(unique=True)
    organization = models.Field(type=types.Related('organization'))
    description = models.Field(required=False, display=False)

    @resources.command(use_fields_as_options=False)
    @click.option('--team', type=types.Related('team'))
    @click.option('--user', type=types.Related('user'))
    def associate(self, team, user):
        """Associate a user with this team."""
        return self._assoc('users', team, user)

    @resources.command(use_fields_as_options=False)
    @click.option('--team', type=types.Related('team'))
    @click.option('--user', type=types.Related('user'))
    def disassociate(self, team, user):
        """Disassociate a user from this team."""
        return self._disassoc('users', team, user)
Esempio n. 16
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)
Esempio n. 17
0
class Resource(models.Resource):
    """A resource for OAuth2 applications."""
    cli_help = 'Manage OAuth2 applications.'
    endpoint = '/applications/'
    dependencies = ['organization']

    name = models.Field(unique=True)
    client_type = models.Field(type=click.Choice(CLIENT_TYPES), required=True)
    redirect_uris = models.Field(required=False)
    authorization_grant_type = models.Field(type=click.Choice(GRANT_TYPES),
                                            required=True)
    skip_authorization = models.Field(type=click.BOOL, required=False)
    organization = models.Field(type=types.Related('organization'),
                                required=True)
class Resource(models.BaseResource):
    """A resource for activity stream.

    This resource is read-only.
    """
    cli_help = 'Activity on server.'
    endpoint = '/activity_stream/'

    operation = models.Field(display=True)
    # TODO: implement a datetime field for timestamp
    timestamp = models.Field(display=True)
    changes = models.Field(display=False)
    object1 = models.Field(display=True)
    object2 = models.Field(display=True)
    actor = models.Field(type=types.Related('user'))

    def __getattribute__(self, attr):
        if attr == 'delete':
            raise AttributeError
        return super(Resource, self).__getattribute__(attr)

    def list(self, *args, **kwargs):
        if ('order_by' not in kwargs
                and ('query' not in kwargs or not kwargs['query'])):
            kwargs['query'] = (('order_by', '-timestamp'), )
        return super(Resource, self).list(*args, **kwargs)

    @staticmethod
    def _promote_actor(d):
        if ('summary_fields' in d and 'actor' in d['summary_fields']
                and d['summary_fields']['actor']):
            d['actor'] = d['summary_fields']['actor']['username']
        else:
            d['actor'] = None

    def read(self, *args, **kwargs):
        '''
        Do extra processing so we can display the actor field as
        a top-level field
        '''
        if 'actor' in kwargs:
            kwargs['actor'] = kwargs.pop('actor')
        r = super(Resource, self).read(*args, **kwargs)
        if 'results' in r:
            for d in r['results']:
                self._promote_actor(d)
        else:
            self._promote_actor(d)
        return r
Esempio n. 19
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)
Esempio n. 20
0
class Resource(models.Resource):
    """A resource for credentials."""
    cli_help = 'Manage credentials within Ansible Tower.'
    endpoint = '/credentials/'
    identity = ('organization', 'user', 'team', 'name')

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)

    # Who owns this credential?
    user = models.Field(display=False,
                        type=types.Related('user'),
                        required=False)
    team = models.Field(display=False,
                        type=types.Related('team'),
                        required=False)
    organization = models.Field(display=False,
                                type=types.Related('organization'),
                                required=False)

    credential_type = models.Field(type=types.Related('credential_type'))
    inputs = models.Field(type=types.StructuredInput(),
                          required=False,
                          display=False)

    @resources.command
    def create(self, **kwargs):
        """Create a credential.

        Fields in the resource's `identity` tuple are used for a lookup;
        if a match is found, then no-op (unless `force_on_exists` is set) but
        do not fail (unless `fail_on_found` is set).

        =====API DOCS=====
        Create a credential.

        :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


        =====API DOCS=====
        """
        if (kwargs.get('user', False) or kwargs.get('team', False)
                or kwargs.get('organization', False)):
            debug.log('Checking Project API Details.', header='details')
            r = client.options('/credentials/')
            if 'organization' in r.json()['actions']['POST']:
                for i in range(len(self.fields)):
                    if self.fields[i].name in ('user', 'team'):
                        self.fields[i].no_lookup = True
        return super(Resource, self).create(**kwargs)
class Resource(models.ExeResource):
    """A resource for jobs.

    This resource has ordinary list and get methods,
    but it does not have create or modify.
    Instead of being created, a job is launched.
    """
    cli_help = 'Launch or monitor jobs.'
    endpoint = '/jobs/'

    job_template = models.Field(key='-J',
                                type=types.Related('job_template'),
                                required=False,
                                display=True)
    job_explanation = models.Field(required=False,
                                   display=False,
                                   read_only=True)
    created = models.Field(required=False, display=True)
    status = models.Field(required=False, display=True)
    elapsed = models.Field(required=False, display=True)

    @resources.command(use_fields_as_options=('job_template', ))
    @click.option('--monitor',
                  is_flag=True,
                  default=False,
                  help='If sent, immediately calls `job monitor` on the newly '
                  'launched job rather than exiting with a success.')
    @click.option('--wait',
                  is_flag=True,
                  default=False,
                  help='Monitor the status of the job, but do not print '
                  'while job is in progress.')
    @click.option('--timeout',
                  required=False,
                  type=int,
                  help='If provided with --monitor, this command (not the job)'
                  ' will time out after the given number of seconds. '
                  'Does nothing if --monitor is not sent.')
    @click.option('--no-input',
                  is_flag=True,
                  default=False,
                  help='Suppress any requests for input.')
    @click.option('-e',
                  '--extra-vars',
                  required=False,
                  multiple=True,
                  help='yaml format text that contains extra variables '
                  'to pass on. Use @ to get these from a file.')
    @click.option('--diff-mode',
                  type=bool,
                  required=False,
                  help='Specify diff mode for job template to run.')
    @click.option('--limit',
                  required=False,
                  help='Specify host limit for job template to run.')
    @click.option('--tags',
                  required=False,
                  help='Specify tagged actions in the playbook to run.')
    @click.option('--skip-tags',
                  required=False,
                  help='Specify tagged actions in the playbook to omit.')
    @click.option('--job-type',
                  required=False,
                  type=click.Choice(['run', 'check']),
                  help='Specify job type for job template to run.')
    @click.option('--verbosity',
                  type=int,
                  required=False,
                  help='Specify verbosity of the playbook run.')
    @click.option('--inventory',
                  required=False,
                  type=types.Related('inventory'),
                  help='Specify inventory for job template to run.')
    @click.option('--credential',
                  required=False,
                  type=types.Related('credential'),
                  help='Specify machine credential for job template to run.')
    def launch(self,
               job_template=None,
               monitor=False,
               wait=False,
               timeout=None,
               no_input=True,
               extra_vars=None,
               **kwargs):
        """Launch a new job based on a job template.

        Creates a new job in Ansible Tower, immediately starts it, and
        returns back an ID in order for its status to be monitored.

        =====API DOCS=====
        Launch a new job based on a job template.

        :param job_template: Primary key or name of the job template to launch new job.
        :type job_template: str
        :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched job rather
                        than exiting with a success.
        :type monitor: bool
        :param wait: Flag that if set, monitor the status of the job, but do not print while job is in progress.
        :type wait: bool
        :param timeout: If provided with ``monitor`` flag set, this attempt will time out after the given number
                        of seconds.
        :type timeout: int
        :param no_input: Flag that if set, suppress any requests for input.
        :type no_input: bool
        :param extra_vars: yaml formatted texts that contains extra variables to pass on.
        :type extra_vars: array of strings
        :param diff_mode: Specify diff mode for job template to run.
        :type diff_mode: bool
        :param limit: Specify host limit for job template to run.
        :type limit: str
        :param tags: Specify tagged actions in the playbook to run.
        :type tags: str
        :param skip_tags: Specify tagged actions in the playbook to omit.
        :type skip_tags: str
        :param job_type: Specify job type for job template to run.
        :type job_type: str
        :param verbosity: Specify verbosity of the playbook run.
        :type verbosity: int
        :param inventory: Specify machine credential for job template to run.
        :type inventory: str
        :param credential: Specify machine credential for job template to run.
        :type credential: str
        :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent
                  ``wait`` call if ``wait`` flag is on; Result of subsequent ``status`` call if none of
                  the two flags are on.
        :rtype: dict

        =====API DOCS=====
        """
        # Get the job template from Ansible Tower.
        # This is used as the baseline for starting the job.

        tags = kwargs.get('tags', None)
        jt_resource = get_resource('job_template')
        jt = jt_resource.get(job_template)

        # Update the job data by adding an automatically-generated job name,
        # and removing the ID.
        data = {}
        if tags:
            data['job_tags'] = tags

        # Initialize an extra_vars list that starts with the job template
        # preferences first, if they exist
        extra_vars_list = []
        if 'extra_vars' in data and len(data['extra_vars']) > 0:
            # But only do this for versions before 2.3
            debug.log('Getting version of Tower.', header='details')
            r = client.get('/config/')
            if LooseVersion(r.json()['version']) < LooseVersion('2.4'):
                extra_vars_list = [data['extra_vars']]

        # Add the runtime extra_vars to this list
        if extra_vars:
            extra_vars_list += list(extra_vars)  # accept tuples

        # If the job template requires prompting for extra variables,
        # do so (unless --no-input is set).
        if jt.get('ask_variables_on_launch', False) and not no_input \
                and not extra_vars:
            # If JT extra_vars are JSON, echo them to user as YAML
            initial = parser.process_extra_vars([jt['extra_vars']],
                                                force_json=False)
            initial = '\n'.join((
                '# Specify extra variables (if any) here as YAML.',
                '# Lines beginning with "#" denote comments.',
                initial,
            ))
            extra_vars = click.edit(initial) or ''
            if extra_vars != initial:
                extra_vars_list = [extra_vars]

        # Data is starting out with JT variables, and we only want to
        # include extra_vars that come from the algorithm here.
        data.pop('extra_vars', None)

        # Replace/populate data fields if prompted.
        modified = set()
        for resource in PROMPT_LIST:
            if jt.pop('ask_' + resource + '_on_launch',
                      False) and not no_input:
                resource_object = kwargs.get(resource, None)
                if type(resource_object) == types.Related:
                    resource_class = get_resource(resource)
                    resource_object = resource_class.get(resource).pop(
                        'id', None)
                if resource_object is None:
                    debug.log('{0} is asked at launch but not provided'.format(
                        resource),
                              header='warning')
                elif resource != 'tags':
                    data[resource] = resource_object
                    modified.add(resource)

        # Dump extra_vars into JSON string for launching job
        if len(extra_vars_list) > 0:
            data['extra_vars'] = parser.process_extra_vars(extra_vars_list,
                                                           force_json=True)

        # Create the new job in Ansible Tower.
        start_data = {}
        endpoint = '/job_templates/%d/launch/' % jt['id']
        if 'extra_vars' in data and len(data['extra_vars']) > 0:
            start_data['extra_vars'] = data['extra_vars']
        if tags:
            start_data['job_tags'] = data['job_tags']
        for resource in PROMPT_LIST:
            if resource in modified:
                start_data[resource] = data[resource]

        # There's a non-trivial chance that we are going to need some
        # additional information to start the job; in particular, many jobs
        # rely on passwords entered at run-time.
        #
        # If there are any such passwords on this job, ask for them now.
        debug.log('Asking for information necessary to start the job.',
                  header='details')
        job_start_info = client.get(endpoint).json()
        for password in job_start_info.get('passwords_needed_to_start', []):
            start_data[password] = getpass('Password for %s: ' % password)

        # Actually start the job.
        debug.log('Launching the job.', header='details')
        self._pop_none(kwargs)
        kwargs.update(start_data)
        job_started = client.post(endpoint, data=kwargs)

        # Get the job ID from the result.
        job_id = job_started.json()['id']

        # If returning json indicates any ignored fields, display it in
        # verbose mode.
        if job_started.text == '':
            ignored_fields = {}
        else:
            ignored_fields = job_started.json().get('ignored_fields', {})
        has_ignored_fields = False
        for key, value in ignored_fields.items():
            if value and value != '{}':
                if not has_ignored_fields:
                    debug.log('List of ignored fields on the server side:',
                              header='detail')
                    has_ignored_fields = True
                debug.log('{0}: {1}'.format(key, value))

        # Get some information about the running job to print
        result = self.status(pk=job_id, detail=True)
        result['changed'] = True

        # If we were told to monitor the job once it started, then call
        # monitor from here.
        if monitor:
            return self.monitor(job_id, timeout=timeout)
        elif wait:
            return self.wait(job_id, timeout=timeout)

        return result
Esempio n. 22
0
class Resource(models.Resource):
    cli_help = 'Manage organizations within Ansible Tower.'
    endpoint = '/organizations/'
    deprecated_methods = ['associate_project', 'disassociate_project']

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--user', type=types.Related('user'), required=True)
    def associate(self, organization, user):
        """Associate a user with this organization.

        =====API DOCS=====
        Associate a user with this organization.

        :param organization: Primary key or name of the organization to associate to.
        :type organization: str
        :param user: Primary key or name of the user to be associated.
        :type user: str
        :returns: Dictionary of only one key "changed", which indicates whether the association succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._assoc('users', organization, user)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--user', type=types.Related('user'), required=True)
    def associate_admin(self, organization, user):
        """Associate an admin with this organization.

        =====API DOCS=====
        Associate an admin with this organization.

        :param organization: Primary key or name of the organization to associate to.
        :type organization: str
        :param user: Primary key or name of the user to be associated.
        :type user: str
        :returns: Dictionary of only one key "changed", which indicates whether the association succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._assoc('admins', organization, user)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--user', type=types.Related('user'), required=True)
    def disassociate(self, organization, user):
        """Disassociate a user from this organization.

        =====API DOCS=====
        Disassociate a user from this organization.

        :param organization: Primary key or name of the organization to disassociate from.
        :type organization: str
        :param user: Primary key or name of the user to be disassociated.
        :type user: str
        :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._disassoc('users', organization, user)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--user', type=types.Related('user'), required=True)
    def disassociate_admin(self, organization, user):
        """Disassociate an admin from this organization.

        =====API DOCS=====
        Disassociate an admin from this organization.

        :param organization: Primary key or name of the organization to disassociate from.
        :type organization: str
        :param user: Primary key or name of the user to be disassociated.
        :type user: str
        :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._disassoc('admins', organization, user)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--instance-group',
                  type=types.Related('instance_group'),
                  required=True)
    def associate_ig(self, organization, instance_group):
        """Associate an instance group with this organization.
        The instance group will be used to run jobs within the organization.

        =====API DOCS=====
        Associate an instance group with this organization.

        :param organization: Primary key or name of the organization to associate to.
        :type organization: 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', organization, instance_group)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--instance-group',
                  type=types.Related('instance_group'),
                  required=True)
    def disassociate_ig(self, organization, instance_group):
        """Disassociate an instance group from this organization.

        =====API DOCS=====
        Disassociate an instance group with this organization.

        :param organization: Primary key or name of the organization to associate to.
        :type organization: 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', organization, instance_group)
Esempio n. 23
0
class Resource(models.Resource):
    """A resource for labels."""
    cli_help = 'Manage labels within Ansible Tower.'
    endpoint = '/labels/'

    name = models.Field(unique=True)
    organization = models.Field(type=types.Related('organization'), display=False)

    def __getattribute__(self, name):
        """Disable inherited methods that cannot be applied to this particular resource.
        """
        if name in ['delete']:
            raise AttributeError
        else:
            return object.__getattribute__(self, name)

    @resources.command
    @click.option('--job-template', type=types.Related('job_template'),
                  required=False, help='The job template to relate to.')
    def create(self, fail_on_found=False, force_on_exists=False, **kwargs):
        """Create a new label.

        There are two types of label creation: isolatedly creating a new label and creating a new label under
        a job template. Here the two types are discriminated by whether to provide --job-template option.

        Fields in the resource's `identity` tuple are used for a lookup; if a match is found, then no-op (unless
        `force_on_exists` is set) but do not fail (unless `fail_on_found` is set).

        =====API DOCS=====
        Create a label.

        :param job_template: Primary key or name of the job template for the created label to associate to.
        :type job_template: str
        :param fail_on_found: Flag that if set, the operation fails if an object matching the unique criteria
                              already exists.
        :type fail_on_found: bool
        :param force_on_exists: Flag that if set, then if a match is found on unique fields, other fields will
                                be updated to the provided values.; If unset, a match causes the request to be
                                a no-op.
        :type force_on_exists: bool
        :param `**kwargs`: Keyword arguments which, all together, will be used as POST body to create the
                           resource object.
        :returns: A dictionary combining the JSON output of the created resource, as well as two extra fields:
                  "changed", a flag indicating if the resource is created successfully; "id", an integer which
                  is the primary key of the created object.
        :rtype: dict
        :raises tower_cli.exceptions.TowerCLIError: When the label already exists and ``fail_on_found`` flag is on.

        =====API DOCS=====
        """
        jt_id = kwargs.pop('job_template', None)
        old_endpoint = self.endpoint
        if jt_id is not None:
            jt = get_resource('job_template')
            jt.get(pk=jt_id)
            try:
                label_id = self.get(name=kwargs.get('name', None), organization=kwargs.get('organization', None))['id']
            except exc.NotFound:
                pass
            else:
                if fail_on_found:
                    raise exc.TowerCLIError('Label already exists and fail-on-found is switched on. Please use'
                                            ' "associate_label" method of job_template instead.')
                else:
                    debug.log('Label already exists, associating with job template.', header='details')
                    return jt.associate_label(job_template=jt_id, label=label_id)
            self.endpoint = '/job_templates/%d/labels/' % jt_id
        result = super(Resource, self).create(fail_on_found=fail_on_found, force_on_exists=force_on_exists, **kwargs)
        self.endpoint = old_endpoint
        return result
Esempio n. 24
0
class Resource(models.Resource):
    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)
    inventory = models.Field(type=types.Related('inventory'),
                             required=False,
                             display=False)
    credential = models.Field(type=types.Related('credential'),
                              required=False,
                              display=False)
    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)

    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."""
        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."""
        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."""
        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."""
        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."""
        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):
        """Add a node to always run after the parent is finished.
        The resulatant 2 nodes will both become root nodes."""
        return self._disassoc(self._forward_rel_name('always'), parent, child)
Esempio n. 25
0
class Resource(models.ExeResource):
    """A resource for ad hoc commands."""
    cli_help = 'Launch commands based on playbook given at runtime.'
    endpoint = '/ad_hoc_commands/'

    # Parameters similar to job
    job_explanation = models.Field(required=False, display=False)
    created = models.Field(required=False, display=True)
    status = models.Field(required=False, display=True)
    elapsed = models.Field(required=False, display=True, type=float)

    # Parameters similar to job_template
    job_type = models.Field(
        default='run',
        display=False,
        show_default=True,
        type=click.Choice(['run', 'check']),
    )
    inventory = models.Field(type=types.Related('inventory'))
    limit = models.Field(required=False, display=False)
    credential = models.Field(display=False, type=types.Related('credential'))
    module_name = models.Field(required=False, display=True, default="command", show_default=True)
    module_args = models.Field(required=False, display=False)
    forks = models.Field(type=int, required=False, display=False)
    verbosity = models.Field(
        display=False,
        type=types.MappedChoice([
            (0, 'default'),
            (1, 'verbose'),
            (2, 'more_verbose'),
            (3, 'debug'),
            (4, 'connection'),
            (5, 'winrm'),
        ]),
        required=False,
    )
    become_enabled = models.Field(type=bool, required=False, display=False)
    diff_mode = models.Field(type=bool, required=False, display=False)

    @resources.command(
        use_fields_as_options=(
            'job_explanation', 'job_type', 'inventory', 'credential', 'module_name', 'module_args', 'forks',
            'limit', 'verbosity', 'become_enabled', 'diff_mode',
        )
    )
    @click.option('--monitor', is_flag=True, default=False,
                  help='If sent, immediately calls `monitor` on the newly '
                       'launched command rather than exiting with a success.')
    @click.option('--wait', is_flag=True, default=False,
                  help='Monitor the status of the job, but do not print '
                       'while job is in progress.')
    @click.option('--timeout', required=False, type=int,
                  help='If provided with --monitor, this attempt'
                       ' will time out after the given number of seconds. '
                       'Does nothing if --monitor is not sent.')
    def launch(self, monitor=False, wait=False, timeout=None, **kwargs):
        """Launch a new ad-hoc command.

        Runs a user-defined command from Ansible Tower, immediately starts it,
        and returns back an ID in order for its status to be monitored.

        =====API DOCS=====
        Launch a new ad-hoc command.

        :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched command rather
                        than exiting with a success.
        :type monitor: bool
        :param wait: Flag that if set, monitor the status of the job, but do not print while job is in progress.
        :type wait: bool
        :param timeout: If provided with ``monitor`` flag set, this attempt will time out after the given number
                        of seconds.
        :type timeout: int
        :param `**kwargs`: Fields needed to create and launch an ad hoc command.
        :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait``
                  call if ``wait`` flag is on; dictionary of "id" and "changed" if none of the two flags are on.
        :rtype: dict
        :raises tower_cli.exceptions.TowerCLIError: When ad hoc commands are not available in Tower backend.

        =====API DOCS=====
        """
        # This feature only exists for versions 2.2 and up
        r = client.get('/')
        if 'ad_hoc_commands' not in r.json():
            raise exc.TowerCLIError('Your host is running an outdated version'
                                    'of Ansible Tower that can not run '
                                    'ad-hoc commands (2.2 or earlier)')

        # Pop the None arguments because we have no .write() method in
        # inheritance chain for this type of resource. This is needed
        self._pop_none(kwargs)

        # Actually start the command.
        debug.log('Launching the ad-hoc command.', header='details')
        result = client.post(self.endpoint, data=kwargs)
        command = result.json()
        command_id = command['id']

        # If we were told to monitor the command once it started, then call
        # monitor from here.
        if monitor:
            return self.monitor(command_id, timeout=timeout)
        elif wait:
            return self.wait(command_id, timeout=timeout)

        # Return the command ID and other response data
        answer = OrderedDict((
            ('changed', True),
            ('id', command_id),
        ))
        answer.update(result.json())
        return answer
Esempio n. 26
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)
Esempio n. 27
0
class Resource(models.Resource):
    cli_help = 'Manage organizations within Ansible Tower.'
    endpoint = '/organizations/'
    deprecated_methods = ['associate_project', 'disassociate_project']

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--user', type=types.Related('user'), required=True)
    def associate(self, organization, user):
        """Associate a user with this organization."""
        return self._assoc('users', organization, user)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--user', type=types.Related('user'), required=True)
    def associate_admin(self, organization, user):
        """Associate an admin with this organization."""
        return self._assoc('admins', organization, user)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--user', type=types.Related('user'), required=True)
    def disassociate(self, organization, user):
        """Disassociate a user from this organization."""
        return self._disassoc('users', organization, user)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--user', type=types.Related('user'), required=True)
    def disassociate_admin(self, organization, user):
        """Disassociate an admin from this organization."""
        return self._disassoc('admins', organization, user)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--project', type=types.Related('project'), required=True)
    def associate_project(self, organization, project):
        """Associate a project with this organization."""
        return self._assoc('projects', organization, project)

    @resources.command(use_fields_as_options=False)
    @click.option('--organization',
                  type=types.Related('organization'),
                  required=True)
    @click.option('--project', type=types.Related('project'), required=True)
    def disassociate_project(self, organization, project):
        """Disassociate a project from this organization."""
        return self._disassoc('projects', organization, project)
Esempio n. 28
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)
Esempio n. 29
0
class Resource(models.Resource):
    """A resource for managing roles.

    This resource has ordinary list and get methods,
    but it roles can not be created or edited, instead, they are
    automatically generated along with the connected resource.
    """
    cli_help = 'Add and remove users/teams from roles.'
    endpoint = '/roles/'

    user = models.Field(type=types.Related('user'),
                        required=False,
                        display=True)
    team = models.Field(type=types.Related('team'),
                        required=False,
                        display=True,
                        help_text='The team that receives the permissions '
                        'specified by the role.')
    type = models.Field(
        required=False,
        display=True,
        type=click.Choice(ROLE_TYPES),
        help_text='The type of permission that the role controls.')

    # These fields are never valid input arguments,
    # they are only used as columns in output
    resource_name = models.Field(required=False, display=False)
    resource_type = models.Field(required=False, display=False)

    # These are purely resource fields, and are always inputs,
    # but are only selectively set as output columns
    target_team = models.Field(type=types.Related('team'),
                               required=False,
                               display=False,
                               help_text='The team that the role acts on.')
    credential = models.Field(type=types.Related('credential'),
                              required=False,
                              display=False)
    inventory = models.Field(type=types.Related('inventory'),
                             required=False,
                             display=False)
    job_template = models.Field(type=types.Related('job_template'),
                                required=False,
                                display=False)
    credential = models.Field(type=types.Related('credential'),
                              required=False,
                              display=False)
    organization = models.Field(type=types.Related('organization'),
                                required=False,
                                display=False)
    project = models.Field(type=types.Related('project'),
                           required=False,
                           display=False)
    workflow = models.Field(type=types.Related('workflow'),
                            required=False,
                            display=False)

    def __getattribute__(self, name):
        """Disable inherited methods that cannot be applied to this
        particular resource.
        """
        if name in ['create', 'delete', 'modify']:
            raise AttributeError
        else:
            return object.__getattribute__(self, name)

    @staticmethod
    def pluralize(kind):
        if kind == 'inventory':
            return 'inventories'
        elif kind == 'workflow':
            return 'workflow_job_templates'
        else:
            return '%ss' % kind

    @staticmethod
    def obj_res(data, fail_on=['type', 'obj', 'res']):
        """
        Given some CLI input data,
        Returns the following and their types:
        obj - the role grantee
        res - the resource that the role applies to
        """
        errors = []
        if not data.get('type', None) and 'type' in fail_on:
            errors += ['You must provide a role type to use this command.']

        # Find the grantee, and remove them from resource_list
        obj = None
        obj_type = None
        for fd in ACTOR_FIELDS:
            if data.get(fd, False):
                if not obj:
                    obj = data[fd]
                    obj_type = fd
                else:
                    errors += [
                        'You can not give a role to a user '
                        'and team at the same time.'
                    ]
                    break
        if not obj and 'obj' in fail_on:
            errors += [
                'You must specify either user or '
                'team to use this command.'
            ]

        # Out of the resource list, pick out available valid resource field
        res = None
        res_type = None
        for fd in RESOURCE_FIELDS:
            if data.get(fd, False):
                if not res:
                    res = data[fd]
                    res_type = fd
                    if res_type == 'target_team':
                        res_type = 'team'
                else:
                    errors += [
                        'You can only give a role to one '
                        'type of resource at a time.'
                    ]
                    break
        if not res and 'res' in fail_on:
            errors += [
                'You must specify a target resource '
                'to use this command.'
            ]

        if errors:
            raise exc.UsageError("\n".join(errors))
        return obj, obj_type, res, res_type

    @classmethod
    def data_endpoint(cls, in_data, ignore=[]):
        """
        Converts a set of CLI input arguments, `in_data`, into
        request data and an endpoint that can be used to look
        up a role or list of roles.

        Also changes the format of `type` in data to what the server
        expects for the role model, as it exists in the database.
        """
        obj, obj_type, res, res_type = cls.obj_res(in_data, fail_on=[])
        data = {}
        if 'obj' in ignore:
            obj = None
        if 'res' in ignore:
            res = None
        # Input fields are not actually present on role model, and all have
        # to be managed as individual special-cases
        if obj and obj_type == 'user':
            data['members__in'] = obj
        if obj and obj_type == 'team':
            endpoint = '%s/%s/roles/' % (cls.pluralize(obj_type), obj)
            if res is not None:
                # For teams, this is the best lookup we can do
                #  without making the additional request for its member_role
                data['object_id'] = res
        elif res:
            endpoint = '%s/%s/object_roles/' % (cls.pluralize(res_type), res)
        else:
            endpoint = '/roles/'
        if in_data.get('type', False):
            data['role_field'] = '%s_role' % in_data['type'].lower()
        return data, endpoint

    @staticmethod
    def populate_resource_columns(item_dict):
        """Operates on item_dict

        Promotes the resource_name and resource_type fields to the
        top-level of the serialization so they can be printed as columns.
        Also makes a copies name field to type, which is a default column."""
        item_dict['type'] = item_dict['name']
        if len(item_dict['summary_fields']) == 0:
            # Singleton roles ommit these fields
            item_dict['resource_name'] = None
            item_dict['resource_type'] = None
        else:
            item_dict['resource_name'] = item_dict['summary_fields'][
                'resource_name']
            item_dict['resource_type'] = item_dict['summary_fields'][
                'resource_type']

    def set_display_columns(self, set_true=[], set_false=[]):
        """Add or remove columns from the output."""
        for i in range(len(self.fields)):
            if self.fields[i].name in set_true:
                self.fields[i].display = True
            elif self.fields[i].name in set_false:
                self.fields[i].display = False

    def configure_display(self, data, kwargs=None, write=False):
        """Populates columns and sets display attribute as needed.
        Operates on data."""
        if settings.format != 'human':
            return  # This is only used for human format
        if write:
            obj, obj_type, res, res_type = self.obj_res(kwargs)
            data['type'] = kwargs['type']
            data[obj_type] = obj
            data[res_type] = res
            self.set_display_columns(
                set_false=['team' if obj_type == 'user' else 'user'],
                set_true=['target_team' if res_type == 'team' else res_type])
        else:
            self.set_display_columns(
                set_false=['user', 'team'],
                set_true=['resource_name', 'resource_type'])
            if 'results' in data:
                for i in range(len(data['results'])):
                    self.populate_resource_columns(data['results'][i])
            else:
                self.populate_resource_columns(data)

    def role_write(self, fail_on_found=False, disassociate=False, **kwargs):
        """Re-implementation of the parent `write` method specific to roles.
        Adds a grantee (user or team) to the resource's role."""

        # Get the role, using only the resource data
        data, self.endpoint = self.data_endpoint(kwargs, ignore=['obj'])
        debug.log('Checking if role exists.', header='details')
        response = self.read(pk=None,
                             fail_on_no_results=True,
                             fail_on_multiple_results=True,
                             **data)
        role_data = response['results'][0]
        role_id = role_data['id']

        # Role exists, change display settings to output something
        self.configure_display(role_data, kwargs, write=True)

        # Check if user/team has this role
        # Implictly, force_on_exists is false for roles
        obj, obj_type, res, res_type = self.obj_res(kwargs)
        debug.log('Checking if %s already has role.' % obj_type,
                  header='details')
        data, self.endpoint = self.data_endpoint(kwargs)
        response = self.read(pk=None,
                             fail_on_no_results=False,
                             fail_on_multiple_results=False,
                             **data)

        msg = ''
        if response['count'] > 0 and not disassociate:
            msg = 'This %s is already a member of the role.' % obj_type
        elif response['count'] == 0 and disassociate:
            msg = 'This %s is already a non-member of the role.' % obj_type

        if msg:
            role_data['changed'] = False
            if fail_on_found:
                raise exc.NotFound(msg)
            else:
                debug.log(msg, header='DECISION')
                return role_data

        # Add or remove the user/team to the role
        debug.log('Attempting to %s the %s in this role.' %
                  ('remove' if disassociate else 'add', obj_type),
                  header='details')
        post_data = {'id': role_id}
        if disassociate:
            post_data['disassociate'] = True
        client.post('%s/%s/roles/' % (self.pluralize(obj_type), obj),
                    data=post_data)
        role_data['changed'] = True
        return role_data

    # Command method for roles
    # TODO: write commands to see access_list for resource
    @resources.command(use_fields_as_options=ACTOR_FIELDS + RESOURCE_FIELDS +
                       ['type'])
    def list(self, **kwargs):
        """Return a list of roles.

        =====API DOCS=====
        Retrieve a list of objects.

        :param all_pages: Flag that if set, collect all pages of content from the API when returning results.
        :type all_pages: bool
        :param page: The page to show. Ignored if all_pages is set.
        :type page: int
        :param query: Contains 2-tuples used as query parameters to filter resulting resource objects.
        :type query: list
        :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects.
        :returns: A JSON object containing details of all resource objects returned by Tower backend.
        :rtype: dict

        =====API DOCS=====
        """
        data, self.endpoint = self.data_endpoint(kwargs)
        r = super(Resource, self).list(**data)

        # Change display settings and data format for human consumption
        self.configure_display(r)
        return r

    @resources.command(use_fields_as_options=ACTOR_FIELDS + RESOURCE_FIELDS +
                       ['type'])
    def get(self, pk=None, **kwargs):
        """Get information about a role.

        =====API DOCS=====
        Retrieve one and exactly one object.

        :param pk: Primary key of the resource to be read. Tower CLI will only attempt to read *that* object
                   if ``pk`` is provided (not ``None``).
        :type pk: int
        :param `**kwargs`: Keyword arguments used to look up resource object to retrieve if ``pk`` is not provided.
        :returns: loaded JSON of the retrieved resource object.
        :rtype: dict

        =====API DOCS=====
        """
        if kwargs.pop('include_debug_header', True):
            debug.log('Getting the role record.', header='details')
        data, self.endpoint = self.data_endpoint(kwargs)
        response = self.read(pk=pk,
                             fail_on_no_results=True,
                             fail_on_multiple_results=True,
                             **data)
        item_dict = response['results'][0]
        self.configure_display(item_dict)
        return item_dict

    @resources.command(use_fields_as_options=ACTOR_FIELDS + RESOURCE_FIELDS +
                       ['type'])
    @click.option('--fail-on-found',
                  default=False,
                  show_default=True,
                  type=bool,
                  is_flag=True,
                  help='If used, return an error if the user already has the '
                  'role.')
    def grant(self, fail_on_found=False, **kwargs):
        """Add a user or a team to a role. Required information:
        1) Type of the role
        2) Resource of the role, inventory, credential, or any other
        3) A user or a team to add to the role

        =====API DOCS=====
        Add a user or a team to a role. Required information:
        * Type of the role.
        * Resource of the role, inventory, credential, or any other.
        * A user or a team to add to the role.

        :param fail_on_found: Flag that if set, the operation fails if a user/team already has the role.
        :type fail_on_found: bool
        :param `**kwargs`: The user to be associated and the role to associate.
        :returns: parsed JSON of role grant.
        :rtype: dict

        =====API DOCS=====
        """
        return self.role_write(fail_on_found=fail_on_found, **kwargs)

    @resources.command(use_fields_as_options=ACTOR_FIELDS + RESOURCE_FIELDS +
                       ['type'])
    @click.option('--fail-on-found',
                  default=False,
                  show_default=True,
                  type=bool,
                  is_flag=True,
                  help='If used, return an error if the user is already '
                  'not a member of the role.')
    def revoke(self, fail_on_found=False, **kwargs):
        """Remove a user or a team from a role. Required information:
        1) Type of the role
        2) Resource of the role, inventory, credential, or any other
        3) A user or a team to add to the role

        =====API DOCS=====
        Remove a user or a team from a role. Required information:
        * Type of the role.
        * Resource of the role, inventory, credential, or any other.
        * A user or a team to add to the role.

        :param fail_on_found: Flag that if set, the operation fails if a user/team dose not have the role.
        :type fail_on_found: bool
        :param `**kwargs`: The user to be disassociated and the role to disassociate.
        :returns: parsed JSON of role revoke.
        :rtype: dict

        =====API DOCS=====
        """
        return self.role_write(fail_on_found=fail_on_found,
                               disassociate=True,
                               **kwargs)
class Resource(models.MonitorableResource):
    cli_help = 'Manage inventory sources within Ansible Tower.'
    endpoint = '/inventory_sources/'
    internal = True
    unified_job_type = '/inventory_updates/'

    name = models.Field(unique=True)
    credential = models.Field(type=types.Related('credential'), required=False)
    source = models.Field(
        default=None,
        help_text='The type of inventory source in use.',
        type=click.Choice(['', 'file', 'ec2', 'rax', 'vmware',
                           'gce', 'azure', 'azure_rm', 'openstack',
                           'satellite6', 'cloudforms', 'custom']),
    )
    source_regions = models.Field(required=False, display=False)
    # Variables not shared by all cloud providers
    source_vars = models.Field(required=False, display=False)
    instance_filters = models.Field(required=False, display=False)
    group_by = models.Field(required=False, display=False)
    source_script = models.Field(type=types.Related('inventory_script'),
                                 required=False, display=False)
    # Boolean variables
    overwrite = models.Field(type=bool, required=False, display=False)
    overwrite_vars = models.Field(type=bool, required=False, display=False)
    update_on_launch = models.Field(type=bool, required=False, display=False)
    # Only used if update_on_launch is used
    update_cache_timeout = models.Field(type=int, required=False,
                                        display=False)
    timeout = models.Field(type=int, required=False, display=False,
                           help_text='The timeout field (in seconds).')

    def _is_full_v1_name(self, name):
        return bool(re.match(r'^.+\s\(.+\s-\s\d+\)$', name))

    def read(self, pk=None, fail_on_no_results=False,
             fail_on_multiple_results=False, **kwargs):
        # Special case to look up inventory sources by partial name
        # TODO: Remove with v1 deprecation
        if (kwargs.get('name', None) and not kwargs.get('group', None) and
                not self._is_full_v1_name(kwargs['name'])):
            kwargs.setdefault('query', [])
            kwargs['query'] += [('name__startswith', kwargs['name'])]
            kwargs.pop('name')
        return super(Resource, self).read(
            pk, fail_on_no_results=fail_on_no_results,
            fail_on_multiple_results=fail_on_multiple_results, **kwargs)

    @click.argument('inventory_source', type=types.Related('inventory_source'))
    @click.option('--monitor', is_flag=True, default=False,
                  help='If sent, immediately calls `monitor` on the newly '
                       'launched job rather than exiting with a success.')
    @click.option('--wait', is_flag=True, default=False,
                  help='Polls server for status, exists when finished.')
    @click.option('--timeout', required=False, type=int,
                  help='If provided with --monitor, this command (not the job)'
                       ' will time out after the given number of seconds. '
                       'Does nothing if --monitor is not sent.')
    @resources.command(use_fields_as_options=False, no_args_is_help=True)
    def update(self, inventory_source, monitor=False, wait=False,
               timeout=None, **kwargs):
        """Update the given inventory source."""

        # Establish that we are able to update this inventory source
        # at all.
        debug.log('Asking whether the inventory source can be updated.',
                  header='details')
        r = client.get('%s%d/update/' % (self.endpoint, inventory_source))
        if not r.json()['can_update']:
            raise exc.BadRequest('Tower says it cannot run an update against '
                                 'this inventory source.')

        # Run the update.
        debug.log('Updating the inventory source.', header='details')
        r = client.post('%s%d/update/' % (self.endpoint, inventory_source))

        # If we were told to monitor the project update's status, do so.
        if monitor or wait:
            inventory_update_id = r.json()['inventory_update']
            if monitor:
                result = self.monitor(
                    inventory_update_id, parent_pk=inventory_source,
                    timeout=timeout)
            elif wait:
                result = self.wait(
                    inventory_update_id, parent_pk=inventory_source,
                    timeout=timeout)
            inventory = client.get('/inventory_sources/%d/' %
                                   result['inventory_source'])\
                              .json()['inventory']
            result['inventory'] = int(inventory)
            return result

        # Done.
        return {'status': 'ok'}

    @resources.command
    @click.option('--detail', is_flag=True, default=False,
                  help='Print more detail.')
    def status(self, pk, detail=False, **kwargs):
        """Print the status of the most recent sync."""
        # Obtain the most recent inventory sync
        job = self.last_job_data(pk, **kwargs)

        # In most cases, we probably only want to know the status of the job
        # and the amount of time elapsed. However, if we were asked for
        # verbose information, provide it.
        if detail:
            return job

        # Print just the information we need.
        return {
            'elapsed': job['elapsed'],
            'failed': job['failed'],
            'status': job['status'],
        }