Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | # Copyright 2020 The Chromium OS Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | """A Nebraska Wrapper to handle update client requests.""" |
| 7 | |
| 8 | from __future__ import print_function |
| 9 | |
| 10 | import os |
| 11 | import re |
| 12 | import shutil |
| 13 | import tempfile |
| 14 | |
| 15 | import requests |
| 16 | from six.moves import urllib |
| 17 | |
| 18 | import cherrypy # pylint: disable=import-error |
| 19 | |
Sanika Kulkarni | b78e16b | 2020-07-13 15:14:27 -0700 | [diff] [blame] | 20 | # Nebraska.py has been added to PYTHONPATH, so gs_archive_server should be able |
| 21 | # to import nebraska.py directly. But if gs_archive_server is triggered from |
| 22 | # ~/chromiumos/src/platform/dev, it will import nebraska, the python package, |
| 23 | # instead of nebraska.py, thus throwing an AttributeError when the module is |
| 24 | # eventually used. To mitigate this, catch the exception and import nebraska.py |
| 25 | # from the nebraska package directly. |
| 26 | try: |
| 27 | import nebraska |
| 28 | nebraska.QueryDictToDict({}) |
| 29 | except AttributeError as e: |
| 30 | from nebraska import nebraska |
| 31 | |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 32 | from chromite.lib import cros_logging as logging |
| 33 | |
| 34 | |
| 35 | # Define module logger. |
| 36 | _logger = logging.getLogger(__file__) |
| 37 | |
| 38 | # Define all GS Cache related constants. |
| 39 | GS_CACHE_PORT = '8888' |
| 40 | GS_ARCHIVE_BUCKET = 'chromeos-image-archive' |
| 41 | GS_CACHE_DWLD_RPC = 'download' |
| 42 | GS_CACHE_LIST_DIR_RPC = 'list_dir' |
| 43 | |
| 44 | |
| 45 | def _log(*args, **kwargs): |
| 46 | """A wrapper function of logging.debug/info, etc.""" |
| 47 | level = kwargs.pop('level', logging.DEBUG) |
| 48 | _logger.log(level, extra=cherrypy.request.headers, *args, **kwargs) |
| 49 | |
| 50 | |
| 51 | class NebraskaWrapperError(Exception): |
| 52 | """Exception class used by this module.""" |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 53 | |
| 54 | |
| 55 | class NebraskaWrapper(object): |
| 56 | """Class that contains functionality that handles Chrome OS update pings.""" |
| 57 | |
| 58 | # Define regexes for properties file. These patterns are the same as the ones |
| 59 | # defined in chromite/lib/xbuddy/build_artifact.py. Only the '.*' in the |
| 60 | # beginning and '$' at the end is different as in this class, we need to |
| 61 | # compare the full gs URL of the file without a newline at the end to this |
| 62 | # regex pattern. |
| 63 | _FULL_PAYLOAD_PROPS_PATTERN = r'.*chromeos_.*_full_dev.*bin(\.json)$' |
| 64 | _DELTA_PAYLOAD_PROPS_PATTERN = r'.*chromeos_.*_delta_dev.*bin(\.json)$' |
| 65 | |
| 66 | def __init__(self, label, server_addr, full_update): |
| 67 | """Initializes the class. |
| 68 | |
| 69 | Args: |
| 70 | label: Label (string) for the update, typically in the format |
Amin Hassani | 89c7fac | 2020-12-10 12:16:26 -0800 | [diff] [blame] | 71 | <board>-<XXXX>/Rxx-xxxxx.x.x-<unique string>. |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 72 | server_addr: IP address (string) for the server on which gs cache is |
| 73 | running. |
| 74 | full_update: Indicates whether the requested update is full or delta. The |
| 75 | string values for this argument can be 'True', 'False', or |
| 76 | 'unspecified'. |
| 77 | """ |
Amin Hassani | 89c7fac | 2020-12-10 12:16:26 -0800 | [diff] [blame] | 78 | self._label = self._GetLabel(label) |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 79 | self._gs_cache_base_url = 'http://%s:%s' % (server_addr, GS_CACHE_PORT) |
| 80 | |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 81 | # When full_update parameter is not specified in the request, the update |
Amin Hassani | 89c7fac | 2020-12-10 12:16:26 -0800 | [diff] [blame] | 82 | # type is 'delta'. |
| 83 | self._is_full_update = full_update.lower().strip() == 'true' |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 84 | |
| 85 | self._props_dir = tempfile.mkdtemp(prefix='gsc-update') |
| 86 | self._payload_props_file = None |
| 87 | |
| 88 | def __enter__(self): |
| 89 | """Called while entering context manager; does nothing.""" |
| 90 | return self |
| 91 | |
| 92 | def __exit__(self, exc_type, exc_value, traceback): |
| 93 | """Called while exiting context manager; cleans up temp dirs.""" |
| 94 | try: |
| 95 | shutil.rmtree(self._props_dir) |
| 96 | except Exception as e: |
| 97 | _log('Something went wrong. Could not delete %s due to exception: %s', |
| 98 | self._props_dir, e, level=logging.WARNING) |
| 99 | |
| 100 | @property |
| 101 | def _PayloadPropsFilename(self): |
| 102 | """Get the name of the payload properties file. |
| 103 | |
| 104 | The name of the properties file is obtained from the list of files returned |
| 105 | by the list_dir RPC by matching the name of the file with the update_type |
| 106 | and file extension. |
| 107 | |
| 108 | Returns: |
| 109 | Name of the payload properties file. |
| 110 | |
| 111 | Raises: |
| 112 | NebraskaWrapperError if the list_dir calls returns 4xx/5xx or if the |
| 113 | correct file could not be determined. |
| 114 | """ |
| 115 | if self._payload_props_file: |
| 116 | return self._payload_props_file |
| 117 | |
| 118 | urlbase = self._GetListDirURL() |
| 119 | url = urllib.parse.urljoin(urlbase, self._label) |
| 120 | |
| 121 | resp = requests.get(url) |
| 122 | try: |
| 123 | resp.raise_for_status() |
| 124 | except Exception as e: |
| 125 | raise NebraskaWrapperError('An error occurred while trying to complete ' |
| 126 | 'the request: %s' % e) |
| 127 | |
| 128 | if self._is_full_update: |
| 129 | pattern = re.compile(self._FULL_PAYLOAD_PROPS_PATTERN) |
| 130 | else: |
| 131 | pattern = re.compile(self._DELTA_PAYLOAD_PROPS_PATTERN) |
| 132 | |
| 133 | # Iterate through all listed files to determine the correct payload |
| 134 | # properties file. Since the listed files will be in the format |
| 135 | # gs://<gs_bucket>/<board>/<version>/<filename>, return the filename only |
| 136 | # once a match is determined. |
| 137 | for fname in [x.strip() for x in resp.content.strip().split('\n')]: |
| 138 | if pattern.match(fname): |
| 139 | self._payload_props_file = fname.rsplit('/', 1)[-1] |
| 140 | return self._payload_props_file |
| 141 | |
| 142 | raise NebraskaWrapperError( |
| 143 | 'Request to %s returned a %s but gs_archive_server was unable to ' |
| 144 | 'determine the name of the properties file.' % |
| 145 | (url, resp.status_code)) |
| 146 | |
Amin Hassani | 89c7fac | 2020-12-10 12:16:26 -0800 | [diff] [blame] | 147 | def _GetLabel(self, label): |
| 148 | """Gets the label for the request. |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 149 | |
Amin Hassani | 89c7fac | 2020-12-10 12:16:26 -0800 | [diff] [blame] | 150 | Removes a trailing /au_nton from the label argument. |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 151 | |
| 152 | Args: |
| 153 | label: A string obtained from the request. |
| 154 | |
| 155 | Returns: |
| 156 | A string in the format <board>-<XXXX>/Rxx-xxxxx.x.x-<unique string>. |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 157 | """ |
Amin Hassani | 89c7fac | 2020-12-10 12:16:26 -0800 | [diff] [blame] | 158 | # TODO(crbug.com/1102552): Remove this logic once all clients stopped |
| 159 | # sending au_nton in the request. |
| 160 | return label[:-len('/au_nton')] if label.endswith('/au_nton') else label |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 161 | |
| 162 | def _GetDownloadURL(self): |
| 163 | """Returns the static url base that should prefix all payload responses.""" |
| 164 | _log('Handling update ping as %s', self._gs_cache_base_url) |
| 165 | return self._GetURL(GS_CACHE_DWLD_RPC) |
| 166 | |
| 167 | def _GetListDirURL(self): |
| 168 | """Returns the static url base that should prefix all list_dir requests.""" |
| 169 | _log('Using base URL to list contents: %s', self._gs_cache_base_url) |
| 170 | return self._GetURL(GS_CACHE_LIST_DIR_RPC) |
| 171 | |
| 172 | def _GetURL(self, rpc_name): |
| 173 | """Construct gs_cache URL for the given RPC. |
| 174 | |
| 175 | Args: |
| 176 | rpc_name: Name of the RPC for which the URL needs to be built. |
| 177 | |
| 178 | Returns: |
| 179 | Base URL to be used. |
| 180 | """ |
| 181 | urlbase = urllib.parse.urljoin(self._gs_cache_base_url, |
| 182 | '%s/%s/' % (rpc_name, GS_ARCHIVE_BUCKET)) |
| 183 | _log('Using static url base %s', urlbase) |
| 184 | return urlbase |
| 185 | |
| 186 | def _GetPayloadPropertiesDir(self, urlbase): |
| 187 | """Download payload properties file from GS Archive |
| 188 | |
| 189 | Args: |
| 190 | urlbase: Base url that should be used to form the download request. |
| 191 | |
| 192 | Returns: |
| 193 | The path to the /tmp directory which stores the payload properties file |
| 194 | that nebraska will use. |
| 195 | |
| 196 | Raises: |
| 197 | NebraskaWrapperError is raised if the method is unable to |
| 198 | download the file for some reason. |
| 199 | """ |
| 200 | local_payload_dir = self._props_dir |
| 201 | partial_url = urllib.parse.urljoin(urlbase, '%s/' % self._label) |
| 202 | _log('Downloading %s from bucket %s.', self._PayloadPropsFilename, |
| 203 | partial_url, level=logging.INFO) |
| 204 | |
| 205 | try: |
| 206 | resp = requests.get(urllib.parse.urljoin(partial_url, |
| 207 | self._PayloadPropsFilename)) |
| 208 | resp.raise_for_status() |
| 209 | file_path = os.path.join(local_payload_dir, self._PayloadPropsFilename) |
| 210 | # We are not worried about multiple threads writing to the same file as |
| 211 | # we are creating a different directory for each initialization of this |
| 212 | # class anyway. |
Sanika Kulkarni | 7883402 | 2021-02-25 15:18:25 -0800 | [diff] [blame^] | 213 | with open(file_path, 'wb') as f: |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 214 | f.write(resp.content) |
| 215 | except Exception as e: |
| 216 | raise NebraskaWrapperError('An error occurred while trying to complete ' |
| 217 | 'the request: %s' % e) |
| 218 | _log('Path to downloaded payload properties file: %s' % file_path) |
| 219 | return local_payload_dir |
| 220 | |
| 221 | def HandleUpdatePing(self, data, **kwargs): |
| 222 | """Handles an update ping from an update client. |
| 223 | |
| 224 | Args: |
| 225 | data: XML blob from client. |
| 226 | kwargs: The map of query strings passed to the /update API. |
| 227 | |
| 228 | Returns: |
| 229 | Update payload message for client. |
| 230 | """ |
| 231 | # Get the static url base that will form that base of our update url e.g. |
| 232 | # http://<GS_CACHE_IP>:<GS_CACHE_PORT>/download/chromeos-image-archive/. |
| 233 | urlbase = self._GetDownloadURL() |
| 234 | # Change the URL's string query dictionary provided by cherrypy to a |
| 235 | # valid dictionary that has proper values for its keys. e.g. True |
| 236 | # instead of 'True'. |
| 237 | kwargs = nebraska.QueryDictToDict(kwargs) |
| 238 | |
| 239 | try: |
| 240 | # Process attributes of the update check. |
| 241 | request = nebraska.Request(data) |
| 242 | if request.request_type == nebraska.Request.RequestType.EVENT: |
| 243 | _log('A non-update event notification received. Returning an ack.', |
| 244 | level=logging.INFO) |
Amin Hassani | bf9e040 | 2021-02-23 19:41:00 -0800 | [diff] [blame] | 245 | n = nebraska.Nebraska() |
| 246 | n.UpdateConfig(**kwargs) |
| 247 | return n.GetResponseToRequest(request) |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 248 | |
| 249 | _log('Update Check Received.') |
| 250 | |
| 251 | base_url = urllib.parse.urljoin(urlbase, '%s/' % self._label) |
| 252 | _log('Responding to client to use url %s to get image', base_url, |
| 253 | level=logging.INFO) |
| 254 | |
| 255 | local_payload_dir = self._GetPayloadPropertiesDir(urlbase=urlbase) |
| 256 | _log('Using %s as the update_metadata_dir for NebraskaProperties.', |
| 257 | local_payload_dir) |
| 258 | |
Amin Hassani | bf9e040 | 2021-02-23 19:41:00 -0800 | [diff] [blame] | 259 | n = nebraska.Nebraska() |
| 260 | n.UpdateConfig(update_payloads_address=base_url, |
| 261 | update_app_index=nebraska.AppIndex(local_payload_dir)) |
| 262 | return n.GetResponseToRequest(request) |
Sanika Kulkarni | 92f96a4 | 2020-06-16 11:31:14 -0700 | [diff] [blame] | 263 | |
| 264 | except Exception as e: |
| 265 | raise NebraskaWrapperError('An error occurred while processing the ' |
| 266 | 'update request: %s' % e) |