| #!/usr/bin/env python |
| # Copyright (c) 2011 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. |
| |
| """This script provides a suite of utility functions for the chroot. |
| """ |
| |
| import optparse |
| import os |
| import shutil |
| import subprocess |
| import sys |
| |
| _PROG = "crdev" |
| _SSH_KEY_FILEPATH = os.path.expanduser("~/.ssh/id_rsa") |
| _PUB_KEY_FILENAME = "~/.ssh/id_rsa.pub" |
| _PUB_KEY_FILEPATH = os.path.expanduser(_PUB_KEY_FILENAME) |
| _AUTH_KEYS_FILEPATH = os.path.expanduser("~/.ssh/authorized_keys") |
| _CHROME_MOUNT_PATH = "/tmp/chrome" |
| # Note: no ~/ for sshfs: |
| _DEFAULT_HOST_CHROOT_DIR = "chromeos/chroot" |
| _HOST_CHROOT_DIR_FILENAME = os.path.expanduser("~/.chromedir") |
| |
| RAN_SUDO = False |
| |
| ############################################################################### |
| # Utility commands |
| |
| def _PrintError(msg, err=""): |
| """Print a message with an optional error message to stderr""" |
| if err: |
| print >> sys.stderr, "%s: %s" % (msg, err) |
| else: |
| print >> sys.stderr, msg |
| |
| def _Confirm(message, default_response="n"): |
| """Request confirmation and return True/False.""" |
| default_lc = default_response.lower() |
| if default_lc == "y": |
| input_msg = "%s [Y/n] " % message |
| else: |
| input_msg = "%s [y/N] " % message |
| |
| reply = raw_input(input_msg).lower() |
| if reply: |
| return reply == "y" |
| return default_lc == "y" |
| |
| |
| def _CheckOverwriteFile(filepath): |
| """Check for a file and request confirmation if it exists.""" |
| if os.path.isfile(filepath): |
| if not _Confirm("%s already exists. Overwrite?" % filepath): |
| return False |
| return True |
| |
| |
| def _WriteFile(filepath, contents): |
| """Write string |contents| to |filepath|.""" |
| try: |
| print "Writing to: ", filepath |
| with open(filepath,"w") as f_1: |
| f_1.write(contents) |
| except EnvironmentError as err: |
| _PrintError("Failed to write to file '%s'" % filepath, err) |
| return False |
| |
| return True |
| |
| |
| def _ReadFile(filepath): |
| """Return contents of |filepath| as a string.""" |
| try: |
| print "Reading from: " + filepath |
| with open(filepath,"r") as f_1: |
| result = f_1.read() |
| except EnvironmentError as err: |
| _PrintError("Failed to read from file '%s'" % filepath, err) |
| return None |
| |
| return result.rstrip() |
| |
| |
| def _Sudo(): |
| """Request sudo access with message.""" |
| global RAN_SUDO |
| if not RAN_SUDO: |
| print "Executing: sudo -v" |
| try: |
| subprocess.call(["sudo", "-v"]) |
| except EnvironmentError as err: |
| _PrintError("Failed to run sudo", err) |
| return False |
| RAN_SUDO = True |
| return True |
| |
| |
| def _RunCommand(args): |
| """Pass |args| to subprocess.call() and check the result.""" |
| cmd = ''.join([x + " " for x in args]) |
| try: |
| if args[0] == "sudo": |
| if _Sudo() == False: |
| return False |
| print "Executing: ", cmd |
| retcode = subprocess.call(args) |
| except EnvironmentError as err: |
| _PrintError("Failed to run command '%s'" % cmd, err) |
| return False |
| else: |
| if retcode != 0: |
| _PrintError("Error running command '%s'" % cmd, "%s" % retcode) |
| return False |
| return True |
| |
| |
| def _GetCommandOutput(args): |
| """Pass |args| to subprocess.Popen and return the output.""" |
| cmd = ''.join([x + " " for x in args]) |
| try: |
| proc = subprocess.Popen(args, stdout=subprocess.PIPE) |
| result = proc.communicate()[0] |
| except EnvironmentError as err: |
| _PrintError("Failed to run command '%s'" % cmd, err) |
| return None |
| result = result.rstrip('\n') |
| return result |
| |
| |
| def _MakeDirectory(dirpath, warn=True): |
| """Create directory |dirpath| if it does not exist.""" |
| if not os.path.isdir(dirpath): |
| try: |
| print "Creating directory: ", dirpath |
| os.makedirs(dirpath) |
| except EnvironmentError as err: |
| if warn: |
| _PrintError("Failed to create directory '%s'" % dirpath, err) |
| return False |
| return True |
| |
| |
| def _RemoveFile(filepath): |
| """Remove file |filepath| if it exists.""" |
| if os.path.isfile(filepath): |
| try: |
| print "Removing file: ", filepath |
| os.remove(filepath) |
| except EnvironmentError as err: |
| _PrintError("Failed to remove file '%s'" % filepath, err) |
| return False |
| return True |
| |
| |
| def _RemoveDirectory(dirpath): |
| """Recursively remove directory |dirpath| if it exists.""" |
| if os.path.isdir(dirpath): |
| try: |
| print "Removing directory: ", dirpath |
| shutil.rmtree(dirpath) |
| except EnvironmentError as err: |
| _PrintError("Failed to remove dir '%s'" % dirpath, err) |
| return False |
| return True |
| |
| |
| def _DevUser(): |
| """Extract the user name from lsb-release.""" |
| # TODO(stevenjb): Refactor this using python re. |
| awk_expr = """/CHROMEOS_RELEASE_DESCRIPTION/ {""" |
| awk_expr += """ sub(/.*Build - /,"");""" |
| awk_expr += """ sub(/\).*/,"");""" |
| awk_expr += """ print; }""" |
| return _GetCommandOutput(["awk", awk_expr, "/etc/lsb-release"]) |
| |
| |
| def _DevHost(): |
| """Extract the host name from lsb-release.""" |
| # TODO(stevenjb): Refactor this using python re. |
| awk_expr = """/CHROMEOS_DEVSERVER/ {""" |
| awk_expr += """ sub(/.*http:\/\//,"");""" |
| awk_expr += """ sub(/:8080.*/,"");""" |
| awk_expr += """ print; }""" |
| return _GetCommandOutput(["awk", awk_expr, "/etc/lsb-release"]) |
| |
| |
| def _DevBoard(): |
| """Extract the board from lsb-release.""" |
| # TODO(stevenjb): Refactor this using python re. |
| awk_expr = """/CHROMEOS_RELEASE_BOARD/ {""" |
| awk_expr += """ sub(/.*=/,"");""" |
| awk_expr += """ print; }""" |
| return _GetCommandOutput(["awk", awk_expr, "/etc/lsb-release"]) |
| |
| |
| def _GetChrootDir(prompt_for_dir=False): |
| """Get the name for the chrome directory on the host.""" |
| if os.path.isfile(_HOST_CHROOT_DIR_FILENAME): |
| chromedir = _ReadFile(_HOST_CHROOT_DIR_FILENAME) |
| else: |
| chromedir = _DEFAULT_HOST_CHROOT_DIR |
| |
| if prompt_for_dir: |
| host = _DevHost() |
| prompt = ("Chroot directory on %s [ %s ]: " % (host, chromedir)) |
| inputdir = raw_input(prompt).rstrip() |
| if inputdir: |
| chromedir = inputdir |
| _WriteFile(_HOST_CHROOT_DIR_FILENAME, chromedir) |
| |
| return chromedir |
| |
| |
| def _DaemonizeAndReRunAsRoot(cmd): |
| """Double-fork to become owned by init, then re-exec this tool as root. |
| |
| Double-forking is the standard way to detach yourself from a |
| process tree and become owned by init. By doing this, you get to |
| live on even if your parent goes away.""" |
| try: |
| pid = os.fork() |
| if pid > 0: |
| sys.exit(0) # Just want to go away silently. |
| except OSError, e: |
| return False |
| try: |
| pid = os.fork() |
| if pid > 0: |
| sys.exit(0) # Just want to go away silently. |
| except OSError, e: |
| return False |
| |
| # re-run self as root. |
| try: |
| os.execlp('sudo', sys.executable, __file__, cmd) |
| except OSError, e: |
| _PrintError("Unable to exec self as root: %s", str(e)) |
| return False |
| |
| |
| ############################################################################### |
| # Other Commands |
| |
| def TestCommand(args): |
| """Test command.""" |
| _WriteFile("/foo/test", "test") |
| if len(args) == 0: |
| args = ["sudo", "ls", "/"] |
| return _RunCommand(args) |
| return False |
| |
| |
| def GetBoard(unused_args=0): |
| """Gets the board name from /etc/lsb-release.""" |
| print _DevBoard() |
| return True |
| |
| |
| def GetUser(unused_args=0): |
| """Gets the user name from /etc/lsb-release.""" |
| print _DevUser() |
| return True |
| |
| |
| def GetHost(unused_args=0): |
| """Gets the host name from /etc/lsb-release.""" |
| print _DevHost() |
| return True |
| |
| |
| def MountWriteable(unused_args=0): |
| """Remounts / as rw.""" |
| return _RunCommand(["sudo", "mount", "-o", "remount,rw", "/"]) |
| |
| |
| def ShowIP(unused_args=0): |
| """Shows the IP address of the device.""" |
| proc1 = subprocess.Popen(["/sbin/ifconfig", "eth0"], stdout=subprocess.PIPE) |
| awk_cmd = """/inet addr/ {""" |
| awk_cmd += """ sub(/.*inet addr:/,""); sub(/ Bcast:.*/,"");""" |
| awk_cmd += """ print; }""" |
| proc2 = subprocess.Popen( |
| ["awk", awk_cmd], stdin=proc1.stdout, stdout=subprocess.PIPE) |
| proc1.stdout.close() |
| result = proc2.communicate()[0].rstrip('\n') |
| print "IP: ", result |
| return True |
| |
| |
| def KillChrome(unused_args=0): |
| """Kills all chrome processes and prevents restarting of chrome.""" |
| res = True |
| res &= _RunCommand(["touch", "/run/disable_chrome_restart"]) |
| res &= _RunCommand(["sudo", "pkill", "-9", "chrome"]) |
| return res |
| |
| |
| def ClearOwnership(unused_args=0): |
| """Clears any state related to the device Ownership (NOT TPM ownership).""" |
| res = True |
| res &= _RunCommand(["sudo", "stop", "ui"]) |
| |
| whitelist_dir = "/var/lib/whitelist" |
| for file in os.listdir(whitelist_dir): |
| res &= _RemoveFile(os.path.join(whitelist_dir, file)) |
| |
| res &= _RemoveDirectory("/home/chronos/Local State") |
| res &= _RemoveFile("/home/chronos/Consent To Send Stats") |
| res &= _RunCommand(["sudo", "start", "ui"]) |
| |
| if not res: |
| _PrintError("Errors encountered while clearing ownership state.") |
| return False |
| return True |
| |
| |
| def ShowOobe(unused_args=0): |
| """Removes .oobe_completed and Local State directory.""" |
| res = True |
| res &= _RemoveFile("/home/chronos/.oobe_completed") |
| res &= ClearOwnership() |
| |
| if not res: |
| _PrintError("Errors encountered while clearing setting up OOBE mode.") |
| return False |
| return _RunCommand(["sudo", "reboot"]) |
| |
| |
| ############################################################################### |
| # Setup Commands |
| |
| def SetBash(unused_args): |
| """Sets the default shell to bash.""" |
| if not _Sudo(): |
| return False |
| if not MountWriteable(): |
| return False |
| res = True |
| res &= _RunCommand(["chsh", "-s", "/bin/bash"]) |
| res &= _RunCommand(["chsh", "-s", "/bin/bash", "chronos"]) |
| return res |
| |
| |
| def SetupBashrc(unused_args): |
| """Sets up .bashrc.""" |
| filepath = os.path.expanduser("~/.bashrc") |
| if not _CheckOverwriteFile(filepath): |
| return True |
| |
| print "Writing to: ", filepath |
| bashrc = "#!/bin/bash\n" |
| bashrc += "# .bashrc file set by %s\n" % _PROG |
| bashrc += "export DISPLAY=:0.0\n" |
| bashrc += "export PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin\n" |
| bashrc += "/sbin/ifconfig eth0 | grep 'inet addr'\n" |
| if not _WriteFile(filepath, bashrc): |
| return False |
| |
| filepath = os.path.expanduser("~/.bash_profile") |
| print "Writing to: ", filepath |
| bashprofile = "#!/bin/bash\n" |
| bashprofile += ". $HOME/.bashrc\n" |
| if not _WriteFile(filepath, bashprofile): |
| return False |
| return True |
| |
| |
| def SetupDev(unused_args): |
| """Developer friendly setup: skip oobe, disable suspend, |
| \t\tenable ssh and remote debugging, run commands from |
| \t\t/tmp,/home.""" |
| if not _Sudo(): |
| return False |
| if not MountWriteable(): |
| return False |
| res = True |
| res &= _WriteFile("/home/chronos/.oobe_completed", "1\n") |
| res &= _MakeDirectory("/usr/share/power_manager") |
| res &= _WriteFile("/tmp/disable_idle_suspend", "1\n") |
| res &= _RunCommand(["sudo", "cp", "/tmp/disable_idle_suspend", |
| "/usr/share/power_manager/"]) |
| # Enable iptables and system-services for remote debugging |
| for filename in ["iptables", "saft"]: |
| res &= _RunCommand(["sudo", "sed", "-i", "-e", |
| "s/#for_test //", "/etc/init/%s.conf" % filename]) |
| # Allow commands to be run from /home and /tmp |
| res &= _RunCommand(["sudo", "sed", "-i", "-e", |
| "s/#mod_for_test#//g", "/sbin/chromeos_startup"]) |
| return res |
| |
| |
| def SetupSsh(unused_args): |
| """Sets up ssh configuration so that the dev host can ssh to the device.""" |
| if not MountWriteable(): |
| return False |
| |
| user = _DevUser() |
| host = _DevHost() |
| |
| res = True |
| if _CheckOverwriteFile(_SSH_KEY_FILEPATH): |
| res &= _RemoveFile(_SSH_KEY_FILEPATH) |
| # Generate an ssh key |
| if _RunCommand(["ssh-keygen", "-f", _SSH_KEY_FILEPATH, "-N", "", "-q"]): |
| host_source_path = "%s@%s:%s" % (user, host, _PUB_KEY_FILENAME) |
| # Copy the ssh key to the host |
| res &= _RunCommand(["scp", host_source_path, _AUTH_KEYS_FILEPATH]) |
| |
| # Enable ssh to device |
| res &= _RunCommand( |
| ["sudo", "sed", "-i", "s/#for_test //", "/etc/init/openssh-server.conf"]) |
| |
| return res |
| |
| |
| def AuthorizeSsh(unused_args): |
| """Authorizes this netbook to connect to your dev host. |
| \t\t*Only use this on a device in a secure location!*""" |
| |
| user = _DevUser() |
| host = _DevHost() |
| |
| if not os.path.isdir(os.path.expanduser("~/.ssh")): |
| print "Run '%s ssh' to set up .ssh directory first." % _PROG |
| return False |
| |
| if not _Confirm("This will append %s to authorized_keys on %s. " |
| "Are you sure?" % (_PUB_KEY_FILENAME, host)): |
| return False |
| |
| proc1 = subprocess.Popen(["cat", _PUB_KEY_FILEPATH], stdout=subprocess.PIPE) |
| try: |
| ssh_args = ["ssh", user+"@"+host, "cat >> ~/.ssh/authorized_keys"] |
| proc2 = subprocess.Popen( |
| ssh_args, stdin=proc1.stdout, stdout=subprocess.PIPE) |
| except EnvironmentError as err1: |
| _PrintError("Error executing '%s'" % ' '.join(ssh_args), err1) |
| return False |
| try: |
| proc1.stdout.close() |
| result, err2 = proc2.communicate() |
| except EnvironmentError: |
| _PrintError("Error completing ssh command '%s'" % result, err2) |
| return False |
| return True |
| |
| |
| def SetupSshfsForChrome(unused_args): |
| """<chrome-drir> Sets up sshfs mount to chrome directory on host.""" |
| |
| user = _DevUser() |
| host = _DevHost() |
| |
| chrootdir = _GetChrootDir(True) |
| |
| target = ("%s@%s:%s/var/lib/portage/distfiles-target/chrome-src" |
| % (user, host, chrootdir)) |
| print "Setting up sshfs mount to: ", target |
| |
| res = True |
| res &= _RunCommand(["sudo", "modprobe", "fuse"]) |
| if os.path.isdir(_CHROME_MOUNT_PATH): |
| res &= _RunCommand(["fusermount", "-q", "-u", _CHROME_MOUNT_PATH]) |
| res &= _RemoveDirectory(_CHROME_MOUNT_PATH) |
| res &= _MakeDirectory(_CHROME_MOUNT_PATH) |
| res &= _RunCommand(["sshfs", target, _CHROME_MOUNT_PATH]) |
| res &= _RunCommand(["sudo", "/sbin/iptables", |
| "-A", "INPUT", "-p", "tcp", "--dport", "1234", |
| "-j", "ACCEPT"]) |
| return res |
| |
| ############################################################################### |
| # Multi-commands (convenience functions) |
| |
| def Setup(args): |
| """Performs default developer setup (bash,bashrc,dev,ssh).""" |
| if not SetBash(args): |
| if not _Confirm("Bash setup failed. Continue?", "y"): |
| return False |
| if not SetupBashrc(args): |
| if not _Confirm(".bashrc setup failed. Continue?", "y"): |
| return False |
| if not SetupDev(args): |
| if not _Confirm("Dev setup failed. Continue?", "y"): |
| return False |
| if not SetupSsh(args): |
| return False |
| return True |
| |
| ############################################################################### |
| |
| _SETUP_COMMANDS = { |
| 'setup': Setup, |
| 'dev': SetupDev, |
| 'bash': SetBash, |
| 'bashrc': SetupBashrc, |
| 'ssh': SetupSsh, |
| 'sshauthorize': AuthorizeSsh, |
| 'sshfs': SetupSshfsForChrome, |
| } |
| |
| |
| _OTHER_COMMANDS = { |
| 'mountrw': MountWriteable, |
| 'ip': ShowIP, |
| 'test': TestCommand, |
| 'board': GetBoard, |
| 'user': GetUser, |
| 'host': GetHost, |
| } |
| |
| |
| _CLEANUP_COMMANDS = { |
| 'show_oobe': ShowOobe, |
| 'clear_owner': ClearOwnership, |
| } |
| |
| |
| def GetUsage(commands): |
| """Get the docstring for each command.""" |
| usage = "" |
| for cmd in commands.items(): |
| usage += " " |
| usage += cmd[0] |
| usage += ":\t" |
| if len(cmd[0]) < 6: |
| usage += "\t" |
| doc = cmd[1].__doc__ |
| if doc: |
| usage += doc |
| usage += "\n" |
| return usage |
| |
| |
| ############################################################################### |
| |
| def main(): |
| """Main crdev function""" |
| usage = """usage: crdev command [options] |
| |
| Note: Beta! Feature requests / changes can be sent to: |
| stevenjb@chromium.org (for now) |
| |
| """ |
| |
| usage += "Setup Commands:\n" |
| usage += GetUsage(_SETUP_COMMANDS) |
| usage += "Other Commands:\n" |
| usage += GetUsage(_OTHER_COMMANDS) |
| usage += "Cleanup Commands:\n" |
| usage += GetUsage(_CLEANUP_COMMANDS) |
| |
| parser = optparse.OptionParser(usage) |
| args = parser.parse_args()[1] |
| |
| if not args: |
| print usage |
| return |
| |
| cmd = args[0] |
| root_ok = cmd in _CLEANUP_COMMANDS.keys() |
| if not root_ok or os.geteuid() != 0: |
| if os.getenv('USER') != 'chronos': |
| _PrintError("%s must be run as chronos." % _PROG) |
| return |
| |
| if cmd in _SETUP_COMMANDS.keys(): |
| res = _SETUP_COMMANDS[cmd](args[1:]) |
| elif cmd in _OTHER_COMMANDS.keys(): |
| res = _OTHER_COMMANDS[cmd](args[1:]) |
| elif cmd in _CLEANUP_COMMANDS.keys(): |
| if os.geteuid() != 0: |
| _DaemonizeAndReRunAsRoot(cmd) |
| _PrintError("Should never return from DaemonizeAndReRunAsRoot()") |
| return |
| res = _CLEANUP_COMMANDS[cmd](args[1:]) |
| else: |
| parser.error("Unknown command: " + cmd) |
| return |
| |
| if not res: |
| _PrintError("Errors encountered when running '%s'" % ' '.join(args)) |
| else: |
| print "Success!" |
| |
| |
| if __name__ == '__main__': |
| main() |