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