| # Copyright (c) 2011-2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Helper methods and classes related to managing sudo.""" |
| |
| from __future__ import print_function |
| |
| import errno |
| import os |
| import signal |
| import subprocess |
| import sys |
| |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_logging as logging |
| |
| |
| class SudoKeepAlive(cros_build_lib.MasterPidContextManager): |
| """Keep sudo auth cookie fresh. |
| |
| This refreshes the sudo auth cookie; this is implemented this |
| way to ensure that sudo has access to both invoking tty, and |
| will update the user's tty-less cookie. |
| see crosbug/18393. |
| """ |
| |
| def __init__(self, ttyless_sudo=True, repeat_interval=4): |
| """Run sudo with a noop, to reset the sudo timestamp. |
| |
| Args: |
| ttyless_sudo: Whether to update the tty-less cookie. |
| repeat_interval: In minutes, the frequency to run the update. |
| """ |
| cros_build_lib.MasterPidContextManager.__init__(self) |
| self._ttyless_sudo = ttyless_sudo |
| self._repeat_interval = repeat_interval |
| self._proc = None |
| self._existing_keepalive_value = None |
| |
| @staticmethod |
| def _IdentifyTTY(): |
| for source in (sys.stdin, sys.stdout, sys.stderr): |
| try: |
| return os.ttyname(source.fileno()) |
| except EnvironmentError as e: |
| if e.errno not in (errno.EINVAL, errno.ENOTTY): |
| raise |
| |
| return 'unknown' |
| |
| def _DaemonNeeded(self): |
| """Discern which TTYs require sudo keep alive code. |
| |
| Returns: |
| A string representing the set of ttys we need daemons for. |
| This will be the empty string if no daemon is needed. |
| """ |
| existing = os.environ.get('CROS_SUDO_KEEP_ALIVE') |
| needed = set([self._IdentifyTTY()]) |
| if self._ttyless_sudo: |
| needed.add('unknown') |
| if existing is not None: |
| needed -= set(existing.split(':')) |
| return ':'.join(needed) |
| |
| def _enter(self): |
| if os.getuid() == 0: |
| cros_build_lib.Die('This script cannot be run as root.') |
| |
| start_for_tty = self._DaemonNeeded() |
| if not start_for_tty: |
| # Daemon is already started. |
| return |
| |
| # Note despite the impulse to use 'sudo -v' instead of 'sudo true', the |
| # builder's sudoers configuration is slightly whacked resulting in it |
| # asking for password everytime. As such use 'sudo true' instead. |
| cmds = ['sudo -n true 2>/dev/null', |
| 'sudo -n true < /dev/null > /dev/null 2>&1'] |
| |
| # First check to see if we're already authed. If so, then we don't |
| # need to prompt the user for their password. |
| for idx, cmd in enumerate(cmds): |
| ret = cros_build_lib.RunCommand( |
| cmd, print_cmd=False, shell=True, error_code_ok=True) |
| |
| if ret.returncode != 0: |
| tty_msg = 'Please disable tty_tickets using these instructions: %s' |
| if os.path.exists("/etc/goobuntu"): |
| url = 'https://goto.google.com/chromeos-sudoers' |
| else: |
| url = 'https://goo.gl/fz9YW' |
| |
| # If ttyless sudo is not strictly required for this script, don't |
| # prompt for a password a second time. Instead, just complain. |
| if idx > 0: |
| logging.error(tty_msg, url) |
| if not self._ttyless_sudo: |
| break |
| |
| # We need to go interactive and allow sudo to ask for credentials. |
| interactive_cmd = cmd.replace(' -n', '') |
| cros_build_lib.RunCommand(interactive_cmd, shell=True, print_cmd=False) |
| |
| # Verify that sudo access is set up properly. |
| try: |
| cros_build_lib.RunCommand(cmd, shell=True, print_cmd=False) |
| except cros_build_lib.RunCommandError: |
| if idx == 0: |
| raise |
| cros_build_lib.Die('tty_tickets must be disabled. ' + tty_msg, url) |
| |
| # Anything other than a timeout results in us shutting down. |
| repeat_interval = self._repeat_interval * 60 |
| cmd = ('while :; do read -t %i; [ $? -le 128 ] && exit; %s; done' % |
| (repeat_interval, '; '.join(cmds))) |
| |
| def ignore_sigint(): |
| # We don't want our sudo process shutdown till we shut it down; |
| # since it's part of the session group it however gets SIGINT. |
| # Thus suppress it (which bash then inherits). |
| signal.signal(signal.SIGINT, signal.SIG_IGN) |
| |
| self._proc = subprocess.Popen(['bash', '-c', cmd], shell=False, |
| close_fds=True, preexec_fn=ignore_sigint, |
| stdin=subprocess.PIPE) |
| |
| self._existing_keepalive_value = os.environ.get('CROS_SUDO_KEEP_ALIVE') |
| os.environ['CROS_SUDO_KEEP_ALIVE'] = start_for_tty |
| |
| # pylint: disable=W0613 |
| def _exit(self, exc_type, exc_value, traceback): |
| if self._proc is None: |
| return |
| |
| try: |
| self._proc.terminate() |
| self._proc.wait() |
| except EnvironmentError as e: |
| if e.errno != errno.ESRCH: |
| raise |
| |
| if self._existing_keepalive_value is not None: |
| os.environ['CROS_SUDO_KEEP_ALIVE'] = self._existing_keepalive_value |
| else: |
| os.environ.pop('CROS_SUDO_KEEP_ALIVE', None) |
| |
| |
| def SetFileContents(path, value, cwd=None): |
| """Set a given filepath contents w/ the passed in value.""" |
| cros_build_lib.SudoRunCommand(['tee', path], redirect_stdout=True, |
| print_cmd=False, input=value, cwd=cwd) |