Пример #1
0
def string_to_dict(var_string, allow_kv=True, require_dict=True):
    """Returns a dictionary given a string with yaml or json syntax.
    If data is not present in a key: value format, then it return
    an empty dictionary.

    Attempts processing string by 3 different methods in order:
        1. as JSON      2. as YAML      3. as custom key=value syntax
    Throws an error if all of these fail in the standard ways."""
    # try:
    #     # Accept all valid "key":value types of json
    #     return_dict = json.loads(var_string)
    #     assert type(return_dict) is dict
    # except (TypeError, AttributeError, ValueError, AssertionError):
    try:
        # Accept all JSON and YAML
        return_dict = yaml.load(var_string, Loader=yaml.SafeLoader)
        if require_dict:
            assert type(return_dict) is dict
    except (AttributeError, yaml.YAMLError, AssertionError):
        # if these fail, parse by key=value syntax
        try:
            assert allow_kv
            return_dict = parse_kv(var_string)
        except Exception:
            raise exc.TowerCLIError('failed to parse some of the extra '
                                    'variables.\nvariables: \n%s' % var_string)
    return return_dict
Пример #2
0
def version():
    """Display full version information."""

    # Print out the current version of Tower CLI.
    click.echo('Tower CLI %s' % __version__)

    # Print out the current API version of the current code base.
    click.echo('API %s' % CUR_API_VERSION)

    # Attempt to connect to the Ansible Tower server.
    # If we succeed, print a version; if not, generate a failure.
    try:
        r = client.get('/config/')
    except RequestException as ex:
        raise exc.TowerCLIError('Could not connect to Ansible Tower.\n%s' %
                                six.text_type(ex))
    config = r.json()
    license = config.get('license_info', {}).get('license_type', 'open')
    if license == 'open':
        server_type = 'AWX'
    else:
        server_type = 'Ansible Tower'
    click.echo('%s %s' % (server_type, config['version']))

    # Print out Ansible version of server
    click.echo('Ansible %s' % config['ansible_version'])
Пример #3
0
    def create(self, fail_on_found=False, force_on_exists=False, **kwargs):
        """Create a new label.

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

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

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

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

        =====API DOCS=====
        """
        jt_id = kwargs.pop('job_template', None)
        old_endpoint = self.endpoint
        if jt_id is not None:
            jt = get_resource('job_template')
            jt.get(pk=jt_id)
            try:
                label_id = self.get(name=kwargs.get('name', None),
                                    organization=kwargs.get(
                                        'organization', None))['id']
            except exc.NotFound:
                pass
            else:
                if fail_on_found:
                    raise exc.TowerCLIError(
                        'Label already exists and fail-on-found is switched on. Please use'
                        ' "associate_label" method of job_template instead.')
                else:
                    debug.log(
                        'Label already exists, associating with job template.',
                        header='details')
                    return jt.associate_label(job_template=jt_id,
                                              label=label_id)
            self.endpoint = '/job_templates/%d/labels/' % jt_id
        result = super(Resource, self).create(fail_on_found=fail_on_found,
                                              force_on_exists=force_on_exists,
                                              **kwargs)
        self.endpoint = old_endpoint
        return result
Пример #4
0
def logout():
    """
    Removes an OAuth2 personal auth token from config.
    """
    if not supports_oauth():
        raise exc.TowerCLIError(
            'This version of Tower does not support OAuth2.0')
    config.main(['oauth_token', '--unset', '--scope=user'])
Пример #5
0
def login(username, password, scope, client_id, client_secret, verbose):
    """
    Retrieves and stores an OAuth2 personal auth token.
    """
    if not supports_oauth():
        raise exc.TowerCLIError(
            'This version of Tower does not support OAuth2.0. Set credentials using tower-cli config.'
        )

    # Explicitly set a basic auth header for PAT acquisition (so that we don't
    # try to auth w/ an existing user+pass or oauth2 token in a config file)

    req = collections.namedtuple('req', 'headers')({})
    if client_id and client_secret:
        HTTPBasicAuth(client_id, client_secret)(req)
        req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
        r = client.post('/o/token/',
                        data={
                            "grant_type": "password",
                            "username": username,
                            "password": password,
                            "scope": scope
                        },
                        headers=req.headers)
    elif client_id:
        req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
        r = client.post('/o/token/',
                        data={
                            "grant_type": "password",
                            "username": username,
                            "password": password,
                            "client_id": client_id,
                            "scope": scope
                        },
                        headers=req.headers)
    else:
        HTTPBasicAuth(username, password)(req)
        r = client.post('/users/{}/personal_tokens/'.format(username),
                        data={
                            "description": "Tower CLI",
                            "application": None,
                            "scope": scope
                        },
                        headers=req.headers)

    if r.ok:
        result = r.json()
        result.pop('summary_fields', None)
        result.pop('related', None)
        if client_id:
            token = result.pop('access_token', None)
        else:
            token = result.pop('token', None)
        if settings.verbose:
            # only print the actual token if -v
            result['token'] = token
        secho(json.dumps(result, indent=1), fg='blue', bold=True)
        config.main(['oauth_token', token, '--scope=user'])
