blob: c78a3cf4d88a13aba9abbb6c2d6fd649e45e1db6 [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
20from nebraska import nebraska
21from 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.
28GS_CACHE_PORT = '8888'
29GS_ARCHIVE_BUCKET = 'chromeos-image-archive'
30GS_CACHE_DWLD_RPC = 'download'
31GS_CACHE_LIST_DIR_RPC = 'list_dir'
32
33
34def _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
40class NebraskaWrapperError(Exception):
41 """Exception class used by this module."""
42 pass
43
44
45class 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)