336 lines
13 KiB
Python
336 lines
13 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.
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import time
|
|
|
|
import common
|
|
import remote_cmd
|
|
import runner_logs
|
|
|
|
|
|
_SHUTDOWN_CMD = ['dm', 'poweroff']
|
|
_ATTACH_RETRY_INTERVAL = 1
|
|
_ATTACH_RETRY_SECONDS = 120
|
|
|
|
# Amount of time to wait for a complete package installation, as a
|
|
# mitigation against hangs due to pkg/network-related failures.
|
|
_INSTALL_TIMEOUT_SECS = 10 * 60
|
|
|
|
|
|
def _GetPackageUri(package_name):
|
|
"""Returns the URI for the specified package name."""
|
|
return 'fuchsia-pkg://fuchsia.com/%s' % (package_name)
|
|
|
|
|
|
def _GetPackageInfo(package_path):
|
|
"""Returns a tuple with the name and version of a package."""
|
|
|
|
# Query the metadata file which resides next to the package file.
|
|
package_info = json.load(
|
|
open(os.path.join(os.path.dirname(package_path), 'package')))
|
|
return package_info['name'], package_info['version'],
|
|
|
|
|
|
class _MapIsolatedPathsForPackage:
|
|
"""Callable object which remaps /data and /tmp paths to their component-
|
|
specific locations, based on the package name and test realm path."""
|
|
|
|
def __init__(self, package_name, package_version, realms):
|
|
realms_path_fragment = '/r/'.join(['r/sys'] + realms)
|
|
package_sub_path = '{2}/fuchsia.com:{0}:{1}#meta:{0}.cmx/'.format(
|
|
package_name, package_version, realms_path_fragment)
|
|
self.isolated_format = '{0}' + package_sub_path + '{1}'
|
|
|
|
def __call__(self, path):
|
|
for isolated_directory in ['/data/' , '/tmp/']:
|
|
if (path+'/').startswith(isolated_directory):
|
|
return self.isolated_format.format(isolated_directory,
|
|
path[len(isolated_directory):])
|
|
return path
|
|
|
|
|
|
class FuchsiaTargetException(Exception):
|
|
def __init__(self, message):
|
|
super(FuchsiaTargetException, self).__init__(message)
|
|
|
|
|
|
class Target(object):
|
|
"""Base class representing a Fuchsia deployment target."""
|
|
|
|
def __init__(self, out_dir, target_cpu):
|
|
self._out_dir = out_dir
|
|
self._started = False
|
|
self._dry_run = False
|
|
self._target_cpu = target_cpu
|
|
self._command_runner = None
|
|
self._ffx_path = os.path.join(common.SDK_ROOT, 'tools',
|
|
common.GetHostArchFromPlatform(), 'ffx')
|
|
|
|
@staticmethod
|
|
def CreateFromArgs(args):
|
|
raise NotImplementedError()
|
|
|
|
@staticmethod
|
|
def RegisterArgs(arg_parser):
|
|
pass
|
|
|
|
# Functions used by the Python context manager for teardown.
|
|
def __enter__(self):
|
|
return self
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
return
|
|
|
|
def Start(self):
|
|
"""Handles the instantiation and connection process for the Fuchsia
|
|
target instance."""
|
|
|
|
def IsStarted(self):
|
|
"""Returns True if the Fuchsia target instance is ready to accept
|
|
commands."""
|
|
return self._started
|
|
|
|
def IsNewInstance(self):
|
|
"""Returns True if the connected target instance is newly provisioned."""
|
|
return True
|
|
|
|
def GetCommandRunner(self):
|
|
"""Returns CommandRunner that can be used to execute commands on the
|
|
target. Most clients should prefer RunCommandPiped() and RunCommand()."""
|
|
self._AssertIsStarted()
|
|
|
|
if self._command_runner is None:
|
|
host, port = self._GetEndpoint()
|
|
self._command_runner = \
|
|
remote_cmd.CommandRunner(self._GetSshConfigPath(), host, port)
|
|
|
|
return self._command_runner
|
|
|
|
def RunCommandPiped(self, command, **kwargs):
|
|
"""Starts a remote command and immediately returns a Popen object for the
|
|
command. The caller may interact with the streams, inspect the status code,
|
|
wait on command termination, etc.
|
|
|
|
command: A list of strings representing the command and arguments.
|
|
kwargs: A dictionary of parameters to be passed to subprocess.Popen().
|
|
The parameters can be used to override stdin and stdout, for
|
|
example.
|
|
|
|
Returns: a Popen object.
|
|
|
|
Note: method does not block.
|
|
"""
|
|
logging.debug('running (non-blocking) \'%s\'.', ' '.join(command))
|
|
return self.GetCommandRunner().RunCommandPiped(command, **kwargs)
|
|
|
|
def RunCommand(self, command, silent=False, timeout_secs=None):
|
|
"""Executes a remote command and waits for it to finish executing.
|
|
|
|
Returns the exit code of the command.
|
|
"""
|
|
logging.debug('running \'%s\'.', ' '.join(command))
|
|
return self.GetCommandRunner().RunCommand(command, silent,
|
|
timeout_secs=timeout_secs)
|
|
|
|
def EnsureIsolatedPathsExist(self, for_package, for_realms):
|
|
"""Ensures that the package's isolated /data and /tmp exist."""
|
|
for isolated_directory in ['/data', '/tmp']:
|
|
self.RunCommand([
|
|
'mkdir', '-p',
|
|
_MapIsolatedPathsForPackage(for_package, 0,
|
|
for_realms)(isolated_directory)
|
|
])
|
|
|
|
def PutFile(self,
|
|
source,
|
|
dest,
|
|
recursive=False,
|
|
for_package=None,
|
|
for_realms=()):
|
|
"""Copies a file from the local filesystem to the target filesystem.
|
|
|
|
source: The path of the file being copied.
|
|
dest: The path on the remote filesystem which will be copied to.
|
|
recursive: If true, performs a recursive copy.
|
|
for_package: If specified, isolated paths in the |dest| are mapped to their
|
|
obsolute paths for the package, on the target. This currently
|
|
affects the /data and /tmp directories.
|
|
for_realms: If specified, identifies the sub-realm of 'sys' under which
|
|
isolated paths (see |for_package|) are stored.
|
|
"""
|
|
assert type(source) is str
|
|
self.PutFiles([source], dest, recursive, for_package, for_realms)
|
|
|
|
def PutFiles(self,
|
|
sources,
|
|
dest,
|
|
recursive=False,
|
|
for_package=None,
|
|
for_realms=()):
|
|
"""Copies files from the local filesystem to the target filesystem.
|
|
|
|
sources: List of local file paths to copy from, or a single path.
|
|
dest: The path on the remote filesystem which will be copied to.
|
|
recursive: If true, performs a recursive copy.
|
|
for_package: If specified, /data in the |dest| is mapped to the package's
|
|
isolated /data location.
|
|
for_realms: If specified, identifies the sub-realm of 'sys' under which
|
|
isolated paths (see |for_package|) are stored.
|
|
"""
|
|
assert type(sources) is tuple or type(sources) is list
|
|
if for_package:
|
|
self.EnsureIsolatedPathsExist(for_package, for_realms)
|
|
dest = _MapIsolatedPathsForPackage(for_package, 0, for_realms)(dest)
|
|
logging.debug('copy local:%s => remote:%s', sources, dest)
|
|
self.GetCommandRunner().RunScp(sources, dest, remote_cmd.COPY_TO_TARGET,
|
|
recursive)
|
|
|
|
def GetFile(self,
|
|
source,
|
|
dest,
|
|
for_package=None,
|
|
for_realms=(),
|
|
recursive=False):
|
|
"""Copies a file from the target filesystem to the local filesystem.
|
|
|
|
source: The path of the file being copied.
|
|
dest: The path on the local filesystem which will be copied to.
|
|
for_package: If specified, /data in paths in |sources| is mapped to the
|
|
package's isolated /data location.
|
|
for_realms: If specified, identifies the sub-realm of 'sys' under which
|
|
isolated paths (see |for_package|) are stored.
|
|
recursive: If true, performs a recursive copy.
|
|
"""
|
|
assert type(source) is str
|
|
self.GetFiles([source], dest, for_package, for_realms, recursive)
|
|
|
|
def GetFiles(self,
|
|
sources,
|
|
dest,
|
|
for_package=None,
|
|
for_realms=(),
|
|
recursive=False):
|
|
"""Copies files from the target filesystem to the local filesystem.
|
|
|
|
sources: List of remote file paths to copy.
|
|
dest: The path on the local filesystem which will be copied to.
|
|
for_package: If specified, /data in paths in |sources| is mapped to the
|
|
package's isolated /data location.
|
|
for_realms: If specified, identifies the sub-realm of 'sys' under which
|
|
isolated paths (see |for_package|) are stored.
|
|
recursive: If true, performs a recursive copy.
|
|
"""
|
|
assert type(sources) is tuple or type(sources) is list
|
|
self._AssertIsStarted()
|
|
if for_package:
|
|
sources = map(_MapIsolatedPathsForPackage(for_package, 0, for_realms),
|
|
sources)
|
|
logging.debug('copy remote:%s => local:%s', sources, dest)
|
|
return self.GetCommandRunner().RunScp(sources, dest,
|
|
remote_cmd.COPY_FROM_TARGET,
|
|
recursive)
|
|
|
|
def _GetEndpoint(self):
|
|
"""Returns a (host, port) tuple for the SSH connection to the target."""
|
|
raise NotImplementedError()
|
|
|
|
def _GetTargetSdkArch(self):
|
|
"""Returns the Fuchsia SDK architecture name for the target CPU."""
|
|
if self._target_cpu == 'arm64' or self._target_cpu == 'x64':
|
|
return self._target_cpu
|
|
raise FuchsiaTargetException('Unknown target_cpu:' + self._target_cpu)
|
|
|
|
def _AssertIsStarted(self):
|
|
assert self.IsStarted()
|
|
|
|
def _WaitUntilReady(self):
|
|
logging.info('Connecting to Fuchsia using SSH.')
|
|
|
|
host, port = self._GetEndpoint()
|
|
end_time = time.time() + _ATTACH_RETRY_SECONDS
|
|
ssh_diagnostic_log = runner_logs.FileStreamFor('ssh_diagnostic_log')
|
|
while time.time() < end_time:
|
|
runner = remote_cmd.CommandRunner(self._GetSshConfigPath(), host, port)
|
|
ssh_proc = runner.RunCommandPiped(['true'],
|
|
ssh_args=['-v'],
|
|
stdout=ssh_diagnostic_log,
|
|
stderr=subprocess.STDOUT)
|
|
if ssh_proc.wait() == 0:
|
|
logging.info('Connected!')
|
|
self._started = True
|
|
return True
|
|
time.sleep(_ATTACH_RETRY_INTERVAL)
|
|
|
|
logging.error('Timeout limit reached.')
|
|
|
|
raise FuchsiaTargetException('Couldn\'t connect using SSH.')
|
|
|
|
def _GetSshConfigPath(self, path):
|
|
raise NotImplementedError()
|
|
|
|
def GetPkgRepo(self):
|
|
"""Returns an PkgRepo instance which serves packages for this Target.
|
|
Callers should typically call GetPkgRepo() in a |with| statement, and
|
|
install and execute commands inside the |with| block, so that the returned
|
|
PkgRepo can teardown correctly, if necessary.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def InstallPackage(self, package_paths):
|
|
"""Installs a package and it's dependencies on the device. If the package is
|
|
already installed then it will be updated to the new version.
|
|
|
|
package_paths: Paths to the .far files to install.
|
|
"""
|
|
with self.GetPkgRepo() as pkg_repo:
|
|
# Publish all packages to the serving TUF repository under |tuf_root|.
|
|
for package_path in package_paths:
|
|
pkg_repo.PublishPackage(package_path)
|
|
|
|
# Resolve all packages, to have them pulled into the device/VM cache.
|
|
for package_path in package_paths:
|
|
package_name, package_version = _GetPackageInfo(package_path)
|
|
logging.info('Resolving %s into cache.', package_name)
|
|
return_code = self.RunCommand(
|
|
['pkgctl', 'resolve',
|
|
_GetPackageUri(package_name), '>/dev/null'],
|
|
timeout_secs=_INSTALL_TIMEOUT_SECS)
|
|
if return_code != 0:
|
|
raise Exception(
|
|
'Error {} while resolving {}.'.format(return_code, package_name))
|
|
|
|
# Verify that the newly resolved versions of packages are reported.
|
|
for package_path in package_paths:
|
|
# Use pkgctl get-hash to determine which version will be resolved.
|
|
package_name, package_version = _GetPackageInfo(package_path)
|
|
pkgctl = self.RunCommandPiped(
|
|
['pkgctl', 'get-hash',
|
|
_GetPackageUri(package_name)],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
pkgctl_out, pkgctl_err = pkgctl.communicate()
|
|
|
|
# Read the expected version from the meta.far Merkel hash file alongside
|
|
# the package's FAR.
|
|
meta_far_path = os.path.join(os.path.dirname(package_path), 'meta.far')
|
|
meta_far_merkel = subprocess.check_output(
|
|
[common.GetHostToolPathFromPlatform('merkleroot'),
|
|
meta_far_path]).split()[0]
|
|
if pkgctl_out != meta_far_merkel:
|
|
raise Exception('Hash mismatch for %s after resolve (%s vs %s).' %
|
|
(package_name, pkgctl_out, meta_far_merkel))
|
|
|
|
def RunFFXCommand(self, ffx_args, **kwargs):
|
|
"""Automatically gets the FFX path and runs FFX based on the
|
|
arguments provided. Extra args can be added to be used with Popen.
|
|
|
|
ffx_args: The arguments for a ffx command.
|
|
kwargs: A dictionary of parameters to be passed to subprocess.Popen().
|
|
|
|
Returns a Popen object for the command."""
|
|
command = [self._ffx_path] + ffx_args
|
|
return subprocess.Popen(command, **kwargs)
|