class PkgMgr(with_metaclass(ABCMeta, object)): @abstractmethod def is_available(self): # This method is supposed to return True/False if the package manager is currently installed/usable # It can also 'prep' the required systems in the process of detecting availability pass @abstractmethod def list_installed(self): # This method should return a list of installed packages, each list item will be passed to get_package_details pass @abstractmethod def get_package_details(self, package): # This takes a 'package' item and returns a dictionary with the package information, name and version are minimal requirements pass def get_packages(self): # Take all of the above and return a dictionary of lists of dictionaries (package = list of installed versions) installed_packages = {} for package in self.list_installed(): package_details = self.get_package_details(package) if 'source' not in package_details: package_details['source'] = self.__class__.__name__.lower() name = package_details['name'] if name not in installed_packages: installed_packages[name] = [package_details] else: installed_packages[name].append(package_details) return installed_packages
class AssiblePlugin(with_metaclass(ABCMeta, object)): # allow extra passthrough parameters allow_extras = False def __init__(self): self._options = {} def get_option(self, option, hostvars=None): if option not in self._options: try: option_value = C.config.get_config_value( option, plugin_type=get_plugin_class(self), plugin_name=self._load_name, variables=hostvars) except AssibleError as e: raise KeyError(to_native(e)) self.set_option(option, option_value) return self._options.get(option) def set_option(self, option, value): self._options[option] = value def set_options(self, task_keys=None, var_options=None, direct=None): ''' Sets the _options attribute with the configuration/keyword information for this plugin :arg task_keys: Dict with playbook keywords that affect this option :arg var_options: Dict with either 'connection variables' :arg direct: Dict with 'direct assignment' ''' self._options = C.config.get_plugin_options(get_plugin_class(self), self._load_name, keys=task_keys, variables=var_options, direct=direct) # allow extras/wildcards from vars that are not directly consumed in configuration # this is needed to support things like winrm that can have extended protocol options we don't directly handle if self.allow_extras and var_options and '_extras' in var_options: self.set_option('_extras', var_options['_extras']) def has_option(self, option): if not self._options: self.set_options() return option in self._options def _check_required(self): # FIXME: standardize required check based on config pass
class TerminalBase(with_metaclass(ABCMeta, object)): ''' A base class for implementing cli connections .. note:: Unlike most of Assible, nearly all strings in :class:`TerminalBase` plugins are byte strings. This is because of how close to the underlying platform these plugins operate. Remember to mark literal strings as byte string (``b"string"``) and to use :func:`~assible.module_utils._text.to_bytes` and :func:`~assible.module_utils._text.to_text` to avoid unexpected problems. ''' #: compiled bytes regular expressions as stdout terminal_stdout_re = [] #: compiled bytes regular expressions as stderr terminal_stderr_re = [] #: compiled bytes regular expressions to remove ANSI codes ansi_re = [ re.compile(br'\x1b\[\?1h\x1b='), # CSI ? 1 h ESC = re.compile(br'\x08.'), # [Backspace] . re.compile(br"\x1b\[m"), # ANSI reset code ] #: terminal initial prompt terminal_initial_prompt = None #: terminal initial answer terminal_initial_answer = None #: Send newline after prompt match terminal_inital_prompt_newline = True def __init__(self, connection): self._connection = connection def _exec_cli_command(self, cmd, check_rc=True): ''' Executes the CLI command on the remote device and returns the output :arg cmd: Byte string command to be executed ''' return self._connection.exec_command(cmd) def _get_prompt(self): """ Returns the current prompt from the device :returns: A byte string of the prompt """ return self._connection.get_prompt() def on_open_shell(self): """Called after the SSH session is established This method is called right after the invoke_shell() is called from the Paramiko SSHClient instance. It provides an opportunity to setup terminal parameters such as disbling paging for instance. """ pass def on_close_shell(self): """Called before the connection is closed This method gets called once the connection close has been requested but before the connection is actually closed. It provides an opportunity to clean up any terminal resources before the shell is actually closed """ pass def on_become(self, passwd=None): """Called when privilege escalation is requested :kwarg passwd: String containing the password This method is called when the privilege is requested to be elevated in the play context by setting become to True. It is the responsibility of the terminal plugin to actually do the privilege escalation such as entering `enable` mode for instance """ pass def on_unbecome(self): """Called when privilege deescalation is requested This method is called when the privilege changed from escalated (become=True) to non escalated (become=False). It is the responsibility of this method to actually perform the deauthorization procedure """ pass def on_authorize(self, passwd=None): """Deprecated method for privilege escalation :kwarg passwd: String containing the password """ return self.on_become(passwd) def on_deauthorize(self): """Deprecated method for privilege deescalation """ return self.on_unbecome()
class CLI(with_metaclass(ABCMeta, object)): ''' code behind bin/assible* programs ''' PAGER = 'less' # -F (quit-if-one-screen) -R (allow raw ansi control chars) # -S (chop long lines) -X (disable termcap init and de-init) LESS_OPTS = 'FRSX' SKIP_INVENTORY_DEFAULTS = False def __init__(self, args, callback=None): """ Base init method for all command line programs """ if not args: raise ValueError('A non-empty list for args is required') self.args = args self.parser = None self.callback = callback if C.DEVEL_WARNING and __version__.endswith('dev0'): display.warning( 'You are running the development version of Assible. You should only run Assible from "devel" if ' 'you are modifying the Assible engine, or trying out features under development. This is a rapidly ' 'changing source of code and can become unstable at any point.' ) @abstractmethod def run(self): """Run the assible command Subclasses must implement this method. It does the actual work of running an Assible command. """ self.parse() display.vv(to_text(opt_help.version(self.parser.prog))) if C.CONFIG_FILE: display.v(u"Using %s as config file" % to_text(C.CONFIG_FILE)) else: display.v(u"No config file found; using defaults") # warn about deprecated config options for deprecated in C.config.DEPRECATED: name = deprecated[0] why = deprecated[1]['why'] if 'alternatives' in deprecated[1]: alt = ', use %s instead' % deprecated[1]['alternatives'] else: alt = '' ver = deprecated[1].get('version') date = deprecated[1].get('date') collection_name = deprecated[1].get('collection_name') display.deprecated("%s option, %s %s" % (name, why, alt), version=ver, date=date, collection_name=collection_name) @staticmethod def split_vault_id(vault_id): # return (before_@, after_@) # if no @, return whole string as after_ if '@' not in vault_id: return (None, vault_id) parts = vault_id.split('@', 1) ret = tuple(parts) return ret @staticmethod def build_vault_ids(vault_ids, vault_password_files=None, ask_vault_pass=None, create_new_password=None, auto_prompt=True): vault_password_files = vault_password_files or [] vault_ids = vault_ids or [] # convert vault_password_files into vault_ids slugs for password_file in vault_password_files: id_slug = u'%s@%s' % (C.DEFAULT_VAULT_IDENTITY, password_file) # note this makes --vault-id higher precedence than --vault-password-file # if we want to intertwingle them in order probably need a cli callback to populate vault_ids # used by --vault-id and --vault-password-file vault_ids.append(id_slug) # if an action needs an encrypt password (create_new_password=True) and we dont # have other secrets setup, then automatically add a password prompt as well. # prompts cant/shouldnt work without a tty, so dont add prompt secrets if ask_vault_pass or (not vault_ids and auto_prompt): id_slug = u'%s@%s' % (C.DEFAULT_VAULT_IDENTITY, u'prompt_ask_vault_pass') vault_ids.append(id_slug) return vault_ids # TODO: remove the now unused args @staticmethod def setup_vault_secrets(loader, vault_ids, vault_password_files=None, ask_vault_pass=None, create_new_password=False, auto_prompt=True): # list of tuples vault_secrets = [] # Depending on the vault_id value (including how --ask-vault-pass / --vault-password-file create a vault_id) # we need to show different prompts. This is for compat with older Towers that expect a # certain vault password prompt format, so 'promp_ask_vault_pass' vault_id gets the old format. prompt_formats = {} # If there are configured default vault identities, they are considered 'first' # so we prepend them to vault_ids (from cli) here vault_password_files = vault_password_files or [] if C.DEFAULT_VAULT_PASSWORD_FILE: vault_password_files.append(C.DEFAULT_VAULT_PASSWORD_FILE) if create_new_password: prompt_formats['prompt'] = [ 'New vault password (%(vault_id)s): ', 'Confirm new vault password (%(vault_id)s): ' ] # 2.3 format prompts for --ask-vault-pass prompt_formats['prompt_ask_vault_pass'] = [ 'New Vault password: '******'Confirm New Vault password: '******'prompt'] = ['Vault password (%(vault_id)s): '] # The format when we use just --ask-vault-pass needs to match 'Vault password:\s*?$' prompt_formats['prompt_ask_vault_pass'] = ['Vault password: '******'prompt', 'prompt_ask_vault_pass']: # --vault-id some_name@prompt_ask_vault_pass --vault-id other_name@prompt_ask_vault_pass will be a little # confusing since it will use the old format without the vault id in the prompt built_vault_id = vault_id_name or C.DEFAULT_VAULT_IDENTITY # choose the prompt based on --vault-id=prompt or --ask-vault-pass. --ask-vault-pass # always gets the old format for Tower compatibility. # ie, we used --ask-vault-pass, so we need to use the old vault password prompt # format since Tower needs to match on that format. prompted_vault_secret = PromptVaultSecret( prompt_formats=prompt_formats[vault_id_value], vault_id=built_vault_id) # a empty or invalid password from the prompt will warn and continue to the next # without erroring globally try: prompted_vault_secret.load() except AssibleError as exc: display.warning('Error in vault password prompt (%s): %s' % (vault_id_name, exc)) raise vault_secrets.append((built_vault_id, prompted_vault_secret)) # update loader with new secrets incrementally, so we can load a vault password # that is encrypted with a vault secret provided earlier loader.set_vault_secrets(vault_secrets) continue # assuming anything else is a password file display.vvvvv('Reading vault password file: %s' % vault_id_value) # read vault_pass from a file file_vault_secret = get_file_vault_secret(filename=vault_id_value, vault_id=vault_id_name, loader=loader) # an invalid password file will error globally try: file_vault_secret.load() except AssibleError as exc: display.warning( 'Error in vault password file loading (%s): %s' % (vault_id_name, to_text(exc))) raise if vault_id_name: vault_secrets.append((vault_id_name, file_vault_secret)) else: vault_secrets.append( (C.DEFAULT_VAULT_IDENTITY, file_vault_secret)) # update loader with as-yet-known vault secrets loader.set_vault_secrets(vault_secrets) return vault_secrets @staticmethod def ask_passwords(): ''' prompt for connection and become passwords if needed ''' op = context.CLIARGS sshpass = None becomepass = None become_prompt = '' become_prompt_method = "BECOME" if C.AGNOSTIC_BECOME_PROMPT else op[ 'become_method'].upper() try: if op['ask_pass']: sshpass = getpass.getpass(prompt="SSH password: "******"%s password[defaults to SSH password]: " % become_prompt_method else: become_prompt = "%s password: "******"The number of processes (--forks) must be >= 1") return op @abstractmethod def init_parser(self, usage="", desc=None, epilog=None): """ Create an options parser for most assible scripts Subclasses need to implement this method. They will usually call the base class's init_parser to create a basic version and then add their own options on top of that. An implementation will look something like this:: def init_parser(self): super(MyCLI, self).init_parser(usage="My Assible CLI", inventory_opts=True) assible.arguments.option_helpers.add_runas_options(self.parser) self.parser.add_option('--my-option', dest='my_option', action='store') """ self.parser = opt_help.create_base_parser( os.path.basename(self.args[0]), usage=usage, desc=desc, epilog=epilog, ) @abstractmethod def post_process_args(self, options): """Process the command line args Subclasses need to implement this method. This method validates and transforms the command line arguments. It can be used to check whether conflicting values were given, whether filenames exist, etc. An implementation will look something like this:: def post_process_args(self, options): options = super(MyCLI, self).post_process_args(options) if options.addition and options.subtraction: raise AssibleOptionsError('Only one of --addition and --subtraction can be specified') if isinstance(options.listofhosts, string_types): options.listofhosts = string_types.split(',') return options """ # process tags if hasattr(options, 'tags') and not options.tags: # optparse defaults does not do what's expected # More specifically, we want `--tags` to be additive. So we cannot # simply change C.TAGS_RUN's default to ["all"] because then passing # --tags foo would cause us to have ['all', 'foo'] options.tags = ['all'] if hasattr(options, 'tags') and options.tags: tags = set() for tag_set in options.tags: for tag in tag_set.split(u','): tags.add(tag.strip()) options.tags = list(tags) # process skip_tags if hasattr(options, 'skip_tags') and options.skip_tags: skip_tags = set() for tag_set in options.skip_tags: for tag in tag_set.split(u','): skip_tags.add(tag.strip()) options.skip_tags = list(skip_tags) # process inventory options except for CLIs that require their own processing if hasattr(options, 'inventory') and not self.SKIP_INVENTORY_DEFAULTS: if options.inventory: # should always be list if isinstance(options.inventory, string_types): options.inventory = [options.inventory] # Ensure full paths when needed options.inventory = [ unfrackpath(opt, follow=False) if ',' not in opt else opt for opt in options.inventory ] else: options.inventory = C.DEFAULT_HOST_LIST # Dup args set on the root parser and sub parsers results in the root parser ignoring the args. e.g. doing # 'assible-galaxy -vvv init' has no verbosity set but 'assible-galaxy init -vvv' sets a level of 3. To preserve # back compat with pre-argparse changes we manually scan and set verbosity based on the argv values. if self.parser.prog in ['assible-galaxy', 'assible-vault' ] and not options.verbosity: verbosity_arg = next( iter([arg for arg in self.args if arg.startswith('-v')]), None) if verbosity_arg: display.deprecated( "Setting verbosity before the arg sub command is deprecated, set the verbosity " "after the sub command", "2.13", collection_name='assible.builtin') options.verbosity = verbosity_arg.count('v') return options def parse(self): """Parse the command line args This method parses the command line arguments. It uses the parser stored in the self.parser attribute and saves the args and options in context.CLIARGS. Subclasses need to implement two helper methods, init_parser() and post_process_args() which are called from this function before and after parsing the arguments. """ self.init_parser() if HAS_ARGCOMPLETE: argcomplete.autocomplete(self.parser) try: options = self.parser.parse_args(self.args[1:]) except SystemExit as e: if (e.code != 0): self.parser.exit(status=2, message=" \n%s " % self.parser.format_help()) raise options = self.post_process_args(options) context._init_global_context(options) @staticmethod def version_info(gitinfo=False): ''' return full assible version info ''' if gitinfo: # expensive call, user with care assible_version_string = opt_help.version() else: assible_version_string = __version__ assible_version = assible_version_string.split()[0] assible_versions = assible_version.split('.') for counter in range(len(assible_versions)): if assible_versions[counter] == "": assible_versions[counter] = 0 try: assible_versions[counter] = int(assible_versions[counter]) except Exception: pass if len(assible_versions) < 3: for counter in range(len(assible_versions), 3): assible_versions.append(0) return { 'string': assible_version_string.strip(), 'full': assible_version, 'major': assible_versions[0], 'minor': assible_versions[1], 'revision': assible_versions[2] } @staticmethod def pager(text): ''' find reasonable way to display text ''' # this is a much simpler form of what is in pydoc.py if not sys.stdout.isatty(): display.display(text, screen_only=True) elif 'PAGER' in os.environ: if sys.platform == 'win32': display.display(text, screen_only=True) else: CLI.pager_pipe(text, os.environ['PAGER']) else: p = subprocess.Popen('less --version', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.communicate() if p.returncode == 0: CLI.pager_pipe(text, 'less') else: display.display(text, screen_only=True) @staticmethod def pager_pipe(text, cmd): ''' pipe text through a pager ''' if 'LESS' not in os.environ: os.environ['LESS'] = CLI.LESS_OPTS try: cmd = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=sys.stdout) cmd.communicate(input=to_bytes(text)) except IOError: pass except KeyboardInterrupt: pass @staticmethod def _play_prereqs(): options = context.CLIARGS # all needs loader loader = DataLoader() basedir = options.get('basedir', False) if basedir: loader.set_basedir(basedir) add_all_plugin_dirs(basedir) AssibleCollectionConfig.playbook_paths = basedir default_collection = _get_collection_name_from_path(basedir) if default_collection: display.warning(u'running with default collection {0}'.format( default_collection)) AssibleCollectionConfig.default_collection = default_collection vault_ids = list(options['vault_ids']) default_vault_ids = C.DEFAULT_VAULT_IDENTITY_LIST vault_ids = default_vault_ids + vault_ids vault_secrets = CLI.setup_vault_secrets( loader, vault_ids=vault_ids, vault_password_files=list(options['vault_password_files']), ask_vault_pass=options['ask_vault_pass'], auto_prompt=False) loader.set_vault_secrets(vault_secrets) # create the inventory, and filter it based on the subset specified (if any) inventory = InventoryManager(loader=loader, sources=options['inventory']) # create the variable manager, which will be shared throughout # the code, ensuring a consistent view of global variables variable_manager = VariableManager( loader=loader, inventory=inventory, version_info=CLI.version_info(gitinfo=False)) return loader, inventory, variable_manager @staticmethod def get_host_list(inventory, subset, pattern='all'): no_hosts = False if len(inventory.list_hosts()) == 0: # Empty inventory if C.LOCALHOST_WARNING and pattern not in C.LOCALHOST: display.warning( "provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'" ) no_hosts = True inventory.subset(subset) hosts = inventory.list_hosts(pattern) if not hosts and no_hosts is False: raise AssibleError( "Specified hosts and/or --limit does not match any hosts") return hosts
class Display(with_metaclass(Singleton, object)): def __init__(self, verbosity=0): self.columns = None self.verbosity = verbosity # list of all deprecation messages to prevent duplicate display self._deprecations = {} self._warns = {} self._errors = {} self.b_cowsay = None self.noncow = C.ASSIBLE_COW_SELECTION self.set_cowsay_info() if self.b_cowsay: try: cmd = subprocess.Popen([self.b_cowsay, "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = cmd.communicate() self.cows_available = set([to_text(c) for c in out.split()]) if C.ASSIBLE_COW_WHITELIST and any(C.ASSIBLE_COW_WHITELIST): self.cows_available = set( C.ASSIBLE_COW_WHITELIST).intersection( self.cows_available) except Exception: # could not execute cowsay for some reason self.b_cowsay = False self._set_column_width() def set_cowsay_info(self): if C.ASSIBLE_NOCOWS: return if C.ASSIBLE_COW_PATH: self.b_cowsay = C.ASSIBLE_COW_PATH else: for b_cow_path in b_COW_PATHS: if os.path.exists(b_cow_path): self.b_cowsay = b_cow_path def display(self, msg, color=None, stderr=False, screen_only=False, log_only=False, newline=True): """ Display a message to the user Note: msg *must* be a unicode string to prevent UnicodeError tracebacks. """ nocolor = msg if not log_only: has_newline = msg.endswith(u'\n') if has_newline: msg2 = msg[:-1] else: msg2 = msg if color: msg2 = stringc(msg2, color) if has_newline or newline: msg2 = msg2 + u'\n' msg2 = to_bytes(msg2, encoding=self._output_encoding(stderr=stderr)) if sys.version_info >= (3, ): # Convert back to text string on python3 # We first convert to a byte string so that we get rid of # characters that are invalid in the user's locale msg2 = to_text(msg2, self._output_encoding(stderr=stderr), errors='replace') # Note: After Display() class is refactored need to update the log capture # code in 'bin/assible-connection' (and other relevant places). if not stderr: fileobj = sys.stdout else: fileobj = sys.stderr fileobj.write(msg2) try: fileobj.flush() except IOError as e: # Ignore EPIPE in case fileobj has been prematurely closed, eg. # when piping to "head -n1" if e.errno != errno.EPIPE: raise if logger and not screen_only: # We first convert to a byte string so that we get rid of # color and characters that are invalid in the user's locale msg2 = to_bytes(nocolor.lstrip(u'\n')) if sys.version_info >= (3, ): # Convert back to text string on python3 msg2 = to_text(msg2, self._output_encoding(stderr=stderr)) lvl = logging.INFO if color: # set logger level based on color (not great) try: lvl = color_to_log_level[color] except KeyError: # this should not happen, but JIC raise AssibleAssertionError( 'Invalid color supplied to display: %s' % color) # actually log logger.log(lvl, msg2) def v(self, msg, host=None): return self.verbose(msg, host=host, caplevel=0) def vv(self, msg, host=None): return self.verbose(msg, host=host, caplevel=1) def vvv(self, msg, host=None): return self.verbose(msg, host=host, caplevel=2) def vvvv(self, msg, host=None): return self.verbose(msg, host=host, caplevel=3) def vvvvv(self, msg, host=None): return self.verbose(msg, host=host, caplevel=4) def vvvvvv(self, msg, host=None): return self.verbose(msg, host=host, caplevel=5) def debug(self, msg, host=None): if C.DEFAULT_DEBUG: if host is None: self.display("%6d %0.5f: %s" % (os.getpid(), time.time(), msg), color=C.COLOR_DEBUG) else: self.display("%6d %0.5f [%s]: %s" % (os.getpid(), time.time(), host, msg), color=C.COLOR_DEBUG) def verbose(self, msg, host=None, caplevel=2): to_stderr = C.VERBOSE_TO_STDERR if self.verbosity > caplevel: if host is None: self.display(msg, color=C.COLOR_VERBOSE, stderr=to_stderr) else: self.display("<%s> %s" % (host, msg), color=C.COLOR_VERBOSE, stderr=to_stderr) def get_deprecation_message(self, msg, version=None, removed=False, date=None, collection_name=None): ''' used to print out a deprecation message.''' msg = msg.strip() if msg and msg[-1] not in ['!', '?', '.']: msg += '.' if collection_name == 'assible.builtin': collection_name = 'assible-base' if removed: header = '[DEPRECATED]: {0}'.format(msg) removal_fragment = 'This feature was removed' help_text = 'Please update your playbooks.' else: header = '[DEPRECATION WARNING]: {0}'.format(msg) removal_fragment = 'This feature will be removed' # FUTURE: make this a standalone warning so it only shows up once? help_text = 'Deprecation warnings can be disabled by setting deprecation_warnings=False in assible.cfg.' if collection_name: from_fragment = 'from {0}'.format(collection_name) else: from_fragment = '' if date: when = 'in a release after {0}.'.format(date) elif version: when = 'in version {0}.'.format(version) else: when = 'in a future release.' message_text = ' '.join( f for f in [header, removal_fragment, from_fragment, when, help_text] if f) return message_text def deprecated(self, msg, version=None, removed=False, date=None, collection_name=None): if not removed and not C.DEPRECATION_WARNINGS: return message_text = self.get_deprecation_message( msg, version=version, removed=removed, date=date, collection_name=collection_name) if removed: raise AssibleError(message_text) wrapped = textwrap.wrap(message_text, self.columns, drop_whitespace=False) message_text = "\n".join(wrapped) + "\n" if message_text not in self._deprecations: self.display(message_text.strip(), color=C.COLOR_DEPRECATE, stderr=True) self._deprecations[message_text] = 1 def warning(self, msg, formatted=False): if not formatted: new_msg = "[WARNING]: %s" % msg wrapped = textwrap.wrap(new_msg, self.columns) new_msg = "\n".join(wrapped) + "\n" else: new_msg = "\n[WARNING]: \n%s" % msg if new_msg not in self._warns: self.display(new_msg, color=C.COLOR_WARN, stderr=True) self._warns[new_msg] = 1 def system_warning(self, msg): if C.SYSTEM_WARNINGS: self.warning(msg) def banner(self, msg, color=None, cows=True): ''' Prints a header-looking line with cowsay or stars with length depending on terminal width (3 minimum) ''' msg = to_text(msg) if self.b_cowsay and cows: try: self.banner_cowsay(msg) return except OSError: self.warning( "somebody cleverly deleted cowsay or something during the PB run. heh." ) msg = msg.strip() try: star_len = self.columns - get_text_width(msg) except EnvironmentError: star_len = self.columns - len(msg) if star_len <= 3: star_len = 3 stars = u"*" * star_len self.display(u"\n%s %s" % (msg, stars), color=color) def banner_cowsay(self, msg, color=None): if u": [" in msg: msg = msg.replace(u"[", u"") if msg.endswith(u"]"): msg = msg[:-1] runcmd = [self.b_cowsay, b"-W", b"60"] if self.noncow: thecow = self.noncow if thecow == 'random': thecow = random.choice(list(self.cows_available)) runcmd.append(b'-f') runcmd.append(to_bytes(thecow)) runcmd.append(to_bytes(msg)) cmd = subprocess.Popen(runcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = cmd.communicate() self.display(u"%s\n" % to_text(out), color=color) def error(self, msg, wrap_text=True): if wrap_text: new_msg = u"\n[ERROR]: %s" % msg wrapped = textwrap.wrap(new_msg, self.columns) new_msg = u"\n".join(wrapped) + u"\n" else: new_msg = u"ERROR! %s" % msg if new_msg not in self._errors: self.display(new_msg, color=C.COLOR_ERROR, stderr=True) self._errors[new_msg] = 1 @staticmethod def prompt(msg, private=False): prompt_string = to_bytes(msg, encoding=Display._output_encoding()) if sys.version_info >= (3, ): # Convert back into text on python3. We do this double conversion # to get rid of characters that are illegal in the user's locale prompt_string = to_text(prompt_string) if private: return getpass.getpass(prompt_string) else: return input(prompt_string) def do_var_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None, unsafe=None): result = None if sys.__stdin__.isatty(): do_prompt = self.prompt if prompt and default is not None: msg = "%s [%s]: " % (prompt, default) elif prompt: msg = "%s: " % prompt else: msg = 'input for %s: ' % varname if confirm: while True: result = do_prompt(msg, private) second = do_prompt("confirm " + msg, private) if result == second: break self.display("***** VALUES ENTERED DO NOT MATCH ****") else: result = do_prompt(msg, private) else: result = None self.warning("Not prompting as we are not in interactive mode") # if result is false and default is not None if not result and default is not None: result = default if encrypt: # Circular import because encrypt needs a display class from assible.utils.encrypt import do_encrypt result = do_encrypt(result, encrypt, salt_size, salt) # handle utf-8 chars result = to_text(result, errors='surrogate_or_strict') if unsafe: result = wrap_var(result) return result @staticmethod def _output_encoding(stderr=False): encoding = locale.getpreferredencoding() # https://bugs.python.org/issue6202 # Python2 hardcodes an obsolete value on Mac. Use MacOSX defaults # instead. if encoding in ('mac-roman', ): encoding = 'utf-8' return encoding def _set_column_width(self): if os.isatty(1): tty_size = unpack( 'HHHH', fcntl.ioctl(1, TIOCGWINSZ, pack('HHHH', 0, 0, 0, 0)))[1] else: tty_size = 0 self.columns = max(79, tty_size - 1)
class YumDnf(with_metaclass(ABCMeta, object)): """ Abstract class that handles the population of instance variables that should be identical between both YUM and DNF modules because of the feature parity and shared argument spec """ def __init__(self, module): self.module = module self.allow_downgrade = self.module.params['allow_downgrade'] self.autoremove = self.module.params['autoremove'] self.bugfix = self.module.params['bugfix'] self.conf_file = self.module.params['conf_file'] self.disable_excludes = self.module.params['disable_excludes'] self.disable_gpg_check = self.module.params['disable_gpg_check'] self.disable_plugin = self.module.params['disable_plugin'] self.disablerepo = self.module.params.get('disablerepo', []) self.download_only = self.module.params['download_only'] self.download_dir = self.module.params['download_dir'] self.enable_plugin = self.module.params['enable_plugin'] self.enablerepo = self.module.params.get('enablerepo', []) self.exclude = self.module.params['exclude'] self.installroot = self.module.params['installroot'] self.install_repoquery = self.module.params['install_repoquery'] self.install_weak_deps = self.module.params['install_weak_deps'] self.list = self.module.params['list'] self.names = [p.strip() for p in self.module.params['name']] self.releasever = self.module.params['releasever'] self.security = self.module.params['security'] self.skip_broken = self.module.params['skip_broken'] self.state = self.module.params['state'] self.update_only = self.module.params['update_only'] self.update_cache = self.module.params['update_cache'] self.validate_certs = self.module.params['validate_certs'] self.lock_timeout = self.module.params['lock_timeout'] # It's possible someone passed a comma separated string since it used # to be a string type, so we should handle that self.names = self.listify_comma_sep_strings_in_list(self.names) self.disablerepo = self.listify_comma_sep_strings_in_list( self.disablerepo) self.enablerepo = self.listify_comma_sep_strings_in_list( self.enablerepo) self.exclude = self.listify_comma_sep_strings_in_list(self.exclude) # Fail if someone passed a space separated string # https://github.com/assible/assible/issues/46301 for name in self.names: if ' ' in name and not any(spec in name for spec in ['@', '>', '<', '=']): module.fail_json( msg= 'It appears that a space separated string of packages was passed in ' 'as an argument. To operate on several packages, pass a comma separated ' 'string of packages or a list of packages.') # Sanity checking for autoremove if self.state is None: if self.autoremove: self.state = "absent" else: self.state = "present" if self.autoremove and (self.state != "absent"): self.module.fail_json( msg="Autoremove should be used alone or with state=absent", results=[], ) # This should really be redefined by both the yum and dnf module but a # default isn't a bad idea self.lockfile = '/var/run/yum.pid' @abstractmethod def is_lockfile_pid_valid(self): return def _is_lockfile_present(self): return (os.path.isfile(self.lockfile) or glob.glob(self.lockfile)) and self.is_lockfile_pid_valid() def wait_for_lock(self): '''Poll until the lock is removed if timeout is a positive number''' if not self._is_lockfile_present(): return if self.lock_timeout > 0: for iteration in range(0, self.lock_timeout): time.sleep(1) if not self._is_lockfile_present(): return self.module.fail_json(msg='{0} lockfile is held by another process'. format(self.pkg_mgr_name)) def listify_comma_sep_strings_in_list(self, some_list): """ method to accept a list of strings as the parameter, find any strings in that list that are comma separated, remove them from the list and add their comma separated elements to the original list """ new_list = [] remove_from_original_list = [] for element in some_list: if ',' in element: remove_from_original_list.append(element) new_list.extend([e.strip() for e in element.split(',')]) for element in remove_from_original_list: some_list.remove(element) some_list.extend(new_list) if some_list == [""]: return [] return some_list @abstractmethod def run(self): raise NotImplementedError
class AssibleCollectionConfig(with_metaclass(_AssibleCollectionConfig)): pass
class FieldAttributeBase(with_metaclass(BaseMeta, object)): def __init__(self): # initialize the data loader and variable manager, which will be provided # later when the object is actually loaded self._loader = None self._variable_manager = None # other internal params self._validated = False self._squashed = False self._finalized = False # every object gets a random uuid: self._uuid = get_unique_id() # we create a copy of the attributes here due to the fact that # it was initialized as a class param in the meta class, so we # need a unique object here (all members contained within are # unique already). self._attributes = self.__class__._attributes.copy() self._attr_defaults = self.__class__._attr_defaults.copy() for key, value in self._attr_defaults.items(): if callable(value): self._attr_defaults[key] = value() # and init vars, avoid using defaults in field declaration as it lives across plays self.vars = dict() def dump_me(self, depth=0): ''' this is never called from production code, it is here to be used when debugging as a 'complex print' ''' if depth == 0: display.debug("DUMPING OBJECT ------------------------------------------------------") display.debug("%s- %s (%s, id=%s)" % (" " * depth, self.__class__.__name__, self, id(self))) if hasattr(self, '_parent') and self._parent: self._parent.dump_me(depth + 2) dep_chain = self._parent.get_dep_chain() if dep_chain: for dep in dep_chain: dep.dump_me(depth + 2) if hasattr(self, '_play') and self._play: self._play.dump_me(depth + 2) def preprocess_data(self, ds): ''' infrequently used method to do some pre-processing of legacy terms ''' return ds def load_data(self, ds, variable_manager=None, loader=None): ''' walk the input datastructure and assign any values ''' if ds is None: raise AssibleAssertionError('ds (%s) should not be None but it is.' % ds) # cache the datastructure internally setattr(self, '_ds', ds) # the variable manager class is used to manage and merge variables # down to a single dictionary for reference in templating, etc. self._variable_manager = variable_manager # the data loader class is used to parse data from strings and files if loader is not None: self._loader = loader else: self._loader = DataLoader() # call the preprocess_data() function to massage the data into # something we can more easily parse, and then call the validation # function on it to ensure there are no incorrect key values ds = self.preprocess_data(ds) self._validate_attributes(ds) # Walk all attributes in the class. We sort them based on their priority # so that certain fields can be loaded before others, if they are dependent. for name, attr in sorted(iteritems(self._valid_attrs), key=operator.itemgetter(1)): # copy the value over unless a _load_field method is defined target_name = name if name in self._alias_attrs: target_name = self._alias_attrs[name] if name in ds: method = getattr(self, '_load_%s' % name, None) if method: self._attributes[target_name] = method(name, ds[name]) else: self._attributes[target_name] = ds[name] # run early, non-critical validation self.validate() # return the constructed object return self def get_ds(self): try: return getattr(self, '_ds') except AttributeError: return None def get_loader(self): return self._loader def get_variable_manager(self): return self._variable_manager def _post_validate_debugger(self, attr, value, templar): value = templar.template(value) valid_values = frozenset(('always', 'on_failed', 'on_unreachable', 'on_skipped', 'never')) if value and isinstance(value, string_types) and value not in valid_values: raise AssibleParserError("'%s' is not a valid value for debugger. Must be one of %s" % (value, ', '.join(valid_values)), obj=self.get_ds()) return value def _validate_attributes(self, ds): ''' Ensures that there are no keys in the datastructure which do not map to attributes for this object. ''' valid_attrs = frozenset(self._valid_attrs.keys()) for key in ds: if key not in valid_attrs: raise AssibleParserError("'%s' is not a valid attribute for a %s" % (key, self.__class__.__name__), obj=ds) def validate(self, all_vars=None): ''' validation that is done at parse time, not load time ''' all_vars = {} if all_vars is None else all_vars if not self._validated: # walk all fields in the object for (name, attribute) in iteritems(self._valid_attrs): if name in self._alias_attrs: name = self._alias_attrs[name] # run validator only if present method = getattr(self, '_validate_%s' % name, None) if method: method(attribute, name, getattr(self, name)) else: # and make sure the attribute is of the type it should be value = self._attributes[name] if value is not None: if attribute.isa == 'string' and isinstance(value, (list, dict)): raise AssibleParserError( "The field '%s' is supposed to be a string type," " however the incoming data structure is a %s" % (name, type(value)), obj=self.get_ds() ) self._validated = True def squash(self): ''' Evaluates all attributes and sets them to the evaluated version, so that all future accesses of attributes do not need to evaluate parent attributes. ''' if not self._squashed: for name in self._valid_attrs.keys(): self._attributes[name] = getattr(self, name) self._squashed = True def copy(self): ''' Create a copy of this object and return it. ''' new_me = self.__class__() for name in self._valid_attrs.keys(): if name in self._alias_attrs: continue new_me._attributes[name] = shallowcopy(self._attributes[name]) new_me._attr_defaults[name] = shallowcopy(self._attr_defaults[name]) new_me._loader = self._loader new_me._variable_manager = self._variable_manager new_me._validated = self._validated new_me._finalized = self._finalized new_me._uuid = self._uuid # if the ds value was set on the object, copy it to the new copy too if hasattr(self, '_ds'): new_me._ds = self._ds return new_me def get_validated_value(self, name, attribute, value, templar): if attribute.isa == 'string': value = to_text(value) elif attribute.isa == 'int': value = int(value) elif attribute.isa == 'float': value = float(value) elif attribute.isa == 'bool': value = boolean(value, strict=True) elif attribute.isa == 'percent': # special value, which may be an integer or float # with an optional '%' at the end if isinstance(value, string_types) and '%' in value: value = value.replace('%', '') value = float(value) elif attribute.isa == 'list': if value is None: value = [] elif not isinstance(value, list): value = [value] if attribute.listof is not None: for item in value: if not isinstance(item, attribute.listof): raise AssibleParserError("the field '%s' should be a list of %s, " "but the item '%s' is a %s" % (name, attribute.listof, item, type(item)), obj=self.get_ds()) elif attribute.required and attribute.listof == string_types: if item is None or item.strip() == "": raise AssibleParserError("the field '%s' is required, and cannot have empty values" % (name,), obj=self.get_ds()) elif attribute.isa == 'set': if value is None: value = set() elif not isinstance(value, (list, set)): if isinstance(value, string_types): value = value.split(',') else: # Making a list like this handles strings of # text and bytes properly value = [value] if not isinstance(value, set): value = set(value) elif attribute.isa == 'dict': if value is None: value = dict() elif not isinstance(value, dict): raise TypeError("%s is not a dictionary" % value) elif attribute.isa == 'class': if not isinstance(value, attribute.class_type): raise TypeError("%s is not a valid %s (got a %s instead)" % (name, attribute.class_type, type(value))) value.post_validate(templar=templar) return value def post_validate(self, templar): ''' we can't tell that everything is of the right type until we have all the variables. Run basic types (from isa) as well as any _post_validate_<foo> functions. ''' # save the omit value for later checking omit_value = templar.available_variables.get('omit') for (name, attribute) in iteritems(self._valid_attrs): if attribute.static: value = getattr(self, name) # we don't template 'vars' but allow template as values for later use if name not in ('vars',) and templar.is_template(value): display.warning('"%s" is not templatable, but we found: %s, ' 'it will not be templated and will be used "as is".' % (name, value)) continue if getattr(self, name) is None: if not attribute.required: continue else: raise AssibleParserError("the field '%s' is required but was not set" % name) elif not attribute.always_post_validate and self.__class__.__name__ not in ('Task', 'Handler', 'PlayContext'): # Intermediate objects like Play() won't have their fields validated by # default, as their values are often inherited by other objects and validated # later, so we don't want them to fail out early continue try: # Run the post-validator if present. These methods are responsible for # using the given templar to template the values, if required. method = getattr(self, '_post_validate_%s' % name, None) if method: value = method(attribute, getattr(self, name), templar) elif attribute.isa == 'class': value = getattr(self, name) else: # if the attribute contains a variable, template it now value = templar.template(getattr(self, name)) # if this evaluated to the omit value, set the value back to # the default specified in the FieldAttribute and move on if omit_value is not None and value == omit_value: if callable(attribute.default): setattr(self, name, attribute.default()) else: setattr(self, name, attribute.default) continue # and make sure the attribute is of the type it should be if value is not None: value = self.get_validated_value(name, attribute, value, templar) # and assign the massaged value back to the attribute field setattr(self, name, value) except (TypeError, ValueError) as e: value = getattr(self, name) raise AssibleParserError("the field '%s' has an invalid value (%s), and could not be converted to an %s." "The error was: %s" % (name, value, attribute.isa, e), obj=self.get_ds(), orig_exc=e) except (AssibleUndefinedVariable, UndefinedError) as e: if templar._fail_on_undefined_errors and name != 'name': if name == 'args': msg = "The task includes an option with an undefined variable. The error was: %s" % (to_native(e)) else: msg = "The field '%s' has an invalid value, which includes an undefined variable. The error was: %s" % (name, to_native(e)) raise AssibleParserError(msg, obj=self.get_ds(), orig_exc=e) self._finalized = True def _load_vars(self, attr, ds): ''' Vars in a play can be specified either as a dictionary directly, or as a list of dictionaries. If the later, this method will turn the list into a single dictionary. ''' def _validate_variable_keys(ds): for key in ds: if not isidentifier(key): raise TypeError("'%s' is not a valid variable name" % key) try: if isinstance(ds, dict): _validate_variable_keys(ds) return combine_vars(self.vars, ds) elif isinstance(ds, list): all_vars = self.vars for item in ds: if not isinstance(item, dict): raise ValueError _validate_variable_keys(item) all_vars = combine_vars(all_vars, item) return all_vars elif ds is None: return {} else: raise ValueError except ValueError as e: raise AssibleParserError("Vars in a %s must be specified as a dictionary, or a list of dictionaries" % self.__class__.__name__, obj=ds, orig_exc=e) except TypeError as e: raise AssibleParserError("Invalid variable name in vars specified for %s: %s" % (self.__class__.__name__, e), obj=ds, orig_exc=e) def _extend_value(self, value, new_value, prepend=False): ''' Will extend the value given with new_value (and will turn both into lists if they are not so already). The values are run through a set to remove duplicate values. ''' if not isinstance(value, list): value = [value] if not isinstance(new_value, list): new_value = [new_value] # Due to where _extend_value may run for some attributes # it is possible to end up with Sentinel in the list of values # ensure we strip them value = [v for v in value if v is not Sentinel] new_value = [v for v in new_value if v is not Sentinel] if prepend: combined = new_value + value else: combined = value + new_value return [i for i, _ in itertools.groupby(combined) if i is not None] def dump_attrs(self): ''' Dumps all attributes to a dictionary ''' attrs = {} for (name, attribute) in iteritems(self._valid_attrs): attr = getattr(self, name) if attribute.isa == 'class' and hasattr(attr, 'serialize'): attrs[name] = attr.serialize() else: attrs[name] = attr return attrs def from_attrs(self, attrs): ''' Loads attributes from a dictionary ''' for (attr, value) in iteritems(attrs): if attr in self._valid_attrs: attribute = self._valid_attrs[attr] if attribute.isa == 'class' and isinstance(value, dict): obj = attribute.class_type() obj.deserialize(value) setattr(self, attr, obj) else: setattr(self, attr, value) def serialize(self): ''' Serializes the object derived from the base object into a dictionary of values. This only serializes the field attributes for the object, so this may need to be overridden for any classes which wish to add additional items not stored as field attributes. ''' repr = self.dump_attrs() # serialize the uuid field repr['uuid'] = self._uuid repr['finalized'] = self._finalized repr['squashed'] = self._squashed return repr def deserialize(self, data): ''' Given a dictionary of values, load up the field attributes for this object. As with serialize(), if there are any non-field attribute data members, this method will need to be overridden and extended. ''' if not isinstance(data, dict): raise AssibleAssertionError('data (%s) should be a dict but is a %s' % (data, type(data))) for (name, attribute) in iteritems(self._valid_attrs): if name in data: setattr(self, name, data[name]) else: if callable(attribute.default): setattr(self, name, attribute.default()) else: setattr(self, name, attribute.default) # restore the UUID field setattr(self, '_uuid', data.get('uuid')) self._finalized = data.get('finalized', False) self._squashed = data.get('squashed', False)