示例#1
0
文件: base.py 项目: arizvisa/ansible
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
示例#2
0
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
示例#3
0
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
示例#4
0
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.')
示例#5
0
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
示例#6
0
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
示例#7
0
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)
示例#8
0
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)
示例#9
0
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
示例#10
0
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))
示例#11
0
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
示例#12
0
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