From 4c84ba74b049986926fafa42328a5b441987ea7a Mon Sep 17 00:00:00 2001 From: Paul Durivage Date: Mon, 20 Jan 2014 18:12:16 -0600 Subject: [PATCH 1/2] Resolve su bug in paramiko libs --- lib/ansible/runner/connection_plugins/paramiko_alt.py | 11 +++++++---- lib/ansible/runner/connection_plugins/paramiko_ssh.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/ansible/runner/connection_plugins/paramiko_alt.py b/lib/ansible/runner/connection_plugins/paramiko_alt.py index fbba732631..1828ba1ab2 100644 --- a/lib/ansible/runner/connection_plugins/paramiko_alt.py +++ b/lib/ansible/runner/connection_plugins/paramiko_alt.py @@ -176,7 +176,7 @@ class Connection(object): 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 ''' bufsize = 4096 @@ -188,7 +188,7 @@ class Connection(object): msg += ": %s" % str(e) 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: quoted_command = executable + ' -c ' + pipes.quote(cmd) else: @@ -208,7 +208,7 @@ class Connection(object): sudo_output = '' try: 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: chunk = chan.recv(bufsize) if not chunk: @@ -220,7 +220,10 @@ class Connection(object): 'closed waiting for password prompt') sudo_output += chunk if success_key not in sudo_output: - chan.sendall(self.runner.sudo_pass + '\n') + if sudoable: + chan.sendall(self.runner.sudo_pass + '\n') + elif su: + chan.sendall(self.runner.su_pass + '\n') except socket.timeout: raise errors.AnsibleError('ssh timed out waiting for sudo.\n' + sudo_output) diff --git a/lib/ansible/runner/connection_plugins/paramiko_ssh.py b/lib/ansible/runner/connection_plugins/paramiko_ssh.py index 667e03c0e8..a5dea39f0f 100644 --- a/lib/ansible/runner/connection_plugins/paramiko_ssh.py +++ b/lib/ansible/runner/connection_plugins/paramiko_ssh.py @@ -191,7 +191,7 @@ class Connection(object): msg += ": %s" % str(e) 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: quoted_command = executable + ' -c ' + pipes.quote(cmd) else: From f72f5a20df22c9231191d15502a606cef7f2f287 Mon Sep 17 00:00:00 2001 From: Paul Durivage Date: Mon, 20 Jan 2014 19:19:03 -0600 Subject: [PATCH 2/2] Revert "Revert "Merge pull request #5325 from angstwad/add-su-support"" This reverts commit c17d0e0357f1f3bdc7389eaa3171444fccda8b76. Conflicts: lib/ansible/runner/connection_plugins/paramiko_ssh.py --- bin/ansible | 41 +++++++-- bin/ansible-playbook | 18 +++- lib/ansible/constants.py | 6 ++ lib/ansible/playbook/__init__.py | 85 ++++++++++++++----- lib/ansible/playbook/play.py | 20 +++-- lib/ansible/playbook/task.py | 20 +++-- lib/ansible/runner/__init__.py | 58 ++++++++++--- .../runner/connection_plugins/accelerate.py | 5 +- .../runner/connection_plugins/chroot.py | 5 +- .../runner/connection_plugins/fireball.py | 6 +- .../runner/connection_plugins/funcd.py | 7 +- lib/ansible/runner/connection_plugins/jail.py | 5 +- .../runner/connection_plugins/local.py | 6 +- .../runner/connection_plugins/paramiko_ssh.py | 14 ++- lib/ansible/runner/connection_plugins/ssh.py | 33 ++++--- .../runner/connection_plugins/ssh_old.py | 31 +++++-- lib/ansible/utils/__init__.py | 38 +++++++-- 17 files changed, 304 insertions(+), 94 deletions(-) diff --git a/bin/ansible b/bin/ansible index 3d4cb07ad9..a0464c142a 100755 --- a/bin/ansible +++ b/bin/ansible @@ -67,6 +67,14 @@ class Cli(object): if len(args) == 0 or len(args) > 1: parser.print_help() 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) # ---------------------------------------------- @@ -96,31 +104,46 @@ class Cli(object): sshpass = None sudopass = None + su_pass = None options.ask_pass = options.ask_pass or C.DEFAULT_ASK_PASS # Never ask for an SSH password when we run with local connection if options.connection == "local": options.ask_pass = False 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) - if options.sudo_user or 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) + 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_user = options.sudo_user or C.DEFAULT_SUDO_USER + options.su_user = options.su_user or C.DEFAULT_SU_USER if options.tree: utils.prepare_writeable_dir(options.tree) + 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, - remote_user=options.remote_user, remote_pass=sshpass, - inventory=inventory_manager, timeout=options.timeout, + remote_user=options.remote_user, + remote_pass=sshpass, + inventory=inventory_manager, + timeout=options.timeout, private_key_file=options.private_key_file, forks=options.forks, pattern=pattern, - callbacks=self.callbacks, sudo=options.sudo, - sudo_pass=sudopass,sudo_user=options.sudo_user, - transport=options.connection, subset=options.subset, + callbacks=self.callbacks, + sudo=options.sudo, + sudo_pass=sudopass, + sudo_user=options.sudo_user, + transport=options.connection, + subset=options.subset, check=options.check, - diff=options.check + diff=options.check, + su=options.su, + su_pass=su_pass, + su_user=options.su_user ) if options.seconds: diff --git a/bin/ansible-playbook b/bin/ansible-playbook index c0db66993c..646b64a764 100755 --- a/bin/ansible-playbook +++ b/bin/ansible-playbook @@ -83,6 +83,13 @@ def main(args): parser.print_help(file=sys.stderr) 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.subset(options.subset) if len(inventory.list_hosts()) == 0: @@ -90,14 +97,18 @@ def main(args): sshpass = None sudopass = None + su_pass = None if not options.listhosts and not options.syntax and not options.listtasks: options.ask_pass = options.ask_pass or C.DEFAULT_ASK_PASS # Never ask for an SSH password when we run with local connection if options.connection == "local": options.ask_pass = False 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.su_user = options.su_user or C.DEFAULT_SU_USER + extra_vars = {} for extra_vars_opt in options.extra_vars: @@ -156,7 +167,10 @@ def main(args): only_tags=only_tags, skip_tags=skip_tags, 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: diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index bf9084ef99..2429aa4e92 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -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_JINJA2_EXTENSIONS = get_config(p, DEFAULTS, 'jinja2_extensions', 'ANSIBLE_JINJA2_EXTENSIONS', None) 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_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_REMOTE_PASS = None DEFAULT_SUBSET = None +DEFAULT_SU_PASS = None diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index dc7991aaf7..c33b290b81 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -68,7 +68,11 @@ class PlayBook(object): inventory = None, check = 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 @@ -122,6 +126,9 @@ class PlayBook(object): self.only_tags = only_tags self.skip_tags = skip_tags 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.runner_callbacks.playbook = self @@ -303,20 +310,39 @@ class PlayBook(object): self.inventory.restrict_to(hosts) runner = ansible.runner.Runner( - pattern=task.play.hosts, inventory=self.inventory, module_name=task.module_name, - module_args=task.module_args, forks=self.forks, - remote_pass=self.remote_pass, module_path=self.module_path, - timeout=self.timeout, remote_user=task.remote_user, - remote_port=task.play.remote_port, module_vars=task.module_vars, - 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, + pattern=task.play.hosts, + inventory=self.inventory, + module_name=task.module_name, + module_args=task.module_args, + forks=self.forks, + remote_pass=self.remote_pass, + module_path=self.module_path, + timeout=self.timeout, + remote_user=task.remote_user, + remote_port=task.play.remote_port, + module_vars=task.module_vars, + 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, - 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: @@ -446,13 +472,30 @@ class PlayBook(object): # push any variables down to the system setup_results = ansible.runner.Runner( - pattern=play.hosts, module_name='setup', module_args={}, inventory=self.inventory, - forks=self.forks, module_path=self.module_path, 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, + pattern=play.hosts, + module_name='setup', + module_args={}, + inventory=self.inventory, + forks=self.forks, + module_path=self.module_path, + 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() self.stats.compute(setup_results, setup=True) diff --git a/lib/ansible/playbook/play.py b/lib/ansible/playbook/play.py index 92a40ba6ec..a1651f5f3c 100644 --- a/lib/ansible/playbook/play.py +++ b/lib/ansible/playbook/play.py @@ -34,7 +34,7 @@ class Play(object): 'handlers', 'remote_user', 'remote_port', 'included_roles', 'accelerate', 'accelerate_port', 'accelerate_ipv6', 'sudo', 'sudo_user', 'transport', 'playbook', '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 @@ -43,7 +43,8 @@ class Play(object): 'hosts', 'name', 'vars', 'vars_prompt', 'vars_files', 'tasks', 'handlers', 'remote_user', 'user', 'port', 'include', 'accelerate', 'accelerate_port', 'accelerate_ipv6', '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_ipv6 = ds.get('accelerate_ipv6', False) 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['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 ''' results = [] @@ -469,7 +473,7 @@ class Play(object): if 'meta' in x: if x['meta'] == 'flush_handlers': - results.append(Task(self,x)) + results.append(Task(self, x)) continue 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) results += loaded 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) else: raise Exception("unexpected task type") diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index df496e11a8..0350ca2ac2 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -30,7 +30,8 @@ class Task(object): 'delegate_to', 'first_available_file', 'ignore_errors', 'local_action', 'transport', 'sudo', 'remote_user', 'sudo_user', 'sudo_pass', '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 @@ -39,7 +40,8 @@ class Task(object): 'first_available_file', 'include', 'tags', 'register', 'ignore_errors', 'delegate_to', 'local_action', 'transport', 'remote_user', 'sudo', 'sudo_user', '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): @@ -117,6 +119,7 @@ class Task(object): self.tags = [ 'all' ] self.register = ds.get('register', None) self.sudo = utils.boolean(ds.get('sudo', play.sudo)) + self.su = utils.boolean(ds.get('sudo', play.su)) self.environment = ds.get('environment', {}) self.role_name = role_name @@ -142,13 +145,18 @@ class Task(object): else: 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: self.sudo_user = ds.get('sudo_user', play.sudo_user) self.sudo_pass = ds.get('sudo_pass', play.playbook.sudo_pass) - else: - self.sudo_user = None - self.sudo_pass = None - + elif self.su: + self.su_user = ds.get('su_user', play.su_user) + self.su_pass = ds.get('su_pass', play.playbook.su_pass) + # Both are defined if ('action' in ds) and ('local_action' in ds): raise errors.AnsibleError("the 'action' and 'local_action' attributes can not be used together") diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index ef8d4aac21..3108bc5e30 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -141,6 +141,9 @@ class Runner(object): accelerate=False, # use accelerated connection accelerate_ipv6=False, # accelerated connection w/ IPv6 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 @@ -188,6 +191,9 @@ class Runner(object): self.accelerate_ipv6 = accelerate_ipv6 self.callbacks.runner = self self.original_transport = self.transport + self.su = su + self.su_user = su_user + self.su_pass = su_pass if self.transport == 'smart': # 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 not conn.has_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) 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 cmd_chmod = "chmod a+r %s" % remote_module_path self._low_level_exec_command(conn, cmd_chmod, tmp, sudoable=False) @@ -343,7 +350,7 @@ class Runner(object): else: 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 cmd_args_chmod = "chmod a+r %s" % argsfile 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]]) else: 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 else: cmd = "%s" % (remote_module_path) @@ -369,7 +376,7 @@ class Runner(object): cmd = cmd.strip() 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 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 sudoable = False - res = self._low_level_exec_command(conn, cmd, tmp, sudoable=sudoable, in_data=in_data) + 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) 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 # have to clean up temp files as original user in a second step 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_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.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: actual_private_key_file = os.path.expanduser(actual_private_key_file) @@ -798,7 +811,10 @@ class Runner(object): if tmp.find("tmp") != -1: # tmp has already been created 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 # or we want to keep the files on the target system 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 ''' if executable is None: executable = C.DEFAULT_EXECUTABLE sudo_user = self.sudo_user + su_user = self.su_user # compare connection user to sudo_user and disable if the same if hasattr(conn, 'user'): - if conn.user == sudo_user: + if conn.user == sudo_user or conn.user == su_user: 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 ]: out = ''.join(stdout.readlines()) @@ -881,11 +913,11 @@ class Runner(object): basefile = 'ansible-tmp-%s-%s' % (time.time(), random.randint(0, 2**48)) 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) 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 += ' && echo %s' % basetmp diff --git a/lib/ansible/runner/connection_plugins/accelerate.py b/lib/ansible/runner/connection_plugins/accelerate.py index d260a63437..17cc99c1d7 100644 --- a/lib/ansible/runner/connection_plugins/accelerate.py +++ b/lib/ansible/runner/connection_plugins/accelerate.py @@ -159,9 +159,12 @@ class Connection(object): except socket.timeout: 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 ''' + if su or su_user: + raise errors.AnsibleError("Internal Error: this module does not support running commands via su") + if in_data: raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining") diff --git a/lib/ansible/runner/connection_plugins/chroot.py b/lib/ansible/runner/connection_plugins/chroot.py index 1080ea54b5..38c8af7a69 100644 --- a/lib/ansible/runner/connection_plugins/chroot.py +++ b/lib/ansible/runner/connection_plugins/chroot.py @@ -60,9 +60,12 @@ class Connection(object): 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 ''' + if su or su_user: + raise errors.AnsibleError("Internal Error: this module does not support running commands via su") + if in_data: raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining") diff --git a/lib/ansible/runner/connection_plugins/fireball.py b/lib/ansible/runner/connection_plugins/fireball.py index 40848edead..dd9e09bacd 100644 --- a/lib/ansible/runner/connection_plugins/fireball.py +++ b/lib/ansible/runner/connection_plugins/fireball.py @@ -68,7 +68,7 @@ class Connection(object): 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 ''' if in_data: @@ -76,9 +76,9 @@ class Connection(object): 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( - "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. " + "Task will communicate with the fireball already running in sudo mode." ) diff --git a/lib/ansible/runner/connection_plugins/funcd.py b/lib/ansible/runner/connection_plugins/funcd.py index a5dc631ef8..7244abcbe9 100644 --- a/lib/ansible/runner/connection_plugins/funcd.py +++ b/lib/ansible/runner/connection_plugins/funcd.py @@ -53,10 +53,13 @@ class Connection(object): self.client = fc.Client(self.host) 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 remote minion ''' + if su or su_user: + raise errors.AnsibleError("Internal Error: this module does not support running commands via su") + if in_data: raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining") diff --git a/lib/ansible/runner/connection_plugins/jail.py b/lib/ansible/runner/connection_plugins/jail.py index 89b13bbc44..b721ad62b5 100644 --- a/lib/ansible/runner/connection_plugins/jail.py +++ b/lib/ansible/runner/connection_plugins/jail.py @@ -91,9 +91,12 @@ class Connection(object): local_cmd = '%s "%s" %s' % (self.jexec_cmd, self.jail, 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 ''' + if su or su_user: + raise errors.AnsibleError("Internal Error: this module does not support running commands via su") + if in_data: raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining") diff --git a/lib/ansible/runner/connection_plugins/local.py b/lib/ansible/runner/connection_plugins/local.py index 0cf7da42be..a752a6a262 100644 --- a/lib/ansible/runner/connection_plugins/local.py +++ b/lib/ansible/runner/connection_plugins/local.py @@ -41,9 +41,13 @@ class Connection(object): 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 ''' + # 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: raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining") diff --git a/lib/ansible/runner/connection_plugins/paramiko_ssh.py b/lib/ansible/runner/connection_plugins/paramiko_ssh.py index a5dea39f0f..e8d035c57d 100644 --- a/lib/ansible/runner/connection_plugins/paramiko_ssh.py +++ b/lib/ansible/runner/connection_plugins/paramiko_ssh.py @@ -176,7 +176,7 @@ class Connection(object): 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 ''' if in_data: @@ -206,12 +206,15 @@ class Connection(object): chan.get_pty(term=os.getenv('TERM', 'vt100'), width=int(os.getenv('COLUMNS', 0)), height=int(os.getenv('LINES', 0))) - shcmd, prompt, success_key = utils.make_sudo_cmd(sudo_user, executable, cmd) + if self.runner.sudo or sudoable: + 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) sudo_output = '' try: 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: chunk = chan.recv(bufsize) if not chunk: @@ -223,7 +226,10 @@ class Connection(object): 'closed waiting for password prompt') sudo_output += chunk if success_key not in sudo_output: - chan.sendall(self.runner.sudo_pass + '\n') + if sudoable: + chan.sendall(self.runner.sudo_pass + '\n') + elif su: + chan.sendall(self.runner.su_pass + '\n') except socket.timeout: raise errors.AnsibleError('ssh timed out waiting for sudo.\n' + sudo_output) diff --git a/lib/ansible/runner/connection_plugins/ssh.py b/lib/ansible/runner/connection_plugins/ssh.py index d987d609a8..3df297adc4 100644 --- a/lib/ansible/runner/connection_plugins/ssh.py +++ b/lib/ansible/runner/connection_plugins/ssh.py @@ -145,7 +145,7 @@ class Connection(object): return False 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 ''' ssh_cmd = self._password_cmd() @@ -165,7 +165,10 @@ class Connection(object): ssh_cmd += ['-6'] 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: ssh_cmd.append(executable + ' -c ' + pipes.quote(cmd)) else: @@ -183,7 +186,7 @@ class Connection(object): # the host to known hosts is not intermingled with multiprocess output. fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX) fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX) - + # create process if in_data: # do not use pseudo-pty @@ -206,7 +209,8 @@ class Connection(object): 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 # * NOPASSWD (tty & no-tty): detect success_key on stdout # * without NOPASSWD: @@ -225,7 +229,7 @@ class Connection(object): if p.stderr in rfd: chunk = p.stderr.read() 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 incorrect_password = gettext.dgettext( "sudo", "Sorry, try again.") @@ -237,16 +241,19 @@ class Connection(object): if p.stdout in rfd: chunk = p.stdout.read() 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 if not rfd: # timeout. wrap up process communication 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: - stdin.write(self.runner.sudo_pass + '\n') + if sudoable: + 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.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 @@ -262,12 +269,18 @@ class Connection(object): while True: 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: incorrect_password = gettext.dgettext( "sudo", "Sorry, try again.") 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: dat = os.read(p.stdout.fileno(), 9000) diff --git a/lib/ansible/runner/connection_plugins/ssh_old.py b/lib/ansible/runner/connection_plugins/ssh_old.py index ea0857d406..db4fc24fcc 100644 --- a/lib/ansible/runner/connection_plugins/ssh_old.py +++ b/lib/ansible/runner/connection_plugins/ssh_old.py @@ -145,7 +145,7 @@ class Connection(object): return False 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 ''' if in_data: @@ -163,7 +163,10 @@ class Connection(object): ssh_cmd += ['-6'] 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: ssh_cmd.append(executable + ' -c ' + pipes.quote(cmd)) else: @@ -183,7 +186,6 @@ class Connection(object): fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX) - try: # Make sure stdin is a proper (pseudo) pty to avoid: tcgetattr errors master, slave = pty.openpty() @@ -198,7 +200,8 @@ class Connection(object): 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_GETFL) | os.O_NONBLOCK) sudo_output = '' @@ -208,13 +211,17 @@ class Connection(object): if p.stdout in rfd: chunk = p.stdout.read() 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 else: 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: - stdin.write(self.runner.sudo_pass + '\n') + if sudoable: + 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) # We can't use p.communicate here because the ControlMaster may have stdout open as well @@ -224,12 +231,18 @@ class Connection(object): while True: 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: incorrect_password = gettext.dgettext( "sudo", "Sorry, try again.") 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: dat = os.read(p.stdout.fileno(), 9000) diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py index a263ed15ff..ec540f6ecd 100644 --- a/lib/ansible/utils/__init__.py +++ b/lib/ansible/utils/__init__.py @@ -645,6 +645,8 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, help='use this file to authenticate the connection') parser.add_option('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true', 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', help='outputs a list of matching hosts; does not execute anything else') 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: parser.add_option("-s", "--sudo", default=constants.DEFAULT_SUDO, action="store_true", dest='sudo', help="run operations with sudo (nopasswd)") - parser.add_option('-U', '--sudo-user', dest='sudo_user', help='desired sudo user (default=root)', - default=None) # Can't default to root because we need to detect when this option was given + parser.add_option('-U', '--sudo-user', dest='sudo_user', default=None, + 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, - dest='remote_user', - help='connect as this user (default=%s)' % constants.DEFAULT_REMOTE_USER) + dest='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: 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 -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 sudopass = None + su_pass = None sudo_prompt = "sudo password: " + su_prompt = "su password: " if ask_pass: 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 == '': 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): 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))) 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)) def to_unicode(value):