Пример #6
0
    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
Пример #7
0
 def set_base_url(self, user, team):
     """Assure that endpoint is nested under a user or team"""
     if self.no_lookup_flag:
         return
     if user:
         self.endpoint = '/users/%d/permissions/' % user
     elif team:
         self.endpoint = '/teams/%d/permissions/' % team
     else:
         raise exc.TowerCLIError('Specify either a user or a team.')
     self.no_lookup_flag = True
Пример #8
0
 def prefix(self):
     """Return the appropriate URL prefix to prepend to requests,
     based on the host provided in settings.
     """
     host = settings.host
     if '://' not in host:
         host = 'https://%s' % host.strip('/')
     elif host.startswith('http://') and settings.verify_ssl:
         raise exc.TowerCLIError(
             'Can not verify ssl with non-https protocol. Change the '
             'verify_ssl configuration setting to continue.')
     return '%s/api/%s/' % (host.rstrip('/'), CUR_API_VERSION)
Пример #9
0
def version(**kwargs):
    """Display version information."""

    # Print out the current version of Tower CLI.
    click.echo('Tower CLI %s' % __version__)

    # Attempt to connect to the Ansible Tower server.
    # If we succeed, print a version; if not, generate a failure.
    try:
        r = client.get('/config/')
        click.echo('Ansible Tower %s' % r.json()['version'])
    except RequestException as ex:
        raise exc.TowerCLIError('Could not connect to Ansible Tower.\n%s' %
                                six.text_type(ex))
