blob: ba21b2b4ba47c5ed08448cde4d41c86bf032e40b [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,
Don Garrett0c880e22010-11-17 18:13:37 -080034 factory_config_path=None, client_prefix=None,
35 forced_image=None, forced_payload=None,
Don Garrettf90edf02010-11-16 17:36:14 -080036 port=8080, src_image='', vm=False, board=None,
Chris Sosae67b78f2010-11-04 17:33:16 -070037 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070038 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070039 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -070040 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -070041 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -070042 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -070043 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -070044 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -070045 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -070046
Chris Sosab63a9282010-09-02 10:43:23 -070047 self.client_prefix = client_prefix
Chris Sosa0356d3b2010-09-16 15:46:22 -070048 self.forced_image = forced_image
Don Garrett0c880e22010-11-17 18:13:37 -080049 self.forced_payload = forced_payload
Chris Sosa62f720b2010-10-26 21:39:48 -070050 self.src_image = src_image
Chris Sosa4136e692010-10-28 23:42:37 -070051 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -070052 self.board = board
Chris Sosa05491b12010-11-08 17:14:16 -080053 self.crosutils = os.path.join(os.path.dirname(__file__), '../../scripts')
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070054
Chris Sosa0356d3b2010-09-16 15:46:22 -070055 def _GetSecondsSinceMidnight(self):
56 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070057 now = time.localtime()
58 return now[3] * 3600 + now[4] * 60 + now[5]
59
Chris Sosa0356d3b2010-09-16 15:46:22 -070060 def _GetDefaultBoardID(self):
61 """Returns the default board id stored in .default_board."""
62 board_file = '%s/.default_board' % (self.scripts_dir)
63 try:
64 return open(board_file).read()
65 except IOError:
66 return 'x86-generic'
67
68 def _GetLatestImageDir(self, board_id):
69 """Returns the latest image dir based on shell script."""
70 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
71 return os.popen(cmd).read().strip()
72
73 def _GetVersionFromDir(self, image_dir):
74 """Returns the version of the image based on the name of the directory."""
75 latest_version = os.path.basename(image_dir)
76 return latest_version.split('-')[0]
77
78 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -080079 """Returns true if the latest_version is greater than the client_version.
80 """
Chris Sosa0356d3b2010-09-16 15:46:22 -070081 client_tokens = client_version.replace('_', '').split('.')
82 latest_tokens = latest_version.replace('_', '').split('.')
Chris Sosa7c931362010-10-11 19:49:01 -070083 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -080084 % (client_version, latest_version))
Chris Sosa0356d3b2010-09-16 15:46:22 -070085 for i in range(4):
86 if int(latest_tokens[i]) == int(client_tokens[i]):
87 continue
88 return int(latest_tokens[i]) > int(client_tokens[i])
89 return False
90
Chris Sosa0356d3b2010-09-16 15:46:22 -070091 def _UnpackZip(self, image_dir):
92 """Unpacks an image.zip into a given directory."""
93 image = os.path.join(image_dir, self._GetImageName())
94 if os.path.exists(image):
95 return True
96 else:
97 # -n, never clobber an existing file, in case we get invoked
98 # simultaneously by multiple request handlers. This means that
99 # we're assuming each image.zip file lives in a versioned
100 # directory (a la Buildbot).
101 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
102
103 def _GetImageName(self):
104 """Returns the name of the image that should be used."""
105 if self.use_test_image:
106 image_name = 'chromiumos_test_image.bin'
107 else:
108 image_name = 'chromiumos_image.bin'
109 return image_name
110
Chris Sosa0356d3b2010-09-16 15:46:22 -0700111 def _GetSize(self, update_path):
112 """Returns the size of the file given."""
113 return os.path.getsize(update_path)
114
115 def _GetHash(self, update_path):
116 """Returns the sha1 of the file given."""
117 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
118 % update_path)
119 return os.popen(cmd).read().rstrip()
120
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700121 def _IsDeltaFormatFile(self, filename):
122 try:
123 file_handle = open(filename, 'r')
124 delta_magic = 'CrAU'
125 magic = file_handle.read(len(delta_magic))
126 return magic == delta_magic
127 except Exception:
128 return False
129
Darin Petkov91436cb2010-09-28 08:52:17 -0700130 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
131 # it takes advantage of reduced I/O and multiple processors. Something like:
132 # % tee < FILE > /dev/null \
133 # >( openssl dgst -sha256 -binary | openssl base64 ) \
134 # >( openssl sha1 -binary | openssl base64 )
135 def _GetSHA256(self, update_path):
136 """Returns the sha256 of the file given."""
137 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
138 update_path)
139 return os.popen(cmd).read().rstrip()
140
Don Garrettf90edf02010-11-16 17:36:14 -0800141 def _GetMd5(self, update_path):
142 """Returns the md5 checksum of the file given."""
143 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
144 return os.popen(cmd).read().rstrip()
145
Don Garrett0c880e22010-11-17 18:13:37 -0800146 def _Copy(self, source, dest):
147 """Copies a file from dest to source (if different)"""
148 _LogMessage('Copy File %s -> %s' % (source, dest))
149 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800150 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800151 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800152
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700153 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700154 """Returns a payload to the client corresponding to a new update.
155
156 Args:
157 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700158 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700159 size: size of update blob
160 url: where to find update blob
161 Returns:
162 Xml string to be passed back to client.
163 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700164 delta = 'false'
165 if is_delta_format:
166 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000167 payload = """<?xml version="1.0" encoding="UTF-8"?>
168 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700169 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000170 <app appid="{%s}" status="ok">
171 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700172 <updatecheck
173 codebase="%s"
174 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700175 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700176 needsadmin="false"
177 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700178 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000179 status="ok"/>
180 </app>
181 </gupdate>
182 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700183 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700184 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000185
rtc@google.com21a5ca32009-11-04 18:23:23 +0000186 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700187 """Returns a payload to the client corresponding to no update."""
188 payload = """ < ?xml version = "1.0" encoding = "UTF-8"? >
189 < gupdate xmlns = "http://www.google.com/update2/response" protocol = "2.0" >
190 < daystart elapsed_seconds = "%s" />
191 < app appid = "{%s}" status = "ok" >
192 < ping status = "ok" />
193 < updatecheck status = "noupdate" />
194 </ app >
195 </ gupdate >
rtc@google.com21a5ca32009-11-04 18:23:23 +0000196 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700197 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000198
Don Garrettf90edf02010-11-16 17:36:14 -0800199 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700200 """Generates an update gz given a full path to an image.
201
202 Args:
203 image_path: Full path to image.
204 Returns:
205 Path to created update_payload or None on error.
206 """
Don Garrettf90edf02010-11-16 17:36:14 -0800207 update_path = os.path.join(output_dir, 'update.gz')
Chris Sosa4136e692010-10-28 23:42:37 -0700208 patch_kernel_flag = '--patch_kernel'
Chris Sosa7c931362010-10-11 19:49:01 -0700209 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700210
Chris Sosa4136e692010-10-28 23:42:37 -0700211 # Don't patch the kernel for vm images as they don't need the patch.
212 if self.vm:
213 patch_kernel_flag = ''
214
Chris Sosa0356d3b2010-09-16 15:46:22 -0700215 mkupdate_command = (
Chris Sosa62f720b2010-10-26 21:39:48 -0700216 '%s/cros_generate_update_payload --image="%s" --output="%s" '
Chris Sosa4136e692010-10-28 23:42:37 -0700217 '%s --noold_style --src_image="%s"' % (
218 self.scripts_dir, image_path, update_path, patch_kernel_flag,
Don Garrettf90edf02010-11-16 17:36:14 -0800219 src_image))
Chris Sosa62f720b2010-10-26 21:39:48 -0700220 _LogMessage(mkupdate_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700221 if os.system(mkupdate_command) != 0:
Chris Sosa7c931362010-10-11 19:49:01 -0700222 _LogMessage('Failed to create base update file')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700223 return None
224
225 return update_path
226
Don Garrettf90edf02010-11-16 17:36:14 -0800227 def GenerateStatefulFile(self, image_path, output_dir):
228 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700229
230 Args:
231 image_path: Full path to image.
232 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800233 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800234 Raises:
235 A subprocess exception if the update generator fails to generate a
236 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700237 """
Don Garrettf90edf02010-11-16 17:36:14 -0800238 output_gz = os.path.join(output_dir, 'stateful.tgz')
Chris Sosa908fd6f2010-11-10 17:31:18 -0800239 subprocess.check_call(
240 ['%s/cros_generate_stateful_update_payload' % self.crosutils,
241 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800242 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800243 ])
Chris Sosa05491b12010-11-08 17:14:16 -0800244 return output_gz
Chris Sosa0356d3b2010-09-16 15:46:22 -0700245
Don Garrettf90edf02010-11-16 17:36:14 -0800246 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
247 """Find directory to store a cached update.
248
249 Given one, or two images for an update, this finds which
250 cache directory should hold the update files, even if they don't exist
251 yet. The directory will be inside static_image_dir, and of the form:
252
253 Non-delta updates:
254 cache/12345678
255
256 Delta updates:
257 cache/12345678_12345678
258 """
259 # If there is no src, we only have an image file, check image for changes
260 if not src_image:
261 return os.path.join('cache', self._GetMd5(dest_image))
262
263 # If we have src and dest, we are a delta, and check both for changes
264 return os.path.join('cache',
265 "%s_%s" % (self._GetMd5(src_image),
266 self._GetMd5(dest_image)))
267
268 def GenerateUpdateImage(self, src_image, image_path, output_dir):
269 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700270
Chris Sosade91f672010-11-16 10:05:44 -0800271 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800272 src_image: image we are updating from (Null/empty for non-delta)
273 image_path: full path to the image.
274 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800275 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800276 update and stateful payload tuple with full file names
Chris Sosade91f672010-11-16 10:05:44 -0800277 """
Don Garrettf90edf02010-11-16 17:36:14 -0800278 update_file = None
279 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700280
Don Garrettf90edf02010-11-16 17:36:14 -0800281 # Actually do the generation
282 _LogMessage('Generating update for image %s' % image_path)
283 update_file = self.GenerateUpdateFile(src_image,
284 image_path,
285 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000286
Don Garrettf90edf02010-11-16 17:36:14 -0800287 if update_file:
288 stateful_update_file = self.GenerateStatefulFile(image_path,
289 output_dir)
290
291 if update_file and stateful_update_file:
292 return update_file, stateful_update_file
293
294 _LogMessage('Failed to generate update')
295
296 # Cleanup incomplete files, if they exist
297 if update_file and os.path.exists(update_file):
298 os.remove(update_file)
299
300 return None
301
302 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
303 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000304
Chris Sosa0356d3b2010-09-16 15:46:22 -0700305 Args:
306 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700307 static_image_dir: the directory to move images to after generating.
308 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800309 update filename (not directory) relative to static_image_dir on success,
310 or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700311 """
Don Garrettf90edf02010-11-16 17:36:14 -0800312 _LogMessage('Generating update for src %s image %s' % (self.src_image,
313 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700314
Don Garrettf90edf02010-11-16 17:36:14 -0800315 # Which sub_dir of static_image_dir should hold our cached update image
316 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
317 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
318
319 # The cached payloads exist in a cache dir
320 cache_update_payload = os.path.join(static_image_dir,
321 cache_sub_dir,
322 'update.gz')
323 cache_stateful_payload = os.path.join(static_image_dir,
324 cache_sub_dir,
325 'stateful.tgz')
326
327 # The final results (symlinks?) exist directly in static
328 update_payload = os.path.join(static_image_dir,
329 'update.gz')
330 stateful_payload = os.path.join(static_image_dir,
331 'stateful.tgz')
332
333 # If there isn't a cached payload, make one
334 if not os.path.exists(cache_update_payload):
335 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
336
337 # Create the directory for the cache values
338 if not os.path.exists(full_cache_dir):
339 os.makedirs(full_cache_dir)
340
341 payloads = self.GenerateUpdateImage(self.src_image,
342 image_path,
343 full_cache_dir)
344
345 if not payloads:
346 # Clean up cache dir if it's not valid
347 os.system("rm -rf %s" % os.path.join(static_image_dir, cache_sub_dir))
348 return None
349
350 # Verify they were created with the expected names
351 new_update_payload, new_stateful_payload = payloads
352
353 _LogMessage('"%s" "%s"' % (new_update_payload, cache_update_payload))
354 assert new_update_payload == cache_update_payload
355 _LogMessage('"%s" "%s"' % (new_stateful_payload, cache_stateful_payload))
356 assert new_stateful_payload == cache_stateful_payload
357
Don Garrett0c880e22010-11-17 18:13:37 -0800358 # If the generation worked, copy files
359 self._Copy(cache_update_payload, update_payload)
360 self._Copy(cache_stateful_payload, stateful_payload)
Don Garrettf90edf02010-11-16 17:36:14 -0800361
362 # return just the filename which is symlink in static_image_dir
363 return 'update.gz'
Chris Sosa0356d3b2010-09-16 15:46:22 -0700364
365 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800366 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700367 """Generates an update using the latest image that has been built.
368
369 This will only generate an update if the newest update is newer than that
370 on the client or client_version is 'ForcedUpdate'.
371
372 Args:
373 board_id: Name of the board.
374 client_version: Current version of the client or 'ForcedUpdate'
375 static_image_dir: the directory to move images to after generating.
376 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800377 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700378 """
379 latest_image_dir = self._GetLatestImageDir(board_id)
380 latest_version = self._GetVersionFromDir(latest_image_dir)
381 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
382
Chris Sosa7c931362010-10-11 19:49:01 -0700383 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700384 latest_image_path)
385
386 # Check to see whether or not we should update.
387 if client_version != 'ForcedUpdate' and not self._CanUpdate(
388 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700389 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800390 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700391
Don Garrettf90edf02010-11-16 17:36:14 -0800392 return self.GenerateUpdateImageWithCache(latest_image_path,
393 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700394
395 def GenerateImageFromZip(self, static_image_dir):
396 """Generates an update from an image zip file.
397
398 This method assumes you have an image.zip in directory you are serving
399 from. If this file is newer than a previously cached file, it will unzip
400 this file, create a payload and serve it.
401
402 Args:
403 static_image_dir: Directory where the zip file exists.
404 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800405 Name of the update payload relative to static_image_dir if successful.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700406 """
Don Garrettf90edf02010-11-16 17:36:14 -0800407 _LogMessage('Preparing to generate update from zip in %s.' %
408 static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700409 image_path = os.path.join(static_image_dir, self._GetImageName())
Chris Sosa0356d3b2010-09-16 15:46:22 -0700410 zip_file_path = os.path.join(static_image_dir, 'image.zip')
Don Garrettf90edf02010-11-16 17:36:14 -0800411
412 # TODO(dgarrett): Either work caching into this path before
413 # we unpack, or remove zip support (sosa is considering).
414 # It does currently cache, but after the unpack.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700415
416 if not self._UnpackZip(static_image_dir):
Chris Sosa7c931362010-10-11 19:49:01 -0700417 _LogMessage('unzip image.zip failed.')
Don Garrettf90edf02010-11-16 17:36:14 -0800418 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700419
Don Garrettf90edf02010-11-16 17:36:14 -0800420 return self.GenerateUpdateImageWithCache(image_path,
421 static_image_dir=static_image_dir)
Darin Petkov8ef83452010-03-23 16:52:29 -0700422
Andrew de los Reyes52620802010-04-12 13:40:07 -0700423 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
424 """Imports a factory-floor server configuration file. The file should
425 be in this format:
426 config = [
427 {
428 'qual_ids': set([1, 2, 3, "x86-generic"]),
429 'factory_image': 'generic-factory.gz',
430 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
431 'release_image': 'generic-release.gz',
432 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
433 'oempartitionimg_image': 'generic-oem.gz',
434 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700435 'efipartitionimg_image': 'generic-efi.gz',
436 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700437 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800438 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800439 'firmware_image': 'generic-firmware.gz',
440 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700441 },
442 {
443 'qual_ids': set([6]),
444 'factory_image': '6-factory.gz',
445 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
446 'release_image': '6-release.gz',
447 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
448 'oempartitionimg_image': '6-oem.gz',
449 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700450 'efipartitionimg_image': '6-efi.gz',
451 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700452 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800453 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800454 'firmware_image': '6-firmware.gz',
455 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700456 },
457 ]
458 The server will look for the files by name in the static files
459 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700460
Andrew de los Reyes52620802010-04-12 13:40:07 -0700461 If validate_checksums is True, validates checksums and exits. If
462 a checksum mismatch is found, it's printed to the screen.
463 """
464 f = open(filename, 'r')
465 output = {}
466 exec(f.read(), output)
467 self.factory_config = output['config']
468 success = True
469 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800470 for key in stanza.copy().iterkeys():
471 suffix = '_image'
472 if key.endswith(suffix):
473 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700474 stanza[kind + '_size'] = self._GetSize(os.path.join(
475 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800476 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700477 factory_checksum = self._GetHash(os.path.join(self.static_dir,
478 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800479 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700480 print ('Error: checksum mismatch for %s. Expected "%s" but file '
481 'has checksum "%s".' % (stanza[kind + '_image'],
482 stanza[kind + '_checksum'],
483 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800484 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700485
Andrew de los Reyes52620802010-04-12 13:40:07 -0700486 if validate_checksums:
487 if success is False:
488 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700489
Andrew de los Reyes52620802010-04-12 13:40:07 -0700490 print 'Config file looks good.'
491
492 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700493 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700494 for stanza in self.factory_config:
495 if board_id not in stanza['qual_ids']:
496 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700497 if kind + '_image' not in stanza:
498 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700499 return (stanza[kind + '_image'],
500 stanza[kind + '_checksum'],
501 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700502 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000503
Chris Sosa7c931362010-10-11 19:49:01 -0700504 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700505 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
506 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700507 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700508 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700509 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700510 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700511 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700512 # Factory install is using memento updater which is using the sha-1 hash so
513 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700514 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700515
Chris Sosa151643e2010-10-28 14:40:57 -0700516 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
517 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800518 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800519
Don Garrettf90edf02010-11-16 17:36:14 -0800520 Returns:
521 file name relative to static_image_dir on success.
522 """
Don Garrett0c880e22010-11-17 18:13:37 -0800523 if self.forced_payload:
524 # If the forced payload is not already in our static_image_dir,
525 # copy it there.
526 if (os.path.dirname(os.path.abspath(self.forced_payload)) !=
527 os.path.abspath(static_image_dir)):
528 self._Copy(self.forced_payload, os.path.join(static_image_dir,
529 'update.gz'))
530
531 self._Copy(os.path.join(os.path.dirname(self.forced_payload),
532 'stateful.tgz'),
533 os.path.join(static_image_dir,
534 'stateful.tgz'))
535
536 return 'update.gz'
537 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800538 return self.GenerateUpdateImageWithCache(
539 self.forced_image,
540 static_image_dir=static_image_dir)
541 elif self.serve_only:
542 return self.GenerateImageFromZip(static_image_dir)
543 else:
544 if board_id:
545 return self.GenerateLatestUpdateImage(board_id,
546 client_version,
547 static_image_dir)
548
549 _LogMessage('You must set --board for pre-generating latest update.')
550 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700551
552 def PreGenerateUpdate(self):
Don Garrettf90edf02010-11-16 17:36:14 -0800553 """Pre-generates an update. Returns True on success.
554 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700555 # Does not work with factory config.
556 assert(not self.factory_config)
557 _LogMessage('Pre-generating the update payload.')
558 # Does not work with labels so just use static dir.
Chris Sosae67b78f2010-11-04 17:33:16 -0700559 if self.GenerateUpdatePayloadForNonFactory(self.board, '0.0.0.0',
560 self.static_dir):
Chris Sosae67b78f2010-11-04 17:33:16 -0700561 _LogMessage('Pre-generated update successfully.')
562 return True
Chris Sosa2c048f12010-10-27 16:05:27 -0700563 else:
564 _LogMessage('Failed to pre-generate update.')
Chris Sosae67b78f2010-11-04 17:33:16 -0700565 return False
Chris Sosa2c048f12010-10-27 16:05:27 -0700566
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700567 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700568 """Handles an update ping from an update client.
569
570 Args:
571 data: xml blob from client.
572 label: optional label for the update.
573 Returns:
574 Update payload message for client.
575 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700576 # Set hostname as the hostname that the client is calling to and set up
577 # the url base.
578 self.hostname = cherrypy.request.base
579 if self.urlbase:
580 static_urlbase = self.urlbase
581 elif self.serve_only:
582 static_urlbase = '%s/static/archive' % self.hostname
583 else:
584 static_urlbase = '%s/static' % self.hostname
585
586 _LogMessage('Using static url base %s' % static_urlbase)
587 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700588
589 # Check the client prefix to make sure you can support this type of update.
Chris Sosa9841e1c2010-10-14 10:51:45 -0700590 update_dom = minidom.parseString(data)
591 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700592 if (root.hasAttribute('updaterversion') and
593 not root.getAttribute('updaterversion').startswith(self.client_prefix)):
Chris Sosa7c931362010-10-11 19:49:01 -0700594 _LogMessage('Got update from unsupported updater:' +
Chris Sosa0356d3b2010-09-16 15:46:22 -0700595 root.getAttribute('updaterversion'))
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700596 return self.GetNoUpdatePayload()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700597
598 # We only generate update payloads for updatecheck requests.
599 update_check = root.getElementsByTagName('o:updatecheck')
600 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700601 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700602 # TODO(sosa): Generate correct non-updatecheck payload to better test
603 # update clients.
604 return self.GetNoUpdatePayload()
605
606 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700607 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800608 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700609 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700610 board_id = (query.hasAttribute('board') and query.getAttribute('board')
611 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700612
Chris Sosa0356d3b2010-09-16 15:46:22 -0700613 # Separate logic as Factory requests have static url's that override
614 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700615 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700616 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700617 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700618 static_image_dir = self.static_dir
619 if label:
620 static_image_dir = os.path.join(static_image_dir, label)
621
Don Garrettf90edf02010-11-16 17:36:14 -0800622 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
623 client_version,
624 static_image_dir)
625 if payload_path:
626 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700627 hash = self._GetHash(filename)
628 sha256 = self._GetSHA256(filename)
629 size = self._GetSize(filename)
630 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700631 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800632 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700633 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800634 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700635
Chris Sosa7c931362010-10-11 19:49:01 -0700636 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700637 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700638 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700639 return self.GetNoUpdatePayload()