示例#1
0
class Resource(models.Resource):
    cli_help = 'Manage projects within Ansible Tower.'
    endpoint = '/projects/'

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)
    organization = models.Field(type=types.Related('organization'),
                                display=False)
    scm_type = models.Field(type=click.Choice(['manual', 'git', 'hg',
                                               'svn']), )
    scm_url = models.Field(required=False)
    scm_branch = models.Field(required=False, display=False)
    scm_credential = models.Field(
        'credential',
        display=False,
        required=False,
        type=types.Related('credential'),
    )
    scm_clean = models.Field(type=bool, required=False, display=False)
    scm_delete_on_update = models.Field(type=bool,
                                        required=False,
                                        display=False)
    scm_update_on_launch = models.Field(type=bool,
                                        required=False,
                                        display=False)
示例#2
0
class Resource(models.Resource):
    cli_help = 'Manage job templates.'
    endpoint = '/job_templates/'

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)
    job_type = models.Field(
        default='run',
        display=False,
        show_default=True,
        type=click.Choice(['run', 'check']),
    )
    inventory = models.Field(type=types.Related('inventory'))
    project = models.Field(type=types.Related('project'))
    playbook = models.Field()
    machine_credential = models.Field('credential',
        display=False,
        type=types.Related('credential'),
    )
    cloud_credential = models.Field(type=types.Related('credential'),
                                    required=False, display=False)
    forks = models.Field(type=int, required=False, display=False)
    limit = models.Field(required=False, display=False)
    verbosity = models.Field(
        display=False,
        type=types.MappedChoice([
            (0, 'default'),
            (1, 'verbose'),
            (2, 'debug'),
        ]),
    )
    job_tags = models.Field(required=False, display=False)
    extra_vars = models.Field(type=models.File('r'), required=False,
                              display=False)
示例#3
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))))
示例#4
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.File('r'),
                             required=False,
                             display=False)

    @resources.command(use_fields_as_options=False)
    @click.option('--host', type=types.Related('host'))
    @click.option('--group', type=types.Related('group'))
    def associate(self, host, group):
        """Associate a group with this host."""
        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)
示例#5
0
class Resource(models.Resource):
    cli_help = 'Manage credentials within Ansible Tower.'
    endpoint = '/credentials/'
    identity = ('user', 'team', 'kind', 'name')

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

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

    # What type of credential is this (machine, SCM, etc.)?
    kind = models.Field(
        help_text='The type of credential being added. '
        'Valid options are: ssh, scm, aws, rax, gce, azure.',
        type=click.Choice(['ssh', 'scm', 'aws', 'rax', 'gce', 'azure']),
    )

    # SSH and SCM fields.
    username = models.Field(
        help_text='The username. For AWS credentials, the access key.',
        required=False,
    )
    password = models.Field(
        help_text='The password. For AWS credentials, the secret key. '
        'For Rackspace credentials, the API key.',
        password=True,
        required=False,
    )
    private_key = models.Field(
        'ssh_key_data',
        display=False,
        help_text="The full path to the SSH private key to store. "
        "(Don't worry; it's encrypted.)",
        required=False,
        type=models.File('r'),
    )
    private_key_password = models.Field('ssh_key_unlock',
                                        password=True,
                                        required=False)

    # SSH specific fields.
    sudo_username = models.Field(required=False, display=False)
    sudo_password = models.Field(password=True, required=False)
    vault_password = models.Field(password=True, required=False)
示例#6
0
class Resource(models.Resource):
    cli_help = 'Manage schedules within Ansible Tower.'
    endpoint = '/schedules/'

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

    # Unified jt fields. note these fields will only be used during creation.
    # Plus, one and only one field should be provided.
    job_template = models.Field(type=types.Related('job_template'),
                                required=False,
                                display=False)
    inventory_source = models.Field(type=types.Related('inventory_source'),
                                    required=False,
                                    display=False)
    project = models.Field(type=types.Related('project'),
                           required=False,
                           display=False)

    # Schedule-specific fields.
    unified_job_template = models.Field(required=False,
                                        type=int,
                                        help_text='Integer used to display'
                                        ' unified job template in result, '
                                        'Please don\'t use it for create/'
                                        'modify.')
    enabled = models.Field(required=False,
                           type=click.BOOL,
                           default=True,
                           help_text='Whether this schedule will be used',
                           show_default=True)
    rrule = models.Field(required=False,
                         display=False,
                         help_text='Schedule rules specifications which is'
                         ' less than 255 characters.')
    extra_data = models.Field(type=types.Variables(),
                              required=False,
                              display=False,
                              help_text='Extra data for '
                              'schedule rules in the form of a .json file.')

    def _get_patch_url(self, url, pk):
        urlTokens = url.split('/')
        if len(urlTokens) > 3:
            # reconstruct url to prevent a rare corner case where resources
            # cannot be constructed independently. Open to modification if
            # API convention changes.
            url = '/'.join(urlTokens[:1] + urlTokens[-2:])
        return super(Resource, self)._get_patch_url(url, pk)
示例#7
0
class Resource(models.Resource):
    cli_help = 'Manage groups belonging to an inventory.'
    endpoint = '/groups/'

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)
    inventory = models.Field(type=types.Related('inventory'))
    variables = models.Field(type=types.File('r'), required=False,
                             display=False)

    @resources.command(ignore_defaults=True, no_args_is_help=False)
    @click.option('--root', is_flag=True, default=False,
                  help='Show only root groups (groups with no parent groups) '
                       'within the given inventory.')
    def list(self, root=False, **kwargs):
        """Return a list of groups."""

        # Sanity check: If we got `--root` and no inventory, that's an
        # error.
        if root and not kwargs.get('inventory', None):
            raise exc.UsageError('The --root option requires specifying an '
                                 'inventory also.')

        # If we are tasked with getting root groups, do that.
        if root:
            inventory_id = kwargs['inventory']
            r = client.get('/inventories/%d/root_groups/' % inventory_id)
            return r.json()

        # Return the superclass implementation.
        return super(Resource, self).list(**kwargs)
示例#8
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(display=False)
    organization = models.Field(type=types.Related('organization'),
                                display=False)
示例#9
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=models.File('r'), required=False,
                             display=False)
示例#10
0
class Resource(models.Resource):
    cli_help = 'Manage organizations within Ansible Tower.'
    endpoint = '/organizations/'

    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'))
    @click.option('--user', type=types.Related('user'))
    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'))
    @click.option('--user', type=types.Related('user'))
    def disassociate(self, organization, user):
        """Disassociate a user from this organization."""
        return self._disassoc('users', organization, user)
示例#11
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.')
示例#12
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)
示例#13
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.File('r'), required=False,
                             display=False)

    @resources.command(use_fields_as_options=False)
    @click.option('--host', type=types.Related('host'))
    @click.option('--group', type=types.Related('group'))
    def associate(self, host, group):
        """Associate a group with this host."""
        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):
        if group:
            kwargs['query'] = (kwargs.get('query', ()) +
                               (('groups__in', group),))
        return super(Resource, self).list(**kwargs)
