blob: 66febf745563216b297b88549a92ca4c7187539b [file] [log] [blame]
Darin Petkovc3fd90c2011-05-11 14:23:00 -07001# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from xml.dom import minidom
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08006import datetime
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07007import json
rtc@google.comded22402009-10-26 22:36:21 +00008import os
Darin Petkov798fe7d2010-03-22 15:18:13 -07009import shutil
Chris Sosa05491b12010-11-08 17:14:16 -080010import subprocess
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070011import time
Gilad Arnold0c9c8602012-10-02 23:58:58 -070012import urllib2
Don Garrett0ad09372010-12-06 16:20:30 -080013import urlparse
Chris Sosa7c931362010-10-11 19:49:01 -070014
Gilad Arnoldabb352e2012-09-23 01:24:27 -070015import cherrypy
16
17from build_util import BuildObject
Gilad Arnold55a2a372012-10-02 09:46:32 -070018import common_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070019import log_util
Chris Sosa05491b12010-11-08 17:14:16 -080020
Gilad Arnoldc65330c2012-09-20 15:17:48 -070021
22# Module-local log function.
23def _Log(message, *args, **kwargs):
24 return log_util.LogWithTag('UPDATE', message, *args, **kwargs)
25
rtc@google.comded22402009-10-26 22:36:21 +000026
Chris Sosa417e55d2011-01-25 16:40:48 -080027UPDATE_FILE = 'update.gz'
28STATEFUL_FILE = 'stateful.tgz'
29CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 15:46:22 -070030
Don Garrett0ad09372010-12-06 16:20:30 -080031
Gilad Arnold0c9c8602012-10-02 23:58:58 -070032class AutoupdateError(Exception):
33 """Exception classes used by this module."""
34 pass
35
36
Don Garrett0ad09372010-12-06 16:20:30 -080037def _ChangeUrlPort(url, new_port):
38 """Return the URL passed in with a different port"""
39 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
40 host_port = netloc.split(':')
41
42 if len(host_port) == 1:
43 host_port.append(new_port)
44 else:
45 host_port[1] = new_port
46
47 print host_port
48 netloc = "%s:%s" % tuple(host_port)
49
50 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
51
52
Gilad Arnold286a0062012-01-12 13:47:02 -080053class HostInfo:
54 """Records information about an individual host.
55
56 Members:
57 attrs: Static attributes (legacy)
58 log: Complete log of recorded client entries
59 """
60
61 def __init__(self):
62 # A dictionary of current attributes pertaining to the host.
63 self.attrs = {}
64
65 # A list of pairs consisting of a timestamp and a dictionary of recorded
66 # attributes.
67 self.log = []
68
69 def __repr__(self):
70 return 'attrs=%s, log=%s' % (self.attrs, self.log)
71
72 def AddLogEntry(self, entry):
73 """Append a new log entry."""
74 # Append a timestamp.
75 assert not 'timestamp' in entry, 'Oops, timestamp field already in use'
76 entry['timestamp'] = time.strftime('%Y-%m-%d %H:%M:%S')
77 # Add entry to hosts' message log.
78 self.log.append(entry)
79
80 def SetAttr(self, attr, value):
81 """Set an attribute value."""
82 self.attrs[attr] = value
83
84 def GetAttr(self, attr):
85 """Returns the value of an attribute."""
86 if attr in self.attrs:
87 return self.attrs[attr]
88
89 def PopAttr(self, attr, default):
90 """Returns and deletes a particular attribute."""
91 return self.attrs.pop(attr, default)
92
93
94class HostInfoTable:
95 """Records information about a set of hosts who engage in update activity.
96
97 Members:
98 table: Table of information on hosts.
99 """
100
101 def __init__(self):
102 # A dictionary of host information. Keys are normally IP addresses.
103 self.table = {}
104
105 def __repr__(self):
106 return '%s' % self.table
107
108 def GetInitHostInfo(self, host_id):
109 """Return a host's info object, or create a new one if none exists."""
110 return self.table.setdefault(host_id, HostInfo())
111
112 def GetHostInfo(self, host_id):
113 """Return an info object for given host, if such exists."""
114 if host_id in self.table:
115 return self.table[host_id]
116
117
rtc@google.com64244662009-11-12 00:52:08 +0000118class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700119 """Class that contains functionality that handles Chrome OS update pings.
120
121 Members:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700122 serve_only: serve only pre-built updates. static_dir must contain
123 update.gz and stateful.tgz.
124 factory_config: path to the factory config file if handling factory
125 requests.
126 use_test_image: use chromiumos_test_image.bin rather than the standard.
127 urlbase: base URL, other than devserver, for update images.
128 forced_image: path to an image to use for all updates.
129 payload_path: path to pre-generated payload to serve.
130 src_image: if specified, creates a delta payload from this image.
131 proxy_port: port of local proxy to tell client to connect to you
132 through.
133 vm: set for VM images (doesn't patch kernel)
134 board: board for the image. Needed for pre-generating of updates.
135 copy_to_static_root: copies images generated from the cache to ~/static.
136 private_key: path to private key in PEM format.
137 critical_update: whether provisioned payload is critical.
138 remote_payload: whether provisioned payload is remotely staged.
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700139 max_updates: maximum number of updates we'll try to provision.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700140 """
rtc@google.comded22402009-10-26 22:36:21 +0000141
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700142 _PAYLOAD_URL_PREFIX = '/static/'
143 _FILEINFO_URL_PREFIX = '/api/fileinfo/'
144
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700145 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Greg Spencerc8b59b22011-03-15 14:15:23 -0700146 factory_config_path=None,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700147 forced_image=None, payload_path=None,
148 proxy_port=None, src_image='', vm=False, board=None,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800149 copy_to_static_root=True, private_key=None,
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700150 critical_update=False, remote_payload=False, max_updates=-1,
Chris Sosae67b78f2010-11-04 17:33:16 -0700151 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700152 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700153 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -0700154 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700155 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -0700156 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700157 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -0700158 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700159 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -0700160
Chris Sosa0356d3b2010-09-16 15:46:22 -0700161 self.forced_image = forced_image
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700162 self.payload_path = payload_path
Chris Sosa62f720b2010-10-26 21:39:48 -0700163 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -0800164 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -0700165 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -0700166 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -0800167 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-14 16:33:22 -0800168 self.private_key = private_key
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800169 self.critical_update = critical_update
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700170 self.remote_payload = remote_payload
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700171 self.max_updates=max_updates
Don Garrettfff4c322010-11-19 13:37:12 -0800172
Chris Sosa417e55d2011-01-25 16:40:48 -0800173 # Path to pre-generated file.
174 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700175
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700176 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 13:47:02 -0800177 # information about a given host. A host is identified by its IP address.
178 # The info stored for each host includes a complete log of events for this
179 # host, as well as a dictionary of current attributes derived from events.
180 self.host_infos = HostInfoTable()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700181
Chris Sosa0356d3b2010-09-16 15:46:22 -0700182 def _GetSecondsSinceMidnight(self):
183 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700184 now = time.localtime()
185 return now[3] * 3600 + now[4] * 60 + now[5]
186
Chris Sosa0356d3b2010-09-16 15:46:22 -0700187 def _GetDefaultBoardID(self):
188 """Returns the default board id stored in .default_board."""
189 board_file = '%s/.default_board' % (self.scripts_dir)
190 try:
191 return open(board_file).read()
192 except IOError:
193 return 'x86-generic'
194
195 def _GetLatestImageDir(self, board_id):
196 """Returns the latest image dir based on shell script."""
197 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
198 return os.popen(cmd).read().strip()
199
200 def _GetVersionFromDir(self, image_dir):
201 """Returns the version of the image based on the name of the directory."""
202 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700203 parts = latest_version.split('-')
204 if len(parts) == 2:
205 # Old-style, e.g. "0.15.938.2011_08_23_0941-a1".
206 # TODO(derat): Remove the code for old-style versions after 20120101.
207 return parts[0]
208 else:
209 # New-style, e.g. "R16-1102.0.2011_09_30_0806-a1".
210 return parts[1]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700211
212 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800213 """Returns true if the latest_version is greater than the client_version.
214 """
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700215 _Log('client version %s latest version %s'
216 % (client_version, latest_version))
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700217
218 client_tokens = client_version.replace('_', '').split('.')
219 # If the client has an old four-token version like "0.16.892.0", drop the
220 # first two tokens -- we use versions like "892.0.0" now.
221 # TODO(derat): Remove the code for old-style versions after 20120101.
222 if len(client_tokens) == 4:
223 client_tokens = client_tokens[2:]
224
225 latest_tokens = latest_version.replace('_', '').split('.')
226 if len(latest_tokens) == 4:
227 latest_tokens = latest_tokens[2:]
228
229 for i in range(min(len(client_tokens), len(latest_tokens))):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700230 if int(latest_tokens[i]) == int(client_tokens[i]):
231 continue
232 return int(latest_tokens[i]) > int(client_tokens[i])
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700233
234 # Favor four-token new-style versions on the server over old-style versions
235 # on the client if everything else matches.
236 return len(latest_tokens) > len(client_tokens)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700237
Chris Sosa0356d3b2010-09-16 15:46:22 -0700238 def _UnpackZip(self, image_dir):
239 """Unpacks an image.zip into a given directory."""
240 image = os.path.join(image_dir, self._GetImageName())
241 if os.path.exists(image):
242 return True
243 else:
244 # -n, never clobber an existing file, in case we get invoked
245 # simultaneously by multiple request handlers. This means that
246 # we're assuming each image.zip file lives in a versioned
247 # directory (a la Buildbot).
248 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
249
250 def _GetImageName(self):
251 """Returns the name of the image that should be used."""
252 if self.use_test_image:
253 image_name = 'chromiumos_test_image.bin'
254 else:
255 image_name = 'chromiumos_image.bin'
256 return image_name
257
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700258 def _IsDeltaFormatFile(self, filename):
259 try:
260 file_handle = open(filename, 'r')
261 delta_magic = 'CrAU'
262 magic = file_handle.read(len(delta_magic))
263 return magic == delta_magic
264 except Exception:
265 return False
266
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700267 def GetUpdatePayload(self, sha1, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700268 """Returns a payload to the client corresponding to a new update.
269
270 Args:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700271 sha1: SHA1 hash of update blob
272 sha256: SHA256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700273 size: size of update blob
274 url: where to find update blob
275 Returns:
276 Xml string to be passed back to client.
277 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700278 delta = 'false'
279 if is_delta_format:
280 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000281 payload = """<?xml version="1.0" encoding="UTF-8"?>
282 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700283 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000284 <app appid="{%s}" status="ok">
285 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700286 <updatecheck
Jay Srinivasan9a1c4572012-03-16 19:16:58 -0700287 ChromeOSVersion="9999.0.0"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700288 codebase="%s"
289 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700290 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700291 needsadmin="false"
292 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700293 IsDelta="%s"
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800294 status="ok"
295 %s/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000296 </app>
297 </gupdate>
298 """
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800299 extra_attributes = []
300 if self.critical_update:
301 # The date string looks like '20111115' (2011-11-15). As of writing,
302 # there's no particular format for the deadline value that the
303 # client expects -- it's just empty vs. non-empty.
304 date_str = datetime.date.today().strftime('%Y%m%d')
305 extra_attributes.append('deadline="%s"' % date_str)
306 xml = payload % (self._GetSecondsSinceMidnight(),
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700307 self.app_id, url, sha1, sha256, size, delta,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800308 ' '.join(extra_attributes))
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700309 _Log('Generated update payload: %s' % xml)
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800310 return xml
rtc@google.comded22402009-10-26 22:36:21 +0000311
rtc@google.com21a5ca32009-11-04 18:23:23 +0000312 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700313 """Returns a payload to the client corresponding to no update."""
Darin Petkov845f1172011-01-05 14:45:24 -0800314 payload = """<?xml version="1.0" encoding="UTF-8"?>
315 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
316 <daystart elapsed_seconds="%s"/>
317 <app appid="{%s}" status="ok">
318 <ping status="ok"/>
319 <updatecheck status="noupdate"/>
320 </app>
321 </gupdate>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000322 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700323 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000324
Don Garrettf90edf02010-11-16 17:36:14 -0800325 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700326 """Generates an update gz given a full path to an image.
327
328 Args:
329 image_path: Full path to image.
330 Returns:
331 Path to created update_payload or None on error.
332 """
Don Garrettfff4c322010-11-19 13:37:12 -0800333 update_path = os.path.join(output_dir, UPDATE_FILE)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700334 _Log('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700335
Chris Sosa0f1ec842011-02-14 16:33:22 -0800336 update_command = [
Chris Sosa5b8b5eb2012-03-27 11:15:27 -0700337 'cros_generate_update_payload',
Chris Sosa0f1ec842011-02-14 16:33:22 -0800338 '--image="%s"' % image_path,
339 '--output="%s"' % update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800340 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700341
Chris Sosa0f1ec842011-02-14 16:33:22 -0800342 if src_image: update_command.append('--src_image="%s"' % src_image)
343 if not self.vm: update_command.append('--patch_kernel')
344 if self.private_key: update_command.append('--private_key="%s"' %
345 self.private_key)
346
347 update_string = ' '.join(update_command)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700348 _Log('Running ' + update_string)
Chris Sosa0f1ec842011-02-14 16:33:22 -0800349 if os.system(update_string) != 0:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700350 _Log('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700351 return None
352
Don Garrettfff4c322010-11-19 13:37:12 -0800353 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700354
Don Garrettf90edf02010-11-16 17:36:14 -0800355 def GenerateStatefulFile(self, image_path, output_dir):
356 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700357
358 Args:
359 image_path: Full path to image.
360 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800361 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800362 Raises:
363 A subprocess exception if the update generator fails to generate a
364 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700365 """
Don Garrettfff4c322010-11-19 13:37:12 -0800366 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800367 subprocess.check_call(
Chris Sosa5b8b5eb2012-03-27 11:15:27 -0700368 ['cros_generate_stateful_update_payload',
Chris Sosa908fd6f2010-11-10 17:31:18 -0800369 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800370 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800371 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800372 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700373
Don Garrettf90edf02010-11-16 17:36:14 -0800374 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
375 """Find directory to store a cached update.
376
Gilad Arnold55a2a372012-10-02 09:46:32 -0700377 Given one, or two images for an update, this finds which cache directory
378 should hold the update files, even if they don't exist yet.
Don Garrettf90edf02010-11-16 17:36:14 -0800379
Gilad Arnold55a2a372012-10-02 09:46:32 -0700380 Returns:
381 A directory path for storing a cached update, of the following form:
382 Non-delta updates:
383 CACHE_DIR/<dest_hash>
384 Delta updates:
385 CACHE_DIR/<src_hash>_<dest_hash>
386 Signed updates (self.private_key):
387 CACHE_DIR/<src_hash>_<dest_hash>+<private_key_hash>
Chris Sosa744e1472011-09-07 19:32:50 -0700388 """
Gilad Arnold55a2a372012-10-02 09:46:32 -0700389 update_dir = ''
Chris Sosa744e1472011-09-07 19:32:50 -0700390 if src_image:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700391 update_dir += common_util.GetFileMd5(src_image) + '_'
Don Garrettf90edf02010-11-16 17:36:14 -0800392
Gilad Arnold55a2a372012-10-02 09:46:32 -0700393 update_dir += common_util.GetFileMd5(dest_image)
Chris Sosa744e1472011-09-07 19:32:50 -0700394 if self.private_key:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700395 update_dir += '+' + common_util.GetFileMd5(self.private_key)
Chris Sosa744e1472011-09-07 19:32:50 -0700396
Chris Sosa9fba7562012-01-31 10:15:47 -0800397 if not self.vm:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700398 update_dir += '+patched_kernel'
Chris Sosa9fba7562012-01-31 10:15:47 -0800399
Gilad Arnold55a2a372012-10-02 09:46:32 -0700400 return os.path.join(CACHE_DIR, update_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800401
Don Garrettfff4c322010-11-19 13:37:12 -0800402 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800403 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700404
Chris Sosade91f672010-11-16 10:05:44 -0800405 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800406 src_image: image we are updating from (Null/empty for non-delta)
407 image_path: full path to the image.
408 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800409 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800410 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800411 """
Don Garrettf90edf02010-11-16 17:36:14 -0800412 update_file = None
413 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700414
Don Garrettf90edf02010-11-16 17:36:14 -0800415 # Actually do the generation
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700416 _Log('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800417 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800418 image_path,
419 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000420
Don Garrettf90edf02010-11-16 17:36:14 -0800421 if update_file:
422 stateful_update_file = self.GenerateStatefulFile(image_path,
423 output_dir)
424
425 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800426 return update_file
Chris Sosa417e55d2011-01-25 16:40:48 -0800427 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700428 _Log('Failed to generate update.')
Chris Sosa417e55d2011-01-25 16:40:48 -0800429 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800430
431 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
432 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000433
Chris Sosa0356d3b2010-09-16 15:46:22 -0700434 Args:
435 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700436 static_image_dir: the directory to move images to after generating.
437 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800438 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-25 16:40:48 -0800439 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700440 """
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700441 _Log('Generating update for src %s image %s' % (self.src_image, image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700442
Chris Sosa417e55d2011-01-25 16:40:48 -0800443 # If it was pregenerated_path, don't regenerate
444 if self.pregenerated_path:
445 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800446
Don Garrettf90edf02010-11-16 17:36:14 -0800447 # Which sub_dir of static_image_dir should hold our cached update image
448 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700449 _Log('Caching in sub_dir "%s"' % cache_sub_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800450
Chris Sosa417e55d2011-01-25 16:40:48 -0800451 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
452
Don Garrettf90edf02010-11-16 17:36:14 -0800453 # The cached payloads exist in a cache dir
454 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-25 16:40:48 -0800455 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800456 cache_stateful_payload = os.path.join(static_image_dir,
457 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800458 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800459
Chris Sosa417e55d2011-01-25 16:40:48 -0800460 # Check to see if this cache directory is valid.
461 if not os.path.exists(cache_update_payload) or not os.path.exists(
462 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800463 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800464 # Clean up stale state.
465 os.system('rm -rf "%s"' % full_cache_dir)
466 os.makedirs(full_cache_dir)
467 return_path = self.GenerateUpdateImage(image_path,
468 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800469
Chris Sosa417e55d2011-01-25 16:40:48 -0800470 # Clean up cache dir since it's not valid.
471 if not return_path:
472 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800473 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800474
475 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800476
Chris Sosa08d55a22011-01-19 16:08:02 -0800477 # Generation complete, copy if requested.
478 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800479 # The final results exist directly in static
480 update_payload = os.path.join(static_image_dir,
481 UPDATE_FILE)
482 stateful_payload = os.path.join(static_image_dir,
483 STATEFUL_FILE)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700484 common_util.CopyFile(cache_update_payload, update_payload)
485 common_util.CopyFile(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-25 16:40:48 -0800486 return UPDATE_FILE
487 else:
488 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700489
490 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800491 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700492 """Generates an update using the latest image that has been built.
493
494 This will only generate an update if the newest update is newer than that
495 on the client or client_version is 'ForcedUpdate'.
496
497 Args:
498 board_id: Name of the board.
499 client_version: Current version of the client or 'ForcedUpdate'
500 static_image_dir: the directory to move images to after generating.
501 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800502 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700503 """
504 latest_image_dir = self._GetLatestImageDir(board_id)
505 latest_version = self._GetVersionFromDir(latest_image_dir)
506 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
507
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700508 _Log('Preparing to generate update from latest built image %s.' %
509 latest_image_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700510
511 # Check to see whether or not we should update.
512 if client_version != 'ForcedUpdate' and not self._CanUpdate(
513 client_version, latest_version):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700514 _Log('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800515 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700516
Don Garrettf90edf02010-11-16 17:36:14 -0800517 return self.GenerateUpdateImageWithCache(latest_image_path,
518 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700519
Andrew de los Reyes52620802010-04-12 13:40:07 -0700520 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
521 """Imports a factory-floor server configuration file. The file should
522 be in this format:
523 config = [
524 {
525 'qual_ids': set([1, 2, 3, "x86-generic"]),
526 'factory_image': 'generic-factory.gz',
527 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
528 'release_image': 'generic-release.gz',
529 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
530 'oempartitionimg_image': 'generic-oem.gz',
531 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700532 'efipartitionimg_image': 'generic-efi.gz',
533 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700534 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800535 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800536 'firmware_image': 'generic-firmware.gz',
537 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700538 },
539 {
540 'qual_ids': set([6]),
541 'factory_image': '6-factory.gz',
542 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
543 'release_image': '6-release.gz',
544 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
545 'oempartitionimg_image': '6-oem.gz',
546 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700547 'efipartitionimg_image': '6-efi.gz',
548 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700549 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800550 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800551 'firmware_image': '6-firmware.gz',
552 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700553 },
554 ]
555 The server will look for the files by name in the static files
556 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700557
Andrew de los Reyes52620802010-04-12 13:40:07 -0700558 If validate_checksums is True, validates checksums and exits. If
559 a checksum mismatch is found, it's printed to the screen.
560 """
561 f = open(filename, 'r')
562 output = {}
563 exec(f.read(), output)
564 self.factory_config = output['config']
565 success = True
566 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800567 for key in stanza.copy().iterkeys():
568 suffix = '_image'
569 if key.endswith(suffix):
570 kind = key[:-len(suffix)]
Gilad Arnold55a2a372012-10-02 09:46:32 -0700571 stanza[kind + '_size'] = common_util.GetFileSize(os.path.join(
Chris Sosa0356d3b2010-09-16 15:46:22 -0700572 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800573 if validate_checksums:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700574 factory_checksum = common_util.GetFileSha1(
575 os.path.join(self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800576 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700577 print ('Error: checksum mismatch for %s. Expected "%s" but file '
578 'has checksum "%s".' % (stanza[kind + '_image'],
579 stanza[kind + '_checksum'],
580 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800581 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700582
Andrew de los Reyes52620802010-04-12 13:40:07 -0700583 if validate_checksums:
584 if success is False:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700585 raise AutoupdateError('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700586
Andrew de los Reyes52620802010-04-12 13:40:07 -0700587 print 'Config file looks good.'
588
589 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700590 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700591 for stanza in self.factory_config:
592 if board_id not in stanza['qual_ids']:
593 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700594 if kind + '_image' not in stanza:
595 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700596 return (stanza[kind + '_image'],
597 stanza[kind + '_checksum'],
598 stanza[kind + '_size'])
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700599 return None, None, None
rtc@google.comded22402009-10-26 22:36:21 +0000600
Chris Sosa7c931362010-10-11 19:49:01 -0700601 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700602 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
603 if filename is None:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700604 _Log('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700605 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700606 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700607 is_delta_format = self._IsDeltaFormatFile(filename)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700608 _Log('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700609 # Factory install is using memento updater which is using the sha-1 hash so
610 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700611 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700612
Chris Sosa151643e2010-10-28 14:40:57 -0700613 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
614 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800615 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800616
Don Garrettf90edf02010-11-16 17:36:14 -0800617 Returns:
618 file name relative to static_image_dir on success.
619 """
Dale Curtis723ec472010-11-30 14:06:47 -0800620 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
621 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
622
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700623 if self.payload_path:
Don Garrett0c880e22010-11-17 18:13:37 -0800624 # If the forced payload is not already in our static_image_dir,
625 # copy it there.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700626 src_path = os.path.abspath(self.payload_path)
Don Garrettee25e552010-11-23 12:09:35 -0800627 src_stateful = os.path.join(os.path.dirname(src_path),
628 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800629
630 # Only copy the files if the source directory is different from dest.
631 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700632 common_util.CopyFile(src_path, dest_path)
Don Garrettee25e552010-11-23 12:09:35 -0800633
634 # The stateful payload is optional.
635 if os.path.exists(src_stateful):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700636 common_util.CopyFile(src_stateful, dest_stateful)
Don Garrettee25e552010-11-23 12:09:35 -0800637 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700638 _Log('WARN: %s not found. Expected for dev and test builds.' %
639 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800640 if os.path.exists(dest_stateful):
641 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800642
Don Garrettfff4c322010-11-19 13:37:12 -0800643 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800644 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800645 return self.GenerateUpdateImageWithCache(
646 self.forced_image,
647 static_image_dir=static_image_dir)
648 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800649 # Warn if update or stateful files can't be found.
650 if not os.path.exists(dest_path):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700651 _Log('WARN: %s not found. Expected for dev and test builds.' %
652 UPDATE_FILE)
Dale Curtis723ec472010-11-30 14:06:47 -0800653
654 if not os.path.exists(dest_stateful):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700655 _Log('WARN: %s not found. Expected for dev and test builds.' %
656 STATEFUL_FILE)
Dale Curtis723ec472010-11-30 14:06:47 -0800657
658 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800659 else:
660 if board_id:
661 return self.GenerateLatestUpdateImage(board_id,
662 client_version,
663 static_image_dir)
664
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700665 _Log('Failed to genereate update. '
666 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800667 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700668
669 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800670 """Pre-generates an update and prints out the relative path it.
671
672 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800673 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700674 # Does not work with factory config.
675 assert(not self.factory_config)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700676 _Log('Pre-generating the update payload.')
Chris Sosa2c048f12010-10-27 16:05:27 -0700677 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-25 16:40:48 -0800678 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
679 self.board, '0.0.0.0', self.static_dir)
680 if pregenerated_update:
681 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
682
683 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700684
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700685 def _GetRemotePayloadAttrs(self, url):
686 """Returns hashes, size and delta flag of a remote update payload.
687
688 Obtain attributes of a payload file available on a remote devserver. This
689 is based on the assumption that the payload URL uses the /static prefix. We
690 need to make sure that both clients (requests) and remote devserver
691 (provisioning) preserve this invariant.
692
693 Args:
694 url: URL of statically staged remote file (http://host:port/static/...)
695 Returns:
696 A tuple containing the SHA1, SHA256, file size and whether or not it's a
697 delta payload (Boolean).
698 """
699 if self._PAYLOAD_URL_PREFIX not in url:
700 raise AutoupdateError(
701 'Payload URL does not have the expected prefix (%s)' %
702 self._PAYLOAD_URL_PREFIX)
703 fileinfo_url = url.replace(self._PAYLOAD_URL_PREFIX,
704 self._FILEINFO_URL_PREFIX)
705 _Log('retrieving file info for remote payload via %s' % fileinfo_url)
706 try:
707 conn = urllib2.urlopen(fileinfo_url)
708 file_attr_dict = json.loads(conn.read())
709 sha1 = file_attr_dict['sha1']
710 sha256 = file_attr_dict['sha256']
711 size = file_attr_dict['size']
712 except Exception, e:
713 _Log('failed to obtain remote payload info: %s' % str(e))
714 raise
715 is_delta_format = ('_mton' in url) or ('_nton' in url)
716
717 return sha1, sha256, size, is_delta_format
718
719 def _GetLocalPayloadAttrs(self, static_image_dir, payload_path):
720 """Returns hashes, size and delta flag of a local update payload.
721
722 Args:
723 static_image_dir: directory where static files are being staged
724 payload_path: path to the payload file inside the static directory
725 Returns:
726 A tuple containing the SHA1, SHA256, file size and whether or not it's a
727 delta payload (Boolean).
728 """
729 filename = os.path.join(static_image_dir, payload_path)
730 sha1 = common_util.GetFileSha1(filename)
731 sha256 = common_util.GetFileSha256(filename)
732 size = common_util.GetFileSize(filename)
733 is_delta_format = self._IsDeltaFormatFile(filename)
734 return sha1, sha256, size, is_delta_format
735
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700736 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700737 """Handles an update ping from an update client.
738
739 Args:
740 data: xml blob from client.
741 label: optional label for the update.
742 Returns:
743 Update payload message for client.
744 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700745 # Set hostname as the hostname that the client is calling to and set up
Chris Sosa28be7db2012-06-13 16:26:10 -0700746 # the url base. If behind apache mod_proxy | mod_rewrite, the hostname will
747 # be in X-Forwarded-Host.
748 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
749 if x_forwarded_host:
750 self.hostname = 'http://' + x_forwarded_host
751 else:
752 self.hostname = cherrypy.request.base
753
Chris Sosa9841e1c2010-10-14 10:51:45 -0700754 if self.urlbase:
755 static_urlbase = self.urlbase
756 elif self.serve_only:
757 static_urlbase = '%s/static/archive' % self.hostname
758 else:
759 static_urlbase = '%s/static' % self.hostname
760
Don Garrett0ad09372010-12-06 16:20:30 -0800761 # If we have a proxy port, adjust the URL we instruct the client to
762 # use to go through the proxy.
763 if self.proxy_port:
764 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
765
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700766 _Log('Using static url base %s' % static_urlbase)
767 _Log('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700768
Chris Sosa9841e1c2010-10-14 10:51:45 -0700769 update_dom = minidom.parseString(data)
770 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700771
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700772 # Determine request IP, strip any IPv6 data for simplicity.
773 client_ip = cherrypy.request.remote.ip.split(':')[-1]
774
Gilad Arnold286a0062012-01-12 13:47:02 -0800775 # Obtain (or init) info object for this client.
776 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
777
778 # Initialize an empty dictionary for event attributes.
779 log_message = {}
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700780
781 # Store event details in the host info dictionary for API usage.
782 event = root.getElementsByTagName('o:event')
783 if event:
Gilad Arnold286a0062012-01-12 13:47:02 -0800784 event_result = int(event[0].getAttribute('eventresult'))
785 event_type = int(event[0].getAttribute('eventtype'))
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700786 client_previous_version = (event[0].getAttribute('previousversion')
787 if event[0].hasAttribute('previousversion')
788 else None)
Gilad Arnold286a0062012-01-12 13:47:02 -0800789 # Store attributes to legacy host info structure
790 curr_host_info.attrs['last_event_status'] = event_result
791 curr_host_info.attrs['last_event_type'] = event_type
792 # Add attributes to log message
793 log_message['event_result'] = event_result
794 log_message['event_type'] = event_type
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700795 if client_previous_version is not None:
796 log_message['previous_version'] = client_previous_version
Gilad Arnold286a0062012-01-12 13:47:02 -0800797
798 # Get information about the requester.
799 query = root.getElementsByTagName('o:app')[0]
800 if query:
801 client_version = query.getAttribute('version')
802 channel = query.getAttribute('track')
803 board_id = (query.hasAttribute('board') and query.getAttribute('board')
804 or self._GetDefaultBoardID())
805 # Add attributes to log message
806 log_message['version'] = client_version
807 log_message['track'] = channel
808 log_message['board'] = board_id
809
810 # Log client's message
811 curr_host_info.AddLogEntry(log_message)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700812
Chris Sosa0356d3b2010-09-16 15:46:22 -0700813 # We only generate update payloads for updatecheck requests.
814 update_check = root.getElementsByTagName('o:updatecheck')
815 if not update_check:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700816 _Log('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700817 # TODO(sosa): Generate correct non-updatecheck payload to better test
818 # update clients.
819 return self.GetNoUpdatePayload()
820
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700821 # Store version for this host in the cache.
Gilad Arnold286a0062012-01-12 13:47:02 -0800822 curr_host_info.attrs['last_known_version'] = client_version
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700823
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700824 # If maximum number of updates already requested, refuse.
825 if self.max_updates > 0:
826 self.max_updates -= 1
827 elif self.max_updates == 0:
828 return self.GetNoUpdatePayload()
829
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700830 # Check if an update has been forced for this client.
Gilad Arnold286a0062012-01-12 13:47:02 -0800831 forced_update = curr_host_info.PopAttr('forced_update_label', None)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700832 if forced_update:
833 label = forced_update
834
Chris Sosa0356d3b2010-09-16 15:46:22 -0700835 # Separate logic as Factory requests have static url's that override
836 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700837 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700838 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700839 else:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700840 url = ''
841 # Are we provisioning a remote or local payload?
842 if self.remote_payload:
843 # If no explicit label was provided, use the value of --payload.
844 if not label and self.payload_path:
845 label = self.payload_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700846
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700847 # Form the URL of the update payload. This assumes that the payload
848 # file name is a devserver constant (which currently is the case).
849 url = '/'.join(filter(None, [static_urlbase, label, UPDATE_FILE]))
Chris Sosa5d342a22010-09-28 16:54:41 -0700850
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700851 # Get remote payload attributes.
852 sha1, sha256, file_size, is_delta_format = \
853 self._GetRemotePayloadAttrs(url)
854 else:
855 # Generate payload.
856 static_image_dir = os.path.join(*filter(None, [self.static_dir, label]))
857 payload_path = self.GenerateUpdatePayloadForNonFactory(
858 board_id, client_version, static_image_dir)
859 # If properly generated, obtain the payload URL and attributes.
860 if payload_path:
861 url = '/'.join(filter(None, [static_urlbase, label, payload_path]))
862 sha1, sha256, file_size, is_delta_format = \
863 self._GetLocalPayloadAttrs(static_image_dir, payload_path)
864
865 # If we end up with an actual payload path, generate a response.
866 if url:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700867 _Log('Responding to client to use url %s to get image.' % url)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700868 return self.GetUpdatePayload(
869 sha1, sha256, file_size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700870 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700871 return self.GetNoUpdatePayload()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700872
873 def HandleHostInfoPing(self, ip):
874 """Returns host info dictionary for the given IP in JSON format."""
875 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800876 if ip in self.host_infos.table:
877 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
878
879 def HandleHostLogPing(self, ip):
880 """Returns a complete log of events for host in JSON format."""
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700881 # If all events requested, return a dictionary of logs keyed by IP address.
Gilad Arnold286a0062012-01-12 13:47:02 -0800882 if ip == 'all':
883 return json.dumps(
884 dict([(key, self.host_infos.table[key].log)
885 for key in self.host_infos.table]))
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700886
887 # Otherwise we're looking for a specific IP address, so find its log.
Gilad Arnold286a0062012-01-12 13:47:02 -0800888 if ip in self.host_infos.table:
889 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700890
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700891 # If no events were logged for this IP, return an empty log.
892 return json.dumps([])
893
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700894 def HandleSetUpdatePing(self, ip, label):
895 """Sets forced_update_label for a given host."""
896 assert ip, 'No ip provided.'
897 assert label, 'No label provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800898 self.host_infos.GetInitHostInfo(ip).attrs['forced_update_label'] = label