blob: b1d5ae779bb70ada4c159d1f7ba400caa55b2063 [file] [log] [blame]
Chris Sosa0356d3b2010-09-16 15:46:22 -07001# Copyright (c) 2009-2010 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
rtc@google.com64244662009-11-12 00:52:08 +00005from buildutil import BuildObject
rtc@google.comded22402009-10-26 22:36:21 +00006from xml.dom import minidom
7
Chris Sosa7c931362010-10-11 19:49:01 -07008import cherrypy
rtc@google.comded22402009-10-26 22:36:21 +00009import os
Darin Petkov798fe7d2010-03-22 15:18:13 -070010import shutil
Chris Sosa7c931362010-10-11 19:49:01 -070011import socket
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070012import time
Chris Sosa7c931362010-10-11 19:49:01 -070013
14def _LogMessage(message):
15 cherrypy.log(message, 'UPDATE')
rtc@google.comded22402009-10-26 22:36:21 +000016
Chris Sosa0356d3b2010-09-16 15:46:22 -070017
rtc@google.com64244662009-11-12 00:52:08 +000018class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -070019 """Class that contains functionality that handles Chrome OS update pings.
20
21 Members:
22 serve_only: Serve images from a pre-built image.zip file. static_dir
23 must be set to the location of the image.zip.
24 factory_config: Path to the factory config file if handling factory
25 requests.
26 use_test_image: Use chromiumos_test_image.bin rather than the standard.
27 static_url_base: base URL, other than devserver, for update images.
28 client_prefix: The prefix for the update engine client.
29 forced_image: Path to an image to use for all updates.
30 """
rtc@google.comded22402009-10-26 22:36:21 +000031
Sean O'Connor1f7fd362010-04-07 16:34:52 -070032 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Chris Sosa0356d3b2010-09-16 15:46:22 -070033 factory_config_path=None, client_prefix=None, forced_image=None,
Chris Sosae67b78f2010-11-04 17:33:16 -070034 use_cached=False, port=8080, src_image='', vm=False, board=None,
35 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070036 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070037 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -070038 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -070039 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -070040 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -070041 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -070042 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -070043 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -070044
Chris Sosab63a9282010-09-02 10:43:23 -070045 self.client_prefix = client_prefix
Chris Sosa0356d3b2010-09-16 15:46:22 -070046 self.forced_image = forced_image
Chris Sosa5d342a22010-09-28 16:54:41 -070047 self.use_cached = use_cached
Chris Sosa62f720b2010-10-26 21:39:48 -070048 self.src_image = src_image
Chris Sosa4136e692010-10-28 23:42:37 -070049 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -070050 self.board = board
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070051
Chris Sosa0356d3b2010-09-16 15:46:22 -070052 def _GetSecondsSinceMidnight(self):
53 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070054 now = time.localtime()
55 return now[3] * 3600 + now[4] * 60 + now[5]
56
Chris Sosa0356d3b2010-09-16 15:46:22 -070057 def _GetDefaultBoardID(self):
58 """Returns the default board id stored in .default_board."""
59 board_file = '%s/.default_board' % (self.scripts_dir)
60 try:
61 return open(board_file).read()
62 except IOError:
63 return 'x86-generic'
64
65 def _GetLatestImageDir(self, board_id):
66 """Returns the latest image dir based on shell script."""
67 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
68 return os.popen(cmd).read().strip()
69
70 def _GetVersionFromDir(self, image_dir):
71 """Returns the version of the image based on the name of the directory."""
72 latest_version = os.path.basename(image_dir)
73 return latest_version.split('-')[0]
74
75 def _CanUpdate(self, client_version, latest_version):
76 """Returns true if the latest_version is greater than the client_version."""
77 client_tokens = client_version.replace('_', '').split('.')
78 latest_tokens = latest_version.replace('_', '').split('.')
Chris Sosa7c931362010-10-11 19:49:01 -070079 _LogMessage('client version %s latest version %s'
Chris Sosa0356d3b2010-09-16 15:46:22 -070080 % (client_version, latest_version))
81 for i in range(4):
82 if int(latest_tokens[i]) == int(client_tokens[i]):
83 continue
84 return int(latest_tokens[i]) > int(client_tokens[i])
85 return False
86
87 def _UnpackStatefulPartition(self, image_path, stateful_file):
88 """Given an image, unpacks its stateful partition to stateful_file."""
89 image_dir = os.path.dirname(image_path)
90 image_file = os.path.basename(image_path)
91
92 get_offset = '$(cgpt show -b -i 1 %s)' % image_file
93 get_size = '$(cgpt show -s -i 1 %s)' % image_file
94 unpack_command = (
95 'cd %s && '
96 'dd if=%s of=%s bs=512 skip=%s count=%s' % (image_dir, image_file,
97 stateful_file, get_offset,
98 get_size))
Chris Sosa7c931362010-10-11 19:49:01 -070099 _LogMessage(unpack_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700100 return os.system(unpack_command) == 0
101
102 def _UnpackZip(self, image_dir):
103 """Unpacks an image.zip into a given directory."""
104 image = os.path.join(image_dir, self._GetImageName())
105 if os.path.exists(image):
106 return True
107 else:
108 # -n, never clobber an existing file, in case we get invoked
109 # simultaneously by multiple request handlers. This means that
110 # we're assuming each image.zip file lives in a versioned
111 # directory (a la Buildbot).
112 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
113
114 def _GetImageName(self):
115 """Returns the name of the image that should be used."""
116 if self.use_test_image:
117 image_name = 'chromiumos_test_image.bin'
118 else:
119 image_name = 'chromiumos_image.bin'
120 return image_name
121
122 def _IsImageNewerThanCached(self, image_path, cached_file_path):
123 """Returns true if the image is newer than the cached image."""
124 if os.path.exists(cached_file_path) and os.path.exists(image_path):
Chris Sosa7c931362010-10-11 19:49:01 -0700125 _LogMessage('Usable cached image found at %s.' % cached_file_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700126 return os.path.getmtime(image_path) > os.path.getmtime(cached_file_path)
127 elif not os.path.exists(cached_file_path) and not os.path.exists(image_path):
128 raise Exception('Image does not exist and cached image missing')
129 else:
130 # Only one is missing, figure out which one.
131 if os.path.exists(image_path):
Chris Sosa7c931362010-10-11 19:49:01 -0700132 _LogMessage('No cached image found - image generation required.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700133 return True
134 else:
Chris Sosa7c931362010-10-11 19:49:01 -0700135 _LogMessage('Cached image found to serve at %s.' % cached_file_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700136 return False
137
138 def _GetSize(self, update_path):
139 """Returns the size of the file given."""
140 return os.path.getsize(update_path)
141
142 def _GetHash(self, update_path):
143 """Returns the sha1 of the file given."""
144 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
145 % update_path)
146 return os.popen(cmd).read().rstrip()
147
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700148 def _IsDeltaFormatFile(self, filename):
149 try:
150 file_handle = open(filename, 'r')
151 delta_magic = 'CrAU'
152 magic = file_handle.read(len(delta_magic))
153 return magic == delta_magic
154 except Exception:
155 return False
156
Darin Petkov91436cb2010-09-28 08:52:17 -0700157 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
158 # it takes advantage of reduced I/O and multiple processors. Something like:
159 # % tee < FILE > /dev/null \
160 # >( openssl dgst -sha256 -binary | openssl base64 ) \
161 # >( openssl sha1 -binary | openssl base64 )
162 def _GetSHA256(self, update_path):
163 """Returns the sha256 of the file given."""
164 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
165 update_path)
166 return os.popen(cmd).read().rstrip()
167
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700168 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700169 """Returns a payload to the client corresponding to a new update.
170
171 Args:
172 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700173 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700174 size: size of update blob
175 url: where to find update blob
176 Returns:
177 Xml string to be passed back to client.
178 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700179 delta = 'false'
180 if is_delta_format:
181 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000182 payload = """<?xml version="1.0" encoding="UTF-8"?>
183 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700184 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000185 <app appid="{%s}" status="ok">
186 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700187 <updatecheck
188 codebase="%s"
189 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700190 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700191 needsadmin="false"
192 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700193 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000194 status="ok"/>
195 </app>
196 </gupdate>
197 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700198 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700199 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000200
rtc@google.com21a5ca32009-11-04 18:23:23 +0000201 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700202 """Returns a payload to the client corresponding to no update."""
203 payload = """ < ?xml version = "1.0" encoding = "UTF-8"? >
204 < gupdate xmlns = "http://www.google.com/update2/response" protocol = "2.0" >
205 < daystart elapsed_seconds = "%s" />
206 < app appid = "{%s}" status = "ok" >
207 < ping status = "ok" />
208 < updatecheck status = "noupdate" />
209 </ app >
210 </ gupdate >
rtc@google.com21a5ca32009-11-04 18:23:23 +0000211 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700212 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000213
Chris Sosa0356d3b2010-09-16 15:46:22 -0700214 def GenerateUpdateFile(self, image_path):
215 """Generates an update gz given a full path to an image.
216
217 Args:
218 image_path: Full path to image.
219 Returns:
220 Path to created update_payload or None on error.
221 """
222 image_dir = os.path.dirname(image_path)
223 update_path = os.path.join(image_dir, 'update.gz')
Chris Sosa4136e692010-10-28 23:42:37 -0700224 patch_kernel_flag = '--patch_kernel'
Chris Sosa7c931362010-10-11 19:49:01 -0700225 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700226
Chris Sosa4136e692010-10-28 23:42:37 -0700227 # Don't patch the kernel for vm images as they don't need the patch.
228 if self.vm:
229 patch_kernel_flag = ''
230
Chris Sosa0356d3b2010-09-16 15:46:22 -0700231 mkupdate_command = (
Chris Sosa62f720b2010-10-26 21:39:48 -0700232 '%s/cros_generate_update_payload --image="%s" --output="%s" '
Chris Sosa4136e692010-10-28 23:42:37 -0700233 '%s --noold_style --src_image="%s"' % (
234 self.scripts_dir, image_path, update_path, patch_kernel_flag,
235 self.src_image))
Chris Sosa62f720b2010-10-26 21:39:48 -0700236 _LogMessage(mkupdate_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700237 if os.system(mkupdate_command) != 0:
Chris Sosa7c931362010-10-11 19:49:01 -0700238 _LogMessage('Failed to create base update file')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700239 return None
240
241 return update_path
242
243 def GenerateStatefulFile(self, image_path):
244 """Generates a stateful update gz given a full path to an image.
245
246 Args:
247 image_path: Full path to image.
248 Returns:
249 Path to created stateful update_payload or None on error.
250 """
251 stateful_partition_path = '%s/stateful.image' % os.path.dirname(image_path)
252
253 # Unpack to get stateful partition.
254 if self._UnpackStatefulPartition(image_path, stateful_partition_path):
255 mkstatefulupdate_command = 'gzip -f %s' % stateful_partition_path
256 if os.system(mkstatefulupdate_command) == 0:
Chris Sosa7c931362010-10-11 19:49:01 -0700257 _LogMessage('Successfully generated %s.gz' % stateful_partition_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700258 return '%s.gz' % stateful_partition_path
259
Chris Sosa7c931362010-10-11 19:49:01 -0700260 _LogMessage('Failed to create stateful update file')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700261 return None
262
263 def MoveImagesToStaticDir(self, update_path, stateful_update_path,
264 static_image_dir):
265 """Moves gz files from their directories to serving directories.
266
267 Args:
268 update_path: full path to main update gz.
269 stateful_update_path: full path to stateful partition gz.
270 static_image_dir: where to put files.
271 Returns:
272 Returns True if the files were moved over successfully.
273 """
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700274 try:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700275 shutil.copy(update_path, static_image_dir)
276 shutil.copy(stateful_update_path, static_image_dir)
277 os.remove(update_path)
278 os.remove(stateful_update_path)
279 except Exception:
Chris Sosa7c931362010-10-11 19:49:01 -0700280 _LogMessage('Failed to move %s and %s to %s' % (update_path,
Chris Sosa0356d3b2010-09-16 15:46:22 -0700281 stateful_update_path,
282 static_image_dir))
283 return False
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700284
rtc@google.com21a5ca32009-11-04 18:23:23 +0000285 return True
rtc@google.comded22402009-10-26 22:36:21 +0000286
Chris Sosa0356d3b2010-09-16 15:46:22 -0700287 def GenerateUpdateImage(self, image_path, move_to_static_dir=False,
288 static_image_dir=None):
289 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000290
Chris Sosa0356d3b2010-09-16 15:46:22 -0700291 Args:
292 image_path: full path to the image.
293 move_to_static_dir: Moves the files from their dir to the static dir.
294 static_image_dir: the directory to move images to after generating.
295 Returns:
296 True if the update payload was created successfully.
297 """
Chris Sosa7c931362010-10-11 19:49:01 -0700298 _LogMessage('Generating update for image %s' % image_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700299 update_path = self.GenerateUpdateFile(image_path)
Chris Sosae67b78f2010-11-04 17:33:16 -0700300 if update_path:
301 stateful_update_path = self.GenerateStatefulFile(image_path)
302 if stateful_update_path:
303 if move_to_static_dir:
304 return self.MoveImagesToStaticDir(update_path, stateful_update_path,
305 static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700306
Chris Sosae67b78f2010-11-04 17:33:16 -0700307 return True
308
309 _LogMessage('Failed to generate update')
310 return False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700311
312 def GenerateLatestUpdateImage(self, board_id, client_version,
313 static_image_dir=None):
314 """Generates an update using the latest image that has been built.
315
316 This will only generate an update if the newest update is newer than that
317 on the client or client_version is 'ForcedUpdate'.
318
319 Args:
320 board_id: Name of the board.
321 client_version: Current version of the client or 'ForcedUpdate'
322 static_image_dir: the directory to move images to after generating.
323 Returns:
324 True if the update payload was created successfully.
325 """
326 latest_image_dir = self._GetLatestImageDir(board_id)
327 latest_version = self._GetVersionFromDir(latest_image_dir)
328 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
329
Chris Sosa7c931362010-10-11 19:49:01 -0700330 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700331 latest_image_path)
332
333 # Check to see whether or not we should update.
334 if client_version != 'ForcedUpdate' and not self._CanUpdate(
335 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700336 _LogMessage('no update')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700337 return False
338
339 cached_file_path = os.path.join(static_image_dir, 'update.gz')
340 if (os.path.exists(cached_file_path) and
341 not self._IsImageNewerThanCached(latest_image_path, cached_file_path)):
342 return True
343
344 return self.GenerateUpdateImage(latest_image_path, move_to_static_dir=True,
345 static_image_dir=static_image_dir)
346
347 def GenerateImageFromZip(self, static_image_dir):
348 """Generates an update from an image zip file.
349
350 This method assumes you have an image.zip in directory you are serving
351 from. If this file is newer than a previously cached file, it will unzip
352 this file, create a payload and serve it.
353
354 Args:
355 static_image_dir: Directory where the zip file exists.
356 Returns:
357 True if the update payload was created successfully.
358 """
Chris Sosa7c931362010-10-11 19:49:01 -0700359 _LogMessage('Preparing to generate update from zip in %s.' % static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700360 image_path = os.path.join(static_image_dir, self._GetImageName())
361 cached_file_path = os.path.join(static_image_dir, 'update.gz')
362 zip_file_path = os.path.join(static_image_dir, 'image.zip')
363 if not self._IsImageNewerThanCached(zip_file_path, cached_file_path):
364 return True
365
366 if not self._UnpackZip(static_image_dir):
Chris Sosa7c931362010-10-11 19:49:01 -0700367 _LogMessage('unzip image.zip failed.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700368 return False
369
370 return self.GenerateUpdateImage(image_path, move_to_static_dir=False,
371 static_image_dir=None)
Darin Petkov8ef83452010-03-23 16:52:29 -0700372
Andrew de los Reyes52620802010-04-12 13:40:07 -0700373 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
374 """Imports a factory-floor server configuration file. The file should
375 be in this format:
376 config = [
377 {
378 'qual_ids': set([1, 2, 3, "x86-generic"]),
379 'factory_image': 'generic-factory.gz',
380 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
381 'release_image': 'generic-release.gz',
382 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
383 'oempartitionimg_image': 'generic-oem.gz',
384 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700385 'efipartitionimg_image': 'generic-efi.gz',
386 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700387 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800388 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800389 'firmware_image': 'generic-firmware.gz',
390 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700391 },
392 {
393 'qual_ids': set([6]),
394 'factory_image': '6-factory.gz',
395 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
396 'release_image': '6-release.gz',
397 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
398 'oempartitionimg_image': '6-oem.gz',
399 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700400 'efipartitionimg_image': '6-efi.gz',
401 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700402 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800403 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800404 'firmware_image': '6-firmware.gz',
405 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700406 },
407 ]
408 The server will look for the files by name in the static files
409 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700410
Andrew de los Reyes52620802010-04-12 13:40:07 -0700411 If validate_checksums is True, validates checksums and exits. If
412 a checksum mismatch is found, it's printed to the screen.
413 """
414 f = open(filename, 'r')
415 output = {}
416 exec(f.read(), output)
417 self.factory_config = output['config']
418 success = True
419 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800420 for key in stanza.copy().iterkeys():
421 suffix = '_image'
422 if key.endswith(suffix):
423 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700424 stanza[kind + '_size'] = self._GetSize(os.path.join(
425 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800426 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700427 factory_checksum = self._GetHash(os.path.join(self.static_dir,
428 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800429 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700430 print ('Error: checksum mismatch for %s. Expected "%s" but file '
431 'has checksum "%s".' % (stanza[kind + '_image'],
432 stanza[kind + '_checksum'],
433 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800434 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700435
Andrew de los Reyes52620802010-04-12 13:40:07 -0700436 if validate_checksums:
437 if success is False:
438 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700439
Andrew de los Reyes52620802010-04-12 13:40:07 -0700440 print 'Config file looks good.'
441
442 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700443 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700444 for stanza in self.factory_config:
445 if board_id not in stanza['qual_ids']:
446 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700447 if kind + '_image' not in stanza:
448 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700449 return (stanza[kind + '_image'],
450 stanza[kind + '_checksum'],
451 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700452 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000453
Chris Sosa7c931362010-10-11 19:49:01 -0700454 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700455 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
456 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700457 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700458 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700459 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700460 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700461 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700462 # Factory install is using memento updater which is using the sha-1 hash so
463 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700464 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700465
Chris Sosa151643e2010-10-28 14:40:57 -0700466 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
467 static_image_dir):
Chris Sosa2c048f12010-10-27 16:05:27 -0700468 """Generates an update for non-factory and returns True on success."""
469 if self.use_cached and os.path.exists(os.path.join(static_image_dir,
470 'update.gz')):
471 _LogMessage('Using cached image regardless of timestamps.')
472 return True
473 else:
474 if self.forced_image:
475 has_built_image = self.GenerateUpdateImage(
476 self.forced_image, move_to_static_dir=True,
477 static_image_dir=static_image_dir)
Chris Sosae67b78f2010-11-04 17:33:16 -0700478 return has_built_image
Chris Sosa2c048f12010-10-27 16:05:27 -0700479 elif self.serve_only:
480 return self.GenerateImageFromZip(static_image_dir)
Chris Sosa151643e2010-10-28 14:40:57 -0700481 else:
Chris Sosae67b78f2010-11-04 17:33:16 -0700482 if board_id:
483 return self.GenerateLatestUpdateImage(board_id,
484 client_version,
485 static_image_dir)
486
487 _LogMessage('You must set --board for pre-generating latest update.')
Chris Sosa151643e2010-10-28 14:40:57 -0700488 return False
Chris Sosa2c048f12010-10-27 16:05:27 -0700489
490 def PreGenerateUpdate(self):
Chris Sosae67b78f2010-11-04 17:33:16 -0700491 """Pre-generates an update. Returns True on success."""
Chris Sosa2c048f12010-10-27 16:05:27 -0700492 # Does not work with factory config.
493 assert(not self.factory_config)
494 _LogMessage('Pre-generating the update payload.')
495 # Does not work with labels so just use static dir.
Chris Sosae67b78f2010-11-04 17:33:16 -0700496 if self.GenerateUpdatePayloadForNonFactory(self.board, '0.0.0.0',
497 self.static_dir):
Chris Sosa2c048f12010-10-27 16:05:27 -0700498 # Force the devserver to use the pre-generated payload.
499 self.use_cached = True
Chris Sosae67b78f2010-11-04 17:33:16 -0700500 _LogMessage('Pre-generated update successfully.')
501 return True
Chris Sosa2c048f12010-10-27 16:05:27 -0700502 else:
503 _LogMessage('Failed to pre-generate update.')
Chris Sosae67b78f2010-11-04 17:33:16 -0700504 return False
Chris Sosa2c048f12010-10-27 16:05:27 -0700505
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700506 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700507 """Handles an update ping from an update client.
508
509 Args:
510 data: xml blob from client.
511 label: optional label for the update.
512 Returns:
513 Update payload message for client.
514 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700515 # Set hostname as the hostname that the client is calling to and set up
516 # the url base.
517 self.hostname = cherrypy.request.base
518 if self.urlbase:
519 static_urlbase = self.urlbase
520 elif self.serve_only:
521 static_urlbase = '%s/static/archive' % self.hostname
522 else:
523 static_urlbase = '%s/static' % self.hostname
524
525 _LogMessage('Using static url base %s' % static_urlbase)
526 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700527
528 # Check the client prefix to make sure you can support this type of update.
Chris Sosa9841e1c2010-10-14 10:51:45 -0700529 update_dom = minidom.parseString(data)
530 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700531 if (root.hasAttribute('updaterversion') and
532 not root.getAttribute('updaterversion').startswith(self.client_prefix)):
Chris Sosa7c931362010-10-11 19:49:01 -0700533 _LogMessage('Got update from unsupported updater:' +
Chris Sosa0356d3b2010-09-16 15:46:22 -0700534 root.getAttribute('updaterversion'))
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700535 return self.GetNoUpdatePayload()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700536
537 # We only generate update payloads for updatecheck requests.
538 update_check = root.getElementsByTagName('o:updatecheck')
539 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700540 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700541 # TODO(sosa): Generate correct non-updatecheck payload to better test
542 # update clients.
543 return self.GetNoUpdatePayload()
544
545 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700546 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800547 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700548 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700549 board_id = (query.hasAttribute('board') and query.getAttribute('board')
550 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700551
Chris Sosa0356d3b2010-09-16 15:46:22 -0700552 # Separate logic as Factory requests have static url's that override
553 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700554 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700555 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700556 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700557 static_image_dir = self.static_dir
558 if label:
559 static_image_dir = os.path.join(static_image_dir, label)
560
Chris Sosa151643e2010-10-28 14:40:57 -0700561 if self.GenerateUpdatePayloadForNonFactory(board_id, client_version,
562 static_image_dir):
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700563 filename = os.path.join(static_image_dir, 'update.gz')
564 hash = self._GetHash(filename)
565 sha256 = self._GetSHA256(filename)
566 size = self._GetSize(filename)
567 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700568 if label:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700569 url = '%s/%s/update.gz' % (static_urlbase, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700570 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700571 url = '%s/update.gz' % static_urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -0700572
Chris Sosa7c931362010-10-11 19:49:01 -0700573 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700574 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700575 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700576 return self.GetNoUpdatePayload()