blob: 84dc69074ac3288561b62baf5682b35de8ac99ee [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.
Don Garrettee25e552010-11-23 12:09:35 -0800532 src_path = os.path.abspath(self.forced_payload)
533 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
Don Garrett0c880e22010-11-17 18:13:37 -0800534
Don Garrettee25e552010-11-23 12:09:35 -0800535 src_stateful = os.path.join(os.path.dirname(src_path),
536 STATEFUL_FILE)
537 dest_stateful = os.path.join(static_image_dir,
538 STATEFUL_FILE)
539
540 # Only copy the files if the source directory is different from dest.
541 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
542 self._Copy(src_path, dest_path)
543
544 # The stateful payload is optional.
545 if os.path.exists(src_stateful):
546 self._Copy(src_stateful, dest_stateful)
547 else:
548 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
549 STATEFUL_FILE)
550 if os.path.exists(dest_stateful):
551 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800552
Don Garrettfff4c322010-11-19 13:37:12 -0800553 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800554 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800555 return self.GenerateUpdateImageWithCache(
556 self.forced_image,
557 static_image_dir=static_image_dir)
558 elif self.serve_only:
559 return self.GenerateImageFromZip(static_image_dir)
560 else:
561 if board_id:
562 return self.GenerateLatestUpdateImage(board_id,
563 client_version,
564 static_image_dir)
565
566 _LogMessage('You must set --board for pre-generating latest update.')
567 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700568
569 def PreGenerateUpdate(self):
Don Garrettf90edf02010-11-16 17:36:14 -0800570 """Pre-generates an update. Returns True on success.
571 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700572 # Does not work with factory config.
573 assert(not self.factory_config)
574 _LogMessage('Pre-generating the update payload.')
575 # Does not work with labels so just use static dir.
Chris Sosae67b78f2010-11-04 17:33:16 -0700576 if self.GenerateUpdatePayloadForNonFactory(self.board, '0.0.0.0',
577 self.static_dir):
Chris Sosae67b78f2010-11-04 17:33:16 -0700578 _LogMessage('Pre-generated update successfully.')
Don Garrettfff4c322010-11-19 13:37:12 -0800579 self.pregenerated = True
Chris Sosae67b78f2010-11-04 17:33:16 -0700580 return True
Chris Sosa2c048f12010-10-27 16:05:27 -0700581 else:
582 _LogMessage('Failed to pre-generate update.')
Chris Sosae67b78f2010-11-04 17:33:16 -0700583 return False
Chris Sosa2c048f12010-10-27 16:05:27 -0700584
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700585 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700586 """Handles an update ping from an update client.
587
588 Args:
589 data: xml blob from client.
590 label: optional label for the update.
591 Returns:
592 Update payload message for client.
593 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700594 # Set hostname as the hostname that the client is calling to and set up
595 # the url base.
596 self.hostname = cherrypy.request.base
597 if self.urlbase:
598 static_urlbase = self.urlbase
599 elif self.serve_only:
600 static_urlbase = '%s/static/archive' % self.hostname
601 else:
602 static_urlbase = '%s/static' % self.hostname
603
604 _LogMessage('Using static url base %s' % static_urlbase)
605 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700606
607 # Check the client prefix to make sure you can support this type of update.
Chris Sosa9841e1c2010-10-14 10:51:45 -0700608 update_dom = minidom.parseString(data)
609 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700610 if (root.hasAttribute('updaterversion') and
611 not root.getAttribute('updaterversion').startswith(self.client_prefix)):
Chris Sosa7c931362010-10-11 19:49:01 -0700612 _LogMessage('Got update from unsupported updater:' +
Chris Sosa0356d3b2010-09-16 15:46:22 -0700613 root.getAttribute('updaterversion'))
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700614 return self.GetNoUpdatePayload()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700615
616 # We only generate update payloads for updatecheck requests.
617 update_check = root.getElementsByTagName('o:updatecheck')
618 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700619 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700620 # TODO(sosa): Generate correct non-updatecheck payload to better test
621 # update clients.
622 return self.GetNoUpdatePayload()
623
624 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700625 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800626 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700627 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700628 board_id = (query.hasAttribute('board') and query.getAttribute('board')
629 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700630
Chris Sosa0356d3b2010-09-16 15:46:22 -0700631 # Separate logic as Factory requests have static url's that override
632 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700633 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700634 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700635 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700636 static_image_dir = self.static_dir
637 if label:
638 static_image_dir = os.path.join(static_image_dir, label)
639
Don Garrettf90edf02010-11-16 17:36:14 -0800640 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
641 client_version,
642 static_image_dir)
643 if payload_path:
644 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700645 hash = self._GetHash(filename)
646 sha256 = self._GetSHA256(filename)
647 size = self._GetSize(filename)
648 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700649 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800650 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700651 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800652 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700653
Chris Sosa7c931362010-10-11 19:49:01 -0700654 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700655 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700656 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700657 return self.GetNoUpdatePayload()