def string_to_dict(var_string, allow_kv=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) 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: raise exc.TowerCLIError( 'failed to parse some of the extra ' 'variables.\nvariables: \n%s' % var_string ) return return_dict
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/v1/' % host.rstrip('/')
def create(self, fail_on_found=False, force_on_exists=False, **kwargs): """Create a notification template. All required configuration-related fields (required according to notification_type) must be provided. There are two types of notification template creation: isolatedly creating a new notification template and creating a new notification template under a job template. Here the two types are discriminated by whether to provide --job-template option. --status option controls more specific, job-run-status-related association. Fields in the resource's `identity` tuple are used for a lookup; if a match is found, then no-op (unless `force_on_exists` is set) but do not fail (unless `fail_on_found` is set). """ config_item = self._separate(kwargs) jt_id = kwargs.pop('job_template', None) status = kwargs.pop('status', 'any') old_endpoint = self.endpoint if jt_id is not None: jt = get_resource('job_template') jt.get(pk=jt_id) try: nt_id = self.get(**copy.deepcopy(kwargs))['id'] except exc.NotFound: pass else: if fail_on_found: raise exc.TowerCLIError('Notification template already ' 'exists and fail-on-found is ' 'switched on. Please use' ' "associate_notification" method' ' of job_template instead.') else: debug.log( 'Notification template already exists, ' 'associating with job template.', header='details') return jt.associate_notification(jt_id, nt_id, status=status) self.endpoint = '/job_templates/%d/notification_templates_%s/' %\ (jt_id, status) self._configuration(kwargs, config_item) result = super(Resource, self).create(**kwargs) self.endpoint = old_endpoint return result
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 cancel(self, pk, fail_if_not_running=False): """Cancel a currently running job. Fails with a non-zero exit status if the job cannot be canceled. """ # Attempt to cancel the job. try: client.post('/jobs/%d/cancel/' % pk) changed = True except exc.MethodNotAllowed: changed = False if fail_if_not_running: raise exc.TowerCLIError('Job not running.') # Return a success. return adict({'status': 'canceled', 'changed': changed})
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 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 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%d/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 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.', '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.')