| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| # Copyright 2009-2012 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Chromium OS development server that can be used for all forms of update. | |
| This devserver can be used to perform system-wide autoupdate and update | |
| of specific portage packages on devices running Chromium OS derived operating | |
| systems. | |
| The devserver is configured to stage and | |
| serve artifacts from Google Storage using the credentials provided to it before | |
| it is run. The easiest way to understand this is that the devserver is | |
| functioning as a local cache for artifacts produced and uploaded by build | |
| servers. Users of this form of devserver can either download the artifacts | |
| from the devservers static directory OR use the update RPC to perform a | |
| system-wide autoupdate. Archive mode is always active. | |
| For autoupdates, there are many more advanced options that can help specify | |
| how to update and which payload to give to a requester. | |
| """ | |
| from __future__ import print_function | |
| import json | |
| import logging | |
| from logging import handlers | |
| import optparse # pylint: disable=deprecated-module | |
| import os | |
| import re | |
| import shutil | |
| import socket | |
| import subprocess | |
| import sys | |
| import tempfile | |
| import threading | |
| import types | |
| import autoupdate | |
| # pylint: disable=no-name-in-module, import-error | |
| import cherrypy | |
| from cherrypy import _cplogging as cplogging | |
| from cherrypy.process import plugins | |
| import cherrypy_ext | |
| import health_checker | |
| # This must happen before any local modules get a chance to import | |
| # anything from chromite. Otherwise, really bad things will happen, and | |
| # you will _not_ understand why. | |
| import setup_chromite # pylint: disable=unused-import | |
| from six.moves import http_client | |
| from chromite.lib import cros_build_lib | |
| from chromite.lib.xbuddy import artifact_info | |
| from chromite.lib.xbuddy import build_artifact | |
| from chromite.lib.xbuddy import common_util | |
| from chromite.lib.xbuddy import devserver_constants | |
| from chromite.lib.xbuddy import downloader | |
| from chromite.lib.xbuddy import xbuddy | |
| # pylint: enable=no-name-in-module, import-error | |
| # Module-local log function. | |
| def _Log(message, *args): | |
| return logging.info(message, *args) | |
| CACHED_ENTRIES = 12 | |
| TELEMETRY_FOLDER = "telemetry_src" | |
| TELEMETRY_DEPS = [ | |
| "dep-telemetry_dep.tar.bz2", | |
| "dep-page_cycler_dep.tar.bz2", | |
| "dep-chrome_test.tar.bz2", | |
| "dep-perf_data_dep.tar.bz2", | |
| ] | |
| # Sets up global to share between classes. | |
| updater = None | |
| # Log rotation parameters. These settings correspond to twice a day once | |
| # devserver is started, with about two weeks (28 backup files) of old logs | |
| # kept for backup. | |
| # | |
| # For more, see the documentation in standard python library for | |
| # logging.handlers.TimedRotatingFileHandler | |
| _LOG_ROTATION_TIME = "H" | |
| _LOG_ROTATION_INTERVAL = 12 # hours | |
| _LOG_ROTATION_BACKUP = 28 # backup counts | |
| # Error msg for deprecated RPC usage. | |
| DEPRECATED_RPC_ERROR_MSG = ( | |
| "The %s RPC has been deprecated. Usage of this " | |
| "RPC is discouraged. Please go to " | |
| "go/devserver-deprecation for more information." | |
| ) | |
| class DevServerError(Exception): | |
| """Exception class used by DevServer.""" | |
| class DeprecatedRPCError(DevServerError): | |
| """Exception class used when an RPC is deprecated but is still being used.""" | |
| def __init__(self, rpc_name): | |
| """Constructor for DeprecatedRPCError class. | |
| :param rpc_name: (str) name of the RPC that has been deprecated. | |
| """ | |
| super(DeprecatedRPCError, self).__init__( | |
| DEPRECATED_RPC_ERROR_MSG % rpc_name | |
| ) | |
| self.rpc_name = rpc_name | |
| class DevServerHTTPError(cherrypy.HTTPError): | |
| """Exception class to log the HTTPResponse before routing it to cherrypy.""" | |
| def __init__(self, status, message): | |
| """CherryPy error with logging. | |
| Args: | |
| status: HTTPResponse status. | |
| message: Message associated with the response. | |
| """ | |
| cherrypy.HTTPError.__init__(self, status, message) | |
| _Log("HTTPError status: %s message: %s", status, message) | |
| def _canonicalize_archive_url(archive_url): | |
| """Canonicalizes archive_url strings. | |
| Raises: | |
| DevserverError: if archive_url is not set. | |
| """ | |
| if archive_url: | |
| if not archive_url.startswith("gs://"): | |
| raise DevServerError( | |
| "Archive URL isn't from Google Storage (%s) ." % archive_url | |
| ) | |
| return archive_url.rstrip("/") | |
| else: | |
| raise DevServerError("Must specify an archive_url in the request") | |
| def _canonicalize_local_path(local_path): | |
| """Canonicalizes |local_path| strings. | |
| Raises: | |
| DevserverError: if |local_path| is not set. | |
| """ | |
| # Restrict staging of local content to only files within the static | |
| # directory. | |
| local_path = os.path.abspath(local_path) | |
| if not local_path.startswith(updater.static_dir): | |
| raise DevServerError( | |
| "Local path %s must be a subdirectory of the static" | |
| " directory: %s" % (local_path, updater.static_dir) | |
| ) | |
| return local_path.rstrip("/") | |
| def _get_artifacts(kwargs): | |
| """Returns a tuple of named and file artifacts given the stage rpc kwargs. | |
| Raises: | |
| DevserverError if no artifacts would be returned. | |
| """ | |
| artifacts = kwargs.get("artifacts") | |
| files = kwargs.get("files") | |
| if not artifacts and not files: | |
| raise DevServerError("No artifacts specified.") | |
| # Note we NEED to coerce files to a string as we get raw unicode from | |
| # cherrypy and we treat files as strings elsewhere in the code. | |
| return ( | |
| str(artifacts).split(",") if artifacts else [], | |
| str(files).split(",") if files else [], | |
| ) | |
| def _get_downloader(kwargs): | |
| """Returns the downloader based on passed in arguments. | |
| Args: | |
| kwargs: Keyword arguments for the request. | |
| """ | |
| local_path = kwargs.get("local_path") | |
| if local_path: | |
| local_path = _canonicalize_local_path(local_path) | |
| dl = None | |
| if local_path: | |
| delete_source = _parse_boolean_arg(kwargs, "delete_source") | |
| dl = downloader.LocalDownloader( | |
| updater.static_dir, local_path, delete_source=delete_source | |
| ) | |
| archive_url = kwargs.get("archive_url") | |
| if not archive_url and not local_path: | |
| raise DevServerError( | |
| "Requires archive_url or local_path to be specified." | |
| ) | |
| if archive_url and local_path: | |
| raise DevServerError( | |
| "archive_url and local_path can not both be specified." | |
| ) | |
| if not dl: | |
| archive_url = _canonicalize_archive_url(archive_url) | |
| dl = downloader.GoogleStorageDownloader( | |
| updater.static_dir, | |
| archive_url, | |
| downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL( | |
| archive_url | |
| ), | |
| ) | |
| return dl | |
| def _get_downloader_and_factory(kwargs): | |
| """Returns the downloader and artifact factory based on passed in arguments. | |
| Args: | |
| kwargs: Keyword arguments for the request. | |
| """ | |
| artifacts, files = _get_artifacts(kwargs) | |
| dl = _get_downloader(kwargs) | |
| if isinstance( | |
| dl, (downloader.GoogleStorageDownloader, downloader.LocalDownloader) | |
| ): | |
| factory_class = build_artifact.ChromeOSArtifactFactory | |
| else: | |
| raise DevServerError( | |
| "Unrecognized value for downloader type: %s" % type(dl) | |
| ) | |
| factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild()) | |
| return dl, factory | |
| def _LeadingWhiteSpaceCount(string): | |
| """Count the amount of leading whitespace in a string. | |
| Args: | |
| string: The string to count leading whitespace in. | |
| Returns: | |
| number of white space chars before characters start. | |
| """ | |
| matched = re.match(r"^\s+", string) | |
| if matched: | |
| return len(matched.group()) | |
| return 0 | |
| def _PrintDocStringAsHTML(func): | |
| """Make a functions docstring somewhat HTML style. | |
| Args: | |
| func: The function to return the docstring from. | |
| Returns: | |
| A string that is somewhat formated for a web browser. | |
| """ | |
| # TODO(scottz): Make this parse Args/Returns in a prettier way. | |
| # Arguments could be bolded and indented etc. | |
| html_doc = [] | |
| for line in func.__doc__.splitlines(): | |
| leading_space = _LeadingWhiteSpaceCount(line) | |
| if leading_space > 0: | |
| line = " " * leading_space + line | |
| html_doc.append("<BR>%s" % line) | |
| return "\n".join(html_doc) | |
| def _GetUpdateTimestampHandler(static_dir): | |
| """Returns a handler to update directory staged.timestamp. | |
| This handler resets the stage.timestamp whenever static content is accessed. | |
| Args: | |
| static_dir: Directory from which static content is being staged. | |
| Returns: | |
| A cherrypy handler to update the timestamp of accessed content. | |
| """ | |
| def UpdateTimestampHandler(): | |
| if not "404" in cherrypy.response.status: | |
| build_match = re.match( | |
| devserver_constants.STAGED_BUILD_REGEX, | |
| cherrypy.request.path_info, | |
| ) | |
| if build_match: | |
| build_dir = os.path.join(static_dir, build_match.group("build")) | |
| downloader.Downloader.TouchTimestampForStaged(build_dir) | |
| return UpdateTimestampHandler | |
| def _GetConfig(options): | |
| """Returns the configuration for the devserver.""" | |
| socket_host = "::" | |
| # Fall back to IPv4 when python is not configured with IPv6. | |
| if not socket.has_ipv6: | |
| socket_host = "0.0.0.0" | |
| # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes | |
| # on the on_end_resource hook. This hook is called once processing is | |
| # complete and the response is ready to be returned. | |
| cherrypy.tools.update_timestamp = cherrypy.Tool( | |
| "on_end_resource", _GetUpdateTimestampHandler(options.static_dir) | |
| ) | |
| base_config = { | |
| "global": { | |
| "server.log_request_headers": True, | |
| "server.protocol_version": "HTTP/1.1", | |
| "server.socket_host": socket_host, | |
| "server.socket_port": int(options.port), | |
| "response.timeout": 6000, | |
| "request.show_tracebacks": True, | |
| "server.socket_timeout": 60, | |
| "server.thread_pool": 2, | |
| "engine.autoreload.on": False, | |
| }, | |
| "/build": { | |
| "response.timeout": 100000, | |
| }, | |
| "/update": { | |
| # Gets rid of cherrypy parsing post file for args. | |
| "request.process_request_body": False, | |
| "response.timeout": 10000, | |
| }, | |
| # Sets up the static dir for file hosting. | |
| "/static": { | |
| "tools.staticdir.dir": options.static_dir, | |
| "tools.staticdir.on": True, | |
| "response.timeout": 10000, | |
| "tools.update_timestamp.on": True, | |
| }, | |
| } | |
| if options.production: | |
| base_config["global"].update({"server.thread_pool": 150}) | |
| return base_config | |
| def _GetRecursiveMemberObject(root, member_list): | |
| """Returns an object corresponding to a nested member list. | |
| Args: | |
| root: the root object to search | |
| member_list: list of nested members to search | |
| Returns: | |
| An object corresponding to the member name list; None otherwise. | |
| """ | |
| for member in member_list: | |
| next_root = root.__class__.__dict__.get(member) | |
| if not next_root: | |
| return None | |
| root = next_root | |
| return root | |
| def _IsExposed(name): | |
| """Returns True iff |name| has an `exposed' attribute and it is set.""" | |
| return hasattr(name, "exposed") and name.exposed | |
| def _GetExposedMethod(nested_member): | |
| """Returns a CherryPy-exposed method, if such exists. | |
| Args: | |
| nested_member: a slash-joined path to the nested member | |
| Returns: | |
| A function object corresponding to the path defined by |nested_member| from | |
| the app root object registered, if the function is exposed; None otherwise. | |
| """ | |
| for app in cherrypy.tree.apps.values(): | |
| # Use the 'index' function doc as the doc of the app. | |
| if nested_member == app.script_name.lstrip("/"): | |
| nested_member = "index" | |
| method = _GetRecursiveMemberObject(app.root, nested_member.split("/")) | |
| if ( | |
| method | |
| and isinstance(method, types.FunctionType) | |
| and _IsExposed(method) | |
| ): | |
| return method | |
| def _FindExposedMethods(root, prefix, unlisted=None): | |
| """Finds exposed CherryPy methods. | |
| Args: | |
| root: the root object for searching | |
| prefix: slash-joined chain of members leading to current object | |
| unlisted: URLs to be excluded regardless of their exposed status | |
| Returns: | |
| List of exposed URLs that are not unlisted. | |
| """ | |
| method_list = [] | |
| for member in root.__class__.__dict__.keys(): | |
| prefixed_member = prefix + "/" + member if prefix else member | |
| if unlisted and prefixed_member in unlisted: | |
| continue | |
| member_obj = root.__class__.__dict__[member] | |
| if _IsExposed(member_obj): | |
| if isinstance(member_obj, types.FunctionType): | |
| # Regard the app name as exposed "method" name if it exposed 'index' | |
| # function. | |
| if prefix and member == "index": | |
| method_list.append(prefix) | |
| else: | |
| method_list.append(prefixed_member) | |
| else: | |
| method_list += _FindExposedMethods( | |
| member_obj, prefixed_member, unlisted | |
| ) | |
| return method_list | |
| def _parse_boolean_arg(kwargs, key): | |
| """Parse boolean arg from kwargs. | |
| Args: | |
| kwargs: the parameters to be checked. | |
| key: the key to be parsed. | |
| Returns: | |
| The boolean value of kwargs[key], or False if key doesn't exist in kwargs. | |
| Raises: | |
| DevServerHTTPError if kwargs[key] is not a boolean variable. | |
| """ | |
| if key in kwargs: | |
| if kwargs[key] == "True": | |
| return True | |
| elif kwargs[key] == "False": | |
| return False | |
| else: | |
| raise DevServerHTTPError( | |
| http_client.INTERNAL_SERVER_ERROR, | |
| "The value for key %s is not boolean." % key, | |
| ) | |
| else: | |
| return False | |
| def _parse_string_arg(kwargs, key): | |
| """Parse string arg from kwargs. | |
| Args: | |
| kwargs: the parameters to be checked. | |
| key: the key to be parsed. | |
| Returns: | |
| The string value of kwargs[key], or None if key doesn't exist in kwargs. | |
| """ | |
| if key in kwargs: | |
| return kwargs[key] | |
| else: | |
| return None | |
| def _build_uri_from_build_name(build_name): | |
| """Get build url from a given build name. | |
| Args: | |
| build_name: the build name to be parsed, whose format is | |
| 'board/release_version'. | |
| Returns: | |
| The release_archive_url on Google Storage for this build name. | |
| """ | |
| # TODO(ahassani): This function doesn't seem to be used anywhere since its | |
| # previous use of lib.paygen.gspath was broken and it doesn't seem to be | |
| # causing any runtime issues. So deprecate this in the future. | |
| tokens = build_name.split("/") | |
| return "gs://chromeos-releases/stable-channel/%s/%s" % ( | |
| tokens[0], | |
| tokens[1], | |
| ) | |
| def is_deprecated_server(): | |
| """Gets whether the devserver has deprecated RPCs.""" | |
| return cherrypy.config.get("infra_removal", False) | |
| class DevServerRoot(object): | |
| """The Root Class for the Dev Server. | |
| CherryPy works as follows: | |
| For each method in this class, cherrpy interprets root/path | |
| as a call to an instance of DevServerRoot->method_name. For example, | |
| a call to http://myhost/build will call build. CherryPy automatically | |
| parses http args and places them as keyword arguments in each method. | |
| For paths http://myhost/update/dir1/dir2, you can use *args so that | |
| cherrypy uses the update method and puts the extra paths in args. | |
| """ | |
| # Method names that should not be listed on the index page. | |
| _UNLISTED_METHODS = ["index", "doc"] | |
| # Number of threads that devserver is staging images. | |
| _staging_thread_count = 0 | |
| # Lock used to lock increasing/decreasing count. | |
| _staging_thread_count_lock = threading.Lock() | |
| def __init__(self, _xbuddy): | |
| self._builder = None | |
| self._telemetry_lock_dict = common_util.LockDict() | |
| self._xbuddy = _xbuddy | |
| @property | |
| def staging_thread_count(self): | |
| """Get the staging thread count.""" | |
| return self._staging_thread_count | |
| @cherrypy.expose | |
| def build(self, board, pkg, **kwargs): | |
| """Builds the package specified.""" | |
| if is_deprecated_server(): | |
| raise DeprecatedRPCError("build") | |
| import builder | |
| if self._builder is None: | |
| self._builder = builder.Builder() | |
| return self._builder.Build(board, pkg, kwargs) | |
| @cherrypy.expose | |
| def is_staged(self, **kwargs): | |
| """Check if artifacts have been downloaded. | |
| Examples: | |
| To check if autotest and test_suites are staged: | |
| http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path& | |
| artifacts=autotest,test_suites | |
| Args: | |
| async: True to return without waiting for download to complete. | |
| artifacts: Comma separated list of named artifacts to download. | |
| These are defined in artifact_info and have their implementation | |
| in build_artifact.py. | |
| files: Comma separated list of file artifacts to stage. These | |
| will be available as is in the corresponding static directory with no | |
| custom post-processing. | |
| Returns: | |
| True of all artifacts are staged. | |
| """ | |
| dl, factory = _get_downloader_and_factory(kwargs) | |
| response = str(dl.IsStaged(factory)) | |
| _Log("Responding to is_staged %s request with %r", kwargs, response) | |
| return response | |
| @cherrypy.expose | |
| def list_image_dir(self, **kwargs): | |
| """Take an archive url and list the contents in its staged directory. | |
| Examples: | |
| To list the contents of where this devserver should have staged | |
| gs://image-archive/<board>-release/<build> call: | |
| http://devserver_url:<port>/list_image_dir?archive_url=<gs://..> | |
| Args: | |
| archive_url: Google Storage URL for the build. | |
| Returns: | |
| A string with information about the contents of the image directory. | |
| """ | |
| dl = _get_downloader(kwargs) | |
| try: | |
| image_dir_contents = dl.ListBuildDir() | |
| except build_artifact.ArtifactDownloadError as e: | |
| return "Cannot list the contents of staged artifacts. %s" % e | |
| if not image_dir_contents: | |
| return ( | |
| "%s has not been staged on this devserver." | |
| % dl.DescribeSource() | |
| ) | |
| return image_dir_contents | |
| @cherrypy.expose | |
| def stage(self, **kwargs): | |
| """Downloads and caches build artifacts. | |
| Downloads and caches build artifacts, possibly from a Google Storage URL, | |
| or from Android's build server. Returns once these have been downloaded | |
| on the devserver. A call to this will attempt to cache non-specified | |
| artifacts in the background for the given from the given URL following | |
| the principle of spatial locality. Spatial locality of different | |
| artifacts is explicitly defined in the build_artifact module. | |
| These artifacts will then be available from the static/ sub-directory of | |
| the devserver. | |
| Examples: | |
| To download the autotest and test suites tarballs: | |
| http://devserver_url:<port>/stage?archive_url=gs://your_url/path& | |
| artifacts=autotest,test_suites | |
| To download the full update payload: | |
| http://devserver_url:<port>/stage?archive_url=gs://your_url/path& | |
| artifacts=full_payload | |
| To download just a file called blah.bin: | |
| http://devserver_url:<port>/stage?archive_url=gs://your_url/path& | |
| files=blah.bin | |
| For both these examples, one could find these artifacts at: | |
| http://devserver_url:<port>/static/<relative_path>* | |
| Note for this example, relative path is the archive_url stripped of its | |
| basename i.e. path/ in the examples above. Specific example: | |
| gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0 | |
| Will get staged to: | |
| http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0 | |
| Args: | |
| archive_url: Google Storage URL for the build. | |
| local_path: Local path for the build. | |
| delete_source: Only meaningful with local_path. bool to indicate if the | |
| source files should be deleted. This is especially useful when staging | |
| a file locally in resource constrained environments as it allows us to | |
| move the relevant files locally instead of copying them. | |
| async: True to return without waiting for download to complete. | |
| artifacts: Comma separated list of named artifacts to download. | |
| These are defined in artifact_info and have their implementation | |
| in build_artifact.py. | |
| files: Comma separated list of files to stage. These | |
| will be available as is in the corresponding static directory with no | |
| custom post-processing. | |
| clean: True to remove any previously staged artifacts first. | |
| """ | |
| dl, factory = _get_downloader_and_factory(kwargs) | |
| with DevServerRoot._staging_thread_count_lock: | |
| DevServerRoot._staging_thread_count += 1 | |
| try: | |
| boolean_string = kwargs.get("clean") | |
| clean = xbuddy.XBuddy.ParseBoolean(boolean_string) | |
| if clean and os.path.exists(dl.GetBuildDir()): | |
| _Log("Removing %s" % dl.GetBuildDir()) | |
| shutil.rmtree(dl.GetBuildDir()) | |
| dl.Download(factory) | |
| finally: | |
| with DevServerRoot._staging_thread_count_lock: | |
| DevServerRoot._staging_thread_count -= 1 | |
| return "Success" | |
| @cherrypy.expose | |
| def locate_file(self, **kwargs): | |
| """Get the path to the given file name. | |
| This method looks up the given file name inside specified build artifacts. | |
| One use case is to help caller to locate an apk file inside a build | |
| artifact. The location of the apk file could be different based on the | |
| branch and target. | |
| Args: | |
| file_name: Name of the file to look for. | |
| artifacts: A list of artifact names to search for the file. | |
| Returns: | |
| Path to the file with the given name. It's relative to the folder for the | |
| build, e.g., DATA/priv-app/sl4a/sl4a.apk | |
| """ | |
| if is_deprecated_server(): | |
| raise DeprecatedRPCError("locate_file") | |
| dl, _ = _get_downloader_and_factory(kwargs) | |
| try: | |
| file_name = kwargs["file_name"] | |
| artifacts = kwargs["artifacts"] | |
| except KeyError: | |
| raise DevServerError( | |
| "`file_name` and `artifacts` are required to search " | |
| "for a file in build artifacts." | |
| ) | |
| build_path = dl.GetBuildDir() | |
| for artifact in artifacts: | |
| # Get the unzipped folder of the artifact. If it's not defined in | |
| # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build | |
| # directory directly. | |
| folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, "") | |
| artifact_path = os.path.join(build_path, folder) | |
| for root, _, filenames in os.walk(artifact_path): | |
| if file_name in set([f for f in filenames]): | |
| return os.path.relpath( | |
| os.path.join(root, file_name), build_path | |
| ) | |
| raise DevServerError( | |
| "File `%s` can not be found in artifacts: %s" | |
| % (file_name, artifacts) | |
| ) | |
| @cherrypy.expose | |
| def setup_telemetry(self, **kwargs): | |
| """Extracts and sets up telemetry | |
| This method goes through the telemetry deps packages, and stages them on | |
| the devserver to be used by the drones and the telemetry tests. | |
| Args: | |
| archive_url: Google Storage URL for the build. | |
| Returns: | |
| Path to the source folder for the telemetry codebase once it is staged. | |
| """ | |
| dl = _get_downloader(kwargs) | |
| build_path = dl.GetBuildDir() | |
| deps_path = os.path.join(build_path, "autotest/packages") | |
| telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER) | |
| src_folder = os.path.join(telemetry_path, "src") | |
| with self._telemetry_lock_dict.lock(telemetry_path): | |
| if os.path.exists(src_folder): | |
| # Telemetry is already fully stage return | |
| return src_folder | |
| common_util.MkDirP(telemetry_path) | |
| # Copy over the required deps tar balls to the telemetry directory. | |
| for dep in TELEMETRY_DEPS: | |
| dep_path = os.path.join(deps_path, dep) | |
| if not os.path.exists(dep_path): | |
| # This dep does not exist (could be new), do not extract it. | |
| continue | |
| try: | |
| cros_build_lib.ExtractTarball(dep_path, telemetry_path) | |
| except cros_build_lib.TarballError as e: | |
| shutil.rmtree(telemetry_path) | |
| raise DevServerError(str(e)) | |
| # By default all the tarballs extract to test_src but some parts of | |
| # the telemetry code specifically hardcoded to exist inside of 'src'. | |
| test_src = os.path.join(telemetry_path, "test_src") | |
| try: | |
| shutil.move(test_src, src_folder) | |
| except shutil.Error: | |
| # This can occur if src_folder already exists. Remove and retry move. | |
| shutil.rmtree(src_folder) | |
| raise DevServerError( | |
| "Failure in telemetry setup for build %s. Appears that the " | |
| "test_src to src move failed." % dl.GetBuild() | |
| ) | |
| return src_folder | |
| @cherrypy.expose | |
| def symbolicate_dump(self, minidump, **kwargs): | |
| """Symbolicates a minidump using pre-downloaded symbols, returns it. | |
| Callers will need to POST to this URL with a body of MIME-type | |
| "multipart/form-data". | |
| The body should include a single argument, 'minidump', containing the | |
| binary-formatted minidump to symbolicate. | |
| Args: | |
| archive_url: Google Storage URL for the build. | |
| minidump: The binary minidump file to symbolicate. | |
| """ | |
| if is_deprecated_server(): | |
| raise DeprecatedRPCError("symbolicate_dump") | |
| # Ensure the symbols have been staged. | |
| # Try debug.tar.xz first, then debug.tgz | |
| for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS): | |
| kwargs["artifacts"] = artifact | |
| dl = _get_downloader(kwargs) | |
| try: | |
| if self.stage(**kwargs) == "Success": | |
| break | |
| except build_artifact.ArtifactDownloadError: | |
| continue | |
| else: | |
| raise DevServerError( | |
| "Failed to stage symbols for %s" % dl.DescribeSource() | |
| ) | |
| to_return = "" | |
| with tempfile.NamedTemporaryFile() as local: | |
| while True: | |
| data = minidump.file.read(8192) | |
| if not data: | |
| break | |
| local.write(data) | |
| local.flush() | |
| symbols_directory = os.path.join( | |
| dl.GetBuildDir(), "debug", "breakpad" | |
| ) | |
| # The location of minidump_stackwalk is defined in chromeos-admin. | |
| stackwalk = subprocess.Popen( | |
| [ | |
| "/usr/local/bin/minidump_stackwalk", | |
| local.name, | |
| symbols_directory, | |
| ], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| ) | |
| to_return, error_text = stackwalk.communicate() | |
| if stackwalk.returncode != 0: | |
| raise DevServerError( | |
| "Can't generate stack trace: %s (rc=%d)" | |
| % (error_text, stackwalk.returncode) | |
| ) | |
| return to_return | |
| @cherrypy.expose | |
| def latestbuild(self, **kwargs): | |
| """Return a string representing the latest build for a given target. | |
| Args: | |
| target: The build target, typically a combination of the board and the | |
| type of build e.g. x86-mario-release. | |
| milestone: The milestone to filter builds on. E.g. R16. Optional, if not | |
| provided the latest RXX build will be returned. | |
| Returns: | |
| A string representation of the latest build if one exists, i.e. | |
| R19-1993.0.0-a1-b1480. | |
| An empty string if no latest could be found. | |
| """ | |
| if is_deprecated_server(): | |
| raise DeprecatedRPCError("latestbuild") | |
| if not kwargs: | |
| return _PrintDocStringAsHTML(self.latestbuild) | |
| if "target" not in kwargs: | |
| raise DevServerHTTPError( | |
| http_client.INTERNAL_SERVER_ERROR, "Error: target= is required!" | |
| ) | |
| try: | |
| return common_util.GetLatestBuildVersion( | |
| updater.static_dir, | |
| kwargs["target"], | |
| milestone=kwargs.get("milestone"), | |
| ) | |
| except common_util.CommonUtilError as errmsg: | |
| raise DevServerHTTPError( | |
| http_client.INTERNAL_SERVER_ERROR, str(errmsg) | |
| ) | |
| @cherrypy.expose | |
| def list_suite_controls(self, **kwargs): | |
| """Return a list of contents of all known control files. | |
| Example URL: | |
| To List all control files' content: | |
| http://dev-server/list_suite_controls?suite_name=bvt& | |
| build=daisy_spring-release/R29-4279.0.0 | |
| Args: | |
| build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450. | |
| suite_name: List the control files belonging to that suite. | |
| Returns: | |
| A dictionary of all control files's path to its content for given suite. | |
| """ | |
| if is_deprecated_server(): | |
| raise DeprecatedRPCError("list_suite_controls") | |
| if not kwargs: | |
| return _PrintDocStringAsHTML(self.controlfiles) | |
| if "build" not in kwargs: | |
| raise DevServerHTTPError( | |
| http_client.INTERNAL_SERVER_ERROR, "Error: build= is required!" | |
| ) | |
| if "suite_name" not in kwargs: | |
| raise DevServerHTTPError( | |
| http_client.INTERNAL_SERVER_ERROR, | |
| "Error: suite_name= is required!", | |
| ) | |
| control_file_list = [ | |
| line.rstrip() | |
| for line in common_util.GetControlFileListForSuite( | |
| updater.static_dir, kwargs["build"], kwargs["suite_name"] | |
| ).splitlines() | |
| ] | |
| control_file_content_dict = {} | |
| for control_path in control_file_list: | |
| control_file_content_dict[ | |
| control_path | |
| ] = common_util.GetControlFile( | |
| updater.static_dir, kwargs["build"], control_path | |
| ) | |
| return json.dumps(control_file_content_dict) | |
| @cherrypy.expose | |
| def controlfiles(self, **kwargs): | |
| """Return a control file or a list of all known control files. | |
| Example URL: | |
| To List all control files: | |
| http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0 | |
| To List all control files for, say, the bvt suite: | |
| http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0 | |
| To return the contents of a path: | |
| http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control | |
| Args: | |
| build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450. | |
| control_path: If you want the contents of a control file set this | |
| to the path. E.g. client/site_tests/sleeptest/control | |
| Optional, if not provided return a list of control files is returned. | |
| suite_name: If control_path is not specified but a suite_name is | |
| specified, list the control files belonging to that suite instead of | |
| all control files. The empty string for suite_name will list all control | |
| files for the build. | |
| Returns: | |
| Contents of a control file if control_path is provided. | |
| A list of control files if no control_path is provided. | |
| """ | |
| if is_deprecated_server(): | |
| raise DeprecatedRPCError("controlfiles") | |
| if not kwargs: | |
| return _PrintDocStringAsHTML(self.controlfiles) | |
| if "build" not in kwargs: | |
| raise DevServerHTTPError( | |
| http_client.INTERNAL_SERVER_ERROR, "Error: build= is required!" | |
| ) | |
| if "control_path" not in kwargs: | |
| if "suite_name" in kwargs and kwargs["suite_name"]: | |
| return common_util.GetControlFileListForSuite( | |
| updater.static_dir, kwargs["build"], kwargs["suite_name"] | |
| ) | |
| else: | |
| return common_util.GetControlFileList( | |
| updater.static_dir, kwargs["build"] | |
| ) | |
| else: | |
| return common_util.GetControlFile( | |
| updater.static_dir, kwargs["build"], kwargs["control_path"] | |
| ) | |
| @cherrypy.expose | |
| def xbuddy_translate(self, *args, **kwargs): | |
| """Translates an xBuddy path to a real path to artifact if it exists. | |
| Args: | |
| args: An xbuddy path in the form of {local|remote}/build_id/artifact. | |
| Local searches the devserver's static directory. Remote searches a | |
| Google Storage image archive. | |
| Kwargs: | |
| image_dir: Google Storage image archive to search in if requesting a | |
| remote artifact. If none uses the default bucket. | |
| Returns: | |
| String in the format of build_id/artifact as stored on the local server | |
| or in Google Storage. | |
| """ | |
| if is_deprecated_server(): | |
| raise DeprecatedRPCError("xbuddy_translate") | |
| build_id, filename = self._xbuddy.Translate( | |
| args, image_dir=kwargs.get("image_dir") | |
| ) | |
| response = os.path.join(build_id, filename) | |
| _Log("Path translation requested, returning: %s", response) | |
| return response | |
| @cherrypy.expose | |
| def xbuddy(self, *args, **kwargs): | |
| """The full xBuddy call, returns resource specified by path_parts. | |
| Args: | |
| path_parts: the path following xbuddy/ in the call url is split into the | |
| components of the path. The path can be understood as | |
| "{local|remote}/build_id/artifact" where build_id is composed of | |
| "board/version." | |
| The first path element is optional, and can be "remote" or "local" | |
| If local (the default), devserver will not attempt to access Google | |
| Storage, and will only search the static directory for the files. | |
| If remote, devserver will try to obtain the artifact off GS if it's | |
| not found locally. | |
| The board is the familiar board name, optionally suffixed. | |
| The version can be the google storage version number, and may also be | |
| any of a number of xBuddy defined version aliases that will be | |
| translated into the latest built image that fits the description. | |
| Defaults to latest. | |
| The artifact is one of a number of image or artifact aliases used by | |
| xbuddy, defined in xbuddy:ALIASES. Defaults to test. | |
| Kwargs: | |
| return_dir: {true|false} | |
| if set to true, returns the url to the update.gz | |
| relative_path: {true|false} | |
| if set to true, returns the relative path to the payload | |
| directory from static_dir. | |
| Example URL: | |
| http://host:port/xbuddy/x86-generic/R26-4000.0.0/test | |
| or | |
| http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true | |
| Returns: | |
| If |return_dir|, return a uri to the folder where the artifact is. E.g., | |
| http://host:port/static/x86-generic-release/R26-4000.0.0/ | |
| If |relative_path| is true, return a relative path the folder where the | |
| payloads are. E.g., | |
| archive/x86-generic-release/R26-4000.0.0 | |
| """ | |
| if is_deprecated_server(): | |
| raise DeprecatedRPCError("xbuddy") | |
| boolean_string = kwargs.get("return_dir") | |
| return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string) | |
| boolean_string = kwargs.get("relative_path") | |
| relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string) | |
| if return_dir and relative_path: | |
| raise DevServerHTTPError( | |
| http_client.INTERNAL_SERVER_ERROR, | |
| "Cannot specify both return_dir and relative_path", | |
| ) | |
| build_id, file_name = self._xbuddy.Get(args) | |
| response = None | |
| if return_dir: | |
| response = os.path.join(cherrypy.request.base, "static", build_id) | |
| _Log("Directory requested, returning: %s", response) | |
| elif relative_path: | |
| response = build_id | |
| _Log("Relative path requested, returning: %s", response) | |
| else: | |
| # Redirect to download the payload if no kwargs are set. | |
| build_id = "/" + os.path.join("static", build_id, file_name) | |
| _Log("Payload requested, returning: %s", build_id) | |
| raise cherrypy.HTTPRedirect(build_id, 302) | |
| return response | |
| @cherrypy.expose | |
| def xbuddy_capacity(self): | |
| """Returns the number of images cached by xBuddy.""" | |
| if is_deprecated_server(): | |
| raise DeprecatedRPCError("xbuddy_capacity") | |
| return self._xbuddy.Capacity() | |
| @cherrypy.expose | |
| def index(self): | |
| """Presents a welcome message and documentation links.""" | |
| if is_deprecated_server(): | |
| raise DeprecatedRPCError("index") | |
| html_template = ( | |
| "Welcome to the Dev Server!<br>\n" | |
| "<br>\n" | |
| "Here are the available methods, click for documentation:<br>\n" | |
| "<br>\n" | |
| "%s" | |
| ) | |
| exposed_methods = [] | |
| for app in cherrypy.tree.apps.values(): | |
| exposed_methods += _FindExposedMethods( | |
| app.root, | |
| app.script_name.lstrip("/"), | |
| unlisted=self._UNLISTED_METHODS, | |
| ) | |
| return html_template % "<br>\n".join( | |
| [ | |
| "<a href=doc/%s>%s</a>" % (name, name) | |
| for name in sorted(exposed_methods) | |
| ] | |
| ) | |
| @cherrypy.expose | |
| def doc(self, *args): | |
| """Shows the documentation for available methods / URLs. | |
| Examples: | |
| http://myhost/doc/update | |
| """ | |
| if is_deprecated_server(): | |
| raise DeprecatedRPCError("doc") | |
| name = "/".join(args) | |
| method = _GetExposedMethod(name) | |
| if not method: | |
| raise DevServerError("No exposed method named `%s'" % name) | |
| if not method.__doc__: | |
| raise DevServerError( | |
| "No documentation for exposed method `%s'" % name | |
| ) | |
| return "<pre>\n%s</pre>" % method.__doc__ | |
| @cherrypy.expose | |
| def update(self, *args, **kwargs): | |
| """Handles an update check from a Chrome OS client. | |
| The HTTP request should contain the standard Omaha-style XML blob. The URL | |
| line may contain an additional intermediate path to the update payload. | |
| This request can be handled in one of 4 ways, depending on the devsever | |
| settings and intermediate path. | |
| 1. No intermediate path. DEPRECATED | |
| 2. Path explicitly invokes XBuddy | |
| If there is a path given, it can explicitly invoke xbuddy by prefixing it | |
| with 'xbuddy'. This path is then used to acquire an image binary for the | |
| devserver to generate an update payload from. Devserver then serves this | |
| payload. | |
| 3. Path is left for the devserver to interpret. | |
| If the path given doesn't explicitly invoke xbuddy, devserver will attempt | |
| to generate a payload from the test image in that directory and serve it. | |
| Examples: | |
| 2. Explicitly invoke xbuddy | |
| update_engine_client --omaha_url= | |
| http://myhost/update/xbuddy/remote/board/version/dev | |
| This would go to GS to download the dev image for the board, from which | |
| the devserver would generate a payload to serve. | |
| 3. Give a path for devserver to interpret | |
| update_engine_client --omaha_url=http://myhost/update/some/random/path | |
| This would attempt, in order to: | |
| a) Generate an update from a test image binary if found in | |
| static_dir/some/random/path. | |
| b) Serve an update payload found in static_dir/some/random/path. | |
| c) Hope that some/random/path takes the form "board/version" and | |
| and attempt to download an update payload for that board/version | |
| from GS. | |
| """ | |
| label = "/".join(args) | |
| body_length = int(cherrypy.request.headers.get("Content-Length", 0)) | |
| data = cherrypy.request.rfile.read(body_length) | |
| return updater.HandleUpdatePing(data, label, **kwargs) | |
| def _CleanCache(cache_dir, wipe): | |
| """Wipes any excess cached items in the cache_dir. | |
| Args: | |
| cache_dir: the directory we are wiping from. | |
| wipe: If True, wipe all the contents -- not just the excess. | |
| """ | |
| if wipe: | |
| # Clear the cache and exit on error. | |
| cmd = "rm -rf %s/*" % cache_dir | |
| if os.system(cmd) != 0: | |
| _Log("Failed to clear the cache with %s" % cmd) | |
| sys.exit(1) | |
| else: | |
| # Clear all but the last N cached updates | |
| cmd = "cd %s; ls -tr | head --lines=-%d | xargs rm -rf" % ( | |
| cache_dir, | |
| CACHED_ENTRIES, | |
| ) | |
| if os.system(cmd) != 0: | |
| _Log("Failed to clean up old delta cache files with %s" % cmd) | |
| sys.exit(1) | |
| def _AddTestingOptions(parser): | |
| group = optparse.OptionGroup( | |
| parser, | |
| "Advanced Testing Options", | |
| "These are used by test scripts and " | |
| "developers writing integration tests utilizing the devserver. They are " | |
| "not intended to be really used outside the scope of someone " | |
| "knowledgable about the test.", | |
| ) | |
| group.add_option( | |
| "--exit", | |
| action="store_true", | |
| help="do not start the server (yet clear cache)", | |
| ) | |
| parser.add_option_group(group) | |
| def _AddProductionOptions(parser): | |
| group = optparse.OptionGroup( | |
| parser, | |
| "Advanced Server Options", | |
| "These options can be used to changed " "for advanced server behavior.", | |
| ) | |
| group.add_option( | |
| "--clear_cache", | |
| action="store_true", | |
| default=False, | |
| help="At startup, removes all cached entries from the" | |
| "devserver's cache.", | |
| ) | |
| group.add_option( | |
| "--logfile", | |
| metavar="PATH", | |
| help="log output to this file instead of stdout", | |
| ) | |
| group.add_option( | |
| "--pidfile", | |
| metavar="PATH", | |
| help="path to output a pid file for the server.", | |
| ) | |
| group.add_option( | |
| "--portfile", | |
| metavar="PATH", | |
| help="path to output the port number being served on.", | |
| ) | |
| group.add_option( | |
| "--production", | |
| action="store_true", | |
| default=False, | |
| help="have the devserver use production values when " | |
| "starting up. This includes using more threads and " | |
| "performing less logging.", | |
| ) | |
| parser.add_option_group(group) | |
| def MakeLogHandler(logfile): | |
| """Create a LogHandler instance used to log all messages.""" | |
| hdlr_cls = handlers.TimedRotatingFileHandler | |
| hdlr = hdlr_cls( | |
| logfile, | |
| when=_LOG_ROTATION_TIME, | |
| interval=_LOG_ROTATION_INTERVAL, | |
| backupCount=_LOG_ROTATION_BACKUP, | |
| ) | |
| hdlr.setFormatter(cplogging.logfmt) | |
| return hdlr | |
| def main(): | |
| usage = "\n\n".join(["usage: %prog [options]", __doc__]) | |
| parser = optparse.OptionParser(usage=usage) | |
| # get directory that the devserver is run from | |
| devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0])) | |
| default_static_dir = "%s/static" % devserver_dir | |
| parser.add_option( | |
| "--static_dir", | |
| metavar="PATH", | |
| default=default_static_dir, | |
| help="writable static directory", | |
| ) | |
| parser.add_option( | |
| "--port", | |
| default=8080, | |
| type="int", | |
| help=( | |
| "port for the dev server to use; if zero, binds to " | |
| "an arbitrary available port (default: 8080)" | |
| ), | |
| ) | |
| parser.add_option( | |
| "-t", "--test_image", action="store_true", help="Deprecated." | |
| ) | |
| parser.add_option( | |
| "-x", | |
| "--xbuddy_manage_builds", | |
| action="store_true", | |
| default=False, | |
| help="If set, allow xbuddy to manage images in" "build/images.", | |
| ) | |
| parser.add_option( | |
| "--infra_removal", | |
| action="store_true", | |
| default=False, | |
| help="If option is present, some RPCs will be disabled to " | |
| "help with infra removal efforts. See " | |
| "go/devserver-deprecation", | |
| ) | |
| _AddProductionOptions(parser) | |
| _AddTestingOptions(parser) | |
| (options, _) = parser.parse_args() | |
| # Handle options that must be set globally in cherrypy. Do this | |
| # work up front, because calls to _Log() below depend on this | |
| # initialization. | |
| if options.production: | |
| cherrypy.config.update({"environment": "production"}) | |
| cherrypy.config.update({"infra_removal": options.infra_removal}) | |
| if not options.logfile: | |
| cherrypy.config.update({"log.screen": True}) | |
| else: | |
| cherrypy.config.update({"log.error_file": "", "log.access_file": ""}) | |
| hdlr = MakeLogHandler(options.logfile) | |
| # Pylint can't seem to process these two calls properly | |
| # pylint: disable=E1101 | |
| cherrypy.log.access_log.addHandler(hdlr) | |
| cherrypy.log.error_log.addHandler(hdlr) | |
| # pylint: enable=E1101 | |
| # set static_dir, from which everything will be served | |
| options.static_dir = os.path.realpath(options.static_dir) | |
| cache_dir = os.path.join(options.static_dir, "cache") | |
| # If our devserver is only supposed to serve payloads, we shouldn't be | |
| # mucking with the cache at all. If the devserver hadn't previously | |
| # generated a cache and is expected, the caller is using it wrong. | |
| if os.path.exists(cache_dir): | |
| _CleanCache(cache_dir, options.clear_cache) | |
| else: | |
| os.makedirs(cache_dir) | |
| pkgroot_dir = os.path.join(options.static_dir, "pkgroot") | |
| common_util.SymlinkFile("/build", pkgroot_dir) | |
| _Log("Using cache directory %s" % cache_dir) | |
| _Log("Serving from %s" % options.static_dir) | |
| _xbuddy = xbuddy.XBuddy( | |
| manage_builds=options.xbuddy_manage_builds, | |
| static_dir=options.static_dir, | |
| ) | |
| if options.clear_cache and options.xbuddy_manage_builds: | |
| _xbuddy.CleanCache() | |
| # We allow global use here to share with cherrypy classes. | |
| # pylint: disable=W0603 | |
| global updater | |
| updater = autoupdate.Autoupdate(_xbuddy, static_dir=options.static_dir) | |
| if options.exit: | |
| return | |
| dev_server = DevServerRoot(_xbuddy) | |
| health_checker_app = health_checker.Root(dev_server, options.static_dir) | |
| if options.pidfile: | |
| plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe() | |
| if options.portfile: | |
| cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe() | |
| cherrypy.tree.mount( | |
| health_checker_app, "/check_health", config=health_checker.get_config() | |
| ) | |
| cherrypy.quickstart(dev_server, config=_GetConfig(options)) | |
| if __name__ == "__main__": | |
| main() |