class Resource(models.ExeResource):
    """A resource for project updates.
    """
    cli_help = 'Launch or monitor project updates.'
    endpoint = '/project_updates/'

    project = models.Field(key='-P',
                           type=types.Related('project'),
                           required=True,
                           display=True)
    name = models.Field(required=False, display=True, read_only=True)
    launch_type = models.Field(type=click.Choice(LAUNCH_TYPE_CHOICES),
                               read_only=True,
                               display=False)
    status = models.Field(type=click.Choice(STATUS_CHOICES), read_only=True)
    job_type = models.Field(type=click.Choice(['run', 'check']),
                            read_only=True)
    job_explanation = models.Field(required=False,
                                   display=False,
                                   read_only=True)
    created = models.Field(required=False, display=True, read_only=True)
    elapsed = models.Field(required=False,
                           display=True,
                           read_only=True,
                           type=float)
    scm_type = models.Field(type=types.MappedChoice([
        ('', 'manual'),
        ('git', 'git'),
        ('hg', 'hg'),
        ('svn', 'svn'),
        ('insights', 'insights'),
    ]),
                            display=False)
Exemplo n.º 2
0
 def test_convert(self):
     """Establish that the convert method converts from the value
     provided to the user to the internal value, and calls the superclass
     method.
     """
     mc = types.MappedChoice({'foo': 'bar', 'spam': 'eggs'})
     self.assertEqual(mc.convert('bar', 'myopt', None), 'foo')
     self.assertEqual(mc.convert('eggs', 'myopt', None), 'spam')
Exemplo n.º 3
0
class Resource(models.ExeResource):
    """A resource for ad hoc commands."""
    cli_help = 'Launch commands based on playbook given at runtime.'
    endpoint = '/ad_hoc_commands/'

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

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

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

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

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

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

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

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

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

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

        # Return the command ID and other response data
        answer = OrderedDict((
            ('changed', True),
            ('id', command_id),
        ))
        answer.update(result.json())
        return answer
