blob: 2addd2c760976eeaa6382ed0b911d5f2b9a2088e [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
Don Garrettfff4c322010-11-19 13:37:12 -080018UPDATE_FILE='update.gz'
19STATEFUL_FILE='stateful.tgz'
Chris Sosa0356d3b2010-09-16 15:46:22 -070020
rtc@google.com64244662009-11-12 00:52:08 +000021class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -070022 """Class that contains functionality that handles Chrome OS update pings.
23
24 Members:
25 serve_only: Serve images from a pre-built image.zip file. static_dir
26 must be set to the location of the image.zip.
27 factory_config: Path to the factory config file if handling factory
28 requests.
29 use_test_image: Use chromiumos_test_image.bin rather than the standard.
30 static_url_base: base URL, other than devserver, for update images.
31 client_prefix: The prefix for the update engine client.
32 forced_image: Path to an image to use for all updates.
33 """
rtc@google.comded22402009-10-26 22:36:21 +000034
Sean O'Connor1f7fd362010-04-07 16:34:52 -070035 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Don Garrett0c880e22010-11-17 18:13:37 -080036 factory_config_path=None, client_prefix=None,
37 forced_image=None, forced_payload=None,
Don Garrettf90edf02010-11-16 17:36:14 -080038 port=8080, src_image='', vm=False, board=None,
Chris Sosae67b78f2010-11-04 17:33:16 -070039 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070040 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070041 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -070042 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -070043 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -070044 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -070045 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -070046 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -070047 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -070048
Chris Sosab63a9282010-09-02 10:43:23 -070049 self.client_prefix = client_prefix
Chris Sosa0356d3b2010-09-16 15:46:22 -070050 self.forced_image = forced_image
Don Garrett0c880e22010-11-17 18:13:37 -080051 self.forced_payload = forced_payload
Chris Sosa62f720b2010-10-26 21:39:48 -070052 self.src_image = src_image
Chris Sosa4136e692010-10-28 23:42:37 -070053 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -070054 self.board = board
Don Garrettfff4c322010-11-19 13:37:12 -080055
56 # Caching is enabled if we are not doing serve_only
57 # aka if --archive_dir was not passed in.
58 self.caching_enabled = not self.serve_only
59
60 # Track update pregeneration, so we don't recopy if not needed.
61 self.pregenerated = False
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070062
Chris Sosa0356d3b2010-09-16 15:46:22 -070063 def _GetSecondsSinceMidnight(self):
64 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070065 now = time.localtime()
66 return now[3] * 3600 + now[4] * 60 + now[5]
67
Chris Sosa0356d3b2010-09-16 15:46:22 -070068 def _GetDefaultBoardID(self):
69 """Returns the default board id stored in .default_board."""
70 board_file = '%s/.default_board' % (self.scripts_dir)
71 try:
72 return open(board_file).read()
73 except IOError:
74 return 'x86-generic'
75
76 def _GetLatestImageDir(self, board_id):
77 """Returns the latest image dir based on shell script."""
78 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
79 return os.popen(cmd).read().strip()
80
81 def _GetVersionFromDir(self, image_dir):
82 """Returns the version of the image based on the name of the directory."""
83 latest_version = os.path.basename(image_dir)
84 return latest_version.split('-')[0]
85
86 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -080087 """Returns true if the latest_version is greater than the client_version.
88 """
Chris Sosa0356d3b2010-09-16 15:46:22 -070089 client_tokens = client_version.replace('_', '').split('.')
90 latest_tokens = latest_version.replace('_', '').split('.')
Chris Sosa7c931362010-10-11 19:49:01 -070091 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -080092 % (client_version, latest_version))
Chris Sosa0356d3b2010-09-16 15:46:22 -070093 for i in range(4):
94 if int(latest_tokens[i]) == int(client_tokens[i]):
95 continue
96 return int(latest_tokens[i]) > int(client_tokens[i])
97 return False
98
Chris Sosa0356d3b2010-09-16 15:46:22 -070099 def _UnpackZip(self, image_dir):
100 """Unpacks an image.zip into a given directory."""
101 image = os.path.join(image_dir, self._GetImageName())
102 if os.path.exists(image):
103 return True
104 else:
105 # -n, never clobber an existing file, in case we get invoked
106 # simultaneously by multiple request handlers. This means that
107 # we're assuming each image.zip file lives in a versioned
108 # directory (a la Buildbot).
109 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
110
111 def _GetImageName(self):
112 """Returns the name of the image that should be used."""
113 if self.use_test_image:
114 image_name = 'chromiumos_test_image.bin'
115 else:
116 image_name = 'chromiumos_image.bin'
117 return image_name
118
Chris Sosa0356d3b2010-09-16 15:46:22 -0700119 def _GetSize(self, update_path):
120 """Returns the size of the file given."""
121 return os.path.getsize(update_path)
122
123 def _GetHash(self, update_path):
124 """Returns the sha1 of the file given."""
125 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
126 % update_path)
127 return os.popen(cmd).read().rstrip()
128
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700129 def _IsDeltaFormatFile(self, filename):
130 try:
131 file_handle = open(filename, 'r')
132 delta_magic = 'CrAU'
133 magic = file_handle.read(len(delta_magic))
134 return magic == delta_magic
135 except Exception:
136 return False
137
Darin Petkov91436cb2010-09-28 08:52:17 -0700138 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
139 # it takes advantage of reduced I/O and multiple processors. Something like:
140 # % tee < FILE > /dev/null \
141 # >( openssl dgst -sha256 -binary | openssl base64 ) \
142 # >( openssl sha1 -binary | openssl base64 )
143 def _GetSHA256(self, update_path):
144 """Returns the sha256 of the file given."""
145 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
146 update_path)
147 return os.popen(cmd).read().rstrip()
148
Don Garrettf90edf02010-11-16 17:36:14 -0800149 def _GetMd5(self, update_path):
150 """Returns the md5 checksum of the file given."""
151 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
152 return os.popen(cmd).read().rstrip()
153
Don Garrett0c880e22010-11-17 18:13:37 -0800154 def _Copy(self, source, dest):
155 """Copies a file from dest to source (if different)"""
156 _LogMessage('Copy File %s -> %s' % (source, dest))
157 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800158 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800159 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800160
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700161 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700162 """Returns a payload to the client corresponding to a new update.
163
164 Args:
165 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700166 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700167 size: size of update blob
168 url: where to find update blob
169 Returns:
170 Xml string to be passed back to client.
171 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700172 delta = 'false'
173 if is_delta_format:
174 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000175 payload = """<?xml version="1.0" encoding="UTF-8"?>
176 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700177 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000178 <app appid="{%s}" status="ok">
179 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700180 <updatecheck
181 codebase="%s"
182 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700183 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700184 needsadmin="false"
185 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700186 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000187 status="ok"/>
188 </app>
189 </gupdate>
190 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700191 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700192 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000193
rtc@google.com21a5ca32009-11-04 18:23:23 +0000194 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700195 """Returns a payload to the client corresponding to no update."""
196 payload = """ < ?xml version = "1.0" encoding = "UTF-8"? >
197 < gupdate xmlns = "http://www.google.com/update2/response" protocol = "2.0" >
198 < daystart elapsed_seconds = "%s" />
199 < app appid = "{%s}" status = "ok" >
200 < ping status = "ok" />
201 < updatecheck status = "noupdate" />
202 </ app >
203 </ gupdate >
rtc@google.com21a5ca32009-11-04 18:23:23 +0000204 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700205 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000206
Don Garrettf90edf02010-11-16 17:36:14 -0800207 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700208 """Generates an update gz given a full path to an image.
209
210 Args:
211 image_path: Full path to image.
212 Returns:
213 Path to created update_payload or None on error.
214 """
Don Garrettfff4c322010-11-19 13:37:12 -0800215 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa4136e692010-10-28 23:42:37 -0700216 patch_kernel_flag = '--patch_kernel'
Chris Sosa7c931362010-10-11 19:49:01 -0700217 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700218
Chris Sosa4136e692010-10-28 23:42:37 -0700219 # Don't patch the kernel for vm images as they don't need the patch.
220 if self.vm:
221 patch_kernel_flag = ''
222
Chris Sosa0356d3b2010-09-16 15:46:22 -0700223 mkupdate_command = (
Chris Sosa62f720b2010-10-26 21:39:48 -0700224 '%s/cros_generate_update_payload --image="%s" --output="%s" '
Chris Sosa4136e692010-10-28 23:42:37 -0700225 '%s --noold_style --src_image="%s"' % (
226 self.scripts_dir, image_path, update_path, patch_kernel_flag,
Don Garrettf90edf02010-11-16 17:36:14 -0800227 src_image))
Chris Sosa62f720b2010-10-26 21:39:48 -0700228 _LogMessage(mkupdate_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700229 if os.system(mkupdate_command) != 0:
Chris Sosa7c931362010-10-11 19:49:01 -0700230 _LogMessage('Failed to create base update file')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700231 return None
232
Don Garrettfff4c322010-11-19 13:37:12 -0800233 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700234
Don Garrettf90edf02010-11-16 17:36:14 -0800235 def GenerateStatefulFile(self, image_path, output_dir):
236 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700237
238 Args:
239 image_path: Full path to image.
240 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800241 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800242 Raises:
243 A subprocess exception if the update generator fails to generate a
244 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700245 """
Don Garrettfff4c322010-11-19 13:37:12 -0800246 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800247 subprocess.check_call(
Don Garrettfff4c322010-11-19 13:37:12 -0800248 ['%s/cros_generate_stateful_update_payload' % self.scripts_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800249 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800250 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800251 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800252 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700253
Don Garrettf90edf02010-11-16 17:36:14 -0800254 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
255 """Find directory to store a cached update.
256
257 Given one, or two images for an update, this finds which
258 cache directory should hold the update files, even if they don't exist
259 yet. The directory will be inside static_image_dir, and of the form:
260
261 Non-delta updates:
262 cache/12345678
263
264 Delta updates:
265 cache/12345678_12345678
266 """
267 # If there is no src, we only have an image file, check image for changes
268 if not src_image:
269 return os.path.join('cache', self._GetMd5(dest_image))
270
271 # If we have src and dest, we are a delta, and check both for changes
272 return os.path.join('cache',
273 "%s_%s" % (self._GetMd5(src_image),
274 self._GetMd5(dest_image)))
275
Don Garrettfff4c322010-11-19 13:37:12 -0800276 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800277 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700278
Chris Sosade91f672010-11-16 10:05:44 -0800279 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800280 src_image: image we are updating from (Null/empty for non-delta)
281 image_path: full path to the image.
282 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800283 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800284 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800285 """
Don Garrettf90edf02010-11-16 17:36:14 -0800286 update_file = None
287 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700288
Don Garrettf90edf02010-11-16 17:36:14 -0800289 # Actually do the generation
290 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800291 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800292 image_path,
293 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000294
Don Garrettf90edf02010-11-16 17:36:14 -0800295 if update_file:
296 stateful_update_file = self.GenerateStatefulFile(image_path,
297 output_dir)
298
299 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800300 return update_file
Don Garrettf90edf02010-11-16 17:36:14 -0800301
302 _LogMessage('Failed to generate update')
303
304 # Cleanup incomplete files, if they exist
305 if update_file and os.path.exists(update_file):
306 os.remove(update_file)
307
308 return None
309
310 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
311 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000312
Chris Sosa0356d3b2010-09-16 15:46:22 -0700313 Args:
314 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700315 static_image_dir: the directory to move images to after generating.
316 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800317 update filename (not directory) relative to static_image_dir on success,
318 or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700319 """
Don Garrettf90edf02010-11-16 17:36:14 -0800320 _LogMessage('Generating update for src %s image %s' % (self.src_image,
321 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700322
Don Garrettfff4c322010-11-19 13:37:12 -0800323 # If it was pregenerated, don't regenerate
324 if self.pregenerated:
325 return UPDATE_FILE
326
327 if not self.caching_enabled:
328 return self.GenerateUpdateImage(image_path, static_image_dir)
329
Don Garrettf90edf02010-11-16 17:36:14 -0800330 # Which sub_dir of static_image_dir should hold our cached update image
331 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
332 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
333
334 # The cached payloads exist in a cache dir
335 cache_update_payload = os.path.join(static_image_dir,
336 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800337 UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800338 cache_stateful_payload = os.path.join(static_image_dir,
339 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800340 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800341
Don Garrettfff4c322010-11-19 13:37:12 -0800342 # The final results exist directly in static
Don Garrettf90edf02010-11-16 17:36:14 -0800343 update_payload = os.path.join(static_image_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800344 UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800345 stateful_payload = os.path.join(static_image_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800346 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800347
348 # If there isn't a cached payload, make one
349 if not os.path.exists(cache_update_payload):
350 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
351
352 # Create the directory for the cache values
353 if not os.path.exists(full_cache_dir):
354 os.makedirs(full_cache_dir)
355
Don Garrettfff4c322010-11-19 13:37:12 -0800356 result = self.GenerateUpdateImage(image_path,
357 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800358
Don Garrettfff4c322010-11-19 13:37:12 -0800359 if not result:
Don Garrettf90edf02010-11-16 17:36:14 -0800360 # Clean up cache dir if it's not valid
361 os.system("rm -rf %s" % os.path.join(static_image_dir, cache_sub_dir))
362 return None
363
Don Garrett0c880e22010-11-17 18:13:37 -0800364 # If the generation worked, copy files
365 self._Copy(cache_update_payload, update_payload)
366 self._Copy(cache_stateful_payload, stateful_payload)
Don Garrettf90edf02010-11-16 17:36:14 -0800367
Don Garrettfff4c322010-11-19 13:37:12 -0800368 # Return just the filename in static_image_dir.
369 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700370
371 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800372 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700373 """Generates an update using the latest image that has been built.
374
375 This will only generate an update if the newest update is newer than that
376 on the client or client_version is 'ForcedUpdate'.
377
378 Args:
379 board_id: Name of the board.
380 client_version: Current version of the client or 'ForcedUpdate'
381 static_image_dir: the directory to move images to after generating.
382 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800383 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700384 """
385 latest_image_dir = self._GetLatestImageDir(board_id)
386 latest_version = self._GetVersionFromDir(latest_image_dir)
387 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
388
Chris Sosa7c931362010-10-11 19:49:01 -0700389 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700390 latest_image_path)
391
392 # Check to see whether or not we should update.
393 if client_version != 'ForcedUpdate' and not self._CanUpdate(
394 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700395 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800396 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700397
Don Garrettf90edf02010-11-16 17:36:14 -0800398 return self.GenerateUpdateImageWithCache(latest_image_path,
399 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700400
401 def GenerateImageFromZip(self, static_image_dir):
402 """Generates an update from an image zip file.
403
404 This method assumes you have an image.zip in directory you are serving
405 from. If this file is newer than a previously cached file, it will unzip
406 this file, create a payload and serve it.
407
408 Args:
409 static_image_dir: Directory where the zip file exists.
410 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800411 Name of the update payload relative to static_image_dir if successful.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700412 """
Don Garrettf90edf02010-11-16 17:36:14 -0800413 _LogMessage('Preparing to generate update from zip in %s.' %
414 static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700415 image_path = os.path.join(static_image_dir, self._GetImageName())
Chris Sosa0356d3b2010-09-16 15:46:22 -0700416 zip_file_path = os.path.join(static_image_dir, 'image.zip')
Don Garrettf90edf02010-11-16 17:36:14 -0800417
418 # TODO(dgarrett): Either work caching into this path before
419 # we unpack, or remove zip support (sosa is considering).
420 # It does currently cache, but after the unpack.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700421
422 if not self._UnpackZip(static_image_dir):
Chris Sosa7c931362010-10-11 19:49:01 -0700423 _LogMessage('unzip image.zip failed.')
Don Garrettf90edf02010-11-16 17:36:14 -0800424 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700425
Don Garrettf90edf02010-11-16 17:36:14 -0800426 return self.GenerateUpdateImageWithCache(image_path,
427 static_image_dir=static_image_dir)
Darin Petkov8ef83452010-03-23 16:52:29 -0700428
Andrew de los Reyes52620802010-04-12 13:40:07 -0700429 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
430 """Imports a factory-floor server configuration file. The file should
431 be in this format:
432 config = [
433 {
434 'qual_ids': set([1, 2, 3, "x86-generic"]),
435 'factory_image': 'generic-factory.gz',
436 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
437 'release_image': 'generic-release.gz',
438 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
439 'oempartitionimg_image': 'generic-oem.gz',
440 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700441 'efipartitionimg_image': 'generic-efi.gz',
442 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700443 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800444 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800445 'firmware_image': 'generic-firmware.gz',
446 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700447 },
448 {
449 'qual_ids': set([6]),
450 'factory_image': '6-factory.gz',
451 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
452 'release_image': '6-release.gz',
453 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
454 'oempartitionimg_image': '6-oem.gz',
455 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700456 'efipartitionimg_image': '6-efi.gz',
457 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700458 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800459 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800460 'firmware_image': '6-firmware.gz',
461 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700462 },
463 ]
464 The server will look for the files by name in the static files
465 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700466
Andrew de los Reyes52620802010-04-12 13:40:07 -0700467 If validate_checksums is True, validates checksums and exits. If
468 a checksum mismatch is found, it's printed to the screen.
469 """
470 f = open(filename, 'r')
471 output = {}
472 exec(f.read(), output)
473 self.factory_config = output['config']
474 success = True
475 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800476 for key in stanza.copy().iterkeys():
477 suffix = '_image'
478 if key.endswith(suffix):
479 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700480 stanza[kind + '_size'] = self._GetSize(os.path.join(
481 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800482 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700483 factory_checksum = self._GetHash(os.path.join(self.static_dir,
484 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800485 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700486 print ('Error: checksum mismatch for %s. Expected "%s" but file '
487 'has checksum "%s".' % (stanza[kind + '_image'],
488 stanza[kind + '_checksum'],
489 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800490 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700491
Andrew de los Reyes52620802010-04-12 13:40:07 -0700492 if validate_checksums:
493 if success is False:
494 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700495
Andrew de los Reyes52620802010-04-12 13:40:07 -0700496 print 'Config file looks good.'
497
498 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700499 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700500 for stanza in self.factory_config:
501 if board_id not in stanza['qual_ids']:
502 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700503 if kind + '_image' not in stanza:
504 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700505 return (stanza[kind + '_image'],
506 stanza[kind + '_checksum'],
507 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700508 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000509
Chris Sosa7c931362010-10-11 19:49:01 -0700510 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700511 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
512 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700513 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700514 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700515 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700516 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700517 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700518 # Factory install is using memento updater which is using the sha-1 hash so
519 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700520 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700521
Chris Sosa151643e2010-10-28 14:40:57 -0700522 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
523 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800524 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800525
Don Garrettf90edf02010-11-16 17:36:14 -0800526 Returns:
527 file name relative to static_image_dir on success.
528 """
Don Garrett0c880e22010-11-17 18:13:37 -0800529 if self.forced_payload:
530 # If the forced payload is not already in our static_image_dir,
531 # copy it there.
532 if (os.path.dirname(os.path.abspath(self.forced_payload)) !=
533 os.path.abspath(static_image_dir)):
534 self._Copy(self.forced_payload, os.path.join(static_image_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800535 UPDATE_FILE))
Don Garrett0c880e22010-11-17 18:13:37 -0800536
537 self._Copy(os.path.join(os.path.dirname(self.forced_payload),
Don Garrettfff4c322010-11-19 13:37:12 -0800538 STATEFUL_FILE),
Don Garrett0c880e22010-11-17 18:13:37 -0800539 os.path.join(static_image_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800540 STATEFUL_FILE))
Don Garrett0c880e22010-11-17 18:13:37 -0800541
Don Garrettfff4c322010-11-19 13:37:12 -0800542 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800543 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800544 return self.GenerateUpdateImageWithCache(
545 self.forced_image,
546 static_image_dir=static_image_dir)
547 elif self.serve_only:
548 return self.GenerateImageFromZip(static_image_dir)
549 else:
550 if board_id:
551 return self.GenerateLatestUpdateImage(board_id,
552 client_version,
553 static_image_dir)
554
555 _LogMessage('You must set --board for pre-generating latest update.')
556 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700557
558 def PreGenerateUpdate(self):
Don Garrettf90edf02010-11-16 17:36:14 -0800559 """Pre-generates an update. Returns True on success.
560 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700561 # Does not work with factory config.
562 assert(not self.factory_config)
563 _LogMessage('Pre-generating the update payload.')
564 # Does not work with labels so just use static dir.
Chris Sosae67b78f2010-11-04 17:33:16 -0700565 if self.GenerateUpdatePayloadForNonFactory(self.board, '0.0.0.0',
566 self.static_dir):
Chris Sosae67b78f2010-11-04 17:33:16 -0700567 _LogMessage('Pre-generated update successfully.')
Don Garrettfff4c322010-11-19 13:37:12 -0800568 self.pregenerated = True
Chris Sosae67b78f2010-11-04 17:33:16 -0700569 return True
Chris Sosa2c048f12010-10-27 16:05:27 -0700570 else:
571 _LogMessage('Failed to pre-generate update.')
Chris Sosae67b78f2010-11-04 17:33:16 -0700572 return False
Chris Sosa2c048f12010-10-27 16:05:27 -0700573
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700574 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700575 """Handles an update ping from an update client.
576
577 Args:
578 data: xml blob from client.
579 label: optional label for the update.
580 Returns:
581 Update payload message for client.
582 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700583 # Set hostname as the hostname that the client is calling to and set up
584 # the url base.
585 self.hostname = cherrypy.request.base
586 if self.urlbase:
587 static_urlbase = self.urlbase
588 elif self.serve_only:
589 static_urlbase = '%s/static/archive' % self.hostname
590 else:
591 static_urlbase = '%s/static' % self.hostname
592
593 _LogMessage('Using static url base %s' % static_urlbase)
594 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700595
596 # Check the client prefix to make sure you can support this type of update.
Chris Sosa9841e1c2010-10-14 10:51:45 -0700597 update_dom = minidom.parseString(data)
598 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700599 if (root.hasAttribute('updaterversion') and
600 not root.getAttribute('updaterversion').startswith(self.client_prefix)):
Chris Sosa7c931362010-10-11 19:49:01 -0700601 _LogMessage('Got update from unsupported updater:' +
Chris Sosa0356d3b2010-09-16 15:46:22 -0700602 root.getAttribute('updaterversion'))
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700603 return self.GetNoUpdatePayload()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700604
605 # We only generate update payloads for updatecheck requests.
606 update_check = root.getElementsByTagName('o:updatecheck')
607 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700608 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700609 # TODO(sosa): Generate correct non-updatecheck payload to better test
610 # update clients.
611 return self.GetNoUpdatePayload()
612
613 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700614 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800615 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700616 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700617 board_id = (query.hasAttribute('board') and query.getAttribute('board')
618 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700619
Chris Sosa0356d3b2010-09-16 15:46:22 -0700620 # Separate logic as Factory requests have static url's that override
621 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700622 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700623 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700624 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700625 static_image_dir = self.static_dir
626 if label:
627 static_image_dir = os.path.join(static_image_dir, label)
628
Don Garrettf90edf02010-11-16 17:36:14 -0800629 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
630 client_version,
631 static_image_dir)
632 if payload_path:
633 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700634 hash = self._GetHash(filename)
635 sha256 = self._GetSHA256(filename)
636 size = self._GetSize(filename)
637 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700638 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800639 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700640 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800641 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700642
Chris Sosa7c931362010-10-11 19:49:01 -0700643 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700644 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700645 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700646 return self.GetNoUpdatePayload()