Steven Bennetts | 62fd725 | 2011-11-03 20:56:09 | [diff] [blame^] | 1 | #!/usr/bin/env python |
| 2 | # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | """This script provides a suite of utility functions for the chroot. |
| 7 | """ |
| 8 | |
| 9 | import optparse |
| 10 | import os |
| 11 | import shutil |
| 12 | import subprocess |
| 13 | import sys |
| 14 | |
| 15 | _PROG = "crdev" |
| 16 | _SSH_KEY_FILEPATH = os.path.expanduser("~/.ssh/id_rsa") |
| 17 | _PUB_KEY_FILENAME = "~/.ssh/id_rsa.pub" |
| 18 | _PUB_KEY_FILEPATH = os.path.expanduser(_PUB_KEY_FILENAME) |
| 19 | _AUTH_KEYS_FILEPATH = os.path.expanduser("~/.ssh/authorized_keys") |
| 20 | _CHROME_MOUNT_PATH = "/tmp/chrome" |
| 21 | _DEFAULT_CHROME_DIR = "~/chromeos/chromium" |
| 22 | |
| 23 | RAN_SUDO = False |
| 24 | |
| 25 | ############################################################################### |
| 26 | # Utility commands |
| 27 | |
| 28 | def _Confirm(message): |
| 29 | """Request confirmation and return True/False.""" |
| 30 | reply = raw_input(message + " [y/N] ") |
| 31 | if reply == "y" or reply == "Y": |
| 32 | return True |
| 33 | return False |
| 34 | |
| 35 | |
| 36 | def _CheckOverwriteFile(filepath): |
| 37 | """Check for a file and request confirmation if it exists.""" |
| 38 | if os.path.isfile(filepath): |
| 39 | if not _Confirm(filepath + " already exists. Overwrite?"): |
| 40 | return False |
| 41 | return True |
| 42 | |
| 43 | |
| 44 | def _WriteFile(filepath, contents): |
| 45 | """Write string |contents| to |filepath|.""" |
| 46 | try: |
| 47 | print "Writing to: ", filepath |
| 48 | with open(filepath,"w") as f_1: |
| 49 | f_1.write(contents) |
| 50 | except EnvironmentError: |
| 51 | print >> sys.stderr, "Failed to write to file: ", filepath |
| 52 | return False |
| 53 | |
| 54 | return True |
| 55 | |
| 56 | |
| 57 | def _Sudo(): |
| 58 | """Request sudo access with message.""" |
| 59 | global RAN_SUDO |
| 60 | if not RAN_SUDO: |
| 61 | print "Executing: sudo -v" |
| 62 | try: |
| 63 | subprocess.call(["sudo", "-v"]) |
| 64 | except EnvironmentError: |
| 65 | print >> sys.stderr, "Failed to run sudo" |
| 66 | return False |
| 67 | RAN_SUDO = True |
| 68 | return True |
| 69 | |
| 70 | |
| 71 | def _RunCommand(args): |
| 72 | """Pass |args| to subprocess.call() and check the result.""" |
| 73 | cmd = ''.join([x + " " for x in args]) |
| 74 | try: |
| 75 | if args[0] == "sudo": |
| 76 | if _Sudo() == False: |
| 77 | return False |
| 78 | print "Executing: ", cmd |
| 79 | retcode = subprocess.call(args) |
| 80 | except EnvironmentError: |
| 81 | print >> sys.stderr, "Failed to run command: ", cmd |
| 82 | return False |
| 83 | else: |
| 84 | if retcode != 0: |
| 85 | print >> sys.stderr, "Error: ", retcode, ", running command: ", cmd |
| 86 | return False |
| 87 | return True |
| 88 | |
| 89 | |
| 90 | def _GetCommandOutput(args): |
| 91 | """Pass |args| to subprocess.Popen and return the output.""" |
| 92 | cmd = ''.join([x + " " for x in args]) |
| 93 | try: |
| 94 | proc = subprocess.Popen(args, stdout=subprocess.PIPE) |
| 95 | result = proc.communicate()[0] |
| 96 | except EnvironmentError: |
| 97 | print >> sys.stderr, "Failed to run command: ", cmd |
| 98 | return None |
| 99 | result = result.rstrip('\n') |
| 100 | return result |
| 101 | |
| 102 | |
| 103 | def _MakeDirectory(dirpath): |
| 104 | """Create directory |dirpath| if it does not exist.""" |
| 105 | if not os.path.isdir(dirpath): |
| 106 | try: |
| 107 | print "Creating directory: ", dirpath |
| 108 | os.makedirs(dirpath) |
| 109 | except EnvironmentError: |
| 110 | print >> sys.stderr, "Failed to create directory: ", dirpath |
| 111 | return False |
| 112 | return True |
| 113 | |
| 114 | |
| 115 | def _RemoveFile(filepath): |
| 116 | """Remove file |filepath| if it exists.""" |
| 117 | if os.path.isfile(filepath): |
| 118 | try: |
| 119 | print "Removing file: ", filepath |
| 120 | os.remove(filepath) |
| 121 | except EnvironmentError: |
| 122 | print >> sys.stderr, "Failed to remove file: ", filepath |
| 123 | return False |
| 124 | return True |
| 125 | |
| 126 | |
| 127 | def _RemoveDirectory(dirpath): |
| 128 | """Recursively remove directory |dirpath| if it exists.""" |
| 129 | if os.path.isdir(dirpath): |
| 130 | try: |
| 131 | print "Removing directory: ", dirpath |
| 132 | shutil.rmtree(dirpath) |
| 133 | except EnvironmentError: |
| 134 | print >> sys.stderr, "Failed to remove directory: ", dirpath |
| 135 | return False |
| 136 | return True |
| 137 | |
| 138 | |
| 139 | def _DevUser(): |
| 140 | """Extract the user name from lsb-release.""" |
| 141 | awk_expr = """/CHROMEOS_RELEASE_DESCRIPTION/ {""" |
| 142 | awk_expr += """ sub(/.*Developer Build - /,"");""" |
| 143 | awk_expr += """ sub(/\).*/,"");""" |
| 144 | awk_expr += """ print; }""" |
| 145 | return _GetCommandOutput(["awk", awk_expr, "/etc/lsb-release"]) |
| 146 | |
| 147 | |
| 148 | def _DevHost(): |
| 149 | """Extract the host name from lsb-release.""" |
| 150 | awk_expr = """/CHROMEOS_DEVSERVER/ {""" |
| 151 | awk_expr += """ sub(/.*http:\/\//,"");""" |
| 152 | awk_expr += """ sub(/:8080.*/,"");""" |
| 153 | awk_expr += """ print; }""" |
| 154 | return _GetCommandOutput(["awk", awk_expr, "/etc/lsb-release"]) |
| 155 | |
| 156 | ############################################################################### |
| 157 | # Other Commands |
| 158 | |
| 159 | def TestCommand(args): |
| 160 | """Test command.""" |
| 161 | if len(args) == 0: |
| 162 | args = ["sudo", "ls", "/"] |
| 163 | return _RunCommand(args) |
| 164 | return False |
| 165 | |
| 166 | |
| 167 | def GetUser(unused_args=0): |
| 168 | """Gets the user name from /etc/lsb-release.""" |
| 169 | print _DevUser() |
| 170 | return True |
| 171 | |
| 172 | |
| 173 | def GetHost(unused_args=0): |
| 174 | """Gets the host name from /etc/lsb-release.""" |
| 175 | print _DevHost() |
| 176 | return True |
| 177 | |
| 178 | |
| 179 | def MountWriteable(unused_args=0): |
| 180 | """Remounts / as rw.""" |
| 181 | return _RunCommand(["sudo", "mount", "-o", "remount,rw", "/"]) |
| 182 | |
| 183 | |
| 184 | def ShowIP(unused_args=0): |
| 185 | """Shows the IP address of the device.""" |
| 186 | proc1 = subprocess.Popen(["/sbin/ifconfig", "eth0"], stdout=subprocess.PIPE) |
| 187 | awk_cmd = """/inet addr/ {""" |
| 188 | awk_cmd += """ sub(/.*inet addr:/,""); sub(/ Bcast:.*/,"");""" |
| 189 | awk_cmd += """ print; }""" |
| 190 | proc2 = subprocess.Popen( |
| 191 | ["awk", awk_cmd], stdin=proc1.stdout, stdout=subprocess.PIPE) |
| 192 | proc1.stdout.close() |
| 193 | result = proc2.communicate()[0].rstrip('\n') |
| 194 | print "IP: ", result |
| 195 | return True |
| 196 | |
| 197 | |
| 198 | def KillChrome(unused_args=0): |
| 199 | """Kills all chrome processes and prevents restarting of chrome.""" |
| 200 | res = True |
| 201 | res &= _RunCommand(["touch", "/tmp/disable_chrome_restart"]) |
| 202 | res &= _RunCommand(["sudo", "pkill", "-9", "chrome"]) |
| 203 | return res |
| 204 | |
| 205 | |
| 206 | def ShowOobe(unused_args=0): |
| 207 | """Removes .oobe_completed and Local State directory.""" |
| 208 | |
| 209 | res = True |
| 210 | res &= KillChrome() |
| 211 | res &= _RemoveFile("/home/chronos/.oobe_completed") |
| 212 | res &= _RemoveDirectory("/home/chronos/Local State") |
| 213 | |
| 214 | if not res: |
| 215 | print >> sys.stderr, "Unable to set up OOBE mode." |
| 216 | return False |
| 217 | |
| 218 | if _Confirm("Reboot is required to enter OOBE mode. Reboot now?"): |
| 219 | _RunCommand(["sudo", "reboot"]) |
| 220 | return True |
| 221 | |
| 222 | ############################################################################### |
| 223 | # Setup Commands |
| 224 | |
| 225 | def SetBash(unused_args): |
| 226 | """Sets the default shell to bash.""" |
| 227 | if not _Sudo(): |
| 228 | return False |
| 229 | if not MountWriteable(): |
| 230 | return False |
| 231 | res = True |
| 232 | res &= _RunCommand(["chsh", "-s", "/bin/bash"]) |
| 233 | res &= _RunCommand(["chsh", "-s", "/bin/bash", "chronos"]) |
| 234 | return res |
| 235 | |
| 236 | |
| 237 | def SetupBashrc(unused_args): |
| 238 | """Sets up .bashrc.""" |
| 239 | filepath = os.path.expanduser("~/.bashrc") |
| 240 | if not _CheckOverwriteFile(filepath): |
| 241 | return False |
| 242 | |
| 243 | print "Writing to: ", filepath |
| 244 | bashrc = "#!/bin/bash\n" |
| 245 | bashrc += "# .bashrc file set by %s\n" % _PROG |
| 246 | bashrc += "export DISPLAY=:0.0\n" |
| 247 | bashrc += "export PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin\n" |
| 248 | bashrc += "/sbin/ifconfig eth0 | grep 'inet addr'\n" |
| 249 | if not _WriteFile(filepath, bashrc): |
| 250 | return False |
| 251 | |
| 252 | filepath = os.path.expanduser("~/.bash_profile") |
| 253 | print "Writing to: ", filepath |
| 254 | bashprofile = "#!/bin/bash\n" |
| 255 | bashprofile += ". $HOME/.bashrc\n" |
| 256 | if not _WriteFile(filepath, bashprofile): |
| 257 | return False |
| 258 | return True |
| 259 | |
| 260 | |
| 261 | def SetupDev(unused_args): |
| 262 | """Developer friendly setup: skip oobe, don't forget usernames, |
| 263 | \t\tdisable suspend, enable ssh and remote debugging, |
| 264 | \t\trun commands from /tmp,/home.""" |
| 265 | if not _Sudo(): |
| 266 | return False |
| 267 | if not MountWriteable(): |
| 268 | return False |
| 269 | res = True |
| 270 | res &= _WriteFile("1", "/home/chronos/.oobe_completed") |
| 271 | res &= _RemoveFile("/root/.forget_usernames") |
| 272 | res &= _MakeDirectory("/usr/share/power_manager") |
| 273 | res &= _WriteFile("1", "/usr/share/power_manager/disable_idle_suspend") |
| 274 | # Enable iptables and system-services for remote debugging |
| 275 | for filename in ["iptables", "saft"]: |
| 276 | res &= _RunCommand(["sudo", "sed", "-i", "-e", |
| 277 | "s/#for_test //", "/etc/init/%s.conf" % filename]) |
| 278 | # Allow commands to be run from /home and /tmp |
| 279 | res &= _RunCommand(["sudo", "sed", "-i", "-e", |
| 280 | "s/#mod_for_test#//g", "/sbin/chromeos_startup"]) |
| 281 | return res |
| 282 | |
| 283 | |
| 284 | def SetupSsh(unused_args): |
| 285 | """Sets up ssh configuration so that the dev host can ssh to the device.""" |
| 286 | if not MountWriteable(): |
| 287 | return False |
| 288 | |
| 289 | user = _DevUser() |
| 290 | host = _DevHost() |
| 291 | |
| 292 | if not _CheckOverwriteFile(_SSH_KEY_FILEPATH): |
| 293 | return False |
| 294 | |
| 295 | res = True |
| 296 | res &= _RemoveFile(_SSH_KEY_FILEPATH) |
| 297 | # Enable ssh to device |
| 298 | res &= _RunCommand( |
| 299 | ["sudo", "sed", "-i", "s/#for_test //", "/etc/init/openssh-server.conf"]) |
| 300 | # Generate an ssh key |
| 301 | if _RunCommand(["ssh-keygen", "-f", _SSH_KEY_FILEPATH, "-N", "", "-q"]): |
| 302 | host_source_path = "%s@%s:%s" % (user, host, _PUB_KEY_FILENAME) |
| 303 | # Copy the ssh key to the host |
| 304 | res &= _RunCommand(["scp", host_source_path, _AUTH_KEYS_FILEPATH]) |
| 305 | |
| 306 | return res |
| 307 | |
| 308 | |
| 309 | def AuthorizeSsh(unused_args): |
| 310 | """Authorizes this netbook to connect to your dev host. |
| 311 | \t\t*Only use this on a device in a secure location!*""" |
| 312 | |
| 313 | user = _DevUser() |
| 314 | host = _DevHost() |
| 315 | |
| 316 | if not _Confirm("This will copy %s to authorized_keys on %s. Are you sure?" % |
| 317 | (_PUB_KEY_FILENAME, host)): |
| 318 | return False |
| 319 | |
| 320 | proc1 = subprocess.Popen(["cat", _PUB_KEY_FILEPATH], stdout=subprocess.PIPE) |
| 321 | try: |
| 322 | ssh_args = ["ssh", user+"@"+host, "cat >> ~/.ssh/authorized_keys"] |
| 323 | proc2 = subprocess.Popen( |
| 324 | ssh_args, stdin=proc1.stdout, stdout=subprocess.PIPE) |
| 325 | except EnvironmentError: |
| 326 | print >> sys.stderr, "Error executing: ", ' '.join(ssh_args) |
| 327 | return False |
| 328 | try: |
| 329 | proc1.stdout.close() |
| 330 | result, err = proc2.communicate() |
| 331 | except EnvironmentError: |
| 332 | print >> sys.stderr, \ |
| 333 | "Error completing ssh command: ", result, "Error: ", err |
| 334 | return False |
| 335 | return True |
| 336 | |
| 337 | |
| 338 | def SetupSshfsForChrome(unused_args): |
| 339 | """<chrome-drir> Sets up sshfs mount to chrome directory on host.""" |
| 340 | |
| 341 | user = _DevUser() |
| 342 | host = _DevHost() |
| 343 | |
| 344 | prompt = "Chrome directory on %s [ %s ]: " % (host, _DEFAULT_CHROME_DIR) |
| 345 | chromedir = raw_input(prompt).rstrip() |
| 346 | if not chromedir: |
| 347 | chromedir = _DEFAULT_CHROME_DIR |
| 348 | |
| 349 | target = user+"@"+host+":"+chromedir |
| 350 | print "Setting up sshfs mount to: ", target |
| 351 | |
| 352 | res = True |
| 353 | res &= _RunCommand(["sudo", "modprobe", "fuse"]) |
| 354 | if os.path.isdir(_CHROME_MOUNT_PATH): |
| 355 | res &= _RunCommand(["fusemount", "-q", "-u", _CHROME_MOUNT_PATH]) |
| 356 | res &= _RemoveDirectory(_CHROME_MOUNT_PATH) |
| 357 | res &= _MakeDirectory(_CHROME_MOUNT_PATH) |
| 358 | res &= _RunCommand(["sshfs", target, _CHROME_MOUNT_PATH]) |
| 359 | res &= _RunCommand(["sudo", "/sbin/iptables", |
| 360 | "-A", "INPUT", "-p", "tcp", "--dport", "1234", |
| 361 | "-j", "ACCEPT"]) |
| 362 | return res |
| 363 | |
| 364 | ############################################################################### |
| 365 | # Multi-commands (convenience functions) |
| 366 | |
| 367 | def Setup(args): |
| 368 | """Performs default developer setup (bash,bashrc,dev,ssh).""" |
| 369 | if not SetBash(args): |
| 370 | if not _Confirm("Bash setup failed. Continue?"): |
| 371 | return False |
| 372 | if not SetupBashrc(args): |
| 373 | if not _Confirm(".bashrc setup failed. Continue?"): |
| 374 | return False |
| 375 | if not SetupDev(args): |
| 376 | if not _Confirm("Dev setup failed. Continue?"): |
| 377 | return False |
| 378 | if not SetupSsh(args): |
| 379 | return False |
| 380 | return True |
| 381 | |
| 382 | ############################################################################### |
| 383 | |
| 384 | _SETUP_COMMANDS = { |
| 385 | 'setup': Setup, |
| 386 | 'dev': SetupDev, |
| 387 | 'bash': SetBash, |
| 388 | 'bashrc': SetupBashrc, |
| 389 | 'ssh': SetupSsh, |
| 390 | 'sshauthorize': AuthorizeSsh, |
| 391 | 'sshfs': SetupSshfsForChrome, |
| 392 | } |
| 393 | |
| 394 | |
| 395 | _OTHER_COMMANDS = { |
| 396 | 'mountrw': MountWriteable, |
| 397 | 'ip': ShowIP, |
| 398 | 'test': TestCommand, |
| 399 | 'user': GetUser, |
| 400 | 'host': GetHost, |
| 401 | } |
| 402 | |
| 403 | |
| 404 | def GetUsage(commands): |
| 405 | """Get the docstring for each command.""" |
| 406 | usage = "" |
| 407 | for cmd in commands.items(): |
| 408 | usage += " " |
| 409 | usage += cmd[0] |
| 410 | usage += ":\t" |
| 411 | if len(cmd[0]) < 6: |
| 412 | usage += "\t" |
| 413 | doc = cmd[1].__doc__ |
| 414 | if doc: |
| 415 | usage += doc |
| 416 | usage += "\n" |
| 417 | return usage |
| 418 | |
| 419 | |
| 420 | ############################################################################### |
| 421 | |
| 422 | def main(): |
| 423 | """Main crdev function""" |
| 424 | usage = """usage: crdev command [options] |
| 425 | |
| 426 | Note: Beta! Send feature requests / changes to [email protected] (for now) |
| 427 | |
| 428 | """ |
| 429 | |
| 430 | usage += "Setup Commands:\n" |
| 431 | usage += GetUsage(_SETUP_COMMANDS) |
| 432 | usage += "Other Commands:\n" |
| 433 | usage += GetUsage(_OTHER_COMMANDS) |
| 434 | |
| 435 | parser = optparse.OptionParser(usage) |
| 436 | args = parser.parse_args()[1] |
| 437 | |
| 438 | if not args: |
| 439 | print usage |
| 440 | return |
| 441 | |
| 442 | if os.getenv('USER') != 'chronos': |
| 443 | print >> sys.stderr, "%s must be run as chronos." % _PROG |
| 444 | return |
| 445 | |
| 446 | cmd = args[0] |
| 447 | if cmd in _SETUP_COMMANDS.keys(): |
| 448 | res = _SETUP_COMMANDS[cmd](args[1:]) |
| 449 | elif cmd in _OTHER_COMMANDS.keys(): |
| 450 | res = _OTHER_COMMANDS[cmd](args[1:]) |
| 451 | else: |
| 452 | parser.error("Unknown command: " + cmd) |
| 453 | return |
| 454 | |
| 455 | if not res: |
| 456 | print >> sys.stderr, "Error running command: " + ' '.join(args) |
| 457 | else: |
| 458 | print "Success!" |
| 459 | |
| 460 | |
| 461 | if __name__ == '__main__': |
| 462 | main() |