def write_to_file(self, filename, formatter=None, json=False): """Write results to file Arguments filename: target filename to write formatter: callback to format the file entry in text files json: if set, formatter is ignored and self.to_json is used to write file Either formatter callback or json=True is required Raises RunnerError if file writing failed. """ if not formatter and not json: raise RunnerError( 'Either formatter callback or json flag must be set') try: fd = open(filename, 'w') if json: fd.write('%s\n' % self.to_json()) elif formatter: for result in self.results['contacted']: if result.module_name == 'setup' and not self.runner.show_facts: continue fd.write('%s\n' % formatter(result)) for result in self.results['dark']: if result.module_name == 'setup' and not self.runner.show_facts: continue fd.write('%s\n' % formatter(result)) fd.close() except IOError, (ecode, emsg): raise RunnerError('Error writing file %s: %s' % (filename, emsg))
def __get_datetime_property__(self, key): """Parse cached datetime property Attempts to retrieve given property and parse it as datetime using RESULT_DATE_FORMAT format. Returns None if value was not found or invalid, and datetime object if value was found and could be parsed. Successfully parsed value is stored as cached property and not calculated again. """ cached = self.__get_cached_property__(key) if cached: return cached value = self.get(key, None) if value is not None: try: value = datetime.strptime(value, RESULT_DATE_FORMAT) except ValueError: raise RunnerError('Error parsing %w date value %s' (key, value)) else: return None self.__set_cached_property__(key, value) return value
def run(self, args): runner = self.runner_class( playbook=args.playbook, host_list=os.path.realpath(args.inventory), module_path=args.module_path, forks='%d' % args.forks, timeout=args.timeout, remote_user=args.user, remote_pass=args.remote_pass, sudo_pass=args.sudo_pass, remote_port=args.port, transport='smart', private_key_file=args.private_key, sudo=args.sudo, sudo_user=args.sudo_user, extra_vars=None, only_tags=None, skip_tags=None, subset=None, inventory=None, check=False, diff=False, any_errors_fatal=False, vault_password=False, force_handlers=False, show_colors=args.colors, show_facts=args.show_facts, ) try: return runner.run() except AnsibleError, emsg: raise RunnerError(emsg)
def create_directory(directory): """Create directory Wrapper to attempt creating directory unless it exists. Raises RunnerError if any errors happen. """ if os.path.isdir(directory): logger.debug('directory already exists: %s' % directory) return try: os.makedirs(directory) except IOError, (ecode, emsg): raise RunnerError('Error creating directory %s: %s' % (directory, emsg))
def write_to_directory(self, directory, formatter, extension): """Write file to directory with formatter callback Write result to given directory with path like: directory/<self.host>.<extension> Callback is used for formatting of the text in the file. Raises RunnerError if file writing failed. """ filename = os.path.join(directory, '%s.%s' % (self.host, extension)) self.log.debug('writing to %s' % filename) try: open(filename, 'w').write('%s\n' % formatter(self)) except IOError, (ecode, emsg): raise RunnerError('Error writing file %s: %s' % (filename, emsg))
def run(self, args): runner = self.runner_class( host_list=os.path.realpath(args.inventory), module_path=args.module_path, module_name=args.module, module_args=args.args, forks='%d' % args.forks, timeout=args.timeout, pattern=args.pattern, remote_user=args.user, remote_pass=args.remote_pass, remote_port=args.port, private_key_file=args.private_key, su=args.su, sudo=args.sudo, sudo_user=args.sudo_user, sudo_pass=args.sudo_pass, show_colors=args.colors, ) try: return runner.run() except AnsibleError, emsg: raise RunnerError(emsg)
class PlaybookResults(ResultList, AggregateStats): """Playbook results Collect results from playbook runs, grouped by host """ def __init__(self, runner, show_colors=False): AggregateStats.__init__(self) ResultList.__init__(self, runner, show_colors) @property def grouped_by_host(self): """Return task output grouped by host Collect task output from different hosts in contacted and dark groups and return as dictionary, where each result is grouped under the host. """ data = {'contacted': [], 'dark': []} res = {} for result in self.results['contacted']: if result.host not in res: res[result.host] = {'host': result.host, 'results': []} if result.module_name == 'setup' and not self.runner.show_facts: continue res[result.host]['results'].append(result.copy()) for key in sorted(res.keys()): data['contacted'].append(res[key]) res = {} for result in self.results['dark']: if result.host not in res: res[result.host] = {'host': result.host, 'results': []} if result.module_name == 'setup' and not self.runner.show_facts: continue res[result.host]['results'].append(result.copy()) for key in sorted(res.keys()): data['dark'].append(res[key]) return data def compute(self, runner_results, setup=False, poll=False, ignore_errors=False): """Import results Override for default ansible playbook callback to import task results to main process. You can't directly write from tasks without callback to main process because they are running in separate processes launched by multiprocess module. """ for (host, value) in runner_results.get('contacted', {}).iteritems(): self.results['contacted'].append(host, value) for (host, value) in runner_results.get('dark', {}).iteritems(): self.results['dark'].append(host, value) def summarize(self, host): """Return summary Return summary for a host. TODO - fix this function to actually return what is expected. Right now it does not and exists only to override ansible playbook default APIs """ return { 'contacted': self.results['contacted'], 'dark': self.results['dark'], } def to_json(self, indent=2): """Return as json Returns data in json format using self.grouped_by_host for ordering. """ return json.dumps(self.grouped_by_host, indent=indent) def write_to_file(self, filename, formatter=None, json=False): """Write results to file Arguments filename: target filename to write formatter: callback to format the file entry in text files json: if set, formatter is ignored and self.to_json is used to write file Either formatter callback or json=True is required Raises RunnerError if file writing failed. """ if not formatter and not json: raise RunnerError( 'Either formatter callback or json flag must be set') try: fd = open(filename, 'w') if json: fd.write('%s\n' % self.to_json()) elif formatter: for result in self.results['contacted']: if result.module_name == 'setup' and not self.runner.show_facts: continue fd.write('%s\n' % formatter(result)) for result in self.results['dark']: if result.module_name == 'setup' and not self.runner.show_facts: continue fd.write('%s\n' % formatter(result)) fd.close() except IOError, (ecode, emsg): raise RunnerError('Error writing file %s: %s' % (filename, emsg)) except OSError, (ecode, emsg): raise RunnerError('Error writing file %s: %s' % (filename, emsg))
class ResultList(object): """List of results Parent class for collected results, with two result sets in self.results: self.results['contacted'] = result set of contacted hosts self.results['dark'] = result set of unreachable hosts Note: a host may be in both sets if it was unreachable in middle of a playbook """ def __init__(self, runner, show_colors=False): self.log = Logger().default_stream self.runner = runner self.show_colors = show_colors self.results = { 'contacted': self.resultset_loader(self, 'contacted'), 'dark': self.resultset_loader(self, 'dark'), } @property def resultset_loader(self): return self.runner.resultset_loader def sort(self): """Sort results Sorts the ResultSets contacted and dark """ self.results['dark'].sort() self.results['contacted'].sort() def to_json(self, indent=2): """Return as json Returns all results formatted to json """ return json.dumps( { 'contacted': self.results['contacted'], 'dark': self.results['dark'], }, indent=indent) def write_to_file(self, filename, formatter=None, json=False): """Write results to file Arguments filename: target filename to write formatter: callback to format the file entry in text files json: if set, formatter is ignored and self.to_json is used to write file Either formatter callback or json=True is required Raises RunnerError if file writing failed. """ if not formatter and not json: raise RunnerError( 'Either formatter callback or json flag must be set') try: fd = open(filename, 'w') if json: fd.write('%s\n' % self.to_json()) elif formatter: for result in self.results['contacted']: fd.write('%s\n' % formatter(result)) for result in self.results['dark']: fd.write('%s\n' % formatter(result)) fd.close() except IOError, (ecode, emsg): raise RunnerError('Error writing file %s: %s' % (filename, emsg)) except OSError, (ecode, emsg): raise RunnerError('Error writing file %s: %s' % (filename, emsg))
class Result(SortedDict): """Ansible result Single result from ansible command or playbook """ compare_fields = ( 'resultset', 'address', 'host', ) def __init__(self, resultset, host, data): SortedDict.__init__(self) self.resultset = resultset self.host = host self.__cached_properties__ = {} try: self.address = IPv4Address(host) except ValueError: self.address = None self.update(**data) def __repr__(self): if 'end' in self: return ' '.join([ self.host, self.status, self.end.strftime('%Y-%m-%d %H:%M:%S') ]) else: return ' '.join([self.host, self.status]) def __set_cached_property__(self, key, value): """Set cached property value Store given value for key to self.__cached_properties__. Returns nothing. """ self.__cached_properties__[key] = value def __get_cached_property__(self, key): """Get cached properties Return cached property value from self.__cached_properties__ or None if key was not stored. """ return self.__cached_properties__.get(key, None) def __get_datetime_property__(self, key): """Parse cached datetime property Attempts to retrieve given property and parse it as datetime using RESULT_DATE_FORMAT format. Returns None if value was not found or invalid, and datetime object if value was found and could be parsed. Successfully parsed value is stored as cached property and not calculated again. """ cached = self.__get_cached_property__(key) if cached: return cached value = self.get(key, None) if value is not None: try: value = datetime.strptime(value, RESULT_DATE_FORMAT) except ValueError: raise RunnerError('Error parsing %w date value %s' (key, value)) else: return None self.__set_cached_property__(key, value) return value def __parse_custom_status_codes__(self): """Parse custom status Override this function to parse status code from custom modules. By default this always returns 'unknown' """ return 'unknown' @property def show_colors(self): """Should be show colors Accessor to runner's show_colors flag. Used for result processing in callbacks """ return self.resultset.runner.show_colors @property def changed(self): """Return changed boolean flag If changed flag is found, return boolean test result for it, otherwise return False. This property always returns boolean, checking if value evaluates to True. """ value = self.get('changed', False) return value and True or False @property def returncode(self): """Return code Return key 'rc' as integer in range 0-255. If value is not integer or not in range, return 255. If 'rc' key is not found, return 0 """ try: return int(self.get('rc', 0)) if rc < 0 or rc > 255: raise ValueError except ValueError: return 255 @property def error(self): """Return error message or 'UNKNOWN ERROR' If error message is set in 'msg' key, return it, otherwise return 'UNKNOWN ERROR' string. This property does not check if result status actually was error, must returns the 'msg' key if found. """ return self.get('msg', 'UNKNOWN ERROR') @property def stdout(self): """Return stdout or '' Return stdout or empty string """ return self.get('stdout', '') @property def stderr(self): """Return stderr or '' Return stderr or empty string """ return self.get('stderr', '') @property def state(self): """Return result state Returns host state i.e. resultset name ('contacted', 'dark') """ return self.resultset.name @property def ansible_facts(self): """Return facts for host If ansible facts were collected, return the dictionary. Otherwise return None. """ return self.resultset.ansible_facts.get(self.host, None) @property def start(self): """Return start as datetime Return start end date as datetime or None if not found """ return self.__get_datetime_property__('start') @property def end(self): """Return end as datetime Return task end date as datetime or None if not found """ return self.__get_datetime_property__('end') @property def delta(self): """Return end - start timedelta value Calculate the 'delta' field again, returning proper datetime.timedelta value from self['end'] - self['start'] instead of string in dictionary. Returns None if either start or end is not a datetime object. """ cached = self.__get_cached_property__('delta') if cached: return cached end = self.end start = self.start if not isinstance(end, datetime) or not isinstance(start, datetime): return None value = end - start self.__set_cached_property__('delta', value) return value @property def module_name(self): """Module name Return invocated module name or empty string if not available. Successfully parsed value is stored as cached property and not calculated again. """ cached = self.__get_cached_property__('module_name') if cached: return cached try: value = self['invocation']['module_name'] except KeyError: return '' self.__set_cached_property__('module_name', value) return value @property def module_args(self): """Module args Similar to command property, returning module_args, but always returns the module_args value or empty string, never returning module_name like self.command Successfully parsed value is stored as cached property and not calculated again. """ cached = self.__get_cached_property__('module_args') if cached: return cached try: value = self['invocation']['module_args'] except KeyError: return '' self.__set_cached_property__('module_args', value) return value @property def command(self): """Return executed command For shell and command moudles, return module_args from self['invocation']['module_args'] or empty string if not available. For any other module return module name. Successfully parsed value is stored as cached property and not calculated again. """ cached = self.__get_cached_property__('command') if cached: return cached if self.module_name in ('command', 'shell'): try: value = self['invocation']['module_args'] except KeyError: return '' else: value = self.module_name self.__set_cached_property__('command', value) return value @property def status(self): """Return result status Parse result status, depending on the module being run. Return values are: 'ok' shell/command returned code 0, ping returned 'pong' 'error' shell/command returned other code than 0 'failed' module run failed (result contains 'failed' key), ping did not return 'facts' ansible facts were parsed successfully 'pending_facts' ansible facts were expected but not received 'unknown' status could not be parsed To extend status parsing for custom modules, please implement the __parse_custom_status_codes__ function in child class. Successfully parsed status is stored as cached property and not calculated again. """ cached = self.__get_cached_property__('status') if cached: return cached if 'failed' in self: value = 'failed' elif 'rc' in self: if self.get('rc', 0) == 0: value = 'ok' else: value = 'error' elif self.module_name == 'ping': value = self.get('ping', None) == 'pong' and 'ok' or 'failed' elif self.module_name == 'setup': if self.get('ansible_facts', None): value = 'facts' else: return 'pending_facts' else: return self.__parse_custom_status_codes__() self.__set_cached_property__('status', value) return value @property def ansible_status(self): """Return ansible style status string Return status string as shown by ansible command output: success: command was successful FAILED: command failed """ if self.status == 'ok': return 'success' elif self.status in ( 'failed', 'error', ): return 'FAILED' return self.status def copy(self): return Result(self.resultset, self.host, self) def write_to_directory(self, directory, formatter, extension): """Write file to directory with formatter callback Write result to given directory with path like: directory/<self.host>.<extension> Callback is used for formatting of the text in the file. Raises RunnerError if file writing failed. """ filename = os.path.join(directory, '%s.%s' % (self.host, extension)) self.log.debug('writing to %s' % filename) try: open(filename, 'w').write('%s\n' % formatter(self)) except IOError, (ecode, emsg): raise RunnerError('Error writing file %s: %s' % (filename, emsg)) except OSError, (ecode, emsg): raise RunnerError('Error writing file %s: %s' % (filename, emsg))
Wrapper to attempt creating directory unless it exists. Raises RunnerError if any errors happen. """ if os.path.isdir(directory): logger.debug('directory already exists: %s' % directory) return try: os.makedirs(directory) except IOError, (ecode, emsg): raise RunnerError('Error creating directory %s: %s' % (directory, emsg)) except OSError, (ecode, emsg): raise RunnerError('Error creating directory %s: %s' % (directory, emsg)) class GenericAnsibleScript(Script): """Ansible script wrapper base class Extend systematic.shell.Script (which wraps argparse.ArgumentParser) to run ansible and playbook commands. This is just the base class for variants. """ def __init__(self, *args, **kwargs): Script.__init__(self, *args, **kwargs) self.runner = None self.mode = '' self.add_argument('--version', action='store_true', help='show version')
def on_no_hosts_matched(self): raise RunnerError('No hosts matched')