summaryrefslogtreecommitdiff
path: root/cli/cros/cros_shell.py
diff options
context:
space:
mode:
authorDavid Pursell <dpursell@chromium.org>2015-03-19 08:53:48 -0700
committerChromeOS Commit Bot <chromeos-commit-bot@chromium.org>2015-03-23 08:39:07 +0000
commitb5bacc2c7d0175d146b2fd073d485cc02a11cbcb (patch)
treeb2873061ffbdc9e4a3b62c618a5466b79a893fff /cli/cros/cros_shell.py
parentf1c27c14ee2150d4c86843eb1b2be0b1ab59acfc (diff)
downloadchromite-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.py187
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