mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-05-22 08:59:08 -07:00
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
This commit is contained in:
parent
c581fbd0be
commit
445ff39f94
73 changed files with 1849 additions and 721 deletions
|
@ -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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue