diff options
author | David Pursell <dpursell@chromium.org> | 2015-03-19 08:53:48 -0700 |
---|---|---|
committer | ChromeOS Commit Bot <chromeos-commit-bot@chromium.org> | 2015-03-23 08:39:07 +0000 |
commit | b5bacc2c7d0175d146b2fd073d485cc02a11cbcb (patch) | |
tree | b2873061ffbdc9e4a3b62c618a5466b79a893fff /cli/cros/cros_shell.py | |
parent | f1c27c14ee2150d4c86843eb1b2be0b1ab59acfc (diff) | |
download | chromite-b5bacc2c7d0175d146b2fd073d485cc02a11cbcb.tar.gz |
cli: Move cros/commands/ to cli/cros/.
This CL moves files from cros/commands to cli/cros. This is part of the
Brillo entry point work, and enables a better folder structure to
support multiple CLIs.
This is a non-functional change, so that moving and modifying files is
done in different CLs for easier review.
See brbug.com/557 for more info.
BUG=brillo:557
TEST=None
CQ-DEPEND=CL:261183
Change-Id: I91bf24cce6f706736ecc2441bc005c8c1cd4c2b3
Reviewed-on: https://chromium-review.googlesource.com/261182
Reviewed-by: David Pursell <dpursell@chromium.org>
Commit-Queue: David Pursell <dpursell@chromium.org>
Trybot-Ready: David Pursell <dpursell@chromium.org>
Tested-by: David Pursell <dpursell@chromium.org>
Diffstat (limited to 'cli/cros/cros_shell.py')
-rw-r--r-- | cli/cros/cros_shell.py | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/cli/cros/cros_shell.py b/cli/cros/cros_shell.py new file mode 100644 index 000000000..b77647147 --- /dev/null +++ b/cli/cros/cros_shell.py @@ -0,0 +1,187 @@ +# Copyright 2015 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. + +"""cros shell: Open a remote shell on the target device.""" + +from __future__ import print_function + +import argparse + +from chromite.cli import command +from chromite.lib import commandline +from chromite.lib import cros_build_lib +from chromite.lib import cros_logging as logging +from chromite.lib import osutils +from chromite.lib import remote_access + + +@command.CommandDecorator('shell') +class ShellCommand(command.CrosCommand): + """Opens a remote shell over SSH on the target device. + + Can be used to start an interactive session or execute a command + remotely. Interactive sessions can be terminated like a normal SSH + session using Ctrl+D, `exit`, or `logout`. + + Unlike other `cros` commands, this allows for both SSH key and user + password authentication. Because a password may be transmitted, the + known_hosts file is used by default to protect against connecting to + the wrong device. + + The exit code will be the same as the last executed command. + """ + + EPILOG = """ +Examples: + Start an interactive session: + cros shell <ip> + cros shell <user>@<ip>:<port> + + Non-interactive remote command: + cros shell <ip> -- cat var/log/messages + +Quoting can be tricky; the rules are the same as with ssh: + Special symbols will end the command unless quoted: + cros shell <ip> -- cat /var/log/messages > log.txt (saves locally) + cros shell <ip> -- "cat /var/log/messages > log.txt" (saves remotely) + + One set of quotes is consumed locally, so remote commands that + require quotes will need double quoting: + cros shell <ip> -- sh -c "exit 42" (executes: sh -c exit 42) + cros shell <ip> -- sh -c "'exit 42'" (executes: sh -c 'exit 42') +""" + + # Override base class property to enable stats upload. + upload_stats = True + + def __init__(self, options): + """Initializes ShellCommand.""" + super(ShellCommand, self).__init__(options) + # SSH connection settings. + self.ssh_hostname = None + self.ssh_port = None + self.ssh_username = None + self.ssh_private_key = None + # Whether to use the SSH known_hosts file or not. + self.known_hosts = None + # How to set SSH StrictHostKeyChecking. Can be 'no', 'yes', or 'ask'. Has + # no effect if |known_hosts| is not True. + self.host_key_checking = None + + @classmethod + def AddParser(cls, parser): + """Adds a parser.""" + super(cls, ShellCommand).AddParser(parser) + parser.add_argument( + 'device', + type=commandline.DeviceParser(commandline.DEVICE_SCHEME_SSH), + help='[user@]IP[:port] address of the target device. Defaults to ' + 'user=root, port=22') + parser.add_argument( + '--private-key', type='path', default=None, + help='SSH identify file (private key).') + parser.add_argument( + '--no-known-hosts', action='store_false', dest='known_hosts', + default=True, help='Do not use a known_hosts file.') + parser.add_argument( + 'command', nargs=argparse.REMAINDER, + help='(optional) Command to execute on the device.') + + def _ReadOptions(self): + """Processes options and set variables.""" + self.ssh_hostname = self.options.device.hostname + self.ssh_username = self.options.device.username + self.ssh_port = self.options.device.port + self.ssh_private_key = self.options.private_key + self.known_hosts = self.options.known_hosts + # By default ask the user if a new key is found. SSH will still reject + # modified keys for existing hosts without asking the user. + self.host_key_checking = 'ask' + + def _ConnectSettings(self): + """Generates the correct SSH connect settings based on our state.""" + kwargs = {'NumberOfPasswordPrompts': 2} + if self.known_hosts: + # Use the default known_hosts and our current key check setting. + kwargs['UserKnownHostsFile'] = None + kwargs['StrictHostKeyChecking'] = self.host_key_checking + return remote_access.CompileSSHConnectSettings(**kwargs) + + def _UserConfirmKeyChange(self): + """Asks the user whether it's OK that a host key has changed. + + A changed key can be fairly common during Chrome OS development, so + instead of outright rejecting a modified key like SSH does, this + provides some common reasons a key may have changed to help the + user decide whether it was legitimate or not. + + Returns: + True if the user is OK with a changed host key. + """ + return cros_build_lib.BooleanPrompt( + prolog='The host ID for "%s" has changed since last connect.\n' + 'Some common reasons for this are:\n' + ' - Device powerwash.\n' + ' - Device flash from a USB stick.\n' + ' - Device flash using "cros flash --clobber-stateful".\n' + 'Otherwise, please verify that this is the correct device' + ' before continuing.' % self.ssh_hostname) + + def _StartSsh(self): + """Starts an SSH session or executes a remote command. + + Requires that _ReadOptions() has already been called to provide the + SSH configuration. + + Returns: + The SSH return code. + + Raises: + SSHConnectionError on SSH connect failure. + """ + with osutils.TempDir(prefix='cros-shell-tmp') as tempdir: + # Use the basic RemoteAccess class rather than the more powerful + # ChromiumOSDevice/RemoteDevice classes because: + # 1. We don't need the additional features for a basic SSH connection. + # 2. These classes add additional SSH commands for setup, which makes + # usage really awkward with password authentication. + remote = remote_access.RemoteAccess( + self.ssh_hostname, tempdir, port=self.ssh_port, + username=self.ssh_username, private_key=self.ssh_private_key) + return remote.RemoteSh(self.options.command, + connect_settings=self._ConnectSettings(), + error_code_ok=True, + mute_output=False, + redirect_stderr=True, + capture_output=False).returncode + + def Run(self): + """Runs `cros shell`.""" + self.options.Freeze() + self._ReadOptions() + # Nested try blocks so the inner can raise to the outer, which handles + # overall failures. + try: + try: + return self._StartSsh() + except remote_access.SSHConnectionError as e: + # Handle a mismatched host key; mismatched keys are a bit of a pain to + # fix manually since `ssh-keygen -R` doesn't work within the chroot. + if e.IsKnownHostsMismatch(): + # The full SSH error message has extra info for the user. + logging.warning('\n%s', e) + if self._UserConfirmKeyChange(): + remote_access.RemoveKnownHost(self.ssh_hostname) + # The user already OK'd so we can skip the additional SSH check. + self.host_key_checking = 'no' + return self._StartSsh() + else: + raise + else: + raise + except (Exception, KeyboardInterrupt) as e: + logging.error('\n%s', e) + logging.error('`cros shell` failed.') + if self.options.debug: + raise |