Exemplo n.º 4
0
class Resource(models.SurveyResource):
    """A resource for job templates."""
    cli_help = 'Manage job templates.'
    endpoint = '/job_templates/'

    name = models.Field(unique=True)
    description = models.Field(required=False, display=False)
    job_type = models.Field(
        required=False,
        display=False,
        type=click.Choice(['run', 'check']),
    )
    inventory = models.Field(type=types.Related('inventory'), required=False)
    project = models.Field(type=types.Related('project'))
    playbook = models.Field()
    credential = models.Field(display=False,
                              required=False,
                              type=types.Related('credential'))
    vault_credential = models.Field(type=types.Related('credential'),
                                    required=False,
                                    display=False)
    forks = models.Field(type=int, required=False, display=False)
    limit = models.Field(required=False, display=False)
    verbosity = models.Field(
        display=False,
        type=types.MappedChoice([
            (0, 'default'),
            (1, 'verbose'),
            (2, 'more_verbose'),
            (3, 'debug'),
            (4, 'connection'),
            (5, 'winrm'),
        ]),
        required=False,
    )
    extra_vars = models.Field(
        type=types.Variables(),
        required=False,
        display=False,
        multiple=True,
        help_text='Extra variables used by Ansible in YAML or key=value '
        'format. Use @ to get YAML from a file.')
    job_tags = models.Field(required=False, display=False)
    force_handlers = models.Field(type=bool, required=False, display=False)
    skip_tags = models.Field(required=False, display=False)
    start_at_task = models.Field(required=False, display=False)
    timeout = models.Field(
        type=int,
        required=False,
        display=False,
        help_text=
        'The amount of time (in seconds) to run before the task is canceled.')
    use_fact_cache = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='If enabled, Tower will act as an Ansible Fact Cache Plugin;'
        ' persisting facts at the end of a playbook run to the database'
        ' and caching facts for use by Ansible.')
    host_config_key = models.Field(
        required=False,
        display=False,
        help_text='Allow Provisioning Callbacks using this host config key')
    ask_diff_mode_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Ask diff mode on launch.')
    ask_variables_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for extra_vars on launch.')
    ask_limit_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for host limits on launch.')
    ask_tags_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for job tags on launch.')
    ask_skip_tags_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for tags to skip on launch.')
    ask_job_type_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for job type on launch.')
    ask_verbosity_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for verbosity on launch.')
    ask_inventory_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for inventory on launch.')
    ask_credential_on_launch = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for machine credential on launch.')
    survey_enabled = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='Prompt user for job type on launch.')
    become_enabled = models.Field(type=bool, required=False, display=False)
    diff_mode = models.Field(
        type=bool,
        required=False,
        display=False,
        help_text='If enabled, textual changes made to any templated files on'
        ' the host are shown in the standard output.')
    allow_simultaneous = models.Field(type=bool, required=False, display=False)

    survey_spec = models.Field(
        type=types.Variables(),
        required=False,
        display=False,
        help_text='On write commands, perform extra POST to the '
        'survey_spec endpoint.')

    def write(self, *args, **kwargs):
        # Provide a default value for job_type, but only in creation of JT
        if (kwargs.get('create_on_missing', False)
                and (not kwargs.get('job_type', None))):
            kwargs['job_type'] = 'run'
        return super(Resource, self).write(*args, **kwargs)

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

        =====API DOCS=====
        Associate an label with this job template.

        :param job_template: The job template to associate to.
        :type job_template: str
        :param label: The label to be associated.
        :type label: str
        :returns: Dictionary of only one key "changed", which indicates whether the association succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._assoc('labels', job_template, label)

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

        =====API DOCS=====
        Disassociate an label from this job template.

        :param job_template: The job template to disassociate from.
        :type job_template: str
        :param label: The label to be disassociated.
        :type label: str
        :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._disassoc('labels', job_template, label)

    @resources.command(use_fields_as_options=False)
    @click.option('--job-template', type=types.Related('job_template'))
    @click.option('--credential', type=types.Related('credential'))
    def associate_credential(self, job_template, credential):
        """Associate a credential with this job template.

        =====API DOCS=====
        Associate a credential with this job template.

        :param job_template: The job template to associate to.
        :type job_template: str
        :param credential: The credential to be associated.
        :type credential: str
        :returns: Dictionary of only one key "changed", which indicates whether the association succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._assoc('extra_credentials', job_template, credential)

    @resources.command(use_fields_as_options=False)
    @click.option('--job-template', type=types.Related('job_template'))
    @click.option('--credential', type=types.Related('credential'))
    def disassociate_credential(self, job_template, credential):
        """Disassociate a credential with this job template.

        =====API DOCS=====
        Disassociate a credential from this job template.

        :param job_template: The job template to disassociate fom.
        :type job_template: str
        :param credential: The credential to be disassociated.
        :type credential: str
        :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._disassoc('extra_credentials', job_template, credential)

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

        =====API DOCS=====
        Associate a notification template from this job template.

        :param job_template: The job template to associate to.
        :type job_template: str
        :param notification_template: The notification template to be associated.
        :type notification_template: str
        :param status: type of notification this notification template should be associated to.
        :type status: str
        :returns: Dictionary of only one key "changed", which indicates whether the association succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._assoc('notification_templates_%s' % status, job_template,
                           notification_template)

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

        =====API DOCS=====
        Disassociate a notification template from this job template.

        :param job_template: The job template to disassociate from.
        :type job_template: str
        :param notification_template: The notification template to be disassociated.
        :type notification_template: str
        :param status: type of notification this notification template should be disassociated from.
        :type status: str
        :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._disassoc('notification_templates_%s' % status,
                              job_template, notification_template)

    @resources.command(use_fields_as_options=('extra_vars'))
    @click.option('--host-config-key',
                  help='Job-template-specific string used to authenticate '
                  'host during provisioning callback.')
    def callback(self, pk=None, host_config_key='', extra_vars=None):
        """Contact Tower and request a configuration update using this job template.

        =====API DOCS=====
        Contact Tower and request a provisioning callback using this job template.

        :param pk: Primary key of the job template to run provisioning callback against.
        :type pk: int
        :param host_config_key: Key string used to authenticate the callback host.
        :type host_config_key: str
        :param extra_vars: Extra variables that are passed to provisioning callback.
        :type extra_vars: array of str
        :returns: A dictionary of a single key "changed", which indicates whether the provisioning callback
                  is successful.
        :rtype: dict

        =====API DOCS=====
        """
        url = self.endpoint + '%s/callback/' % pk
        if not host_config_key:
            host_config_key = client.get(url).json()['host_config_key']
        post_data = {'host_config_key': host_config_key}
        if extra_vars:
            post_data['extra_vars'] = parser.process_extra_vars(
                list(extra_vars), force_json=True)
        r = client.post(url, data=post_data, auth=None)
        if r.status_code == 201:
            return {'changed': True}

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

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

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

        =====API DOCS=====
        """
        return self._assoc('instance_groups', job_template, instance_group)

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

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

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

        =====API DOCS=====
        """
        return self._disassoc('instance_groups', job_template, instance_group)
