class BaseCacheModule(with_metaclass(ABCMeta, object)): _display = display @abstractmethod def get(self, key): pass @abstractmethod def set(self, key, value): pass @abstractmethod def keys(self): pass @abstractmethod def contains(self, key): pass @abstractmethod def delete(self, key): pass @abstractmethod def flush(self): pass @abstractmethod def copy(self): pass
class BaseCacheModule(with_metaclass(ABCMeta, object)): # Backwards compat only. Just import the global display instead _display = display @abstractmethod def get(self, key): pass @abstractmethod def set(self, key, value): pass @abstractmethod def keys(self): pass @abstractmethod def contains(self, key): pass @abstractmethod def delete(self, key): pass @abstractmethod def flush(self): pass @abstractmethod def copy(self): pass
class InventoryParser(with_metaclass(ABCMeta, object)): '''Abstract Base Class for retrieving inventory information Any InventoryParser functions by taking an inven_source. The caller then calls the parser() method. Once parser is called, the caller can access InventoryParser.hosts for a mapping of Host objects and InventoryParser.Groups for a mapping of Group objects. ''' def __init__(self, inven_source): ''' InventoryParser contructors take a source of inventory information that they will parse the host and group information from. ''' self.inven_source = inven_source self.reset_parser() @abstractmethod def reset_parser(self): ''' InventoryParsers generally cache their data once parser() is called. This method initializes any parser state before calling parser again. ''' self.hosts = dict() self.groups = dict() self.parsed = False def _merge(self, target, addition): ''' This method is provided to InventoryParsers to merge host or group dicts since it may take several passes to get all of the data Example usage: self.hosts = self.from_ini(filename) new_hosts = self.from_script(scriptname) self._merge(self.hosts, new_hosts) ''' for i in addition: if i in target: target[i].merge(addition[i]) else: target[i] = addition[i] @abstractmethod def parse(self, refresh=False): if refresh: self.reset_parser() if self.parsed: return self.parsed # Parse self.inven_sources here pass
class ActionBase(with_metaclass(ABCMeta, object)): ''' This class is the base class for all action plugins, and defines code common to all actions. The base class handles the connection by putting/getting files and executing commands based on the current action in use. ''' def __init__(self, task, connection, play_context, loader, templar, shared_loader_obj): self._task = task self._connection = connection self._play_context = play_context self._loader = loader self._templar = templar self._shared_loader_obj = shared_loader_obj # Backwards compat: self._display isn't really needed, just import the global display and use that. self._display = display self._cleanup_remote_tmp = False self._supports_check_mode = True self._supports_async = False @abstractmethod def run(self, tmp=None, task_vars=None): """ Action Plugins should implement this method to perform their tasks. Everything else in this base class is a helper method for the action plugin to do that. :kwarg tmp: Temporary directory. Sometimes an action plugin sets up a temporary directory and then calls another module. This parameter allows us to reuse the same directory for both. :kwarg task_vars: The variables (host vars, group vars, config vars, etc) associated with this task. :returns: dictionary of results from the module Implementors of action modules may find the following variables especially useful: * Module parameters. These are stored in self._task.args """ result = {} if self._task.async and not self._supports_async: raise AnsibleActionFail('async is not supported for this task.') elif self._play_context.check_mode and not self._supports_check_mode: raise AnsibleActionSkip('check mode is not supported for this task.')
class ActionBase(with_metaclass(ABCMeta, object)): ''' This class is the base class for all action plugins, and defines code common to all actions. The base class handles the connection by putting/getting files and executing commands based on the current action in use. ''' def __init__(self, task, connection, play_context, loader, templar, shared_loader_obj): self._task = task self._connection = connection self._play_context = play_context self._loader = loader self._templar = templar self._shared_loader_obj = shared_loader_obj self._display = display self._supports_check_mode = True @abstractmethod def run(self, tmp=None, task_vars=None): """ Action Plugins should implement this method to perform their tasks. Everything else in this base class is a helper method for the action plugin to do that. :kwarg tmp: Temporary directory. Sometimes an action plugin sets up a temporary directory and then calls another module. This parameter allows us to reuse the same directory for both. :kwarg task_vars: The variables (host vars, group vars, config vars, etc) associated with this task. :returns: dictionary of results from the module Implementors of action modules may find the following variables especially useful: * Module parameters. These are stored in self._task.args """ # store the module invocation details into the results results = {} if self._task. async == 0: results['invocation'] = dict( module_name=self._task.action, module_args=self._task.args, ) return results
class TerminalBase(with_metaclass(ABCMeta, object)): ''' A base class for implementing cli connections ''' terminalprompts_re = [] terminalerrors_re = [] ansi_re = [re.compile(r'(\x1b\[\?1h\x1b=)'), re.compile(r'\x08.')] supports_multiplexing = True def __init__(self, connection): self._connection = connection def _exec_cli_command(self, cmd, check_rc=True): rc, out, err = self._connection.exec_command(cmd) if check_rc and rc != 0: raise AnsibleConnectionFailure(err) return rc, out, err def _get_prompt(self): for cmd in ['\n', 'prompt()']: rc, out, err = self._exec_cli_command(cmd) return out def on_open_shell(self): pass def on_close_shell(self): pass def on_authorize(self, passwd=None): pass def on_deauthorize(self): pass @staticmethod def guess_network_os(conn): pass
class Base(with_metaclass(BaseMeta, object)): # connection/transport _connection = FieldAttribute(isa='string') _port = FieldAttribute(isa='int') _remote_user = FieldAttribute(isa='string') # variables _vars = FieldAttribute(isa='dict', priority=100, inherit=False) # flags and misc. settings _environment = FieldAttribute(isa='list') _no_log = FieldAttribute(isa='bool') _always_run = FieldAttribute(isa='bool') _run_once = FieldAttribute(isa='bool') _ignore_errors = FieldAttribute(isa='bool') _check_mode = FieldAttribute(isa='bool') _any_errors_fatal = FieldAttribute(isa='bool', default=False, always_post_validate=True) # param names which have been deprecated/removed DEPRECATED_ATTRIBUTES = [ 'sudo', 'sudo_user', 'sudo_pass', 'sudo_exe', 'sudo_flags', 'su', 'su_user', 'su_pass', 'su_exe', 'su_flags', ] 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 intialized 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._attributes.copy() # and init vars, avoid using defaults in field declaration as it lives across plays self.vars = dict() def dump_me(self, depth=0): if depth == 0: print( "DUMPING OBJECT ------------------------------------------------------" ) print("%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 ''' for base_class in self.__class__.mro(): method = getattr( self, "_preprocess_data_%s" % base_class.__name__.lower(), None) if method: return method(ds) return ds def load_data(self, ds, variable_manager=None, loader=None): ''' walk the input datastructure and assign any values ''' assert ds is not None # 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 if name in ds: method = getattr(self, '_load_%s' % name, None) if method: self._attributes[name] = method(name, ds[name]) else: self._attributes[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 _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 AnsibleParserError( "'%s' is not a valid attribute for a %s" % (key, self.__class__.__name__), obj=ds) def validate(self, all_vars=dict()): ''' validation that is done at parse time, not load time ''' if not self._validated: # walk all fields in the object for (name, attribute) in iteritems(self._valid_attrs): # 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 = getattr(self, name) if value is not None: if attribute.isa == 'string' and isinstance( value, (list, dict)): raise AnsibleParserError( "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(): new_me._attributes[name] = shallowcopy(self._attributes[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 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 getattr(self, name) is None: if not attribute.required: continue else: raise AnsibleParserError( "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: setattr(self, name, attribute.default) continue # and make sure the attribute is of the type it should be if value is not None: 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) 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 in ('list', 'barelist'): if value is None: value = [] elif not isinstance(value, list): if isinstance(value, string_types ) and attribute.isa == 'barelist': display.deprecated( "Using comma separated values for a list has been deprecated. " "You should instead use the correct YAML syntax for lists. " ) value = value.split(',') else: value = [value] if attribute.listof is not None: for item in value: if not isinstance(item, attribute.listof): raise AnsibleParserError( "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 AnsibleParserError( "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) # and assign the massaged value back to the attribute field setattr(self, name, value) except (TypeError, ValueError) as e: raise AnsibleParserError( "the field '%s' has an invalid value (%s), and could not be converted to an %s." " Error was: %s" % (name, value, attribute.isa, e), obj=self.get_ds()) except (AnsibleUndefinedVariable, UndefinedError) as e: if templar._fail_on_undefined_errors and name != 'name': raise AnsibleParserError( "the field '%s' has an invalid value, which appears to include a variable that is undefined." " The error was: %s" % (name, e), obj=self.get_ds()) 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 ds elif isinstance(ds, list): all_vars = dict() 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: raise AnsibleParserError( "Vars in a %s must be specified as a dictionary, or a list of dictionaries" % self.__class__.__name__, obj=ds) except TypeError as e: raise AnsibleParserError( "Invalid variable name in vars specified for %s: %s" % (self.__class__.__name__, e), obj=ds) 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] 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 = dict() for (name, attribute) in iteritems(self._valid_attrs): attr = getattr(self, name) if attribute.isa == 'class' and attr is not None 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. ''' assert isinstance(data, dict) for (name, attribute) in iteritems(self._valid_attrs): if name in data: setattr(self, name, data[name]) 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)
class ConnectionBase(with_metaclass(ABCMeta, object)): ''' A base class for connections to contain common code. ''' has_pipelining = False become_methods = C.BECOME_METHODS # When running over this connection type, prefer modules written in a certain language # as discovered by the specified file extension. An empty string as the # language means any language. module_implementation_preferences = ('', ) allow_executable = True def __init__(self, play_context, new_stdin, *args, **kwargs): # All these hasattrs allow subclasses to override these parameters if not hasattr(self, '_play_context'): self._play_context = play_context if not hasattr(self, '_new_stdin'): self._new_stdin = new_stdin # Backwards compat: self._display isn't really needed, just import the global display and use that. if not hasattr(self, '_display'): self._display = display if not hasattr(self, '_connected'): self._connected = False self.success_key = None self.prompt = None self._connected = False # load the shell plugin for this action/connection if play_context.shell: shell_type = play_context.shell elif hasattr(self, '_shell_type'): shell_type = getattr(self, '_shell_type') else: shell_type = 'sh' shell_filename = os.path.basename(self._play_context.executable) for shell in shell_loader.all(): if shell_filename in shell.COMPATIBLE_SHELLS: shell_type = shell.SHELL_FAMILY break self._shell = shell_loader.get(shell_type) if not self._shell: raise AnsibleError( "Invalid shell type specified (%s), or the plugin for that shell type is missing." % shell_type) @property def connected(self): '''Read-only property holding whether the connection to the remote host is active or closed.''' return self._connected def _become_method_supported(self): ''' Checks if the current class supports this privilege escalation method ''' if self._play_context.become_method in self.become_methods: return True raise AnsibleError( "Internal Error: this connection module does not support running commands via %s" % self._play_context.become_method) def set_host_overrides(self, host, hostvars=None): ''' An optional method, which can be used to set connection plugin parameters from variables set on the host (or groups to which the host belongs) Any connection plugin using this should first initialize its attributes in an overridden `def __init__(self):`, and then use `host.get_vars()` to find variables which may be used to set those attributes in this method. ''' pass @staticmethod def _split_ssh_args(argstring): """ Takes a string like '-o Foo=1 -o Bar="foo bar"' and returns a list ['-o', 'Foo=1', '-o', 'Bar=foo bar'] that can be added to the argument list. The list will not contain any empty elements. """ try: # Python 2.6.x shlex doesn't handle unicode type so we have to # convert args to byte string for that case. More efficient to # try without conversion first but python2.6 doesn't throw an # exception, it merely mangles the output: # >>> shlex.split(u't e') # ['t\x00\x00\x00', '\x00\x00\x00e\x00\x00\x00'] return [ to_text(x.strip()) for x in shlex.split(to_bytes(argstring)) if x.strip() ] except AttributeError: # In Python3, shlex.split doesn't work on a byte string. return [ to_text(x.strip()) for x in shlex.split(argstring) if x.strip() ] @abstractproperty def transport(self): """String used to identify this Connection class from other classes""" pass @abstractmethod def _connect(self): """Connect to the host we've been initialized with""" # Check if PE is supported if self._play_context.become: self._become_method_supported() @ensure_connect @abstractmethod def exec_command(self, cmd, in_data=None, sudoable=True): """Run a command on the remote host. :arg cmd: byte string containing the command :kwarg in_data: If set, this data is passed to the command's stdin. This is used to implement pipelining. Currently not all connection plugins implement pipelining. :kwarg sudoable: Tell the connection plugin if we're executing a command via a privilege escalation mechanism. This may affect how the connection plugin returns data. Note that not all connections can handle privilege escalation. :returns: a tuple of (return code, stdout, stderr) The return code is an int while stdout and stderr are both byte strings. When a command is executed, it goes through multiple commands to get there. It looks approximately like this:: [LocalShell] ConnectionCommand [UsersLoginShell (*)] ANSIBLE_SHELL_EXECUTABLE [(BecomeCommand ANSIBLE_SHELL_EXECUTABLE)] Command :LocalShell: Is optional. It is run locally to invoke the ``Connection Command``. In most instances, the ``ConnectionCommand`` can be invoked directly instead. The ssh connection plugin which can have values that need expanding locally specified via ssh_args is the sole known exception to this. Shell metacharacters in the command itself should be processed on the remote machine, not on the local machine so no shell is needed on the local machine. (Example, ``/bin/sh``) :ConnectionCommand: This is the command that connects us to the remote machine to run the rest of the command. ``ansible_ssh_user``, ``ansible_ssh_host`` and so forth are fed to this piece of the command to connect to the correct host (Examples ``ssh``, ``chroot``) :UsersLoginShell: This shell may or may not be created depending on the ConnectionCommand used by the connection plugin. This is the shell that the ``ansible_ssh_user`` has configured as their login shell. In traditional UNIX parlance, this is the last field of a user's ``/etc/passwd`` entry We do not specifically try to run the ``UsersLoginShell`` when we connect. Instead it is implicit in the actions that the ``ConnectionCommand`` takes when it connects to a remote machine. ``ansible_shell_type`` may be set to inform ansible of differences in how the ``UsersLoginShell`` handles things like quoting if a shell has different semantics than the Bourne shell. :ANSIBLE_SHELL_EXECUTABLE: This is the shell set via the inventory var ``ansible_shell_executable`` or via ``constants.DEFAULT_EXECUTABLE`` if the inventory var is not set. We explicitly invoke this shell so that we have predictable quoting rules at this point. ``ANSIBLE_SHELL_EXECUTABLE`` is only settable by the user because some sudo setups may only allow invoking a specific shell. (For instance, ``/bin/bash`` may be allowed but ``/bin/sh``, our default, may not). We invoke this twice, once after the ``ConnectionCommand`` and once after the ``BecomeCommand``. After the ConnectionCommand, this is run by the ``UsersLoginShell``. After the ``BecomeCommand`` we specify that the ``ANSIBLE_SHELL_EXECUTABLE`` is being invoked directly. :BecomeComand ANSIBLE_SHELL_EXECUTABLE: Is the command that performs privilege escalation. Setting this up is performed by the action plugin prior to running ``exec_command``. So we just get passed :param:`cmd` which has the BecomeCommand already added. (Examples: sudo, su) If we have a BecomeCommand then we will invoke a ANSIBLE_SHELL_EXECUTABLE shell inside of it so that we have a consistent view of quoting. :Command: Is the command we're actually trying to run remotely. (Examples: mkdir -p $HOME/.ansible, python $HOME/.ansible/tmp-script-file) """ pass @ensure_connect @abstractmethod def put_file(self, in_path, out_path): """Transfer a file from local to remote""" pass @ensure_connect @abstractmethod def fetch_file(self, in_path, out_path): """Fetch a file from remote to local""" pass @abstractmethod def close(self): """Terminate the connection""" pass def check_become_success(self, b_output): b_success_key = to_bytes(self._play_context.success_key) for b_line in b_output.splitlines(True): if b_success_key == b_line.rstrip(): return True return False def check_password_prompt(self, b_output): if self._play_context.prompt is None: return False elif isinstance(self._play_context.prompt, string_types): b_prompt = to_bytes(self._play_context.prompt) return b_prompt in b_output else: return self._play_context.prompt(b_output) def check_incorrect_password(self, b_output): b_incorrect_password = to_bytes( gettext.dgettext( self._play_context.become_method, C.BECOME_ERROR_STRINGS[self._play_context.become_method])) return b_incorrect_password and b_incorrect_password in b_output def check_missing_password(self, b_output): b_missing_password = to_bytes( gettext.dgettext( self._play_context.become_method, C.BECOME_MISSING_STRINGS[self._play_context.become_method])) return b_missing_password and b_missing_password in b_output def connection_lock(self): f = self._play_context.connection_lockfd display.vvvv('CONNECTION: pid %d waiting for lock on %d' % (os.getpid(), f), host=self._play_context.remote_addr) fcntl.lockf(f, fcntl.LOCK_EX) display.vvvv('CONNECTION: pid %d acquired lock on %d' % (os.getpid(), f), host=self._play_context.remote_addr) def connection_unlock(self): f = self._play_context.connection_lockfd fcntl.lockf(f, fcntl.LOCK_UN) display.vvvv('CONNECTION: pid %d released lock on %d' % (os.getpid(), f), host=self._play_context.remote_addr)
class CLI(with_metaclass(ABCMeta, object)): ''' code behind bin/ansible* programs ''' VALID_ACTIONS = ['No Actions'] _ITALIC = re.compile(r"I\(([^)]+)\)") _BOLD = re.compile(r"B\(([^)]+)\)") _MODULE = re.compile(r"M\(([^)]+)\)") _URL = re.compile(r"U\(([^)]+)\)") _CONST = re.compile(r"C\(([^)]+)\)") PAGER = 'less' LESS_OPTS = 'FRSX' # -F (quit-if-one-screen) -R (allow raw ansi control chars) # -S (chop long lines) -X (disable termcap init and de-init) def __init__(self, args, callback=None): """ Base init method for all command line programs """ self.args = args self.options = None self.parser = None self.action = None self.callback = callback def set_action(self): """ Get the action the user wants to execute from the sys argv list. """ for i in range(0, len(self.args)): arg = self.args[i] if arg in self.VALID_ACTIONS: self.action = arg del self.args[i] break if not self.action: # if we're asked for help or version, we don't need an action. # have to use a special purpose Option Parser to figure that out as # the standard OptionParser throws an error for unknown options and # without knowing action, we only know of a subset of the options # that could be legal for this command tmp_parser = InvalidOptsParser(self.parser) tmp_options, tmp_args = tmp_parser.parse_args(self.args) if not (hasattr(tmp_options, 'help') and tmp_options.help) or ( hasattr(tmp_options, 'version') and tmp_options.version): raise AnsibleOptionsError("Missing required action") def execute(self): """ Actually runs a child defined method using the execute_<action> pattern """ fn = getattr(self, "execute_%s" % self.action) fn() @abstractmethod def run(self): """Run the ansible command Subclasses must implement this method. It does the actual work of running an Ansible command. """ if self.options.verbosity > 0: if C.CONFIG_FILE: display.display(u"Using %s as config file" % to_text(C.CONFIG_FILE)) else: display.display(u"No config file found; using defaults") @staticmethod def ask_vault_passwords(ask_new_vault_pass=False, rekey=False): ''' prompt for vault password and/or password change ''' vault_pass = None new_vault_pass = None try: if rekey or not ask_new_vault_pass: vault_pass = getpass.getpass(prompt="Vault password: "******"New Vault password: "******"Confirm New Vault password: "******"Passwords do not match") except EOFError: pass # enforce no newline chars at the end of passwords if vault_pass: vault_pass = to_bytes(vault_pass, errors='strict', nonstring='simplerepr').strip() if new_vault_pass: new_vault_pass = to_bytes(new_vault_pass, errors='strict', nonstring='simplerepr').strip() if ask_new_vault_pass and not rekey: vault_pass = new_vault_pass return vault_pass, new_vault_pass def ask_passwords(self): ''' prompt for connection and become passwords if needed ''' op = self.options sshpass = None becomepass = None become_prompt = '' try: if op.ask_pass: sshpass = getpass.getpass(prompt="SSH password: "******"%s password[defaults to SSH password]: " % op.become_method.upper( ) if sshpass: sshpass = to_bytes(sshpass, errors='strict', nonstring='simplerepr') else: become_prompt = "%s password: "******"--ask-vault-pass and --vault-password-file are mutually exclusive" ) if runas_opts: # Check for privilege escalation conflicts if (op.su or op.su_user) and (op.sudo or op.sudo_user) or \ (op.su or op.su_user) and (op.become or op.become_user) or \ (op.sudo or op.sudo_user) and (op.become or op.become_user): self.parser.error( "Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') " "and su arguments ('-su', '--su-user', and '--ask-su-pass') " "and become arguments ('--become', '--become-user', and '--ask-become-pass')" " are exclusive of each other") if fork_opts: if op.forks < 1: self.parser.error( "The number of processes (--forks) must be >= 1") @staticmethod def expand_tilde(option, opt, value, parser): setattr(parser.values, option.dest, os.path.expanduser(value)) @staticmethod def expand_paths(option, opt, value, parser): """optparse action callback to convert a PATH style string arg to a list of path strings. For ex, cli arg of '-p /blip/foo:/foo/bar' would be split on the default os.pathsep and the option value would be set to the list ['/blip/foo', '/foo/bar']. Each path string in the list will also have '~/' values expand via os.path.expanduser().""" path_entries = value.split(os.pathsep) expanded_path_entries = [ os.path.expanduser(path_entry) for path_entry in path_entries ] setattr(parser.values, option.dest, expanded_path_entries) @staticmethod def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False, runtask_opts=False, vault_opts=False, module_opts=False, async_opts=False, connect_opts=False, subset_opts=False, check_opts=False, inventory_opts=False, epilog=None, fork_opts=False, runas_prompt_opts=False): ''' create an options parser for most ansible scripts ''' # TODO: implement epilog parsing # OptionParser.format_epilog = lambda self, formatter: self.epilog # base opts parser = SortedOptParser(usage, version=CLI.version("%prog")) parser.add_option( '-v', '--verbose', dest='verbosity', default=0, action="count", help= "verbose mode (-vvv for more, -vvvv to enable connection debugging)" ) if inventory_opts: parser.add_option( '-i', '--inventory-file', dest='inventory', help= "specify inventory host path (default=%s) or comma separated host list." % C.DEFAULT_HOST_LIST, default=C.DEFAULT_HOST_LIST, action="callback", callback=CLI.expand_tilde, type=str) parser.add_option( '--list-hosts', dest='listhosts', action='store_true', help= 'outputs a list of matching hosts; does not execute anything else' ) parser.add_option( '-l', '--limit', default=C.DEFAULT_SUBSET, dest='subset', help='further limit selected hosts to an additional pattern') if module_opts: parser.add_option( '-M', '--module-path', dest='module_path', default=None, help="specify path(s) to module library (default=%s)" % C.DEFAULT_MODULE_PATH, action="callback", callback=CLI.expand_tilde, type=str) if runtask_opts: parser.add_option( '-e', '--extra-vars', dest="extra_vars", action="append", help="set additional variables as key=value or YAML/JSON", default=[]) if fork_opts: parser.add_option( '-f', '--forks', dest='forks', default=C.DEFAULT_FORKS, type='int', help="specify number of parallel processes to use (default=%s)" % C.DEFAULT_FORKS) if vault_opts: parser.add_option('--ask-vault-pass', default=C.DEFAULT_ASK_VAULT_PASS, dest='ask_vault_pass', action='store_true', help='ask for vault password') parser.add_option('--vault-password-file', default=C.DEFAULT_VAULT_PASSWORD_FILE, dest='vault_password_file', help="vault password file", action="callback", callback=CLI.expand_tilde, type=str) parser.add_option('--new-vault-password-file', dest='new_vault_password_file', help="new vault password file for rekey", action="callback", callback=CLI.expand_tilde, type=str) parser.add_option( '--output', default=None, dest='output_file', help= 'output file name for encrypt or decrypt; use - for stdout', action="callback", callback=CLI.expand_tilde, type=str) if subset_opts: parser.add_option( '-t', '--tags', dest='tags', default=[], action='append', help="only run plays and tasks tagged with these values") parser.add_option( '--skip-tags', dest='skip_tags', default=[], action='append', help= "only run plays and tasks whose tags do not match these values" ) if output_opts: parser.add_option('-o', '--one-line', dest='one_line', action='store_true', help='condense output') parser.add_option('-t', '--tree', dest='tree', default=None, help='log output to this directory') if connect_opts: connect_group = optparse.OptionGroup( parser, "Connection Options", "control as whom and how to connect to hosts") connect_group.add_option('-k', '--ask-pass', default=C.DEFAULT_ASK_PASS, dest='ask_pass', action='store_true', help='ask for connection password') connect_group.add_option( '--private-key', '--key-file', default=C.DEFAULT_PRIVATE_KEY_FILE, dest='private_key_file', help='use this file to authenticate the connection') connect_group.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER, dest='remote_user', help='connect as this user (default=%s)' % C.DEFAULT_REMOTE_USER) connect_group.add_option( '-c', '--connection', dest='connection', default=C.DEFAULT_TRANSPORT, help="connection type to use (default=%s)" % C.DEFAULT_TRANSPORT) connect_group.add_option( '-T', '--timeout', default=C.DEFAULT_TIMEOUT, type='int', dest='timeout', help="override the connection timeout in seconds (default=%s)" % C.DEFAULT_TIMEOUT) connect_group.add_option( '--ssh-common-args', default='', dest='ssh_common_args', help= "specify common arguments to pass to sftp/scp/ssh (e.g. ProxyCommand)" ) connect_group.add_option( '--sftp-extra-args', default='', dest='sftp_extra_args', help= "specify extra arguments to pass to sftp only (e.g. -f, -l)") connect_group.add_option( '--scp-extra-args', default='', dest='scp_extra_args', help="specify extra arguments to pass to scp only (e.g. -l)") connect_group.add_option( '--ssh-extra-args', default='', dest='ssh_extra_args', help="specify extra arguments to pass to ssh only (e.g. -R)") parser.add_option_group(connect_group) runas_group = None rg = optparse.OptionGroup( parser, "Privilege Escalation Options", "control how and which user you become as on target hosts") if runas_opts: runas_group = rg # priv user defaults to root later on to enable detecting when this option was given here runas_group.add_option( "-s", "--sudo", default=C.DEFAULT_SUDO, action="store_true", dest='sudo', help= "run operations with sudo (nopasswd) (deprecated, use become)") runas_group.add_option( '-U', '--sudo-user', dest='sudo_user', default=None, help='desired sudo user (default=root) (deprecated, use become)' ) runas_group.add_option( '-S', '--su', default=C.DEFAULT_SU, action='store_true', help='run operations with su (deprecated, use become)') runas_group.add_option( '-R', '--su-user', default=None, help= 'run operations with su as this user (default=%s) (deprecated, use become)' % C.DEFAULT_SU_USER) # consolidated privilege escalation (become) runas_group.add_option( "-b", "--become", default=C.DEFAULT_BECOME, action="store_true", dest='become', help= "run operations with become (does not imply password prompting)" ) runas_group.add_option( '--become-method', dest='become_method', default=C.DEFAULT_BECOME_METHOD, type='choice', choices=C.BECOME_METHODS, help= "privilege escalation method to use (default=%s), valid choices: [ %s ]" % (C.DEFAULT_BECOME_METHOD, ' | '.join(C.BECOME_METHODS))) runas_group.add_option( '--become-user', default=None, dest='become_user', type='string', help='run operations as this user (default=%s)' % C.DEFAULT_BECOME_USER) if runas_opts or runas_prompt_opts: if not runas_group: runas_group = rg runas_group.add_option( '--ask-sudo-pass', default=C.DEFAULT_ASK_SUDO_PASS, dest='ask_sudo_pass', action='store_true', help='ask for sudo password (deprecated, use become)') runas_group.add_option( '--ask-su-pass', default=C.DEFAULT_ASK_SU_PASS, dest='ask_su_pass', action='store_true', help='ask for su password (deprecated, use become)') runas_group.add_option( '-K', '--ask-become-pass', default=False, dest='become_ask_pass', action='store_true', help='ask for privilege escalation password') if runas_group: parser.add_option_group(runas_group) if async_opts: parser.add_option( '-P', '--poll', default=C.DEFAULT_POLL_INTERVAL, type='int', dest='poll_interval', help="set the poll interval if using -B (default=%s)" % C.DEFAULT_POLL_INTERVAL) parser.add_option( '-B', '--background', dest='seconds', type='int', default=0, help='run asynchronously, failing after X seconds (default=N/A)' ) if check_opts: parser.add_option( "-C", "--check", default=False, dest='check', action='store_true', help= "don't make any changes; instead, try to predict some of the changes that may occur" ) parser.add_option( '--syntax-check', dest='syntax', action='store_true', help= "perform a syntax check on the playbook, but do not execute it" ) parser.add_option( "-D", "--diff", default=False, dest='diff', action='store_true', help= "when changing (small) files and templates, show the differences in those files; works great with --check" ) if meta_opts: parser.add_option('--force-handlers', default=C.DEFAULT_FORCE_HANDLERS, dest='force_handlers', action='store_true', help="run handlers even if a task fails") parser.add_option('--flush-cache', dest='flush_cache', action='store_true', help="clear the fact cache") return parser @abstractmethod 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 self.args and self.options respectively. Subclasses need to implement this method. They will usually create a base_parser, add their own options to the base_parser, and then call this method to do the actual parsing. An implementation will look something like this:: def parse(self): parser = super(MyCLI, self).base_parser(usage="My Ansible CLI", inventory_opts=True) parser.add_option('--my-option', dest='my_option', action='store') self.parser = parser super(MyCLI, self).parse() # If some additional transformations are needed for the # arguments and options, do it here. """ self.options, self.args = self.parser.parse_args(self.args[1:]) if hasattr(self.options, 'tags') and not self.options.tags: # optparse defaults does not do what's expected self.options.tags = ['all'] if hasattr(self.options, 'tags') and self.options.tags: if not C.MERGE_MULTIPLE_CLI_TAGS: if len(self.options.tags) > 1: display.deprecated( 'Specifying --tags multiple times on the command line currently uses the last specified value. In 2.4, values will be merged instead. Set merge_multiple_cli_tags=True in ansible.cfg to get this behavior now.', version=2.5, removed=False) self.options.tags = [self.options.tags[-1]] tags = set() for tag_set in self.options.tags: for tag in tag_set.split(u','): tags.add(tag.strip()) self.options.tags = list(tags) if hasattr(self.options, 'skip_tags') and self.options.skip_tags: if not C.MERGE_MULTIPLE_CLI_TAGS: if len(self.options.skip_tags) > 1: display.deprecated( 'Specifying --skip-tags multiple times on the command line currently uses the last specified value. In 2.4, values will be merged instead. Set merge_multiple_cli_tags=True in ansible.cfg to get this behavior now.', version=2.5, removed=False) self.options.skip_tags = [self.options.skip_tags[-1]] skip_tags = set() for tag_set in self.options.skip_tags: for tag in tag_set.split(u','): skip_tags.add(tag.strip()) self.options.skip_tags = list(skip_tags) @staticmethod def version(prog): ''' return ansible version ''' result = "{0} {1}".format(prog, __version__) gitinfo = CLI._gitinfo() if gitinfo: result = result + " {0}".format(gitinfo) result += "\n config file = %s" % C.CONFIG_FILE if C.DEFAULT_MODULE_PATH is None: cpath = "Default w/o overrides" else: cpath = C.DEFAULT_MODULE_PATH result = result + "\n configured module search path = %s" % cpath return result @staticmethod def version_info(gitinfo=False): ''' return full ansible version info ''' if gitinfo: # expensive call, user with care ansible_version_string = CLI.version('') else: ansible_version_string = __version__ ansible_version = ansible_version_string.split()[0] ansible_versions = ansible_version.split('.') for counter in range(len(ansible_versions)): if ansible_versions[counter] == "": ansible_versions[counter] = 0 try: ansible_versions[counter] = int(ansible_versions[counter]) except: pass if len(ansible_versions) < 3: for counter in range(len(ansible_versions), 3): ansible_versions.append(0) return { 'string': ansible_version_string.strip(), 'full': ansible_version, 'major': ansible_versions[0], 'minor': ansible_versions[1], 'revision': ansible_versions[2] } @staticmethod def _git_repo_info(repo_path): ''' returns a string containing git branch, commit id and commit date ''' result = None if os.path.exists(repo_path): # Check if the .git is a file. If it is a file, it means that we are in a submodule structure. if os.path.isfile(repo_path): try: gitdir = yaml.safe_load(open(repo_path)).get('gitdir') # There is a possibility the .git file to have an absolute path. if os.path.isabs(gitdir): repo_path = gitdir else: repo_path = os.path.join(repo_path[:-4], gitdir) except (IOError, AttributeError): return '' f = open(os.path.join(repo_path, "HEAD")) line = f.readline().rstrip("\n") if line.startswith("ref:"): branch_path = os.path.join(repo_path, line[5:]) else: branch_path = None f.close() if branch_path and os.path.exists(branch_path): branch = '/'.join(line.split('/')[2:]) f = open(branch_path) commit = f.readline()[:10] f.close() else: # detached HEAD commit = line[:10] branch = 'detached HEAD' branch_path = os.path.join(repo_path, "HEAD") date = time.localtime(os.stat(branch_path).st_mtime) if time.daylight == 0: offset = time.timezone else: offset = time.altzone result = "({0} {1}) last updated {2} (GMT {3:+04d})".format( branch, commit, time.strftime("%Y/%m/%d %H:%M:%S", date), int(offset / -36)) else: result = '' return result @staticmethod def _gitinfo(): basedir = os.path.join(os.path.dirname(__file__), '..', '..', '..') repo_path = os.path.join(basedir, '.git') result = CLI._git_repo_info(repo_path) submodules = os.path.join(basedir, '.gitmodules') if not os.path.exists(submodules): return result f = open(submodules) for line in f: tokens = line.strip().split(' ') if tokens[0] == 'path': submodule_path = tokens[2] submodule_info = CLI._git_repo_info( os.path.join(basedir, submodule_path, '.git')) if not submodule_info: submodule_info = ' not found - use git submodule update --init ' + submodule_path result += "\n {0}: {1}".format(submodule_path, submodule_info) f.close() return result def pager(self, 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) elif 'PAGER' in os.environ: if sys.platform == 'win32': display.display(text) else: self.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: self.pager_pipe(text, 'less') else: display.display(text) @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 @classmethod def tty_ify(cls, text): t = cls._ITALIC.sub("`" + r"\1" + "'", text) # I(word) => `word' t = cls._BOLD.sub("*" + r"\1" + "*", t) # B(word) => *word* t = cls._MODULE.sub("[" + r"\1" + "]", t) # M(word) => [word] t = cls._URL.sub(r"\1", t) # U(word) => word t = cls._CONST.sub("`" + r"\1" + "'", t) # C(word) => `word' return t @staticmethod def read_vault_password_file(vault_password_file, loader): """ Read a vault password from a file or if executable, execute the script and retrieve password from STDOUT """ this_path = os.path.realpath(os.path.expanduser(vault_password_file)) if not os.path.exists(this_path): raise AnsibleError("The vault password file %s was not found" % this_path) if loader.is_executable(this_path): try: # STDERR not captured to make it easier for users to prompt for input in their scripts p = subprocess.Popen(this_path, stdout=subprocess.PIPE) except OSError as e: raise AnsibleError( "Problem running vault password script %s (%s). If this is not a script, remove the executable bit from the file." % (' '.join(this_path), e)) stdout, stderr = p.communicate() if p.returncode != 0: raise AnsibleError( "Vault password script %s returned non-zero (%s): %s" % (this_path, p.returncode, p.stderr)) vault_pass = stdout.strip('\r\n') else: try: f = open(this_path, "rb") vault_pass = f.read().strip() f.close() except (OSError, IOError) as e: raise AnsibleError( "Could not read vault password file %s: %s" % (this_path, e)) return vault_pass def get_opt(self, k, defval=""): """ Returns an option from an Optparse values instance. """ try: data = getattr(self.options, k) except: return defval # FIXME: Can this be removed if cli and/or constants ensures it's a # list? if k == "roles_path": if os.pathsep in data: data = data.split(os.pathsep)[0] return data
class ConnectionBase(with_metaclass(ABCMeta, object)): ''' A base class for connections to contain common code. ''' has_pipelining = False become_methods = C.BECOME_METHODS # When running over this connection type, prefer modules written in a certain language # as discovered by the specified file extension. An empty string as the # language means any language. module_implementation_preferences = ('',) def __init__(self, play_context, new_stdin, *args, **kwargs): # All these hasattrs allow subclasses to override these parameters if not hasattr(self, '_play_context'): self._play_context = play_context if not hasattr(self, '_new_stdin'): self._new_stdin = new_stdin if not hasattr(self, '_display'): self._display = display if not hasattr(self, '_connected'): self._connected = False self.success_key = None self.prompt = None # load the shell plugin for this action/connection if play_context.shell: shell_type = play_context.shell elif hasattr(self, '_shell_type'): shell_type = getattr(self, '_shell_type') else: shell_type = os.path.basename(C.DEFAULT_EXECUTABLE) self._shell = shell_loader.get(shell_type) if not self._shell: raise AnsibleError("Invalid shell type specified (%s), or the plugin for that shell type is missing." % shell_type) # allocate the lockfile f = self._play_context.connection_lockfd self._lockfd = portable.LockFile(f) def _become_method_supported(self): ''' Checks if the current class supports this privilege escalation method ''' if self._play_context.become_method in self.become_methods: return True raise AnsibleError("Internal Error: this connection module does not support running commands via %s" % self._play_context.become_method) def set_host_overrides(self, host): ''' An optional method, which can be used to set connection plugin parameters from variables set on the host (or groups to which the host belongs) Any connection plugin using this should first initialize its attributes in an overridden `def __init__(self):`, and then use `host.get_vars()` to find variables which may be used to set those attributes in this method. ''' pass @abstractproperty def transport(self): """String used to identify this Connection class from other classes""" pass @abstractmethod def _connect(self): """Connect to the host we've been initialized with""" # Check if PE is supported if self._play_context.become: self._become_method_supported() @ensure_connect @abstractmethod def exec_command(self, cmd, in_data=None, sudoable=True): """Run a command on the remote host. :arg cmd: byte string containing the command :kwarg in_data: If set, this data is passed to the command's stdin. This is used to implement pipelining. Currently not all connection plugins implement pipelining. :kwarg sudoable: Tell the connection plugin if we're executing a command via a privilege escalation mechanism. This may affect how the connection plugin returns data. Note that not all connections can handle privilege escalation. :returns: a tuple of (return code, stdout, stderr) The return code is an int while stdout and stderr are both byte strings. When a command is executed, it goes through multiple commands to get there. It looks approximately like this:: HardCodedShell ConnectionCommand UsersLoginShell DEFAULT_EXECUTABLE BecomeCommand DEFAULT_EXECUTABLE Command :HardCodedShell: Is optional. It is run locally to invoke the ``Connection Command``. In most instances, the ``ConnectionCommand`` can be invoked directly instead. The ssh connection plugin which can have values that need expanding locally specified via ssh_args is the sole known exception to this. Shell metacharacters in the command itself should be processed on the remote machine, not on the local machine so no shell is needed on the local machine. (Example, ``/bin/sh``) :ConnectionCommand: This is the command that connects us to the remote machine to run the rest of the command. ``ansible_ssh_user``, ``ansible_ssh_host`` and so forth are fed to this piece of the command to connect to the correct host (Examples ``ssh``, ``chroot``) :UsersLoginShell: This is the shell that the ``ansible_ssh_user`` has configured as their login shell. In traditional UNIX parlance, this is the last field of a user's ``/etc/passwd`` entry We do not specifically try to run the ``UsersLoginShell`` when we connect. Instead it is implicit in the actions that the ``ConnectionCommand`` takes when it connects to a remote machine. ``ansible_shell_type`` may be set to inform ansible of differences in how the ``UsersLoginShell`` handles things like quoting if a shell has different semantics than the Bourne shell. :DEFAULT_EXECUTABLE: This is the shell accessible via ``ansible.constants.DEFAULT_EXECUTABLE``. We explicitly invoke this shell so that we have predictable quoting rules at this point. The ``DEFAULT_EXECUTABLE`` is only settable by the user because some sudo setups may only allow invoking a specific Bourne shell. (For instance, ``/bin/bash`` may be allowed but ``/bin/sh``, our default, may not). We invoke this twice, once after the ``ConnectionCommand`` and once after the ``BecomeCommand``. After the ConnectionCommand, this is run by the ``UsersLoginShell``. After the ``BecomeCommand`` we specify that the ``DEFAULT_EXECUTABLE`` is being invoked directly. :BecomeComand: Is the command that performs privilege escalation. Setting this up is performed by the action plugin prior to running ``exec_command``. So we just get passed :param:`cmd` which has the BecomeCommand already added. (Examples: sudo, su) :Command: Is the command we're actualy trying to run remotely. (Examples: mkdir -p $HOME/.ansible, python $HOME/.ansible/tmp-script-file) """ pass @ensure_connect @abstractmethod def put_file(self, in_path, out_path): """Transfer a file from local to remote""" pass @ensure_connect @abstractmethod def fetch_file(self, in_path, out_path): """Fetch a file from remote to local""" pass @abstractmethod def close(self): """Terminate the connection""" pass def check_become_success(self, output): return self._play_context.success_key == output.rstrip() def check_password_prompt(self, output): if self._play_context.prompt is None: return False elif isinstance(self._play_context.prompt, basestring): return output.startswith(self._play_context.prompt) else: return self._play_context.prompt(output) def check_incorrect_password(self, output): incorrect_password = gettext.dgettext(self._play_context.become_method, C.BECOME_ERROR_STRINGS[self._play_context.become_method]) return incorrect_password and incorrect_password in output def check_missing_password(self, output): missing_password = gettext.dgettext(self._play_context.become_method, C.BECOME_MISSING_STRINGS[self._play_context.become_method]) return missing_password and missing_password in output def connection_lock(self): self._display.vvvv('CONNECTION: pid %d waiting for lock on %d' % (os.getpid(), f)) self._lockfd.acquire() self._display.vvvv('CONNECTION: pid %d acquired lock on %d' % (os.getpid(), f)) def connection_unlock(self): f = self._play_context.connection_lockfd self._lockfd.release() self._display.vvvv('CONNECTION: pid %d released lock on %d' % (os.getpid(), f))
class TerminalBase(with_metaclass(ABCMeta, object)): ''' A base class for implementing cli connections ''' # compiled regular expression as stdout terminal_stdout_re = [] # compiled regular expression as stderr terminal_stderr_re = [] # copiled regular expression to remove ANSI codes ansi_re = [re.compile(r'(\x1b\[\?1h\x1b=)'), re.compile(r'\x08.')] def __init__(self, connection): self._connection = connection def _exec_cli_command(self, cmd, check_rc=True): """Executes a CLI command on the device""" rc, out, err = self._connection.exec_command(cmd) if check_rc and rc != 0: raise AnsibleConnectionFailure(err) return rc, out, err def _get_prompt(self): """ Returns the current prompt from the device""" for cmd in ['\n', 'prompt()']: rc, out, err = self._exec_cli_command(cmd) return out 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_authorize(self, passwd=None): """Called when privilege escalation is requested 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_deauthorize(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 the this method to actually perform the deauthorization procedure """ pass
class LookupBase(with_metaclass(ABCMeta, object)): def __init__(self, loader=None, templar=None, **kwargs): self._loader = loader self._templar = templar # Backwards compat: self._display isn't really needed, just import the global display and use that. self._display = display def get_basedir(self, variables): if 'role_path' in variables: return variables['role_path'] else: return self._loader.get_basedir() @staticmethod def _flatten(terms): ret = [] for term in terms: if isinstance(term, (list, tuple)): ret.extend(term) else: ret.append(term) return ret @staticmethod def _combine(a, b): results = [] for x in a: for y in b: results.append(LookupBase._flatten([x, y])) return results @staticmethod def _flatten_hash_to_list(terms): ret = [] for key in terms: ret.append({'key': key, 'value': terms[key]}) return ret @abstractmethod def run(self, terms, variables=None, **kwargs): """ When the playbook specifies a lookup, this method is run. The arguments to the lookup become the arguments to this method. One additional keyword argument named ``variables`` is added to the method call. It contains the variables available to ansible at the time the lookup is templated. For instance:: "{{ lookup('url', 'https://toshio.fedorapeople.org/one.txt', validate_certs=True) }}" would end up calling the lookup plugin named url's run method like this:: run(['https://toshio.fedorapeople.org/one.txt'], variables=available_variables, validate_certs=True) Lookup plugins can be used within playbooks for looping. When this happens, the first argument is a list containing the terms. Lookup plugins can also be called from within playbooks to return their values into a variable or parameter. If the user passes a string in this case, it is converted into a list. Errors encountered during execution should be returned by raising AnsibleError() with a message describing the error. Any strings returned by this method that could ever contain non-ascii must be converted into python's unicode type as the strings will be run through jinja2 which has this requirement. You can use:: from ansible.module_utils.unicode import to_unicode result_string = to_unicode(result_string) """ pass def find_file_in_search_path(self, myvars, subdir, needle): ''' Return a file (needle) in the task's expected search path. ''' if 'ansible_search_path' in myvars: paths = myvars['ansible_search_path'] else: paths = self.get_basedir(myvars) result = self._loader.path_dwim_relative_stack(paths, subdir, needle) if result is None: raise AnsibleFileNotFound( "Unable to find '%s' in expected paths." % needle) return result