示例#14
0
class Resource(models.Resource, models.MonitorableResource):
    cli_help = 'Manage projects within Ansible Tower.'
    endpoint = '/projects/'
    unified_job_type = '/project_updates/'

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)
    organization = models.Field(type=types.Related('organization'),
                                display=False, required=False)
    scm_type = models.Field(
        type=types.MappedChoice([
            ('', 'manual'),
            ('git', 'git'),
            ('hg', 'hg'),
            ('svn', 'svn'),
        ]),
    )
    scm_url = models.Field(required=False)
    local_path = models.Field(
        help_text='For manual projects, the server playbook directory name',
        required=False)
    scm_branch = models.Field(required=False, display=False)
    scm_credential = models.Field(
        'credential', display=False, required=False,
        type=types.Related('credential'),
    )
    scm_clean = models.Field(type=bool, required=False, display=False)
    scm_delete_on_update = models.Field(type=bool, required=False,
                                        display=False)
    scm_update_on_launch = models.Field(type=bool, required=False,
                                        display=False)
    job_timeout = models.Field(type=int, required=False, display=False,
                               help_text='The timeout field (in seconds).')

    @resources.command
    @click.option('--monitor', is_flag=True, default=False,
                  help='If sent, immediately calls `project monitor` on the '
                       'project rather than exiting with a success.'
                       'It polls for status until the SCM is updated.')
    @click.option('--wait', is_flag=True, default=False,
                  help='Polls server for status, exists when finished.')
    @click.option('--timeout', required=False, type=int,
                  help='If provided with --monitor, the SCM update'
                       ' will time out after the given number of seconds. '
                       'Does nothing if --monitor is not sent.')
    def create(self, organization=None, monitor=False, wait=False,
               timeout=None, fail_on_found=False, force_on_exists=False,
               **kwargs):
        """Create a new item of resource, with or w/o org.
        This would be a shared class with user, but it needs the ability
        to monitor if the flag is set.
        """
        if 'job_timeout' in kwargs and 'timeout' not in kwargs:
            kwargs['timeout'] = kwargs.pop('job_timeout')

        post_associate = False
        if organization:
            # Processing the organization flag depends on version
            debug.log('Checking Organization Relationship.', header='details')
            r = client.options('/projects/')
            if 'organization' in r.json()['actions']['POST']:
                kwargs['organization'] = organization
            else:
                post_associate = True

        # First, run the create method, ignoring the organization given
        answer = super(Resource, self).write(
            create_on_missing=True,
            fail_on_found=fail_on_found, force_on_exists=force_on_exists,
            **kwargs
        )
        project_id = answer['id']

        # If an organization is given, associate it here
        if post_associate:

            # Get the organization from Tower, will lookup name if needed
            org_resource = get_resource('organization')
            org_data = org_resource.get(organization)
            org_pk = org_data['id']

            debug.log("associating the project with its organization",
                      header='details', nl=1)
            org_resource._assoc('projects', org_pk, project_id)

        # if the monitor flag is set, wait for the SCM to update
        if monitor and answer.get('changed', False):
            return self.monitor(pk=None, parent_pk=project_id, timeout=timeout)
        elif wait and answer.get('changed', False):
            return self.wait(pk=None, parent_pk=project_id, timeout=timeout)

        return answer

    @resources.command(use_fields_as_options=(
        'name', 'description', 'scm_type', 'scm_url', 'local_path',
        'scm_branch', 'scm_credential', 'scm_clean', 'scm_delete_on_update',
        'scm_update_on_launch', 'job_timeout'
    ))
    def modify(self, pk=None, create_on_missing=False, **kwargs):
        """Modify an already existing.

        To edit the project's organizations, see help for organizations.

        Fields in the resource's `identity` tuple can be used in lieu of a
        primary key for a lookup; in such a case, only other fields are
        written.

        To modify unique fields, you must use the primary key for the lookup.
        """
        # Associated with issue #52, the organization can't be modified
        #    with the 'modify' command. This would create confusion about
        #    whether its flag is an identifier versus a field to modify.
        if 'job_timeout' in kwargs and 'timeout' not in kwargs:
            kwargs['timeout'] = kwargs.pop('job_timeout')
        return super(Resource, self).write(
            pk, create_on_missing=create_on_missing,
            force_on_exists=True, **kwargs
        )

    @resources.command(use_fields_as_options=('name', 'organization'))
    @click.option('--monitor', is_flag=True, default=False,
                  help='If sent, immediately calls `job monitor` on the newly '
                       'launched job rather than exiting with a success.')
    @click.option('--wait', is_flag=True, default=False,
                  help='Polls server for status, exists when finished.')
    @click.option('--timeout', required=False, type=int,
                  help='If provided with --monitor, this command (not the job)'
                       ' will time out after the given number of seconds. '
                       'Does nothing if --monitor is not sent.')
    def update(self, pk=None, create_on_missing=False, monitor=False,
               wait=False, timeout=None, name=None, organization=None):
        """Trigger a project update job within Ansible Tower.
        Only meaningful on non-manual projects.
        """
        # First, get the appropriate project.
        # This should be uniquely identified at this point, and if not, then
        # we just want the error that `get` will throw to bubble up.
        project = self.get(pk, name=name, organization=organization)
        pk = project['id']

        # Determine whether this project is able to be updated.
        debug.log('Asking whether the project can be updated.',
                  header='details')
        result = client.get('/projects/%d/update/' % pk)
        if not result.json()['can_update']:
            raise exc.CannotStartJob('Cannot update project.')

        # Okay, this project can be updated, according to Tower.
        # Commence the update.
        debug.log('Updating the project.', header='details')
        result = client.post('/projects/%d/update/' % pk)

        # If we were told to monitor the project update's status, do so.
        if monitor:
            project_update_id = result.json()['project_update']
            return self.monitor(project_update_id, parent_pk=pk,
                                timeout=timeout)
        elif wait:
            project_update_id = result.json()['project_update']
            return self.wait(project_update_id, parent_pk=pk, timeout=timeout)

        # Return the project update ID.
        return {
            'changed': True,
        }

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

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

        # Print just the information we need.
        return {
            'elapsed': job['elapsed'],
            'failed': job['failed'],
            'status': job['status'],
        }
示例#15
0
 def setUp(self):
     self.related = types.Related('user')
