Revert "autoupdate.py: Deprecate autoupdate.py"
This reverts commit 3de3e2bad7d6707a0548ead60ff4ab19f731ce7f.
Reason for revert: It turns out moblab still has this use case and there
are external users use it too. so I guess we have to just suck it up and
put this back.
Original change's description:
> autoupdate.py: Deprecate autoupdate.py
>
> This mechanism is not used in the lab anymore and has been replaced by
> gs_cache/nebraska_wrapper.py in crrev.com/c/2258234.
>
> BUG=chromium:1078188
> TEST=./devserver_integration_test.py
>
> Change-Id: Ifa007df983aab49c808360df6ba0b62b7031221c
> Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2486466
> Tested-by: Amin Hassani <ahassani@chromium.org>
> Commit-Queue: Amin Hassani <ahassani@chromium.org>
> Reviewed-by: Jae Hoon Kim <kimjae@chromium.org>
Bug: chromium:1078188, chromium:1149703
Change-Id: Ic9a1841419de5d4a7f2158e432cdaf3f79b273e9
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2546047
Tested-by: Amin Hassani <ahassani@chromium.org>
Commit-Queue: Amin Hassani <ahassani@chromium.org>
Reviewed-by: Amin Hassani <ahassani@chromium.org>
diff --git a/Makefile b/Makefile
index fe2f97c..e584dcc 100644
--- a/Makefile
+++ b/Makefile
@@ -14,6 +14,7 @@
install -m 0755 host/start_devserver "${DESTDIR}/usr/bin"
install -m 0755 devserver.py strip_package.py "${DESTDIR}/usr/lib/devserver"
install -m 0644 \
+ autoupdate.py \
builder.py \
cherrypy_ext.py \
health_checker.py \
diff --git a/autoupdate.py b/autoupdate.py
new file mode 100644
index 0000000..88def64
--- /dev/null
+++ b/autoupdate.py
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Devserver module for handling update client requests."""
+
+from __future__ import print_function
+
+import os
+
+from six.moves import urllib
+
+import cherrypy # pylint: disable=import-error
+
+# TODO(crbug.com/872441): We try to import nebraska from different places
+# because when we install the devserver, we copy the nebraska.py into the main
+# directory. Once this bug is resolved, we can always import from nebraska
+# directory.
+try:
+ from nebraska import nebraska
+except ImportError:
+ import nebraska
+
+import setup_chromite # pylint: disable=unused-import
+from chromite.lib.xbuddy import cherrypy_log_util
+from chromite.lib.xbuddy import devserver_constants as constants
+
+
+# Module-local log function.
+def _Log(message, *args):
+ return cherrypy_log_util.LogWithTag('UPDATE', message, *args)
+
+class AutoupdateError(Exception):
+ """Exception classes used by this module."""
+ pass
+
+
+def _ChangeUrlPort(url, new_port):
+ """Return the URL passed in with a different port"""
+ scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
+ host_port = netloc.split(':')
+
+ if len(host_port) == 1:
+ host_port.append(new_port)
+ else:
+ host_port[1] = new_port
+
+ print(host_port)
+ netloc = '%s:%s' % tuple(host_port)
+
+ # pylint: disable=too-many-function-args
+ return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
+
+def _NonePathJoin(*args):
+ """os.path.join that filters None's from the argument list."""
+ return os.path.join(*[x for x in args if x is not None])
+
+
+class Autoupdate(object):
+ """Class that contains functionality that handles Chrome OS update pings."""
+
+ def __init__(self, xbuddy, static_dir=None):
+ """Initializes the class.
+
+ Args:
+ xbuddy: The xbuddy path.
+ static_dir: The path to the devserver static directory.
+ """
+ self.xbuddy = xbuddy
+ self.static_dir = static_dir
+
+ def GetUpdateForLabel(self, label):
+ """Given a label, get an update from the directory.
+
+ Args:
+ label: the relative directory inside the static dir
+
+ Returns:
+ A relative path to the directory with the update payload.
+ This is the label if an update did not need to be generated, but can
+ be label/cache/hashed_dir_for_update.
+
+ Raises:
+ AutoupdateError: If client version is higher than available update found
+ at the directory given by the label.
+ """
+ _Log('Update label: %s', label)
+ static_update_path = _NonePathJoin(self.static_dir, label,
+ constants.UPDATE_FILE)
+
+ if label and os.path.exists(static_update_path):
+ # An update payload was found for the given label, return it.
+ return label
+
+ # The label didn't resolve.
+ _Log('Did not found any update payload for label %s.', label)
+ return None
+
+ def GetDevserverUrl(self):
+ """Returns the devserver url base."""
+ x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
+ if x_forwarded_host:
+ # Select the left most <ip>:<port> value so that the request is
+ # forwarded correctly.
+ x_forwarded_host = [x.strip() for x in x_forwarded_host.split(',')][0]
+ hostname = 'http://' + x_forwarded_host
+ else:
+ hostname = cherrypy.request.base
+
+ return hostname
+
+ def GetStaticUrl(self):
+ """Returns the static url base that should prefix all payload responses."""
+ hostname = self.GetDevserverUrl()
+ _Log('Handling update ping as %s', hostname)
+
+ static_urlbase = '%s/static' % hostname
+ _Log('Using static url base %s', static_urlbase)
+ return static_urlbase
+
+ def GetPathToPayload(self, label, board):
+ """Find a payload locally.
+
+ See devserver's update rpc for documentation.
+
+ Args:
+ label: from update request
+ board: from update request
+
+ Returns:
+ The relative path to an update from the static_dir
+
+ Raises:
+ AutoupdateError: If the update could not be found.
+ """
+ label = label or ''
+ label_list = label.split('/')
+ # Suppose that the path follows old protocol of indexing straight
+ # into static_dir with board/version label.
+ # Attempt to get the update in that directory, generating if necc.
+ path_to_payload = self.GetUpdateForLabel(label)
+ if path_to_payload is None:
+ # There was no update found in the directory. Let XBuddy find the
+ # payloads.
+ if label_list[0] == 'xbuddy':
+ # If path explicitly calls xbuddy, pop off the tag.
+ label_list.pop()
+ x_label, _ = self.xbuddy.Translate(label_list, board=board)
+ # Path has been resolved, try to get the payload.
+ path_to_payload = self.GetUpdateForLabel(x_label)
+ if path_to_payload is None:
+ # No update payload found after translation. Try to get an update to
+ # a test image from GS using the label.
+ path_to_payload, _image_name = self.xbuddy.Get(
+ ['remote', label, 'full_payload'])
+
+ # One of the above options should have gotten us a relative path.
+ if path_to_payload is None:
+ raise AutoupdateError('Failed to get an update for: %s' % label)
+
+ return path_to_payload
+
+ def HandleUpdatePing(self, data, label='', **kwargs):
+ """Handles an update ping from an update client.
+
+ Args:
+ data: XML blob from client.
+ label: optional label for the update.
+ kwargs: The map of query strings passed to the /update API.
+
+ Returns:
+ Update payload message for client.
+ """
+ # Get the static url base that will form that base of our update url e.g.
+ # http://hostname:8080/static/update.gz.
+ static_urlbase = self.GetStaticUrl()
+ # Change the URL's string query dictionary provided by cherrypy to a valid
+ # dictionary that has proper values for its keys. e.g. True instead of
+ # 'True'.
+ kwargs = nebraska.QueryDictToDict(kwargs)
+
+ # Process attributes of the update check.
+ request = nebraska.Request(data)
+ if request.request_type == nebraska.Request.RequestType.EVENT:
+ _Log('A non-update event notification received. Returning an ack.')
+ return nebraska.Nebraska().GetResponseToRequest(
+ request, response_props=nebraska.ResponseProperties(**kwargs))
+
+ _Log('Update Check Received.')
+
+ try:
+ path_to_payload = self.GetPathToPayload(label, request.board)
+ base_url = _NonePathJoin(static_urlbase, path_to_payload)
+ local_payload_dir = _NonePathJoin(self.static_dir, path_to_payload)
+ except AutoupdateError as e:
+ # Raised if we fail to generate an update payload.
+ _Log('Failed to process an update request, but we will defer to '
+ 'nebraska to respond with no-update. The error was %s', e)
+
+ _Log('Responding to client to use url %s to get image', base_url)
+ nebraska_props = nebraska.NebraskaProperties(
+ update_payloads_address=base_url,
+ update_metadata_dir=local_payload_dir)
+ nebraska_obj = nebraska.Nebraska(nebraska_props=nebraska_props)
+ return nebraska_obj.GetResponseToRequest(
+ request, response_props=nebraska.ResponseProperties(**kwargs))
diff --git a/autoupdate_unittest.py b/autoupdate_unittest.py
new file mode 100755
index 0000000..648ef7c
--- /dev/null
+++ b/autoupdate_unittest.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unit tests for autoupdate.py."""
+
+from __future__ import print_function
+
+import json
+import shutil
+import socket
+import tempfile
+import unittest
+
+import mock
+import cherrypy # pylint: disable=import-error
+
+import autoupdate
+
+import setup_chromite # pylint: disable=unused-import
+from chromite.lib.xbuddy import common_util
+from chromite.lib.xbuddy import xbuddy
+
+
+_TEST_REQUEST = """<?xml version="1.0" encoding="UTF-8"?>
+<request protocol="3.0" updater="ChromeOSUpdateEngine" updaterversion="0.1.0.0">
+ <app appid="test-appid" version="%(version)s" track="%(track)s"
+ board="%(board)s" hardware_class="%(hwclass)s">
+ <updatecheck />
+ </app>
+</request>"""
+
+class AutoupdateTest(unittest.TestCase):
+ """Tests for the autoupdate.Autoupdate class."""
+
+ def setUp(self):
+ self.port = 8080
+ self.test_board = 'test-board'
+ self.build_root = tempfile.mkdtemp('autoupdate_build_root')
+ self.tempdir = tempfile.mkdtemp('tempdir')
+ self.latest_dir = '12345_af_12-a1'
+ self.latest_verision = '12345_af_12'
+ self.static_image_dir = tempfile.mkdtemp('autoupdate_static_dir')
+ self.hostname = '%s:%s' % (socket.gethostname(), self.port)
+ self.test_dict = {
+ 'version': 'ForcedUpdate',
+ 'track': 'test-channel',
+ 'board': self.test_board,
+ 'hwclass': 'test-hardware-class',
+ }
+ self.test_data = _TEST_REQUEST % self.test_dict
+ self.payload = 'My payload'
+ cherrypy.request.base = 'http://%s' % self.hostname
+ common_util.MkDirP(self.static_image_dir)
+ self._xbuddy = xbuddy.XBuddy(False,
+ static_dir=self.static_image_dir)
+ mock.patch.object(xbuddy.XBuddy, '_GetArtifact')
+
+ def tearDown(self):
+ shutil.rmtree(self.build_root)
+ shutil.rmtree(self.tempdir)
+ shutil.rmtree(self.static_image_dir)
+
+ def _DummyAutoupdateConstructor(self, **kwargs):
+ """Creates a dummy autoupdater. Used to avoid using constructor."""
+ dummy = autoupdate.Autoupdate(self._xbuddy,
+ static_dir=self.static_image_dir,
+ **kwargs)
+ return dummy
+
+ def testChangeUrlPort(self):
+ # pylint: disable=protected-access
+ r = autoupdate._ChangeUrlPort('http://fuzzy:8080/static', 8085)
+ self.assertEqual(r, 'http://fuzzy:8085/static')
+
+ r = autoupdate._ChangeUrlPort('http://fuzzy/static', 8085)
+ self.assertEqual(r, 'http://fuzzy:8085/static')
+
+ r = autoupdate._ChangeUrlPort('ftp://fuzzy/static', 8085)
+ self.assertEqual(r, 'ftp://fuzzy:8085/static')
+
+ r = autoupdate._ChangeUrlPort('ftp://fuzzy', 8085)
+ self.assertEqual(r, 'ftp://fuzzy:8085')
+
+ def testHandleHostInfoPing(self):
+ au_mock = self._DummyAutoupdateConstructor()
+ self.assertRaises(AssertionError, au_mock.HandleHostInfoPing, None)
+
+ # Setup fake host_infos entry and ensure it comes back to us in one piece.
+ test_ip = '1.2.3.4'
+ au_mock.host_infos.GetInitHostInfo(test_ip).attrs = self.test_dict
+ self.assertEqual(
+ json.loads(au_mock.HandleHostInfoPing(test_ip)), self.test_dict)
+
+ @mock.patch.object(autoupdate.Autoupdate, 'GetPathToPayload')
+ def testHandleUpdatePing(self, path_to_payload_mock):
+ """Tests HandleUpdatePing"""
+ au_mock = self._DummyAutoupdateConstructor()
+ path_to_payload_mock.return_value = self.tempdir
+ request = """<?xml version="1.0" encoding="UTF-8"?>
+<request protocol="3.0">
+ <os version="Indy" platform="Chrome OS" sp="10323.52.0_x86_64"></os>
+ <app appid="platform" version="1.0.0" delta_okay="true"
+ track="stable-channel" board="eve">
+ <ping active="1" a="1" r="1"></ping>
+ <updatecheck targetversionprefix=""></updatecheck>
+ </app>
+</request>"""
+
+ self.assertIn('error-unknownApplication', au_mock.HandleUpdatePing(request))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/devserver.py b/devserver.py
index d713780..9f29604 100755
--- a/devserver.py
+++ b/devserver.py
@@ -4,14 +4,22 @@
# 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 caching files.
+"""Chromium OS development server that can be used for all forms of update.
-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 download the artifacts from the devservers static directory.
-Archive mode is always active.
+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
@@ -37,6 +45,7 @@
from cherrypy.process import plugins
# pylint: enable=no-name-in-module, import-error
+import autoupdate
import cherrypy_ext
import health_checker
@@ -66,6 +75,9 @@
'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.
@@ -128,23 +140,19 @@
raise DevServerError('Must specify an archive_url in the request')
-def _canonicalize_local_path(local_path, static_dir):
+def _canonicalize_local_path(local_path):
"""Canonicalizes |local_path| strings.
- Args:
- local_path: The input path.
- static_dir: Devserver's static cache directory.
-
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(static_dir):
+ if not local_path.startswith(updater.static_dir):
raise DevServerError(
'Local path %s must be a subdirectory of the static'
- ' directory: %s' % (local_path, static_dir))
+ ' directory: %s' % (local_path, updater.static_dir))
return local_path.rstrip('/')
@@ -183,21 +191,20 @@
return os_type == 'android'
-def _get_downloader(static_dir, kwargs):
+def _get_downloader(kwargs):
"""Returns the downloader based on passed in arguments.
Args:
- static_dir: Devserver's static cache directory.
kwargs: Keyword arguments for the request.
"""
local_path = kwargs.get('local_path')
if local_path:
- local_path = _canonicalize_local_path(local_path, static_dir)
+ local_path = _canonicalize_local_path(local_path)
dl = None
if local_path:
delete_source = _parse_boolean_arg(kwargs, 'delete_source')
- dl = downloader.LocalDownloader(static_dir, local_path,
+ dl = downloader.LocalDownloader(updater.static_dir, local_path,
delete_source=delete_source)
if not _is_android_build_request(kwargs):
@@ -211,7 +218,7 @@
if not dl:
archive_url = _canonicalize_archive_url(archive_url)
dl = downloader.GoogleStorageDownloader(
- static_dir, archive_url,
+ updater.static_dir, archive_url,
downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
archive_url))
elif not dl:
@@ -221,21 +228,20 @@
if not target or not branch or not build_id:
raise DevServerError('target, branch, build ID must all be specified for '
'downloading Android build.')
- dl = downloader.AndroidBuildDownloader(static_dir, branch, build_id,
+ dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
target)
return dl
-def _get_downloader_and_factory(static_dir, kwargs):
+def _get_downloader_and_factory(kwargs):
"""Returns the downloader and artifact factory based on passed in arguments.
Args:
- static_dir: Devserver's static cache directory.
kwargs: Keyword arguments for the request.
"""
artifacts, files = _get_artifacts(kwargs)
- dl = _get_downloader(static_dir, kwargs)
+ dl = _get_downloader(kwargs)
if (isinstance(dl, (downloader.GoogleStorageDownloader,
downloader.LocalDownloader))):
@@ -339,6 +345,11 @@
'/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,
@@ -509,11 +520,10 @@
# Lock used to lock increasing/decreasing count.
_staging_thread_count_lock = threading.Lock()
- def __init__(self, _xbuddy, static_dir):
+ def __init__(self, _xbuddy):
self._builder = None
self._telemetry_lock_dict = common_util.LockDict()
self._xbuddy = _xbuddy
- self._static_dir = static_dir
@property
def staging_thread_count(self):
@@ -552,7 +562,7 @@
Returns:
True of all artifacts are staged.
"""
- dl, factory = _get_downloader_and_factory(self._static_dir, kwargs)
+ 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
@@ -572,7 +582,7 @@
Returns:
A string with information about the contents of the image directory.
"""
- dl = _get_downloader(self._static_dir, kwargs)
+ dl = _get_downloader(kwargs)
try:
image_dir_contents = dl.ListBuildDir()
except build_artifact.ArtifactDownloadError as e:
@@ -634,7 +644,7 @@
custom post-processing.
clean: True to remove any previously staged artifacts first.
"""
- dl, factory = _get_downloader_and_factory(self._static_dir, kwargs)
+ dl, factory = _get_downloader_and_factory(kwargs)
with DevServerRoot._staging_thread_count_lock:
DevServerRoot._staging_thread_count += 1
@@ -670,7 +680,7 @@
if is_deprecated_server():
raise DeprecatedRPCError('locate_file')
- dl, _ = _get_downloader_and_factory(self._static_dir, kwargs)
+ dl, _ = _get_downloader_and_factory(kwargs)
try:
file_name = kwargs['file_name']
artifacts = kwargs['artifacts']
@@ -704,7 +714,7 @@
Returns:
Path to the source folder for the telemetry codebase once it is staged.
"""
- dl = _get_downloader(self._static_dir, kwargs)
+ dl = _get_downloader(kwargs)
build_path = dl.GetBuildDir()
deps_path = os.path.join(build_path, 'autotest/packages')
@@ -764,7 +774,7 @@
# Try debug.tar.xz first, then debug.tgz
for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
kwargs['artifacts'] = artifact
- dl = _get_downloader(self._static_dir, kwargs)
+ dl = _get_downloader(kwargs)
try:
if self.stage(**kwargs) == 'Success':
@@ -835,7 +845,7 @@
try:
return common_util.GetLatestBuildVersion(
- self._static_dir, kwargs['target'],
+ updater.static_dir, kwargs['target'],
milestone=kwargs.get('milestone'))
except common_util.CommonUtilError as errmsg:
raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
@@ -873,13 +883,13 @@
control_file_list = [
line.rstrip() for line in common_util.GetControlFileListForSuite(
- self._static_dir, kwargs['build'],
+ 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(
- self._static_dir, kwargs['build'], control_path))
+ updater.static_dir, kwargs['build'], control_path))
return json.dumps(control_file_content_dict)
@@ -922,13 +932,13 @@
if 'control_path' not in kwargs:
if 'suite_name' in kwargs and kwargs['suite_name']:
return common_util.GetControlFileListForSuite(
- self._static_dir, kwargs['build'], kwargs['suite_name'])
+ updater.static_dir, kwargs['build'], kwargs['suite_name'])
else:
return common_util.GetControlFileList(
- self._static_dir, kwargs['build'])
+ updater.static_dir, kwargs['build'])
else:
return common_util.GetControlFile(
- self._static_dir, kwargs['build'], kwargs['control_path'])
+ updater.static_dir, kwargs['build'], kwargs['control_path'])
@cherrypy.expose
def xbuddy_translate(self, *args, **kwargs):
@@ -1063,7 +1073,7 @@
"""Shows the documentation for available methods / URLs.
Examples:
- http://myhost/doc/xbuddy
+ http://myhost/doc/update
"""
if is_deprecated_server():
raise DeprecatedRPCError('doc')
@@ -1076,6 +1086,51 @@
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.
@@ -1223,10 +1278,15 @@
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, options.static_dir)
+ dev_server = DevServerRoot(_xbuddy)
health_checker_app = health_checker.Root(dev_server, options.static_dir)
if options.pidfile:
diff --git a/devserver_integration_test.py b/devserver_integration_test.py
index 89c5d19..64a492f 100755
--- a/devserver_integration_test.py
+++ b/devserver_integration_test.py
@@ -26,8 +26,12 @@
from string import Template
+from xml.dom import minidom
+
import requests
+from six.moves import urllib
+
import psutil # pylint: disable=import-error
import setup_chromite # pylint: disable=unused-import
@@ -211,6 +215,64 @@
self.pid = None
self.devserver = None
+
+ def VerifyHandleUpdate(self, label, use_test_payload=True,
+ appid='{DEV-BUILD}'):
+ """Verifies that we can send an update request to the devserver.
+
+ This method verifies (using a fake update_request blob) that the devserver
+ can interpret the payload and give us back the right payload.
+
+ Args:
+ label: Label that update is served from e.g. <board>-release/<version>
+ use_test_payload: If set to true, expects to serve payload under
+ testdata/ and does extra checks i.e. compares hash and content of
+ payload.
+ appid: The APP ID of the board.
+
+ Returns:
+ url of the update payload if we verified the update.
+ """
+ update_label = '/'.join([UPDATE, label])
+ response = self._MakeRPC(
+ update_label, data=UPDATE_REQUEST.substitute({'appid': appid}),
+ critical_update=True)
+ self.assertNotEqual('', response)
+ self.assertIn('deadline="now"', response)
+
+ # Parse the response and check if it contains the right result.
+ dom = minidom.parseString(response)
+ update = dom.getElementsByTagName('updatecheck')[0]
+ expected_static_url = '/'.join([self.devserver_url, STATIC, label])
+ url = self.VerifyV3Response(update, expected_static_url)
+
+ # Verify the image we download is correct since we already know what it is.
+ if use_test_payload:
+ connection = urllib.request.urlopen(url)
+ contents = connection.read().decode('utf-8')
+ connection.close()
+ self.assertEqual('Developers, developers, developers!\n', contents)
+
+ return url
+
+ def VerifyV3Response(self, update, expected_static_url):
+ """Verifies the update DOM from a v3 response and returns the url."""
+ # Parse the response and check if it contains the right result.
+ urls = update.getElementsByTagName('urls')[0]
+ url = urls.getElementsByTagName('url')[0]
+
+ static_url = url.getAttribute('codebase')
+ # Static url's end in /.
+ self.assertEqual(expected_static_url + '/', static_url)
+
+ manifest = update.getElementsByTagName('manifest')[0]
+ packages = manifest.getElementsByTagName('packages')[0]
+ package = packages.getElementsByTagName('package')[0]
+ filename = package.getAttribute('name')
+ self.assertEqual(TEST_UPDATE_PAYLOAD_NAME, filename)
+
+ return os.path.join(static_url, filename)
+
def _MakeRPC(self, rpc, data=None, timeout=None, **kwargs):
"""Makes an RPC call to the devserver.
@@ -276,6 +338,9 @@
they are lumped with the remote tests here.
"""
+ def testHandleUpdateV3(self):
+ self.VerifyHandleUpdate(label=LABEL)
+
def testXBuddyLocalAlias(self):
"""Extensive local image xbuddy unittest.
@@ -310,7 +375,7 @@
xbuddy_path = '/'.join([build_id, item])
logging.info('Testing xbuddy path %s', xbuddy_path)
- response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), timeout=3)
+ response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]))
self.assertEqual(response, data)
expected_dir = '/'.join([self.devserver_url, STATIC, build_id])
@@ -321,6 +386,14 @@
relative_path=True)
self.assertEqual(response, build_id)
+ logging.info('Verifying the actual payload data')
+ url = self.VerifyHandleUpdate(build_id, use_test_payload=False)
+ logging.info('Verify the actual content of the update payload')
+ connection = urllib.request.urlopen(url)
+ contents = connection.read().decode('utf-8')
+ connection.close()
+ self.assertEqual(update_data, contents)
+
def testPidFile(self):
"""Test that using a pidfile works correctly."""
with open(self.pidfile, 'r') as f:
@@ -339,6 +412,37 @@
2) time. These tests actually download the artifacts needed.
"""
+ def testStageAndUpdate(self):
+ """Tests core stage/update autotest workflow where with a test payload."""
+ build_id = 'eve-release/R78-12499.0.0'
+ archive_url = 'gs://chromeos-image-archive/%s' % build_id
+
+ response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
+ artifacts='full_payload,stateful')
+ self.assertEqual(response, 'False')
+
+ logging.info('Staging update artifacts')
+ self._MakeRPC(STAGE, archive_url=archive_url,
+ artifacts='full_payload,stateful')
+ logging.info('Staging complete. '
+ 'Verifying files exist and are staged in the staging '
+ 'directory.')
+ response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
+ artifacts='full_payload,stateful')
+ self.assertEqual(response, 'True')
+ staged_dir = os.path.join(self.test_data_path, build_id)
+ self.assertTrue(os.path.isdir(staged_dir))
+ self.assertTrue(os.path.exists(
+ os.path.join(staged_dir, devserver_constants.UPDATE_FILE)))
+ self.assertTrue(os.path.exists(
+ os.path.join(staged_dir, devserver_constants.UPDATE_METADATA_FILE)))
+ self.assertTrue(os.path.exists(
+ os.path.join(staged_dir, devserver_constants.STATEFUL_FILE)))
+
+ logging.info('Verifying we can update using the stage update artifacts.')
+ self.VerifyHandleUpdate(build_id, use_test_payload=False,
+ appid='{01906EA2-3EB2-41F1-8F62-F0B7120EFD2E}')
+
@unittest.skip('crbug.com/640063 Broken test.')
def testStageAutotestAndGetPackages(self):
"""Another stage/update autotest workflow test with a test payload."""