blob: d9ef0bcc7b4eb4fe7e1bc9f1a05de41de2bfca24 [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
Don Garrettf90edf02010-11-16 17:36:14 -08007
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 Sosa05491b12010-11-08 17:14:16 -080011import subprocess
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070012import time
Chris Sosa7c931362010-10-11 19:49:01 -070013
Chris Sosa05491b12010-11-08 17:14:16 -080014
Chris Sosa7c931362010-10-11 19:49:01 -070015def _LogMessage(message):
16 cherrypy.log(message, 'UPDATE')
rtc@google.comded22402009-10-26 22:36:21 +000017
Chris Sosa0356d3b2010-09-16 15:46:22 -070018
rtc@google.com64244662009-11-12 00:52:08 +000019class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -070020 """Class that contains functionality that handles Chrome OS update pings.
21
22 Members:
23 serve_only: Serve images from a pre-built image.zip file. static_dir
24 must be set to the location of the image.zip.
25 factory_config: Path to the factory config file if handling factory
26 requests.
27 use_test_image: Use chromiumos_test_image.bin rather than the standard.
28 static_url_base: base URL, other than devserver, for update images.
29 client_prefix: The prefix for the update engine client.
30 forced_image: Path to an image to use for all updates.
31 """
rtc@google.comded22402009-10-26 22:36:21 +000032
Sean O'Connor1f7fd362010-04-07 16:34:52 -070033 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Chris Sosa0356d3b2010-09-16 15:46:22 -070034 factory_config_path=None, client_prefix=None, forced_image=None,
Don Garrettf90edf02010-11-16 17:36:14 -080035 port=8080, src_image='', vm=False, board=None,
Chris Sosae67b78f2010-11-04 17:33:16 -070036 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070037 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070038 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -070039 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -070040 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -070041 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -070042 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -070043 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -070044 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -070045
Chris Sosab63a9282010-09-02 10:43:23 -070046 self.client_prefix = client_prefix
Chris Sosa0356d3b2010-09-16 15:46:22 -070047 self.forced_image = forced_image
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
Chris Sosa05491b12010-11-08 17:14:16 -080051 self.crosutils = os.path.join(os.path.dirname(__file__), '../../scripts')
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070052
Chris Sosa0356d3b2010-09-16 15:46:22 -070053 def _GetSecondsSinceMidnight(self):
54 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070055 now = time.localtime()
56 return now[3] * 3600 + now[4] * 60 + now[5]
57
Chris Sosa0356d3b2010-09-16 15:46:22 -070058 def _GetDefaultBoardID(self):
59 """Returns the default board id stored in .default_board."""
60 board_file = '%s/.default_board' % (self.scripts_dir)
61 try:
62 return open(board_file).read()
63 except IOError:
64 return 'x86-generic'
65
66 def _GetLatestImageDir(self, board_id):
67 """Returns the latest image dir based on shell script."""
68 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
69 return os.popen(cmd).read().strip()
70
71 def _GetVersionFromDir(self, image_dir):
72 """Returns the version of the image based on the name of the directory."""
73 latest_version = os.path.basename(image_dir)
74 return latest_version.split('-')[0]
75
76 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -080077 """Returns true if the latest_version is greater than the client_version.
78 """
Chris Sosa0356d3b2010-09-16 15:46:22 -070079 client_tokens = client_version.replace('_', '').split('.')
80 latest_tokens = latest_version.replace('_', '').split('.')
Chris Sosa7c931362010-10-11 19:49:01 -070081 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -080082 % (client_version, latest_version))
Chris Sosa0356d3b2010-09-16 15:46:22 -070083 for i in range(4):
84 if int(latest_tokens[i]) == int(client_tokens[i]):
85 continue
86 return int(latest_tokens[i]) > int(client_tokens[i])
87 return False
88
Chris Sosa0356d3b2010-09-16 15:46:22 -070089 def _UnpackZip(self, image_dir):
90 """Unpacks an image.zip into a given directory."""
91 image = os.path.join(image_dir, self._GetImageName())
92 if os.path.exists(image):
93 return True
94 else:
95 # -n, never clobber an existing file, in case we get invoked
96 # simultaneously by multiple request handlers. This means that
97 # we're assuming each image.zip file lives in a versioned
98 # directory (a la Buildbot).
99 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
100
101 def _GetImageName(self):
102 """Returns the name of the image that should be used."""
103 if self.use_test_image:
104 image_name = 'chromiumos_test_image.bin'
105 else:
106 image_name = 'chromiumos_image.bin'
107 return image_name
108
Chris Sosa0356d3b2010-09-16 15:46:22 -0700109 def _GetSize(self, update_path):
110 """Returns the size of the file given."""
111 return os.path.getsize(update_path)
112
113 def _GetHash(self, update_path):
114 """Returns the sha1 of the file given."""
115 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
116 % update_path)
117 return os.popen(cmd).read().rstrip()
118
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700119 def _IsDeltaFormatFile(self, filename):
120 try:
121 file_handle = open(filename, 'r')
122 delta_magic = 'CrAU'
123 magic = file_handle.read(len(delta_magic))
124 return magic == delta_magic
125 except Exception:
126 return False
127
Darin Petkov91436cb2010-09-28 08:52:17 -0700128 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
129 # it takes advantage of reduced I/O and multiple processors. Something like:
130 # % tee < FILE > /dev/null \
131 # >( openssl dgst -sha256 -binary | openssl base64 ) \
132 # >( openssl sha1 -binary | openssl base64 )
133 def _GetSHA256(self, update_path):
134 """Returns the sha256 of the file given."""
135 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
136 update_path)
137 return os.popen(cmd).read().rstrip()
138
Don Garrettf90edf02010-11-16 17:36:14 -0800139 def _GetMd5(self, update_path):
140 """Returns the md5 checksum of the file given."""
141 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
142 return os.popen(cmd).read().rstrip()
143
144 def _Symlink(self, source, dest):
145 """Creates a symlink at dest to source"""
146 if os.path.exists(dest):
147 os.remove(dest)
148 os.symlink(source, dest)
149
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700150 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700151 """Returns a payload to the client corresponding to a new update.
152
153 Args:
154 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700155 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700156 size: size of update blob
157 url: where to find update blob
158 Returns:
159 Xml string to be passed back to client.
160 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700161 delta = 'false'
162 if is_delta_format:
163 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000164 payload = """<?xml version="1.0" encoding="UTF-8"?>
165 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700166 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000167 <app appid="{%s}" status="ok">
168 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700169 <updatecheck
170 codebase="%s"
171 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700172 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700173 needsadmin="false"
174 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700175 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000176 status="ok"/>
177 </app>
178 </gupdate>
179 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700180 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700181 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000182
rtc@google.com21a5ca32009-11-04 18:23:23 +0000183 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700184 """Returns a payload to the client corresponding to no update."""
185 payload = """ < ?xml version = "1.0" encoding = "UTF-8"? >
186 < gupdate xmlns = "http://www.google.com/update2/response" protocol = "2.0" >
187 < daystart elapsed_seconds = "%s" />
188 < app appid = "{%s}" status = "ok" >
189 < ping status = "ok" />
190 < updatecheck status = "noupdate" />
191 </ app >
192 </ gupdate >
rtc@google.com21a5ca32009-11-04 18:23:23 +0000193 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700194 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000195
Don Garrettf90edf02010-11-16 17:36:14 -0800196 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700197 """Generates an update gz given a full path to an image.
198
199 Args:
200 image_path: Full path to image.
201 Returns:
202 Path to created update_payload or None on error.
203 """
Don Garrettf90edf02010-11-16 17:36:14 -0800204 update_path = os.path.join(output_dir, 'update.gz')
Chris Sosa4136e692010-10-28 23:42:37 -0700205 patch_kernel_flag = '--patch_kernel'
Chris Sosa7c931362010-10-11 19:49:01 -0700206 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700207
Chris Sosa4136e692010-10-28 23:42:37 -0700208 # Don't patch the kernel for vm images as they don't need the patch.
209 if self.vm:
210 patch_kernel_flag = ''
211
Chris Sosa0356d3b2010-09-16 15:46:22 -0700212 mkupdate_command = (
Chris Sosa62f720b2010-10-26 21:39:48 -0700213 '%s/cros_generate_update_payload --image="%s" --output="%s" '
Chris Sosa4136e692010-10-28 23:42:37 -0700214 '%s --noold_style --src_image="%s"' % (
215 self.scripts_dir, image_path, update_path, patch_kernel_flag,
Don Garrettf90edf02010-11-16 17:36:14 -0800216 src_image))
Chris Sosa62f720b2010-10-26 21:39:48 -0700217 _LogMessage(mkupdate_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700218 if os.system(mkupdate_command) != 0:
Chris Sosa7c931362010-10-11 19:49:01 -0700219 _LogMessage('Failed to create base update file')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700220 return None
221
222 return update_path
223
Don Garrettf90edf02010-11-16 17:36:14 -0800224 def GenerateStatefulFile(self, image_path, output_dir):
225 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700226
227 Args:
228 image_path: Full path to image.
229 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800230 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800231 Raises:
232 A subprocess exception if the update generator fails to generate a
233 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700234 """
Don Garrettf90edf02010-11-16 17:36:14 -0800235 output_gz = os.path.join(output_dir, 'stateful.tgz')
Chris Sosa908fd6f2010-11-10 17:31:18 -0800236 subprocess.check_call(
237 ['%s/cros_generate_stateful_update_payload' % self.crosutils,
238 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800239 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800240 ])
Chris Sosa05491b12010-11-08 17:14:16 -0800241 return output_gz
Chris Sosa0356d3b2010-09-16 15:46:22 -0700242
Don Garrettf90edf02010-11-16 17:36:14 -0800243 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
244 """Find directory to store a cached update.
245
246 Given one, or two images for an update, this finds which
247 cache directory should hold the update files, even if they don't exist
248 yet. The directory will be inside static_image_dir, and of the form:
249
250 Non-delta updates:
251 cache/12345678
252
253 Delta updates:
254 cache/12345678_12345678
255 """
256 # If there is no src, we only have an image file, check image for changes
257 if not src_image:
258 return os.path.join('cache', self._GetMd5(dest_image))
259
260 # If we have src and dest, we are a delta, and check both for changes
261 return os.path.join('cache',
262 "%s_%s" % (self._GetMd5(src_image),
263 self._GetMd5(dest_image)))
264
265 def GenerateUpdateImage(self, src_image, image_path, output_dir):
266 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700267
Chris Sosade91f672010-11-16 10:05:44 -0800268 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800269 src_image: image we are updating from (Null/empty for non-delta)
270 image_path: full path to the image.
271 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800272 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800273 update and stateful payload tuple with full file names
Chris Sosade91f672010-11-16 10:05:44 -0800274 """
Don Garrettf90edf02010-11-16 17:36:14 -0800275 update_file = None
276 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700277
Don Garrettf90edf02010-11-16 17:36:14 -0800278 # Actually do the generation
279 _LogMessage('Generating update for image %s' % image_path)
280 update_file = self.GenerateUpdateFile(src_image,
281 image_path,
282 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000283
Don Garrettf90edf02010-11-16 17:36:14 -0800284 if update_file:
285 stateful_update_file = self.GenerateStatefulFile(image_path,
286 output_dir)
287
288 if update_file and stateful_update_file:
289 return update_file, stateful_update_file
290
291 _LogMessage('Failed to generate update')
292
293 # Cleanup incomplete files, if they exist
294 if update_file and os.path.exists(update_file):
295 os.remove(update_file)
296
297 return None
298
299 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
300 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000301
Chris Sosa0356d3b2010-09-16 15:46:22 -0700302 Args:
303 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700304 static_image_dir: the directory to move images to after generating.
305 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800306 update filename (not directory) relative to static_image_dir on success,
307 or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700308 """
Don Garrettf90edf02010-11-16 17:36:14 -0800309 _LogMessage('Generating update for src %s image %s' % (self.src_image,
310 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700311
Don Garrettf90edf02010-11-16 17:36:14 -0800312 # Which sub_dir of static_image_dir should hold our cached update image
313 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
314 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
315
316 # The cached payloads exist in a cache dir
317 cache_update_payload = os.path.join(static_image_dir,
318 cache_sub_dir,
319 'update.gz')
320 cache_stateful_payload = os.path.join(static_image_dir,
321 cache_sub_dir,
322 'stateful.tgz')
323
324 # The final results (symlinks?) exist directly in static
325 update_payload = os.path.join(static_image_dir,
326 'update.gz')
327 stateful_payload = os.path.join(static_image_dir,
328 'stateful.tgz')
329
330 # If there isn't a cached payload, make one
331 if not os.path.exists(cache_update_payload):
332 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
333
334 # Create the directory for the cache values
335 if not os.path.exists(full_cache_dir):
336 os.makedirs(full_cache_dir)
337
338 payloads = self.GenerateUpdateImage(self.src_image,
339 image_path,
340 full_cache_dir)
341
342 if not payloads:
343 # Clean up cache dir if it's not valid
344 os.system("rm -rf %s" % os.path.join(static_image_dir, cache_sub_dir))
345 return None
346
347 # Verify they were created with the expected names
348 new_update_payload, new_stateful_payload = payloads
349
350 _LogMessage('"%s" "%s"' % (new_update_payload, cache_update_payload))
351 assert new_update_payload == cache_update_payload
352 _LogMessage('"%s" "%s"' % (new_stateful_payload, cache_stateful_payload))
353 assert new_stateful_payload == cache_stateful_payload
354
355 # If the generation worked, create symlinks
356 self._Symlink(cache_update_payload, update_payload)
357 self._Symlink(cache_stateful_payload, stateful_payload)
358
359 # return just the filename which is symlink in static_image_dir
360 return 'update.gz'
Chris Sosa0356d3b2010-09-16 15:46:22 -0700361
362 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800363 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700364 """Generates an update using the latest image that has been built.
365
366 This will only generate an update if the newest update is newer than that
367 on the client or client_version is 'ForcedUpdate'.
368
369 Args:
370 board_id: Name of the board.
371 client_version: Current version of the client or 'ForcedUpdate'
372 static_image_dir: the directory to move images to after generating.
373 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800374 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700375 """
376 latest_image_dir = self._GetLatestImageDir(board_id)
377 latest_version = self._GetVersionFromDir(latest_image_dir)
378 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
379
Chris Sosa7c931362010-10-11 19:49:01 -0700380 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700381 latest_image_path)
382
383 # Check to see whether or not we should update.
384 if client_version != 'ForcedUpdate' and not self._CanUpdate(
385 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700386 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800387 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700388
Don Garrettf90edf02010-11-16 17:36:14 -0800389 return self.GenerateUpdateImageWithCache(latest_image_path,
390 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700391
392 def GenerateImageFromZip(self, static_image_dir):
393 """Generates an update from an image zip file.
394
395 This method assumes you have an image.zip in directory you are serving
396 from. If this file is newer than a previously cached file, it will unzip
397 this file, create a payload and serve it.
398
399 Args:
400 static_image_dir: Directory where the zip file exists.
401 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800402 Name of the update payload relative to static_image_dir if successful.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700403 """
Don Garrettf90edf02010-11-16 17:36:14 -0800404 _LogMessage('Preparing to generate update from zip in %s.' %
405 static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700406 image_path = os.path.join(static_image_dir, self._GetImageName())
Chris Sosa0356d3b2010-09-16 15:46:22 -0700407 zip_file_path = os.path.join(static_image_dir, 'image.zip')
Don Garrettf90edf02010-11-16 17:36:14 -0800408
409 # TODO(dgarrett): Either work caching into this path before
410 # we unpack, or remove zip support (sosa is considering).
411 # It does currently cache, but after the unpack.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700412
413 if not self._UnpackZip(static_image_dir):
Chris Sosa7c931362010-10-11 19:49:01 -0700414 _LogMessage('unzip image.zip failed.')
Don Garrettf90edf02010-11-16 17:36:14 -0800415 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700416
Don Garrettf90edf02010-11-16 17:36:14 -0800417 return self.GenerateUpdateImageWithCache(image_path,
418 static_image_dir=static_image_dir)
Darin Petkov8ef83452010-03-23 16:52:29 -0700419
Andrew de los Reyes52620802010-04-12 13:40:07 -0700420 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
421 """Imports a factory-floor server configuration file. The file should
422 be in this format:
423 config = [
424 {
425 'qual_ids': set([1, 2, 3, "x86-generic"]),
426 'factory_image': 'generic-factory.gz',
427 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
428 'release_image': 'generic-release.gz',
429 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
430 'oempartitionimg_image': 'generic-oem.gz',
431 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700432 'efipartitionimg_image': 'generic-efi.gz',
433 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700434 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800435 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800436 'firmware_image': 'generic-firmware.gz',
437 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700438 },
439 {
440 'qual_ids': set([6]),
441 'factory_image': '6-factory.gz',
442 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
443 'release_image': '6-release.gz',
444 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
445 'oempartitionimg_image': '6-oem.gz',
446 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700447 'efipartitionimg_image': '6-efi.gz',
448 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700449 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800450 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800451 'firmware_image': '6-firmware.gz',
452 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700453 },
454 ]
455 The server will look for the files by name in the static files
456 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700457
Andrew de los Reyes52620802010-04-12 13:40:07 -0700458 If validate_checksums is True, validates checksums and exits. If
459 a checksum mismatch is found, it's printed to the screen.
460 """
461 f = open(filename, 'r')
462 output = {}
463 exec(f.read(), output)
464 self.factory_config = output['config']
465 success = True
466 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800467 for key in stanza.copy().iterkeys():
468 suffix = '_image'
469 if key.endswith(suffix):
470 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700471 stanza[kind + '_size'] = self._GetSize(os.path.join(
472 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800473 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700474 factory_checksum = self._GetHash(os.path.join(self.static_dir,
475 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800476 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700477 print ('Error: checksum mismatch for %s. Expected "%s" but file '
478 'has checksum "%s".' % (stanza[kind + '_image'],
479 stanza[kind + '_checksum'],
480 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800481 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700482
Andrew de los Reyes52620802010-04-12 13:40:07 -0700483 if validate_checksums:
484 if success is False:
485 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700486
Andrew de los Reyes52620802010-04-12 13:40:07 -0700487 print 'Config file looks good.'
488
489 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700490 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700491 for stanza in self.factory_config:
492 if board_id not in stanza['qual_ids']:
493 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700494 if kind + '_image' not in stanza:
495 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700496 return (stanza[kind + '_image'],
497 stanza[kind + '_checksum'],
498 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700499 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000500
Chris Sosa7c931362010-10-11 19:49:01 -0700501 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700502 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
503 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700504 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700505 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700506 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700507 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700508 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700509 # Factory install is using memento updater which is using the sha-1 hash so
510 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700511 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700512
Chris Sosa151643e2010-10-28 14:40:57 -0700513 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
514 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800515 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800516
Don Garrettf90edf02010-11-16 17:36:14 -0800517 Returns:
518 file name relative to static_image_dir on success.
519 """
520 if self.forced_image:
521 return self.GenerateUpdateImageWithCache(
522 self.forced_image,
523 static_image_dir=static_image_dir)
524 elif self.serve_only:
525 return self.GenerateImageFromZip(static_image_dir)
526 else:
527 if board_id:
528 return self.GenerateLatestUpdateImage(board_id,
529 client_version,
530 static_image_dir)
531
532 _LogMessage('You must set --board for pre-generating latest update.')
533 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700534
535 def PreGenerateUpdate(self):
Don Garrettf90edf02010-11-16 17:36:14 -0800536 """Pre-generates an update. Returns True on success.
537 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700538 # Does not work with factory config.
539 assert(not self.factory_config)
540 _LogMessage('Pre-generating the update payload.')
541 # Does not work with labels so just use static dir.
Chris Sosae67b78f2010-11-04 17:33:16 -0700542 if self.GenerateUpdatePayloadForNonFactory(self.board, '0.0.0.0',
543 self.static_dir):
Chris Sosae67b78f2010-11-04 17:33:16 -0700544 _LogMessage('Pre-generated update successfully.')
545 return True
Chris Sosa2c048f12010-10-27 16:05:27 -0700546 else:
547 _LogMessage('Failed to pre-generate update.')
Chris Sosae67b78f2010-11-04 17:33:16 -0700548 return False
Chris Sosa2c048f12010-10-27 16:05:27 -0700549
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700550 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700551 """Handles an update ping from an update client.
552
553 Args:
554 data: xml blob from client.
555 label: optional label for the update.
556 Returns:
557 Update payload message for client.
558 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700559 # Set hostname as the hostname that the client is calling to and set up
560 # the url base.
561 self.hostname = cherrypy.request.base
562 if self.urlbase:
563 static_urlbase = self.urlbase
564 elif self.serve_only:
565 static_urlbase = '%s/static/archive' % self.hostname
566 else:
567 static_urlbase = '%s/static' % self.hostname
568
569 _LogMessage('Using static url base %s' % static_urlbase)
570 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700571
572 # Check the client prefix to make sure you can support this type of update.
Chris Sosa9841e1c2010-10-14 10:51:45 -0700573 update_dom = minidom.parseString(data)
574 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700575 if (root.hasAttribute('updaterversion') and
576 not root.getAttribute('updaterversion').startswith(self.client_prefix)):
Chris Sosa7c931362010-10-11 19:49:01 -0700577 _LogMessage('Got update from unsupported updater:' +
Chris Sosa0356d3b2010-09-16 15:46:22 -0700578 root.getAttribute('updaterversion'))
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700579 return self.GetNoUpdatePayload()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700580
581 # We only generate update payloads for updatecheck requests.
582 update_check = root.getElementsByTagName('o:updatecheck')
583 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700584 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700585 # TODO(sosa): Generate correct non-updatecheck payload to better test
586 # update clients.
587 return self.GetNoUpdatePayload()
588
589 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700590 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800591 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700592 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700593 board_id = (query.hasAttribute('board') and query.getAttribute('board')
594 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700595
Chris Sosa0356d3b2010-09-16 15:46:22 -0700596 # Separate logic as Factory requests have static url's that override
597 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700598 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700599 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700600 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700601 static_image_dir = self.static_dir
602 if label:
603 static_image_dir = os.path.join(static_image_dir, label)
604
Don Garrettf90edf02010-11-16 17:36:14 -0800605 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
606 client_version,
607 static_image_dir)
608 if payload_path:
609 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700610 hash = self._GetHash(filename)
611 sha256 = self._GetSHA256(filename)
612 size = self._GetSize(filename)
613 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700614 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800615 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700616 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800617 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700618
Chris Sosa7c931362010-10-11 19:49:01 -0700619 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700620 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700621 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700622 return self.GetNoUpdatePayload()