class Resource(models.Resource):
    """A resource for schedules."""
    cli_help = 'Manage schedules within Ansible Tower.'
    endpoint = '/schedules/'

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

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

    # Schedule-specific fields.
    unified_job_template = models.Field(required=False,
                                        type=int,
                                        help_text='Integer used to display'
                                        ' unified job template in result, '
                                        'Do not use it for create/'
                                        'modify.')
    enabled = models.Field(required=False,
                           type=click.BOOL,
                           default=True,
                           help_text='Whether this schedule will be used.',
                           show_default=True)
    rrule = models.Field(required=False,
                         display=False,
                         help_text='Schedule rules specifications which is'
                         ' less than 255 characters.')

    # Prompts
    extra_data = models.Field(type=types.Variables(),
                              required=False,
                              display=False,
                              help_text='Extra data for '
                              'schedule rules in the form of a .json file.')
    inventory = models.Field(type=types.Related('inventory'),
                             required=False,
                             display=False)
    credentials = models.ManyToManyField('credential')
    job_type = models.Field(required=False, display=False)
    job_tags = models.Field(required=False, display=False)
    skip_tags = models.Field(required=False, display=False)
    limit = models.Field(required=False, display=False)
    diff_mode = models.Field(type=bool, required=False, display=False)
    verbosity = models.Field(
        display=False,
        type=types.MappedChoice([
            (0, 'default'),
            (1, 'verbose'),
            (2, 'more_verbose'),
            (3, 'debug'),
            (4, 'connection'),
            (5, 'winrm'),
        ]),
        required=False,
    )

    def _get_patch_url(self, url, pk):
        urlTokens = url.split('/')
        if len(urlTokens) > 3:
            # reconstruct url to prevent a rare corner case where resources
            # cannot be constructed independently. Open to modification if
            # API convention changes.
            url = '/'.join(urlTokens[:1] + urlTokens[-2:])
        return super(Resource, self)._get_patch_url(url, pk)
