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