become - stop using play context in more places (#62373)
* become - stop using play context in more places - ci_complete * Fix up review points
This commit is contained in:
parent
a385ad321b
commit
480b106d65
13 changed files with 116 additions and 84 deletions
3
changelogs/fragments/become-pass-precedence.yaml
Normal file
3
changelogs/fragments/become-pass-precedence.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
bugfixes:
|
||||
- become - Fix various plugins that still used play_context to get the become password instead of through the plugin - https://github.com/ansible/ansible/issues/62367
|
||||
- runas - Fix the ``runas`` ``become_pass`` variable fallback from ``ansible_runas_runas`` to ``ansible_runas_pass``
|
|
@ -175,6 +175,17 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
|||
final_environment = dict()
|
||||
self._compute_environment_string(final_environment)
|
||||
|
||||
become_kwargs = {}
|
||||
if self._connection.become:
|
||||
become_kwargs['become'] = True
|
||||
become_kwargs['become_method'] = self._connection.become.name
|
||||
become_kwargs['become_user'] = self._connection.become.get_option('become_user',
|
||||
playcontext=self._play_context)
|
||||
become_kwargs['become_password'] = self._connection.become.get_option('become_pass',
|
||||
playcontext=self._play_context)
|
||||
become_kwargs['become_flags'] = self._connection.become.get_option('become_flags',
|
||||
playcontext=self._play_context)
|
||||
|
||||
# modify_module will exit early if interpreter discovery is required; re-run after if necessary
|
||||
for dummy in (1, 2):
|
||||
try:
|
||||
|
@ -182,12 +193,8 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
|||
task_vars=task_vars,
|
||||
module_compression=self._play_context.module_compression,
|
||||
async_timeout=self._task.async_val,
|
||||
become=self._play_context.become,
|
||||
become_method=self._play_context.become_method,
|
||||
become_user=self._play_context.become_user,
|
||||
become_password=self._play_context.become_pass,
|
||||
become_flags=self._play_context.become_flags,
|
||||
environment=final_environment)
|
||||
environment=final_environment,
|
||||
**become_kwargs)
|
||||
break
|
||||
except InterpreterDiscoveryRequiredError as idre:
|
||||
self._discovered_interpreter = AnsibleUnsafeText(discover_interpreter(
|
||||
|
@ -260,7 +267,7 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
|||
module_style == "new", # old style modules do not support pipelining
|
||||
not C.DEFAULT_KEEP_REMOTE_FILES, # user wants remote files
|
||||
not wrap_async or self._connection.always_pipeline_modules, # async does not normally support pipelining unless it does (eg winrm)
|
||||
self._play_context.become_method != 'su', # su does not work with pipelining,
|
||||
(self._connection.become.name if self._connection.become else '') != 'su', # su does not work with pipelining,
|
||||
# FIXME: we might need to make become_method exclusion a configurable list
|
||||
]:
|
||||
if not condition:
|
||||
|
@ -298,7 +305,7 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
|||
'''
|
||||
# if we don't use become then we know we aren't switching to a
|
||||
# different unprivileged user
|
||||
if not self._play_context.become:
|
||||
if not self._connection.become:
|
||||
return False
|
||||
|
||||
# if we use become and the user is not an admin (or same user) then
|
||||
|
@ -634,7 +641,7 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
|||
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 become_user:
|
||||
elif sudoable and self._connection.become and become_user:
|
||||
expand_path = '~%s' % become_user
|
||||
else:
|
||||
# use remote user instead, if none set default to current user
|
||||
|
|
|
@ -72,7 +72,7 @@ class ActionModule(ActionBase):
|
|||
source = self._remote_expand_user(source)
|
||||
|
||||
remote_checksum = None
|
||||
if not self._play_context.become:
|
||||
if not self._connection.become:
|
||||
# calculate checksum for the remote file, don't bother if using become as slurp will be used
|
||||
# Force remote_checksum to follow symlinks because fetch always follows symlinks
|
||||
remote_checksum = self._remote_checksum(source, all_vars=task_vars, follow=True)
|
||||
|
|
|
@ -8,6 +8,7 @@ from ansible.module_utils._text import to_text
|
|||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible.parsing.yaml.objects import AnsibleUnicode
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.plugins.loader import become_loader
|
||||
from ansible.utils.display import Display
|
||||
|
||||
display = Display()
|
||||
|
@ -103,27 +104,19 @@ class ActionModule(ActionBase):
|
|||
|
||||
def _execute_module_with_become(self, module_name, module_args, task_vars,
|
||||
wrap_async, use_task):
|
||||
orig_become = self._play_context.become
|
||||
orig_become_method = self._play_context.become_method
|
||||
orig_become_user = self._play_context.become_user
|
||||
|
||||
if not use_task:
|
||||
if orig_become is None or orig_become is False:
|
||||
self._play_context.become = True
|
||||
if orig_become_method != 'runas':
|
||||
self._play_context.become_method = 'runas'
|
||||
if orig_become_user is None or orig_become_user == 'root':
|
||||
self._play_context.become_user = 'SYSTEM'
|
||||
|
||||
orig_become = self._connection.become
|
||||
try:
|
||||
if not use_task and orig_become is None:
|
||||
become = become_loader.get('runas')
|
||||
become.set_options(direct={'become_user': 'SYSTEM', 'become_pass': None})
|
||||
self._connection.set_become_plugin(become)
|
||||
|
||||
module_res = self._execute_module(module_name=module_name,
|
||||
module_args=module_args,
|
||||
task_vars=task_vars,
|
||||
wrap_async=wrap_async)
|
||||
finally:
|
||||
self._play_context.become = orig_become
|
||||
self._play_context.become_method = orig_become_method
|
||||
self._play_context.become_user = orig_become_user
|
||||
self._connection.set_become_plugin(orig_become)
|
||||
|
||||
return module_res
|
||||
|
||||
|
|
|
@ -40,6 +40,19 @@ class BecomeBase(AnsiblePlugin):
|
|||
self._id = ''
|
||||
self.success = ''
|
||||
|
||||
def get_option(self, option, hostvars=None, playcontext=None):
|
||||
""" Overrides the base get_option to provide a fallback to playcontext vars in case a 3rd party plugin did not
|
||||
implement the base become options required in Ansible. """
|
||||
# TODO: add deprecation warning for ValueError in devel that removes the playcontext fallback
|
||||
try:
|
||||
return super(BecomeBase, self).get_option(option, hostvars=hostvars)
|
||||
except KeyError:
|
||||
pc_fallback = ['become_user', 'become_pass', 'become_flags', 'become_exe']
|
||||
if option not in pc_fallback:
|
||||
raise
|
||||
|
||||
return getattr(playcontext, option, None)
|
||||
|
||||
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.
|
||||
|
|
|
@ -48,7 +48,7 @@ DOCUMENTATION = """
|
|||
vars:
|
||||
- name: ansible_become_password
|
||||
- name: ansible_become_pass
|
||||
- name: ansible_runas_runas
|
||||
- name: ansible_runas_pass
|
||||
env:
|
||||
- name: ANSIBLE_BECOME_PASS
|
||||
- name: ANSIBLE_RUNAS_PASS
|
||||
|
|
|
@ -249,7 +249,8 @@ class Connection(ConnectionBase):
|
|||
selector.close()
|
||||
|
||||
if not self.become.check_success(become_output):
|
||||
p.stdin.write(to_bytes(self._play_context.become_pass, errors='surrogate_or_strict') + b'\n')
|
||||
become_pass = self.become.get_option('become_pass', playcontext=self._play_context)
|
||||
p.stdin.write(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
|
||||
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)
|
||||
|
||||
|
|
|
@ -118,7 +118,8 @@ class Connection(ConnectionBase):
|
|||
selector.close()
|
||||
|
||||
if not self.become.check_success(become_output):
|
||||
p.stdin.write(to_bytes(self._play_context.become_pass, errors='surrogate_or_strict') + b'\n')
|
||||
become_pass = self.become.get_option('become_pass', playcontext=self._play_context)
|
||||
p.stdin.write(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
|
||||
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)
|
||||
|
||||
|
|
|
@ -415,7 +415,9 @@ class Connection(ConnectionBase):
|
|||
display.debug("chunk is: %s" % chunk)
|
||||
if not chunk:
|
||||
if b'unknown user' in become_output:
|
||||
raise AnsibleError('user %s does not exist' % self._play_context.become_user)
|
||||
n_become_user = to_native(self.become.get_option('become_user',
|
||||
playcontext=self._play_context))
|
||||
raise AnsibleError('user %s does not exist' % n_become_user)
|
||||
else:
|
||||
break
|
||||
# raise AnsibleError('ssh connection closed waiting for password prompt')
|
||||
|
@ -432,8 +434,9 @@ class Connection(ConnectionBase):
|
|||
break
|
||||
|
||||
if passprompt:
|
||||
if self._play_context.become and self._play_context.become_pass:
|
||||
chan.sendall(to_bytes(self._play_context.become_pass) + b'\n')
|
||||
if self.become:
|
||||
become_pass = self.become.get_option('become_pass', playcontext=self._play_context)
|
||||
chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
|
||||
else:
|
||||
raise AnsibleError("A password is required but none was supplied")
|
||||
else:
|
||||
|
|
|
@ -577,10 +577,6 @@ if ($bytes_read -gt 0) {
|
|||
self._connected = False
|
||||
|
||||
def _build_kwargs(self):
|
||||
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._psrp_host = self.get_option('remote_addr')
|
||||
self._psrp_user = self.get_option('remote_user')
|
||||
self._psrp_pass = self._play_context.password
|
||||
|
|
|
@ -837,7 +837,7 @@ class Connection(ConnectionBase):
|
|||
# wait for a password prompt.
|
||||
state = states.index('awaiting_prompt')
|
||||
display.debug(u'Initial state: %s: %s' % (states[state], to_text(prompt)))
|
||||
elif self._play_context.become and self.become.success:
|
||||
elif self.become and self.become.success:
|
||||
# We're requesting escalation without a password, so we have to
|
||||
# detect success/failure before sending any initial data.
|
||||
state = states.index('awaiting_escalation')
|
||||
|
@ -947,7 +947,8 @@ class Connection(ConnectionBase):
|
|||
if states[state] == 'awaiting_prompt':
|
||||
if self._flags['become_prompt']:
|
||||
display.debug(u'Sending become_password in response to prompt')
|
||||
stdin.write(to_bytes(self._play_context.become_pass) + b'\n')
|
||||
become_pass = self.become.get_option('become_pass', playcontext=self._play_context)
|
||||
stdin.write(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
|
||||
# On python3 stdin is a BufferedWriter, and we don't have a guarantee
|
||||
# that the write will happen without a flush
|
||||
stdin.flush()
|
||||
|
@ -968,19 +969,19 @@ class Connection(ConnectionBase):
|
|||
display.vvv(u'Escalation failed')
|
||||
self._terminate_process(p)
|
||||
self._flags['become_error'] = False
|
||||
raise AnsibleError('Incorrect %s password' % self._play_context.become_method)
|
||||
raise AnsibleError('Incorrect %s password' % self.become.name)
|
||||
elif self._flags['become_nopasswd_error']:
|
||||
display.vvv(u'Escalation requires password')
|
||||
self._terminate_process(p)
|
||||
self._flags['become_nopasswd_error'] = False
|
||||
raise AnsibleError('Missing %s password' % self._play_context.become_method)
|
||||
raise AnsibleError('Missing %s password' % self.become.name)
|
||||
elif self._flags['become_prompt']:
|
||||
# This shouldn't happen, because we should see the "Sorry,
|
||||
# try again" message first.
|
||||
display.vvv(u'Escalation prompt repeated')
|
||||
self._terminate_process(p)
|
||||
self._flags['become_prompt'] = False
|
||||
raise AnsibleError('Incorrect %s password' % self._play_context.become_method)
|
||||
raise AnsibleError('Incorrect %s password' % self.become.name)
|
||||
|
||||
# Once we're sure that the privilege escalation prompt, if any, has
|
||||
# been dealt with, we can send any initial data and start waiting
|
||||
|
|
|
@ -143,6 +143,15 @@
|
|||
- '"LogonUser failed" not in become_invalid_pass.msg'
|
||||
- '"Win32ErrorCode 1326 - 0x0000052E)" not in become_invalid_pass.msg'
|
||||
|
||||
- name: test become password precedence
|
||||
win_whoami:
|
||||
become: yes
|
||||
become_method: runas
|
||||
become_user: '{{ become_test_username }}'
|
||||
vars:
|
||||
ansible_become_pass: broken
|
||||
ansible_runas_pass: '{{ gen_pw }}' # should have a higher precedence than ansible_become_pass
|
||||
|
||||
- name: test become + async
|
||||
vars: *become_vars
|
||||
win_command: whoami
|
||||
|
|
|
@ -6,13 +6,30 @@
|
|||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from units.compat.mock import patch, MagicMock
|
||||
from ansible.plugins.action.win_updates import ActionModule
|
||||
from ansible.plugins.become.runas import BecomeModule
|
||||
from ansible.playbook.task import Task
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def test_win_updates():
|
||||
task = MagicMock(Task)
|
||||
task.args = {}
|
||||
|
||||
connection = MagicMock()
|
||||
connection.module_implementation_preferences = ('.ps1', '.exe', '')
|
||||
|
||||
play_context = MagicMock()
|
||||
play_context.check_mode = False
|
||||
|
||||
plugin = ActionModule(task, connection, play_context, loader=None, templar=None, shared_loader_obj=None)
|
||||
return plugin
|
||||
|
||||
|
||||
class TestWinUpdatesActionPlugin(object):
|
||||
|
||||
INVALID_OPTIONS = (
|
||||
|
@ -58,58 +75,46 @@ class TestWinUpdatesActionPlugin(object):
|
|||
assert res['failed']
|
||||
assert expected in res['msg']
|
||||
|
||||
BECOME_OPTIONS = (
|
||||
(False, False, "sudo", "root", True, "runas", "SYSTEM"),
|
||||
(False, True, "sudo", "root", True, "runas", "SYSTEM"),
|
||||
(False, False, "runas", "root", True, "runas", "SYSTEM"),
|
||||
(False, False, "sudo", "user", True, "runas", "user"),
|
||||
(False, None, "sudo", None, True, "runas", "SYSTEM"),
|
||||
def test_exec_with_become(self, test_win_updates):
|
||||
test_become = os.urandom(8)
|
||||
|
||||
# use scheduled task, we shouldn't change anything
|
||||
(True, False, "sudo", None, False, "sudo", None),
|
||||
(True, True, "runas", "SYSTEM", True, "runas", "SYSTEM"),
|
||||
)
|
||||
set_become_mock = MagicMock()
|
||||
test_win_updates._connection.become = test_become
|
||||
test_win_updates._connection.set_become_plugin = set_become_mock
|
||||
|
||||
# pylint bug: https://github.com/PyCQA/pylint/issues/511
|
||||
# pylint: disable=undefined-variable
|
||||
@pytest.mark.parametrize('use_task, o_b, o_bmethod, o_buser, e_b, e_bmethod, e_buser',
|
||||
((u, ob, obm, obu, eb, ebm, ebu)
|
||||
for u, ob, obm, obu, eb, ebm, ebu in BECOME_OPTIONS))
|
||||
def test_module_exec_with_become(self, use_task, o_b, o_bmethod, o_buser,
|
||||
e_b, e_bmethod, e_buser):
|
||||
def mock_execute_module(self, **kwargs):
|
||||
pc = self._play_context
|
||||
return {"become": pc.become, "become_method": pc.become_method,
|
||||
"become_user": pc.become_user}
|
||||
with patch('ansible.plugins.action.ActionBase._execute_module', new=MagicMock()):
|
||||
test_win_updates._execute_module_with_become('win_updates', {}, {}, True, False)
|
||||
|
||||
task = MagicMock(Task)
|
||||
task.args = {}
|
||||
# Asserts we don't override the become plugin.
|
||||
assert set_become_mock.call_count == 1
|
||||
assert set_become_mock.mock_calls[0][1][0] == test_become
|
||||
|
||||
connection = MagicMock()
|
||||
connection.module_implementation_preferences = ('.ps1', '.exe', '')
|
||||
def test_exec_with_become_no_plugin_set(self, test_win_updates):
|
||||
set_become_mock = MagicMock()
|
||||
test_win_updates._connection.become = None
|
||||
test_win_updates._connection.set_become_plugin = set_become_mock
|
||||
|
||||
play_context = MagicMock()
|
||||
play_context.check_mode = False
|
||||
play_context.become = o_b
|
||||
play_context.become_method = o_bmethod
|
||||
play_context.become_user = o_buser
|
||||
with patch('ansible.plugins.action.ActionBase._execute_module', new=MagicMock()):
|
||||
test_win_updates._execute_module_with_become('win_updates', {}, {}, True, False)
|
||||
|
||||
plugin = ActionModule(task, connection, play_context, loader=None,
|
||||
templar=None, shared_loader_obj=None)
|
||||
with patch('ansible.plugins.action.ActionBase._execute_module',
|
||||
new=mock_execute_module):
|
||||
actual = plugin._execute_module_with_become('win_updates', {}, {},
|
||||
True, use_task)
|
||||
assert set_become_mock.call_count == 2
|
||||
assert isinstance(set_become_mock.mock_calls[0][1][0], BecomeModule)
|
||||
assert set_become_mock.mock_calls[0][1][0].name == 'runas'
|
||||
assert set_become_mock.mock_calls[0][1][0].get_option('become_user') == 'SYSTEM'
|
||||
assert set_become_mock.mock_calls[0][1][0].get_option('become_flags') == ''
|
||||
assert set_become_mock.mock_calls[0][1][0].get_option('become_pass') is None
|
||||
assert set_become_mock.mock_calls[1][1] == (None,)
|
||||
|
||||
# always make sure we reset back to the defaults
|
||||
assert play_context.become == o_b
|
||||
assert play_context.become_method == o_bmethod
|
||||
assert play_context.become_user == o_buser
|
||||
def test_exec_with_become_no_plugin_set_use_task(self, test_win_updates):
|
||||
set_become_mock = MagicMock()
|
||||
test_win_updates._connection.become = None
|
||||
test_win_updates._connection.set_become_plugin = set_become_mock
|
||||
|
||||
# verify what was set when _execute_module was called
|
||||
assert actual['become'] == e_b
|
||||
assert actual['become_method'] == e_bmethod
|
||||
assert actual['become_user'] == e_buser
|
||||
with patch('ansible.plugins.action.ActionBase._execute_module', new=MagicMock()):
|
||||
test_win_updates._execute_module_with_become('win_updates', {}, {}, True, True)
|
||||
|
||||
assert set_become_mock.call_count == 1
|
||||
assert set_become_mock.mock_calls[0][1][0] is None
|
||||
|
||||
def test_module_exec_async_result(self, monkeypatch):
|
||||
return_val = {
|
||||
|
|
Loading…
Reference in a new issue