blob: db97ae37ebabef9fdec892ece28996b8283902ff [file] [log] [blame]
Sanika Kulkarni92f96a42020-06-16 11:31:14 -07001# -*- 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
8from __future__ import print_function
9
10import os
11import re
12import shutil
13import tempfile
14
15import requests
16from six.moves import urllib
17
18import cherrypy # pylint: disable=import-error
19
Sanika Kulkarnib78e16b2020-07-13 15:14:27 -070020# 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.
26try:
27 import nebraska
28 nebraska.QueryDictToDict({})
29except AttributeError as e:
30 from nebraska import nebraska
31
Sanika Kulkarni92f96a42020-06-16 11:31:14 -070032from 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.
39GS_CACHE_PORT = '8888'
40GS_ARCHIVE_BUCKET = 'chromeos-image-archive'
41GS_CACHE_DWLD_RPC = 'download'
42GS_CACHE_LIST_DIR_RPC = 'list_dir'
43
44
45def _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
51class NebraskaWrapperError(Exception):
52 """Exception class used by this module."""
Sanika Kulkarni92f96a42020-06-16 11:31:14 -070053
54
55class 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 Hassani89c7fac2020-12-10 12:16:26 -080071 <board>-<XXXX>/Rxx-xxxxx.x.x-<unique string>.
Sanika Kulkarni92f96a42020-06-16 11:31:14 -070072 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 Hassani89c7fac2020-12-10 12:16:26 -080078 self._label = self._GetLabel(label)
Sanika Kulkarni92f96a42020-06-16 11:31:14 -070079 self._gs_cache_base_url = 'http://%s:%s' % (server_addr, GS_CACHE_PORT)
80
Sanika Kulkarni92f96a42020-06-16 11:31:14 -070081 # When full_update parameter is not specified in the request, the update
Amin Hassani89c7fac2020-12-10 12:16:26 -080082 # type is 'delta'.
83 self._is_full_update = full_update.lower().strip() == 'true'
Sanika Kulkarni92f96a42020-06-16 11:31:14 -070084
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 Hassani89c7fac2020-12-10 12:16:26 -0800147 def _GetLabel(self, label):
148 """Gets the label for the request.
Sanika Kulkarni92f96a42020-06-16 11:31:14 -0700149
Amin Hassani89c7fac2020-12-10 12:16:26 -0800150 Removes a trailing /au_nton from the label argument.
Sanika Kulkarni92f96a42020-06-16 11:31:14 -0700151
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 Kulkarni92f96a42020-06-16 11:31:14 -0700157 """
Amin Hassani89c7fac2020-12-10 12:16:26 -0800158 # 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 Kulkarni92f96a42020-06-16 11:31:14 -0700161
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 Kulkarni78834022021-02-25 15:18:25 -0800213 with open(file_path, 'wb') as f:
Sanika Kulkarni92f96a42020-06-16 11:31:14 -0700214 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 Hassanibf9e0402021-02-23 19:41:00 -0800245 n = nebraska.Nebraska()
246 n.UpdateConfig(**kwargs)
247 return n.GetResponseToRequest(request)
Sanika Kulkarni92f96a42020-06-16 11:31:14 -0700248
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 Hassanibf9e0402021-02-23 19:41:00 -0800259 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 Kulkarni92f96a42020-06-16 11:31:14 -0700263
264 except Exception as e:
265 raise NebraskaWrapperError('An error occurred while processing the '
266 'update request: %s' % e)