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."""