示例#16
0
class Resource(models.Resource):
    cli_help = 'Manage notification templates within Ansible Tower.'
    endpoint = '/notification_templates/'

    # Actual fields
    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)
    organization = models.Field(type=types.Related('organization'),
                                required=False,
                                display=False)
    notification_type = models.Field(type=click.Choice([
        'email', 'slack', 'twilio', 'pagerduty', 'hipchat', 'webhook', 'irc'
    ]))
    notification_configuration = models.Field(
        type=models.File('r', lazy=True),
        required=False,
        display=False,
        help_text='The notification configuration field. Note providing this'
        ' field would disable all notification-configuration-related'
        ' fields.')

    # Fields that are part of notification_configuration
    config_fields = [
        'notification_configuration', 'channels', 'token', 'username',
        'sender', 'recipients', 'use_tls', 'host', 'use_ssl', 'password',
        'port', 'account_token', 'from_number', 'to_numbers', 'account_sid',
        'subdomain', 'service_key', 'client_name', 'message_from', 'api_url',
        'color', 'notify', 'rooms', 'url', 'headers', 'server', 'nickname',
        'targets'
    ]
    # Fields that are part of notification_configuration which are categorized
    # according to notification_type
    configuration = {
        'slack': ['channels', 'token'],
        'email': [
            'username', 'sender', 'recipients', 'use_tls', 'host', 'use_ssl',
            'password', 'port'
        ],
        'twilio':
        ['account_token', 'from_number', 'to_numbers', 'account_sid'],
        'pagerduty': ['token', 'subdomain', 'service_key', 'client_name'],
        'hipchat':
        ['message_from', 'api_url', 'color', 'token', 'notify', 'rooms'],
        'webhook': ['url', 'headers'],
        'irc':
        ['server', 'port', 'use_ssl', 'password', 'nickname', 'targets']
    }

    # Fields which are expected to be json files.
    json_fields = ['notification_configuration', 'headers']
    encrypted_fields = ['password', 'token', 'account_token']

    # notification_configuration-related fields. fields with default values
    # are optional.
    username = models.Field(required=False,
                            display=False,
                            help_text='[{0}]The username.'.format('email'))
    sender = models.Field(required=False,
                          display=False,
                          help_text='[{0}]The sender.'.format('email'))
    recipients = models.Field(required=False,
                              display=False,
                              multiple=True,
                              help_text='[{0}]The recipients.'.format('email'))
    use_tls = models.Field(required=False,
                           display=False,
                           type=click.BOOL,
                           default=False,
                           help_text='[{0}]The tls trigger.'.format('email'))
    host = models.Field(required=False,
                        display=False,
                        help_text='[{0}]The host.'.format('email'))
    use_ssl = models.Field(
        required=False,
        display=False,
        type=click.BOOL,
        default=False,
        help_text='[{0}]The ssl trigger.'.format('email/irc'))
    password = models.Field(required=False,
                            display=False,
                            password=True,
                            help_text='[{0}]The password.'.format('email/irc'))
    port = models.Field(required=False,
                        display=False,
                        type=click.INT,
                        help_text='[{0}]The email port.'.format('email/irc'))
    channels = models.Field(required=False,
                            display=False,
                            multiple=True,
                            help_text='[{0}]The channel.'.format('slack'))
    token = models.Field(
        required=False,
        display=False,
        password=True,
        help_text='[{0}]The token.'.format('slack/pagerduty/hipchat'))
    account_token = models.Field(
        required=False,
        display=False,
        password=True,
        help_text='[{0}]The account token.'.format('twilio'))
    from_number = models.Field(
        required=False,
        display=False,
        help_text='[{0}]The source phone number.'.format('twilio'))
    to_numbers = models.Field(
        required=False,
        display=False,
        multiple=True,
        help_text='[{0}]The destination SMS numbers.'.format('twilio'))
    account_sid = models.Field(
        required=False,
        display=False,
        help_text='[{0}The account sid.'.format('twilio'))
    subdomain = models.Field(
        required=False,
        display=False,
        help_text='[{0}]The subdomain.'.format('pagerduty'))
    service_key = models.Field(required=False,
                               display=False,
                               help_text='[{0}]The API service/integration'
                               ' key.'.format('pagerduty'))
    client_name = models.Field(
        required=False,
        display=False,
        help_text='[{0}]The client identifier.'.format('pagerduty'))
    message_from = models.Field(required=False,
                                display=False,
                                help_text='[{0}]The label to be shown with '
                                'notification.'.format('hipchat'))
    api_url = models.Field(required=False,
                           display=False,
                           help_text='[{0}]The api url.'.format('hipchat'))
    color = models.Field(
        required=False,
        display=False,
        type=click.Choice(
            ['yellow', 'green', 'red', 'purple', 'gray', 'random']),
        help_text='[{0}]The notification color.'.format('hipchat'))
    notify = models.Field(
        required=False,
        display=False,
        default=False,
        help_text='[{0}]The notify channel trigger.'.format('hipchat'))
    url = models.Field(required=False,
                       display=False,
                       help_text='[{0}]The target URL.'.format('webhook'))
    headers = models.Field(
        required=False,
        display=False,
        type=models.File('r', lazy=True),
        help_text='[{0}]The http headers.'.format('webhook'))
    server = models.Field(required=False,
                          display=False,
                          help_text='[{0}]Server address.'.format('irc'))
    nickname = models.Field(required=False,
                            display=False,
                            help_text='[{0}]The irc nick.'.format('irc'))
    target = models.Field(
        required=False,
        display=False,
        help_text='[{0}]The distination channels or users.'.format('irc'))

    def _separate(self, kwargs):
        """Remove None-valued and configuration-related keyworded arguments
        """
        self._pop_none(kwargs)
        result = {}
        for field in Resource.config_fields:
            if field in kwargs:
                result[field] = kwargs.pop(field)
                if field in Resource.json_fields:
                    try:
                        data = json.loads(result[field])
                        result[field] = data
                    except ValueError:
                        raise exc.TowerCLIError('Provided json file format '
                                                'invalid. Please recheck.')
        return result

    def _configuration(self, kwargs, config_item):
        """Combine configuration-related keyworded arguments into
        notification_configuration.
        """
        if 'notification_configuration' not in config_item:
            if 'notification_type' not in kwargs:
                return
            nc = kwargs['notification_configuration'] = {}
            for field in Resource.configuration[kwargs['notification_type']]:
                if field not in config_item:
                    raise exc.TowerCLIError('Required config field %s not'
                                            ' provided.' % field)
                else:
                    nc[field] = config_item[field]
        else:
            kwargs['notification_configuration'] = \
                    config_item['notification_configuration']

    @resources.command
    @click.option('--job-template',
                  type=types.Related('job_template'),
                  required=False,
                  help='The job template to relate to.')
    @click.option('--status',
                  type=click.Choice(['error', 'success']),
                  required=False,
                  help='Specify job run status of job '
                  'template to relate to.')
    def create(self, fail_on_found=False, force_on_exists=False, **kwargs):
        """Create a notification template.

        All required configuration-related fields (required according to
        notification_type) must be provided.

        There are two types of notification template creation: isolatedly
        creating a new notification template and creating a new notification
        template under a job template. Here the two types are discriminated by
        whether to provide --job-template option. --status option controls
        more specific, job-run-status-related association.

        Fields in the resource's `identity` tuple are used for a lookup;
        if a match is found, then no-op (unless `force_on_exists` is set) but
        do not fail (unless `fail_on_found` is set).
        """
        config_item = self._separate(kwargs)
        jt_id = kwargs.pop('job_template', None)
        status = kwargs.pop('status', 'any')
        old_endpoint = self.endpoint
        if jt_id is not None:
            jt = get_resource('job_template')
            jt.get(pk=jt_id)
            try:
                nt_id = self.get(**copy.deepcopy(kwargs))['id']
            except exc.NotFound:
                pass
            else:
                if fail_on_found:
                    raise exc.TowerCLIError('Notification template already '
                                            'exists and fail-on-found is '
                                            'switched on. Please use'
                                            ' "associate_notification" method'
                                            ' of job_template instead.')
                else:
                    debug.log(
                        'Notification template already exists, '
                        'associating with job template.',
                        header='details')
                    return jt.associate_notification(jt_id,
                                                     nt_id,
                                                     status=status)
            self.endpoint = '/job_templates/%d/notification_templates_%s/' %\
                            (jt_id, status)
        self._configuration(kwargs, config_item)
        result = super(Resource, self).create(**kwargs)
        self.endpoint = old_endpoint
        return result

    @resources.command
    def modify(self, pk=None, create_on_missing=False, **kwargs):
        """Modify an existing notification template.

        Not all required configuration-related fields (required according to
        notification_type) should be provided.

        Fields in the resource's `identity` tuple can be used in lieu of a
        primary key for a lookup; in such a case, only other fields are
        written.

        To modify unique fields, you must use the primary key for the lookup.
        """
        # Create the resource if needed.
        if pk is None and create_on_missing:
            try:
                self.get(**copy.deepcopy(kwargs))
            except exc.NotFound:
                return self.create(**kwargs)

        # Modify everything except notification type and configuration
        config_item = self._separate(kwargs)
        notification_type = kwargs.pop('notification_type', None)
        debug.log(
            'Modify everything except notification type and'
            ' configuration',
            header='details')
        part_result = super(Resource, self).\
            modify(pk=pk, create_on_missing=create_on_missing, **kwargs)

        # Modify notification type and configuration
        if notification_type is None or \
           notification_type == part_result['notification_type']:
            for item in part_result['notification_configuration']:
                if item not in config_item or not config_item[item]:
                    to_add = part_result['notification_configuration'][item]
                    if not (to_add == '$encrypted$'
                            and item in Resource.encrypted_fields):
                        config_item[item] = to_add
        if notification_type is None:
            kwargs['notification_type'] = part_result['notification_type']
        else:
            kwargs['notification_type'] = notification_type
        self._configuration(kwargs, config_item)
        debug.log('Modify notification type and configuration',
                  header='details')
        result = super(Resource, self).\
            modify(pk=pk, create_on_missing=create_on_missing, **kwargs)

        # Update 'changed' field to give general changed info
        if 'changed' in result and 'changed' in part_result:
            result['changed'] = result['changed'] or part_result['changed']
        return result

    @resources.command
    def delete(self, pk=None, fail_on_missing=False, **kwargs):
        """Remove the given notification template.

        Note here configuration-related fields like
        'notification_configuration' and 'channels' will not be
        used even provided.

        If `fail_on_missing` is True, then the object's not being found is
        considered a failure; otherwise, a success with no change is reported.
        """
        self._separate(kwargs)
        return super(Resource, self).\
            delete(pk=pk, fail_on_missing=fail_on_missing, **kwargs)

    @resources.command
    def list(self, all_pages=False, **kwargs):
        """Return a list of notification templates.

        Note here configuration-related fields like
        'notification_configuration' and 'channels' will not be
        used even provided.

        If one or more filters are provided through keyword arguments,
        filter the results accordingly.

        If no filters are provided, return all results.
        """
        self._separate(kwargs)
        return super(Resource, self).list(all_pages=all_pages, **kwargs)

    @resources.command
    def get(self, pk=None, **kwargs):
        """Return one and exactly one notification template.

        Note here configuration-related fields like
        'notification_configuration' and 'channels' will not be
        used even provided.

        Lookups may be through a primary key, specified as a positional
        argument, and/or through filters specified through keyword arguments.

        If the number of results does not equal one, raise an exception.
        """
        self._separate(kwargs)
        return super(Resource, self).get(pk=pk, **kwargs)
