blob: 6f9e83a5d07e2afe8c6515e450084b7ff7e5da83 [file] [log] [blame]
Amin Hassani8d718d12019-06-02 21:28:39 -07001# -*- coding: utf-8 -*-
Darin Petkovc3fd90c2011-05-11 14:23:00 -07002# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00003# 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 13:00:41 -07006"""Devserver module for handling update client requests."""
7
Don Garrettfb15e322016-06-21 19:12:08 -07008from __future__ import print_function
9
Amin Hassaniaef2b292020-01-10 10:44:16 -080010import collections
11import contextlib
12import datetime
Dale Curtisc9aaf3a2011-08-09 15:47:40 -070013import json
rtc@google.comded22402009-10-26 22:36:21 +000014import os
Gilad Arnoldd0c71752013-12-06 11:48:45 -080015import threading
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070016import time
Chris Sosa7c931362010-10-11 19:49:01 -070017
Amin Hassani4f1e4622019-10-03 10:40:50 -070018from six.moves import urllib
19
20import cherrypy # pylint: disable=import-error
Gilad Arnoldabb352e2012-09-23 01:24:27 -070021
Amin Hassani8d718d12019-06-02 21:28:39 -070022# TODO(crbug.com/872441): We try to import nebraska from different places
23# because when we install the devserver, we copy the nebraska.py into the main
24# directory. Once this bug is resolved, we can always import from nebraska
25# directory.
26try:
27 from nebraska import nebraska
28except ImportError:
29 import nebraska
Chris Sosa05491b12010-11-08 17:14:16 -080030
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070031import setup_chromite # pylint: disable=unused-import
32from chromite.lib.xbuddy import build_util
33from chromite.lib.xbuddy import cherrypy_log_util
34from chromite.lib.xbuddy import common_util
35from chromite.lib.xbuddy import devserver_constants as constants
36
Gilad Arnoldc65330c2012-09-20 15:17:48 -070037
38# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080039def _Log(message, *args):
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070040 return cherrypy_log_util.LogWithTag('UPDATE', message, *args)
rtc@google.comded22402009-10-26 22:36:21 +000041
Gilad Arnold0c9c8602012-10-02 23:58:58 -070042class AutoupdateError(Exception):
43 """Exception classes used by this module."""
44 pass
45
46
Don Garrett0ad09372010-12-06 16:20:30 -080047def _ChangeUrlPort(url, new_port):
48 """Return the URL passed in with a different port"""
Amin Hassani4f1e4622019-10-03 10:40:50 -070049 scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
Don Garrett0ad09372010-12-06 16:20:30 -080050 host_port = netloc.split(':')
51
52 if len(host_port) == 1:
53 host_port.append(new_port)
54 else:
55 host_port[1] = new_port
56
Don Garrettfb15e322016-06-21 19:12:08 -070057 print(host_port)
joychen121fc9b2013-08-02 14:30:30 -070058 netloc = '%s:%s' % tuple(host_port)
Don Garrett0ad09372010-12-06 16:20:30 -080059
Amin Hassani4f1e4622019-10-03 10:40:50 -070060 # pylint: disable=too-many-function-args
61 return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
Don Garrett0ad09372010-12-06 16:20:30 -080062
Chris Sosa6a3697f2013-01-29 16:44:43 -080063def _NonePathJoin(*args):
64 """os.path.join that filters None's from the argument list."""
Amin Hassani4f1e4622019-10-03 10:40:50 -070065 return os.path.join(*[x for x in args if x is not None])
Don Garrett0ad09372010-12-06 16:20:30 -080066
Chris Sosa6a3697f2013-01-29 16:44:43 -080067
68class HostInfo(object):
Gilad Arnold286a0062012-01-12 13:47:02 -080069 """Records information about an individual host.
70
Amin Hassanie7ead902019-10-11 16:42:43 -070071 Attributes:
Gilad Arnold286a0062012-01-12 13:47:02 -080072 attrs: Static attributes (legacy)
73 log: Complete log of recorded client entries
74 """
75
76 def __init__(self):
77 # A dictionary of current attributes pertaining to the host.
78 self.attrs = {}
79
80 # A list of pairs consisting of a timestamp and a dictionary of recorded
81 # attributes.
82 self.log = []
83
84 def __repr__(self):
85 return 'attrs=%s, log=%s' % (self.attrs, self.log)
86
87 def AddLogEntry(self, entry):
88 """Append a new log entry."""
89 # Append a timestamp.
90 assert not 'timestamp' in entry, 'Oops, timestamp field already in use'
91 entry['timestamp'] = time.strftime('%Y-%m-%d %H:%M:%S')
92 # Add entry to hosts' message log.
93 self.log.append(entry)
94
Gilad Arnold286a0062012-01-12 13:47:02 -080095
Chris Sosa6a3697f2013-01-29 16:44:43 -080096class HostInfoTable(object):
Gilad Arnold286a0062012-01-12 13:47:02 -080097 """Records information about a set of hosts who engage in update activity.
98
Amin Hassanie7ead902019-10-11 16:42:43 -070099 Attributes:
Gilad Arnold286a0062012-01-12 13:47:02 -0800100 table: Table of information on hosts.
101 """
102
103 def __init__(self):
104 # A dictionary of host information. Keys are normally IP addresses.
105 self.table = {}
106
107 def __repr__(self):
108 return '%s' % self.table
109
110 def GetInitHostInfo(self, host_id):
111 """Return a host's info object, or create a new one if none exists."""
112 return self.table.setdefault(host_id, HostInfo())
113
114 def GetHostInfo(self, host_id):
115 """Return an info object for given host, if such exists."""
Chris Sosa1885d032012-11-29 17:07:27 -0800116 return self.table.get(host_id)
Gilad Arnold286a0062012-01-12 13:47:02 -0800117
118
Amin Hassaniaef2b292020-01-10 10:44:16 -0800119class SessionTable(object):
120 """A class to keep a map of session IDs and data.
121
122 This can be used to set some configuration related to a session and
123 retrieve/manipulate the configuration whenever needed. This is basically a map
124 of string to a dict object.
125 """
126
127 SESSION_EXPIRATION_TIMEDIFF = datetime.timedelta(hours=1)
128 OCCASIONAL_PURGE_TIMEDIFF = datetime.timedelta(hours=1)
129
130 Session = collections.namedtuple('Session', ['timestamp', 'data'])
131
132 def __init__(self):
133 """Initializes the SessionTable class."""
134 self._table = {}
135 # Since multiple requests might come for this session table by multiple
136 # threads, keep it under a lock.
137 self._lock = threading.Lock()
138 self._last_purge_time = datetime.datetime.now()
139
140 def _ShouldPurge(self):
141 """Returns whether its time to do an occasional purge."""
142 return (datetime.datetime.now() - self._last_purge_time >
143 self.OCCASIONAL_PURGE_TIMEDIFF)
144
145 def _IsSessionExpired(self, session):
146 """Returns whether a session needs to be purged.
147
148 Args:
149 session: A unique identifer string for a session.
150 """
151 return (datetime.datetime.now() - session.timestamp >
152 self.SESSION_EXPIRATION_TIMEDIFF)
153
154 def _Purge(self):
155 """Cleans up entries that have been here long enough.
156
157 This is so the memory usage of devserver doesn't get bloated.
158 """
159 # Try to purge once every hour or so.
160 if not self._ShouldPurge():
161 return
162
163 # Purge the ones not in use.
164 self._table = {k: v for k, v in self._table.items()
165 if not self._IsSessionExpired(v)}
166
167 def SetSessionData(self, session, data):
168 """Sets data for the given a session ID.
169
170 Args:
171 session: A unique identifier string.
172 data: A data to set for this session ID.
173 """
174 if not session or data is None:
175 return
176
177 with self._lock:
178 self._Purge()
179
180 if self._table.get(session) is not None:
181 _Log('Replacing an existing session %s', session)
182 self._table[session] = SessionTable.Session(datetime.datetime.now(), data)
183
184 @contextlib.contextmanager
185 def SessionData(self, session):
186 """Returns the session data for manipulation.
187
188 Args:
189 session: A unique identifier string.
190 """
191 # Cherrypy has multiple threads and this data structure is global, so lock
192 # it to restrict simultaneous access by multiple threads.
193 with self._lock:
194 session_value = self._table.get(session)
195 # If not in the table, just assume it wasn't supposed to be.
196 if session_value is None:
197 yield {}
198 else:
199 # To update the timestamp.
200 self._table[session] = SessionTable.Session(datetime.datetime.now(),
201 session_value.data)
202 yield session_value.data
203
204
joychen921e1fb2013-06-28 11:12:20 -0700205class Autoupdate(build_util.BuildObject):
Amin Hassanie9ffb862019-09-25 17:10:40 -0700206 """Class that contains functionality that handles Chrome OS update pings."""
rtc@google.comded22402009-10-26 22:36:21 +0000207
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700208 _PAYLOAD_URL_PREFIX = '/static/'
Chris Sosa6a3697f2013-01-29 16:44:43 -0800209
Amin Hassanie9ffb862019-09-25 17:10:40 -0700210 def __init__(self, xbuddy, payload_path=None, proxy_port=None,
Amin Hassanic9dd11e2019-07-11 15:33:55 -0700211 critical_update=False, max_updates=-1, host_log=False,
212 *args, **kwargs):
Amin Hassanie9ffb862019-09-25 17:10:40 -0700213 """Initializes the class.
214
215 Args:
216 xbuddy: The xbuddy path.
217 payload_path: The path to pre-generated payload to serve.
218 proxy_port: The port of local proxy to tell client to connect to you
219 through.
220 critical_update: Whether provisioned payload is critical.
221 max_updates: The maximum number of updates we'll try to provision.
222 host_log: Record full history of host update events.
223 """
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700224 super(Autoupdate, self).__init__(*args, **kwargs)
joychen121fc9b2013-08-02 14:30:30 -0700225 self.xbuddy = xbuddy
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700226 self.payload_path = payload_path
Don Garrett0ad09372010-12-06 16:20:30 -0800227 self.proxy_port = proxy_port
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800228 self.critical_update = critical_update
Jay Srinivasanac69d262012-10-30 19:05:53 -0700229 self.max_updates = max_updates
Gilad Arnold8318eac2012-10-04 12:52:23 -0700230 self.host_log = host_log
Don Garrettfff4c322010-11-19 13:37:12 -0800231
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700232 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 13:47:02 -0800233 # information about a given host. A host is identified by its IP address.
234 # The info stored for each host includes a complete log of events for this
235 # host, as well as a dictionary of current attributes derived from events.
236 self.host_infos = HostInfoTable()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700237
Amin Hassaniaef2b292020-01-10 10:44:16 -0800238 self._session_table = SessionTable()
239
Gilad Arnolde7819e72014-03-21 12:50:48 -0700240 self._update_count_lock = threading.Lock()
Gilad Arnoldd0c71752013-12-06 11:48:45 -0800241
Amin Hassanie9ffb862019-09-25 17:10:40 -0700242 def GetUpdateForLabel(self, label):
joychen121fc9b2013-08-02 14:30:30 -0700243 """Given a label, get an update from the directory.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700244
joychen121fc9b2013-08-02 14:30:30 -0700245 Args:
joychen121fc9b2013-08-02 14:30:30 -0700246 label: the relative directory inside the static dir
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700247
Chris Sosa6a3697f2013-01-29 16:44:43 -0800248 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700249 A relative path to the directory with the update payload.
250 This is the label if an update did not need to be generated, but can
251 be label/cache/hashed_dir_for_update.
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700252
Chris Sosa6a3697f2013-01-29 16:44:43 -0800253 Raises:
joychen121fc9b2013-08-02 14:30:30 -0700254 AutoupdateError: If client version is higher than available update found
255 at the directory given by the label.
Don Garrettf90edf02010-11-16 17:36:14 -0800256 """
Amin Hassanie9ffb862019-09-25 17:10:40 -0700257 _Log('Update label: %s', label)
258 static_update_path = _NonePathJoin(self.static_dir, label,
259 constants.UPDATE_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800260
joychen121fc9b2013-08-02 14:30:30 -0700261 if label and os.path.exists(static_update_path):
262 # An update payload was found for the given label, return it.
263 return label
Don Garrett0c880e22010-11-17 18:13:37 -0800264
joychen121fc9b2013-08-02 14:30:30 -0700265 # The label didn't resolve.
Amin Hassanie9ffb862019-09-25 17:10:40 -0700266 _Log('Did not found any update payload for label %s.', label)
joychen121fc9b2013-08-02 14:30:30 -0700267 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700268
Amin Hassanie7ead902019-10-11 16:42:43 -0700269 def _LogRequest(self, request):
270 """Logs the incoming request in the hostlog.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800271
Gilad Arnolde7819e72014-03-21 12:50:48 -0700272 Args:
Amin Hassani8d718d12019-06-02 21:28:39 -0700273 request: A nebraska.Request object representing the update request.
Gilad Arnolde7819e72014-03-21 12:50:48 -0700274
275 Returns:
276 A named tuple containing attributes of the update requests as the
Amin Hassani542b5492019-09-26 14:53:26 -0700277 following fields: 'board', 'event_result' and 'event_type'.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700278 """
Amin Hassanie7ead902019-10-11 16:42:43 -0700279 if not self.host_log:
280 return
281
282 # Add attributes to log message. Some of these values might be None.
283 log_message = {
284 'version': request.version,
285 'track': request.track,
286 'board': request.board or self.GetDefaultBoardID(),
287 'event_result': request.app_requests[0].event_result,
288 'event_type': request.app_requests[0].event_type,
289 'previous_version': request.app_requests[0].previous_version,
290 }
291 if log_message['previous_version'] is None:
292 del log_message['previous_version']
Jay Srinivasanac69d262012-10-30 19:05:53 -0700293
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700294 # Determine request IP, strip any IPv6 data for simplicity.
295 client_ip = cherrypy.request.remote.ip.split(':')[-1]
Gilad Arnold286a0062012-01-12 13:47:02 -0800296 # Obtain (or init) info object for this client.
297 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
Amin Hassanie7ead902019-10-11 16:42:43 -0700298 curr_host_info.AddLogEntry(log_message)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800299
David Rileyee75de22017-11-02 10:48:15 -0700300 def GetDevserverUrl(self):
301 """Returns the devserver url base."""
Chris Sosa6a3697f2013-01-29 16:44:43 -0800302 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
303 if x_forwarded_host:
304 hostname = 'http://' + x_forwarded_host
305 else:
306 hostname = cherrypy.request.base
307
David Rileyee75de22017-11-02 10:48:15 -0700308 return hostname
309
310 def GetStaticUrl(self):
311 """Returns the static url base that should prefix all payload responses."""
312 hostname = self.GetDevserverUrl()
313
Amin Hassanic9dd11e2019-07-11 15:33:55 -0700314 static_urlbase = '%s/static' % hostname
Chris Sosa6a3697f2013-01-29 16:44:43 -0800315 # If we have a proxy port, adjust the URL we instruct the client to
316 # use to go through the proxy.
317 if self.proxy_port:
318 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
319
320 _Log('Using static url base %s', static_urlbase)
321 _Log('Handling update ping as %s', hostname)
322 return static_urlbase
323
Amin Hassanie9ffb862019-09-25 17:10:40 -0700324 def GetPathToPayload(self, label, board):
joychen121fc9b2013-08-02 14:30:30 -0700325 """Find a payload locally.
326
327 See devserver's update rpc for documentation.
328
329 Args:
330 label: from update request
joychen121fc9b2013-08-02 14:30:30 -0700331 board: from update request
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700332
333 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700334 The relative path to an update from the static_dir
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700335
joychen121fc9b2013-08-02 14:30:30 -0700336 Raises:
337 AutoupdateError: If the update could not be found.
338 """
339 path_to_payload = None
Amin Hassanie9ffb862019-09-25 17:10:40 -0700340 # TODO(crbug.com/1006305): deprecate --payload flag
joychen121fc9b2013-08-02 14:30:30 -0700341 if self.payload_path:
342 # Copy the image from the path to '/forced_payload'
343 label = 'forced_payload'
344 dest_path = os.path.join(self.static_dir, label, constants.UPDATE_FILE)
345 dest_stateful = os.path.join(self.static_dir, label,
346 constants.STATEFUL_FILE)
Amin Hassani8d718d12019-06-02 21:28:39 -0700347 dest_meta = os.path.join(self.static_dir, label,
348 constants.UPDATE_METADATA_FILE)
joychen121fc9b2013-08-02 14:30:30 -0700349
350 src_path = os.path.abspath(self.payload_path)
Amin Hassanie9ffb862019-09-25 17:10:40 -0700351 src_meta = os.path.abspath(self.payload_path + '.json')
joychen121fc9b2013-08-02 14:30:30 -0700352 src_stateful = os.path.join(os.path.dirname(src_path),
353 constants.STATEFUL_FILE)
354 common_util.MkDirP(os.path.join(self.static_dir, label))
Alex Deymo3e2d4952013-09-03 21:49:41 -0700355 common_util.SymlinkFile(src_path, dest_path)
Amin Hassanie9ffb862019-09-25 17:10:40 -0700356 common_util.SymlinkFile(src_meta, dest_meta)
joychen121fc9b2013-08-02 14:30:30 -0700357 if os.path.exists(src_stateful):
358 # The stateful payload is optional.
Alex Deymo3e2d4952013-09-03 21:49:41 -0700359 common_util.SymlinkFile(src_stateful, dest_stateful)
joychen121fc9b2013-08-02 14:30:30 -0700360 else:
361 _Log('WARN: %s not found. Expected for dev and test builds',
362 constants.STATEFUL_FILE)
363 if os.path.exists(dest_stateful):
364 os.remove(dest_stateful)
Amin Hassanie9ffb862019-09-25 17:10:40 -0700365 path_to_payload = self.GetUpdateForLabel(label)
joychen121fc9b2013-08-02 14:30:30 -0700366 else:
367 label = label or ''
368 label_list = label.split('/')
369 # Suppose that the path follows old protocol of indexing straight
370 # into static_dir with board/version label.
371 # Attempt to get the update in that directory, generating if necc.
Amin Hassanie9ffb862019-09-25 17:10:40 -0700372 path_to_payload = self.GetUpdateForLabel(label)
joychen121fc9b2013-08-02 14:30:30 -0700373 if path_to_payload is None:
Amin Hassanie9ffb862019-09-25 17:10:40 -0700374 # There was no update found in the directory. Let XBuddy find the
375 # payloads.
joychen121fc9b2013-08-02 14:30:30 -0700376 if label_list[0] == 'xbuddy':
377 # If path explicitly calls xbuddy, pop off the tag.
378 label_list.pop()
Amin Hassanie9ffb862019-09-25 17:10:40 -0700379 x_label, _ = self.xbuddy.Translate(label_list, board=board)
380 # Path has been resolved, try to get the payload.
381 path_to_payload = self.GetUpdateForLabel(x_label)
joychen121fc9b2013-08-02 14:30:30 -0700382 if path_to_payload is None:
Amin Hassanie9ffb862019-09-25 17:10:40 -0700383 # No update payload found after translation. Try to get an update to
384 # a test image from GS using the label.
joychen121fc9b2013-08-02 14:30:30 -0700385 path_to_payload, _image_name = self.xbuddy.Get(
386 ['remote', label, 'full_payload'])
387
388 # One of the above options should have gotten us a relative path.
389 if path_to_payload is None:
390 raise AutoupdateError('Failed to get an update for: %s' % label)
Amin Hassani8d718d12019-06-02 21:28:39 -0700391
392 return path_to_payload
joychen121fc9b2013-08-02 14:30:30 -0700393
Amin Hassani6eec8792020-01-09 14:06:48 -0800394 def HandleUpdatePing(self, data, label='', **kwargs):
Chris Sosa6a3697f2013-01-29 16:44:43 -0800395 """Handles an update ping from an update client.
396
397 Args:
398 data: XML blob from client.
399 label: optional label for the update.
Amin Hassani6eec8792020-01-09 14:06:48 -0800400 kwargs: The map of query strings passed to the /update API.
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700401
Chris Sosa6a3697f2013-01-29 16:44:43 -0800402 Returns:
403 Update payload message for client.
404 """
405 # Get the static url base that will form that base of our update url e.g.
406 # http://hostname:8080/static/update.gz.
David Rileyee75de22017-11-02 10:48:15 -0700407 static_urlbase = self.GetStaticUrl()
Chris Sosa6a3697f2013-01-29 16:44:43 -0800408
Chris Sosab26b1202013-08-16 16:40:55 -0700409 # Process attributes of the update check.
Amin Hassani8d718d12019-06-02 21:28:39 -0700410 request = nebraska.Request(data)
Amin Hassanie7ead902019-10-11 16:42:43 -0700411 self._LogRequest(request)
Chris Sosab26b1202013-08-16 16:40:55 -0700412
Amin Hassaniaef2b292020-01-10 10:44:16 -0800413 session = kwargs.get('session')
414 _Log('Requested session is: %s', session)
415
Amin Hassani8d718d12019-06-02 21:28:39 -0700416 if request.request_type == nebraska.Request.RequestType.EVENT:
Amin Hassania50fa632019-10-15 20:49:51 -0700417 if (request.app_requests[0].event_type ==
418 nebraska.Request.EVENT_TYPE_UPDATE_DOWNLOAD_STARTED and
419 request.app_requests[0].event_result ==
420 nebraska.Request.EVENT_RESULT_SUCCESS):
Amin Hassaniaef2b292020-01-10 10:44:16 -0800421 err_msg = ('Received too many download_started notifications. This '
422 'probably means a bug in the test environment, such as too '
423 'many clients running concurrently. Alternatively, it could '
424 'be a bug in the update client.')
425
Gilad Arnolde7819e72014-03-21 12:50:48 -0700426 with self._update_count_lock:
427 if self.max_updates == 0:
Amin Hassaniaef2b292020-01-10 10:44:16 -0800428 _Log(err_msg)
Gilad Arnolde7819e72014-03-21 12:50:48 -0700429 elif self.max_updates > 0:
430 self.max_updates -= 1
joychen121fc9b2013-08-02 14:30:30 -0700431
Amin Hassaniaef2b292020-01-10 10:44:16 -0800432 with self._session_table.SessionData(session) as session_data:
433 value = session_data.get('max_updates')
434 if value is not None:
435 session_data['max_updates'] = max(value - 1, 0)
436 if value == 0:
437 _Log(err_msg)
438
Gilad Arnolde7819e72014-03-21 12:50:48 -0700439 _Log('A non-update event notification received. Returning an ack.')
Amin Hassani083e3fe2020-02-13 11:39:18 -0800440 return nebraska.Nebraska().GetResponseToRequest(
441 request, response_props=nebraska.ResponseProperties(**kwargs))
Chris Sosa6a3697f2013-01-29 16:44:43 -0800442
Gilad Arnolde7819e72014-03-21 12:50:48 -0700443 # Make sure that we did not already exceed the max number of allowed update
444 # responses. Note that the counter is only decremented when the client
445 # reports an actual download, to avoid race conditions between concurrent
446 # update requests from the same client due to a timeout.
Amin Hassaniaef2b292020-01-10 10:44:16 -0800447 max_updates = None
448 with self._session_table.SessionData(session) as session_data:
449 max_updates = session_data.get('max_updates')
450
451 if self.max_updates == 0 or max_updates == 0:
Gilad Arnolde7819e72014-03-21 12:50:48 -0700452 _Log('Request received but max number of updates already served.')
Amin Hassani083e3fe2020-02-13 11:39:18 -0800453 kwargs['no_update'] = True
454 # Override the noupdate to make sure the response is noupdate.
455 return nebraska.Nebraska().GetResponseToRequest(
456 request, response_props=nebraska.ResponseProperties(**kwargs))
joychen121fc9b2013-08-02 14:30:30 -0700457
Amin Hassani8d718d12019-06-02 21:28:39 -0700458 _Log('Update Check Received.')
Chris Sosa6a3697f2013-01-29 16:44:43 -0800459
460 try:
Amin Hassanie7ead902019-10-11 16:42:43 -0700461 path_to_payload = self.GetPathToPayload(label, request.board)
Amin Hassani8d718d12019-06-02 21:28:39 -0700462 base_url = _NonePathJoin(static_urlbase, path_to_payload)
Amin Hassanic9dd11e2019-07-11 15:33:55 -0700463 local_payload_dir = _NonePathJoin(self.static_dir, path_to_payload)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800464 except AutoupdateError as e:
465 # Raised if we fail to generate an update payload.
Amin Hassani8d718d12019-06-02 21:28:39 -0700466 _Log('Failed to process an update request, but we will defer to '
467 'nebraska to respond with no-update. The error was %s', e)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800468
Amin Hassani6eec8792020-01-09 14:06:48 -0800469 if self.critical_update:
470 kwargs['critical_update'] = True
471
Amin Hassani8d718d12019-06-02 21:28:39 -0700472 _Log('Responding to client to use url %s to get image', base_url)
Amin Hassanic91fc0d2019-12-04 11:07:16 -0800473 nebraska_props = nebraska.NebraskaProperties(
474 update_payloads_address=base_url,
475 update_metadata_dir=local_payload_dir)
Amin Hassanic91fc0d2019-12-04 11:07:16 -0800476 nebraska_obj = nebraska.Nebraska(nebraska_props=nebraska_props)
Amin Hassani083e3fe2020-02-13 11:39:18 -0800477 return nebraska_obj.GetResponseToRequest(
478 request, response_props=nebraska.ResponseProperties(**kwargs))
Gilad Arnoldd0c71752013-12-06 11:48:45 -0800479
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700480 def HandleHostInfoPing(self, ip):
481 """Returns host info dictionary for the given IP in JSON format."""
482 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800483 if ip in self.host_infos.table:
484 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
485
486 def HandleHostLogPing(self, ip):
487 """Returns a complete log of events for host in JSON format."""
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700488 # If all events requested, return a dictionary of logs keyed by IP address.
Gilad Arnold286a0062012-01-12 13:47:02 -0800489 if ip == 'all':
490 return json.dumps(
491 dict([(key, self.host_infos.table[key].log)
492 for key in self.host_infos.table]))
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700493
494 # Otherwise we're looking for a specific IP address, so find its log.
Gilad Arnold286a0062012-01-12 13:47:02 -0800495 if ip in self.host_infos.table:
496 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700497
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700498 # If no events were logged for this IP, return an empty log.
499 return json.dumps([])
Amin Hassaniaef2b292020-01-10 10:44:16 -0800500
501 def SetSessionData(self, session, data):
502 """Sets the session ID for the current run.
503
504 Args:
505 session: A unique identifier string.
506 data: A dictionary containing some data.
507 """
508 self._session_table.SetSessionData(session, data)