Exemplo n.º 6
0
class Resource(models.Resource):
    """A resource for workflow nodes."""
    cli_help = 'Manage nodes inside of a workflow job template.'
    endpoint = '/workflow_job_template_nodes/'
    identity = ('id', )

    workflow_job_template = models.Field(key='-W',
                                         type=types.Related('workflow'))
    unified_job_template = models.Field(required=False)

    # Prompts
    extra_data = models.Field(type=types.Variables(),
                              required=False,
                              display=False,
                              help_text='Extra data for '
                              'schedule rules in the form of a .json file.')
    inventory = models.Field(type=types.Related('inventory'),
                             required=False,
                             display=False)
    credential = models.Field(type=types.Related('credential'),
                              required=False,
                              display=False)
    credentials = models.ManyToManyField('credential')
    job_type = models.Field(required=False, display=False)
    job_tags = models.Field(required=False, display=False)
    skip_tags = models.Field(required=False, display=False)
    limit = models.Field(required=False, display=False)
    diff_mode = models.Field(type=bool, required=False, display=False)
    verbosity = models.Field(
        display=False,
        type=types.MappedChoice([
            (0, 'default'),
            (1, 'verbose'),
            (2, 'more_verbose'),
            (3, 'debug'),
            (4, 'connection'),
            (5, 'winrm'),
        ]),
        required=False,
    )

    def __new__(cls, *args, **kwargs):
        for attr in ['create', 'modify', 'list']:
            setattr(cls, attr, unified_job_template_options(getattr(cls,
                                                                    attr)))
        return super(Resource, cls).__new__(cls, *args, **kwargs)

    @staticmethod
    def _forward_rel_name(rel):
        return '{0}_nodes'.format(rel)

    @staticmethod
    def _reverse_rel_name(rel):
        return 'workflowjobtemplatenodes_{0}'.format(rel)

    def _parent_filter(self, parent, relationship, **kwargs):
        """
        Returns filtering parameters to limit a search to the children
        of a particular node by a particular relationship.
        """
        if parent is None or relationship is None:
            return {}
        parent_filter_kwargs = {}
        query_params = ((self._reverse_rel_name(relationship), parent), )
        parent_filter_kwargs['query'] = query_params
        if kwargs.get('workflow_job_template', None) is None:
            parent_data = self.read(pk=parent)['results'][0]
            parent_filter_kwargs['workflow_job_template'] = parent_data[
                'workflow_job_template']
        return parent_filter_kwargs

    @unified_job_template_options
    def _get_or_create_child(self, parent, relationship, **kwargs):
        ujt_pk = kwargs.get('unified_job_template', None)
        if ujt_pk is None:
            raise exceptions.BadRequest(
                'A child node must be specified by one of the options '
                'unified-job-template, job-template, project, or '
                'inventory-source')
        kwargs.update(self._parent_filter(parent, relationship, **kwargs))
        response = self.read(fail_on_no_results=False,
                             fail_on_multiple_results=False,
                             **kwargs)
        if len(response['results']) == 0:
            debug.log('Creating new workflow node.', header='details')
            return client.post(self.endpoint, data=kwargs).json()
        else:
            return response['results'][0]

    def _assoc_or_create(self, relationship, parent, child, **kwargs):
        if child is None:
            child_data = self._get_or_create_child(parent, relationship,
                                                   **kwargs)
            return child_data
        return self._assoc(self._forward_rel_name(relationship), parent, child)

    @resources.command
    @unified_job_template_options
    @click.argument('parent', type=types.Related('node'))
    @click.argument('child', type=types.Related('node'), required=False)
    def associate_success_node(self, parent, child=None, **kwargs):
        """Add a node to run on success.

        =====API DOCS=====
        Add a node to run on success.

        :param parent: Primary key of parent node to associate success node to.
        :type parent: int
        :param child: Primary key of child node to be associated.
        :type child: int
        :param `**kwargs`: Fields used to create child node if ``child`` is not provided.
        :returns: Dictionary of only one key "changed", which indicates whether the association succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._assoc_or_create('success', parent, child, **kwargs)

    @resources.command(use_fields_as_options=False)
    @click.argument('parent', type=types.Related('node'))
    @click.argument('child', type=types.Related('node'))
    def disassociate_success_node(self, parent, child):
        """Remove success node.
        The resulatant 2 nodes will both become root nodes.

        =====API DOCS=====
        Remove success node.

        :param parent: Primary key of parent node to disassociate success node from.
        :type parent: int
        :param child: Primary key of child node to be disassociated.
        :type child: int
        :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._disassoc(self._forward_rel_name('success'), parent, child)

    @resources.command
    @unified_job_template_options
    @click.argument('parent', type=types.Related('node'))
    @click.argument('child', type=types.Related('node'), required=False)
    def associate_failure_node(self, parent, child=None, **kwargs):
        """Add a node to run on failure.

        =====API DOCS=====
        Add a node to run on failure.

        :param parent: Primary key of parent node to associate failure node to.
        :type parent: int
        :param child: Primary key of child node to be associated.
        :type child: int
        :param `**kwargs`: Fields used to create child node if ``child`` is not provided.
        :returns: Dictionary of only one key "changed", which indicates whether the association succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._assoc_or_create('failure', parent, child, **kwargs)

    @resources.command(use_fields_as_options=False)
    @click.argument('parent', type=types.Related('node'))
    @click.argument('child', type=types.Related('node'))
    def disassociate_failure_node(self, parent, child):
        """Remove a failure node link.
        The resulatant 2 nodes will both become root nodes.

        =====API DOCS=====
        Remove a failure node link.

        :param parent: Primary key of parent node to disassociate failure node from.
        :type parent: int
        :param child: Primary key of child node to be disassociated.
        :type child: int
        :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._disassoc(self._forward_rel_name('failure'), parent, child)

    @resources.command
    @unified_job_template_options
    @click.argument('parent', type=types.Related('node'))
    @click.argument('child', type=types.Related('node'), required=False)
    def associate_always_node(self, parent, child=None, **kwargs):
        """Add a node to always run after the parent is finished.

        =====API DOCS=====
        Add a node to always run after the parent is finished.

        :param parent: Primary key of parent node to associate always node to.
        :type parent: int
        :param child: Primary key of child node to be associated.
        :type child: int
        :param `**kwargs`: Fields used to create child node if ``child`` is not provided.
        :returns: Dictionary of only one key "changed", which indicates whether the association succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._assoc_or_create('always', parent, child, **kwargs)

    @resources.command(use_fields_as_options=False)
    @click.argument('parent', type=types.Related('node'))
    @click.argument('child', type=types.Related('node'))
    def disassociate_always_node(self, parent, child):
        """Remove an always node link.
        The resultant 2 nodes will both become root nodes.

        =====API DOCS=====
        Remove an always node link.

        :param parent: Primary key of parent node to disassociate always node from.
        :type parent: int
        :param child: Primary key of child node to be disassociated.
        :type child: int
        :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded.
        :rtype: dict

        =====API DOCS=====
        """
        return self._disassoc(self._forward_rel_name('always'), parent, child)
Exemplo n.º 7
0
class Resource(models.Resource, models.MonitorableResource):
    """A resource for projects."""
    cli_help = 'Manage projects within Ansible Tower.'
    endpoint = '/projects/'
    unified_job_type = '/project_updates/'
    dependencies = ['organization', 'credential']
    related = ['notification_templates', 'schedules']

    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'),
        ('insights', 'insights'),
    ]),
                            required=False)
    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)
    scm_update_cache_timeout = models.Field(type=int,
                                            required=False,
                                            display=False)
    job_timeout = models.Field(type=int,
                               required=False,
                               display=False,
                               help_text='The timeout field (in seconds).')
    custom_virtualenv = models.Field(required=False, display=False)

    @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.

        =====API DOCS=====
        Create a project and, if related flags are set, monitor or wait the triggered initial project update.

        :param monitor: Flag that if set, immediately calls ``monitor`` on the newly triggered project update
                        rather than exiting with a success.
        :type monitor: bool
        :param wait: Flag that if set, monitor the status of the triggered project update, but do not print
                     while it is in progress.
        :type wait: bool
        :param timeout: If provided with ``monitor`` flag set, this attempt will time out after the given number
                        of seconds.
        :type timeout: bool
        :param fail_on_found: Flag that if set, the operation fails if an object matching the unique criteria
                              already exists.
        :type fail_on_found: bool
        :param force_on_exists: Flag that if set, then if a match is found on unique fields, other fields will
                                be updated to the provided values.; If unset, a match causes the request to be
                                a no-op.
        :type force_on_exists: bool
        :param `**kwargs`: Keyword arguments which, all together, will be used as POST body to create the
                           resource object.
        :returns: A dictionary combining the JSON output of the created resource, as well as two extra fields:
                  "changed", a flag indicating if the resource is created successfully; "id", an integer which
                  is the primary key of the created object.
        :rtype: dict

        =====API DOCS=====
        """
        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().get('actions', {}).get('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',
                               'custom_virtualenv'))
    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.

        =====API DOCS=====
        Modify an already existing project.

        :param pk: Primary key of the resource to be modified.
        :type pk: int
        :param create_on_missing: Flag that if set, a new object is created if ``pk`` is not set and objects
                                  matching the appropriate unique criteria is not found.
        :type create_on_missing: bool
        :param `**kwargs`: Keyword arguments which, all together, will be used as PATCH body to modify the
                           resource object. if ``pk`` is not set, key-value pairs of ``**kwargs`` which are
                           also in resource's identity will be used to lookup existing reosource.
        :returns: A dictionary combining the JSON output of the modified resource, as well as two extra fields:
                  "changed", a flag indicating if the resource is successfully updated; "id", an integer which
                  is the primary key of the updated object.
        :rtype: dict

        =====API DOCS=====
        """
        # 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.

        =====API DOCS=====
        Update the given project.

        :param pk: Primary key of the project to be updated.
        :type pk: int
        :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched project update
                        rather than exiting with a success.
        :type monitor: bool
        :param wait: Flag that if set, monitor the status of the project update, but do not print while it is
                     in progress.
        :type wait: bool
        :param timeout: If provided with ``monitor`` flag set, this attempt will time out after the given number
                        of seconds.
        :type timeout: int
        :param name: Name of the project to be updated if ``pk`` is not set.
        :type name: str
        :param organization: Primary key or name of the organization the project to be updated belonging to if
                             ``pk`` is not set.
        :type organization: str
        :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait``
                  call if ``wait`` flag is on; dictionary of "status" if none of the two flags are on.
        :rtype: dict
        :raises tower_cli.exceptions.CannotStartJob: When the project cannot be updated.

        =====API DOCS=====
        """
        # 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(project_update_id,
                                parent_pk=pk,
                                timeout=timeout)
        elif wait:
            return self.wait(project_update_id, parent_pk=pk, timeout=timeout)

        # Return the project update ID.
        return {
            'id': project_update_id,
            '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.

        =====API DOCS=====
        Print the status of the most recent update.

        :param pk: Primary key of the resource to retrieve status from.
        :type pk: int
        :param detail: Flag that if set, return the full JSON of the job resource rather than a status summary.
        :type detail: bool
        :param `**kwargs`: Keyword arguments used to look up resource object to retrieve status from if ``pk``
                           is not provided.
        :returns: full loaded JSON of the specified unified job if ``detail`` flag is on; trimed JSON containing
                  only "elapsed", "failed" and "status" fields of the unified job if ``detail`` flag is off.
        :rtype: dict
        =====API DOCS=====
        """
        # 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'],
        }
Exemplo n.º 8
0
class Resource(models.SurveyResource):
    cli_help = 'Manage job templates.'
    endpoint = '/job_templates/'

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

    def write(self, *args, **kwargs):
        # Provide a default value for job_type, but only in creation of JT
        if (kwargs.get('create_on_missing', False) and
                (not kwargs.get('job_type', None))):
            kwargs['job_type'] = 'run'
        return super(Resource, self).write(*args, **kwargs)

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

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

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

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

    @resources.command(use_fields_as_options=('extra_vars'))
    @click.option('--host-config-key', help='Job-template-specific string used to authenticate '
                  'host during provisioning callback.')
    def callback(self, pk=None, host_config_key='', extra_vars=None):
        """Contact Tower and request a configuration update using this job template."""
        url = self.endpoint + '%s/callback/' % pk
        if not host_config_key:
            host_config_key = client.get(url).json()['host_config_key']
        post_data = {'host_config_key': host_config_key}
        if extra_vars:
            post_data['extra_vars'] = parser.process_extra_vars(list(extra_vars), force_json=True)
        r = client.post(url, data=post_data, auth=None)
        if r.status_code == 201:
            return {'changed': True}
Exemplo n.º 9
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 escalate
    become_method = models.Field(
        display=False,
        help_text='Privilege 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)
Exemplo n.º 10
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'],
        }