示例#17
0
class Resource(models.Resource):
    cli_help = 'Manage groups belonging to an inventory.'
    endpoint = '/groups/'
    identity = ('inventory', 'name')

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)
    inventory = models.Field(type=types.Related('inventory'))
    variables = models.Field(type=types.File('r'),
                             required=False,
                             display=False)

    @click.option('--credential',
                  type=types.Related('credential'),
                  required=False,
                  help='The cloud credential to use.')
    @click.option('--source',
                  type=click.Choice(INVENTORY_SOURCES),
                  default='manual',
                  help='The source to use for this group.')
    def create(self, credential=None, source=None, **kwargs):
        """Create a group and, if necessary, modify the inventory source within
        the group.
        """
        # First, create the group.
        answer = super(Resource, self).create(**kwargs)

        # If the group already exists and we aren't supposed to make changes,
        # then we're done.
        if not kwargs.pop('force_on_exists', False) and not answer['changed']:
            return answer

        # Sanity check: A group was created, but do we need to do anything
        # with the inventory source at all? If no credential or source
        # was specified, then we'd just be updating the inventory source
        # with an effective no-op.
        if not credential and source in ('manual', None):
            return answer

        # Get the inventory source ID ("isid").
        # Inventory sources are not created directly; rather, one was created
        # automatically when the group was created.
        isid = self._get_inventory_source_id(answer)

        # We now have our inventory source ID; modify it according to the
        # provided parameters.
        isrc = get_resource('inventory_source')
        return isrc.modify(isid,
                           credential=credential,
                           source=source,
                           force_on_exists=True,
                           **kwargs)

    @click.option('--credential',
                  type=types.Related('credential'),
                  required=False)
    @click.option('--source',
                  type=click.Choice(INVENTORY_SOURCES),
                  default='manual',
                  help='The source to use for this group.')
    def modify(self, pk=None, credential=None, source=None, **kwargs):
        """Modify a group and, if necessary, the inventory source within
        the group.
        """
        # First, modify the group.
        answer = super(Resource, self).modify(pk=pk, **kwargs)

        # If the group already exists and we aren't supposed to make changes,
        # then we're done.
        if not kwargs.pop('force_on_exists', True) and not answer['changed']:
            return answer

        # Get the inventory source ID ("isid").
        # Inventory sources are not created directly; rather, one was created
        # automatically when the group was created.
        isid = self._get_inventory_source_id(answer)

        # We now have our inventory source ID; modify it according to the
        # provided parameters.
        #
        # Note: Any fields that were part of the group modification need
        # to be expunged from kwargs before making this call.
        isrc = get_resource('inventory_source')
        for field in self.fields:
            kwargs.pop(field.name, None)
        return isrc.modify(isid,
                           credential=credential,
                           source=source,
                           force_on_exists=True,
                           **kwargs)

    @resources.command(ignore_defaults=True, no_args_is_help=False)
    @click.option('--root',
                  is_flag=True,
                  default=False,
                  help='Show only root groups (groups with no parent groups) '
                  'within the given inventory.')
    def list(self, root=False, **kwargs):
        """Return a list of groups."""

        # Sanity check: If we got `--root` and no inventory, that's an
        # error.
        if root and not kwargs.get('inventory', None):
            raise exc.UsageError('The --root option requires specifying an '
                                 'inventory also.')

        # If we are tasked with getting root groups, do that.
        if root:
            inventory_id = kwargs['inventory']
            r = client.get('/inventories/%d/root_groups/' % inventory_id)
            return r.json()

        # Return the superclass implementation.
        return super(Resource, self).list(**kwargs)

    @click.argument('group', type=types.Related('group'))
    @click.option('--monitor',
                  is_flag=True,
                  default=False,
                  help='If sent, immediately calls `monitor` on the newly '
                  'launched job rather than exiting with a success.')
    @click.option('--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 sync(self, group, monitor=False, timeout=None, **kwargs):
        """Update the given group's inventory source."""

        isrc = get_resource('inventory_source')
        isid = self._get_inventory_source_id(group)
        return isrc.update(isid, monitor=monitor, timeout=timeout, **kwargs)

    def _get_inventory_source_id(self, group):
        """Return the inventory source ID given a group dictionary returned
        from the Tower API.
        """
        # If we got a group ID rather than a group, get the group.
        if isinstance(group, int):
            group = self.get(group)

        # Return the inventory soruce ID.
        return int(group['related']['inventory_source'].split('/')[-2])
示例#18
0
class Resource(models.Resource):
    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).
        """
        jt_id = kwargs.pop('job_template', None)
        old_endpoint = self.endpoint
        if jt_id is not None:
            jt = get_resource('job_template')
            jt.get(pk=jt_id)
            try:
                label_id = self.get(name=kwargs.get('name', None),
                                    organization=kwargs.get(
                                        'organization', None))['id']
            except exc.NotFound:
                pass
            else:
                if fail_on_found:
                    raise exc.TowerCLIError('Label already exists and fail-on'
                                            '-found is switched on. Please use'
                                            ' "associate_label" method of job'
                                            '_template instead.')
                else:
                    debug.log(
                        'Label already exists, associating with job '
                        'template.',
                        header='details')
                    return jt.associate_label(jt_id, label_id)
            self.endpoint = '/job_templates/%d/labels/' % jt_id
        result = super(Resource, self).\
            create(fail_on_found=fail_on_found,
                   force_on_exists=force_on_exists, **kwargs)
        self.endpoint = old_endpoint
        return result
示例#19
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)
示例#20
0
class Resource(models.MonitorableResource):
    cli_help = 'Manage projects within Ansible Tower.'
    endpoint = '/projects/'

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)
    organization = models.Field(type=types.Related('organization'),
                                display=False)
    scm_type = models.Field(type=types.MappedChoice([
        ('', 'manual'),
        ('git', 'git'),
        ('hg', 'hg'),
        ('svn', 'svn'),
    ]), )
    scm_url = models.Field(required=False)
    scm_branch = models.Field(required=False, display=False)
    scm_credential = models.Field(
        'credential',
        display=False,
        required=False,
        type=types.Related('credential'),
    )
    scm_clean = models.Field(type=bool, required=False, display=False)
    scm_delete_on_update = models.Field(type=bool,
                                        required=False,
                                        display=False)
    scm_update_on_launch = models.Field(type=bool,
                                        required=False,
                                        display=False)

    @resources.command(use_fields_as_options=('name', 'organization'))
    @click.option('--monitor',
                  is_flag=True,
                  default=False,
                  help='If sent, immediately calls `job monitor` on the newly '
                  'launched job rather than exiting with a success.')
    @click.option('--timeout',
                  required=False,
                  type=int,
                  help='If provided with --monitor, this command (not the job)'
                  ' will time out after the given number of seconds. '
                  'Does nothing if --monitor is not sent.')
    def update(self,
               pk,
               monitor=False,
               timeout=None,
               name=None,
               organization=None):
        """Trigger a project update job within Ansible Tower.
        Only meaningful on non-manual projects.
        """
        # First, get the appropriate project.
        # This should be uniquely identified at this point, and if not, then
        # we just want the error that `get` will throw to bubble up.
        project = self.get(pk, name=name, organization=organization)
        pk = project['id']

        # Determine whether this project is able to be updated.
        debug.log('Asking whether the project can be updated.',
                  header='details')
        result = client.get('/projects/%d/update/' % pk)
        if not result.json()['can_update']:
            raise exc.CannotStartJob('Cannot update project.')

        # Okay, this project can be updated, according to Tower.
        # Commence the update.
        debug.log('Updating the project.', header='details')
        result = client.post('/projects/%d/update/' % pk)
        project_update_id = result.json()['project_update']

        # If we were told to monitor the project update's status, do so.
        if monitor:
            return self.monitor(pk, timeout=timeout)

        # Return the project update ID.
        return {
            'changed': True,
        }

    @resources.command
    @click.option('--detail',
                  is_flag=True,
                  default=False,
                  help='Print more detail.')
    def status(self, pk, detail=False):
        """Print the current job status."""
        # Get the job from Ansible Tower.
        debug.log('Asking for project update status.', header='details')
        project = client.get('/projects/%d/' % pk).json()

        # Determine the appropriate project update.
        if 'current_update' in project['related']:
            debug.log('A current update exists; retrieving it.',
                      header='details')
            job = client.get(project['related']['current_update'][7:]).json()
        elif project['related'].get('last_update', None):
            debug.log(
                'No current update exists; retrieving the most '
                'recent update.',
                header='details')
            job = client.get(project['related']['last_update'][7:]).json()
        else:
            raise exc.NotFound('No project updates exist.')

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

        # Print just the information we need.
        return adict({
            'elapsed': job['elapsed'],
            'failed': job['failed'],
            'status': job['status'],
        })
示例#21
0
class Resource(models.SurveyResource):
    cli_help = 'Manage job templates.'
    endpoint = '/job_templates/'

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)
    job_type = models.Field(
        required=False,
        display=False,
        type=click.Choice(['run', 'check', 'scan']),
    )
    inventory = models.Field(type=types.Related('inventory'), required=False)
    project = models.Field(type=types.Related('project'))
    playbook = models.Field()
    machine_credential = models.Field(
        'credential',
        display=False,
        required=False,
        type=types.Related('credential'),
    )
    cloud_credential = models.Field(type=types.Related('credential'),
                                    required=False,
                                    display=False)
    network_credential = models.Field(type=types.Related('credential'),
                                      required=False,
                                      display=False)
    forks = models.Field(type=int, required=False, display=False)
    limit = models.Field(required=False, display=False)
    verbosity = models.Field(
        display=False,
        type=types.MappedChoice([
            (0, 'default'),
            (1, 'verbose'),
            (2, 'more_verbose'),
            (3, 'debug'),
            (4, 'connection'),
            (5, 'winrm'),
        ]),
        required=False,
    )
    job_tags = models.Field(required=False, display=False)
    skip_tags = models.Field(required=False, display=False)
    extra_vars = models.Field(
        type=types.Variables(),
        required=False,
        display=False,
        multiple=True,
        help_text='Extra variables used by Ansible in YAML or key=value '
        'format. Use @ to get YAML from a file.')
    host_config_key = models.Field(
        required=False,
        display=False,
        help_text='Allow Provisioning Callbacks using this host config key')
    ask_variables_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for extra_vars on launch.')
    ask_limit_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for host limits on launch.')
    ask_tags_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for job tags on launch.')
    ask_skip_tags_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for tags to skip on launch.')
    ask_job_type_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for job type on launch.')
    ask_inventory_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for inventory on launch.')
    ask_credential_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for machine credential on launch.')
    become_enabled = models.Field(type=bool, required=False, display=False)
    timeout = models.Field(type=int,
                           required=False,
                           display=False,
                           help_text='The timeout field (in seconds).')
    survey_enabled = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for job type on launch.')
    survey_spec = models.Field(
        type=types.Variables(),
        required=False,
        display=False,
        help_text='On write commands, perform extra POST to the '
        'survey_spec endpoint.')

    @resources.command
    def create(self,
               fail_on_found=False,
               force_on_exists=False,
               extra_vars=None,
               **kwargs):
        """Create a job template."""
        # Provide a default value for job_type, but only in creation of JT
        if not kwargs.get('job_type', False):
            kwargs['job_type'] = 'run'
        return super(Resource, self).create(fail_on_found=fail_on_found,
                                            force_on_exists=force_on_exists,
                                            **kwargs)

    @resources.command(use_fields_as_options=False)
    @click.option('--job-template', type=types.Related('job_template'))
    @click.option('--label', type=types.Related('label'))
    def associate_label(self, job_template, label):
        """Associate an label with this job template."""
        return self._assoc('labels', job_template, label)

    @resources.command(use_fields_as_options=False)
    @click.option('--job-template', type=types.Related('job_template'))
    @click.option('--label', type=types.Related('label'))
    def disassociate_label(self, job_template, label):
        """Disassociate an label from this job template."""
        return self._disassoc('labels', job_template, label)

    @resources.command(use_fields_as_options=False)
    @click.option('--job-template', type=types.Related('job_template'))
    @click.option('--notification-template',
                  type=types.Related('notification_template'))
    @click.option('--status',
                  type=click.Choice(['any', 'error', 'success']),
                  required=False,
                  default='any',
                  help='Specify job run status'
                  ' of job template to relate to.')
    def associate_notification_template(self, job_template,
                                        notification_template, status):
        """Associate a notification template from this job template."""
        return self._assoc('notification_templates_%s' % status, job_template,
                           notification_template)

    @resources.command(use_fields_as_options=False)
    @click.option('--job-template', type=types.Related('job_template'))
    @click.option('--notification-template',
                  type=types.Related('notification_template'))
    @click.option('--status',
                  type=click.Choice(['any', 'error', 'success']),
                  required=False,
                  default='any',
                  help='Specify job run status'
                  ' of job template to relate to.')
    def disassociate_notification_template(self, job_template,
                                           notification_template, status):
        """Disassociate a notification template from this job template."""
        return self._disassoc('notification_templates_%s' % status,
                              job_template, notification_template)
示例#22
0
class Resource(models.MonitorableResource):
    cli_help = 'Manage inventory sources within Ansible Tower.'
    endpoint = '/inventory_sources/'
    internal = True
    unified_job_type = '/inventory_updates/'

    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)
    # 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).')

    @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'],
        }
示例#23
0
文件: job.py 项目: is00hcw/tower-cli
class Resource(models.MonitorableResource):
    """A resource for jobs.

    As a base resource, this resource does *not* have the normal create, list,
    etc. methods.
    """
    cli_help = 'Launch or monitor jobs.'
    endpoint = '/jobs/'

    @resources.command
    @click.option('--job-template', type=types.Related('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('--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('--extra-vars', type=types.File('r'), required=False)
    @click.option('--tags', required=False)
    def launch(self,
               job_template,
               tags=None,
               monitor=False,
               timeout=None,
               no_input=True,
               extra_vars=None):
        """Launch a new job based on a job template.

        Creates a new job in Ansible Tower, immediately stats it, and
        returns back an ID in order for its status to be monitored.
        """
        # Get the job template from Ansible Tower.
        # This is used as the baseline for starting the job.
        jt_resource = get_resource('job_template')
        jt = jt_resource.get(job_template)

        # Update the job data by adding an automatically-generated job name,
        # and removing the ID.
        data = copy(jt)
        data['job_template'] = data.pop('id')
        data['name'] = '%s [invoked via. Tower CLI]' % data['name']
        if tags:
            data['job_tags'] = tags

        # If the job template requires prompting for extra variables,
        # do so (unless --no-input is set).
        if extra_vars:
            if hasattr(extra_vars, 'read'):
                extra_vars = extra_vars.read()
            data['extra_vars'] = extra_vars
        elif data.pop('ask_variables_on_launch', False) and not no_input:
            initial = data['extra_vars']
            initial = '\n'.join((
                '# Specify extra variables (if any) here.',
                '# Lines beginning with "#" are ignored.',
                initial,
            ))
            extra_vars = click.edit(initial) or ''
            extra_vars = '\n'.join(
                [i for i in extra_vars.split('\n') if not i.startswith('#')])
            data['extra_vars'] = extra_vars

        # In Tower 2.1 and later, we create the new job with
        # /job_templates/N/launch/; in Tower 2.0 and before, there is a two
        # step process of posting to /jobs/ and then /jobs/N/start/.
        supports_job_template_launch = False
        if 'launch' in jt['related']:
            supports_job_template_launch = True

        # Create the new job in Ansible Tower.
        start_data = {}
        if supports_job_template_launch:
            endpoint = '/job_templates/%d/launch/' % jt['id']
            if 'extra_vars' in data:
                start_data['extra_vars'] = data['extra_vars']
            if tags:
                start_data['job_tags'] = data['job_tags']
        else:
            debug.log('Creating the job.', header='details')
            job = client.post('/jobs/', data=data).json()
            job_id = job['id']
            endpoint = '/jobs/%d/start/' % job_id

        # There's a non-trivial chance that we are going to need some
        # additional information to start the job; in particular, many jobs
        # rely on passwords entered at run-time.
        #
        # If there are any such passwords on this job, ask for them now.

        debug.log('Asking for information necessary to start the job.',
                  header='details')
        job_start_info = client.get(endpoint).json()
        for password in job_start_info.get('passwords_needed_to_start', []):
            start_data[password] = getpass('Password for %s: ' % password)

        # Actually start the job.
        debug.log('Launching the job.', header='details')
        result = client.post(endpoint, start_data)

        # If this used the /job_template/N/launch/ route, get the job
        # ID from the result.
        if supports_job_template_launch:
            job_id = result.json()['job']

        # 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)

        # Return the job ID.
        return {
            'changed': True,
            'id': job_id,
        }

    @resources.command
    @click.option('--detail',
                  is_flag=True,
                  default=False,
                  help='Print more detail.')
    def status(self, pk, detail=False):
        """Print the current job status."""
        # Get the job from Ansible Tower.
        debug.log('Asking for job status.', header='details')
        job = client.get('/jobs/%d/' % pk).json()

        # 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 adict({
            'elapsed': job['elapsed'],
            'failed': job['failed'],
            'status': job['status'],
        })

    @resources.command
    @click.option('--fail-if-not-running',
                  is_flag=True,
                  default=False,
                  help='Fail loudly if the job is not currently running.')
    def cancel(self, pk, fail_if_not_running=False):
        """Cancel a currently running job.

        Fails with a non-zero exit status if the job cannot be canceled.
        """
        # Attempt to cancel the job.
        try:
            client.post('/jobs/%d/cancel/' % pk)
            changed = True
        except exc.MethodNotAllowed:
            changed = False
            if fail_if_not_running:
                raise exc.TowerCLIError('Job not running.')

        # Return a success.
        return adict({'status': 'canceled', 'changed': changed})
示例#24
0
class Resource(models.ExeResource):
    """A resource for jobs.

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

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

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

        Creates a new job in Ansible Tower, immediately starts it, and
        returns back an ID in order for its status to be monitored.
        """
        # Get the job template from Ansible Tower.
        # This is used as the baseline for starting the job.

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

        # Update the job data by adding an automatically-generated job name,
        # and removing the ID.
        data = copy(jt)
        data['job_template'] = data.pop('id')
        data['name'] = '%s [invoked via. Tower CLI]' % data['name']
        if tags:
            data['job_tags'] = tags

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

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

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

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

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

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

        # In Tower 2.1 and later, we create the new job with
        # /job_templates/N/launch/; in Tower 2.0 and before, there is a two
        # step process of posting to /jobs/ and then /jobs/N/start/.
        supports_job_template_launch = False
        if 'launch' in jt['related']:
            supports_job_template_launch = True

        # Create the new job in Ansible Tower.
        start_data = {}
        if supports_job_template_launch and not use_job_endpoint:
            endpoint = '/job_templates/%d/launch/' % jt['id']
            if 'extra_vars' in data and len(data['extra_vars']) > 0:
                start_data['extra_vars'] = data['extra_vars']
            if tags:
                start_data['job_tags'] = data['job_tags']
            for resource in PROMPT_LIST:
                if resource in modified:
                    start_data[resource] = data[resource]
        else:
            debug.log('Creating the job.', header='details')
            job = client.post('/jobs/', data=data).json()
            job_id = job['id']
            endpoint = '/jobs/%d/start/' % job_id

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

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

        # If this used the /job_template/N/launch/ route, get the job
        # ID from the result.
        if supports_job_template_launch and not use_job_endpoint:
            job_id = job_started.json()['job']

        # 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)

        return result
示例#25
0
class Resource(models.Resource):
    cli_help = 'Manage credentials within Ansible Tower.'
    endpoint = '/credentials/'
    identity = ('organization', 'user', 'team', 'kind', 'name')

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

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

    # What type of credential is this (machine, SCM, etc.)?
    kind = models.Field(
        display=True,
        help_text='The type of credential being added. '
        'Valid options are: ssh, net, scm, aws, rax, vmware,'
        ' satellite6, cloudforms, gce, azure, azure_rm, openstack.',
        type=click.Choice([
            'ssh', 'net', 'scm', 'aws', 'rax', 'vmware', 'satellite6',
            'cloudforms', 'gce', 'azure', 'azure_rm', 'openstack'
        ]),
    )

    # need host in order to use VMware
    host = models.Field(help_text='The hostname or IP address to use.',
                        required=False,
                        display=False)
    # need project to use openstack
    project = models.Field(help_text='The identifier for the project.',
                           required=False,
                           display=False)

    # SSH and SCM fields.
    username = models.Field(
        help_text='The username. For AWS credentials, the access key.',
        required=False,
    )
    password = models.Field(
        help_text='%sThe password. For AWS credentials, the secret key. '
        'For Rackspace credentials, the API key.' % PROMPT,
        password=True,
        required=False,
    )
    ssh_key_data = models.Field(
        'ssh_key_data',
        display=False,
        help_text="The full path to the SSH private key to store. "
        "(Don't worry; it's encrypted.)",
        required=False,
        type=models.File('r'),
    )
    ssh_key_unlock = models.Field(help_text='%sssh_key_unlock' % PROMPT,
                                  password=True,
                                  required=False)

    # Extra fields in 3.0
    authorize = models.Field(
        help_text='Whether to use the authorize mechanism when type is "net".',
        required=False,
        display=False,
        type=click.BOOL,
    )
    authorize_password = models.Field(
        help_text='Password used by the authorize mechanism when type is '
        '"net".',
        password=True,
        required=False,
        display=False,
    )
    client = models.Field(
        help_text='Client Id or Application Id for the credential when type '
        'is "azure_rm".',
        required=False,
        display=False,
    )
    secret = models.Field(
        help_text='Secret Token for this credential when type is "azure_rm".',
        required=False,
        display=False)
    subscription = models.Field(
        help_text='Subscription identifier for this credential when type is '
        '"azure_rm".',
        required=False,
        display=False,
    )
    tenant = models.Field(
        help_text='Tenant identifier for this credential when type is '
        '"azure_rm"',
        required=False,
        display=False,
    )
    domain = models.Field(
        help_text='Domain name for this credential when type is "openstack".',
        required=False,
        display=False,
    )

    # Method with which to esclate
    become_method = models.Field(
        display=False,
        help_text='Privledge escalation method. ',
        type=types.MappedChoice([
            ('', 'None'),
            ('sudo', 'sudo'),
            ('su', 'su'),
            ('pbrun', 'pbrun'),
            ('pfexec', 'pfexec'),
        ]),
        required=False,
    )

    # SSH specific fields.
    become_username = models.Field(required=False, display=False)
    become_password = models.Field(password=True,
                                   required=False,
                                   help_text='%sThe become_password field' %
                                   PROMPT)
    vault_password = models.Field(password=True,
                                  required=False,
                                  help_text='%sThe vault_password field' %
                                  PROMPT)

    @resources.command
    def create(self, **kwargs):
        """Create a credential.
        """
        if (kwargs.get('user', False) or kwargs.get('team', False)
                or kwargs.get('organization', False)):
            debug.log('Checking Project API Details.', header='details')
            r = client.options('/credentials/')
            if 'organization' in r.json()['actions']['POST']:
                for i in range(len(self.fields)):
                    if self.fields[i].name in ('user', 'team'):
                        self.fields[i].no_lookup = True
        return super(Resource, self).create(**kwargs)
示例#26
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 addional request for its member_role
                data['object_id'] = res
        elif res:
            endpoint = '%s/%s/object_roles/' % (cls.pluralize(res_type), res)
        else:
            endpoint = '/roles/'
        if in_data.get('type', False):
            data['role_field'] = '%s_role' % in_data['type'].lower()
        return data, endpoint

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

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

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

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

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

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

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

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

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

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

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

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

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

    @resources.command(use_fields_as_options=ACTOR_FIELDS + RESOURCE_FIELDS +
                       ['type'])
    def get(self, pk=None, **kwargs):
        """Get information about a role."""
        if kwargs.pop('include_debug_header', True):
            debug.log('Getting the role record.', header='details')
        data, self.endpoint = self.data_endpoint(kwargs)
        response = self.read(pk=pk,
                             fail_on_no_results=True,
                             fail_on_multiple_results=True,
                             **data)
        item_dict = response['results'][0]
        self.configure_display(item_dict)
        return item_dict

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

    @resources.command(use_fields_as_options=ACTOR_FIELDS + RESOURCE_FIELDS +
                       ['type'])
    @click.option('--fail-on-found',
                  default=False,
                  show_default=True,
                  type=bool,
                  is_flag=True,
                  help='If used, return an error if the user is already '
                  'not a member of the role.')
    def revoke(self, fail_on_found=False, **kwargs):
        """Remove a user or a team from a role. Required information:
        1) Type of the role
        2) Resource of the role, inventory, credential, or any other
        3) A user or a team to add to the role"""
        return self.role_write(fail_on_found=fail_on_found,
                               disassociate=True,
                               **kwargs)
示例#27
0
class Resource(models.Resource):
    cli_help = 'Manage credentials within Ansible Tower.'
    endpoint = '/credentials/'
    identity = ('user', 'team', 'kind', 'name')

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

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

    # What type of credential is this (machine, SCM, etc.)?
    kind = models.Field(
        display=True,
        help_text='The type of credential being added. '
        'Valid options are: ssh, scm, aws, rax, vmware,'
        ' gce, azure, openstack.',
        type=click.Choice([
            'ssh', 'scm', 'aws', 'rax', 'vmware', 'gce', 'azure', 'openstack'
        ]),
    )

    # need host in order to use VMware
    host = models.Field(help_text='The hostname or IP address to use.',
                        required=False,
                        display=False)
    # need project to use openstack
    project = models.Field(help_text='The identifier for the project.',
                           required=False,
                           display=False)

    # SSH and SCM fields.
    username = models.Field(
        help_text='The username. For AWS credentials, the access key.',
        required=False,
    )
    password = models.Field(
        help_text='The password. For AWS credentials, the secret key. '
        'For Rackspace credentials, the API key.',
        password=True,
        required=False,
    )
    ssh_key_data = models.Field(
        'ssh_key_data',
        display=False,
        help_text="The full path to the SSH private key to store. "
        "(Don't worry; it's encrypted.)",
        required=False,
        type=models.File('r'),
    )
    ssh_key_unlock = models.Field('ssh_key_unlock',
                                  password=True,
                                  required=False)

    # Method with which to esclate
    become_method = models.Field(
        help_text='Privledge escalation method. ',
        type=types.MappedChoice([
            ('', 'None'),
            ('sudo', 'sudo'),
            ('su', 'su'),
            ('pbrun', 'pbrun'),
            ('pfexec', 'pfexec'),
        ]),
        required=False,
    )

    # SSH specific fields.
    become_username = models.Field(required=False, display=False)
    become_password = models.Field(password=True, required=False)
    vault_password = models.Field(password=True, required=False)
示例#28
0
class Resource(models.Resource):
    cli_help = 'Manage groups belonging to an inventory.'
    endpoint = '/groups/'
    identity = ('inventory', 'name')

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)
    inventory = models.Field(type=types.Related('inventory'))
    variables = models.Field(type=types.File('r'),
                             required=False,
                             display=False)

    # Basic options for the source
    @click.option('--credential',
                  type=types.Related('credential'),
                  required=False,
                  help='The cloud credential to use.')
    @click.option('--source',
                  type=click.Choice(INVENTORY_SOURCES),
                  default='manual',
                  help='The source to use for this group.')
    @click.option('--source-regions', help='Regions for your cloud provider.')
    # Options may not be valid for certain types of cloud servers
    @click.option('--source-vars',
                  help='Override variables found on source '
                  'with variables defined in this field.')
    @click.option('--overwrite',
                  type=bool,
                  help='Delete child groups and hosts not found in source.')
    @click.option('--overwrite-vars',
                  type=bool,
                  help='Override vars in child groups and hosts with those '
                  'from the external source.')
    @click.option('--update-on-launch',
                  type=bool,
                  help='Refresh inventory '
                  'data from its source each time a job is run.')
    def create(self, fail_on_found=False, force_on_exists=False, **kwargs):
        """Create a group and, if necessary, modify the inventory source within
        the group.
        """
        # Break out the options for the group vs its inventory_source
        group_fields = [f.name for f in self.fields]
        is_kwargs = {}
        for field in kwargs.copy():
            if field not in group_fields:
                is_kwargs[field] = kwargs.pop(field)

        # Handle alias for "manual" source
        if is_kwargs.get('source', None) == 'manual':
            is_kwargs.pop('source')

        # First, create the group.
        answer = super(Resource, self).create(fail_on_found=fail_on_found,
                                              force_on_exists=force_on_exists,
                                              **kwargs)

        # If the group already exists and we aren't supposed to make changes,
        # then we're done.
        if not force_on_exists and not answer['changed']:
            return answer

        # Sanity check: A group was created, but do we need to do anything
        # with the inventory source at all? If no credential or source
        # was specified, then we'd just be updating the inventory source
        # with an effective no-op.
        if len(is_kwargs) == 0:
            return answer

        # Get the inventory source ID ("isid").
        # Inventory sources are not created directly; rather, one was created
        # automatically when the group was created.
        isid = self._get_inventory_source_id(answer)

        # We now have our inventory source ID; modify it according to the
        # provided parameters.
        isrc = get_resource('inventory_source')
        is_answer = isrc.write(pk=isid, force_on_exists=True, **is_kwargs)

        # If either the inventory_source or the group objects were modified
        # then refelect this in the output to avoid confusing the user.
        if is_answer['changed']:
            answer['changed'] = True
        return answer

    @click.option('--credential',
                  type=types.Related('credential'),
                  required=False)
    @click.option('--source',
                  type=click.Choice(INVENTORY_SOURCES),
                  help='The source to use for this group.')
    @click.option('--source-regions', help='Regions for your cloud provider.')
    # Options may not be valid for certain types of cloud servers
    @click.option('--source-vars',
                  help='Override variables found on source '
                  'with variables defined in this field.')
    @click.option('--overwrite',
                  type=bool,
                  help='Delete child groups and hosts not found in source.')
    @click.option('--overwrite-vars',
                  type=bool,
                  help='Override vars in child groups and hosts with those '
                  'from the external source.')
    @click.option('--update-on-launch',
                  type=bool,
                  help='Refersh inventory '
                  'data from its source each time a job is run.')
    def modify(self, pk=None, create_on_missing=False, **kwargs):
        """Modify a group and, if necessary, the inventory source within
        the group.
        """
        # Break out the options for the group vs its inventory_source
        group_fields = [f.name for f in self.fields]
        is_kwargs = {}
        for field in kwargs.copy():
            if field not in group_fields:
                is_kwargs[field] = kwargs.pop(field)

        # Handle alias for "manual" source
        if is_kwargs.get('source', None) == 'manual':
            is_kwargs['source'] = ''

        # First, modify the group.
        answer = super(Resource,
                       self).modify(pk=pk,
                                    create_on_missing=create_on_missing,
                                    **kwargs)

        # If the group already exists and we aren't supposed to make changes,
        # then we're done.
        if len(is_kwargs) == 0:
            return answer

        # Get the inventory source ID ("isid").
        # Inventory sources are not created directly; rather, one was created
        # automatically when the group was created.
        isid = self._get_inventory_source_id(answer)

        # We now have our inventory source ID; modify it according to the
        # provided parameters.
        #
        # Note: Any fields that were part of the group modification need
        # to be expunged from kwargs before making this call.
        isrc = get_resource('inventory_source')
        is_answer = isrc.write(pk=isid, force_on_exists=True, **is_kwargs)

        # If either the inventory_source or the group objects were modified
        # then refelect this in the output to avoid confusing the user.
        if is_answer['changed']:
            answer['changed'] = True
        return answer

    @resources.command(ignore_defaults=True, no_args_is_help=False)
    @click.option('--root',
                  is_flag=True,
                  default=False,
                  help='Show only root groups (groups with no parent groups) '
                  'within the given inventory.')
    def list(self, root=False, **kwargs):
        """Return a list of groups."""

        # Sanity check: If we got `--root` and no inventory, that's an
        # error.
        if root and not kwargs.get('inventory', None):
            raise exc.UsageError('The --root option requires specifying an '
                                 'inventory also.')

        # If we are tasked with getting root groups, do that.
        if root:
            inventory_id = kwargs['inventory']
            r = client.get('/inventories/%d/root_groups/' % inventory_id)
            return r.json()

        # Return the superclass implementation.
        return super(Resource, self).list(**kwargs)

    @click.argument('group', type=types.Related('group'))
    @click.option('--monitor',
                  is_flag=True,
                  default=False,
                  help='If sent, immediately calls `monitor` on the newly '
                  'launched job rather than exiting with a success.')
    @click.option('--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 sync(self, group, monitor=False, timeout=None, **kwargs):
        """Update the given group's inventory source."""

        isrc = get_resource('inventory_source')
        isid = self._get_inventory_source_id(group)
        return isrc.update(isid, monitor=monitor, timeout=timeout, **kwargs)

    def _get_inventory_source_id(self, group):
        """Return the inventory source ID given a group dictionary returned
        from the Tower API.
        """
        # If we got a group ID rather than a group, get the group.
        if isinstance(group, int):
            group = self.get(group)

        # Return the inventory source ID.
        return int(group['related']['inventory_source'].split('/')[-2])
