266 lines
9.6 KiB
Python
266 lines
9.6 KiB
Python
# Copyright 2018 The Chromium Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""Implements commands for running and interacting with Fuchsia on devices."""
|
|
|
|
import boot_data
|
|
import logging
|
|
import os
|
|
import pkg_repo
|
|
import re
|
|
import subprocess
|
|
import target
|
|
import time
|
|
|
|
from common import EnsurePathExists, GetHostToolPathFromPlatform
|
|
|
|
# The maximum times to attempt mDNS resolution when connecting to a freshly
|
|
# booted Fuchsia instance before aborting.
|
|
BOOT_DISCOVERY_ATTEMPTS = 30
|
|
|
|
# Number of failed connection attempts before redirecting system logs to stdout.
|
|
CONNECT_RETRY_COUNT_BEFORE_LOGGING = 10
|
|
|
|
# Number of seconds between each device discovery.
|
|
BOOT_DISCOVERY_DELAY_SECS = 4
|
|
|
|
# Time between a reboot command is issued and when connection attempts from the
|
|
# host begin.
|
|
_REBOOT_SLEEP_PERIOD = 20
|
|
|
|
|
|
def GetTargetType():
|
|
return DeviceTarget
|
|
|
|
|
|
class DeviceTarget(target.Target):
|
|
"""Prepares a device to be used as a deployment target. Depending on the
|
|
command line parameters, it automatically handling a number of preparatory
|
|
steps relating to address resolution.
|
|
|
|
If |_node_name| is unset:
|
|
If there is one running device, use it for deployment and execution.
|
|
|
|
If there are more than one running devices, then abort and instruct the
|
|
user to re-run the command with |_node_name|
|
|
|
|
If |_node_name| is set:
|
|
If there is a running device with a matching nodename, then it is used
|
|
for deployment and execution.
|
|
|
|
If |_host| is set:
|
|
Deploy to a device at the host IP address as-is."""
|
|
|
|
def __init__(self,
|
|
out_dir,
|
|
target_cpu,
|
|
host=None,
|
|
node_name=None,
|
|
port=None,
|
|
ssh_config=None,
|
|
fuchsia_out_dir=None,
|
|
os_check='update',
|
|
system_log_file=None):
|
|
"""out_dir: The directory which will contain the files that are
|
|
generated to support the deployment.
|
|
target_cpu: The CPU architecture of the deployment target. Can be
|
|
"x64" or "arm64".
|
|
host: The address of the deployment target device.
|
|
node_name: The node name of the deployment target device.
|
|
port: The port of the SSH service on the deployment target device.
|
|
ssh_config: The path to SSH configuration data.
|
|
fuchsia_out_dir: The path to a Fuchsia build output directory, for
|
|
deployments to devices paved with local Fuchsia builds.
|
|
os_check: If 'check', the target's SDK version must match.
|
|
If 'update', the target will be repaved if the SDK versions
|
|
mismatch.
|
|
If 'ignore', the target's SDK version is ignored."""
|
|
|
|
super(DeviceTarget, self).__init__(out_dir, target_cpu)
|
|
|
|
self._system_log_file = system_log_file
|
|
self._host = host
|
|
self._port = port
|
|
self._fuchsia_out_dir = None
|
|
self._node_name = node_name
|
|
self._os_check = os_check
|
|
self._pkg_repo = None
|
|
|
|
if self._host and self._node_name:
|
|
raise Exception('Only one of "--host" or "--name" can be specified.')
|
|
|
|
if fuchsia_out_dir:
|
|
if ssh_config:
|
|
raise Exception('Only one of "--fuchsia-out-dir" or "--ssh_config" can '
|
|
'be specified.')
|
|
|
|
self._fuchsia_out_dir = os.path.expanduser(fuchsia_out_dir)
|
|
# Use SSH keys from the Fuchsia output directory.
|
|
self._ssh_config_path = os.path.join(self._fuchsia_out_dir, 'ssh-keys',
|
|
'ssh_config')
|
|
self._os_check = 'ignore'
|
|
|
|
elif ssh_config:
|
|
# Use the SSH config provided via the commandline.
|
|
self._ssh_config_path = os.path.expanduser(ssh_config)
|
|
|
|
else:
|
|
# Default to using an automatically generated SSH config and keys.
|
|
boot_data.ProvisionSSH()
|
|
self._ssh_config_path = boot_data.GetSSHConfigPath()
|
|
|
|
@staticmethod
|
|
def CreateFromArgs(args):
|
|
return DeviceTarget(args.out_dir, args.target_cpu, args.host,
|
|
args.node_name, args.port, args.ssh_config,
|
|
args.fuchsia_out_dir, args.os_check,
|
|
args.system_log_file)
|
|
|
|
@staticmethod
|
|
def RegisterArgs(arg_parser):
|
|
device_args = arg_parser.add_argument_group(
|
|
'device', 'External device deployment arguments')
|
|
device_args.add_argument('--host',
|
|
help='The IP of the target device. Optional.')
|
|
device_args.add_argument('--node-name',
|
|
help='The node-name of the device to boot or '
|
|
'deploy to. Optional, will use the first '
|
|
'discovered device if omitted.')
|
|
device_args.add_argument('--port',
|
|
'-p',
|
|
type=int,
|
|
default=None,
|
|
help='The port of the SSH service running on the '
|
|
'device. Optional.')
|
|
device_args.add_argument('--ssh-config',
|
|
'-F',
|
|
help='The path to the SSH configuration used for '
|
|
'connecting to the target device.')
|
|
device_args.add_argument(
|
|
'--os-check',
|
|
choices=['check', 'update', 'ignore'],
|
|
default='update',
|
|
help="Sets the OS version enforcement policy. If 'check', then the "
|
|
"deployment process will halt if the target\'s version doesn\'t "
|
|
"match. If 'update', then the target device will automatically "
|
|
"be repaved. If 'ignore', then the OS version won\'t be checked.")
|
|
|
|
def _ProvisionDeviceIfNecessary(self):
|
|
if self._Discover():
|
|
self._WaitUntilReady()
|
|
else:
|
|
raise Exception('Could not find device. If the device is connected '
|
|
'to the host remotely, make sure that --host flag is '
|
|
'set and that remote serving is set up.')
|
|
|
|
def _Discover(self):
|
|
"""Queries mDNS for the IP address of a booted Fuchsia instance whose name
|
|
matches |_node_name| on the local area network. If |_node_name| isn't
|
|
specified, and there is only one device on the network, then returns the
|
|
IP address of that advice.
|
|
|
|
Sets |_host_name| and returns True if the device was found,
|
|
or waits up to |timeout| seconds and returns False if the device couldn't
|
|
be found."""
|
|
|
|
dev_finder_path = GetHostToolPathFromPlatform('device-finder')
|
|
|
|
if self._node_name:
|
|
command = [
|
|
dev_finder_path,
|
|
'resolve',
|
|
'-device-limit',
|
|
'1', # Exit early as soon as a host is found.
|
|
self._node_name
|
|
]
|
|
proc = subprocess.Popen(command,
|
|
stdout=subprocess.PIPE,
|
|
stderr=open(os.devnull, 'w'))
|
|
else:
|
|
proc = self.RunFFXCommand(['target', 'list', '-f', 'simple'],
|
|
stdout=subprocess.PIPE,
|
|
stderr=open(os.devnull, 'w'))
|
|
|
|
output = set(proc.communicate()[0].strip().split('\n'))
|
|
if proc.returncode != 0:
|
|
return False
|
|
|
|
if self._node_name:
|
|
# Handle the result of "device-finder resolve".
|
|
self._host = output.pop().strip()
|
|
else:
|
|
name_host_pairs = [x.strip().split(' ') for x in output]
|
|
|
|
if len(name_host_pairs) > 1:
|
|
logging.info('More than one device was discovered on the network. '
|
|
'Use --node-name <name> to specify the device to use.')
|
|
logging.info('List of devices:')
|
|
logging.info(output)
|
|
raise Exception('Ambiguous target device specification.')
|
|
assert len(name_host_pairs) == 1
|
|
# Check if device has both address and name.
|
|
if len(name_host_pairs[0]) < 2:
|
|
return False
|
|
self._host, self._node_name = name_host_pairs[0]
|
|
|
|
logging.info('Found device "%s" at address %s.' % (self._node_name,
|
|
self._host))
|
|
|
|
return True
|
|
|
|
def Start(self):
|
|
if self._host:
|
|
self._WaitUntilReady()
|
|
else:
|
|
self._ProvisionDeviceIfNecessary()
|
|
|
|
def GetPkgRepo(self):
|
|
if not self._pkg_repo:
|
|
if self._fuchsia_out_dir:
|
|
# Deploy to an already-booted device running a local Fuchsia build.
|
|
self._pkg_repo = pkg_repo.ExternalPkgRepo(
|
|
os.path.join(self._fuchsia_out_dir, 'amber-files'))
|
|
else:
|
|
# Create an ephemeral package repository, then start both "pm serve" as
|
|
# well as the bootserver.
|
|
self._pkg_repo = pkg_repo.ManagedPkgRepo(self)
|
|
|
|
return self._pkg_repo
|
|
|
|
def _ParseNodename(self, output):
|
|
# Parse the nodename from bootserver stdout.
|
|
m = re.search(r'.*Proceeding with nodename (?P<nodename>.*)$', output,
|
|
re.MULTILINE)
|
|
if not m:
|
|
raise Exception('Couldn\'t parse nodename from bootserver output.')
|
|
self._node_name = m.groupdict()['nodename']
|
|
logging.info('Booted device "%s".' % self._node_name)
|
|
|
|
# Repeatedly search for a device for |BOOT_DISCOVERY_ATTEMPT|
|
|
# number of attempts. If a device isn't found, wait
|
|
# |BOOT_DISCOVERY_DELAY_SECS| before searching again.
|
|
logging.info('Waiting for device to join network.')
|
|
for _ in xrange(BOOT_DISCOVERY_ATTEMPTS):
|
|
if self._Discover():
|
|
break
|
|
time.sleep(BOOT_DISCOVERY_DELAY_SECS)
|
|
|
|
if not self._host:
|
|
raise Exception('Device %s couldn\'t be discovered via mDNS.' %
|
|
self._node_name)
|
|
|
|
self._WaitUntilReady();
|
|
|
|
def _GetEndpoint(self):
|
|
return (self._host, self._port)
|
|
|
|
def _GetSshConfigPath(self):
|
|
return self._ssh_config_path
|
|
|
|
def Restart(self):
|
|
"""Restart the device."""
|
|
|
|
self.RunCommandPiped('dm reboot')
|
|
time.sleep(_REBOOT_SLEEP_PERIOD)
|
|
self.Start()
|