mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Merge pull request #5694 from angstwad/add-su-support-revert
Add su support
This commit is contained in:
commit
da136dbe7c
18 changed files with 312 additions and 99 deletions
41
bin/ansible
41
bin/ansible
|
@ -67,6 +67,14 @@ class Cli(object):
|
||||||
if len(args) == 0 or len(args) > 1:
|
if len(args) == 0 or len(args) > 1:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# su and sudo command line arguments need to be mutually exclusive
|
||||||
|
if (options.su or options.su_user or options.ask_su_pass) and \
|
||||||
|
(options.sudo or options.sudo_user or options.ask_sudo_pass):
|
||||||
|
parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') "
|
||||||
|
"and su arguments ('-su', '--su-user', and '--ask-su-pass') are "
|
||||||
|
"mutually exclusive")
|
||||||
|
|
||||||
return (options, args)
|
return (options, args)
|
||||||
|
|
||||||
# ----------------------------------------------
|
# ----------------------------------------------
|
||||||
|
@ -96,31 +104,46 @@ class Cli(object):
|
||||||
|
|
||||||
sshpass = None
|
sshpass = None
|
||||||
sudopass = None
|
sudopass = None
|
||||||
|
su_pass = None
|
||||||
options.ask_pass = options.ask_pass or C.DEFAULT_ASK_PASS
|
options.ask_pass = options.ask_pass or C.DEFAULT_ASK_PASS
|
||||||
# Never ask for an SSH password when we run with local connection
|
# Never ask for an SSH password when we run with local connection
|
||||||
if options.connection == "local":
|
if options.connection == "local":
|
||||||
options.ask_pass = False
|
options.ask_pass = False
|
||||||
options.ask_sudo_pass = options.ask_sudo_pass or C.DEFAULT_ASK_SUDO_PASS
|
options.ask_sudo_pass = options.ask_sudo_pass or C.DEFAULT_ASK_SUDO_PASS
|
||||||
(sshpass, sudopass) = utils.ask_passwords(ask_pass=options.ask_pass, ask_sudo_pass=options.ask_sudo_pass)
|
options.ask_su_pass = options.ask_su_pass or C.DEFAULT_ASK_SU_PASS
|
||||||
if options.sudo_user or options.ask_sudo_pass:
|
(sshpass, sudopass, su_pass) = utils.ask_passwords(ask_pass=options.ask_pass, ask_sudo_pass=options.ask_sudo_pass, ask_su_pass=options.ask_su_pass)
|
||||||
|
if options.su_user or options.ask_su_pass:
|
||||||
|
options.su = True
|
||||||
|
elif options.sudo_user or options.ask_sudo_pass:
|
||||||
options.sudo = True
|
options.sudo = True
|
||||||
options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER
|
options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER
|
||||||
|
options.su_user = options.su_user or C.DEFAULT_SU_USER
|
||||||
if options.tree:
|
if options.tree:
|
||||||
utils.prepare_writeable_dir(options.tree)
|
utils.prepare_writeable_dir(options.tree)
|
||||||
|
|
||||||
|
|
||||||
runner = Runner(
|
runner = Runner(
|
||||||
module_name=options.module_name, module_path=options.module_path,
|
module_name=options.module_name,
|
||||||
|
module_path=options.module_path,
|
||||||
module_args=options.module_args,
|
module_args=options.module_args,
|
||||||
remote_user=options.remote_user, remote_pass=sshpass,
|
remote_user=options.remote_user,
|
||||||
inventory=inventory_manager, timeout=options.timeout,
|
remote_pass=sshpass,
|
||||||
|
inventory=inventory_manager,
|
||||||
|
timeout=options.timeout,
|
||||||
private_key_file=options.private_key_file,
|
private_key_file=options.private_key_file,
|
||||||
forks=options.forks,
|
forks=options.forks,
|
||||||
pattern=pattern,
|
pattern=pattern,
|
||||||
callbacks=self.callbacks, sudo=options.sudo,
|
callbacks=self.callbacks,
|
||||||
sudo_pass=sudopass,sudo_user=options.sudo_user,
|
sudo=options.sudo,
|
||||||
transport=options.connection, subset=options.subset,
|
sudo_pass=sudopass,
|
||||||
|
sudo_user=options.sudo_user,
|
||||||
|
transport=options.connection,
|
||||||
|
subset=options.subset,
|
||||||
check=options.check,
|
check=options.check,
|
||||||
diff=options.check
|
diff=options.check,
|
||||||
|
su=options.su,
|
||||||
|
su_pass=su_pass,
|
||||||
|
su_user=options.su_user
|
||||||
)
|
)
|
||||||
|
|
||||||
if options.seconds:
|
if options.seconds:
|
||||||
|
|
|
@ -83,6 +83,13 @@ def main(args):
|
||||||
parser.print_help(file=sys.stderr)
|
parser.print_help(file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
# su and sudo command line arguments need to be mutually exclusive
|
||||||
|
if (options.su or options.su_user or options.ask_su_pass) and \
|
||||||
|
(options.sudo or options.sudo_user or options.ask_sudo_pass):
|
||||||
|
parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') "
|
||||||
|
"and su arguments ('-su', '--su-user', and '--ask-su-pass') are "
|
||||||
|
"mutually exclusive")
|
||||||
|
|
||||||
inventory = ansible.inventory.Inventory(options.inventory)
|
inventory = ansible.inventory.Inventory(options.inventory)
|
||||||
inventory.subset(options.subset)
|
inventory.subset(options.subset)
|
||||||
if len(inventory.list_hosts()) == 0:
|
if len(inventory.list_hosts()) == 0:
|
||||||
|
@ -90,14 +97,18 @@ def main(args):
|
||||||
|
|
||||||
sshpass = None
|
sshpass = None
|
||||||
sudopass = None
|
sudopass = None
|
||||||
|
su_pass = None
|
||||||
if not options.listhosts and not options.syntax and not options.listtasks:
|
if not options.listhosts and not options.syntax and not options.listtasks:
|
||||||
options.ask_pass = options.ask_pass or C.DEFAULT_ASK_PASS
|
options.ask_pass = options.ask_pass or C.DEFAULT_ASK_PASS
|
||||||
# Never ask for an SSH password when we run with local connection
|
# Never ask for an SSH password when we run with local connection
|
||||||
if options.connection == "local":
|
if options.connection == "local":
|
||||||
options.ask_pass = False
|
options.ask_pass = False
|
||||||
options.ask_sudo_pass = options.ask_sudo_pass or C.DEFAULT_ASK_SUDO_PASS
|
options.ask_sudo_pass = options.ask_sudo_pass or C.DEFAULT_ASK_SUDO_PASS
|
||||||
(sshpass, sudopass) = utils.ask_passwords(ask_pass=options.ask_pass, ask_sudo_pass=options.ask_sudo_pass)
|
options.ask_su_pass = options.ask_su_pass or C.DEFAULT_ASK_SU_PASS
|
||||||
|
(sshpass, sudopass, su_pass) = utils.ask_passwords(ask_pass=options.ask_pass, ask_sudo_pass=options.ask_sudo_pass, ask_su_pass=options.ask_su_pass)
|
||||||
options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER
|
options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER
|
||||||
|
options.su_user = options.su_user or C.DEFAULT_SU_USER
|
||||||
|
|
||||||
|
|
||||||
extra_vars = {}
|
extra_vars = {}
|
||||||
for extra_vars_opt in options.extra_vars:
|
for extra_vars_opt in options.extra_vars:
|
||||||
|
@ -156,7 +167,10 @@ def main(args):
|
||||||
only_tags=only_tags,
|
only_tags=only_tags,
|
||||||
skip_tags=skip_tags,
|
skip_tags=skip_tags,
|
||||||
check=options.check,
|
check=options.check,
|
||||||
diff=options.diff
|
diff=options.diff,
|
||||||
|
su=options.su,
|
||||||
|
su_pass=su_pass,
|
||||||
|
su_user=options.su_user
|
||||||
)
|
)
|
||||||
|
|
||||||
if options.listhosts or options.listtasks or options.syntax:
|
if options.listhosts or options.listtasks or options.syntax:
|
||||||
|
|
|
@ -127,6 +127,11 @@ DEFAULT_HASH_BEHAVIOUR = get_config(p, DEFAULTS, 'hash_behaviour', 'ANSIBLE_H
|
||||||
DEFAULT_LEGACY_PLAYBOOK_VARIABLES = get_config(p, DEFAULTS, 'legacy_playbook_variables', 'ANSIBLE_LEGACY_PLAYBOOK_VARIABLES', True, boolean=True)
|
DEFAULT_LEGACY_PLAYBOOK_VARIABLES = get_config(p, DEFAULTS, 'legacy_playbook_variables', 'ANSIBLE_LEGACY_PLAYBOOK_VARIABLES', True, boolean=True)
|
||||||
DEFAULT_JINJA2_EXTENSIONS = get_config(p, DEFAULTS, 'jinja2_extensions', 'ANSIBLE_JINJA2_EXTENSIONS', None)
|
DEFAULT_JINJA2_EXTENSIONS = get_config(p, DEFAULTS, 'jinja2_extensions', 'ANSIBLE_JINJA2_EXTENSIONS', None)
|
||||||
DEFAULT_EXECUTABLE = get_config(p, DEFAULTS, 'executable', 'ANSIBLE_EXECUTABLE', '/bin/sh')
|
DEFAULT_EXECUTABLE = get_config(p, DEFAULTS, 'executable', 'ANSIBLE_EXECUTABLE', '/bin/sh')
|
||||||
|
DEFAULT_SU_EXE = get_config(p, DEFAULTS, 'su_exe', 'ANSIBLE_SU_EXE', 'su')
|
||||||
|
DEFAULT_SU = get_config(p, DEFAULTS, 'su', 'ANSIBLE_SU', False, boolean=True)
|
||||||
|
DEFAULT_SU_FLAGS = get_config(p, DEFAULTS, 'su_flags', 'ANSIBLE_SU_FLAGS', '')
|
||||||
|
DEFAULT_SU_USER = get_config(p, DEFAULTS, 'su_user', 'ANSIBLE_SU_USER', 'root')
|
||||||
|
DEFAULT_ASK_SU_PASS = get_config(p, DEFAULTS, 'ask_su_pass', 'ANSIBLE_ASK_SU_PASS', False, boolean=True)
|
||||||
|
|
||||||
DEFAULT_ACTION_PLUGIN_PATH = get_config(p, DEFAULTS, 'action_plugins', 'ANSIBLE_ACTION_PLUGINS', '/usr/share/ansible_plugins/action_plugins')
|
DEFAULT_ACTION_PLUGIN_PATH = get_config(p, DEFAULTS, 'action_plugins', 'ANSIBLE_ACTION_PLUGINS', '/usr/share/ansible_plugins/action_plugins')
|
||||||
DEFAULT_CALLBACK_PLUGIN_PATH = get_config(p, DEFAULTS, 'callback_plugins', 'ANSIBLE_CALLBACK_PLUGINS', '/usr/share/ansible_plugins/callback_plugins')
|
DEFAULT_CALLBACK_PLUGIN_PATH = get_config(p, DEFAULTS, 'callback_plugins', 'ANSIBLE_CALLBACK_PLUGINS', '/usr/share/ansible_plugins/callback_plugins')
|
||||||
|
@ -161,4 +166,5 @@ DEFAULT_PASSWORD_CHARS = ascii_letters + digits + ".,:-_"
|
||||||
DEFAULT_SUDO_PASS = None
|
DEFAULT_SUDO_PASS = None
|
||||||
DEFAULT_REMOTE_PASS = None
|
DEFAULT_REMOTE_PASS = None
|
||||||
DEFAULT_SUBSET = None
|
DEFAULT_SUBSET = None
|
||||||
|
DEFAULT_SU_PASS = None
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,11 @@ class PlayBook(object):
|
||||||
inventory = None,
|
inventory = None,
|
||||||
check = False,
|
check = False,
|
||||||
diff = False,
|
diff = False,
|
||||||
any_errors_fatal = False):
|
any_errors_fatal = False,
|
||||||
|
su = False,
|
||||||
|
su_user = False,
|
||||||
|
su_pass = False,
|
||||||
|
):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
playbook: path to a playbook file
|
playbook: path to a playbook file
|
||||||
|
@ -122,6 +126,9 @@ class PlayBook(object):
|
||||||
self.only_tags = only_tags
|
self.only_tags = only_tags
|
||||||
self.skip_tags = skip_tags
|
self.skip_tags = skip_tags
|
||||||
self.any_errors_fatal = any_errors_fatal
|
self.any_errors_fatal = any_errors_fatal
|
||||||
|
self.su = su
|
||||||
|
self.su_user = su_user
|
||||||
|
self.su_pass = su_pass
|
||||||
|
|
||||||
self.callbacks.playbook = self
|
self.callbacks.playbook = self
|
||||||
self.runner_callbacks.playbook = self
|
self.runner_callbacks.playbook = self
|
||||||
|
@ -303,20 +310,39 @@ class PlayBook(object):
|
||||||
self.inventory.restrict_to(hosts)
|
self.inventory.restrict_to(hosts)
|
||||||
|
|
||||||
runner = ansible.runner.Runner(
|
runner = ansible.runner.Runner(
|
||||||
pattern=task.play.hosts, inventory=self.inventory, module_name=task.module_name,
|
pattern=task.play.hosts,
|
||||||
module_args=task.module_args, forks=self.forks,
|
inventory=self.inventory,
|
||||||
remote_pass=self.remote_pass, module_path=self.module_path,
|
module_name=task.module_name,
|
||||||
timeout=self.timeout, remote_user=task.remote_user,
|
module_args=task.module_args,
|
||||||
remote_port=task.play.remote_port, module_vars=task.module_vars,
|
forks=self.forks,
|
||||||
default_vars=task.default_vars, private_key_file=self.private_key_file,
|
remote_pass=self.remote_pass,
|
||||||
setup_cache=self.SETUP_CACHE, basedir=task.play.basedir,
|
module_path=self.module_path,
|
||||||
conditional=task.when, callbacks=self.runner_callbacks,
|
timeout=self.timeout,
|
||||||
sudo=task.sudo, sudo_user=task.sudo_user,
|
remote_user=task.remote_user,
|
||||||
transport=task.transport, sudo_pass=task.sudo_pass, is_playbook=True,
|
remote_port=task.play.remote_port,
|
||||||
check=self.check, diff=self.diff, environment=task.environment, complex_args=task.args,
|
module_vars=task.module_vars,
|
||||||
accelerate=task.play.accelerate, accelerate_port=task.play.accelerate_port,
|
default_vars=task.default_vars,
|
||||||
|
private_key_file=self.private_key_file,
|
||||||
|
setup_cache=self.SETUP_CACHE,
|
||||||
|
basedir=task.play.basedir,
|
||||||
|
conditional=task.when,
|
||||||
|
callbacks=self.runner_callbacks,
|
||||||
|
sudo=task.sudo,
|
||||||
|
sudo_user=task.sudo_user,
|
||||||
|
transport=task.transport,
|
||||||
|
sudo_pass=task.sudo_pass,
|
||||||
|
is_playbook=True,
|
||||||
|
check=self.check,
|
||||||
|
diff=self.diff,
|
||||||
|
environment=task.environment,
|
||||||
|
complex_args=task.args,
|
||||||
|
accelerate=task.play.accelerate,
|
||||||
|
accelerate_port=task.play.accelerate_port,
|
||||||
accelerate_ipv6=task.play.accelerate_ipv6,
|
accelerate_ipv6=task.play.accelerate_ipv6,
|
||||||
error_on_undefined_vars=C.DEFAULT_UNDEFINED_VAR_BEHAVIOR
|
error_on_undefined_vars=C.DEFAULT_UNDEFINED_VAR_BEHAVIOR,
|
||||||
|
su=task.su,
|
||||||
|
su_user=task.su_user,
|
||||||
|
su_pass=task.su_pass
|
||||||
)
|
)
|
||||||
|
|
||||||
if task.async_seconds == 0:
|
if task.async_seconds == 0:
|
||||||
|
@ -446,13 +472,30 @@ class PlayBook(object):
|
||||||
|
|
||||||
# push any variables down to the system
|
# push any variables down to the system
|
||||||
setup_results = ansible.runner.Runner(
|
setup_results = ansible.runner.Runner(
|
||||||
pattern=play.hosts, module_name='setup', module_args={}, inventory=self.inventory,
|
pattern=play.hosts,
|
||||||
forks=self.forks, module_path=self.module_path, timeout=self.timeout, remote_user=play.remote_user,
|
module_name='setup',
|
||||||
remote_pass=self.remote_pass, remote_port=play.remote_port, private_key_file=self.private_key_file,
|
module_args={},
|
||||||
setup_cache=self.SETUP_CACHE, callbacks=self.runner_callbacks, sudo=play.sudo, sudo_user=play.sudo_user,
|
inventory=self.inventory,
|
||||||
transport=play.transport, sudo_pass=self.sudo_pass, is_playbook=True, module_vars=play.vars,
|
forks=self.forks,
|
||||||
default_vars=play.default_vars, check=self.check, diff=self.diff,
|
module_path=self.module_path,
|
||||||
accelerate=play.accelerate, accelerate_port=play.accelerate_port,
|
timeout=self.timeout,
|
||||||
|
remote_user=play.remote_user,
|
||||||
|
remote_pass=self.remote_pass,
|
||||||
|
remote_port=play.remote_port,
|
||||||
|
private_key_file=self.private_key_file,
|
||||||
|
setup_cache=self.SETUP_CACHE,
|
||||||
|
callbacks=self.runner_callbacks,
|
||||||
|
sudo=play.sudo,
|
||||||
|
sudo_user=play.sudo_user,
|
||||||
|
transport=play.transport,
|
||||||
|
sudo_pass=self.sudo_pass,
|
||||||
|
is_playbook=True,
|
||||||
|
module_vars=play.vars,
|
||||||
|
default_vars=play.default_vars,
|
||||||
|
check=self.check,
|
||||||
|
diff=self.diff,
|
||||||
|
accelerate=play.accelerate,
|
||||||
|
accelerate_port=play.accelerate_port,
|
||||||
).run()
|
).run()
|
||||||
self.stats.compute(setup_results, setup=True)
|
self.stats.compute(setup_results, setup=True)
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ class Play(object):
|
||||||
'handlers', 'remote_user', 'remote_port', 'included_roles', 'accelerate',
|
'handlers', 'remote_user', 'remote_port', 'included_roles', 'accelerate',
|
||||||
'accelerate_port', 'accelerate_ipv6', 'sudo', 'sudo_user', 'transport', 'playbook',
|
'accelerate_port', 'accelerate_ipv6', 'sudo', 'sudo_user', 'transport', 'playbook',
|
||||||
'tags', 'gather_facts', 'serial', '_ds', '_handlers', '_tasks',
|
'tags', 'gather_facts', 'serial', '_ds', '_handlers', '_tasks',
|
||||||
'basedir', 'any_errors_fatal', 'roles', 'max_fail_pct'
|
'basedir', 'any_errors_fatal', 'roles', 'max_fail_pct', 'su', 'su_user'
|
||||||
]
|
]
|
||||||
|
|
||||||
# to catch typos and so forth -- these are userland names
|
# to catch typos and so forth -- these are userland names
|
||||||
|
@ -43,7 +43,8 @@ class Play(object):
|
||||||
'hosts', 'name', 'vars', 'vars_prompt', 'vars_files',
|
'hosts', 'name', 'vars', 'vars_prompt', 'vars_files',
|
||||||
'tasks', 'handlers', 'remote_user', 'user', 'port', 'include', 'accelerate', 'accelerate_port', 'accelerate_ipv6',
|
'tasks', 'handlers', 'remote_user', 'user', 'port', 'include', 'accelerate', 'accelerate_port', 'accelerate_ipv6',
|
||||||
'sudo', 'sudo_user', 'connection', 'tags', 'gather_facts', 'serial',
|
'sudo', 'sudo_user', 'connection', 'tags', 'gather_facts', 'serial',
|
||||||
'any_errors_fatal', 'roles', 'pre_tasks', 'post_tasks', 'max_fail_percentage'
|
'any_errors_fatal', 'roles', 'pre_tasks', 'post_tasks', 'max_fail_percentage',
|
||||||
|
'su', 'su_user'
|
||||||
]
|
]
|
||||||
|
|
||||||
# *************************************************
|
# *************************************************
|
||||||
|
@ -121,6 +122,8 @@ class Play(object):
|
||||||
self.accelerate_port = ds.get('accelerate_port', None)
|
self.accelerate_port = ds.get('accelerate_port', None)
|
||||||
self.accelerate_ipv6 = ds.get('accelerate_ipv6', False)
|
self.accelerate_ipv6 = ds.get('accelerate_ipv6', False)
|
||||||
self.max_fail_pct = int(ds.get('max_fail_percentage', 100))
|
self.max_fail_pct = int(ds.get('max_fail_percentage', 100))
|
||||||
|
self.su = ds.get('su', self.playbook.su)
|
||||||
|
self.su_user = ds.get('su_user', self.playbook.su_user)
|
||||||
|
|
||||||
load_vars = {}
|
load_vars = {}
|
||||||
load_vars['playbook_dir'] = self.basedir
|
load_vars['playbook_dir'] = self.basedir
|
||||||
|
@ -432,7 +435,8 @@ class Play(object):
|
||||||
|
|
||||||
# *************************************************
|
# *************************************************
|
||||||
|
|
||||||
def _load_tasks(self, tasks, vars=None, default_vars=None, sudo_vars=None, additional_conditions=None, original_file=None, role_name=None):
|
def _load_tasks(self, tasks, vars=None, default_vars=None, sudo_vars=None,
|
||||||
|
additional_conditions=None, original_file=None, role_name=None):
|
||||||
''' handle task and handler include statements '''
|
''' handle task and handler include statements '''
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
@ -469,7 +473,7 @@ class Play(object):
|
||||||
|
|
||||||
if 'meta' in x:
|
if 'meta' in x:
|
||||||
if x['meta'] == 'flush_handlers':
|
if x['meta'] == 'flush_handlers':
|
||||||
results.append(Task(self,x))
|
results.append(Task(self, x))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
task_vars = self.vars.copy()
|
task_vars = self.vars.copy()
|
||||||
|
@ -537,7 +541,13 @@ class Play(object):
|
||||||
loaded = self._load_tasks(data, mv, default_vars, included_sudo_vars, list(included_additional_conditions), original_file=include_filename, role_name=new_role)
|
loaded = self._load_tasks(data, mv, default_vars, included_sudo_vars, list(included_additional_conditions), original_file=include_filename, role_name=new_role)
|
||||||
results += loaded
|
results += loaded
|
||||||
elif type(x) == dict:
|
elif type(x) == dict:
|
||||||
task = Task(self,x,module_vars=task_vars,default_vars=default_vars,additional_conditions=list(additional_conditions),role_name=role_name)
|
task = Task(
|
||||||
|
self, x,
|
||||||
|
module_vars=task_vars,
|
||||||
|
default_vars=default_vars,
|
||||||
|
additional_conditions=list(additional_conditions),
|
||||||
|
role_name=role_name
|
||||||
|
)
|
||||||
results.append(task)
|
results.append(task)
|
||||||
else:
|
else:
|
||||||
raise Exception("unexpected task type")
|
raise Exception("unexpected task type")
|
||||||
|
|
|
@ -30,7 +30,8 @@ class Task(object):
|
||||||
'delegate_to', 'first_available_file', 'ignore_errors',
|
'delegate_to', 'first_available_file', 'ignore_errors',
|
||||||
'local_action', 'transport', 'sudo', 'remote_user', 'sudo_user', 'sudo_pass',
|
'local_action', 'transport', 'sudo', 'remote_user', 'sudo_user', 'sudo_pass',
|
||||||
'items_lookup_plugin', 'items_lookup_terms', 'environment', 'args',
|
'items_lookup_plugin', 'items_lookup_terms', 'environment', 'args',
|
||||||
'any_errors_fatal', 'changed_when', 'failed_when', 'always_run', 'delay', 'retries', 'until'
|
'any_errors_fatal', 'changed_when', 'failed_when', 'always_run', 'delay', 'retries', 'until',
|
||||||
|
'su', 'su_user', 'su_pass'
|
||||||
]
|
]
|
||||||
|
|
||||||
# to prevent typos and such
|
# to prevent typos and such
|
||||||
|
@ -39,7 +40,8 @@ class Task(object):
|
||||||
'first_available_file', 'include', 'tags', 'register', 'ignore_errors',
|
'first_available_file', 'include', 'tags', 'register', 'ignore_errors',
|
||||||
'delegate_to', 'local_action', 'transport', 'remote_user', 'sudo', 'sudo_user',
|
'delegate_to', 'local_action', 'transport', 'remote_user', 'sudo', 'sudo_user',
|
||||||
'sudo_pass', 'when', 'connection', 'environment', 'args',
|
'sudo_pass', 'when', 'connection', 'environment', 'args',
|
||||||
'any_errors_fatal', 'changed_when', 'failed_when', 'always_run', 'delay', 'retries', 'until'
|
'any_errors_fatal', 'changed_when', 'failed_when', 'always_run', 'delay', 'retries', 'until',
|
||||||
|
'su', 'su_user', 'su_pass'
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, play, ds, module_vars=None, default_vars=None, additional_conditions=None, role_name=None):
|
def __init__(self, play, ds, module_vars=None, default_vars=None, additional_conditions=None, role_name=None):
|
||||||
|
@ -117,6 +119,7 @@ class Task(object):
|
||||||
self.tags = [ 'all' ]
|
self.tags = [ 'all' ]
|
||||||
self.register = ds.get('register', None)
|
self.register = ds.get('register', None)
|
||||||
self.sudo = utils.boolean(ds.get('sudo', play.sudo))
|
self.sudo = utils.boolean(ds.get('sudo', play.sudo))
|
||||||
|
self.su = utils.boolean(ds.get('sudo', play.su))
|
||||||
self.environment = ds.get('environment', {})
|
self.environment = ds.get('environment', {})
|
||||||
self.role_name = role_name
|
self.role_name = role_name
|
||||||
|
|
||||||
|
@ -142,12 +145,17 @@ class Task(object):
|
||||||
else:
|
else:
|
||||||
self.remote_user = ds.get('remote_user', play.playbook.remote_user)
|
self.remote_user = ds.get('remote_user', play.playbook.remote_user)
|
||||||
|
|
||||||
|
self.sudo_user = None
|
||||||
|
self.sudo_pass = None
|
||||||
|
self.su_user = None
|
||||||
|
self.su_pass = None
|
||||||
|
|
||||||
if self.sudo:
|
if self.sudo:
|
||||||
self.sudo_user = ds.get('sudo_user', play.sudo_user)
|
self.sudo_user = ds.get('sudo_user', play.sudo_user)
|
||||||
self.sudo_pass = ds.get('sudo_pass', play.playbook.sudo_pass)
|
self.sudo_pass = ds.get('sudo_pass', play.playbook.sudo_pass)
|
||||||
else:
|
elif self.su:
|
||||||
self.sudo_user = None
|
self.su_user = ds.get('su_user', play.su_user)
|
||||||
self.sudo_pass = None
|
self.su_pass = ds.get('su_pass', play.playbook.su_pass)
|
||||||
|
|
||||||
# Both are defined
|
# Both are defined
|
||||||
if ('action' in ds) and ('local_action' in ds):
|
if ('action' in ds) and ('local_action' in ds):
|
||||||
|
|
|
@ -141,6 +141,9 @@ class Runner(object):
|
||||||
accelerate=False, # use accelerated connection
|
accelerate=False, # use accelerated connection
|
||||||
accelerate_ipv6=False, # accelerated connection w/ IPv6
|
accelerate_ipv6=False, # accelerated connection w/ IPv6
|
||||||
accelerate_port=None, # port to use with accelerated connection
|
accelerate_port=None, # port to use with accelerated connection
|
||||||
|
su=False, # Are we running our command via su?
|
||||||
|
su_user=None, # User to su to when running command, ex: 'root'
|
||||||
|
su_pass=C.DEFAULT_SU_PASS
|
||||||
):
|
):
|
||||||
|
|
||||||
# used to lock multiprocess inputs and outputs at various levels
|
# used to lock multiprocess inputs and outputs at various levels
|
||||||
|
@ -188,6 +191,9 @@ class Runner(object):
|
||||||
self.accelerate_ipv6 = accelerate_ipv6
|
self.accelerate_ipv6 = accelerate_ipv6
|
||||||
self.callbacks.runner = self
|
self.callbacks.runner = self
|
||||||
self.original_transport = self.transport
|
self.original_transport = self.transport
|
||||||
|
self.su = su
|
||||||
|
self.su_user = su_user
|
||||||
|
self.su_pass = su_pass
|
||||||
|
|
||||||
if self.transport == 'smart':
|
if self.transport == 'smart':
|
||||||
# if the transport is 'smart' see if SSH can support ControlPersist if not use paramiko
|
# if the transport is 'smart' see if SSH can support ControlPersist if not use paramiko
|
||||||
|
@ -311,12 +317,13 @@ class Runner(object):
|
||||||
or async_jid is not None
|
or async_jid is not None
|
||||||
or not conn.has_pipelining
|
or not conn.has_pipelining
|
||||||
or not C.ANSIBLE_SSH_PIPELINING
|
or not C.ANSIBLE_SSH_PIPELINING
|
||||||
or C.DEFAULT_KEEP_REMOTE_FILES):
|
or C.DEFAULT_KEEP_REMOTE_FILES
|
||||||
|
or self.su):
|
||||||
self._transfer_str(conn, tmp, module_name, module_data)
|
self._transfer_str(conn, tmp, module_name, module_data)
|
||||||
|
|
||||||
environment_string = self._compute_environment_string(inject)
|
environment_string = self._compute_environment_string(inject)
|
||||||
|
|
||||||
if tmp.find("tmp") != -1 and self.sudo and self.sudo_user != 'root':
|
if (self.sudo or self.su) and (self.sudo_user != 'root' or self.su_user != 'root'):
|
||||||
# deal with possible umask issues once sudo'ed to other user
|
# deal with possible umask issues once sudo'ed to other user
|
||||||
cmd_chmod = "chmod a+r %s" % remote_module_path
|
cmd_chmod = "chmod a+r %s" % remote_module_path
|
||||||
self._low_level_exec_command(conn, cmd_chmod, tmp, sudoable=False)
|
self._low_level_exec_command(conn, cmd_chmod, tmp, sudoable=False)
|
||||||
|
@ -343,7 +350,7 @@ class Runner(object):
|
||||||
else:
|
else:
|
||||||
argsfile = self._transfer_str(conn, tmp, 'arguments', args)
|
argsfile = self._transfer_str(conn, tmp, 'arguments', args)
|
||||||
|
|
||||||
if self.sudo and self.sudo_user != 'root':
|
if (self.sudo or self.su) and (self.sudo_user != 'root' or self.su_user != 'root'):
|
||||||
# deal with possible umask issues once sudo'ed to other user
|
# deal with possible umask issues once sudo'ed to other user
|
||||||
cmd_args_chmod = "chmod a+r %s" % argsfile
|
cmd_args_chmod = "chmod a+r %s" % argsfile
|
||||||
self._low_level_exec_command(conn, cmd_args_chmod, tmp, sudoable=False)
|
self._low_level_exec_command(conn, cmd_args_chmod, tmp, sudoable=False)
|
||||||
|
@ -354,7 +361,7 @@ class Runner(object):
|
||||||
cmd = " ".join([str(x) for x in [remote_module_path, async_jid, async_limit, async_module, argsfile]])
|
cmd = " ".join([str(x) for x in [remote_module_path, async_jid, async_limit, async_module, argsfile]])
|
||||||
else:
|
else:
|
||||||
if async_jid is None:
|
if async_jid is None:
|
||||||
if conn.has_pipelining and C.ANSIBLE_SSH_PIPELINING and not C.DEFAULT_KEEP_REMOTE_FILES:
|
if conn.has_pipelining and C.ANSIBLE_SSH_PIPELINING and not C.DEFAULT_KEEP_REMOTE_FILES and not self.su:
|
||||||
in_data = module_data
|
in_data = module_data
|
||||||
else:
|
else:
|
||||||
cmd = "%s" % (remote_module_path)
|
cmd = "%s" % (remote_module_path)
|
||||||
|
@ -369,7 +376,7 @@ class Runner(object):
|
||||||
cmd = cmd.strip()
|
cmd = cmd.strip()
|
||||||
|
|
||||||
if tmp.find("tmp") != -1 and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files:
|
if tmp.find("tmp") != -1 and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files:
|
||||||
if not self.sudo or self.sudo_user == 'root':
|
if not self.sudo or self.su or self.sudo_user == 'root' or self.su_user == 'root':
|
||||||
# not sudoing or sudoing to root, so can cleanup files in the same step
|
# not sudoing or sudoing to root, so can cleanup files in the same step
|
||||||
cmd = cmd + "; rm -rf %s >/dev/null 2>&1" % tmp
|
cmd = cmd + "; rm -rf %s >/dev/null 2>&1" % tmp
|
||||||
|
|
||||||
|
@ -379,10 +386,13 @@ class Runner(object):
|
||||||
# specified in the play, not the sudo_user
|
# specified in the play, not the sudo_user
|
||||||
sudoable = False
|
sudoable = False
|
||||||
|
|
||||||
|
if self.su:
|
||||||
|
res = self._low_level_exec_command(conn, cmd, tmp, su=sudoable, in_data=in_data)
|
||||||
|
else:
|
||||||
res = self._low_level_exec_command(conn, cmd, tmp, sudoable=sudoable, in_data=in_data)
|
res = self._low_level_exec_command(conn, cmd, tmp, sudoable=sudoable, in_data=in_data)
|
||||||
|
|
||||||
if tmp.find("tmp") != -1 and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files:
|
if tmp.find("tmp") != -1 and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files:
|
||||||
if self.sudo and self.sudo_user != 'root':
|
if (self.sudo or self.su) and (self.sudo_user != 'root' or self.su_user != 'root'):
|
||||||
# not sudoing to root, so maybe can't delete files as that other user
|
# not sudoing to root, so maybe can't delete files as that other user
|
||||||
# have to clean up temp files as original user in a second step
|
# have to clean up temp files as original user in a second step
|
||||||
cmd2 = "rm -rf %s >/dev/null 2>&1" % tmp
|
cmd2 = "rm -rf %s >/dev/null 2>&1" % tmp
|
||||||
|
@ -613,6 +623,9 @@ class Runner(object):
|
||||||
actual_transport = inject.get('ansible_connection', self.transport)
|
actual_transport = inject.get('ansible_connection', self.transport)
|
||||||
actual_private_key_file = inject.get('ansible_ssh_private_key_file', self.private_key_file)
|
actual_private_key_file = inject.get('ansible_ssh_private_key_file', self.private_key_file)
|
||||||
self.sudo_pass = inject.get('ansible_sudo_pass', self.sudo_pass)
|
self.sudo_pass = inject.get('ansible_sudo_pass', self.sudo_pass)
|
||||||
|
self.su = inject.get('ansible_su', self.su_pass)
|
||||||
|
self.su_user = inject.get('ansible_su_user', self.su_user)
|
||||||
|
self.su_pass = inject.get('ansible_su_pass', self.su_pass)
|
||||||
|
|
||||||
if actual_private_key_file is not None:
|
if actual_private_key_file is not None:
|
||||||
actual_private_key_file = os.path.expanduser(actual_private_key_file)
|
actual_private_key_file = os.path.expanduser(actual_private_key_file)
|
||||||
|
@ -798,7 +811,10 @@ class Runner(object):
|
||||||
if tmp.find("tmp") != -1:
|
if tmp.find("tmp") != -1:
|
||||||
# tmp has already been created
|
# tmp has already been created
|
||||||
return False
|
return False
|
||||||
if not conn.has_pipelining or not C.ANSIBLE_SSH_PIPELINING or C.DEFAULT_KEEP_REMOTE_FILES:
|
if not conn.has_pipelining or not C.ANSIBLE_SSH_PIPELINING or C.DEFAULT_KEEP_REMOTE_FILES or self.su:
|
||||||
|
# tmp is necessary to store module source code
|
||||||
|
return True
|
||||||
|
if not conn.has_pipelining:
|
||||||
# tmp is necessary to store the module source code
|
# tmp is necessary to store the module source code
|
||||||
# or we want to keep the files on the target system
|
# or we want to keep the files on the target system
|
||||||
return True
|
return True
|
||||||
|
@ -810,20 +826,36 @@ class Runner(object):
|
||||||
|
|
||||||
# *****************************************************
|
# *****************************************************
|
||||||
|
|
||||||
def _low_level_exec_command(self, conn, cmd, tmp, sudoable=False, executable=None, in_data=None):
|
def _low_level_exec_command(self, conn, cmd, tmp, sudoable=False,
|
||||||
|
executable=None, su=False, in_data=None):
|
||||||
''' execute a command string over SSH, return the output '''
|
''' execute a command string over SSH, return the output '''
|
||||||
|
|
||||||
if executable is None:
|
if executable is None:
|
||||||
executable = C.DEFAULT_EXECUTABLE
|
executable = C.DEFAULT_EXECUTABLE
|
||||||
|
|
||||||
sudo_user = self.sudo_user
|
sudo_user = self.sudo_user
|
||||||
|
su_user = self.su_user
|
||||||
|
|
||||||
# compare connection user to sudo_user and disable if the same
|
# compare connection user to sudo_user and disable if the same
|
||||||
if hasattr(conn, 'user'):
|
if hasattr(conn, 'user'):
|
||||||
if conn.user == sudo_user:
|
if conn.user == sudo_user or conn.user == su_user:
|
||||||
sudoable = False
|
sudoable = False
|
||||||
|
su = False
|
||||||
|
|
||||||
rc, stdin, stdout, stderr = conn.exec_command(cmd, tmp, sudo_user, sudoable=sudoable, executable=executable, in_data=in_data)
|
if su:
|
||||||
|
rc, stdin, stdout, stderr = conn.exec_command(cmd,
|
||||||
|
tmp,
|
||||||
|
su_user=su_user,
|
||||||
|
su=su,
|
||||||
|
executable=executable,
|
||||||
|
in_data=in_data)
|
||||||
|
else:
|
||||||
|
rc, stdin, stdout, stderr = conn.exec_command(cmd,
|
||||||
|
tmp,
|
||||||
|
sudo_user,
|
||||||
|
sudoable=sudoable,
|
||||||
|
executable=executable,
|
||||||
|
in_data=in_data)
|
||||||
|
|
||||||
if type(stdout) not in [ str, unicode ]:
|
if type(stdout) not in [ str, unicode ]:
|
||||||
out = ''.join(stdout.readlines())
|
out = ''.join(stdout.readlines())
|
||||||
|
@ -881,11 +913,11 @@ class Runner(object):
|
||||||
|
|
||||||
basefile = 'ansible-tmp-%s-%s' % (time.time(), random.randint(0, 2**48))
|
basefile = 'ansible-tmp-%s-%s' % (time.time(), random.randint(0, 2**48))
|
||||||
basetmp = os.path.join(C.DEFAULT_REMOTE_TMP, basefile)
|
basetmp = os.path.join(C.DEFAULT_REMOTE_TMP, basefile)
|
||||||
if self.sudo and self.sudo_user != 'root' and basetmp.startswith('$HOME'):
|
if (self.sudo or self.su) and (self.sudo_user != 'root' or self.su != 'root') and basetmp.startswith('$HOME'):
|
||||||
basetmp = os.path.join('/tmp', basefile)
|
basetmp = os.path.join('/tmp', basefile)
|
||||||
|
|
||||||
cmd = 'mkdir -p %s' % basetmp
|
cmd = 'mkdir -p %s' % basetmp
|
||||||
if self.remote_user != 'root' or (self.sudo and self.sudo_user != 'root'):
|
if self.remote_user != 'root' or ((self.sudo or self.su) and (self.sudo_user != 'root' or self.su != 'root')):
|
||||||
cmd += ' && chmod a+rx %s' % basetmp
|
cmd += ' && chmod a+rx %s' % basetmp
|
||||||
cmd += ' && echo %s' % basetmp
|
cmd += ' && echo %s' % basetmp
|
||||||
|
|
||||||
|
|
|
@ -159,9 +159,12 @@ class Connection(object):
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
raise errors.AnsibleError("timed out while waiting to receive data")
|
raise errors.AnsibleError("timed out while waiting to receive data")
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False, executable='/bin/sh', in_data=None):
|
def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su=None, su_user=None):
|
||||||
''' run a command on the remote host '''
|
''' run a command on the remote host '''
|
||||||
|
|
||||||
|
if su or su_user:
|
||||||
|
raise errors.AnsibleError("Internal Error: this module does not support running commands via su")
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||||
|
|
||||||
|
|
|
@ -60,9 +60,12 @@ class Connection(object):
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False, executable='/bin/sh', in_data=None):
|
def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su=None, su_user=None):
|
||||||
''' run a command on the chroot '''
|
''' run a command on the chroot '''
|
||||||
|
|
||||||
|
if su or su_user:
|
||||||
|
raise errors.AnsibleError("Internal Error: this module does not support running commands via su")
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ class Connection(object):
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False, executable='/bin/sh', in_data=None):
|
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False, executable='/bin/sh', in_data=None, su_user=None, su=None):
|
||||||
''' run a command on the remote host '''
|
''' run a command on the remote host '''
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
|
@ -76,9 +76,9 @@ class Connection(object):
|
||||||
|
|
||||||
vvv("EXEC COMMAND %s" % cmd)
|
vvv("EXEC COMMAND %s" % cmd)
|
||||||
|
|
||||||
if self.runner.sudo and sudoable:
|
if (self.runner.sudo and sudoable) or (self.runner.su and su):
|
||||||
raise errors.AnsibleError(
|
raise errors.AnsibleError(
|
||||||
"When using fireball, do not specify sudo to run your tasks. " +
|
"When using fireball, do not specify sudo or su to run your tasks. " +
|
||||||
"Instead sudo the fireball action with sudo. " +
|
"Instead sudo the fireball action with sudo. " +
|
||||||
"Task will communicate with the fireball already running in sudo mode."
|
"Task will communicate with the fireball already running in sudo mode."
|
||||||
)
|
)
|
||||||
|
|
|
@ -53,10 +53,13 @@ class Connection(object):
|
||||||
self.client = fc.Client(self.host)
|
self.client = fc.Client(self.host)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False,
|
def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False,
|
||||||
executable='/bin/sh', in_data=None):
|
executable='/bin/sh', in_data=None, su=None, su_user=None):
|
||||||
''' run a command on the remote minion '''
|
''' run a command on the remote minion '''
|
||||||
|
|
||||||
|
if su or su_user:
|
||||||
|
raise errors.AnsibleError("Internal Error: this module does not support running commands via su")
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||||
|
|
||||||
|
|
|
@ -91,9 +91,12 @@ class Connection(object):
|
||||||
local_cmd = '%s "%s" %s' % (self.jexec_cmd, self.jail, cmd)
|
local_cmd = '%s "%s" %s' % (self.jexec_cmd, self.jail, cmd)
|
||||||
return local_cmd
|
return local_cmd
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False, executable='/bin/sh', in_data=None):
|
def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su=None, su_user=None):
|
||||||
''' run a command on the chroot '''
|
''' run a command on the chroot '''
|
||||||
|
|
||||||
|
if su or su_user:
|
||||||
|
raise errors.AnsibleError("Internal Error: this module does not support running commands via su")
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||||
|
|
||||||
|
|
|
@ -41,9 +41,13 @@ class Connection(object):
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False, executable='/bin/sh', in_data=None):
|
def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su=None, su_user=None):
|
||||||
''' run a command on the local host '''
|
''' run a command on the local host '''
|
||||||
|
|
||||||
|
# su requires to be run from a terminal, and therefore isn't supported here (yet?)
|
||||||
|
if su or su_user:
|
||||||
|
raise errors.AnsibleError("Internal Error: this module does not support running commands via su")
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||||
|
|
||||||
|
|
|
@ -176,7 +176,7 @@ class Connection(object):
|
||||||
|
|
||||||
return ssh
|
return ssh
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False, executable='/bin/sh', in_data=None):
|
def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su=None, su_user=None):
|
||||||
''' run a command on the remote host '''
|
''' run a command on the remote host '''
|
||||||
|
|
||||||
bufsize = 4096
|
bufsize = 4096
|
||||||
|
@ -188,7 +188,7 @@ class Connection(object):
|
||||||
msg += ": %s" % str(e)
|
msg += ": %s" % str(e)
|
||||||
raise errors.AnsibleConnectionFailed(msg)
|
raise errors.AnsibleConnectionFailed(msg)
|
||||||
|
|
||||||
if not self.runner.sudo or not sudoable or in_data:
|
if not (self.runner.sudo and sudoable) and not (self.runner.su and su) or in_data:
|
||||||
if executable:
|
if executable:
|
||||||
quoted_command = executable + ' -c ' + pipes.quote(cmd)
|
quoted_command = executable + ' -c ' + pipes.quote(cmd)
|
||||||
else:
|
else:
|
||||||
|
@ -208,7 +208,7 @@ class Connection(object):
|
||||||
sudo_output = ''
|
sudo_output = ''
|
||||||
try:
|
try:
|
||||||
chan.exec_command(shcmd)
|
chan.exec_command(shcmd)
|
||||||
if self.runner.sudo_pass:
|
if self.runner.sudo_pass or self.runner.su_pass:
|
||||||
while not sudo_output.endswith(prompt) and success_key not in sudo_output:
|
while not sudo_output.endswith(prompt) and success_key not in sudo_output:
|
||||||
chunk = chan.recv(bufsize)
|
chunk = chan.recv(bufsize)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
|
@ -220,7 +220,10 @@ class Connection(object):
|
||||||
'closed waiting for password prompt')
|
'closed waiting for password prompt')
|
||||||
sudo_output += chunk
|
sudo_output += chunk
|
||||||
if success_key not in sudo_output:
|
if success_key not in sudo_output:
|
||||||
|
if sudoable:
|
||||||
chan.sendall(self.runner.sudo_pass + '\n')
|
chan.sendall(self.runner.sudo_pass + '\n')
|
||||||
|
elif su:
|
||||||
|
chan.sendall(self.runner.su_pass + '\n')
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
raise errors.AnsibleError('ssh timed out waiting for sudo.\n' + sudo_output)
|
raise errors.AnsibleError('ssh timed out waiting for sudo.\n' + sudo_output)
|
||||||
|
|
||||||
|
|
|
@ -176,7 +176,7 @@ class Connection(object):
|
||||||
|
|
||||||
return ssh
|
return ssh
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False, executable='/bin/sh', in_data=None):
|
def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su=None, su_user=None):
|
||||||
''' run a command on the remote host '''
|
''' run a command on the remote host '''
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
|
@ -191,7 +191,7 @@ class Connection(object):
|
||||||
msg += ": %s" % str(e)
|
msg += ": %s" % str(e)
|
||||||
raise errors.AnsibleConnectionFailed(msg)
|
raise errors.AnsibleConnectionFailed(msg)
|
||||||
|
|
||||||
if not self.runner.sudo or not sudoable:
|
if not (self.runner.sudo and sudoable) and not (self.runner.su and su):
|
||||||
if executable:
|
if executable:
|
||||||
quoted_command = executable + ' -c ' + pipes.quote(cmd)
|
quoted_command = executable + ' -c ' + pipes.quote(cmd)
|
||||||
else:
|
else:
|
||||||
|
@ -206,12 +206,15 @@ class Connection(object):
|
||||||
chan.get_pty(term=os.getenv('TERM', 'vt100'),
|
chan.get_pty(term=os.getenv('TERM', 'vt100'),
|
||||||
width=int(os.getenv('COLUMNS', 0)),
|
width=int(os.getenv('COLUMNS', 0)),
|
||||||
height=int(os.getenv('LINES', 0)))
|
height=int(os.getenv('LINES', 0)))
|
||||||
|
if self.runner.sudo or sudoable:
|
||||||
shcmd, prompt, success_key = utils.make_sudo_cmd(sudo_user, executable, cmd)
|
shcmd, prompt, success_key = utils.make_sudo_cmd(sudo_user, executable, cmd)
|
||||||
|
elif self.runner.su or su:
|
||||||
|
shcmd, prompt, success_key = utils.make_su_cmd(su_user, executable, cmd)
|
||||||
vvv("EXEC %s" % shcmd, host=self.host)
|
vvv("EXEC %s" % shcmd, host=self.host)
|
||||||
sudo_output = ''
|
sudo_output = ''
|
||||||
try:
|
try:
|
||||||
chan.exec_command(shcmd)
|
chan.exec_command(shcmd)
|
||||||
if self.runner.sudo_pass:
|
if self.runner.sudo_pass or self.runner.su_pass:
|
||||||
while not sudo_output.endswith(prompt) and success_key not in sudo_output:
|
while not sudo_output.endswith(prompt) and success_key not in sudo_output:
|
||||||
chunk = chan.recv(bufsize)
|
chunk = chan.recv(bufsize)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
|
@ -223,7 +226,10 @@ class Connection(object):
|
||||||
'closed waiting for password prompt')
|
'closed waiting for password prompt')
|
||||||
sudo_output += chunk
|
sudo_output += chunk
|
||||||
if success_key not in sudo_output:
|
if success_key not in sudo_output:
|
||||||
|
if sudoable:
|
||||||
chan.sendall(self.runner.sudo_pass + '\n')
|
chan.sendall(self.runner.sudo_pass + '\n')
|
||||||
|
elif su:
|
||||||
|
chan.sendall(self.runner.su_pass + '\n')
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
raise errors.AnsibleError('ssh timed out waiting for sudo.\n' + sudo_output)
|
raise errors.AnsibleError('ssh timed out waiting for sudo.\n' + sudo_output)
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@ class Connection(object):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, sudo_user,sudoable=False, executable='/bin/sh', in_data=None):
|
def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su_user=None, su=False):
|
||||||
''' run a command on the remote host '''
|
''' run a command on the remote host '''
|
||||||
|
|
||||||
ssh_cmd = self._password_cmd()
|
ssh_cmd = self._password_cmd()
|
||||||
|
@ -165,7 +165,10 @@ class Connection(object):
|
||||||
ssh_cmd += ['-6']
|
ssh_cmd += ['-6']
|
||||||
ssh_cmd += [self.host]
|
ssh_cmd += [self.host]
|
||||||
|
|
||||||
if not self.runner.sudo or not sudoable:
|
if su and su_user:
|
||||||
|
sudocmd, prompt, success_key = utils.make_su_cmd(su_user, executable, cmd)
|
||||||
|
ssh_cmd.append(sudocmd)
|
||||||
|
elif not self.runner.sudo or not sudoable:
|
||||||
if executable:
|
if executable:
|
||||||
ssh_cmd.append(executable + ' -c ' + pipes.quote(cmd))
|
ssh_cmd.append(executable + ' -c ' + pipes.quote(cmd))
|
||||||
else:
|
else:
|
||||||
|
@ -206,7 +209,8 @@ class Connection(object):
|
||||||
|
|
||||||
self._send_password()
|
self._send_password()
|
||||||
|
|
||||||
if self.runner.sudo and sudoable and self.runner.sudo_pass:
|
if (self.runner.sudo and sudoable and self.runner.sudo_pass) or \
|
||||||
|
(self.runner.su and su and self.runner.su_pass):
|
||||||
# several cases are handled for sudo privileges with password
|
# several cases are handled for sudo privileges with password
|
||||||
# * NOPASSWD (tty & no-tty): detect success_key on stdout
|
# * NOPASSWD (tty & no-tty): detect success_key on stdout
|
||||||
# * without NOPASSWD:
|
# * without NOPASSWD:
|
||||||
|
@ -225,7 +229,7 @@ class Connection(object):
|
||||||
if p.stderr in rfd:
|
if p.stderr in rfd:
|
||||||
chunk = p.stderr.read()
|
chunk = p.stderr.read()
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise errors.AnsibleError('ssh connection closed waiting for sudo password prompt')
|
raise errors.AnsibleError('ssh connection closed waiting for sudo or su password prompt')
|
||||||
sudo_errput += chunk
|
sudo_errput += chunk
|
||||||
incorrect_password = gettext.dgettext(
|
incorrect_password = gettext.dgettext(
|
||||||
"sudo", "Sorry, try again.")
|
"sudo", "Sorry, try again.")
|
||||||
|
@ -237,16 +241,19 @@ class Connection(object):
|
||||||
if p.stdout in rfd:
|
if p.stdout in rfd:
|
||||||
chunk = p.stdout.read()
|
chunk = p.stdout.read()
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise errors.AnsibleError('ssh connection closed waiting for sudo password prompt')
|
raise errors.AnsibleError('ssh connection closed waiting for sudo or su password prompt')
|
||||||
sudo_output += chunk
|
sudo_output += chunk
|
||||||
|
|
||||||
if not rfd:
|
if not rfd:
|
||||||
# timeout. wrap up process communication
|
# timeout. wrap up process communication
|
||||||
stdout = p.communicate()
|
stdout = p.communicate()
|
||||||
raise errors.AnsibleError('ssh connection error waiting for sudo password prompt')
|
raise errors.AnsibleError('ssh connection error waiting for sudo or su password prompt')
|
||||||
|
|
||||||
if success_key not in sudo_output:
|
if success_key not in sudo_output:
|
||||||
|
if sudoable:
|
||||||
stdin.write(self.runner.sudo_pass + '\n')
|
stdin.write(self.runner.sudo_pass + '\n')
|
||||||
|
elif su:
|
||||||
|
stdin.write(self.runner.su_pass + '\n')
|
||||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
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)
|
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||||
# We can't use p.communicate here because the ControlMaster may have stdout open as well
|
# We can't use p.communicate here because the ControlMaster may have stdout open as well
|
||||||
|
@ -262,13 +269,19 @@ class Connection(object):
|
||||||
while True:
|
while True:
|
||||||
rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
|
rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
|
||||||
|
|
||||||
# fail early if the sudo password is wrong
|
# fail early if the sudo/su password is wrong
|
||||||
if self.runner.sudo and sudoable and self.runner.sudo_pass:
|
if self.runner.sudo and sudoable and self.runner.sudo_pass:
|
||||||
incorrect_password = gettext.dgettext(
|
incorrect_password = gettext.dgettext(
|
||||||
"sudo", "Sorry, try again.")
|
"sudo", "Sorry, try again.")
|
||||||
if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
|
if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
|
||||||
raise errors.AnsibleError('Incorrect sudo password')
|
raise errors.AnsibleError('Incorrect sudo password')
|
||||||
|
|
||||||
|
if self.runner.su and su and self.runner.sudo_pass:
|
||||||
|
incorrect_password = gettext.dgettext(
|
||||||
|
"su", "Sorry")
|
||||||
|
if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
|
||||||
|
raise errors.AnsibleError('Incorrect su password')
|
||||||
|
|
||||||
if p.stdout in rfd:
|
if p.stdout in rfd:
|
||||||
dat = os.read(p.stdout.fileno(), 9000)
|
dat = os.read(p.stdout.fileno(), 9000)
|
||||||
stdout += dat
|
stdout += dat
|
||||||
|
|
|
@ -145,7 +145,7 @@ class Connection(object):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, sudo_user,sudoable=False, executable='/bin/sh', in_data=None):
|
def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su=False, su_user=None):
|
||||||
''' run a command on the remote host '''
|
''' run a command on the remote host '''
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
|
@ -163,7 +163,10 @@ class Connection(object):
|
||||||
ssh_cmd += ['-6']
|
ssh_cmd += ['-6']
|
||||||
ssh_cmd += [self.host]
|
ssh_cmd += [self.host]
|
||||||
|
|
||||||
if not self.runner.sudo or not sudoable:
|
if su and su_user:
|
||||||
|
sudocmd, prompt, success_key = utils.make_su_cmd(su_user, executable, cmd)
|
||||||
|
ssh_cmd.append(sudocmd)
|
||||||
|
elif not self.runner.sudo or not sudoable:
|
||||||
if executable:
|
if executable:
|
||||||
ssh_cmd.append(executable + ' -c ' + pipes.quote(cmd))
|
ssh_cmd.append(executable + ' -c ' + pipes.quote(cmd))
|
||||||
else:
|
else:
|
||||||
|
@ -183,7 +186,6 @@ class Connection(object):
|
||||||
fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
|
fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Make sure stdin is a proper (pseudo) pty to avoid: tcgetattr errors
|
# Make sure stdin is a proper (pseudo) pty to avoid: tcgetattr errors
|
||||||
master, slave = pty.openpty()
|
master, slave = pty.openpty()
|
||||||
|
@ -198,7 +200,8 @@ class Connection(object):
|
||||||
|
|
||||||
self._send_password()
|
self._send_password()
|
||||||
|
|
||||||
if self.runner.sudo and sudoable and self.runner.sudo_pass:
|
if (self.runner.sudo and sudoable and self.runner.sudo_pass) or \
|
||||||
|
(self.runner.su and su and self.runner.su_pass):
|
||||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL,
|
fcntl.fcntl(p.stdout, fcntl.F_SETFL,
|
||||||
fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
|
fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||||
sudo_output = ''
|
sudo_output = ''
|
||||||
|
@ -208,13 +211,17 @@ class Connection(object):
|
||||||
if p.stdout in rfd:
|
if p.stdout in rfd:
|
||||||
chunk = p.stdout.read()
|
chunk = p.stdout.read()
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise errors.AnsibleError('ssh connection closed waiting for sudo password prompt')
|
raise errors.AnsibleError('ssh connection closed waiting for sudo or su password prompt')
|
||||||
sudo_output += chunk
|
sudo_output += chunk
|
||||||
else:
|
else:
|
||||||
stdout = p.communicate()
|
stdout = p.communicate()
|
||||||
raise errors.AnsibleError('ssh connection error waiting for sudo password prompt')
|
raise errors.AnsibleError('ssh connection error waiting for sudo or su password prompt')
|
||||||
|
|
||||||
if success_key not in sudo_output:
|
if success_key not in sudo_output:
|
||||||
|
if sudoable:
|
||||||
stdin.write(self.runner.sudo_pass + '\n')
|
stdin.write(self.runner.sudo_pass + '\n')
|
||||||
|
elif su:
|
||||||
|
stdin.write(self.runner.su_pass + '\n')
|
||||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||||
|
|
||||||
# We can't use p.communicate here because the ControlMaster may have stdout open as well
|
# We can't use p.communicate here because the ControlMaster may have stdout open as well
|
||||||
|
@ -224,13 +231,19 @@ class Connection(object):
|
||||||
while True:
|
while True:
|
||||||
rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
|
rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
|
||||||
|
|
||||||
# fail early if the sudo password is wrong
|
# fail early if the sudo/su password is wrong
|
||||||
if self.runner.sudo and sudoable and self.runner.sudo_pass:
|
if self.runner.sudo and sudoable and self.runner.sudo_pass:
|
||||||
incorrect_password = gettext.dgettext(
|
incorrect_password = gettext.dgettext(
|
||||||
"sudo", "Sorry, try again.")
|
"sudo", "Sorry, try again.")
|
||||||
if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
|
if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
|
||||||
raise errors.AnsibleError('Incorrect sudo password')
|
raise errors.AnsibleError('Incorrect sudo password')
|
||||||
|
|
||||||
|
if self.runner.su and su and self.runner.su_pass:
|
||||||
|
incorrect_password = gettext.dgettext(
|
||||||
|
"su", "su: Authentication failure")
|
||||||
|
if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
|
||||||
|
raise errors.AnsibleError('Incorrect su password')
|
||||||
|
|
||||||
if p.stdout in rfd:
|
if p.stdout in rfd:
|
||||||
dat = os.read(p.stdout.fileno(), 9000)
|
dat = os.read(p.stdout.fileno(), 9000)
|
||||||
stdout += dat
|
stdout += dat
|
||||||
|
|
|
@ -645,6 +645,8 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False,
|
||||||
help='use this file to authenticate the connection')
|
help='use this file to authenticate the connection')
|
||||||
parser.add_option('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true',
|
parser.add_option('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true',
|
||||||
help='ask for sudo password')
|
help='ask for sudo password')
|
||||||
|
parser.add_option('--ask-su-pass', default=False, dest='ask_su_pass',
|
||||||
|
action='store_true', help='ask for su password')
|
||||||
parser.add_option('--list-hosts', dest='listhosts', action='store_true',
|
parser.add_option('--list-hosts', dest='listhosts', action='store_true',
|
||||||
help='outputs a list of matching hosts; does not execute anything else')
|
help='outputs a list of matching hosts; does not execute anything else')
|
||||||
parser.add_option('-M', '--module-path', dest='module_path',
|
parser.add_option('-M', '--module-path', dest='module_path',
|
||||||
|
@ -668,11 +670,15 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False,
|
||||||
if runas_opts:
|
if runas_opts:
|
||||||
parser.add_option("-s", "--sudo", default=constants.DEFAULT_SUDO, action="store_true",
|
parser.add_option("-s", "--sudo", default=constants.DEFAULT_SUDO, action="store_true",
|
||||||
dest='sudo', help="run operations with sudo (nopasswd)")
|
dest='sudo', help="run operations with sudo (nopasswd)")
|
||||||
parser.add_option('-U', '--sudo-user', dest='sudo_user', help='desired sudo user (default=root)',
|
parser.add_option('-U', '--sudo-user', dest='sudo_user', default=None,
|
||||||
default=None) # Can't default to root because we need to detect when this option was given
|
help='desired sudo user (default=root)') # Can't default to root because we need to detect when this option was given
|
||||||
parser.add_option('-u', '--user', default=constants.DEFAULT_REMOTE_USER,
|
parser.add_option('-u', '--user', default=constants.DEFAULT_REMOTE_USER,
|
||||||
dest='remote_user',
|
dest='remote_user', help='connect as this user (default=%s)' % constants.DEFAULT_REMOTE_USER)
|
||||||
help='connect as this user (default=%s)' % constants.DEFAULT_REMOTE_USER)
|
|
||||||
|
parser.add_option('-S', '--su', default=constants.DEFAULT_SU,
|
||||||
|
action='store_true', help='run operations with su')
|
||||||
|
parser.add_option('-R', '--su-user', help='run operations with su as this '
|
||||||
|
'user (default=%s)' % constants.DEFAULT_SU_USER)
|
||||||
|
|
||||||
if connect_opts:
|
if connect_opts:
|
||||||
parser.add_option('-c', '--connection', dest='connection',
|
parser.add_option('-c', '--connection', dest='connection',
|
||||||
|
@ -699,10 +705,12 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False,
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def ask_passwords(ask_pass=False, ask_sudo_pass=False):
|
def ask_passwords(ask_pass=False, ask_sudo_pass=False, ask_su_pass=False):
|
||||||
sshpass = None
|
sshpass = None
|
||||||
sudopass = None
|
sudopass = None
|
||||||
|
su_pass = None
|
||||||
sudo_prompt = "sudo password: "
|
sudo_prompt = "sudo password: "
|
||||||
|
su_prompt = "su password: "
|
||||||
|
|
||||||
if ask_pass:
|
if ask_pass:
|
||||||
sshpass = getpass.getpass(prompt="SSH password: ")
|
sshpass = getpass.getpass(prompt="SSH password: ")
|
||||||
|
@ -713,7 +721,10 @@ def ask_passwords(ask_pass=False, ask_sudo_pass=False):
|
||||||
if ask_pass and sudopass == '':
|
if ask_pass and sudopass == '':
|
||||||
sudopass = sshpass
|
sudopass = sshpass
|
||||||
|
|
||||||
return (sshpass, sudopass)
|
if ask_su_pass:
|
||||||
|
su_pass = getpass.getpass(prompt=su_prompt)
|
||||||
|
|
||||||
|
return (sshpass, sudopass, su_pass)
|
||||||
|
|
||||||
def do_encrypt(result, encrypt, salt_size=None, salt=None):
|
def do_encrypt(result, encrypt, salt_size=None, salt=None):
|
||||||
if PASSLIB_AVAILABLE:
|
if PASSLIB_AVAILABLE:
|
||||||
|
@ -786,6 +797,21 @@ def make_sudo_cmd(sudo_user, executable, cmd):
|
||||||
prompt, sudo_user, executable or '$SHELL', pipes.quote('echo %s; %s' % (success_key, cmd)))
|
prompt, sudo_user, executable or '$SHELL', pipes.quote('echo %s; %s' % (success_key, cmd)))
|
||||||
return ('/bin/sh -c ' + pipes.quote(sudocmd), prompt, success_key)
|
return ('/bin/sh -c ' + pipes.quote(sudocmd), prompt, success_key)
|
||||||
|
|
||||||
|
|
||||||
|
def make_su_cmd(su_user, executable, cmd):
|
||||||
|
"""
|
||||||
|
Helper function for connection plugins to create direct su commands
|
||||||
|
"""
|
||||||
|
# TODO: work on this function
|
||||||
|
randbits = ''.join(chr(random.randint(ord('a'), ord('z'))) for x in xrange(32))
|
||||||
|
prompt = 'assword: '
|
||||||
|
success_key = 'SUDO-SUCCESS-%s' % randbits
|
||||||
|
sudocmd = '%s %s %s %s -c %s' % (
|
||||||
|
C.DEFAULT_SU_EXE, C.DEFAULT_SU_FLAGS, su_user, executable or '$SHELL',
|
||||||
|
pipes.quote('echo %s; %s' % (success_key, cmd))
|
||||||
|
)
|
||||||
|
return ('/bin/sh -c ' + pipes.quote(sudocmd), prompt, success_key)
|
||||||
|
|
||||||
_TO_UNICODE_TYPES = (unicode, type(None))
|
_TO_UNICODE_TYPES = (unicode, type(None))
|
||||||
|
|
||||||
def to_unicode(value):
|
def to_unicode(value):
|
||||||
|
|
Loading…
Reference in a new issue