示例#29
0
class Resource(models.MonitorableResource):
    cli_help = 'Manage inventory sources within Ansible Tower.'
    endpoint = '/inventory_sources/'
    internal = True

    credential = models.Field(type=types.Related('credential'), required=False)
    source = models.Field(
        default='manual',
        help_text='The type of inventory source in use.',
        type=click.Choice(['manual', 'ec2', 'rax', 'gce', 'azure']),
    )

    @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('--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, 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:
            return self.monitor(inventory_source, timeout=timeout)

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

    @resources.command
    @click.option('--detail',
                  is_flag=True,
                  default=False,
                  help='Print more detail.')
    def status(self, pk, detail=False):
        """Print the current job status."""
        # Get the job from Ansible Tower.
        debug.log('Asking for inventory source status.', header='details')
        inv_src = client.get('/inventory_sources/%d/' % pk).json()

        # Determine the appropriate inventory source update.
        if 'current_update' in inv_src['related']:
            debug.log('A current update exists; retrieving it.',
                      header='details')
            job = client.get(inv_src['related']['current_update'][7:]).json()
        elif inv_src['related'].get('last_update', None):
            debug.log(
                'No current update exists; retrieving the most '
                'recent update.',
                header='details')
            job = client.get(inv_src['related']['last_update'][7:]).json()
        else:
            raise exc.NotFound('No inventory source updates exist.')

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

        # Print just the information we need.
        return adict({
            'elapsed': job['elapsed'],
            'failed': job['failed'],
            'status': job['status'],
        })
示例#30
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)

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

    @resources.command(use_fields_as_options=(
        'job_explanation',
        'job_type',
        'inventory',
        'machine_credential',
        'cloud_credential',
        'module_name',
        'module_args',
        'forks',
        'limit',
        'verbosity',
        'become_enabled',
    ))
    @click.option('--monitor',
                  is_flag=True,
                  default=False,
                  help='If sent, immediately calls `monitor` on the newly '
                  'launched command rather than exiting with a success.')
    @click.option('--timeout',
                  required=False,
                  type=int,
                  help='If provided with --monitor, this attempt'
                  ' will time out after the given number of seconds. '
                  'Does nothing if --monitor is not sent.')
    @click.option('--become',
                  required=False,
                  is_flag=True,
                  help='If used, privledge escalation will be enabled for '
                  'this command.')
    def launch(self, monitor=False, timeout=None, become=False, **kwargs):
        """Launch a new ad-hoc command.

        Runs a user-defined command from Ansible Tower, immediately starts it,
        and returns back an ID in order for its status to be monitored.
        """
        # This feature only exists for versions 2.2 and up
        r = client.get('/')
        if 'ad_hoc_commands' not in r.json():
            raise exc.TowerCLIError('Your host is running an outdated version'
                                    'of Ansible Tower that can not run '
                                    'ad-hoc commands (2.2 or earlier)')

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

        # Change the flag to the dictionary format
        if become:
            kwargs['become_enabled'] = True

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

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

        # Return the command ID and other response data
        answer = OrderedDict((
            ('changed', True),
            ('id', command_id),
        ))
        answer.update(result.json())
        return answer