From 445ff39f944f45492bf7b2360d99fdadc02d142b Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 11 Feb 2019 11:27:44 -0600 Subject: [PATCH] Become plugins (#50991) * [WIP] become plugins Move from hardcoded method to plugins for ease of use, expansion and overrides - load into connection as it is going to be the main consumer - play_context will also use to keep backwards compat API - ensure shell is used to construct commands when needed - migrate settings remove from base config in favor of plugin specific configs - cleanup ansible-doc - add become plugin docs - remove deprecated sudo/su code and keywords - adjust become options for cli - set plugin options from context - ensure config defs are avaialbe before instance - refactored getting the shell plugin, fixed tests - changed into regex as they were string matching, which does not work with random string generation - explicitly set flags for play context tests - moved plugin loading up front - now loads for basedir also - allow pyc/o for non m modules - fixes to tests and some plugins - migrate to play objects fro play_context - simiplify gathering - added utf8 headers - moved option setting - add fail msg to dzdo - use tuple for multiple options on fail/missing - fix relative plugin paths - shift from play context to play - all tasks already inherit this from play directly - remove obsolete 'set play' - correct environment handling - add wrap_exe option to pfexec - fix runas to noop - fixed setting play context - added password configs - removed required false - remove from doc building till they are ready future development: - deal with 'enable' and 'runas' which are not 'command wrappers' but 'state flags' and currently hardcoded in diff subsystems * cleanup remove callers to removed func removed --sudo cli doc refs remove runas become_exe ensure keyerorr on plugin also fix backwards compat, missing method is attributeerror, not ansible error get remote_user consistently ignore missing system_tmpdirs on plugin load correct config precedence add deprecation fix networking imports backwards compat for plugins using BECOME_METHODS * Port become_plugins to context.CLIARGS This is a work in progress: * Stop passing options around everywhere as we can use context.CLIARGS instead * Refactor make_become_commands as asked for by alikins * Typo in comment fix * Stop loading values from the cli in more than one place Both play and play_context were saving default values from the cli arguments directly. This changes things so that the default values are loaded into the play and then play_context takes them from there. * Rename BECOME_PLUGIN_PATH to DEFAULT_BECOME_PLUGIN_PATH As alikins said, all other plugin paths are named DEFAULT_plugintype_PLUGIN_PATH. If we're going to rename these, that should be done all at one time rather than piecemeal. * One to throw away This is a set of hacks to get setting FieldAttribute defaults to command line args to work. It's not fully done yet. After talking it over with sivel and jimi-c this should be done by fixing FieldAttributeBase and _get_parent_attribute() calls to do the right thing when there is a non-None default. What we want to be able to do ideally is something like this: class Base(FieldAttributeBase): _check_mode = FieldAttribute([..] default=lambda: context.CLIARGS['check']) class Play(Base): # lambda so that we have a chance to parse the command line args # before we get here. In the future we might be able to restructure # this so that the cli parsing code runs before these classes are # defined. class Task(Base): pass And still have a playbook like this function: --- - hosts: tasks: - command: whoami check_mode: True (The check_mode test that is added as a separate commit in this PR will let you test variations on this case). There's a few separate reasons that the code doesn't let us do this or a non-ugly workaround for this as written right now. The fix that jimi-c, sivel, and I talked about may let us do this or it may still require a workaround (but less ugly) (having one class that has the FieldAttributes with default values and one class that inherits from that but just overrides the FieldAttributes which now have defaults) * Revert "One to throw away" This reverts commit 23aa883cbed11429ef1be2a2d0ed18f83a3b8064. * Set FieldAttr defaults directly from CLIARGS * Remove dead code * Move timeout directly to PlayContext, it's never needed on Play * just for backwards compat, add a static version of BECOME_METHODS to constants * Make the become attr on the connection public, since it's used outside of the connection * Logic fix * Nuke connection testing if it supports specific become methods * Remove unused vars * Address rebase issues * Fix path encoding issue * Remove unused import * Various cleanups * Restore network_cli check in _low_level_execute_command * type improvements for cliargs_deferred_get and swap shallowcopy to default to False * minor cleanups * Allow the su plugin to work, since it doesn't define a prompt the same way * Fix up ksu become plugin * Only set prompt if build_become_command was called * Add helper to assist connection plugins in knowing they need to wait for a prompt * Fix tests and code expectations * Doc updates * Various additional minor cleanups * Make doas functional * Don't change connection signature, load become plugin from TaskExecutor * Remove unused imports * Add comment about setting the become plugin on the playcontext * Fix up tests for recent changes * Support 'Password:' natively for the doas plugin * Make default prompts raw * wording cleanups. ci_complete * Remove unrelated changes * Address spelling mistake * Restore removed test, and udpate to use new functionality * Add changelog fragment * Don't hard fail in set_attributes_from_cli on missing CLI keys * Remove unrelated change to loader * Remove internal deprecated FieldAttributes now * Emit deprecation warnings now --- changelogs/fragments/become-plugins.yaml | 8 + docs/docsite/Makefile | 2 +- .../installation_guide/intro_installation.rst | 2 +- docs/docsite/rst/plugins/become.rst | 60 ++++ docs/docsite/rst/plugins/index.html | 4 + docs/docsite/rst/plugins/plugins.rst | 1 + .../rst/user_guide/intro_getting_started.rst | 16 +- .../rst/user_guide/playbooks_intro.rst | 8 +- examples/ansible.cfg | 1 + lib/ansible/cli/__init__.py | 48 +-- lib/ansible/cli/adhoc.py | 7 +- lib/ansible/cli/arguments/optparse_helpers.py | 20 +- lib/ansible/cli/console.py | 1 - lib/ansible/cli/playbook.py | 25 +- lib/ansible/cli/pull.py | 2 +- lib/ansible/config/base.yml | 53 +-- lib/ansible/config/manager.py | 4 +- lib/ansible/constants.py | 52 +-- lib/ansible/context.py | 23 ++ lib/ansible/executor/play_iterator.py | 20 +- lib/ansible/executor/playbook_executor.py | 4 +- lib/ansible/executor/task_executor.py | 82 ++++- lib/ansible/playbook/__init__.py | 8 - lib/ansible/playbook/base.py | 14 +- lib/ansible/playbook/become.py | 7 +- lib/ansible/playbook/block.py | 9 +- lib/ansible/playbook/play.py | 10 +- lib/ansible/playbook/play_context.py | 308 ++++-------------- lib/ansible/playbook/role/__init__.py | 10 +- lib/ansible/plugins/__init__.py | 7 +- lib/ansible/plugins/action/__init__.py | 111 +++---- lib/ansible/plugins/action/async_status.py | 7 +- lib/ansible/plugins/action/reboot.py | 4 +- lib/ansible/plugins/become/__init__.py | 89 +++++ lib/ansible/plugins/become/doas.py | 128 ++++++++ lib/ansible/plugins/become/dzdo.py | 97 ++++++ lib/ansible/plugins/become/ksu.py | 120 +++++++ lib/ansible/plugins/become/machinectl.py | 87 +++++ lib/ansible/plugins/become/pbrun.py | 103 ++++++ lib/ansible/plugins/become/pfexec.py | 103 ++++++ lib/ansible/plugins/become/pmrun.py | 75 +++++ lib/ansible/plugins/become/runas.py | 69 ++++ lib/ansible/plugins/become/sesu.py | 90 +++++ lib/ansible/plugins/become/su.py | 158 +++++++++ lib/ansible/plugins/become/sudo.py | 104 ++++++ lib/ansible/plugins/connection/__init__.py | 111 +++---- lib/ansible/plugins/connection/buildah.py | 1 - lib/ansible/plugins/connection/chroot.py | 2 +- lib/ansible/plugins/connection/docker.py | 1 - lib/ansible/plugins/connection/jail.py | 3 +- lib/ansible/plugins/connection/kubectl.py | 1 - lib/ansible/plugins/connection/libvirt_lxc.py | 2 +- lib/ansible/plugins/connection/local.py | 2 +- lib/ansible/plugins/connection/lxc.py | 1 - lib/ansible/plugins/connection/napalm.py | 6 +- lib/ansible/plugins/connection/netconf.py | 2 +- .../plugins/connection/paramiko_ssh.py | 2 +- lib/ansible/plugins/connection/psrp.py | 1 - lib/ansible/plugins/connection/qubes.py | 1 - lib/ansible/plugins/connection/ssh.py | 10 +- lib/ansible/plugins/connection/winrm.py | 5 - lib/ansible/plugins/connection/zone.py | 2 +- lib/ansible/plugins/loader.py | 59 +++- lib/ansible/plugins/shell/__init__.py | 12 +- lib/ansible/plugins/shell/sh.py | 4 + lib/ansible/plugins/strategy/__init__.py | 4 +- lib/ansible/plugins/strategy/free.py | 2 +- lib/ansible/plugins/strategy/linear.py | 2 +- .../connection_plugins/dummy.py | 1 - test/units/playbook/test_play_context.py | 115 ++++--- test/units/plugins/action/test_action.py | 34 +- .../plugins/connection/test_connection.py | 3 + test/units/plugins/connection/test_ssh.py | 20 +- 73 files changed, 1849 insertions(+), 721 deletions(-) create mode 100644 changelogs/fragments/become-plugins.yaml create mode 100644 docs/docsite/rst/plugins/become.rst create mode 100644 docs/docsite/rst/plugins/index.html create mode 100644 lib/ansible/plugins/become/__init__.py create mode 100644 lib/ansible/plugins/become/doas.py create mode 100644 lib/ansible/plugins/become/dzdo.py create mode 100644 lib/ansible/plugins/become/ksu.py create mode 100644 lib/ansible/plugins/become/machinectl.py create mode 100644 lib/ansible/plugins/become/pbrun.py create mode 100644 lib/ansible/plugins/become/pfexec.py create mode 100644 lib/ansible/plugins/become/pmrun.py create mode 100644 lib/ansible/plugins/become/runas.py create mode 100644 lib/ansible/plugins/become/sesu.py create mode 100644 lib/ansible/plugins/become/su.py create mode 100644 lib/ansible/plugins/become/sudo.py diff --git a/changelogs/fragments/become-plugins.yaml b/changelogs/fragments/become-plugins.yaml new file mode 100644 index 00000000000..7a36949159f --- /dev/null +++ b/changelogs/fragments/become-plugins.yaml @@ -0,0 +1,8 @@ +major_changes: +- become - become functionality has been migrated to a plugin architecture, to + allow customization of become functionality and 3rd party become methods + (https://github.com/ansible/ansible/pull/50991) +- become - The deprecated CLI arguments for ``--sudo``, ``--sudo-user``, + ``--ask-sudo-pass``, ``-su``, ``--su-user``, and ``--ask-su-pass`` have been + removed, in favor of the more generic ``--become``, ``--become-user``, + ``--become-method``, and ``--ask-become-pass``. diff --git a/docs/docsite/Makefile b/docs/docsite/Makefile index c97936bc70d..7b090703357 100644 --- a/docs/docsite/Makefile +++ b/docs/docsite/Makefile @@ -26,7 +26,7 @@ ifdef PLUGINS PLUGIN_ARGS = -l $(PLUGINS) endif -DOC_PLUGINS ?= cache callback cliconf connection httpapi inventory lookup shell strategy vars +DOC_PLUGINS ?= become cache callback cliconf connection httpapi inventory lookup shell strategy vars assertrst: ifndef rst diff --git a/docs/docsite/rst/installation_guide/intro_installation.rst b/docs/docsite/rst/installation_guide/intro_installation.rst index c75d4af272b..350f59d38d5 100644 --- a/docs/docsite/rst/installation_guide/intro_installation.rst +++ b/docs/docsite/rst/installation_guide/intro_installation.rst @@ -90,7 +90,7 @@ later). .. code-block:: shell - $ ansible myhost --sudo -m raw -a "yum install -y python2" + $ ansible myhost --become -m raw -a "yum install -y python2" .. _installing_the_control_node: diff --git a/docs/docsite/rst/plugins/become.rst b/docs/docsite/rst/plugins/become.rst new file mode 100644 index 00000000000..f1438a2cb9e --- /dev/null +++ b/docs/docsite/rst/plugins/become.rst @@ -0,0 +1,60 @@ +.. contents:: Topics + +.. versionadded:: 2.8 + +Become Plugins +-------------- + +Become plugins work to ensure that Ansible can use certain privilege escalation systems when running the basic +commands to work with the target machine as well as the modules required to execute the tasks specified in +the play. + +These utilities (``sudo``, ``su``, ``doas``, etc) generally let you 'become' another user to execute a command +with the permissions of that user. + + +.. _enabling_become: + +Enabling Become Plugins ++++++++++++++++++++++++ + +The become plugins shipped with Ansible are already enabled. Custom plugins can be added by placing +them into a ``become_plugins`` directory adjacent to your play, inside a role, or by placing them in one of +the become plugin directory sources configured in :ref:`ansible.cfg `. + + +.. _using_become: + +Using Become Plugins +++++++++++++++++++++ + +In addition to the default configuration settings in :ref:`ansible_configuration_settings` or the +``--become-method`` command line option, you can use the ``become_method`` keyword in a play or, if you need +to be 'host specific', the connection variable ``ansible_become_method`` to select the plugin to use. + +You can further control the settings for each plugin via other configuration options detailed in the plugin +themselves (linked below). + +.. toctree:: :maxdepth: 1 + :glob: + + become/* + +.. seealso:: + + :doc:`../user_guide/playbooks` + An introduction to playbooks + :doc:`inventory` + Ansible inventory plugins + :doc:`callback` + Ansible callback plugins + :doc:`../user_guide/playbooks_filters` + Jinja2 filter plugins + :doc:`../user_guide/playbooks_tests` + Jinja2 test plugins + :doc:`../user_guide/playbooks_lookups` + Jinja2 lookup plugins + `User Mailing List `_ + Have a question? Stop by the google group! + `irc.freenode.net `_ + #ansible IRC chat channel diff --git a/docs/docsite/rst/plugins/index.html b/docs/docsite/rst/plugins/index.html new file mode 100644 index 00000000000..a7eac85695c --- /dev/null +++ b/docs/docsite/rst/plugins/index.html @@ -0,0 +1,4 @@ + + += 1") @@ -502,6 +462,7 @@ class CLI(with_metaclass(ABCMeta, object)): basedir = options.get('basedir', False) if basedir: loader.set_basedir(basedir) + add_all_plugin_dirs(basedir) vault_ids = list(options['vault_ids']) default_vault_ids = C.DEFAULT_VAULT_IDENTITY_LIST @@ -516,6 +477,9 @@ class CLI(with_metaclass(ABCMeta, object)): # create the inventory, and filter it based on the subset specified (if any) inventory = InventoryManager(loader=loader, sources=options['inventory']) + subset = options.get('subset', False) + if subset: + inventory.subset(subset) # create the variable manager, which will be shared throughout # the code, ensuring a consistent view of global variables @@ -533,8 +497,6 @@ class CLI(with_metaclass(ABCMeta, object)): display.warning("provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'") no_hosts = True - inventory.subset(subset) - hosts = inventory.list_hosts(pattern) if len(hosts) == 0 and no_hosts is False: raise AnsibleError("Specified hosts and/or --limit does not match any hosts") diff --git a/lib/ansible/cli/adhoc.py b/lib/ansible/cli/adhoc.py index 3d92af00f8f..c39e2caa270 100644 --- a/lib/ansible/cli/adhoc.py +++ b/lib/ansible/cli/adhoc.py @@ -15,7 +15,6 @@ from ansible.module_utils._text import to_text from ansible.parsing.splitter import parse_kv from ansible.playbook import Playbook from ansible.playbook.play import Play -from ansible.plugins.loader import get_all_plugin_loaders from ansible.utils.display import Display display = Display() @@ -66,8 +65,6 @@ class AdHocCLI(CLI): display.verbosity = options.verbosity self.validate_conflicts(options, runas_opts=True, vault_opts=True, fork_opts=True) - options = self.normalize_become_options(options) - return options, args def _play_ds(self, pattern, async_val, poll): @@ -100,9 +97,7 @@ class AdHocCLI(CLI): (sshpass, becomepass) = self.ask_passwords() passwords = {'conn_pass': sshpass, 'become_pass': becomepass} - # dynamically load any plugins - get_all_plugin_loaders() - + # get basic objects loader, inventory, variable_manager = self._play_prereqs() try: diff --git a/lib/ansible/cli/arguments/optparse_helpers.py b/lib/ansible/cli/arguments/optparse_helpers.py index d9278bb215e..6ecbd523d06 100644 --- a/lib/ansible/cli/arguments/optparse_helpers.py +++ b/lib/ansible/cli/arguments/optparse_helpers.py @@ -303,22 +303,12 @@ def add_runas_options(parser): """ runas_group = optparse.OptionGroup(parser, "Privilege Escalation Options", "control how and which user you become as on target hosts") - # 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-method', dest='become_method', default=C.DEFAULT_BECOME_METHOD, + help="privilege escalation method to use (default=%default), use " + "`ansible-doc -t become -l` to list valid choices.") 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) @@ -336,10 +326,6 @@ def add_runas_prompt_options(parser, runas_group=None): runas_group = optparse.OptionGroup(parser, "Privilege Escalation Options", "control how and which user you become as on target hosts") - 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') diff --git a/lib/ansible/cli/console.py b/lib/ansible/cli/console.py index 5f3a4142758..0015401bcca 100644 --- a/lib/ansible/cli/console.py +++ b/lib/ansible/cli/console.py @@ -100,7 +100,6 @@ class ConsoleCLI(CLI, cmd.Cmd): def post_process_args(self, options, args): options, args = super(ConsoleCLI, self).post_process_args(options, args) display.verbosity = options.verbosity - options = self.normalize_become_options(options) self.validate_conflicts(options, runas_opts=True, vault_opts=True, fork_opts=True) return options, args diff --git a/lib/ansible/cli/playbook.py b/lib/ansible/cli/playbook.py index 66dfc56861b..8b10c95e6a8 100644 --- a/lib/ansible/cli/playbook.py +++ b/lib/ansible/cli/playbook.py @@ -13,9 +13,10 @@ from ansible.cli import CLI from ansible.cli.arguments import optparse_helpers as opt_help from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.executor.playbook_executor import PlaybookExecutor +from ansible.module_utils._text import to_bytes from ansible.playbook.block import Block -from ansible.playbook.play_context import PlayContext from ansible.utils.display import Display +from ansible.plugins.loader import add_all_plugin_dirs display = Display() @@ -61,8 +62,6 @@ class PlaybookCLI(CLI): display.verbosity = options.verbosity self.validate_conflicts(options, runas_opts=True, vault_opts=True, fork_opts=True) - options = self.normalize_become_options(options) - return options, args def run(self): @@ -82,6 +81,14 @@ class PlaybookCLI(CLI): raise AnsibleError("the playbook: %s could not be found" % playbook) if not (os.path.isfile(playbook) or stat.S_ISFIFO(os.stat(playbook).st_mode)): raise AnsibleError("the playbook: %s does not appear to be a file" % playbook) + # load plugins from all playbooks in case they add callbacks/inventory/etc + add_all_plugin_dirs( + os.path.dirname( + os.path.abspath( + to_bytes(playbook, errors='surrogate_or_strict') + ) + ) + ) # don't deal with privilege escalation or passwords when we don't need to if not (context.CLIARGS['listhosts'] or context.CLIARGS['listtasks'] or @@ -89,16 +96,9 @@ class PlaybookCLI(CLI): (sshpass, becomepass) = self.ask_passwords() passwords = {'conn_pass': sshpass, 'become_pass': becomepass} + # create base objects loader, inventory, variable_manager = self._play_prereqs() - # (which is not returned in list_hosts()) is taken into account for - # warning if inventory is empty. But it can't be taken into account for - # checking if limit doesn't match any hosts. Instead we don't worry about - # limit if only implicit localhost was in inventory to start with. - # - # Fix this when we rewrite inventory by making localhost a real host (and thus show up in list_hosts()) - hosts = self.get_host_list(inventory, context.CLIARGS['subset']) - # flush fact cache if requested if context.CLIARGS['flush_cache']: self._flush_cache(inventory, variable_manager) @@ -161,9 +161,8 @@ class PlaybookCLI(CLI): return taskmsg all_vars = variable_manager.get_vars(play=play) - play_context = PlayContext(play=play) for block in play.compile(): - block = block.filter_tagged_tasks(play_context, all_vars) + block = block.filter_tagged_tasks(all_vars) if not block.has_tasks(): continue taskmsg += _process_block(block) diff --git a/lib/ansible/cli/pull.py b/lib/ansible/cli/pull.py index 715ec2d0a27..6e8adfeca89 100644 --- a/lib/ansible/cli/pull.py +++ b/lib/ansible/cli/pull.py @@ -260,7 +260,7 @@ class PullCLI(CLI): for ev in context.CLIARGS['extra_vars']: cmd += ' -e %s' % shlex_quote(ev) - if context.CLIARGS['ask_sudo_pass'] or context.CLIARGS['ask_su_pass'] or context.CLIARGS['become_ask_pass']: + if context.CLIARGS['become_ask_pass']: cmd += ' --ask-become-pass' if context.CLIARGS['skip_tags']: cmd += ' --skip-tags "%s"' % to_native(u','.join(context.CLIARGS['skip_tags'])) diff --git a/lib/ansible/config/base.yml b/lib/ansible/config/base.yml index dde91320da3..e9f0ee6e87f 100644 --- a/lib/ansible/config/base.yml +++ b/lib/ansible/config/base.yml @@ -481,6 +481,14 @@ DEFAULT_BECOME_FLAGS: env: [{name: ANSIBLE_BECOME_FLAGS}] ini: - {key: become_flags, section: privilege_escalation} +DEFAULT_BECOME_PLUGIN_PATH: + name: Become plugins path + default: ~/.ansible/plugins/become:/usr/share/ansible/become + description: Colon separated paths in which Ansible will search for Become Plugins. + env: [{name: ANSIBLE_BECOME_PLUGINS}] + ini: + - {key: become_plugins, section: defaults} + type: pathspec DEFAULT_BECOME_USER: # FIXME: should really be blank and make -u passing optional depending on it name: Set the user you 'become' via privilege escalation @@ -632,6 +640,7 @@ DEFAULT_GATHER_SUBSET: - key: gather_subset section: defaults version_added: "2.1" + type: list DEFAULT_GATHER_TIMEOUT: name: Gather facts timeout default: 10 @@ -1081,50 +1090,6 @@ DEFAULT_SU: - {key: su, section: defaults} type: boolean yaml: {key: defaults.su} -DEFAULT_SUDO: - default: False - deprecated: - why: In favor of Ansible Become, which is a generic framework - version: "2.9" - alternatives: become - description: 'Toggle the use of "sudo" for tasks.' - env: [{name: ANSIBLE_SUDO}] - ini: - - {key: sudo, section: defaults} - type: boolean -DEFAULT_SUDO_EXE: - name: sudo executable - default: sudo - deprecated: - why: In favor of Ansible Become, which is a generic framework. See become_exe. - version: "2.9" - alternatives: become - description: 'specify an "sudo" executable, otherwise it relies on PATH.' - env: [{name: ANSIBLE_SUDO_EXE}] - ini: - - {key: sudo_exe, section: defaults} -DEFAULT_SUDO_FLAGS: - name: sudo flags - default: '-H -S -n' - deprecated: - why: In favor of Ansible Become, which is a generic framework. See become_flags. - version: "2.9" - alternatives: become - description: 'Flags to pass to "sudo"' - env: [{name: ANSIBLE_SUDO_FLAGS}] - ini: - - {key: sudo_flags, section: defaults} -DEFAULT_SUDO_USER: - name: sudo user - default: - deprecated: - why: In favor of Ansible Become, which is a generic framework. See become_user. - version: "2.9" - alternatives: become - description: 'User you become when using "sudo", leaving it blank will use the default configured on the target (normally root)' - env: [{name: ANSIBLE_SUDO_USER}] - ini: - - {key: sudo_user, section: defaults} DEFAULT_SU_EXE: name: su executable default: su diff --git a/lib/ansible/config/manager.py b/lib/ansible/config/manager.py index d3e9f379325..df88ad6666d 100644 --- a/lib/ansible/config/manager.py +++ b/lib/ansible/config/manager.py @@ -398,8 +398,8 @@ class ConfigManager(object): origin = 'var: %s' % origin # use playbook keywords if you have em - if value is None and keys and defs[config].get('keywords'): - value, origin = self._loop_entries(keys, defs[config]['keywords']) + if value is None and keys and config in keys: + value, origin = keys[config], 'keyword' origin = 'keyword: %s' % origin # env vars are next precedence diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index 8167e67f213..f0986553fb0 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -12,6 +12,7 @@ from jinja2 import Template from string import ascii_letters, digits from ansible.module_utils._text import to_text +from ansible.module_utils.common.collections import Sequence from ansible.module_utils.parsing.convert_bool import boolean, BOOLEANS_TRUE from ansible.module_utils.six import string_types from ansible.config.manager import ConfigManager, ensure_type, get_ini_config_value @@ -68,32 +69,31 @@ def set_constant(name, value, export=vars()): export[name] = value +class _DeprecatedSequenceConstant(Sequence): + def __init__(self, value, msg, version): + self._value = value + self._msg = msg + self._version = version + + def __len__(self): + _deprecated(self._msg, version=self._version) + return len(self._value) + + def __getitem__(self, y): + _deprecated(self._msg, version=self._version) + return self._value[y] + + +# Deprecated constants +BECOME_METHODS = _DeprecatedSequenceConstant( + ['sudo', 'su', 'pbrun', 'pfexec', 'doas', 'dzdo', 'ksu', 'runas', 'pmrun', 'enable', 'machinectl'], + ('ansible.constants.BECOME_METHODS is deprecated, please use ' + 'ansible.plugins.loader.become_loader. This list is statically ' + 'defined and may not include all become methods'), + '2.10' +) + # CONSTANTS ### yes, actual ones -BECOME_METHODS = ['sudo', 'su', 'pbrun', 'pfexec', 'doas', 'dzdo', 'ksu', 'runas', 'pmrun', 'enable', 'machinectl'] -BECOME_ERROR_STRINGS = { - 'sudo': 'Sorry, try again.', - 'su': 'Authentication failure', - 'pbrun': '', - 'pfexec': '', - 'doas': 'Permission denied', - 'dzdo': 'Sorry, try again.', - 'ksu': 'Password incorrect', - 'pmrun': 'You are not permitted to run this command', - 'enable': '', - 'machinectl': '', -} # FIXME: deal with i18n -BECOME_MISSING_STRINGS = { - 'sudo': 'sorry, a password is required to run sudo', - 'su': '', - 'pbrun': '', - 'pfexec': '', - 'doas': 'Authorization required', - 'dzdo': '', - 'ksu': 'No password given', - 'pmrun': '', - 'enable': '', - 'machinectl': '', -} # FIXME: deal with i18n BLACKLIST_EXTS = ('.pyc', '.pyo', '.swp', '.bak', '~', '.rpm', '.md', '.txt', '.rst') BOOL_TRUE = BOOLEANS_TRUE CONTROLLER_LANG = os.getenv('LANG', 'en_US.UTF-8') @@ -104,7 +104,7 @@ DEFAULT_REMOTE_PASS = None DEFAULT_SUBSET = None DEFAULT_SU_PASS = None # FIXME: expand to other plugins, but never doc fragments -CONFIGURABLE_PLUGINS = ('cache', 'callback', 'connection', 'inventory', 'lookup', 'shell', 'cliconf', 'httpapi') +CONFIGURABLE_PLUGINS = ('become', 'cache', 'callback', 'cliconf', 'connection', 'httpapi', 'inventory', 'lookup', 'shell') # NOTE: always update the docs/docsite/Makefile to match DOCUMENTABLE_PLUGINS = CONFIGURABLE_PLUGINS + ('module', 'strategy', 'vars') IGNORE_FILES = ("COPYING", "CONTRIBUTING", "LICENSE", "README", "VERSION", "GUIDELINES") # ignore during module search diff --git a/lib/ansible/context.py b/lib/ansible/context.py index 67786a02920..96f13feac75 100644 --- a/lib/ansible/context.py +++ b/lib/ansible/context.py @@ -15,6 +15,8 @@ running the ansible command line tools. These APIs are still in flux so do not use them unless you are willing to update them with every Ansible release """ +from ansible.module_utils.common._collections_compat import Mapping, Set +from ansible.module_utils.common.collections import is_sequence from ansible.utils.context_objects import CLIArgs, GlobalCLIArgs @@ -31,3 +33,24 @@ def _init_global_context(cli_args): """Initialize the global context objects""" global CLIARGS CLIARGS = GlobalCLIArgs.from_options(cli_args) + + +def cliargs_deferred_get(key, default=None, shallowcopy=False): + """Closure over getting a key from CLIARGS with shallow copy functionality + + Primarily used in ``FieldAttribute`` where we need to defer setting the default + until after the CLI arguments have been parsed + + This function is not directly bound to ``CliArgs`` so that it works with + ``CLIARGS`` being replaced + """ + def inner(): + value = CLIARGS.get(key, default=default) + if not shallowcopy: + return value + elif is_sequence(value): + return value[:] + elif isinstance(value, (Mapping, Set)): + return value.copy() + return value + return inner diff --git a/lib/ansible/executor/play_iterator.py b/lib/ansible/executor/play_iterator.py index 92ab0bed764..ab40ce6723e 100644 --- a/lib/ansible/executor/play_iterator.py +++ b/lib/ansible/executor/play_iterator.py @@ -151,19 +151,9 @@ class PlayIterator: self._variable_manager = variable_manager # Default options to gather - gather_subset = play_context.gather_subset - gather_timeout = play_context.gather_timeout - fact_path = play_context.fact_path - - # Retrieve subset to gather - if self._play.gather_subset is not None: - gather_subset = self._play.gather_subset - # Retrieve timeout for gather - if self._play.gather_timeout is not None: - gather_timeout = self._play.gather_timeout - # Retrieve fact_path - if self._play.fact_path is not None: - fact_path = self._play.fact_path + gather_subset = self._play.gather_subset + gather_timeout = self._play.gather_timeout + fact_path = self._play.fact_path setup_block = Block(play=self._play) # Gathering facts with run_once would copy the facts from one host to @@ -190,11 +180,11 @@ class PlayIterator: setup_task.when = self._play._included_conditional[:] setup_block.block = [setup_task] - setup_block = setup_block.filter_tagged_tasks(play_context, all_vars) + setup_block = setup_block.filter_tagged_tasks(all_vars) self._blocks.append(setup_block) for block in self._play.compile(): - new_block = block.filter_tagged_tasks(play_context, all_vars) + new_block = block.filter_tagged_tasks(all_vars) if new_block.has_tasks(): self._blocks.append(new_block) diff --git a/lib/ansible/executor/playbook_executor.py b/lib/ansible/executor/playbook_executor.py index 4fad903a523..51c42b5021b 100644 --- a/lib/ansible/executor/playbook_executor.py +++ b/lib/ansible/executor/playbook_executor.py @@ -25,6 +25,7 @@ from ansible import constants as C from ansible import context from ansible.executor.task_queue_manager import TaskQueueManager from ansible.module_utils._text import to_native, to_text +from ansible.plugins.loader import become_loader, connection_loader, shell_loader from ansible.playbook import Playbook from ansible.template import Templar from ansible.plugins.loader import connection_loader, shell_loader @@ -82,9 +83,10 @@ class PlaybookExecutor: entrylist = [] entry = {} try: - # preload become/connecition/shell to set config defs cached + # preload become/connection/shell to set config defs cached list(connection_loader.all(class_only=True)) list(shell_loader.all(class_only=True)) + list(become_loader.all(class_only=True)) for playbook_path in self._playbooks: pb = Playbook.load(playbook_path, variable_manager=self._variable_manager, loader=self._loader) diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py index 8bf0adf1bbb..0786660f2cb 100644 --- a/lib/ansible/executor/task_executor.py +++ b/lib/ansible/executor/task_executor.py @@ -23,6 +23,7 @@ from ansible.module_utils._text import to_text, to_native from ansible.module_utils.connection import write_to_file_descriptor from ansible.playbook.conditional import Conditional from ansible.playbook.task import Task +from ansible.plugins.loader import become_loader from ansible.template import Templar from ansible.utils.listify import listify_lookup_plugin_terms from ansible.utils.unsafe_proxy import UnsafeProxy, wrap_var @@ -590,7 +591,6 @@ class TaskExecutor: self._connection._play_context = self._play_context self._set_connection_options(variables, templar) - self._set_shell_options(variables, templar) # get handler self._handler = self._get_action_handler(connection=self._connection, templar=templar) @@ -849,6 +849,13 @@ class TaskExecutor: else: return async_result + def _get_become(self, name): + become = become_loader.get(name) + if not become: + raise AnsibleError("Invalid become method specified, could not find matching plugin: '%s'. " + "Use `ansible-doc -t become -l` to list available plugins." % name) + return become + def _get_connection(self, variables, templar): ''' Reads the connection property for the host, and returns the @@ -869,8 +876,8 @@ class TaskExecutor: if isinstance(i, string_types) and i.startswith("ansible_") and i.endswith("_interpreter"): variables[i] = delegated_vars[i] + # load connection conn_type = self._play_context.connection - connection = self._shared_loader_obj.connection_loader.get( conn_type, self._play_context, @@ -882,8 +889,30 @@ class TaskExecutor: if not connection: raise AnsibleError("the connection plugin '%s' was not found" % conn_type) + # load become plugin if needed + become_plugin = None + if self._play_context.become: + become_plugin = self._get_become(self._play_context.become_method) + + if getattr(become_plugin, 'require_tty', False) and not getattr(connection, 'has_tty', False): + raise AnsibleError( + "The '%s' connection does not provide a tty which is requied for the selected " + "become plugin: %s." % (conn_type, become_plugin.name) + ) + + try: + connection.set_become_plugin(become_plugin) + except AttributeError: + # Connection plugin does not support set_become_plugin + pass + + # Backwards compat for connection plugins that don't support become plugins + # Just do this unconditionally for now, we could move it inside of the + # AttributeError above later + self._play_context.set_become_plugin(become_plugin) + # FIXME: remove once all plugins pull all data from self._options - self._play_context.set_options_from_plugin(connection) + self._play_context.set_attributes_from_plugin(connection) if any(((connection.supports_persistence and C.USE_PERSISTENT_CONNECTIONS), connection.force_persistence)): self._play_context.timeout = connection.get_option('persistent_command_timeout') @@ -912,13 +941,30 @@ class TaskExecutor: return options + def _set_plugin_options(self, plugin_type, variables, templar, task_keys): + try: + plugin = getattr(self._connection, '_%s' % plugin_type) + except AttributeError: + # Some plugins are assigned to private attrs, ``become`` is not + plugin = getattr(self._connection, plugin_type) + option_vars = C.config.get_plugin_vars(plugin_type, plugin._load_name) + options = {} + for k in option_vars: + if k in variables: + options[k] = templar.template(variables[k]) + # TODO move to task method? + plugin.set_options(task_keys=task_keys, var_options=options) + def _set_connection_options(self, variables, templar): # Keep the pre-delegate values for these keys PRESERVE_ORIG = ('inventory_hostname',) # create copy with delegation built in - final_vars = combine_vars(variables, variables.get('ansible_delegated_vars', dict()).get(self._task.delegate_to, dict())) + final_vars = combine_vars( + variables, + variables.get('ansible_delegated_vars', {}).get(self._task.delegate_to, {}) + ) # grab list of usable vars for this plugin option_vars = C.config.get_plugin_vars('connection', self._connection._load_name) @@ -937,17 +983,25 @@ class TaskExecutor: if k.startswith('ansible_%s_' % self._connection._load_name) and k not in options: options['_extras'][k] = templar.template(final_vars[k]) - # set options with 'templated vars' specific to this plugin - self._connection.set_options(var_options=options) - self._set_shell_options(final_vars, templar) + task_keys = self._task.dump_attrs() - def _set_shell_options(self, variables, templar): - option_vars = C.config.get_plugin_vars('shell', self._connection._shell._load_name) - options = {} - for k in option_vars: - if k in variables: - options[k] = templar.template(variables[k]) - self._connection._shell.set_options(var_options=options) + # set options with 'templated vars' specific to this plugin and dependant ones + self._connection.set_options(task_keys=task_keys, var_options=options) + self._set_plugin_options('shell', final_vars, templar, task_keys) + + if self._connection.become is not None: + # FIXME: find alternate route to provide passwords, + # keep out of play objects to avoid accidental disclosure + task_keys['become_pass'] = self._play_context.become_pass + self._set_plugin_options('become', final_vars, templar, task_keys) + + # FOR BACKWARDS COMPAT: + for option in ('become_user', 'become_flags', 'become_exe'): + try: + setattr(self._play_context, option, self._connection.become.get_option(option)) + except KeyError: + pass # some plugins don't support all base flags + self._play_context.prompt = self._connection.become.prompt def _get_action_handler(self, connection, templar): ''' diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index a72f5fecd14..5c912f6d2a9 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -26,7 +26,6 @@ from ansible.errors import AnsibleParserError from ansible.module_utils._text import to_bytes, to_text, to_native from ansible.playbook.play import Play from ansible.playbook.playbook_include import PlaybookInclude -from ansible.plugins.loader import get_all_plugin_loaders from ansible.utils.display import Display display = Display() @@ -64,13 +63,6 @@ class Playbook: self._file_name = file_name - # dynamically load any plugins from the playbook directory - for name, obj in get_all_plugin_loaders(): - if obj.subdir: - plugin_path = os.path.join(self._basedir, obj.subdir) - if os.path.isdir(to_bytes(plugin_path)): - obj.add_directory(plugin_path) - try: ds = self._loader.load_from_file(os.path.basename(file_name)) except UnicodeDecodeError as e: diff --git a/lib/ansible/playbook/base.py b/lib/ansible/playbook/base.py index c8eae37b941..55530938252 100644 --- a/lib/ansible/playbook/base.py +++ b/lib/ansible/playbook/base.py @@ -14,7 +14,7 @@ from functools import partial from jinja2.exceptions import UndefinedError from ansible import constants as C - +from ansible import context from ansible.module_utils.six import iteritems, string_types, with_metaclass from ansible.module_utils.parsing.convert_bool import boolean from ansible.errors import AnsibleParserError, AnsibleUndefinedVariable, AnsibleAssertionError @@ -520,10 +520,10 @@ class FieldAttributeBase(with_metaclass(BaseMeta, object)): ''' Dumps all attributes to a dictionary ''' - attrs = dict() + attrs = {} 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'): + if attribute.isa == 'class' and hasattr(attr, 'serialize'): attrs[name] = attr.serialize() else: attrs[name] = attr @@ -592,9 +592,9 @@ class Base(FieldAttributeBase): _name = FieldAttribute(isa='string', default='', always_post_validate=True, inherit=False) # connection/transport - _connection = FieldAttribute(isa='string') + _connection = FieldAttribute(isa='string', default=context.cliargs_deferred_get('connection')) _port = FieldAttribute(isa='int') - _remote_user = FieldAttribute(isa='string') + _remote_user = FieldAttribute(isa='string', default=context.cliargs_deferred_get('remote_user')) # variables _vars = FieldAttribute(isa='dict', priority=100, inherit=False) @@ -608,8 +608,8 @@ class Base(FieldAttributeBase): _run_once = FieldAttribute(isa='bool') _ignore_errors = FieldAttribute(isa='bool') _ignore_unreachable = FieldAttribute(isa='bool') - _check_mode = FieldAttribute(isa='bool') - _diff = FieldAttribute(isa='bool') + _check_mode = FieldAttribute(isa='bool', default=context.cliargs_deferred_get('check')) + _diff = FieldAttribute(isa='bool', default=context.cliargs_deferred_get('diff')) _any_errors_fatal = FieldAttribute(isa='bool', default=C.ANY_ERRORS_FATAL) # explicitly invoke a debugger on tasks diff --git a/lib/ansible/playbook/become.py b/lib/ansible/playbook/become.py index 374411cb141..cdaa0ce8a44 100644 --- a/lib/ansible/playbook/become.py +++ b/lib/ansible/playbook/become.py @@ -20,6 +20,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type from ansible import constants as C +from ansible import context from ansible.errors import AnsibleParserError from ansible.playbook.attribute import FieldAttribute from ansible.utils.display import Display @@ -30,9 +31,9 @@ display = Display() class Become: # Privilege escalation - _become = FieldAttribute(isa='bool') - _become_method = FieldAttribute(isa='string') - _become_user = FieldAttribute(isa='string') + _become = FieldAttribute(isa='bool', default=context.cliargs_deferred_get('become')) + _become_method = FieldAttribute(isa='string', default=context.cliargs_deferred_get('become_method')) + _become_user = FieldAttribute(isa='string', default=context.cliargs_deferred_get('become_user')) _become_flags = FieldAttribute(isa='string') def __init__(self): diff --git a/lib/ansible/playbook/block.py b/lib/ansible/playbook/block.py index 9f5a8cf133d..418af42ddff 100644 --- a/lib/ansible/playbook/block.py +++ b/lib/ansible/playbook/block.py @@ -362,10 +362,9 @@ class Block(Base, Become, Conditional, Taggable): return value - def filter_tagged_tasks(self, play_context, all_vars): + def filter_tagged_tasks(self, all_vars): ''' - Creates a new block, with task lists filtered based on the tags contained - within the play_context object. + Creates a new block, with task lists filtered based on the tags. ''' def evaluate_and_append_task(target): @@ -374,8 +373,8 @@ class Block(Base, Become, Conditional, Taggable): if isinstance(task, Block): tmp_list.append(evaluate_block(task)) elif (task.action == 'meta' or - (task.action == 'include' and task.evaluate_tags([], play_context.skip_tags, all_vars=all_vars)) or - task.evaluate_tags(play_context.only_tags, play_context.skip_tags, all_vars=all_vars)): + (task.action == 'include' and task.evaluate_tags([], self._play.skip_tags, all_vars=all_vars)) or + task.evaluate_tags(self._play.only_tags, self._play.skip_tags, all_vars=all_vars)): tmp_list.append(task) return tmp_list diff --git a/lib/ansible/playbook/play.py b/lib/ansible/playbook/play.py index cc73c5e721b..05c6aea2cb4 100644 --- a/lib/ansible/playbook/play.py +++ b/lib/ansible/playbook/play.py @@ -20,6 +20,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type from ansible import constants as C +from ansible import context from ansible.errors import AnsibleParserError, AnsibleAssertionError from ansible.module_utils.six import string_types from ansible.playbook.attribute import FieldAttribute @@ -54,10 +55,10 @@ class Play(Base, Taggable, Become): _hosts = FieldAttribute(isa='list', required=True, listof=string_types, always_post_validate=True) # Facts - _fact_path = FieldAttribute(isa='string', default=None) _gather_facts = FieldAttribute(isa='bool', default=None, always_post_validate=True) _gather_subset = FieldAttribute(isa='list', default=None, listof=string_types, always_post_validate=True) - _gather_timeout = FieldAttribute(isa='int', default=None, always_post_validate=True) + _gather_timeout = FieldAttribute(isa='int', default=C.DEFAULT_GATHER_TIMEOUT, always_post_validate=True) + _fact_path = FieldAttribute(isa='string', default=C.DEFAULT_FACT_PATH) # Variable Attributes _vars_files = FieldAttribute(isa='list', default=list, priority=99) @@ -73,7 +74,7 @@ class Play(Base, Taggable, Become): _tasks = FieldAttribute(isa='list', default=list) # Flag/Setting Attributes - _force_handlers = FieldAttribute(isa='bool', always_post_validate=True) + _force_handlers = FieldAttribute(isa='bool', default=context.cliargs_deferred_get('force_handlers'), always_post_validate=True) _max_fail_percentage = FieldAttribute(isa='percent', always_post_validate=True) _serial = FieldAttribute(isa='list', default=list, always_post_validate=True) _strategy = FieldAttribute(isa='string', default=C.DEFAULT_STRATEGY, always_post_validate=True) @@ -89,6 +90,9 @@ class Play(Base, Taggable, Become): self._removed_hosts = [] self.ROLE_CACHE = {} + self.only_tags = set(context.CLIARGS.get('tags', [])) or frozenset(('all',)) + self.skip_tags = set(context.CLIARGS.get('skip_tags', [])) + def __repr__(self): return self.get_name() diff --git a/lib/ansible/playbook/play_context.py b/lib/ansible/playbook/play_context.py index 8afc561b748..ea04d23151d 100644 --- a/lib/ansible/playbook/play_context.py +++ b/lib/ansible/playbook/play_context.py @@ -23,22 +23,17 @@ __metaclass__ = type import os import pwd -import random -import re -import string import sys from ansible import constants as C from ansible import context from ansible.errors import AnsibleError from ansible.module_utils.six import iteritems -from ansible.module_utils.six.moves import shlex_quote -from ansible.module_utils._text import to_bytes -from ansible.module_utils.parsing.convert_bool import boolean from ansible.playbook.attribute import FieldAttribute from ansible.playbook.base import Base from ansible.plugins import get_plugin_class from ansible.utils.display import Display +from ansible.plugins.loader import get_shell_plugin from ansible.utils.ssh_functions import check_for_controlpersist @@ -47,41 +42,6 @@ display = Display() __all__ = ['PlayContext'] -# TODO: needs to be configurable -b_SU_PROMPT_LOCALIZATIONS = [ - to_bytes('Password'), - to_bytes('암호'), - to_bytes('パスワード'), - to_bytes('Adgangskode'), - to_bytes('Contraseña'), - to_bytes('Contrasenya'), - to_bytes('Hasło'), - to_bytes('Heslo'), - to_bytes('Jelszó'), - to_bytes('Lösenord'), - to_bytes('Mật khẩu'), - to_bytes('Mot de passe'), - to_bytes('Parola'), - to_bytes('Parool'), - to_bytes('Pasahitza'), - to_bytes('Passord'), - to_bytes('Passwort'), - to_bytes('Salasana'), - to_bytes('Sandi'), - to_bytes('Senha'), - to_bytes('Wachtwoord'), - to_bytes('ססמה'), - to_bytes('Лозинка'), - to_bytes('Парола'), - to_bytes('Пароль'), - to_bytes('गुप्तशब्द'), - to_bytes('शब्दकूट'), - to_bytes('సంకేతపదము'), - to_bytes('හස්පදය'), - to_bytes('密码'), - to_bytes('密碼'), - to_bytes('口令'), -] TASK_ATTRIBUTE_OVERRIDES = ( 'become', @@ -113,9 +73,6 @@ RESET_VARS = ( 'ansible_ssh_executable', ) -OPTION_FLAGS = ('connection', 'remote_user', 'private_key_file', 'verbosity', 'force_handlers', 'step', 'start_at_task', 'diff', - 'ssh_common_args', 'docker_extra_args', 'sftp_extra_args', 'scp_extra_args', 'ssh_extra_args') - class PlayContext(Base): @@ -166,14 +123,6 @@ class PlayContext(Base): _become_flags = FieldAttribute(isa='string', default=C.DEFAULT_BECOME_FLAGS) _prompt = FieldAttribute(isa='string') - # DEPRECATED: backwards compatibility fields for sudo/su - _sudo_exe = FieldAttribute(isa='string', default=C.DEFAULT_SUDO_EXE) - _sudo_flags = FieldAttribute(isa='string', default=C.DEFAULT_SUDO_FLAGS) - _sudo_pass = FieldAttribute(isa='string') - _su_exe = FieldAttribute(isa='string', default=C.DEFAULT_SU_EXE) - _su_flags = FieldAttribute(isa='string', default=C.DEFAULT_SU_FLAGS) - _su_pass = FieldAttribute(isa='string') - # general flags _verbosity = FieldAttribute(isa='int', default=0) _only_tags = FieldAttribute(isa='set', default=set) @@ -182,12 +131,10 @@ class PlayContext(Base): _start_at_task = FieldAttribute(isa='string') _step = FieldAttribute(isa='bool', default=False) - # Fact gathering settings - _gather_subset = FieldAttribute(isa='string', default=C.DEFAULT_GATHER_SUBSET) - _gather_timeout = FieldAttribute(isa='string', default=C.DEFAULT_GATHER_TIMEOUT) - _fact_path = FieldAttribute(isa='string', default=C.DEFAULT_FACT_PATH) - def __init__(self, play=None, passwords=None, connection_lockfd=None): + # Note: play is really not optional. The only time it could be omitted is when we create + # a PlayContext just so we can invoke its deserialize method to load it from a serialized + # data source. super(PlayContext, self).__init__() @@ -197,6 +144,8 @@ class PlayContext(Base): self.password = passwords.get('conn_pass', '') self.become_pass = passwords.get('become_pass', '') + self._become_plugin = None + self.prompt = '' self.success_key = '' @@ -205,37 +154,12 @@ class PlayContext(Base): # set options before play to allow play to override them if context.CLIARGS: - self.set_options() + self.set_attributes_from_cli() if play: - self.set_play(play) + self.set_attributes_from_play(play) - def set_play(self, play): - ''' - Configures this connection information instance with data from - the play class. - ''' - - if play.connection: - self.connection = play.connection - - if play.remote_user: - self.remote_user = play.remote_user - - if play.port: - self.port = int(play.port) - - if play.become is not None: - self.become = play.become - if play.become_method: - self.become_method = play.become_method - if play.become_user: - self.become_user = play.become_user - - if play.force_handlers is not None: - self.force_handlers = play.force_handlers - - def set_options_from_plugin(self, plugin): + def set_attributes_from_plugin(self, plugin): # generic derived from connection plugin, temporary for backwards compat, in the end we should not set play_context properties # get options for plugins @@ -246,46 +170,43 @@ class PlayContext(Base): if flag: setattr(self, flag, self.connection.get_option(flag)) - # TODO: made irrelavent by above - # get ssh options - # for flag in ('ssh_common_args', 'docker_extra_args', 'sftp_extra_args', 'scp_extra_args', 'ssh_extra_args'): - # setattr(self, flag, getattr(options, flag, '')) + def set_attributes_from_play(self, play): + # From ansible.playbook.Become + self.become = play.become + self.become_method = play.become_method + self.become_user = play.become_user - def set_options(self): + # From ansible.playbook.Base + self.check_mode = play.check_mode + self.diff = play.diff + self.connection = play.connection + self.remote_user = play.remote_user + + # from ansible.playbook.Play + self.force_handlers = play.force_handlers + self.only_tags = play.only_tags + self.skip_tags = play.skip_tags + + def set_attributes_from_cli(self): ''' Configures this connection information instance with data from options specified by the user on the command line. These have a lower precedence than those set on the play or host. ''' - - # privilege escalation - self.become = context.CLIARGS['become'] - self.become_method = context.CLIARGS['become_method'] - self.become_user = context.CLIARGS['become_user'] - - self.check_mode = boolean(context.CLIARGS['check'], strict=False) - self.diff = boolean(context.CLIARGS['diff'], strict=False) - - # general flags (should we move out?) - # should only be 'non plugin' flags - for flag in OPTION_FLAGS: - attribute = context.CLIARGS.get(flag, False) - if attribute: - setattr(self, flag, attribute) - if context.CLIARGS.get('timeout', False): - self.timeout = context.CLIARGS['timeout'] + self.timeout = int(context.CLIARGS['timeout']) - # get the tag info from options. We check to see if the options have - # the attribute, as it is not always added via the CLI - if context.CLIARGS.get('tags', False): - self.only_tags.update(context.CLIARGS['tags']) + # From the command line. These should probably be used directly by plugins instead + # For now, they are likely to be moved to FieldAttribute defaults + self.private_key_file = context.CLIARGS.get('private_key_file') # Else default + self.verbosity = context.CLIARGS.get('verbosity') # Else default + self.ssh_common_args = context.CLIARGS.get('ssh_common_args') # Else default + self.ssh_extra_args = context.CLIARGS.get('ssh_extra_args') # Else default + self.sftp_extra_args = context.CLIARGS.get('sftp_extra_args') # Else default + self.scp_extra_args = context.CLIARGS.get('scp_extra_args') # Else default - if len(self.only_tags) == 0: - self.only_tags = set(['all']) - - if context.CLIARGS.get('skip_tags', False): - self.skip_tags.update(context.CLIARGS['skip_tags']) + # Not every cli that uses PlayContext has these command line args so have a default + self.start_at_task = context.CLIARGS.get('start_at_task', None) # Else default def set_task_and_variable_override(self, task, variables, templar): ''' @@ -376,13 +297,6 @@ class PlayContext(Base): attrs_considered.append(attr) # no else, as no other vars should be considered - # become legacy updates -- from commandline - if not new_info.become_pass: - if new_info.become_method == 'sudo' and new_info.sudo_pass: - new_info.become_pass = new_info.sudo_pass - elif new_info.become_method == 'su' and new_info.su_pass: - new_info.become_pass = new_info.su_pass - # become legacy updates -- from inventory file (inventory overrides # commandline) for become_pass_name in C.MAGIC_VARIABLE_MAPPING.get('become_pass'): @@ -442,135 +356,43 @@ class PlayContext(Base): return new_info + def set_become_plugin(self, plugin): + self._become_plugin = plugin + def make_become_cmd(self, cmd, executable=None): """ helper function to create privilege escalation commands """ + display.deprecated( + "PlayContext.make_become_cmd should not be used, the calling code should be using become plugins instead", + version="2.12" + ) - prompt = None - success_key = None - self.prompt = None + if not cmd or not self.become: + return cmd - if self.become: + become_method = self.become_method + + # load/call become plugins here + plugin = self._become_plugin + + if plugin: + options = { + 'become_exe': self.become_exe or become_method, + 'become_flags': self.become_flags or '', + 'become_user': self.become_user, + 'become_pass': self.become_pass + } + plugin.set_options(direct=options) if not executable: executable = self.executable - becomecmd = None - randbits = ''.join(random.choice(string.ascii_lowercase) for x in range(32)) - success_key = 'BECOME-SUCCESS-%s' % randbits - success_cmd = shlex_quote('echo %s; %s' % (success_key, cmd)) - - if executable: - command = '%s -c %s' % (executable, success_cmd) - else: - command = success_cmd - - # set executable to use for the privilege escalation method, with various overrides - exe = self.become_exe or getattr(self, '%s_exe' % self.become_method, self.become_method) - - # set flags to use for the privilege escalation method, with various overrides - flags = self.become_flags or getattr(self, '%s_flags' % self.become_method, '') - - if self.become_method == 'sudo': - # If we have a password, we run sudo with a randomly-generated - # prompt set using -p. Otherwise we run it with default -n, which makes - # it fail if it would have prompted for a password. - # Cannot rely on -n as it can be removed from defaults, which should be - # done for older versions of sudo that do not support the option. - # - # Passing a quoted compound command to sudo (or sudo -s) - # directly doesn't work, so we shellquote it with shlex_quote() - # and pass the quoted string to the user's shell. - - # force quick error if password is required but not supplied, should prevent sudo hangs. - if self.become_pass: - prompt = '[sudo via ansible, key=%s] password: ' % randbits - becomecmd = '%s %s -p "%s" -u %s %s' % (exe, flags.replace('-n', ''), prompt, self.become_user, command) - else: - becomecmd = '%s %s -u %s %s' % (exe, flags, self.become_user, command) - - elif self.become_method == 'su': - - # passing code ref to examine prompt as simple string comparisson isn't good enough with su - def detect_su_prompt(b_data): - b_password_string = b"|".join([br'(\w+\'s )?' + x for x in b_SU_PROMPT_LOCALIZATIONS]) - # Colon or unicode fullwidth colon - b_password_string = b_password_string + to_bytes(u' ?(:|:) ?') - b_SU_PROMPT_LOCALIZATIONS_RE = re.compile(b_password_string, flags=re.IGNORECASE) - return bool(b_SU_PROMPT_LOCALIZATIONS_RE.match(b_data)) - prompt = detect_su_prompt - - becomecmd = '%s %s %s -c %s' % (exe, flags, self.become_user, shlex_quote(command)) - - elif self.become_method == 'pbrun': - - prompt = 'Password:' - becomecmd = '%s %s -u %s %s' % (exe, flags, self.become_user, success_cmd) - - elif self.become_method == 'ksu': - def detect_ksu_prompt(b_data): - return re.match(b"Kerberos password for .*@.*:", b_data) - - prompt = detect_ksu_prompt - becomecmd = '%s %s %s -e %s' % (exe, self.become_user, flags, command) - - elif self.become_method == 'pfexec': - - # No user as it uses it's own exec_attr to figure it out - becomecmd = '%s %s "%s"' % (exe, flags, success_cmd) - - elif self.become_method == 'runas': - # become is handled inside the WinRM connection plugin - if not self.become_user: - raise AnsibleError(("The 'runas' become method requires a username " - "(specify with the '--become-user' CLI arg, the 'become_user' keyword, or the 'ansible_become_user' variable)")) - becomecmd = cmd - - elif self.become_method == 'doas': - - prompt = 'doas (%s@' % self.remote_user - exe = self.become_exe or 'doas' - - if not self.become_pass: - flags += ' -n ' - - if self.become_user: - flags += ' -u %s ' % self.become_user - - # FIXME: make shell independent - becomecmd = '%s %s %s -c %s' % (exe, flags, executable, success_cmd) - - elif self.become_method == 'dzdo': - # If we have a password, we run dzdo with a randomly-generated - # prompt set using -p. Otherwise we run it with -n, if - # requested, which makes it fail if it would have prompted for a - # password. - - exe = self.become_exe or 'dzdo' - if self.become_pass: - prompt = '[dzdo via ansible, key=%s] password: ' % randbits - becomecmd = '%s %s -p %s -u %s %s' % (exe, flags.replace('-n', ''), shlex_quote(prompt), self.become_user, command) - else: - becomecmd = '%s %s -u %s %s' % (exe, flags, self.become_user, command) - - elif self.become_method == 'pmrun': - - exe = self.become_exe or 'pmrun' - - prompt = 'Enter UPM user password:' - becomecmd = '%s %s %s' % (exe, flags, shlex_quote(command)) - - elif self.become_method == 'machinectl': - - exe = self.become_exe or 'machinectl' - becomecmd = '%s shell -q %s %s@ %s' % (exe, flags, self.become_user, command) - - else: - raise AnsibleError("Privilege escalation method not found: %s" % self.become_method) - + shell = get_shell_plugin(executable=executable) + cmd = plugin.build_become_command(cmd, shell) + # for backwards compat: if self.become_pass: - self.prompt = prompt - self.success_key = success_key - return becomecmd + self.prompt = plugin.prompt + else: + raise AnsibleError("Privilege escalation method not found: %s" % become_method) return cmd diff --git a/lib/ansible/playbook/role/__init__.py b/lib/ansible/playbook/role/__init__.py index 3d5008346de..5a01b453ed6 100644 --- a/lib/ansible/playbook/role/__init__.py +++ b/lib/ansible/playbook/role/__init__.py @@ -31,7 +31,7 @@ from ansible.playbook.conditional import Conditional from ansible.playbook.helpers import load_list_of_blocks from ansible.playbook.role.metadata import RoleMetadata from ansible.playbook.taggable import Taggable -from ansible.plugins.loader import get_all_plugin_loaders +from ansible.plugins.loader import add_all_plugin_dirs from ansible.utils.vars import combine_vars @@ -194,12 +194,8 @@ class Role(Base, Become, Conditional, Taggable): else: self._attributes[attr_name] = role_include._attributes[attr_name] - # dynamically load any plugins from the role directory - for name, obj in get_all_plugin_loaders(): - if obj.subdir: - plugin_path = os.path.join(self._role_path, obj.subdir) - if os.path.isdir(plugin_path): - obj.add_directory(plugin_path) + # ensure all plugins dirs for this role are added to plugin search path + add_all_plugin_dirs(self._role_path) # vars and default vars are regular dictionaries self._role_vars = self._load_role_yaml('vars', main=self._from_files.get('vars'), allow_dir=True) diff --git a/lib/ansible/plugins/__init__.py b/lib/ansible/plugins/__init__.py index a788222936d..bcd43d5ca88 100644 --- a/lib/ansible/plugins/__init__.py +++ b/lib/ansible/plugins/__init__.py @@ -24,6 +24,8 @@ __metaclass__ = type from abc import ABCMeta from ansible import constants as C +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_native from ansible.module_utils.six import with_metaclass, string_types from ansible.utils.display import Display @@ -52,7 +54,10 @@ class AnsiblePlugin(with_metaclass(ABCMeta, object)): def get_option(self, option, hostvars=None): if option not in self._options: - option_value = C.config.get_config_value(option, plugin_type=get_plugin_class(self), plugin_name=self._load_name, variables=hostvars) + try: + option_value = C.config.get_config_value(option, plugin_type=get_plugin_class(self), plugin_name=self._load_name, variables=hostvars) + except AnsibleError as e: + raise KeyError(to_native(e)) self.set_option(option, option_value) return self._options.get(option) diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index 745f0c1a2f3..1b3929a3a74 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -108,6 +108,24 @@ class ActionBase(with_metaclass(ABCMeta, object)): return result + def get_plugin_option(self, plugin, option, default=None): + """Helper to get an option from a plugin without having to use + the try/except dance everywhere to set a default + """ + try: + return plugin.get_option(option) + except (AttributeError, KeyError): + return default + + def get_become_option(self, option, default=None): + return self.get_plugin_option(self._connection.become, option, default=default) + + def get_connection_option(self, option, default=None): + return self.get_plugin_option(self._connection, option, default=default) + + def get_shell_option(self, option, default=None): + return self.get_plugin_option(self._connection._shell, option, default=default) + def _remote_file_exists(self, path): cmd = self._connection._shell.exists(path) result = self._low_level_execute_command(cmd=cmd, sudoable=True) @@ -241,12 +259,23 @@ class ActionBase(with_metaclass(ABCMeta, object)): Returns a list of admin users that are configured for the current shell plugin ''' + + return self.get_shell_option('admin_users', ['root']) + + def _get_remote_user(self): + ''' consistently get the 'remote_user' for the action plugin ''' + # TODO: use 'current user running ansible' as fallback when moving away from play_context + # pwd.getpwuid(os.getuid()).pw_name + remote_user = None try: - admin_users = self._connection._shell.get_option('admin_users') - except AnsibleError: - # fallback for old custom plugins w/o get_option - admin_users = ['root'] - return admin_users + remote_user = self._connection.get_option('remote_user') + except KeyError: + # plugin does not have remote_user option, fallback to default and/play_context + remote_user = getattr(self._connection, 'default_user', None) or self._play_context.remote_user + except AttributeError: + # plugin does not use config system, fallback to old play_context + remote_user = self._play_context.remote_user + return remote_user def _is_become_unprivileged(self): ''' @@ -261,11 +290,8 @@ class ActionBase(with_metaclass(ABCMeta, object)): # if we use become and the user is not an admin (or same user) then # we need to return become_unprivileged as True admin_users = self._get_admin_users() - try: - remote_user = self._connection.get_option('remote_user') - except AnsibleError: - remote_user = self._play_context.remote_user - return bool(self._play_context.become_user not in admin_users + [remote_user]) + remote_user = self._get_remote_user() + return bool(self.get_become_option('become_user') not in admin_users + [remote_user]) def _make_tmp_path(self, remote_user=None): ''' @@ -273,10 +299,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): ''' become_unprivileged = self._is_become_unprivileged() - try: - remote_tmp = self._connection._shell.get_option('remote_tmp') - except AnsibleError: - remote_tmp = '~/.ansible/tmp' + remote_tmp = self.get_shell_option('remote_tmp', default='~/.ansible/tmp') # deal with tmpdir creation basefile = 'ansible-tmp-%s-%s' % (time.time(), random.randint(0, 2**48)) @@ -409,7 +432,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): "allow_world_readable_tmpfiles" in the ansible.cfg """ if remote_user is None: - remote_user = self._play_context.remote_user + remote_user = self._get_remote_user() if self._connection._shell.SHELL_FAMILY == 'powershell': # This won't work on Powershell as-is, so we'll just completely skip until @@ -432,7 +455,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): # start to we'll have to fix this. setfacl_mode = 'r-X' - res = self._remote_set_user_facl(remote_paths, self._play_context.become_user, setfacl_mode) + res = self._remote_set_user_facl(remote_paths, self.get_become_option('become_user'), setfacl_mode) if res['rc'] != 0: # File system acls failed; let's try to use chown next # Set executable bit first as on some systems an @@ -442,7 +465,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): if res['rc'] != 0: raise AnsibleError('Failed to set file mode on remote temporary files (rc: {0}, err: {1})'.format(res['rc'], to_native(res['stderr']))) - res = self._remote_chown(remote_paths, self._play_context.become_user) + res = self._remote_chown(remote_paths, self.get_become_option('become_user')) if res['rc'] != 0 and remote_user in self._get_admin_users(): # chown failed even if remote_user is administrator/root raise AnsibleError('Failed to change ownership of the temporary files Ansible needs to create despite connecting as a privileged user. ' @@ -579,13 +602,14 @@ class ActionBase(with_metaclass(ABCMeta, object)): # Network connection plugins (network_cli, netconf, etc.) execute on the controller, rather than the remote host. # As such, we want to avoid using remote_user for paths as remote_user may not line up with the local user # This is a hack and should be solved by more intelligent handling of remote_tmp in 2.7 + become_user = self.get_become_option('become_user') if getattr(self._connection, '_remote_is_local', False): pass - elif sudoable and self._play_context.become and self._play_context.become_user: - expand_path = '~%s' % self._play_context.become_user + elif sudoable and self._play_context.become and become_user: + expand_path = '~%s' % become_user else: # use remote user instead, if none set default to current user - expand_path = '~%s' % (self._play_context.remote_user or self._connection.default_user or '') + expand_path = '~%s' % (self._get_remote_user() or '') # use shell to construct appropriate command and execute cmd = self._connection._shell.expand_user(expand_path) @@ -673,26 +697,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): module_args['_ansible_tmpdir'] = self._connection._shell.tmpdir # make sure the remote_tmp value is sent through in case modules needs to create their own - try: - module_args['_ansible_remote_tmp'] = self._connection._shell.get_option('remote_tmp') - except KeyError: - # here for 3rd party shell plugin compatibility in case they do not define the remote_tmp option - module_args['_ansible_remote_tmp'] = '~/.ansible/tmp' - - def _update_connection_options(self, options, variables=None): - ''' ensures connections have the appropriate information ''' - update = {} - - if getattr(self.connection, 'glob_option_vars', False): - # if the connection allows for it, pass any variables matching it. - if variables is not None: - for varname in variables: - if varname.match('ansible_%s_' % self.connection._load_name): - update[varname] = variables[varname] - - # always override existing with options - update.update(options) - self.connection.set_options(update) + module_args['_ansible_remote_tmp'] = self.get_shell_option('remote_tmp', default='~/.ansible/tmp') def _execute_module(self, module_name=None, module_args=None, tmp=None, task_vars=None, persist_files=False, delete_remote_tmp=None, wrap_async=False): ''' @@ -748,11 +753,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): # ANSIBLE_ASYNC_DIR is not set on the task, we get the value # from the shell option and temporarily add to the environment # list for async_wrapper to pick up - try: - async_dir = self._connection._shell.get_option('async_dir') - except KeyError: - # in case 3rd party plugin has not set this, use the default - async_dir = "~/.ansible_async" + async_dir = self.get_shell_option('async_dir', default="~/.ansible_async") remove_async_dir = len(self._task.environment) self._task.environment.append({"ANSIBLE_ASYNC_DIR": async_dir}) @@ -861,7 +862,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): if remote_files: # remove none/empty remote_files = [x for x in remote_files if x] - self._fixup_perms2(remote_files, self._play_context.remote_user) + self._fixup_perms2(remote_files, self._get_remote_user()) # actually execute res = self._low_level_execute_command(cmd, sudoable=sudoable, in_data=in_data) @@ -934,6 +935,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): data['rc'] = res['rc'] return data + # FIXME: move to connection base def _low_level_execute_command(self, cmd, sudoable=True, in_data=None, executable=None, encoding_errors='surrogate_then_replace', chdir=None): ''' This is the function which executes the low level shell command, which @@ -951,21 +953,20 @@ class ActionBase(with_metaclass(ABCMeta, object)): ''' display.debug("_low_level_execute_command(): starting") -# if not cmd: -# # this can happen with powershell modules when there is no analog to a Windows command (like chmod) -# display.debug("_low_level_execute_command(): no command, exiting") -# return dict(stdout='', stderr='', rc=254) + # if not cmd: + # # this can happen with powershell modules when there is no analog to a Windows command (like chmod) + # display.debug("_low_level_execute_command(): no command, exiting") + # return dict(stdout='', stderr='', rc=254) if chdir: display.debug("_low_level_execute_command(): changing cwd to %s for this command" % chdir) cmd = self._connection._shell.append_command('cd %s' % chdir, cmd) - allow_same_user = C.BECOME_ALLOW_SAME_USER - same_user = self._play_context.become_user == self._play_context.remote_user - if sudoable and self._play_context.become and (allow_same_user or not same_user): + if (sudoable and self._connection.transport != 'network_cli' and self._connection.become and + (C.BECOME_ALLOW_SAME_USER or + self.get_become_option('become_user') != self._get_remote_user())): display.debug("_low_level_execute_command(): using become for this command") - if self._connection.transport != 'network_cli' and self._play_context.become_method != 'enable': - cmd = self._play_context.make_become_cmd(cmd, executable=executable) + cmd = self._connection.become.build_become_command(cmd, self._connection._shell) if self._connection.allow_executable: if executable is None: diff --git a/lib/ansible/plugins/action/async_status.py b/lib/ansible/plugins/action/async_status.py index 108d81c3e6a..96888d98da3 100644 --- a/lib/ansible/plugins/action/async_status.py +++ b/lib/ansible/plugins/action/async_status.py @@ -37,12 +37,7 @@ class ActionModule(ActionBase): else: # inject the async directory based on the shell option into the # module args - try: - async_dir = self._connection._shell.get_option('async_dir') - except KeyError: - # here for 3rd party shell plugin compatibility in case they do - # not define the async_dir option - async_dir = "~/.ansible_async" + async_dir = self.get_shell_option('async_dir', default="~/.ansible_async") module_args = dict(jid=jid, mode=mode, _async_dir=async_dir) status = self._execute_module(task_vars=task_vars, diff --git a/lib/ansible/plugins/action/reboot.py b/lib/ansible/plugins/action/reboot.py index 00bb719a2b9..c2d92e86a37 100644 --- a/lib/ansible/plugins/action/reboot.py +++ b/lib/ansible/plugins/action/reboot.py @@ -307,7 +307,7 @@ class ActionModule(ActionBase): # Get the connect_timeout set on the connection to compare to the original try: connect_timeout = self._connection.get_option('connection_timeout') - except AnsibleError: + except KeyError: pass else: if original_connection_timeout != connect_timeout: @@ -380,7 +380,7 @@ class ActionModule(ActionBase): try: original_connection_timeout = self._connection.get_option('connection_timeout') display.debug("{action}: saving original connect_timeout of {timeout}".format(action=self._task.action, timeout=original_connection_timeout)) - except AnsibleError: + except KeyError: display.debug("{action}: connect_timeout connection option has not been set".format(action=self._task.action)) # Initiate reboot reboot_result = self.perform_reboot(task_vars, distribution) diff --git a/lib/ansible/plugins/become/__init__.py b/lib/ansible/plugins/become/__init__.py new file mode 100644 index 00000000000..6d27b1d70e9 --- /dev/null +++ b/lib/ansible/plugins/become/__init__.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from abc import abstractmethod +from random import choice +from string import ascii_lowercase +from gettext import dgettext + +from ansible.module_utils.six.moves import shlex_quote +from ansible.module_utils._text import to_bytes +from ansible.plugins import AnsiblePlugin + + +def _gen_id(length=32): + ''' return random string used to identify the current privelege escalation ''' + return ''.join(choice(ascii_lowercase) for x in range(length)) + + +class BecomeBase(AnsiblePlugin): + + name = None + + # messages for detecting prompted password issues + fail = tuple() + missing = tuple() + + # many connection plugins cannot provide tty, set to True if your become + # plugin requires a tty, i.e su + require_tty = False + + # prompt to match + prompt = '' + + def __init__(self): + super(BecomeBase, self).__init__() + self._id = '' + self.success = '' + + def expect_prompt(self): + """This function assists connection plugins in determining if they need to wait for + a prompt. Both a prompt and a password are required. + """ + return self.prompt and self.get_option('become_pass') + + def _build_success_command(self, cmd, shell, noexe=False): + if not all((cmd, shell, self.success)): + return cmd + + cmd = shlex_quote('%s %s %s %s' % (shell.ECHO, self.success, shell.COMMAND_SEP, cmd)) + exe = getattr(shell, 'executable', None) + if exe and not noexe: + cmd = '%s -c %s' % (exe, cmd) + return cmd + + @abstractmethod + def build_become_command(self, cmd, shell): + self._id = _gen_id() + self.success = 'BECOME-SUCCESS-%s' % self._id + + def check_success(self, b_output): + b_success = to_bytes(self.success) + return any(b_success in l.rstrip() for l in b_output.splitlines(True)) + + def check_password_prompt(self, b_output): + ''' checks if the expected passwod prompt exists in b_output ''' + if self.prompt: + b_prompt = to_bytes(self.prompt).strip() + return any(l.strip().startswith(b_prompt) for l in b_output.splitlines()) + return False + + def _check_password_error(self, b_out, msg): + ''' returns True/False if domain specific i18n version of msg is found in b_out ''' + b_fail = to_bytes(dgettext(self.name, msg)) + return b_fail and b_fail in b_out + + def check_incorrect_password(self, b_output): + for errstring in self.fail: + if self._check_password_error(b_output, errstring): + return True + return False + + def check_missing_password(self, b_output): + for errstring in self.missing: + if self._check_password_error(b_output, errstring): + return True + return False diff --git a/lib/ansible/plugins/become/doas.py b/lib/ansible/plugins/become/doas.py new file mode 100644 index 00000000000..16c9436e47f --- /dev/null +++ b/lib/ansible/plugins/become/doas.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + become: doas + short_description: Do As user + description: + - This become plugins allows your remote/login user to execute commands as another user via the doas utility. + author: ansible (@core) + version_added: "2.8" + options: + become_user: + description: User you 'become' to execute the task + ini: + - section: privilege_escalation + key: become_user + - section: doas_become_plugin + key: user + vars: + - name: ansible_become_user + - name: ansible_doas_user + env: + - name: ANSIBLE_BECOME_USER + - name: ANSIBLE_DOAS_USER + become_exe: + description: Doas executable + default: doas + ini: + - section: privilege_escalation + key: become_exe + - section: doas_become_plugin + key: executable + vars: + - name: ansible_become_exe + - name: ansible_doas_exe + env: + - name: ANSIBLE_BECOME_EXE + - name: ANSIBLE_DOAS_EXE + become_flags: + description: Options to pass to doas + default: + ini: + - section: privilege_escalation + key: become_flags + - section: doas_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_doas_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_DOAS_FLAGS + become_pass: + description: password for doas prompt + required: False + vars: + - name: ansible_become_password + - name: ansible_become_pass + - name: ansible_doas_pass + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_DOAS_PASS + ini: + - section: doas_become_plugin + key: password + prompt_l10n: + description: + - List of localized strings to match for prompt detection + - If empty we'll use the built in one + default: [] + ini: + - section: doas_become_plugin + key: localized_prompts + vars: + - name: ansible_doas_prompt_l10n + env: + - name: ANSIBLE_DOAS_PROMPT_L10N +""" + +import re + +from ansible.module_utils._text import to_bytes +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + + name = 'doas' + + # messages for detecting prompted password issues + fail = ('Permission denied',) + missing = ('Authorization required',) + + def check_password_prompt(self, b_output): + ''' checks if the expected passwod prompt exists in b_output ''' + + # FIXME: more accurate would be: 'doas (%s@' % remote_user + # however become plugins don't have that information currently + b_prompts = [to_bytes(p) for p in self.get_option('prompt_l10n')] or [br'doas \(', br'Password:'] + b_prompt = b"|".join(b_prompts) + + return bool(re.match(b_prompt, b_output)) + + def build_become_command(self, cmd, shell): + super(BecomeModule, self).build_become_command(cmd, shell) + + if not cmd: + return cmd + + self.prompt = True + + become_exe = self.get_option('become_exe') or self.name + + flags = self.get_option('become_flags') or '' + if not self.get_option('become_pass') and '-n' not in flags: + flags += ' -n' + + user = self.get_option('become_user') or '' + if user: + user = '-u %s' % (user) + + success_cmd = self._build_success_command(cmd, shell, noexe=True) + executable = getattr(shell, 'executable', shell.SHELL_FAMILY) + + return '%s %s %s %s -c %s' % (become_exe, flags, user, executable, success_cmd) diff --git a/lib/ansible/plugins/become/dzdo.py b/lib/ansible/plugins/become/dzdo.py new file mode 100644 index 00000000000..8305a58a05e --- /dev/null +++ b/lib/ansible/plugins/become/dzdo.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + become: dzdo + short_description: Centrify's Direct Authorize + description: + - This become plugins allows your remote/login user to execute commands as another user via the dzdo utility. + author: ansible (@core) + version_added: "2.8" + options: + become_user: + description: User you 'become' to execute the task + ini: + - section: privilege_escalation + key: become_user + - section: dzdo_become_plugin + key: user + vars: + - name: ansible_become_user + - name: ansible_dzdo_user + env: + - name: ANSIBLE_BECOME_USER + - name: ANSIBLE_DZDO_USER + become_exe: + description: Sudo executable + default: dzdo + ini: + - section: privilege_escalation + key: become_exe + - section: dzdo_become_plugin + key: executable + vars: + - name: ansible_become_exe + - name: ansible_dzdo_exe + env: + - name: ANSIBLE_BECOME_EXE + - name: ANSIBLE_DZDO_EXE + become_flags: + description: Options to pass to dzdo + default: -H -S -n + ini: + - section: privilege_escalation + key: become_flags + - section: dzdo_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_dzdo_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_DZDO_FLAGS + become_pass: + description: Options to pass to dzdo + required: False + vars: + - name: ansible_become_password + - name: ansible_become_pass + - name: ansible_dzdo_pass + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_DZDO_PASS + ini: + - section: dzdo_become_plugin + key: password +""" + +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + + name = 'dzdo' + + # messages for detecting prompted password issues + fail = ('Sorry, try again.',) + + def build_become_command(self, cmd, shell): + super(BecomeModule, self).build_become_command(cmd, shell) + + if not cmd: + return cmd + + becomecmd = self.get_option('become_exe') or self.name + + flags = self.get_option('become_flags') or '' + if self.get_option('become_pass'): + self._prompt = '[dzdo via ansible, key=%s] password:' % self._id + flags = '%s -p "%s"' % (flags.replace('-n', ''), self._prompt) + + user = self.get_option('become_user') or '' + if user: + user = '-u %s' % (user) + + return ' '.join([becomecmd, flags, user, self._build_success_command(cmd, shell)]) diff --git a/lib/ansible/plugins/become/ksu.py b/lib/ansible/plugins/become/ksu.py new file mode 100644 index 00000000000..bb5f88aa6af --- /dev/null +++ b/lib/ansible/plugins/become/ksu.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + become: ksu + short_description: Kerberos substitute user + description: + - This become plugins allows your remote/login user to execute commands as another user via the ksu utility. + author: ansible (@core) + version_added: "2.8" + options: + become_user: + description: User you 'become' to execute the task + ini: + - section: privilege_escalation + key: become_user + - section: ksu_become_plugin + key: user + vars: + - name: ansible_become_user + - name: ansible_ksu_user + env: + - name: ANSIBLE_BECOME_USER + - name: ANSIBLE_KSU_USER + required: True + become_exe: + description: Su executable + default: ksu + ini: + - section: privilege_escalation + key: become_exe + - section: ksu_become_plugin + key: executable + vars: + - name: ansible_become_exe + - name: ansible_ksu_exe + env: + - name: ANSIBLE_BECOME_EXE + - name: ANSIBLE_KSU_EXE + become_flags: + description: Options to pass to ksu + default: '' + ini: + - section: privilege_escalation + key: become_flags + - section: ksu_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_ksu_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_KSU_FLAGS + become_pass: + description: ksu password + required: False + vars: + - name: ansible_ksu_pass + - name: ansible_become_pass + - name: ansible_become_password + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_KSU_PASS + ini: + - section: ksu_become_plugin + key: password + prompt_l10n: + description: + - List of localized strings to match for prompt detection + - If empty we'll use the built in one + default: [] + ini: + - section: ksu_become_plugin + key: localized_prompts + vars: + - name: ansible_ksu_prompt_l10n + env: + - name: ANSIBLE_KSU_PROMPT_L10N +""" + +import re + +from ansible.module_utils._text import to_bytes +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + + name = 'ksu' + + # messages for detecting prompted password issues + fail = ('Password incorrect',) + missing = ('No password given',) + + def check_password_prompt(self, b_output): + ''' checks if the expected passwod prompt exists in b_output ''' + + prompts = self.get_option('prompt_l10n') or ["Kerberos password for .*@.*:"] + b_prompt = b"|".join(to_bytes(p) for p in prompts) + + return bool(re.match(b_prompt, b_output)) + + def build_become_command(self, cmd, shell): + + super(BecomeModule, self).build_become_command(cmd, shell) + + # Prompt handling for ``ksu`` is more complicated, this + # is used to satisfy the connection plugin + self.prompt = True + + if not cmd: + return cmd + + exe = self.get_option('become_exe') or self.name + flags = self.get_option('become_flags') or '' + user = self.get_option('become_user') or '' + return '%s %s %s -e %s ' % (exe, user, flags, self._build_success_command(cmd, shell)) diff --git a/lib/ansible/plugins/become/machinectl.py b/lib/ansible/plugins/become/machinectl.py new file mode 100644 index 00000000000..811f9c514ef --- /dev/null +++ b/lib/ansible/plugins/become/machinectl.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + become: machinectl + short_description: Systemd's machinectl privilege escalation + description: + - This become plugins allows your remote/login user to execute commands as another user via the machinectl utility. + author: ansible (@core) + version_added: "2.8" + options: + become_user: + description: User you 'become' to execute the task + ini: + - section: privilege_escalation + key: become_user + - section: machinectl_become_plugin + key: user + vars: + - name: ansible_become_user + - name: ansible_machinectl_user + env: + - name: ANSIBLE_BECOME_USER + - name: ANSIBLE_MACHINECTL_USER + become_exe: + description: Machinectl executable + default: machinectl + ini: + - section: privilege_escalation + key: become_exe + - section: machinectl_become_plugin + key: executable + vars: + - name: ansible_become_exe + - name: ansible_machinectl_exe + env: + - name: ANSIBLE_BECOME_EXE + - name: ANSIBLE_MACHINECTL_EXE + become_flags: + description: Options to pass to machinectl + default: '' + ini: + - section: privilege_escalation + key: become_flags + - section: machinectl_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_machinectl_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_MACHINECTL_FLAGS + become_pass: + description: Password for machinectl + required: False + vars: + - name: ansible_become_password + - name: ansible_become_pass + - name: ansible_machinectl_pass + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_MACHINECTL_PASS + ini: + - section: machinectl_become_plugin + key: password +""" + +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + + name = 'machinectl' + + def build_become_command(self, cmd, shell): + super(BecomeModule, self).build_become_command(cmd, shell) + + if not cmd: + return cmd + + become = self._get_option('become_exe') or self.name + flags = self.get_option('flags') or '' + user = self.get_option('become_user') or '' + return '%s shell -q %s %s@ %s' % (become, flags, user, cmd) diff --git a/lib/ansible/plugins/become/pbrun.py b/lib/ansible/plugins/become/pbrun.py new file mode 100644 index 00000000000..e232ccec343 --- /dev/null +++ b/lib/ansible/plugins/become/pbrun.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + become: pbrun + short_description: PowerBroker run + description: + - This become plugins allows your remote/login user to execute commands as another user via the pbrun utility. + author: ansible (@core) + version_added: "2.8" + options: + become_user: + description: User you 'become' to execute the task + ini: + - section: privilege_escalation + key: become_user + - section: pbrun_become_plugin + key: user + vars: + - name: ansible_become_user + - name: ansible_pbrun_user + env: + - name: ANSIBLE_BECOME_USER + - name: ANSIBLE_PBRUN_USER + become_exe: + description: Sudo executable + default: pbrun + ini: + - section: privilege_escalation + key: become_exe + - section: pbrun_become_plugin + key: executable + vars: + - name: ansible_become_exe + - name: ansible_pbrun_exe + env: + - name: ANSIBLE_BECOME_EXE + - name: ANSIBLE_PBRUN_EXE + become_flags: + description: Options to pass to pbrun + ini: + - section: privilege_escalation + key: become_flags + - section: pbrun_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_pbrun_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_PBRUN_FLAGS + become_pass: + description: Password for pbrun + required: False + vars: + - name: ansible_become_password + - name: ansible_become_pass + - name: ansible_pbrun_pass + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_PBRUN_PASS + ini: + - section: pbrun_become_plugin + key: password + wrap_exe: + description: Toggle to wrap the command pbrun calls in 'shell -c' or not + default: False + type: bool + ini: + - section: pbrun_become_plugin + key: wrap_execution + vars: + - name: ansible_pbrun_wrap_execution + env: + - name: ANSIBLE_PBRUN_WRAP_EXECUTION +""" + +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + + name = 'pbrun' + + prompt = 'Password:' + + def build_become_command(self, cmd, shell): + super(BecomeModule, self).build_become_command(cmd, shell) + + if not cmd: + return cmd + + become_exe = self.get_option('become_exe') or self.name + flags = self.get_option('become_flags') or '' + user = self.get_option('become_user') + if user: + user = '-u %s' % (user) + noexe = not self.get_option('wrap_exe') + + return ' '.join([become_exe, flags, user, self._build_success_command(cmd, shell, noexe=noexe)]) diff --git a/lib/ansible/plugins/become/pfexec.py b/lib/ansible/plugins/become/pfexec.py new file mode 100644 index 00000000000..1e1835f1cc5 --- /dev/null +++ b/lib/ansible/plugins/become/pfexec.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + become: pfexec + short_description: profile based execution + description: + - This become plugins allows your remote/login user to execute commands as another user via the pfexec utility. + author: ansible (@core) + version_added: "2.8" + options: + become_user: + description: + - User you 'become' to execute the task + - This plugin ignores this settingas pfexec uses it's own ``exec_attr`` to figure this out, + but it is supplied here for Ansible to make decisions needed for the task execution, like file permissions. + default: root + ini: + - section: privilege_escalation + key: become_user + - section: pfexec_become_plugin + key: user + vars: + - name: ansible_become_user + - name: ansible_pfexec_user + env: + - name: ANSIBLE_BECOME_USER + - name: ANSIBLE_PFEXEC_USER + become_exe: + description: Sudo executable + default: pfexec + ini: + - section: privilege_escalation + key: become_exe + - section: pfexec_become_plugin + key: executable + vars: + - name: ansible_become_exe + - name: ansible_pfexec_exe + env: + - name: ANSIBLE_BECOME_EXE + - name: ANSIBLE_PFEXEC_EXE + become_flags: + description: Options to pass to pfexec + default: -H -S -n + ini: + - section: privilege_escalation + key: become_flags + - section: pfexec_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_pfexec_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_PFEXEC_FLAGS + become_pass: + description: pfexec password + required: False + vars: + - name: ansible_become_password + - name: ansible_become_pass + - name: ansible_pfexec_pass + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_PFEXEC_PASS + ini: + - section: pfexec_become_plugin + key: password + wrap_exe: + description: Toggle to wrap the command pfexec calls in 'shell -c' or not + default: False + type: bool + ini: + - section: pfexec_become_plugin + key: wrap_execution + vars: + - name: ansible_pfexec_wrap_execution + env: + note: + - This plugin ignores ``become_user`` as pfexec uses it's own ``exec_attr`` to figure this out. +""" + +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + + name = 'pfexec' + + def build_become_command(self, cmd, shell): + super(BecomeModule, self).build_become_command(cmd, shell) + + if not cmd: + return cmd + + exe = self.get_option('become_exe') or self.name + flags = self.get_option('become_flags') + noexe = not self.get_option('wrap_exe') + return '%s %s "%s"' % (exe, flags, self._build_success_command(cmd, shell, noexe=noexe)) diff --git a/lib/ansible/plugins/become/pmrun.py b/lib/ansible/plugins/become/pmrun.py new file mode 100644 index 00000000000..023cf408ba3 --- /dev/null +++ b/lib/ansible/plugins/become/pmrun.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + become: pmrun + short_description: Privilege Manager run + description: + - This become plugins allows your remote/login user to execute commands as another user via the pmrun utility. + author: ansible (@core) + version_added: "2.8" + options: + become_exe: + description: Sudo executable + default: pmrun + ini: + - section: privilege_escalation + key: become_exe + - section: pmrun_become_plugin + key: executable + vars: + - name: ansible_become_exe + - name: ansible_pmrun_exe + env: + - name: ANSIBLE_BECOME_EXE + - name: ANSIBLE_PMRUN_EXE + become_flags: + description: Options to pass to pmrun + ini: + - section: privilege_escalation + key: become_flags + - section: pmrun_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_pmrun_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_PMRUN_FLAGS + become_pass: + description: pmrun password + required: False + vars: + - name: ansible_become_password + - name: ansible_become_pass + - name: ansible_pmrun_pass + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_PMRUN_PASS + ini: + - section: pmrun_become_plugin + key: password + notes: + - This plugin ignores the become_user supplied and uses pmrun's own configuration to select the user. +""" + +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + + name = 'pmrun' + prompt = 'Enter UPM user password:' + + def build_become_command(self, cmd, shell): + super(BecomeModule, self).build_become_command(cmd, shell) + + if not cmd: + return cmd + + become = self.get_option('become_exe') or self.name + flags = self.get_option('become_flags') or '' + return '%s %s %s' % (become, flags, self._build_success_command(cmd, shell)) diff --git a/lib/ansible/plugins/become/runas.py b/lib/ansible/plugins/become/runas.py new file mode 100644 index 00000000000..ab191577acb --- /dev/null +++ b/lib/ansible/plugins/become/runas.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + become: runas + short_description: Run As user + description: + - This become plugins allows your remote/login user to execute commands as another user via the windows runas facility. + author: ansible (@core) + version_added: "2.8" + options: + become_user: + description: User you 'become' to execute the task + ini: + - section: privilege_escalation + key: become_user + - section: runas_become_plugin + key: user + vars: + - name: ansible_become_user + - name: ansible_runas_user + env: + - name: ANSIBLE_BECOME_USER + - name: ANSIBLE_RUNAS_USER + required: True + become_flags: + description: Options to pass to runas, a space delimited list of k=v pairs + default: '' + ini: + - section: privilege_escalation + key: become_flags + - section: runas_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_runas_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_RUNAS_FLAGS + become_pass: + description: password + ini: + - section: runas_become_plugin + key: password + vars: + - name: ansible_become_password + - name: ansible_become_pass + - name: ansible_runas_runas + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_RUNAS_PASS + notes: + - runas is really implemented in the powershell module handler and as such can only be used with winrm connections. + - This plugin ignores the 'become_exe' setting as it uses an API and not an executable. +""" + +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + + name = 'runas' + + def build_become_command(self, cmd, shell): + # runas is implemented inside the winrm connection plugin + return cmd diff --git a/lib/ansible/plugins/become/sesu.py b/lib/ansible/plugins/become/sesu.py new file mode 100644 index 00000000000..372594f6dad --- /dev/null +++ b/lib/ansible/plugins/become/sesu.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + become: sesu + short_description: CA Privilged Access Manager + description: + - This become plugins allows your remote/login user to execute commands as another user via the sesu utility. + author: ansible (@nekonyuu) + version_added: "2.8" + options: + become_user: + description: User you 'become' to execute the task + ini: + - section: privilege_escalation + key: become_user + - section: sesu_become_plugin + key: user + vars: + - name: ansible_become_user + - name: ansible_sesu_user + env: + - name: ANSIBLE_BECOME_USER + - name: ANSIBLE_SESU_USER + become_exe: + description: sesu executable + default: sesu + ini: + - section: privilege_escalation + key: become_exe + - section: sesu_become_plugin + key: executable + vars: + - name: ansible_become_exe + - name: ansible_sesu_exe + env: + - name: ANSIBLE_BECOME_EXE + - name: ANSIBLE_SESU_EXE + become_flags: + description: Options to pass to sesu + default: -H -S -n + ini: + - section: privilege_escalation + key: become_flags + - section: sesu_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_sesu_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_SESU_FLAGS + become_pass: + description: Password to pass to sesu + required: False + vars: + - name: ansible_become_password + - name: ansible_become_pass + - name: ansible_sesu_pass + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_SESU_PASS + ini: + - section: sesu_become_plugin + key: password +""" + +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + + name = 'sesu' + + _prompt = 'Please enter your password:' + fail = missing = ('Sorry, try again with sesu.',) + + def build_become_command(self, cmd, shell): + super(BecomeModule, self).build_become_command(cmd, shell) + + if not cmd: + return cmd + + become = self.get_option('become_exe') or self.name + flags = self.get_option('become_flags') or '' + user = self.get_option('become_user') or '' + return '%s %s %s -c %s' % (become, flags, user, self._build_success_command(cmd, shell)) diff --git a/lib/ansible/plugins/become/su.py b/lib/ansible/plugins/become/su.py new file mode 100644 index 00000000000..d5df235761f --- /dev/null +++ b/lib/ansible/plugins/become/su.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + become: su + short_description: Substitute User + description: + - This become plugins allows your remote/login user to execute commands as another user via the su utility. + author: ansible (@core) + version_added: "2.8" + options: + become_user: + description: User you 'become' to execute the task + default: root + ini: + - section: privilege_escalation + key: become_user + - section: su_become_plugin + key: user + vars: + - name: ansible_become_user + - name: ansible_su_user + env: + - name: ANSIBLE_BECOME_USER + - name: ANSIBLE_SU_USER + become_exe: + description: Su executable + default: su + ini: + - section: privilege_escalation + key: become_exe + - section: su_become_plugin + key: executable + vars: + - name: ansible_become_exe + - name: ansible_su_exe + env: + - name: ANSIBLE_BECOME_EXE + - name: ANSIBLE_SU_EXE + become_flags: + description: Options to pass to su + default: '' + ini: + - section: privilege_escalation + key: become_flags + - section: su_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_su_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_SU_FLAGS + become_pass: + description: Password to pass to su + required: False + vars: + - name: ansible_become_password + - name: ansible_become_pass + - name: ansible_su_pass + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_SU_PASS + ini: + - section: su_become_plugin + key: password + prompt_l10n: + description: + - List of localized strings to match for prompt detection + - If empty we'll use the built in one + default: [] + ini: + - section: su_become_plugin + key: localized_prompts + vars: + - name: ansible_su_prompt_l10n + env: + - name: ANSIBLE_SU_PROMPT_L10N +""" + +import re + +from ansible.module_utils._text import to_bytes +from ansible.module_utils.six.moves import shlex_quote +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + + name = 'su' + + # messages for detecting prompted password issues + fail = ('Authentication failure',) + + SU_PROMPT_LOCALIZATIONS = [ + 'Password', + '암호', + 'パスワード', + 'Adgangskode', + 'Contraseña', + 'Contrasenya', + 'Hasło', + 'Heslo', + 'Jelszó', + 'Lösenord', + 'Mật khẩu', + 'Mot de passe', + 'Parola', + 'Parool', + 'Pasahitza', + 'Passord', + 'Passwort', + 'Salasana', + 'Sandi', + 'Senha', + 'Wachtwoord', + 'ססמה', + 'Лозинка', + 'Парола', + 'Пароль', + 'गुप्तशब्द', + 'शब्दकूट', + 'సంకేతపదము', + 'හස්පදය', + '密码', + '密碼', + '口令', + ] + + def check_password_prompt(self, b_output): + ''' checks if the expected passwod prompt exists in b_output ''' + + prompts = self.get_option('prompt_l10n') or self.SU_PROMPT_LOCALIZATIONS + b_password_string = b"|".join((br'(\w+\'s )?' + to_bytes(p)) for p in prompts) + # Colon or unicode fullwidth colon + b_password_string = b_password_string + to_bytes(u' ?(:|:) ?') + b_su_prompt_localizations_re = re.compile(b_password_string, flags=re.IGNORECASE) + return bool(b_su_prompt_localizations_re.match(b_output)) + + def build_become_command(self, cmd, shell): + super(BecomeModule, self).build_become_command(cmd, shell) + + # Prompt handling for ``su`` is more complicated, this + # is used to satisfy the connection plugin + self.prompt = True + + if not cmd: + return cmd + + exe = self.get_option('become_exe') or self.name + flags = self.get_option('become_flags') or '' + user = self.get_option('become_user') or '' + success_cmd = self._build_success_command(cmd, shell) + + return "%s %s %s -c %s" % (exe, flags, user, shlex_quote(success_cmd)) diff --git a/lib/ansible/plugins/become/sudo.py b/lib/ansible/plugins/become/sudo.py new file mode 100644 index 00000000000..a7593cce95f --- /dev/null +++ b/lib/ansible/plugins/become/sudo.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + become: sudo + short_description: Substitute User DO + description: + - This become plugins allows your remote/login user to execute commands as another user via the sudo utility. + author: ansible (@core) + version_added: "2.8" + options: + become_user: + description: User you 'become' to execute the task + default: root + ini: + - section: privilege_escalation + key: become_user + - section: sudo_become_plugin + key: user + vars: + - name: ansible_become_user + - name: ansible_sudo_user + env: + - name: ANSIBLE_BECOME_USER + - name: ANSIBLE_SUDO_USER + become_exe: + description: Sudo executable + default: sudo + ini: + - section: privilege_escalation + key: become_exe + - section: sudo_become_plugin + key: executable + vars: + - name: ansible_become_exe + - name: ansible_sudo_exe + env: + - name: ANSIBLE_BECOME_EXE + - name: ANSIBLE_SUDO_EXE + become_flags: + description: Options to pass to sudo + default: -H -S -n + ini: + - section: privilege_escalation + key: become_flags + - section: sudo_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_sudo_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_SUDO_FLAGS + become_pass: + description: Password to pass to sudo + required: False + vars: + - name: ansible_become_password + - name: ansible_become_pass + - name: ansible_sudo_pass + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_SUDO_PASS + ini: + - section: sudo_become_plugin + key: password +""" + + +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + + name = 'sudo' + + # messages for detecting prompted password issues + fail = ('Sorry, try again.',) + missing = ('Sorry, a password is required to run sudo', 'sudo: a password is required') + + def build_become_command(self, cmd, shell): + super(BecomeModule, self).build_become_command(cmd, shell) + + if not cmd: + return cmd + + becomecmd = self.get_option('become_exe') or self.name + + flags = self.get_option('become_flags') or '' + prompt = '' + if self.get_option('become_pass'): + self.prompt = '[sudo via ansible, key=%s] password:' % self._id + if flags: # this could be simplified, but kept as is for now for backwards string matching + flags = flags.replace('-n', '') + prompt = '-p "%s"' % (self.prompt) + + user = self.get_option('become_user') or '' + if user: + user = '-u %s' % (user) + + return ' '.join([becomecmd, flags, prompt, user, self._build_success_command(cmd, shell)]) diff --git a/lib/ansible/plugins/connection/__init__.py b/lib/ansible/plugins/connection/__init__.py index c36d51ec318..82d3854f117 100644 --- a/lib/ansible/plugins/connection/__init__.py +++ b/lib/ansible/plugins/connection/__init__.py @@ -6,19 +6,16 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import fcntl -import gettext import os import shlex from abc import abstractmethod, abstractproperty from functools import wraps from ansible import constants as C -from ansible.errors import AnsibleError -from ansible.module_utils.six import string_types from ansible.module_utils._text import to_bytes, to_text from ansible.plugins import AnsiblePlugin -from ansible.plugins.loader import shell_loader, connection_loader from ansible.utils.display import Display +from ansible.plugins.loader import connection_loader, get_shell_plugin from ansible.utils.path import unfrackpath display = Display() @@ -46,7 +43,7 @@ class ConnectionBase(AnsiblePlugin): has_pipelining = False has_native_async = False # eg, winrm always_pipeline_modules = False # eg, winrm - become_methods = C.BECOME_METHODS + has_tty = True # for interacting with become plugins # 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. @@ -66,11 +63,12 @@ class ConnectionBase(AnsiblePlugin): # All these hasattrs allow subclasses to override these parameters if not hasattr(self, '_play_context'): + # Backwards compat: self._play_context isn't really needed, using set_options/get_option 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'): + # Backwards compat: self._display isn't really needed, just import the global display and use that. self._display = display if not hasattr(self, '_connected'): self._connected = False @@ -80,30 +78,17 @@ class ConnectionBase(AnsiblePlugin): self._connected = False self._socket_path = None - if shell is not None: - self._shell = shell + # helper plugins + self._shell = shell - # 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) - try: - shell = shell_loader.get(shell_filename) - except Exception: - shell = None - if shell is None: - for shell in shell_loader.all(): - if shell_filename in shell.COMPATIBLE_SHELLS: - break - shell_type = shell.SHELL_FAMILY - - self._shell = shell_loader.get(shell_type) + # we always must have shell if not self._shell: - raise AnsibleError("Invalid shell type specified (%s), or the plugin for that shell type is missing." % shell_type) + self._shell = get_shell_plugin(shell_type=getattr(self, '_shell_type', None), executable=self._play_context.executable) + + self.become = None + + def set_become_plugin(self, plugin): + self.become = plugin @property def connected(self): @@ -115,14 +100,6 @@ class ConnectionBase(AnsiblePlugin): '''Read-only property holding the connection socket path for this remote host''' return self._socket_path - 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) - @staticmethod def _split_ssh_args(argstring): """ @@ -151,10 +128,6 @@ class ConnectionBase(AnsiblePlugin): 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): @@ -240,31 +213,6 @@ class ConnectionBase(AnsiblePlugin): """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).strip() - b_lines = b_output.splitlines() - return any(l.strip().startswith(b_prompt) for l in b_lines) - 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) @@ -279,6 +227,39 @@ class ConnectionBase(AnsiblePlugin): def reset(self): display.warning("Reset is not implemented for this connection") + # NOTE: these password functions are all become specific, the name is + # confusing as it does not handle 'protocol passwords' + # DEPRECATED: + # These are kept for backwards compatiblity + # Use the methods provided by the become plugins instead + def check_become_success(self, b_output): + display.deprecated( + "Connection.check_become_success is deprecated, calling code should be using become plugins instead", + version="2.12" + ) + return self.become.check_success(b_output) + + def check_password_prompt(self, b_output): + display.deprecated( + "Connection.check_password_prompt is deprecated, calling code should be using become plugins instead", + version="2.12" + ) + return self.become.check_password_prompt(b_output) + + def check_incorrect_password(self, b_output): + display.deprecated( + "Connection.check_incorrect_password is deprecated, calling code should be using become plugins instead", + version="2.12" + ) + return self.become.check_incorrect_password(b_output) + + def check_missing_password(self, b_output): + display.deprecated( + "Connection.check_missing_password is deprecated, calling code should be using become plugins instead", + version="2.12" + ) + return self.become.check_missing_password(b_output) + class NetworkConnectionBase(ConnectionBase): """ diff --git a/lib/ansible/plugins/connection/buildah.py b/lib/ansible/plugins/connection/buildah.py index c4e2c73f0f3..470173be18f 100644 --- a/lib/ansible/plugins/connection/buildah.py +++ b/lib/ansible/plugins/connection/buildah.py @@ -63,7 +63,6 @@ class Connection(ConnectionBase): # String used to identify this Connection class from other classes transport = 'buildah' has_pipelining = True - become_methods = frozenset(C.BECOME_METHODS) def __init__(self, play_context, new_stdin, *args, **kwargs): super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) diff --git a/lib/ansible/plugins/connection/chroot.py b/lib/ansible/plugins/connection/chroot.py index 86168a45d9d..b4800565425 100644 --- a/lib/ansible/plugins/connection/chroot.py +++ b/lib/ansible/plugins/connection/chroot.py @@ -59,7 +59,7 @@ class Connection(ConnectionBase): # su currently has an undiagnosed issue with calculating the file # checksums (so copy, for instance, doesn't work right) # Have to look into that before re-enabling this - become_methods = frozenset(C.BECOME_METHODS).difference(('su',)) + has_tty = False default_user = 'root' diff --git a/lib/ansible/plugins/connection/docker.py b/lib/ansible/plugins/connection/docker.py index 68869e0ae12..231402e2d28 100644 --- a/lib/ansible/plugins/connection/docker.py +++ b/lib/ansible/plugins/connection/docker.py @@ -62,7 +62,6 @@ class Connection(ConnectionBase): transport = 'docker' has_pipelining = True - become_methods = frozenset(C.BECOME_METHODS) def __init__(self, play_context, new_stdin, *args, **kwargs): super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) diff --git a/lib/ansible/plugins/connection/jail.py b/lib/ansible/plugins/connection/jail.py index 6747d538295..161817ba2d4 100644 --- a/lib/ansible/plugins/connection/jail.py +++ b/lib/ansible/plugins/connection/jail.py @@ -56,8 +56,7 @@ class Connection(ConnectionBase): # Pipelining may work. Someone needs to test by setting this to True and # having pipelining=True in their ansible.cfg has_pipelining = True - - become_methods = frozenset(C.BECOME_METHODS) + has_tty = False def __init__(self, play_context, new_stdin, *args, **kwargs): super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) diff --git a/lib/ansible/plugins/connection/kubectl.py b/lib/ansible/plugins/connection/kubectl.py index 82a5002df87..55927c898c4 100644 --- a/lib/ansible/plugins/connection/kubectl.py +++ b/lib/ansible/plugins/connection/kubectl.py @@ -198,7 +198,6 @@ class Connection(ConnectionBase): connection_options = CONNECTION_OPTIONS documentation = DOCUMENTATION has_pipelining = True - become_methods = frozenset(C.BECOME_METHODS) transport_cmd = None def __init__(self, play_context, new_stdin, *args, **kwargs): diff --git a/lib/ansible/plugins/connection/libvirt_lxc.py b/lib/ansible/plugins/connection/libvirt_lxc.py index 4ef116cbcde..99525bcbfb0 100644 --- a/lib/ansible/plugins/connection/libvirt_lxc.py +++ b/lib/ansible/plugins/connection/libvirt_lxc.py @@ -49,8 +49,8 @@ class Connection(ConnectionBase): # su currently has an undiagnosed issue with calculating the file # checksums (so copy, for instance, doesn't work right) # Have to look into that before re-enabling this - become_methods = frozenset(C.BECOME_METHODS).difference(('su',)) default_user = 'root' + has_tty = False def __init__(self, play_context, new_stdin, *args, **kwargs): super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) diff --git a/lib/ansible/plugins/connection/local.py b/lib/ansible/plugins/connection/local.py index 0ed9d820cb8..bc0ce19e291 100644 --- a/lib/ansible/plugins/connection/local.py +++ b/lib/ansible/plugins/connection/local.py @@ -83,7 +83,7 @@ class Connection(ConnectionBase): ) display.debug("done running command with Popen()") - if self._play_context.prompt and sudoable: + if self.become and self.become.expect_prompt() and sudoable: fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK) fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK) selector = selectors.DefaultSelector() diff --git a/lib/ansible/plugins/connection/lxc.py b/lib/ansible/plugins/connection/lxc.py index 4a0b40300be..42276a8eabc 100644 --- a/lib/ansible/plugins/connection/lxc.py +++ b/lib/ansible/plugins/connection/lxc.py @@ -54,7 +54,6 @@ class Connection(ConnectionBase): transport = 'lxc' has_pipelining = True - become_methods = frozenset(C.BECOME_METHODS) default_user = 'root' def __init__(self, play_context, new_stdin, *args, **kwargs): diff --git a/lib/ansible/plugins/connection/napalm.py b/lib/ansible/plugins/connection/napalm.py index 50c500527d0..22ebd4df2ef 100644 --- a/lib/ansible/plugins/connection/napalm.py +++ b/lib/ansible/plugins/connection/napalm.py @@ -156,10 +156,8 @@ class Connection(NetworkConnectionBase): def _connect(self): if not HAS_NAPALM: - raise AnsibleError( - 'Napalm is required to use the napalm connection type.\n' - 'Please run pip install napalm' - ) + raise AnsibleError('The "napalm" python library is required to use the napalm connection type.\n') + super(Connection, self)._connect() if not self.connected: diff --git a/lib/ansible/plugins/connection/netconf.py b/lib/ansible/plugins/connection/netconf.py index 65c2c1ff0e8..0093ec21606 100644 --- a/lib/ansible/plugins/connection/netconf.py +++ b/lib/ansible/plugins/connection/netconf.py @@ -274,7 +274,7 @@ class Connection(NetworkConnectionBase): def _connect(self): if not HAS_NCCLIENT: raise AnsibleError( - 'ncclient is required to use the netconf connection type: %s.\n' + 'The required "ncclient" python library is required to use the netconf connection type: %s.\n' 'Please run pip install ncclient' % to_native(NCCLIENT_IMP_ERR) ) diff --git a/lib/ansible/plugins/connection/paramiko_ssh.py b/lib/ansible/plugins/connection/paramiko_ssh.py index aaab9a07de9..0a328f5c2a8 100644 --- a/lib/ansible/plugins/connection/paramiko_ssh.py +++ b/lib/ansible/plugins/connection/paramiko_ssh.py @@ -415,7 +415,7 @@ class Connection(ConnectionBase): try: chan.exec_command(cmd) - if self._play_context.prompt: + if self.become and self.become.expect_prompt(): passprompt = False become_sucess = False while not (become_sucess or passprompt): diff --git a/lib/ansible/plugins/connection/psrp.py b/lib/ansible/plugins/connection/psrp.py index eac990c3004..14f23740bd9 100644 --- a/lib/ansible/plugins/connection/psrp.py +++ b/lib/ansible/plugins/connection/psrp.py @@ -218,7 +218,6 @@ class Connection(ConnectionBase): transport = 'psrp' module_implementation_preferences = ('.ps1', '.exe', '') - become_methods = ['runas'] allow_executable = False has_pipelining = True allow_extras = True diff --git a/lib/ansible/plugins/connection/qubes.py b/lib/ansible/plugins/connection/qubes.py index 240cb1d651c..58a94677650 100644 --- a/lib/ansible/plugins/connection/qubes.py +++ b/lib/ansible/plugins/connection/qubes.py @@ -61,7 +61,6 @@ class Connection(ConnectionBase): # String used to identify this Connection class from other classes transport = 'qubes' has_pipelining = True - become_methods = frozenset(C.BECOME_METHODS) def __init__(self, play_context, new_stdin, *args, **kwargs): super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) diff --git a/lib/ansible/plugins/connection/ssh.py b/lib/ansible/plugins/connection/ssh.py index 3d091f41818..713bbd93c62 100644 --- a/lib/ansible/plugins/connection/ssh.py +++ b/lib/ansible/plugins/connection/ssh.py @@ -443,7 +443,6 @@ class Connection(ConnectionBase): transport = 'ssh' has_pipelining = True - become_methods = frozenset(C.BECOME_METHODS).difference(['runas']) def __init__(self, *args, **kwargs): super(Connection, self).__init__(*args, **kwargs) @@ -708,11 +707,11 @@ class Connection(ConnectionBase): suppress_output = False # display.debug("Examining line (source=%s, state=%s): '%s'" % (source, state, display_line)) - if self._play_context.prompt and self.check_password_prompt(b_line): + if self.become.expect_prompt() and self.check_password_prompt(b_line): display.debug("become_prompt: (source=%s, state=%s): '%s'" % (source, state, display_line)) self._flags['become_prompt'] = True suppress_output = True - elif self._play_context.success_key and self.check_become_success(b_line): + elif self.become.success and self.check_become_success(b_line): display.debug("become_success: (source=%s, state=%s): '%s'" % (source, state, display_line)) self._flags['become_success'] = True suppress_output = True @@ -811,11 +810,12 @@ class Connection(ConnectionBase): state = states.index('ready_to_send') if to_bytes(self.get_option('ssh_executable')) in cmd and sudoable: - if self._play_context.prompt: + prompt = getattr(self.become, 'prompt', None) + if prompt: # We're requesting escalation with a password, so we have to # wait for a password prompt. state = states.index('awaiting_prompt') - display.debug(u'Initial state: %s: %s' % (states[state], self._play_context.prompt)) + display.debug(u'Initial state: %s: %s' % (states[state], prompt)) elif self._play_context.become and self._play_context.success_key: # We're requesting escalation without a password, so we have to # detect success/failure before sending any initial data. diff --git a/lib/ansible/plugins/connection/winrm.py b/lib/ansible/plugins/connection/winrm.py index e80d8f163c3..69a767f78ec 100644 --- a/lib/ansible/plugins/connection/winrm.py +++ b/lib/ansible/plugins/connection/winrm.py @@ -176,7 +176,6 @@ class Connection(ConnectionBase): transport = 'winrm' module_implementation_preferences = ('.ps1', '.exe', '') - become_methods = ['runas'] allow_executable = False has_pipelining = True allow_extras = True @@ -207,10 +206,6 @@ class Connection(ConnectionBase): self._winrm_user = self.get_option('remote_user') self._winrm_pass = self._play_context.password - self._become_method = self._play_context.become_method - self._become_user = self._play_context.become_user - self._become_pass = self._play_context.become_pass - self._winrm_port = self.get_option('port') self._winrm_scheme = self.get_option('scheme') diff --git a/lib/ansible/plugins/connection/zone.py b/lib/ansible/plugins/connection/zone.py index b8c8cf768c9..119d6e3afb1 100644 --- a/lib/ansible/plugins/connection/zone.py +++ b/lib/ansible/plugins/connection/zone.py @@ -47,7 +47,7 @@ class Connection(ConnectionBase): transport = 'zone' has_pipelining = True - become_methods = frozenset(C.BECOME_METHODS).difference(('su',)) + has_tty = False def __init__(self, play_context, new_stdin, *args, **kwargs): super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) diff --git a/lib/ansible/plugins/loader.py b/lib/ansible/plugins/loader.py index 6d1ae7d3e83..23328a13f3f 100644 --- a/lib/ansible/plugins/loader.py +++ b/lib/ansible/plugins/loader.py @@ -18,13 +18,15 @@ from collections import defaultdict from ansible import constants as C from ansible.errors import AnsibleError -from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils._text import to_bytes, to_text, to_native +from ansible.module_utils.six import string_types from ansible.parsing.utils.yaml import from_yaml from ansible.parsing.yaml.loader import AnsibleLoader from ansible.plugins import get_plugin_class, MODULE_CACHE, PATH_CACHE, PLUGIN_PATH_CACHE from ansible.utils.display import Display from ansible.utils.plugin_docs import add_fragments + display = Display() @@ -32,6 +34,52 @@ def get_all_plugin_loaders(): return [(name, obj) for (name, obj) in globals().items() if isinstance(obj, PluginLoader)] +def add_all_plugin_dirs(path): + ''' add any existing plugin dirs in the path provided ''' + b_path = to_bytes(path, errors='surrogate_or_strict') + if os.path.isdir(b_path): + for name, obj in get_all_plugin_loaders(): + if obj.subdir: + plugin_path = os.path.join(b_path, to_bytes(obj.subdir)) + if os.path.isdir(plugin_path): + obj.add_directory(to_text(plugin_path)) + else: + display.warning("Ignoring invalid path provided to plugin path: %s is not a directory" % to_native(path)) + + +def get_shell_plugin(shell_type=None, executable=None): + + if not shell_type: + # default to sh + shell_type = 'sh' + + # mostly for backwards compat + if executable: + if isinstance(executable, string_types): + shell_filename = os.path.basename(executable) + try: + shell = shell_loader.get(shell_filename) + except Exception: + shell = None + + if shell is None: + for shell in shell_loader.all(): + if shell_filename in shell.COMPATIBLE_SHELLS: + shell_type = shell.SHELL_FAMILY + break + else: + raise AnsibleError("Either a shell type or a shell executable must be provided ") + + shell = shell_loader.get(shell_type) + if not shell: + raise AnsibleError("Could not find the shell plugin required (%s)." % shell_type) + + if executable: + setattr(shell, 'executable', executable) + + return shell + + class PluginLoader: ''' PluginLoader loads plugins from the configured plugin directories. @@ -394,6 +442,7 @@ class PluginLoader: return None self._display_plugin_load(self.class_name, name, self._searched_paths, path, found_in_cache=found_in_cache, class_only=class_only) + if not class_only: try: obj = obj(*args, **kwargs) @@ -513,6 +562,7 @@ class PluginLoader: continue self._display_plugin_load(self.class_name, basename, self._searched_paths, path, found_in_cache=found_in_cache, class_only=class_only) + if not class_only: try: obj = obj(*args, **kwargs) @@ -774,3 +824,10 @@ httpapi_loader = PluginLoader( 'httpapi_plugins', required_base_class='HttpApiBase', ) + +become_loader = PluginLoader( + 'BecomeModule', + 'ansible.plugins.become', + C.DEFAULT_BECOME_PLUGIN_PATH, + 'become_plugins' +) diff --git a/lib/ansible/plugins/shell/__init__.py b/lib/ansible/plugins/shell/__init__.py index 0c035b1c54d..bd1d62ab202 100644 --- a/lib/ansible/plugins/shell/__init__.py +++ b/lib/ansible/plugins/shell/__init__.py @@ -46,6 +46,7 @@ class ShellBase(AnsiblePlugin): 'LC_MESSAGES': module_locale} self.tmpdir = None + self.executable = None def _normalize_system_tmpdirs(self): # Normalize the tmp directory strings. We don't use expanduser/expandvars because those @@ -65,15 +66,20 @@ class ShellBase(AnsiblePlugin): super(ShellBase, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) - # set env - self.env.update(self.get_option('environment')) + # set env if needed, deal with environment's 'dual nature' list of dicts or dict + env = self.get_option('environment') + if isinstance(env, list): + for env_dict in env: + self.env.update(env_dict) + else: + self.env.update(env) # We can remove the try: except in the future when we make ShellBase a proper subset of # *all* shells. Right now powershell and third party shells which do not use the # shell_common documentation fragment (and so do not have system_tmpdirs) will fail try: self._normalize_system_tmpdirs() - except AnsibleError: + except KeyError: pass def env_prefix(self, **kwargs): diff --git a/lib/ansible/plugins/shell/sh.py b/lib/ansible/plugins/shell/sh.py index b4c59f4d467..76a386f77a9 100644 --- a/lib/ansible/plugins/shell/sh.py +++ b/lib/ansible/plugins/shell/sh.py @@ -30,6 +30,10 @@ class ShellModule(ShellBase): # Family of shells this has. Must match the filename without extension SHELL_FAMILY = 'sh' + # commonly used + ECHO = 'echo' + COMMAND_SEP = ';' + # How to end lines in a python script one-liner _SHELL_EMBEDDED_PY_EOL = '\n' _SHELL_REDIRECT_ALLNULL = '> /dev/null 2>&1' diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py index 40dfc8f0912..823aa8715b4 100644 --- a/lib/ansible/plugins/strategy/__init__.py +++ b/lib/ansible/plugins/strategy/__init__.py @@ -880,7 +880,7 @@ class StrategyBase: host_results = [] for host in notified_hosts: - if not iterator.is_failed(host) or play_context.force_handlers: + if not iterator.is_failed(host) or iterator._play.force_handlers: task_vars = self._variable_manager.get_vars(play=iterator._play, host=host, task=handler) self.add_tqm_variables(task_vars, play=iterator._play) self._queue_task(host, handler, task_vars, play_context) @@ -1061,7 +1061,7 @@ class StrategyBase: del self._active_connections[target_host] else: connection = connection_loader.get(play_context.connection, play_context, os.devnull) - play_context.set_options_from_plugin(connection) + play_context.set_attributes_from_plugin(connection) if connection: try: diff --git a/lib/ansible/plugins/strategy/free.py b/lib/ansible/plugins/strategy/free.py index eb2e334e4f7..b712d08d540 100644 --- a/lib/ansible/plugins/strategy/free.py +++ b/lib/ansible/plugins/strategy/free.py @@ -235,7 +235,7 @@ class StrategyModule(StrategyBase): for new_block in new_blocks: task_vars = self._variable_manager.get_vars(play=iterator._play, task=new_block._parent) - final_block = new_block.filter_tagged_tasks(play_context, task_vars) + final_block = new_block.filter_tagged_tasks(task_vars) for host in hosts_left: if host in included_file._hosts: all_blocks[host].append(final_block) diff --git a/lib/ansible/plugins/strategy/linear.py b/lib/ansible/plugins/strategy/linear.py index 88e5f2174e7..93d767e3658 100644 --- a/lib/ansible/plugins/strategy/linear.py +++ b/lib/ansible/plugins/strategy/linear.py @@ -365,7 +365,7 @@ class StrategyModule(StrategyBase): task=new_block._parent ) display.debug("filtering new block on tags") - final_block = new_block.filter_tagged_tasks(play_context, task_vars) + final_block = new_block.filter_tagged_tasks(task_vars) display.debug("done filtering new block on tags") noop_block = self._prepare_and_create_noop_block_from(final_block, task._parent, iterator) diff --git a/test/integration/targets/error_from_connection/connection_plugins/dummy.py b/test/integration/targets/error_from_connection/connection_plugins/dummy.py index de84bb43419..2a2c879581a 100644 --- a/test/integration/targets/error_from_connection/connection_plugins/dummy.py +++ b/test/integration/targets/error_from_connection/connection_plugins/dummy.py @@ -20,7 +20,6 @@ class Connection(ConnectionBase): transport = 'dummy' has_pipelining = True - become_methods = frozenset(C.BECOME_METHODS) def __init__(self, play_context, new_stdin, *args, **kwargs): super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) diff --git a/test/units/playbook/test_play_context.py b/test/units/playbook/test_play_context.py index 451b0c6a3b1..e08cfc5f27c 100644 --- a/test/units/playbook/test_play_context.py +++ b/test/units/playbook/test_play_context.py @@ -7,20 +7,17 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import os - +import re import pytest from ansible import constants as C from ansible import context from ansible.cli.arguments import optparse_helpers as opt_help -from ansible.errors import AnsibleError, AnsibleParserError -from ansible.module_utils.six.moves import shlex_quote +from ansible.errors import AnsibleError from ansible.playbook.play_context import PlayContext +from ansible.playbook.play import Play +from ansible.plugins.loader import become_loader from ansible.utils import context_objects as co -from units.compat import unittest - -from units.mock.loader import DictDataLoader @pytest.fixture @@ -51,8 +48,13 @@ def test_play_context(mocker, parser, reset_cli_args): (options, args) = parser.parse_args(['-vv', '--check']) options.args = args context._init_global_context(options) - play_context = PlayContext() + play = Play.load({}) + play_context = PlayContext(play=play) + # Note: **Must** test the value from _attributes here because play_context.connection will end + # up calling PlayContext._get_attr_connection() which changes the 'smart' connection type into + # the best guessed type (and since C.DEFAULT_TRANSPORT starts off as smart, we would then never + # match) assert play_context._attributes['connection'] == C.DEFAULT_TRANSPORT assert play_context.remote_addr is None assert play_context.remote_user is None @@ -65,28 +67,11 @@ def test_play_context(mocker, parser, reset_cli_args): assert play_context.check_mode is True assert play_context.no_log is None - mock_play = mocker.MagicMock() - mock_play.connection = 'mock' - mock_play.remote_user = 'mock' - mock_play.port = 1234 - mock_play.become = True - mock_play.become_method = 'mock' - mock_play.become_user = 'mockroot' - mock_play.no_log = True - - play_context = PlayContext(play=mock_play) - assert play_context.connection == 'mock' - assert play_context.remote_user == 'mock' - assert play_context.password == '' - assert play_context.port == 1234 - assert play_context.become is True - assert play_context.become_method == "mock" - assert play_context.become_user == "mockroot" - mock_task = mocker.MagicMock() mock_task.connection = 'mocktask' mock_task.remote_user = 'mocktask' - mock_task.no_log = mock_play.no_log + mock_task.port = 1234 + mock_task.no_log = True mock_task.become = True mock_task.become_method = 'mocktask' mock_task.become_user = 'mocktaskroot' @@ -101,7 +86,7 @@ def test_play_context(mocker, parser, reset_cli_args): mock_templar = mocker.MagicMock() - play_context = PlayContext(play=mock_play) + play_context = PlayContext() play_context = play_context.set_task_and_variable_override(task=mock_task, variables=all_vars, templar=mock_templar) assert play_context.connection == 'mock_inventory' @@ -126,16 +111,16 @@ def test_play_context_make_become_cmd(mocker, parser, reset_cli_args): default_cmd = "/bin/foo" default_exe = "/bin/bash" - sudo_exe = C.DEFAULT_SUDO_EXE or 'sudo' - sudo_flags = C.DEFAULT_SUDO_FLAGS - su_exe = C.DEFAULT_SU_EXE or 'su' - su_flags = C.DEFAULT_SU_FLAGS or '' + sudo_exe = 'sudo' + sudo_flags = '-H -s -n' + su_exe = 'su' + su_flags = '' pbrun_exe = 'pbrun' pbrun_flags = '' pfexec_exe = 'pfexec' pfexec_flags = '' doas_exe = 'doas' - doas_flags = ' -n -u foo ' + doas_flags = '-n' ksu_exe = 'ksu' ksu_flags = '' dzdo_exe = 'dzdo' @@ -144,54 +129,64 @@ def test_play_context_make_become_cmd(mocker, parser, reset_cli_args): cmd = play_context.make_become_cmd(cmd=default_cmd, executable=default_exe) assert cmd == default_cmd + success = 'BECOME-SUCCESS-.+?' + play_context.become = True play_context.become_user = 'foo' - - play_context.become_method = 'sudo' + play_context.set_become_plugin(become_loader.get('sudo')) + play_context.become_flags = sudo_flags cmd = play_context.make_become_cmd(cmd=default_cmd, executable="/bin/bash") - assert (cmd == """%s %s -u %s %s -c 'echo %s; %s'""" % (sudo_exe, sudo_flags, play_context.become_user, - default_exe, play_context.success_key, default_cmd)) + + assert (re.match("""%s %s -u %s %s -c 'echo %s; %s'""" % (sudo_exe, sudo_flags, play_context.become_user, + default_exe, success, default_cmd), cmd) is not None) play_context.become_pass = 'testpass' cmd = play_context.make_become_cmd(cmd=default_cmd, executable=default_exe) - assert (cmd == """%s %s -p "%s" -u %s %s -c 'echo %s; %s'""" % (sudo_exe, sudo_flags.replace('-n', ''), - play_context.prompt, play_context.become_user, default_exe, - play_context.success_key, default_cmd)) + assert (re.match("""%s %s -p "%s" -u %s %s -c 'echo %s; %s'""" % (sudo_exe, sudo_flags.replace('-n', ''), + r"\[sudo via ansible, key=.+?\] password:", play_context.become_user, + default_exe, success, default_cmd), cmd) is not None) play_context.become_pass = None - play_context.become_method = 'su' + play_context.set_become_plugin(become_loader.get('su')) + play_context.become_flags = su_flags cmd = play_context.make_become_cmd(cmd=default_cmd, executable="/bin/bash") - assert (cmd == """%s %s -c '%s -c '"'"'echo %s; %s'"'"''""" % (su_exe, play_context.become_user, default_exe, - play_context.success_key, default_cmd)) + assert (re.match("""%s %s -c '%s -c '"'"'echo %s; %s'"'"''""" % (su_exe, play_context.become_user, default_exe, + success, default_cmd), cmd) is not None) - play_context.become_method = 'pbrun' + play_context.set_become_plugin(become_loader.get('pbrun')) + play_context.become_flags = pbrun_flags cmd = play_context.make_become_cmd(cmd=default_cmd, executable="/bin/bash") - assert cmd == """%s %s -u %s 'echo %s; %s'""" % (pbrun_exe, pbrun_flags, play_context.become_user, play_context.success_key, default_cmd) + assert re.match("""%s %s -u %s 'echo %s; %s'""" % (pbrun_exe, pbrun_flags, play_context.become_user, + success, default_cmd), cmd) is not None - play_context.become_method = 'pfexec' + play_context.set_become_plugin(become_loader.get('pfexec')) + play_context.become_flags = pfexec_flags cmd = play_context.make_become_cmd(cmd=default_cmd, executable="/bin/bash") - assert cmd == '''%s %s "'echo %s; %s'"''' % (pfexec_exe, pfexec_flags, play_context.success_key, default_cmd) + assert re.match('''%s %s "'echo %s; %s'"''' % (pfexec_exe, pfexec_flags, success, default_cmd), cmd) is not None - play_context.become_method = 'doas' + play_context.set_become_plugin(become_loader.get('doas')) + play_context.become_flags = doas_flags cmd = play_context.make_become_cmd(cmd=default_cmd, executable="/bin/bash") - assert (cmd == """%s %s %s -c 'echo %s; %s'""" % (doas_exe, doas_flags, default_exe, play_context.success_key, default_cmd)) + assert (re.match("""%s %s -u %s %s -c 'echo %s; %s'""" % (doas_exe, doas_flags, play_context.become_user, default_exe, success, + default_cmd), cmd) is not None) - play_context.become_method = 'ksu' + play_context.set_become_plugin(become_loader.get('ksu')) + play_context.become_flags = ksu_flags cmd = play_context.make_become_cmd(cmd=default_cmd, executable="/bin/bash") - assert (cmd == """%s %s %s -e %s -c 'echo %s; %s'""" % (ksu_exe, play_context.become_user, ksu_flags, - default_exe, play_context.success_key, default_cmd)) + assert (re.match("""%s %s %s -e %s -c 'echo %s; %s'""" % (ksu_exe, play_context.become_user, ksu_flags, + default_exe, success, default_cmd), cmd) is not None) - play_context.become_method = 'bad' + play_context.set_become_plugin(become_loader.get('bad')) with pytest.raises(AnsibleError): play_context.make_become_cmd(cmd=default_cmd, executable="/bin/bash") - play_context.become_method = 'dzdo' + play_context.set_become_plugin(become_loader.get('dzdo')) + play_context.become_flags = dzdo_flags cmd = play_context.make_become_cmd(cmd=default_cmd, executable="/bin/bash") - assert cmd == """%s %s -u %s %s -c 'echo %s; %s'""" % (dzdo_exe, dzdo_flags, play_context.become_user, default_exe, play_context.success_key, default_cmd) - + assert re.match("""%s %s -u %s %s -c 'echo %s; %s'""" % (dzdo_exe, dzdo_flags, play_context.become_user, default_exe, + success, default_cmd), cmd) is not None play_context.become_pass = 'testpass' - play_context.become_method = 'dzdo' + play_context.set_become_plugin(become_loader.get('dzdo')) cmd = play_context.make_become_cmd(cmd=default_cmd, executable="/bin/bash") - assert (cmd == """%s %s -p %s -u %s %s -c 'echo %s; %s'""" % (dzdo_exe, dzdo_flags, shlex_quote(play_context.prompt), - play_context.become_user, default_exe, - play_context.success_key, default_cmd)) + assert re.match("""%s %s -p %s -u %s %s -c 'echo %s; %s'""" % (dzdo_exe, dzdo_flags, r'\"\[dzdo via ansible, key=.+?\] password:\"', + play_context.become_user, default_exe, success, default_cmd), cmd) is not None diff --git a/test/units/plugins/action/test_action.py b/test/units/plugins/action/test_action.py index f08d2644d08..89d7377e8d0 100644 --- a/test/units/plugins/action/test_action.py +++ b/test/units/plugins/action/test_action.py @@ -500,31 +500,31 @@ class TestActionBase(unittest.TestCase): fake_loader = MagicMock() fake_loader.get_basedir.return_value = os.getcwd() play_context = PlayContext() - action_base = DerivedActionBase(None, None, play_context, fake_loader, None, None) - action_base._connection = MagicMock(exec_command=MagicMock(return_value=(0, '', ''))) - action_base._connection._shell = MagicMock(append_command=MagicMock(return_value=('JOINED CMD'))) - play_context.become = True - play_context.become_user = play_context.remote_user = 'root' - play_context.make_become_cmd = MagicMock(return_value='CMD') + action_base = DerivedActionBase(None, None, play_context, fake_loader, None, None) + action_base.get_become_option = MagicMock(return_value='root') + action_base._get_remote_user = MagicMock(return_value='root') + + action_base._connection = MagicMock(exec_command=MagicMock(return_value=(0, '', ''))) + + action_base._connection._shell = shell = MagicMock(append_command=MagicMock(return_value=('JOINED CMD'))) + + action_base._connection.become = become = MagicMock() + become.build_become_command.return_value = 'foo' action_base._low_level_execute_command('ECHO', sudoable=True) - play_context.make_become_cmd.assert_not_called() + become.build_become_command.assert_not_called() - play_context.remote_user = 'apo' + action_base._get_remote_user.return_value = 'apo' action_base._low_level_execute_command('ECHO', sudoable=True, executable='/bin/csh') - play_context.make_become_cmd.assert_called_once_with("ECHO", executable='/bin/csh') + become.build_become_command.assert_called_once_with("ECHO", shell) - play_context.make_become_cmd.reset_mock() + become.build_become_command.reset_mock() - become_allow_same_user = C.BECOME_ALLOW_SAME_USER - C.BECOME_ALLOW_SAME_USER = True - try: - play_context.remote_user = 'root' + with patch.object(C, 'BECOME_ALLOW_SAME_USER', new=True): + action_base._get_remote_user.return_value = 'root' action_base._low_level_execute_command('ECHO SAME', sudoable=True) - play_context.make_become_cmd.assert_called_once_with("ECHO SAME", executable=None) - finally: - C.BECOME_ALLOW_SAME_USER = become_allow_same_user + become.build_become_command.assert_called_once_with("ECHO SAME", shell) class TestActionBaseCleanReturnedData(unittest.TestCase): diff --git a/test/units/plugins/connection/test_connection.py b/test/units/plugins/connection/test_connection.py index d54056bd8ea..1733bb57409 100644 --- a/test/units/plugins/connection/test_connection.py +++ b/test/units/plugins/connection/test_connection.py @@ -30,6 +30,7 @@ from units.compat.mock import patch from ansible.errors import AnsibleError from ansible.playbook.play_context import PlayContext from ansible.plugins.connection import ConnectionBase +from ansible.plugins.loader import become_loader # from ansible.plugins.connection.accelerate import Connection as AccelerateConnection # from ansible.plugins.connection.chroot import Connection as ChrootConnection # from ansible.plugins.connection.funcd import Connection as FuncdConnection @@ -250,6 +251,8 @@ debug1: Sending command: /bin/sh -c 'sudo -H -S -p "[sudo via ansible, key=ouzm pass c = ConnectionFoo(self.play_context, self.in_stream) + c.set_become_plugin(become_loader.get('sudo')) + c.become.prompt = '[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: ' self.assertTrue(c.check_password_prompt(local)) self.assertTrue(c.check_password_prompt(ssh_pipelining_vvvv)) diff --git a/test/units/plugins/connection/test_ssh.py b/test/units/plugins/connection/test_ssh.py index e367fa366c7..2b4e1a940e3 100644 --- a/test/units/plugins/connection/test_ssh.py +++ b/test/units/plugins/connection/test_ssh.py @@ -34,7 +34,7 @@ from ansible.module_utils.six.moves import shlex_quote from ansible.module_utils._text import to_bytes from ansible.playbook.play_context import PlayContext from ansible.plugins.connection import ssh -from ansible.plugins.loader import connection_loader +from ansible.plugins.loader import connection_loader, become_loader class TestConnectionBaseClass(unittest.TestCase): @@ -93,6 +93,7 @@ class TestConnectionBaseClass(unittest.TestCase): new_stdin = StringIO() conn = connection_loader.get('ssh', pc, new_stdin) + conn.set_become_plugin(become_loader.get('sudo')) conn.check_password_prompt = MagicMock() conn.check_become_success = MagicMock() @@ -133,6 +134,14 @@ class TestConnectionBaseClass(unittest.TestCase): ) pc.prompt = True + conn.become.prompt = True + + def get_option(option): + if option == 'become_pass': + return 'password' + return None + + conn.become.get_option = get_option output, unprocessed = conn._examine_output(u'source', u'state', b'line 1\nline 2\nfoo\nline 3\nthis should be the remainder', False) self.assertEqual(output, b'line 1\nline 2\nline 3\n') self.assertEqual(unprocessed, b'this should be the remainder') @@ -150,7 +159,9 @@ class TestConnectionBaseClass(unittest.TestCase): ) pc.prompt = False + conn.become.prompt = False pc.success_key = u'BECOME-SUCCESS-abcdefghijklmnopqrstuvxyz' + conn.become.success = u'BECOME-SUCCESS-abcdefghijklmnopqrstuvxyz' output, unprocessed = conn._examine_output(u'source', u'state', b'line 1\nline 2\nBECOME-SUCCESS-abcdefghijklmnopqrstuvxyz\nline 3\n', False) self.assertEqual(output, b'line 1\nline 2\nline 3\n') self.assertEqual(unprocessed, b'') @@ -168,6 +179,7 @@ class TestConnectionBaseClass(unittest.TestCase): ) pc.prompt = False + conn.become.prompt = False pc.success_key = None output, unprocessed = conn._examine_output(u'source', u'state', b'line 1\nline 2\nincorrect password\n', True) self.assertEqual(output, b'line 1\nline 2\nincorrect password\n') @@ -186,6 +198,7 @@ class TestConnectionBaseClass(unittest.TestCase): ) pc.prompt = False + conn.become.prompt = False pc.success_key = None output, unprocessed = conn._examine_output(u'source', u'state', b'line 1\nbad password\n', True) self.assertEqual(output, b'line 1\nbad password\n') @@ -332,6 +345,7 @@ def mock_run_env(request, mocker): new_stdin = StringIO() conn = connection_loader.get('ssh', pc, new_stdin) + conn.set_become_plugin(become_loader.get('sudo')) conn._send_initial_data = MagicMock() conn._examine_output = MagicMock() conn._terminate_process = MagicMock() @@ -425,7 +439,7 @@ class TestSSHConnectionRun(object): def test_password_with_prompt(self): # test with password prompting enabled self.pc.password = None - self.pc.prompt = b'Password:' + self.conn.become.prompt = b'Password:' self.conn._examine_output.side_effect = self._password_with_prompt_examine_output self.mock_popen_res.stdout.read.side_effect = [b"Password:", b"Success", b""] self.mock_popen_res.stderr.read.side_effect = [b""] @@ -450,8 +464,10 @@ class TestSSHConnectionRun(object): def test_password_with_become(self): # test with some become settings self.pc.prompt = b'Password:' + self.conn.become.prompt = b'Password:' self.pc.become = True self.pc.success_key = 'BECOME-SUCCESS-abcdefg' + self.conn.become._id = 'abcdefg' self.conn._examine_output.side_effect = self._password_with_prompt_examine_output self.mock_popen_res.stdout.read.side_effect = [b"Password:", b"BECOME-SUCCESS-abcdefg", b"abc"] self.mock_popen_res.stderr.read.side_effect = [b"123"]