blob: 957bcc9f1190e2cb6db42b0c75cbb3e1d748fd58 [file] [log] [blame]
Amin Hassani8d718d12019-06-03 04:28:391# -*- coding: utf-8 -*-
Darin Petkovc3fd90c2011-05-11 21:23:002# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
[email protected]ded22402009-10-26 22:36:213# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Gilad Arnoldd8d595c2014-03-21 20:00:416"""Devserver module for handling update client requests."""
7
Don Garrettfb15e322016-06-22 02:12:088from __future__ import print_function
9
Dale Curtisc9aaf3a2011-08-09 22:47:4010import json
[email protected]ded22402009-10-26 22:36:2111import os
Gilad Arnoldd0c71752013-12-06 19:48:4512import threading
Darin Petkov2b2ff4b2010-07-27 22:02:0913import time
Chris Sosa7c931362010-10-12 02:49:0114
Amin Hassani4f1e4622019-10-03 17:40:5015from six.moves import urllib
16
17import cherrypy # pylint: disable=import-error
Gilad Arnoldabb352e2012-09-23 08:24:2718
Amin Hassani8d718d12019-06-03 04:28:3919# TODO(crbug.com/872441): We try to import nebraska from different places
20# because when we install the devserver, we copy the nebraska.py into the main
21# directory. Once this bug is resolved, we can always import from nebraska
22# directory.
23try:
24 from nebraska import nebraska
25except ImportError:
26 import nebraska
Chris Sosa05491b12010-11-09 01:14:1627
Achuith Bhandarkar662fb722019-10-31 23:12:4928import setup_chromite # pylint: disable=unused-import
29from chromite.lib.xbuddy import build_util
30from chromite.lib.xbuddy import cherrypy_log_util
31from chromite.lib.xbuddy import common_util
32from chromite.lib.xbuddy import devserver_constants as constants
33
Gilad Arnoldc65330c2012-09-20 22:17:4834
Gilad Arnoldc65330c2012-09-20 22:17:4835# Module-local log function.
Chris Sosa6a3697f2013-01-30 00:44:4336def _Log(message, *args):
Achuith Bhandarkar662fb722019-10-31 23:12:4937 return cherrypy_log_util.LogWithTag('UPDATE', message, *args)
[email protected]ded22402009-10-26 22:36:2138
Gilad Arnold0c9c8602012-10-03 06:58:5839class AutoupdateError(Exception):
40 """Exception classes used by this module."""
41 pass
42
43
Don Garrett0ad09372010-12-07 00:20:3044def _ChangeUrlPort(url, new_port):
45 """Return the URL passed in with a different port"""
Amin Hassani4f1e4622019-10-03 17:40:5046 scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
Don Garrett0ad09372010-12-07 00:20:3047 host_port = netloc.split(':')
48
49 if len(host_port) == 1:
50 host_port.append(new_port)
51 else:
52 host_port[1] = new_port
53
Don Garrettfb15e322016-06-22 02:12:0854 print(host_port)
joychen121fc9b2013-08-02 21:30:3055 netloc = '%s:%s' % tuple(host_port)
Don Garrett0ad09372010-12-07 00:20:3056
Amin Hassani4f1e4622019-10-03 17:40:5057 # pylint: disable=too-many-function-args
58 return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
Don Garrett0ad09372010-12-07 00:20:3059
Chris Sosa6a3697f2013-01-30 00:44:4360def _NonePathJoin(*args):
61 """os.path.join that filters None's from the argument list."""
Amin Hassani4f1e4622019-10-03 17:40:5062 return os.path.join(*[x for x in args if x is not None])
Don Garrett0ad09372010-12-07 00:20:3063
Chris Sosa6a3697f2013-01-30 00:44:4364
65class HostInfo(object):
Gilad Arnold286a0062012-01-12 21:47:0266 """Records information about an individual host.
67
Amin Hassanie7ead902019-10-11 23:42:4368 Attributes:
Gilad Arnold286a0062012-01-12 21:47:0269 attrs: Static attributes (legacy)
70 log: Complete log of recorded client entries
71 """
72
73 def __init__(self):
74 # A dictionary of current attributes pertaining to the host.
75 self.attrs = {}
76
77 # A list of pairs consisting of a timestamp and a dictionary of recorded
78 # attributes.
79 self.log = []
80
81 def __repr__(self):
82 return 'attrs=%s, log=%s' % (self.attrs, self.log)
83
84 def AddLogEntry(self, entry):
85 """Append a new log entry."""
86 # Append a timestamp.
87 assert not 'timestamp' in entry, 'Oops, timestamp field already in use'
88 entry['timestamp'] = time.strftime('%Y-%m-%d %H:%M:%S')
89 # Add entry to hosts' message log.
90 self.log.append(entry)
91
Gilad Arnold286a0062012-01-12 21:47:0292
Chris Sosa6a3697f2013-01-30 00:44:4393class HostInfoTable(object):
Gilad Arnold286a0062012-01-12 21:47:0294 """Records information about a set of hosts who engage in update activity.
95
Amin Hassanie7ead902019-10-11 23:42:4396 Attributes:
Gilad Arnold286a0062012-01-12 21:47:0297 table: Table of information on hosts.
98 """
99
100 def __init__(self):
101 # A dictionary of host information. Keys are normally IP addresses.
102 self.table = {}
103
104 def __repr__(self):
105 return '%s' % self.table
106
107 def GetInitHostInfo(self, host_id):
108 """Return a host's info object, or create a new one if none exists."""
109 return self.table.setdefault(host_id, HostInfo())
110
111 def GetHostInfo(self, host_id):
112 """Return an info object for given host, if such exists."""
Chris Sosa1885d032012-11-30 01:07:27113 return self.table.get(host_id)
Gilad Arnold286a0062012-01-12 21:47:02114
115
joychen921e1fb2013-06-28 18:12:20116class Autoupdate(build_util.BuildObject):
Amin Hassanie9ffb862019-09-26 00:10:40117 """Class that contains functionality that handles Chrome OS update pings."""
[email protected]ded22402009-10-26 22:36:21118
Gilad Arnold0c9c8602012-10-03 06:58:58119 _PAYLOAD_URL_PREFIX = '/static/'
Chris Sosa6a3697f2013-01-30 00:44:43120
Amin Hassanie9ffb862019-09-26 00:10:40121 def __init__(self, xbuddy, payload_path=None, proxy_port=None,
Amin Hassanic9dd11e2019-07-11 22:33:55122 critical_update=False, max_updates=-1, host_log=False,
123 *args, **kwargs):
Amin Hassanie9ffb862019-09-26 00:10:40124 """Initializes the class.
125
126 Args:
127 xbuddy: The xbuddy path.
128 payload_path: The path to pre-generated payload to serve.
129 proxy_port: The port of local proxy to tell client to connect to you
130 through.
131 critical_update: Whether provisioned payload is critical.
132 max_updates: The maximum number of updates we'll try to provision.
133 host_log: Record full history of host update events.
134 """
Sean O'Connor14b6a0a2010-03-21 06:23:48135 super(Autoupdate, self).__init__(*args, **kwargs)
joychen121fc9b2013-08-02 21:30:30136 self.xbuddy = xbuddy
Gilad Arnold0c9c8602012-10-03 06:58:58137 self.payload_path = payload_path
Don Garrett0ad09372010-12-07 00:20:30138 self.proxy_port = proxy_port
Satoru Takabayashid733cbe2011-11-15 17:36:32139 self.critical_update = critical_update
Jay Srinivasanac69d262012-10-31 02:05:53140 self.max_updates = max_updates
Gilad Arnold8318eac2012-10-04 19:52:23141 self.host_log = host_log
Don Garrettfff4c322010-11-19 21:37:12142
Dale Curtisc9aaf3a2011-08-09 22:47:40143 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 21:47:02144 # information about a given host. A host is identified by its IP address.
145 # The info stored for each host includes a complete log of events for this
146 # host, as well as a dictionary of current attributes derived from events.
147 self.host_infos = HostInfoTable()
Dale Curtisc9aaf3a2011-08-09 22:47:40148
Gilad Arnolde7819e72014-03-21 19:50:48149 self._update_count_lock = threading.Lock()
Gilad Arnoldd0c71752013-12-06 19:48:45150
Amin Hassanie9ffb862019-09-26 00:10:40151 def GetUpdateForLabel(self, label):
joychen121fc9b2013-08-02 21:30:30152 """Given a label, get an update from the directory.
Chris Sosa0356d3b2010-09-16 22:46:22153
joychen121fc9b2013-08-02 21:30:30154 Args:
joychen121fc9b2013-08-02 21:30:30155 label: the relative directory inside the static dir
Gilad Arnoldd8d595c2014-03-21 20:00:41156
Chris Sosa6a3697f2013-01-30 00:44:43157 Returns:
joychen121fc9b2013-08-02 21:30:30158 A relative path to the directory with the update payload.
159 This is the label if an update did not need to be generated, but can
160 be label/cache/hashed_dir_for_update.
Gilad Arnoldd8d595c2014-03-21 20:00:41161
Chris Sosa6a3697f2013-01-30 00:44:43162 Raises:
joychen121fc9b2013-08-02 21:30:30163 AutoupdateError: If client version is higher than available update found
164 at the directory given by the label.
Don Garrettf90edf02010-11-17 01:36:14165 """
Amin Hassanie9ffb862019-09-26 00:10:40166 _Log('Update label: %s', label)
167 static_update_path = _NonePathJoin(self.static_dir, label,
168 constants.UPDATE_FILE)
Don Garrettee25e552010-11-23 20:09:35169
joychen121fc9b2013-08-02 21:30:30170 if label and os.path.exists(static_update_path):
171 # An update payload was found for the given label, return it.
172 return label
Don Garrett0c880e22010-11-18 02:13:37173
joychen121fc9b2013-08-02 21:30:30174 # The label didn't resolve.
Amin Hassanie9ffb862019-09-26 00:10:40175 _Log('Did not found any update payload for label %s.', label)
joychen121fc9b2013-08-02 21:30:30176 return None
Chris Sosa2c048f12010-10-27 23:05:27177
Amin Hassanie7ead902019-10-11 23:42:43178 def _LogRequest(self, request):
179 """Logs the incoming request in the hostlog.
Chris Sosa6a3697f2013-01-30 00:44:43180
Gilad Arnolde7819e72014-03-21 19:50:48181 Args:
Amin Hassani8d718d12019-06-03 04:28:39182 request: A nebraska.Request object representing the update request.
Gilad Arnolde7819e72014-03-21 19:50:48183
184 Returns:
185 A named tuple containing attributes of the update requests as the
Amin Hassani542b5492019-09-26 21:53:26186 following fields: 'board', 'event_result' and 'event_type'.
Chris Sosa0356d3b2010-09-16 22:46:22187 """
Amin Hassanie7ead902019-10-11 23:42:43188 if not self.host_log:
189 return
190
191 # Add attributes to log message. Some of these values might be None.
192 log_message = {
193 'version': request.version,
194 'track': request.track,
195 'board': request.board or self.GetDefaultBoardID(),
196 'event_result': request.app_requests[0].event_result,
197 'event_type': request.app_requests[0].event_type,
198 'previous_version': request.app_requests[0].previous_version,
199 }
200 if log_message['previous_version'] is None:
201 del log_message['previous_version']
Jay Srinivasanac69d262012-10-31 02:05:53202
Dale Curtisc9aaf3a2011-08-09 22:47:40203 # Determine request IP, strip any IPv6 data for simplicity.
204 client_ip = cherrypy.request.remote.ip.split(':')[-1]
Gilad Arnold286a0062012-01-12 21:47:02205 # Obtain (or init) info object for this client.
206 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
Amin Hassanie7ead902019-10-11 23:42:43207 curr_host_info.AddLogEntry(log_message)
Chris Sosa6a3697f2013-01-30 00:44:43208
David Rileyee75de22017-11-02 17:48:15209 def GetDevserverUrl(self):
210 """Returns the devserver url base."""
Chris Sosa6a3697f2013-01-30 00:44:43211 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
212 if x_forwarded_host:
213 hostname = 'http://' + x_forwarded_host
214 else:
215 hostname = cherrypy.request.base
216
David Rileyee75de22017-11-02 17:48:15217 return hostname
218
219 def GetStaticUrl(self):
220 """Returns the static url base that should prefix all payload responses."""
221 hostname = self.GetDevserverUrl()
222
Amin Hassanic9dd11e2019-07-11 22:33:55223 static_urlbase = '%s/static' % hostname
Chris Sosa6a3697f2013-01-30 00:44:43224 # If we have a proxy port, adjust the URL we instruct the client to
225 # use to go through the proxy.
226 if self.proxy_port:
227 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
228
229 _Log('Using static url base %s', static_urlbase)
230 _Log('Handling update ping as %s', hostname)
231 return static_urlbase
232
Amin Hassanie9ffb862019-09-26 00:10:40233 def GetPathToPayload(self, label, board):
joychen121fc9b2013-08-02 21:30:30234 """Find a payload locally.
235
236 See devserver's update rpc for documentation.
237
238 Args:
239 label: from update request
joychen121fc9b2013-08-02 21:30:30240 board: from update request
Gilad Arnoldd8d595c2014-03-21 20:00:41241
242 Returns:
joychen121fc9b2013-08-02 21:30:30243 The relative path to an update from the static_dir
Gilad Arnoldd8d595c2014-03-21 20:00:41244
joychen121fc9b2013-08-02 21:30:30245 Raises:
246 AutoupdateError: If the update could not be found.
247 """
248 path_to_payload = None
Amin Hassanie9ffb862019-09-26 00:10:40249 # TODO(crbug.com/1006305): deprecate --payload flag
joychen121fc9b2013-08-02 21:30:30250 if self.payload_path:
251 # Copy the image from the path to '/forced_payload'
252 label = 'forced_payload'
253 dest_path = os.path.join(self.static_dir, label, constants.UPDATE_FILE)
254 dest_stateful = os.path.join(self.static_dir, label,
255 constants.STATEFUL_FILE)
Amin Hassani8d718d12019-06-03 04:28:39256 dest_meta = os.path.join(self.static_dir, label,
257 constants.UPDATE_METADATA_FILE)
joychen121fc9b2013-08-02 21:30:30258
259 src_path = os.path.abspath(self.payload_path)
Amin Hassanie9ffb862019-09-26 00:10:40260 src_meta = os.path.abspath(self.payload_path + '.json')
joychen121fc9b2013-08-02 21:30:30261 src_stateful = os.path.join(os.path.dirname(src_path),
262 constants.STATEFUL_FILE)
263 common_util.MkDirP(os.path.join(self.static_dir, label))
Alex Deymo3e2d4952013-09-04 04:49:41264 common_util.SymlinkFile(src_path, dest_path)
Amin Hassanie9ffb862019-09-26 00:10:40265 common_util.SymlinkFile(src_meta, dest_meta)
joychen121fc9b2013-08-02 21:30:30266 if os.path.exists(src_stateful):
267 # The stateful payload is optional.
Alex Deymo3e2d4952013-09-04 04:49:41268 common_util.SymlinkFile(src_stateful, dest_stateful)
joychen121fc9b2013-08-02 21:30:30269 else:
270 _Log('WARN: %s not found. Expected for dev and test builds',
271 constants.STATEFUL_FILE)
272 if os.path.exists(dest_stateful):
273 os.remove(dest_stateful)
Amin Hassanie9ffb862019-09-26 00:10:40274 path_to_payload = self.GetUpdateForLabel(label)
joychen121fc9b2013-08-02 21:30:30275 else:
276 label = label or ''
277 label_list = label.split('/')
278 # Suppose that the path follows old protocol of indexing straight
279 # into static_dir with board/version label.
280 # Attempt to get the update in that directory, generating if necc.
Amin Hassanie9ffb862019-09-26 00:10:40281 path_to_payload = self.GetUpdateForLabel(label)
joychen121fc9b2013-08-02 21:30:30282 if path_to_payload is None:
Amin Hassanie9ffb862019-09-26 00:10:40283 # There was no update found in the directory. Let XBuddy find the
284 # payloads.
joychen121fc9b2013-08-02 21:30:30285 if label_list[0] == 'xbuddy':
286 # If path explicitly calls xbuddy, pop off the tag.
287 label_list.pop()
Amin Hassanie9ffb862019-09-26 00:10:40288 x_label, _ = self.xbuddy.Translate(label_list, board=board)
289 # Path has been resolved, try to get the payload.
290 path_to_payload = self.GetUpdateForLabel(x_label)
joychen121fc9b2013-08-02 21:30:30291 if path_to_payload is None:
Amin Hassanie9ffb862019-09-26 00:10:40292 # No update payload found after translation. Try to get an update to
293 # a test image from GS using the label.
joychen121fc9b2013-08-02 21:30:30294 path_to_payload, _image_name = self.xbuddy.Get(
295 ['remote', label, 'full_payload'])
296
297 # One of the above options should have gotten us a relative path.
298 if path_to_payload is None:
299 raise AutoupdateError('Failed to get an update for: %s' % label)
Amin Hassani8d718d12019-06-03 04:28:39300
301 return path_to_payload
joychen121fc9b2013-08-02 21:30:30302
Amin Hassani6eec8792020-01-09 22:06:48303 def HandleUpdatePing(self, data, label='', **kwargs):
Chris Sosa6a3697f2013-01-30 00:44:43304 """Handles an update ping from an update client.
305
306 Args:
307 data: XML blob from client.
308 label: optional label for the update.
Amin Hassani6eec8792020-01-09 22:06:48309 kwargs: The map of query strings passed to the /update API.
Gilad Arnoldd8d595c2014-03-21 20:00:41310
Chris Sosa6a3697f2013-01-30 00:44:43311 Returns:
312 Update payload message for client.
313 """
314 # Get the static url base that will form that base of our update url e.g.
315 # http://hostname:8080/static/update.gz.
David Rileyee75de22017-11-02 17:48:15316 static_urlbase = self.GetStaticUrl()
Chris Sosa6a3697f2013-01-30 00:44:43317
Chris Sosab26b1202013-08-16 23:40:55318 # Process attributes of the update check.
Amin Hassani8d718d12019-06-03 04:28:39319 request = nebraska.Request(data)
Amin Hassanie7ead902019-10-11 23:42:43320 self._LogRequest(request)
Chris Sosab26b1202013-08-16 23:40:55321
Amin Hassani8d718d12019-06-03 04:28:39322 if request.request_type == nebraska.Request.RequestType.EVENT:
Amin Hassania50fa632019-10-16 03:49:51323 if (request.app_requests[0].event_type ==
324 nebraska.Request.EVENT_TYPE_UPDATE_DOWNLOAD_STARTED and
325 request.app_requests[0].event_result ==
326 nebraska.Request.EVENT_RESULT_SUCCESS):
Gilad Arnolde7819e72014-03-21 19:50:48327 with self._update_count_lock:
328 if self.max_updates == 0:
Amin Hassani6aa075c2020-02-21 18:36:44329 _Log('Received too many download_started notifications. This '
330 'probably means a bug in the test environment, such as too '
331 'many clients running concurrently. Alternatively, it could '
332 'be a bug in the update client.')
Gilad Arnolde7819e72014-03-21 19:50:48333 elif self.max_updates > 0:
334 self.max_updates -= 1
joychen121fc9b2013-08-02 21:30:30335
Gilad Arnolde7819e72014-03-21 19:50:48336 _Log('A non-update event notification received. Returning an ack.')
Amin Hassani083e3fe2020-02-13 19:39:18337 return nebraska.Nebraska().GetResponseToRequest(
338 request, response_props=nebraska.ResponseProperties(**kwargs))
Chris Sosa6a3697f2013-01-30 00:44:43339
Gilad Arnolde7819e72014-03-21 19:50:48340 # Make sure that we did not already exceed the max number of allowed update
341 # responses. Note that the counter is only decremented when the client
342 # reports an actual download, to avoid race conditions between concurrent
343 # update requests from the same client due to a timeout.
Amin Hassani6aa075c2020-02-21 18:36:44344 if self.max_updates == 0:
Gilad Arnolde7819e72014-03-21 19:50:48345 _Log('Request received but max number of updates already served.')
Amin Hassani083e3fe2020-02-13 19:39:18346 kwargs['no_update'] = True
347 # Override the noupdate to make sure the response is noupdate.
348 return nebraska.Nebraska().GetResponseToRequest(
349 request, response_props=nebraska.ResponseProperties(**kwargs))
joychen121fc9b2013-08-02 21:30:30350
Amin Hassani8d718d12019-06-03 04:28:39351 _Log('Update Check Received.')
Chris Sosa6a3697f2013-01-30 00:44:43352
353 try:
Amin Hassanie7ead902019-10-11 23:42:43354 path_to_payload = self.GetPathToPayload(label, request.board)
Amin Hassani8d718d12019-06-03 04:28:39355 base_url = _NonePathJoin(static_urlbase, path_to_payload)
Amin Hassanic9dd11e2019-07-11 22:33:55356 local_payload_dir = _NonePathJoin(self.static_dir, path_to_payload)
Chris Sosa6a3697f2013-01-30 00:44:43357 except AutoupdateError as e:
358 # Raised if we fail to generate an update payload.
Amin Hassani8d718d12019-06-03 04:28:39359 _Log('Failed to process an update request, but we will defer to '
360 'nebraska to respond with no-update. The error was %s', e)
Chris Sosa6a3697f2013-01-30 00:44:43361
Amin Hassani6eec8792020-01-09 22:06:48362 if self.critical_update:
363 kwargs['critical_update'] = True
364
Amin Hassani8d718d12019-06-03 04:28:39365 _Log('Responding to client to use url %s to get image', base_url)
Amin Hassanic91fc0d2019-12-04 19:07:16366 nebraska_props = nebraska.NebraskaProperties(
367 update_payloads_address=base_url,
368 update_metadata_dir=local_payload_dir)
Amin Hassanic91fc0d2019-12-04 19:07:16369 nebraska_obj = nebraska.Nebraska(nebraska_props=nebraska_props)
Amin Hassani083e3fe2020-02-13 19:39:18370 return nebraska_obj.GetResponseToRequest(
371 request, response_props=nebraska.ResponseProperties(**kwargs))
Gilad Arnoldd0c71752013-12-06 19:48:45372
Dale Curtisc9aaf3a2011-08-09 22:47:40373 def HandleHostInfoPing(self, ip):
374 """Returns host info dictionary for the given IP in JSON format."""
375 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 21:47:02376 if ip in self.host_infos.table:
377 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
378
379 def HandleHostLogPing(self, ip):
380 """Returns a complete log of events for host in JSON format."""
Gilad Arnold4ba437d2012-10-05 22:28:27381 # If all events requested, return a dictionary of logs keyed by IP address.
Gilad Arnold286a0062012-01-12 21:47:02382 if ip == 'all':
383 return json.dumps(
384 dict([(key, self.host_infos.table[key].log)
385 for key in self.host_infos.table]))
Gilad Arnold4ba437d2012-10-05 22:28:27386
387 # Otherwise we're looking for a specific IP address, so find its log.
Gilad Arnold286a0062012-01-12 21:47:02388 if ip in self.host_infos.table:
389 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 22:47:40390
Gilad Arnold4ba437d2012-10-05 22:28:27391 # If no events were logged for this IP, return an empty log.
392 return json.dumps([])