Пример #10
0
    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_template(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
 def get_prefix(self, include_version=True):
     """Return the appropriate URL prefix to prepend to requests,
     based on the host provided in settings.
     """
     host = settings.host
     if '://' not in host:
         host = 'https://%s' % host.strip('/')
     elif host.startswith('http://') and settings.verify_ssl:
         raise exc.TowerCLIError(
             'Can not verify ssl with non-https protocol. Change the '
             'verify_ssl configuration setting to continue.')
     prefix = os.path.sep.join([host.rstrip('/'), 'api', ''])
     if include_version:
         prefix = os.path.sep.join(
             [prefix.rstrip('/'), CUR_API_VERSION, ''])
     return prefix
Пример #12
0
 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
Пример #13
0
    def launch(self,
               monitor=False,
               wait=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)
        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
Пример #14
0
 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']
Пример #15
0
    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
Пример #16
0
    def cancel(self, pk=None, fail_if_not_running=False, **kwargs):
        """Cancel a currently running job.

        Fails with a non-zero exit status if the job cannot be canceled.
        You must provide either a pk or parameters in the job's identity.
        """
        # Search for the record if pk not given
        if not pk:
            existing_data = self.get(**kwargs)
            pk = existing_data['id']

        cancel_endpoint = '%s%s/cancel/' % (self.endpoint, pk)
        # Attempt to cancel the job.
        try:
            client.post(cancel_endpoint)
            changed = True
        except exc.MethodNotAllowed:
            changed = False
            if fail_if_not_running:
                raise exc.TowerCLIError('Job not running.')

        # Return a success.
        return {'status': 'canceled', 'changed': changed}
 def coerce_type(self, key, value):
     if key == 'LICENSE':
         return json.loads(value)
     r = client.options(self.endpoint)
     if key not in r.json()['actions']['PUT']:
         raise exc.TowerCLIError('You are trying to modify value of a '
                                 'Read-Only field, which is not allowed')
     to_type = r.json()['actions']['PUT'].get(key, {}).get('type')
     if to_type == 'integer':
         if value != 'null':
             return int(value)
         else:
             return None
     elif to_type == 'boolean':
         return bool(strtobool(value))
     elif to_type in ('list', 'nested object'):
         try:
             return json.loads(value)
         except Exception:
             debug.log('Could not parse value as JSON, trying as python.',
                       header='details')
             return ast.literal_eval(value)
     return value
Пример #18
0
    def get_prefix(self, include_version=True):
        """Return the appropriate URL prefix to prepend to requests,
        based on the host provided in settings.
        """
        host = settings.host
        if '://' not in host:
            host = 'https://%s' % host.strip('/')
        elif host.startswith('http://') and settings.verify_ssl:
            raise exc.TowerCLIError(
                'Can not verify ssl with non-https protocol. Change the '
                'verify_ssl configuration setting to continue.')
        # Validate that we have either an http or https based URL
        url_pieces = urlparse(host)
        if url_pieces[0] not in ['http', 'https']:
            raise exc.ConnectionError(
                'URL must be http(s), {} is not valid'.format(url_pieces[0]))

        prefix = urljoin(host, '/api/')
        if include_version:
            # We add the / to the end of {} so that our URL has the ending slash.
            prefix = urljoin(prefix, "{}/".format(CUR_API_VERSION))

        return prefix
Пример #19
0
def config(key=None, value=None, scope='user', global_=False, unset=False):
    """Read or write tower-cli configuration.

    `tower config` saves the given setting to the appropriate Tower CLI;
    either the user's ~/.tower_cli.cfg file, or the /etc/tower/tower_cli.cfg
    file if --global is used.

    Writing to /etc/tower/tower_cli.cfg is likely to require heightened
    permissions (in other words, sudo).
    """
    # If the old-style `global_` option is set, issue a deprecation notice.
    if global_:
        scope = 'global'
        warnings.warn(
            'The `--global` option is deprecated and will be '
            'removed. Use `--scope=global` to get the same effect.',
            DeprecationWarning)

    # If no key was provided, print out the current configuration
    # in play.
    if not key:
        seen = set()
        parser_desc = {
            'runtime':
            'Runtime options.',
            'environment':
            'Options from environment variables.',
            'local':
            'Local options (set with `tower-cli config '
            '--scope=local`; stored in .tower_cli.cfg of this '
            'directory or a parent)',
            'user':
            '******'
            '~/.tower_cli.cfg).',
            'global':
            'Global options (set with `tower-cli config '
            '--scope=global`, stored in /etc/tower/tower_cli.cfg).',
            'defaults':
            'Defaults.',
        }

        # Iterate over each parser (English: location we can get settings from)
        # and print any settings that we haven't already seen.
        #
        # We iterate over settings from highest precedence to lowest, so any
        # seen settings are overridden by the version we iterated over already.
        click.echo('')
        for name, parser in zip(settings._parser_names, settings._parsers):
            # Determine if we're going to see any options in this
            # parser that get echoed.
            will_echo = False
            for option in parser.options('general'):
                if option in seen:
                    continue
                will_echo = True

            # Print a segment header
            if will_echo:
                secho('# %s' % parser_desc[name], fg='green', bold=True)

            # Iterate over each option in the parser and, if we haven't
            # already seen an option at higher precedence, print it.
            for option in parser.options('general'):
                if option in seen:
                    continue
                _echo_setting(option)
                seen.add(option)

            # Print a nice newline, for formatting.
            if will_echo:
                click.echo('')
        return

    # Sanity check: Is this a valid configuration option? If it's not
    # a key we recognize, abort.
    if not hasattr(settings, key):
        raise exc.TowerCLIError('Invalid configuration option "%s".' % key)

    # Sanity check: The combination of a value and --unset makes no
    # sense.
    if value and unset:
        raise exc.UsageError('Cannot provide both a value and --unset.')

    # If a key was provided but no value was provided, then just
    # print the current value for that key.
    if key and not value and not unset:
        _echo_setting(key)
        return

    # Okay, so we're *writing* a key. Let's do this.
    # First, we need the appropriate file.
    filename = os.path.expanduser('~/.tower_cli.cfg')
    if scope == 'global':
        if not os.path.isdir('/etc/tower/'):
            raise exc.TowerCLIError('/etc/tower/ does not exist, and this '
                                    'command cowardly declines to create it.')
        filename = '/etc/tower/tower_cli.cfg'
    elif scope == 'local':
        filename = '.tower_cli.cfg'

    # Read in the appropriate config file, write this value, and save
    # the result back to the file.
    parser = Parser()
    parser.add_section('general')
    parser.read(filename)
    if unset:
        parser.remove_option('general', key)
    else:
        parser.set('general', key, value)
    with open(filename, 'w') as config_file:
        parser.write(config_file)

    # Give rw permissions to user only fix for issue number 48
    try:
        os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR)
    except Exception as e:
        warnings.warn(
            'Unable to set permissions on {0} - {1} '.format(filename, e),
            UserWarning)
    click.echo('Configuration updated successfully.')
Пример #20
0
    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).

        =====API DOCS=====
        Create an object.

        :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=====
        """
        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_template(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