| # Copyright 2019 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 common |
| import json |
| import logging |
| import os |
| import shutil |
| import subprocess |
| import tempfile |
| import time |
| |
| from six.moves import urllib |
| |
| |
| # Maximum amount of time to block while waiting for "pm serve" to come up. |
| _PM_SERVE_LIVENESS_TIMEOUT_SECS = 10 |
| |
| _MANAGED_REPO_NAME = 'chrome_runner' |
| |
| |
| class AmberRepo(object): |
| """Abstract interface for a repository used to serve packages to devices.""" |
| |
| def __init__(self, target): |
| self._target = target |
| |
| def PublishPackage(self, package_path): |
| pm_tool = common.GetHostToolPathFromPlatform('pm') |
| subprocess.check_call( |
| [pm_tool, 'publish', '-a', '-f', package_path, '-r', self.GetPath(), |
| '-vt', '-v'], |
| stderr=subprocess.STDOUT) |
| |
| def GetPath(self): |
| pass |
| |
| |
| class ManagedAmberRepo(AmberRepo): |
| """Creates and serves packages from an ephemeral repository.""" |
| |
| def __init__(self, target): |
| AmberRepo.__init__(self, target) |
| self._with_count = 0 |
| |
| self._amber_root = tempfile.mkdtemp() |
| pm_tool = common.GetHostToolPathFromPlatform('pm') |
| subprocess.check_call([pm_tool, 'newrepo', '-repo', self._amber_root]) |
| logging.info('Creating and serving temporary Amber root: {}.'.format( |
| self._amber_root)) |
| |
| serve_port = common.GetAvailableTcpPort() |
| self._pm_serve_task = subprocess.Popen( |
| [pm_tool, 'serve', '-d', os.path.join(self._amber_root, 'repository'), |
| '-l', ':%d' % serve_port, '-q']) |
| |
| # Block until "pm serve" starts serving HTTP traffic at |serve_port|. |
| timeout = time.time() + _PM_SERVE_LIVENESS_TIMEOUT_SECS |
| while True: |
| try: |
| urllib.request.urlopen('http://localhost:%d' % serve_port, |
| timeout=1).read() |
| break |
| except urllib.error.URLError: |
| logging.info('Waiting until \'pm serve\' is up...') |
| |
| if time.time() >= timeout: |
| raise Exception('Timed out while waiting for \'pm serve\'.') |
| |
| time.sleep(1) |
| |
| remote_port = common.ConnectPortForwardingTask(target, serve_port, 0) |
| self._RegisterAmberRepository(self._amber_root, remote_port) |
| |
| def __enter__(self): |
| self._with_count += 1 |
| return self |
| |
| def __exit__(self, type, value, tb): |
| """Allows the repository to delete itself when it leaves the scope of a |
| 'with' block.""" |
| self._with_count -= 1 |
| if self._with_count > 0: |
| return |
| |
| logging.info('Cleaning up Amber root: ' + self._amber_root) |
| shutil.rmtree(self._amber_root) |
| self._amber_root = None |
| |
| self._UnregisterAmberRepository() |
| self._pm_serve_task.kill() |
| self._pm_serve_task = None |
| |
| def GetPath(self): |
| return self._amber_root |
| |
| def _RegisterAmberRepository(self, tuf_repo, remote_port): |
| """Configures a device to use a local TUF repository as an installation |
| source for packages. |
| |tuf_repo|: The host filesystem path to the TUF repository. |
| |remote_port|: The reverse-forwarded port used to connect to instance of |
| `pm serve` that is serving the contents of |tuf_repo|.""" |
| |
| # Extract the public signing key for inclusion in the config file. |
| root_keys = [] |
| root_json_path = os.path.join(tuf_repo, 'repository', 'root.json') |
| root_json = json.load(open(root_json_path, 'r')) |
| for root_key_id in root_json['signed']['roles']['root']['keyids']: |
| root_keys.append({ |
| 'Type': root_json['signed']['keys'][root_key_id]['keytype'], |
| 'Value': root_json['signed']['keys'][root_key_id]['keyval']['public'] |
| }) |
| |
| # "pm serve" can automatically generate a "config.json" file at query time, |
| # but the file is unusable because it specifies URLs with port |
| # numbers that are unreachable from across the port forwarding boundary. |
| # So instead, we generate our own config file with the forwarded port |
| # numbers instead. |
| config_file = open(os.path.join(tuf_repo, 'repository', 'repo_config.json'), |
| 'w') |
| json.dump({ |
| 'ID': _MANAGED_REPO_NAME, |
| 'RepoURL': "http://127.0.0.1:%d" % remote_port, |
| 'BlobRepoURL': "http://127.0.0.1:%d/blobs" % remote_port, |
| 'RatePeriod': 10, |
| 'RootKeys': root_keys, |
| 'StatusConfig': { |
| 'Enabled': True |
| }, |
| 'Auto': True |
| }, config_file) |
| config_file.close() |
| |
| # Register the repo. |
| return_code = self._target.RunCommand( |
| [('amberctl rm_src -n %s; ' + |
| 'amberctl add_src -f http://127.0.0.1:%d/repo_config.json') |
| % (_MANAGED_REPO_NAME, remote_port)]) |
| if return_code != 0: |
| raise Exception('Error code %d when running amberctl.' % return_code) |
| |
| |
| def _UnregisterAmberRepository(self): |
| """Unregisters the Amber repository.""" |
| |
| logging.debug('Unregistering Amber repository.') |
| self._target.RunCommand(['amberctl', 'rm_src', '-n', _MANAGED_REPO_NAME]) |
| |
| # Re-enable 'devhost' repo if it's present. This is useful for devices that |
| # were booted with 'fx serve'. |
| self._target.RunCommand(['amberctl', 'enable_src', '-n', 'devhost'], |
| silent=True) |
| |
| |
| class ExternalAmberRepo(AmberRepo): |
| """Publishes packages to an Amber repository located and served externally |
| (ie. located under a Fuchsia build directory and served by "fx serve".""" |
| |
| def __init__(self, amber_root): |
| self._amber_root = amber_root |
| logging.info('Using existing Amber root: {}'.format(amber_root)) |
| logging.info('Ensure that "fx serve" is running.') |
| |
| def GetPath(self): |
| return self._amber_root |
| |
| def __enter__(self): |
| return self |
| |
| def __exit__(self, type, value, tb): |
| pass |