blob: 7849e535314fe90418e6f60de99a79f9d2a256c3 [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:
Dale Curtis723ec472010-11-30 14:06:47 -080025 serve_only: Serve only pre-built updates. static_dir must contain update.gz
26 and stateful.tgz.
Chris Sosa0356d3b2010-09-16 15:46:22 -070027 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
Don Garrettfff4c322010-11-19 13:37:12 -080056 # Track update pregeneration, so we don't recopy if not needed.
57 self.pregenerated = False
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070058
Chris Sosa0356d3b2010-09-16 15:46:22 -070059 def _GetSecondsSinceMidnight(self):
60 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070061 now = time.localtime()
62 return now[3] * 3600 + now[4] * 60 + now[5]
63
Chris Sosa0356d3b2010-09-16 15:46:22 -070064 def _GetDefaultBoardID(self):
65 """Returns the default board id stored in .default_board."""
66 board_file = '%s/.default_board' % (self.scripts_dir)
67 try:
68 return open(board_file).read()
69 except IOError:
70 return 'x86-generic'
71
72 def _GetLatestImageDir(self, board_id):
73 """Returns the latest image dir based on shell script."""
74 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
75 return os.popen(cmd).read().strip()
76
77 def _GetVersionFromDir(self, image_dir):
78 """Returns the version of the image based on the name of the directory."""
79 latest_version = os.path.basename(image_dir)
80 return latest_version.split('-')[0]
81
82 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -080083 """Returns true if the latest_version is greater than the client_version.
84 """
Chris Sosa0356d3b2010-09-16 15:46:22 -070085 client_tokens = client_version.replace('_', '').split('.')
86 latest_tokens = latest_version.replace('_', '').split('.')
Chris Sosa7c931362010-10-11 19:49:01 -070087 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -080088 % (client_version, latest_version))
Chris Sosa0356d3b2010-09-16 15:46:22 -070089 for i in range(4):
90 if int(latest_tokens[i]) == int(client_tokens[i]):
91 continue
92 return int(latest_tokens[i]) > int(client_tokens[i])
93 return False
94
Chris Sosa0356d3b2010-09-16 15:46:22 -070095 def _UnpackZip(self, image_dir):
96 """Unpacks an image.zip into a given directory."""
97 image = os.path.join(image_dir, self._GetImageName())
98 if os.path.exists(image):
99 return True
100 else:
101 # -n, never clobber an existing file, in case we get invoked
102 # simultaneously by multiple request handlers. This means that
103 # we're assuming each image.zip file lives in a versioned
104 # directory (a la Buildbot).
105 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
106
107 def _GetImageName(self):
108 """Returns the name of the image that should be used."""
109 if self.use_test_image:
110 image_name = 'chromiumos_test_image.bin'
111 else:
112 image_name = 'chromiumos_image.bin'
113 return image_name
114
Chris Sosa0356d3b2010-09-16 15:46:22 -0700115 def _GetSize(self, update_path):
116 """Returns the size of the file given."""
117 return os.path.getsize(update_path)
118
119 def _GetHash(self, update_path):
120 """Returns the sha1 of the file given."""
121 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
122 % update_path)
123 return os.popen(cmd).read().rstrip()
124
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700125 def _IsDeltaFormatFile(self, filename):
126 try:
127 file_handle = open(filename, 'r')
128 delta_magic = 'CrAU'
129 magic = file_handle.read(len(delta_magic))
130 return magic == delta_magic
131 except Exception:
132 return False
133
Darin Petkov91436cb2010-09-28 08:52:17 -0700134 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
135 # it takes advantage of reduced I/O and multiple processors. Something like:
136 # % tee < FILE > /dev/null \
137 # >( openssl dgst -sha256 -binary | openssl base64 ) \
138 # >( openssl sha1 -binary | openssl base64 )
139 def _GetSHA256(self, update_path):
140 """Returns the sha256 of the file given."""
141 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
142 update_path)
143 return os.popen(cmd).read().rstrip()
144
Don Garrettf90edf02010-11-16 17:36:14 -0800145 def _GetMd5(self, update_path):
146 """Returns the md5 checksum of the file given."""
147 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
148 return os.popen(cmd).read().rstrip()
149
Don Garrett0c880e22010-11-17 18:13:37 -0800150 def _Copy(self, source, dest):
151 """Copies a file from dest to source (if different)"""
152 _LogMessage('Copy File %s -> %s' % (source, dest))
153 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800154 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800155 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800156
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700157 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700158 """Returns a payload to the client corresponding to a new update.
159
160 Args:
161 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700162 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700163 size: size of update blob
164 url: where to find update blob
165 Returns:
166 Xml string to be passed back to client.
167 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700168 delta = 'false'
169 if is_delta_format:
170 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000171 payload = """<?xml version="1.0" encoding="UTF-8"?>
172 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700173 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000174 <app appid="{%s}" status="ok">
175 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700176 <updatecheck
177 codebase="%s"
178 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700179 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700180 needsadmin="false"
181 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700182 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000183 status="ok"/>
184 </app>
185 </gupdate>
186 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700187 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700188 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000189
rtc@google.com21a5ca32009-11-04 18:23:23 +0000190 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700191 """Returns a payload to the client corresponding to no update."""
192 payload = """ < ?xml version = "1.0" encoding = "UTF-8"? >
193 < gupdate xmlns = "http://www.google.com/update2/response" protocol = "2.0" >
194 < daystart elapsed_seconds = "%s" />
195 < app appid = "{%s}" status = "ok" >
196 < ping status = "ok" />
197 < updatecheck status = "noupdate" />
198 </ app >
199 </ gupdate >
rtc@google.com21a5ca32009-11-04 18:23:23 +0000200 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700201 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000202
Don Garrettf90edf02010-11-16 17:36:14 -0800203 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700204 """Generates an update gz given a full path to an image.
205
206 Args:
207 image_path: Full path to image.
208 Returns:
209 Path to created update_payload or None on error.
210 """
Don Garrettfff4c322010-11-19 13:37:12 -0800211 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa4136e692010-10-28 23:42:37 -0700212 patch_kernel_flag = '--patch_kernel'
Chris Sosa7c931362010-10-11 19:49:01 -0700213 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700214
Chris Sosa4136e692010-10-28 23:42:37 -0700215 # Don't patch the kernel for vm images as they don't need the patch.
216 if self.vm:
217 patch_kernel_flag = ''
218
Chris Sosa0356d3b2010-09-16 15:46:22 -0700219 mkupdate_command = (
Chris Sosa62f720b2010-10-26 21:39:48 -0700220 '%s/cros_generate_update_payload --image="%s" --output="%s" '
Chris Sosa4136e692010-10-28 23:42:37 -0700221 '%s --noold_style --src_image="%s"' % (
222 self.scripts_dir, image_path, update_path, patch_kernel_flag,
Don Garrettf90edf02010-11-16 17:36:14 -0800223 src_image))
Chris Sosa62f720b2010-10-26 21:39:48 -0700224 _LogMessage(mkupdate_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700225 if os.system(mkupdate_command) != 0:
Chris Sosa7c931362010-10-11 19:49:01 -0700226 _LogMessage('Failed to create base update file')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700227 return None
228
Don Garrettfff4c322010-11-19 13:37:12 -0800229 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700230
Don Garrettf90edf02010-11-16 17:36:14 -0800231 def GenerateStatefulFile(self, image_path, output_dir):
232 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700233
234 Args:
235 image_path: Full path to image.
236 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800237 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800238 Raises:
239 A subprocess exception if the update generator fails to generate a
240 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700241 """
Don Garrettfff4c322010-11-19 13:37:12 -0800242 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800243 subprocess.check_call(
Don Garrettfff4c322010-11-19 13:37:12 -0800244 ['%s/cros_generate_stateful_update_payload' % self.scripts_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800245 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800246 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800247 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800248 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700249
Don Garrettf90edf02010-11-16 17:36:14 -0800250 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
251 """Find directory to store a cached update.
252
253 Given one, or two images for an update, this finds which
254 cache directory should hold the update files, even if they don't exist
255 yet. The directory will be inside static_image_dir, and of the form:
256
257 Non-delta updates:
258 cache/12345678
259
260 Delta updates:
261 cache/12345678_12345678
262 """
263 # If there is no src, we only have an image file, check image for changes
264 if not src_image:
265 return os.path.join('cache', self._GetMd5(dest_image))
266
267 # If we have src and dest, we are a delta, and check both for changes
268 return os.path.join('cache',
269 "%s_%s" % (self._GetMd5(src_image),
270 self._GetMd5(dest_image)))
271
Don Garrettfff4c322010-11-19 13:37:12 -0800272 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800273 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700274
Chris Sosade91f672010-11-16 10:05:44 -0800275 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800276 src_image: image we are updating from (Null/empty for non-delta)
277 image_path: full path to the image.
278 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800279 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800280 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800281 """
Don Garrettf90edf02010-11-16 17:36:14 -0800282 update_file = None
283 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700284
Don Garrettf90edf02010-11-16 17:36:14 -0800285 # Actually do the generation
286 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800287 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800288 image_path,
289 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000290
Don Garrettf90edf02010-11-16 17:36:14 -0800291 if update_file:
292 stateful_update_file = self.GenerateStatefulFile(image_path,
293 output_dir)
294
295 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800296 return update_file
Don Garrettf90edf02010-11-16 17:36:14 -0800297
298 _LogMessage('Failed to generate update')
299
300 # Cleanup incomplete files, if they exist
301 if update_file and os.path.exists(update_file):
302 os.remove(update_file)
303
304 return None
305
306 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
307 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000308
Chris Sosa0356d3b2010-09-16 15:46:22 -0700309 Args:
310 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700311 static_image_dir: the directory to move images to after generating.
312 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800313 update filename (not directory) relative to static_image_dir on success,
314 or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700315 """
Don Garrettf90edf02010-11-16 17:36:14 -0800316 _LogMessage('Generating update for src %s image %s' % (self.src_image,
317 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700318
Don Garrettfff4c322010-11-19 13:37:12 -0800319 # If it was pregenerated, don't regenerate
320 if self.pregenerated:
321 return UPDATE_FILE
322
Don Garrettf90edf02010-11-16 17:36:14 -0800323 # Which sub_dir of static_image_dir should hold our cached update image
324 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
325 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
326
327 # The cached payloads exist in a cache dir
328 cache_update_payload = os.path.join(static_image_dir,
329 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800330 UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800331 cache_stateful_payload = os.path.join(static_image_dir,
332 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800333 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800334
Don Garrettfff4c322010-11-19 13:37:12 -0800335 # The final results exist directly in static
Don Garrettf90edf02010-11-16 17:36:14 -0800336 update_payload = os.path.join(static_image_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800337 UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800338 stateful_payload = os.path.join(static_image_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800339 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800340
341 # If there isn't a cached payload, make one
342 if not os.path.exists(cache_update_payload):
343 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
344
345 # Create the directory for the cache values
346 if not os.path.exists(full_cache_dir):
347 os.makedirs(full_cache_dir)
348
Don Garrettfff4c322010-11-19 13:37:12 -0800349 result = self.GenerateUpdateImage(image_path,
350 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800351
Don Garrettfff4c322010-11-19 13:37:12 -0800352 if not result:
Don Garrettf90edf02010-11-16 17:36:14 -0800353 # Clean up cache dir if it's not valid
354 os.system("rm -rf %s" % os.path.join(static_image_dir, cache_sub_dir))
355 return None
356
Don Garrett0c880e22010-11-17 18:13:37 -0800357 # If the generation worked, copy files
358 self._Copy(cache_update_payload, update_payload)
359 self._Copy(cache_stateful_payload, stateful_payload)
Don Garrettf90edf02010-11-16 17:36:14 -0800360
Don Garrettfff4c322010-11-19 13:37:12 -0800361 # Return just the filename in static_image_dir.
362 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700363
364 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800365 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700366 """Generates an update using the latest image that has been built.
367
368 This will only generate an update if the newest update is newer than that
369 on the client or client_version is 'ForcedUpdate'.
370
371 Args:
372 board_id: Name of the board.
373 client_version: Current version of the client or 'ForcedUpdate'
374 static_image_dir: the directory to move images to after generating.
375 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800376 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700377 """
378 latest_image_dir = self._GetLatestImageDir(board_id)
379 latest_version = self._GetVersionFromDir(latest_image_dir)
380 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
381
Chris Sosa7c931362010-10-11 19:49:01 -0700382 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700383 latest_image_path)
384
385 # Check to see whether or not we should update.
386 if client_version != 'ForcedUpdate' and not self._CanUpdate(
387 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700388 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800389 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700390
Don Garrettf90edf02010-11-16 17:36:14 -0800391 return self.GenerateUpdateImageWithCache(latest_image_path,
392 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700393
Andrew de los Reyes52620802010-04-12 13:40:07 -0700394 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
395 """Imports a factory-floor server configuration file. The file should
396 be in this format:
397 config = [
398 {
399 'qual_ids': set([1, 2, 3, "x86-generic"]),
400 'factory_image': 'generic-factory.gz',
401 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
402 'release_image': 'generic-release.gz',
403 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
404 'oempartitionimg_image': 'generic-oem.gz',
405 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700406 'efipartitionimg_image': 'generic-efi.gz',
407 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700408 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800409 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800410 'firmware_image': 'generic-firmware.gz',
411 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700412 },
413 {
414 'qual_ids': set([6]),
415 'factory_image': '6-factory.gz',
416 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
417 'release_image': '6-release.gz',
418 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
419 'oempartitionimg_image': '6-oem.gz',
420 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700421 'efipartitionimg_image': '6-efi.gz',
422 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700423 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800424 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800425 'firmware_image': '6-firmware.gz',
426 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700427 },
428 ]
429 The server will look for the files by name in the static files
430 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700431
Andrew de los Reyes52620802010-04-12 13:40:07 -0700432 If validate_checksums is True, validates checksums and exits. If
433 a checksum mismatch is found, it's printed to the screen.
434 """
435 f = open(filename, 'r')
436 output = {}
437 exec(f.read(), output)
438 self.factory_config = output['config']
439 success = True
440 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800441 for key in stanza.copy().iterkeys():
442 suffix = '_image'
443 if key.endswith(suffix):
444 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700445 stanza[kind + '_size'] = self._GetSize(os.path.join(
446 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800447 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700448 factory_checksum = self._GetHash(os.path.join(self.static_dir,
449 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800450 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700451 print ('Error: checksum mismatch for %s. Expected "%s" but file '
452 'has checksum "%s".' % (stanza[kind + '_image'],
453 stanza[kind + '_checksum'],
454 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800455 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700456
Andrew de los Reyes52620802010-04-12 13:40:07 -0700457 if validate_checksums:
458 if success is False:
459 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700460
Andrew de los Reyes52620802010-04-12 13:40:07 -0700461 print 'Config file looks good.'
462
463 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700464 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700465 for stanza in self.factory_config:
466 if board_id not in stanza['qual_ids']:
467 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700468 if kind + '_image' not in stanza:
469 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700470 return (stanza[kind + '_image'],
471 stanza[kind + '_checksum'],
472 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700473 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000474
Chris Sosa7c931362010-10-11 19:49:01 -0700475 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700476 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
477 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700478 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700479 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700480 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700481 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700482 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700483 # Factory install is using memento updater which is using the sha-1 hash so
484 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700485 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700486
Chris Sosa151643e2010-10-28 14:40:57 -0700487 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
488 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800489 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800490
Don Garrettf90edf02010-11-16 17:36:14 -0800491 Returns:
492 file name relative to static_image_dir on success.
493 """
Dale Curtis723ec472010-11-30 14:06:47 -0800494 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
495 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
496
Don Garrett0c880e22010-11-17 18:13:37 -0800497 if self.forced_payload:
498 # If the forced payload is not already in our static_image_dir,
499 # copy it there.
Don Garrettee25e552010-11-23 12:09:35 -0800500 src_path = os.path.abspath(self.forced_payload)
Don Garrett0c880e22010-11-17 18:13:37 -0800501
Don Garrettee25e552010-11-23 12:09:35 -0800502 src_stateful = os.path.join(os.path.dirname(src_path),
503 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800504
505 # Only copy the files if the source directory is different from dest.
506 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
507 self._Copy(src_path, dest_path)
508
509 # The stateful payload is optional.
510 if os.path.exists(src_stateful):
511 self._Copy(src_stateful, dest_stateful)
512 else:
513 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
514 STATEFUL_FILE)
515 if os.path.exists(dest_stateful):
516 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800517
Don Garrettfff4c322010-11-19 13:37:12 -0800518 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800519 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800520 return self.GenerateUpdateImageWithCache(
521 self.forced_image,
522 static_image_dir=static_image_dir)
523 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800524 # Warn if update or stateful files can't be found.
525 if not os.path.exists(dest_path):
526 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
527 UPDATE_FILE)
528
529 if not os.path.exists(dest_stateful):
530 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
531 STATEFUL_FILE)
532
533 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800534 else:
535 if board_id:
536 return self.GenerateLatestUpdateImage(board_id,
537 client_version,
538 static_image_dir)
539
540 _LogMessage('You must set --board for pre-generating latest update.')
541 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700542
543 def PreGenerateUpdate(self):
Don Garrettf90edf02010-11-16 17:36:14 -0800544 """Pre-generates an update. Returns True on success.
545 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700546 # Does not work with factory config.
547 assert(not self.factory_config)
548 _LogMessage('Pre-generating the update payload.')
549 # Does not work with labels so just use static dir.
Chris Sosae67b78f2010-11-04 17:33:16 -0700550 if self.GenerateUpdatePayloadForNonFactory(self.board, '0.0.0.0',
551 self.static_dir):
Chris Sosae67b78f2010-11-04 17:33:16 -0700552 _LogMessage('Pre-generated update successfully.')
Don Garrettfff4c322010-11-19 13:37:12 -0800553 self.pregenerated = True
Chris Sosae67b78f2010-11-04 17:33:16 -0700554 return True
Chris Sosa2c048f12010-10-27 16:05:27 -0700555 else:
556 _LogMessage('Failed to pre-generate update.')
Chris Sosae67b78f2010-11-04 17:33:16 -0700557 return False
Chris Sosa2c048f12010-10-27 16:05:27 -0700558
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700559 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700560 """Handles an update ping from an update client.
561
562 Args:
563 data: xml blob from client.
564 label: optional label for the update.
565 Returns:
566 Update payload message for client.
567 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700568 # Set hostname as the hostname that the client is calling to and set up
569 # the url base.
570 self.hostname = cherrypy.request.base
571 if self.urlbase:
572 static_urlbase = self.urlbase
573 elif self.serve_only:
574 static_urlbase = '%s/static/archive' % self.hostname
575 else:
576 static_urlbase = '%s/static' % self.hostname
577
578 _LogMessage('Using static url base %s' % static_urlbase)
579 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700580
581 # Check the client prefix to make sure you can support this type of update.
Chris Sosa9841e1c2010-10-14 10:51:45 -0700582 update_dom = minidom.parseString(data)
583 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700584 if (root.hasAttribute('updaterversion') and
585 not root.getAttribute('updaterversion').startswith(self.client_prefix)):
Chris Sosa7c931362010-10-11 19:49:01 -0700586 _LogMessage('Got update from unsupported updater:' +
Chris Sosa0356d3b2010-09-16 15:46:22 -0700587 root.getAttribute('updaterversion'))
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700588 return self.GetNoUpdatePayload()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700589
590 # We only generate update payloads for updatecheck requests.
591 update_check = root.getElementsByTagName('o:updatecheck')
592 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700593 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700594 # TODO(sosa): Generate correct non-updatecheck payload to better test
595 # update clients.
596 return self.GetNoUpdatePayload()
597
598 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700599 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800600 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700601 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700602 board_id = (query.hasAttribute('board') and query.getAttribute('board')
603 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700604
Chris Sosa0356d3b2010-09-16 15:46:22 -0700605 # Separate logic as Factory requests have static url's that override
606 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700607 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700608 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700609 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700610 static_image_dir = self.static_dir
611 if label:
612 static_image_dir = os.path.join(static_image_dir, label)
613
Don Garrettf90edf02010-11-16 17:36:14 -0800614 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
615 client_version,
616 static_image_dir)
617 if payload_path:
618 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700619 hash = self._GetHash(filename)
620 sha256 = self._GetSHA256(filename)
621 size = self._GetSize(filename)
622 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700623 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800624 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700625 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800626 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700627
Chris Sosa7c931362010-10-11 19:49:01 -0700628 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700629 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700630 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700631 return self.GetNoUpdatePayload()