# 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.""" from __future__ import print_function import amber_repo import boot_data import filecmp import logging import os import re import subprocess import sys import target import tempfile import time import uuid from common import SDK_ROOT, 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 seconds to wait when querying a list of all devices over mDNS. _LIST_DEVICES_TIMEOUT_SECS = 3 #Number of failed connection attempts before redirecting system logs to stdout. CONNECT_RETRY_COUNT_BEFORE_LOGGING = 10 TARGET_HASH_FILE_PATH = '/data/.hash' 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, device provisioning, and SDK versioning. If |_node_name| is unset: If there is one running device, use it for deployment and execution. The device's SDK version is checked unless --os-check=ignore is set. If --os-check=update is set, then the target device is repaved if the SDK version doesn't match. If there are more than one running devices, then abort and instruct the user to re-run the command with |_node_name| Otherwise, if there are no running devices, then search for a device running Zedboot, and pave it. If |_node_name| is set: If there is a running device with a matching nodename, then it is used for deployment and execution. Otherwise, attempt to pave a device with a matching nodename, and use it for deployment and execution. If |_host| is set: Deploy to a device at the host IP address as-is.""" def __init__(self, output_dir, target_cpu, host=None, node_name=None, port=None, ssh_config=None, fuchsia_out_dir=None, os_check='update', system_log_file=None): """output_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__(output_dir, target_cpu) self._port = port if port else 22 self._system_log_file = system_log_file self._loglistener = None self._host = host self._fuchsia_out_dir = os.path.expanduser(fuchsia_out_dir) self._node_name = node_name self._os_check = os_check self._amber_repo = None if self._host and self._node_name: raise Exception('Only one of "--host" or "--name" can be specified.') if self._fuchsia_out_dir: if ssh_config: raise Exception('Only one of "--fuchsia-out-dir" or "--ssh_config" can ' 'be specified.') # 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(output_dir) self._ssh_config_path = boot_data.GetSSHConfigPath(output_dir) def __exit__(self, exc_type, exc_val, exc_tb): if self._loglistener: self._loglistener.kill() def _SDKHashMatches(self): """Checks if /data/.hash on the device matches SDK_ROOT/.hash. Returns True if the files are identical, or False otherwise. """ with tempfile.NamedTemporaryFile() as tmp: try: self.GetFile(TARGET_HASH_FILE_PATH, tmp.name) except subprocess.CalledProcessError: # If the file is unretrievable for whatever reason, assume mismatch. return False return filecmp.cmp(tmp.name, os.path.join(SDK_ROOT, '.hash'), False) 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] else: command = [ dev_finder_path, 'list', '-full', '-timeout', "%ds" % _LIST_DEVICES_TIMEOUT_SECS ] proc = subprocess.Popen(command, 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] # Handle the output of "device-finder list". if len(name_host_pairs) > 1: print('More than one device was discovered on the network.') print('Use --node-name to specify the device to use.') print('\nList of devices:') for pair in name_host_pairs: print(' ' + pair[1]) print() raise Exception('Ambiguous target device specification.') assert len(name_host_pairs) == 1 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: should_provision = False if self.__Discover(): self._WaitUntilReady() if self._os_check != 'ignore': if self._SDKHashMatches(): if self._os_check == 'update': logging.info( 'SDK hash does not match; rebooting and repaving.') self.RunCommand(['dm', 'reboot']) should_provision = True elif self._os_check == 'check': raise Exception('Target device SDK version does not match.') else: should_provision = True if should_provision: boot_data.AssertBootImagesExist(self._GetTargetSdkArch(), 'generic') self.__ProvisionDevice() assert self._node_name assert self._host def GetAmberRepo(self): if not self._amber_repo: if self._fuchsia_out_dir: # Deploy to an already-booted device running a local Fuchsia build. self._amber_repo = amber_repo.ExternalAmberRepo( os.path.join(self._fuchsia_out_dir, 'amber-files')) else: # Create an ephemeral Amber repo, then start both "pm serve" as well as # the bootserver. self._amber_repo = amber_repo.ManagedAmberRepo(self) return self._amber_repo def __ProvisionDevice(self): """Netboots a device with Fuchsia. If |_node_name| is set, then only a device with a matching node name is used. The device is up and reachable via SSH when the function is successfully completes.""" bootserver_path = GetHostToolPathFromPlatform('bootserver') bootserver_command = [ bootserver_path, '-1', '--fvm', EnsurePathExists( boot_data.GetTargetFile('storage-sparse.blk', self._GetTargetSdkArch(), boot_data.TARGET_TYPE_GENERIC)), EnsurePathExists(boot_data.GetBootImage(self._output_dir, self._GetTargetSdkArch(), boot_data.TARGET_TYPE_GENERIC))] if self._node_name: bootserver_command += ['-n', self._node_name] bootserver_command += ['--'] bootserver_command += boot_data.GetKernelArgs(self._output_dir) logging.debug(' '.join(bootserver_command)) stdout = subprocess.check_output(bootserver_command, stderr=subprocess.STDOUT) # Parse the nodename from bootserver stdout. m = re.search(r'.*Proceeding with nodename (?P.*)$', stdout, 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) # Start loglistener to save system logs. if self._system_log_file: loglistener_path = GetHostToolPathFromPlatform('loglistener') self._loglistener = subprocess.Popen( [loglistener_path, self._node_name], stdout=self._system_log_file, stderr=subprocess.STDOUT, stdin=open(os.devnull)) # Repeatdly query mDNS until we find the device, or we hit the timeout of # DISCOVERY_TIMEOUT_SECS. logging.info('Waiting for device to join network.') for _ in xrange(_BOOT_DISCOVERY_ATTEMPTS): if self.__Discover(): break if not self._host: raise Exception('Device %s couldn\'t be discovered via mDNS.' % self._node_name) self._WaitUntilReady(); # Update the target's hash to match the current tree's. self.PutFile(os.path.join(SDK_ROOT, '.hash'), TARGET_HASH_FILE_PATH) def _GetEndpoint(self): return (self._host, self._port) def _GetSshConfigPath(self): return self._ssh_config_path