blob: 68aaba37ee1b7e50ebe528498c38b218eeac37eb [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
Don Garrett0ad09372010-12-06 16:20:30 -080013import urlparse
Chris Sosa7c931362010-10-11 19:49:01 -070014
Chris Sosa05491b12010-11-08 17:14:16 -080015
Chris Sosa7c931362010-10-11 19:49:01 -070016def _LogMessage(message):
17 cherrypy.log(message, 'UPDATE')
rtc@google.comded22402009-10-26 22:36:21 +000018
Chris Sosa417e55d2011-01-25 16:40:48 -080019UPDATE_FILE = 'update.gz'
20STATEFUL_FILE = 'stateful.tgz'
21CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 15:46:22 -070022
Don Garrett0ad09372010-12-06 16:20:30 -080023
24def _ChangeUrlPort(url, new_port):
25 """Return the URL passed in with a different port"""
26 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
27 host_port = netloc.split(':')
28
29 if len(host_port) == 1:
30 host_port.append(new_port)
31 else:
32 host_port[1] = new_port
33
34 print host_port
35 netloc = "%s:%s" % tuple(host_port)
36
37 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
38
39
rtc@google.com64244662009-11-12 00:52:08 +000040class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -070041 """Class that contains functionality that handles Chrome OS update pings.
42
43 Members:
Dale Curtis723ec472010-11-30 14:06:47 -080044 serve_only: Serve only pre-built updates. static_dir must contain update.gz
45 and stateful.tgz.
Chris Sosa0356d3b2010-09-16 15:46:22 -070046 factory_config: Path to the factory config file if handling factory
47 requests.
48 use_test_image: Use chromiumos_test_image.bin rather than the standard.
49 static_url_base: base URL, other than devserver, for update images.
50 client_prefix: The prefix for the update engine client.
51 forced_image: Path to an image to use for all updates.
Chris Sosa08d55a22011-01-19 16:08:02 -080052 forced_payload: Path to pre-generated payload to serve.
53 port: port to host devserver
54 proxy_port: port of local proxy to tell client to connect to you through.
55 src_image: If specified, creates a delta payload from this image.
56 vm: Set for VM images (doesn't patch kernel)
57 board: board for the image. Needed for pre-generating of updates.
58 copy_to_static_root: Copies images generated from the cache to
59 ~/static.
Chris Sosa0356d3b2010-09-16 15:46:22 -070060 """
rtc@google.comded22402009-10-26 22:36:21 +000061
Sean O'Connor1f7fd362010-04-07 16:34:52 -070062 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Don Garrett0c880e22010-11-17 18:13:37 -080063 factory_config_path=None, client_prefix=None,
64 forced_image=None, forced_payload=None,
Don Garrett0ad09372010-12-06 16:20:30 -080065 port=8080, proxy_port=None, src_image='', vm=False, board=None,
Chris Sosa0f1ec842011-02-14 16:33:22 -080066 copy_to_static_root=True, private_key=None,
Chris Sosae67b78f2010-11-04 17:33:16 -070067 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070068 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070069 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -070070 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -070071 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -070072 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -070073 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -070074 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -070075 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -070076
Chris Sosab63a9282010-09-02 10:43:23 -070077 self.client_prefix = client_prefix
Chris Sosa0356d3b2010-09-16 15:46:22 -070078 self.forced_image = forced_image
Don Garrett0c880e22010-11-17 18:13:37 -080079 self.forced_payload = forced_payload
Chris Sosa62f720b2010-10-26 21:39:48 -070080 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -080081 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -070082 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -070083 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -080084 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-14 16:33:22 -080085 self.private_key = private_key
Don Garrettfff4c322010-11-19 13:37:12 -080086
Chris Sosa417e55d2011-01-25 16:40:48 -080087 # Path to pre-generated file.
88 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070089
Chris Sosa0356d3b2010-09-16 15:46:22 -070090 def _GetSecondsSinceMidnight(self):
91 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070092 now = time.localtime()
93 return now[3] * 3600 + now[4] * 60 + now[5]
94
Chris Sosa0356d3b2010-09-16 15:46:22 -070095 def _GetDefaultBoardID(self):
96 """Returns the default board id stored in .default_board."""
97 board_file = '%s/.default_board' % (self.scripts_dir)
98 try:
99 return open(board_file).read()
100 except IOError:
101 return 'x86-generic'
102
103 def _GetLatestImageDir(self, board_id):
104 """Returns the latest image dir based on shell script."""
105 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
106 return os.popen(cmd).read().strip()
107
108 def _GetVersionFromDir(self, image_dir):
109 """Returns the version of the image based on the name of the directory."""
110 latest_version = os.path.basename(image_dir)
111 return latest_version.split('-')[0]
112
113 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800114 """Returns true if the latest_version is greater than the client_version.
115 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700116 client_tokens = client_version.replace('_', '').split('.')
117 latest_tokens = latest_version.replace('_', '').split('.')
Chris Sosa7c931362010-10-11 19:49:01 -0700118 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -0800119 % (client_version, latest_version))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700120 for i in range(4):
121 if int(latest_tokens[i]) == int(client_tokens[i]):
122 continue
123 return int(latest_tokens[i]) > int(client_tokens[i])
124 return False
125
Chris Sosa0356d3b2010-09-16 15:46:22 -0700126 def _UnpackZip(self, image_dir):
127 """Unpacks an image.zip into a given directory."""
128 image = os.path.join(image_dir, self._GetImageName())
129 if os.path.exists(image):
130 return True
131 else:
132 # -n, never clobber an existing file, in case we get invoked
133 # simultaneously by multiple request handlers. This means that
134 # we're assuming each image.zip file lives in a versioned
135 # directory (a la Buildbot).
136 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
137
138 def _GetImageName(self):
139 """Returns the name of the image that should be used."""
140 if self.use_test_image:
141 image_name = 'chromiumos_test_image.bin'
142 else:
143 image_name = 'chromiumos_image.bin'
144 return image_name
145
Chris Sosa0356d3b2010-09-16 15:46:22 -0700146 def _GetSize(self, update_path):
147 """Returns the size of the file given."""
148 return os.path.getsize(update_path)
149
150 def _GetHash(self, update_path):
151 """Returns the sha1 of the file given."""
152 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
153 % update_path)
154 return os.popen(cmd).read().rstrip()
155
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700156 def _IsDeltaFormatFile(self, filename):
157 try:
158 file_handle = open(filename, 'r')
159 delta_magic = 'CrAU'
160 magic = file_handle.read(len(delta_magic))
161 return magic == delta_magic
162 except Exception:
163 return False
164
Darin Petkov91436cb2010-09-28 08:52:17 -0700165 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
166 # it takes advantage of reduced I/O and multiple processors. Something like:
167 # % tee < FILE > /dev/null \
168 # >( openssl dgst -sha256 -binary | openssl base64 ) \
169 # >( openssl sha1 -binary | openssl base64 )
170 def _GetSHA256(self, update_path):
171 """Returns the sha256 of the file given."""
172 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
173 update_path)
174 return os.popen(cmd).read().rstrip()
175
Don Garrettf90edf02010-11-16 17:36:14 -0800176 def _GetMd5(self, update_path):
177 """Returns the md5 checksum of the file given."""
178 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
179 return os.popen(cmd).read().rstrip()
180
Don Garrett0c880e22010-11-17 18:13:37 -0800181 def _Copy(self, source, dest):
182 """Copies a file from dest to source (if different)"""
183 _LogMessage('Copy File %s -> %s' % (source, dest))
184 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800185 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800186 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800187
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700188 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700189 """Returns a payload to the client corresponding to a new update.
190
191 Args:
192 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700193 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700194 size: size of update blob
195 url: where to find update blob
196 Returns:
197 Xml string to be passed back to client.
198 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700199 delta = 'false'
200 if is_delta_format:
201 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000202 payload = """<?xml version="1.0" encoding="UTF-8"?>
203 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700204 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000205 <app appid="{%s}" status="ok">
206 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700207 <updatecheck
208 codebase="%s"
209 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700210 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700211 needsadmin="false"
212 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700213 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000214 status="ok"/>
215 </app>
216 </gupdate>
217 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700218 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700219 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000220
rtc@google.com21a5ca32009-11-04 18:23:23 +0000221 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700222 """Returns a payload to the client corresponding to no update."""
Darin Petkov845f1172011-01-05 14:45:24 -0800223 payload = """<?xml version="1.0" encoding="UTF-8"?>
224 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
225 <daystart elapsed_seconds="%s"/>
226 <app appid="{%s}" status="ok">
227 <ping status="ok"/>
228 <updatecheck status="noupdate"/>
229 </app>
230 </gupdate>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000231 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700232 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000233
Don Garrettf90edf02010-11-16 17:36:14 -0800234 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700235 """Generates an update gz given a full path to an image.
236
237 Args:
238 image_path: Full path to image.
239 Returns:
240 Path to created update_payload or None on error.
241 """
Don Garrettfff4c322010-11-19 13:37:12 -0800242 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa7c931362010-10-11 19:49:01 -0700243 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700244
Chris Sosa0f1ec842011-02-14 16:33:22 -0800245 update_command = [
246 '%s/cros_generate_update_payload' % self.scripts_dir,
247 '--image="%s"' % image_path,
248 '--output="%s"' % update_path,
249 '--noold_style',
250 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700251
Chris Sosa0f1ec842011-02-14 16:33:22 -0800252 if src_image: update_command.append('--src_image="%s"' % src_image)
253 if not self.vm: update_command.append('--patch_kernel')
254 if self.private_key: update_command.append('--private_key="%s"' %
255 self.private_key)
256
257 update_string = ' '.join(update_command)
258 _LogMessage('Running ' + update_string)
259 if os.system(update_string) != 0:
Chris Sosa417e55d2011-01-25 16:40:48 -0800260 _LogMessage('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700261 return None
262
Don Garrettfff4c322010-11-19 13:37:12 -0800263 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700264
Don Garrettf90edf02010-11-16 17:36:14 -0800265 def GenerateStatefulFile(self, image_path, output_dir):
266 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700267
268 Args:
269 image_path: Full path to image.
270 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800271 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800272 Raises:
273 A subprocess exception if the update generator fails to generate a
274 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700275 """
Don Garrettfff4c322010-11-19 13:37:12 -0800276 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800277 subprocess.check_call(
Don Garrettfff4c322010-11-19 13:37:12 -0800278 ['%s/cros_generate_stateful_update_payload' % self.scripts_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800279 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800280 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800281 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800282 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700283
Don Garrettf90edf02010-11-16 17:36:14 -0800284 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
285 """Find directory to store a cached update.
286
287 Given one, or two images for an update, this finds which
288 cache directory should hold the update files, even if they don't exist
289 yet. The directory will be inside static_image_dir, and of the form:
290
291 Non-delta updates:
Chris Sosa417e55d2011-01-25 16:40:48 -0800292 CACHE_DIR/12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800293
294 Delta updates:
Chris Sosa417e55d2011-01-25 16:40:48 -0800295 CACHE_DIR/12345678_12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800296 """
297 # If there is no src, we only have an image file, check image for changes
298 if not src_image:
Chris Sosa417e55d2011-01-25 16:40:48 -0800299 return os.path.join(CACHE_DIR, self._GetMd5(dest_image))
Don Garrettf90edf02010-11-16 17:36:14 -0800300
301 # If we have src and dest, we are a delta, and check both for changes
Chris Sosa417e55d2011-01-25 16:40:48 -0800302 return os.path.join(CACHE_DIR,
Don Garrettf90edf02010-11-16 17:36:14 -0800303 "%s_%s" % (self._GetMd5(src_image),
304 self._GetMd5(dest_image)))
305
Don Garrettfff4c322010-11-19 13:37:12 -0800306 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800307 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700308
Chris Sosade91f672010-11-16 10:05:44 -0800309 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800310 src_image: image we are updating from (Null/empty for non-delta)
311 image_path: full path to the image.
312 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800313 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800314 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800315 """
Don Garrettf90edf02010-11-16 17:36:14 -0800316 update_file = None
317 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700318
Don Garrettf90edf02010-11-16 17:36:14 -0800319 # Actually do the generation
320 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800321 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800322 image_path,
323 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000324
Don Garrettf90edf02010-11-16 17:36:14 -0800325 if update_file:
326 stateful_update_file = self.GenerateStatefulFile(image_path,
327 output_dir)
328
329 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800330 return update_file
Chris Sosa417e55d2011-01-25 16:40:48 -0800331 else:
332 _LogMessage('Failed to generate update.')
333 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800334
335 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
336 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000337
Chris Sosa0356d3b2010-09-16 15:46:22 -0700338 Args:
339 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700340 static_image_dir: the directory to move images to after generating.
341 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800342 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-25 16:40:48 -0800343 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700344 """
Don Garrettf90edf02010-11-16 17:36:14 -0800345 _LogMessage('Generating update for src %s image %s' % (self.src_image,
346 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700347
Chris Sosa417e55d2011-01-25 16:40:48 -0800348 # If it was pregenerated_path, don't regenerate
349 if self.pregenerated_path:
350 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800351
Don Garrettf90edf02010-11-16 17:36:14 -0800352 # Which sub_dir of static_image_dir should hold our cached update image
353 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
354 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
355
Chris Sosa417e55d2011-01-25 16:40:48 -0800356 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
357
Don Garrettf90edf02010-11-16 17:36:14 -0800358 # The cached payloads exist in a cache dir
359 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-25 16:40:48 -0800360 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800361 cache_stateful_payload = os.path.join(static_image_dir,
362 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800363 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800364
Chris Sosa417e55d2011-01-25 16:40:48 -0800365 # Check to see if this cache directory is valid.
366 if not os.path.exists(cache_update_payload) or not os.path.exists(
367 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800368 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800369 # Clean up stale state.
370 os.system('rm -rf "%s"' % full_cache_dir)
371 os.makedirs(full_cache_dir)
372 return_path = self.GenerateUpdateImage(image_path,
373 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800374
Chris Sosa417e55d2011-01-25 16:40:48 -0800375 # Clean up cache dir since it's not valid.
376 if not return_path:
377 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800378 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800379 else:
380 assert (return_path == update_path,
381 'Returned path %s not equal to %s' % (return_path, update_path))
382
383 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800384
Chris Sosa08d55a22011-01-19 16:08:02 -0800385 # Generation complete, copy if requested.
386 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800387 # The final results exist directly in static
388 update_payload = os.path.join(static_image_dir,
389 UPDATE_FILE)
390 stateful_payload = os.path.join(static_image_dir,
391 STATEFUL_FILE)
Chris Sosa08d55a22011-01-19 16:08:02 -0800392 self._Copy(cache_update_payload, update_payload)
393 self._Copy(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-25 16:40:48 -0800394 return UPDATE_FILE
395 else:
396 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700397
398 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800399 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700400 """Generates an update using the latest image that has been built.
401
402 This will only generate an update if the newest update is newer than that
403 on the client or client_version is 'ForcedUpdate'.
404
405 Args:
406 board_id: Name of the board.
407 client_version: Current version of the client or 'ForcedUpdate'
408 static_image_dir: the directory to move images to after generating.
409 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800410 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700411 """
412 latest_image_dir = self._GetLatestImageDir(board_id)
413 latest_version = self._GetVersionFromDir(latest_image_dir)
414 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
415
Chris Sosa7c931362010-10-11 19:49:01 -0700416 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700417 latest_image_path)
418
419 # Check to see whether or not we should update.
420 if client_version != 'ForcedUpdate' and not self._CanUpdate(
421 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700422 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800423 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700424
Don Garrettf90edf02010-11-16 17:36:14 -0800425 return self.GenerateUpdateImageWithCache(latest_image_path,
426 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700427
Andrew de los Reyes52620802010-04-12 13:40:07 -0700428 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
429 """Imports a factory-floor server configuration file. The file should
430 be in this format:
431 config = [
432 {
433 'qual_ids': set([1, 2, 3, "x86-generic"]),
434 'factory_image': 'generic-factory.gz',
435 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
436 'release_image': 'generic-release.gz',
437 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
438 'oempartitionimg_image': 'generic-oem.gz',
439 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700440 'efipartitionimg_image': 'generic-efi.gz',
441 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700442 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800443 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800444 'firmware_image': 'generic-firmware.gz',
445 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700446 },
447 {
448 'qual_ids': set([6]),
449 'factory_image': '6-factory.gz',
450 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
451 'release_image': '6-release.gz',
452 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
453 'oempartitionimg_image': '6-oem.gz',
454 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700455 'efipartitionimg_image': '6-efi.gz',
456 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700457 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800458 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800459 'firmware_image': '6-firmware.gz',
460 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700461 },
462 ]
463 The server will look for the files by name in the static files
464 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700465
Andrew de los Reyes52620802010-04-12 13:40:07 -0700466 If validate_checksums is True, validates checksums and exits. If
467 a checksum mismatch is found, it's printed to the screen.
468 """
469 f = open(filename, 'r')
470 output = {}
471 exec(f.read(), output)
472 self.factory_config = output['config']
473 success = True
474 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800475 for key in stanza.copy().iterkeys():
476 suffix = '_image'
477 if key.endswith(suffix):
478 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700479 stanza[kind + '_size'] = self._GetSize(os.path.join(
480 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800481 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700482 factory_checksum = self._GetHash(os.path.join(self.static_dir,
483 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800484 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700485 print ('Error: checksum mismatch for %s. Expected "%s" but file '
486 'has checksum "%s".' % (stanza[kind + '_image'],
487 stanza[kind + '_checksum'],
488 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800489 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700490
Andrew de los Reyes52620802010-04-12 13:40:07 -0700491 if validate_checksums:
492 if success is False:
493 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700494
Andrew de los Reyes52620802010-04-12 13:40:07 -0700495 print 'Config file looks good.'
496
497 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700498 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700499 for stanza in self.factory_config:
500 if board_id not in stanza['qual_ids']:
501 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700502 if kind + '_image' not in stanza:
503 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700504 return (stanza[kind + '_image'],
505 stanza[kind + '_checksum'],
506 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700507 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000508
Chris Sosa7c931362010-10-11 19:49:01 -0700509 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700510 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
511 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700512 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700513 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700514 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700515 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700516 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700517 # Factory install is using memento updater which is using the sha-1 hash so
518 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700519 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700520
Chris Sosa151643e2010-10-28 14:40:57 -0700521 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
522 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800523 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800524
Don Garrettf90edf02010-11-16 17:36:14 -0800525 Returns:
526 file name relative to static_image_dir on success.
527 """
Dale Curtis723ec472010-11-30 14:06:47 -0800528 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
529 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
530
Don Garrett0c880e22010-11-17 18:13:37 -0800531 if self.forced_payload:
532 # If the forced payload is not already in our static_image_dir,
533 # copy it there.
Don Garrettee25e552010-11-23 12:09:35 -0800534 src_path = os.path.abspath(self.forced_payload)
Don Garrettee25e552010-11-23 12:09:35 -0800535 src_stateful = os.path.join(os.path.dirname(src_path),
536 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800537
538 # Only copy the files if the source directory is different from dest.
539 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
540 self._Copy(src_path, dest_path)
541
542 # The stateful payload is optional.
543 if os.path.exists(src_stateful):
544 self._Copy(src_stateful, dest_stateful)
545 else:
546 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
547 STATEFUL_FILE)
548 if os.path.exists(dest_stateful):
549 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800550
Don Garrettfff4c322010-11-19 13:37:12 -0800551 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800552 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800553 return self.GenerateUpdateImageWithCache(
554 self.forced_image,
555 static_image_dir=static_image_dir)
556 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800557 # Warn if update or stateful files can't be found.
558 if not os.path.exists(dest_path):
559 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
560 UPDATE_FILE)
561
562 if not os.path.exists(dest_stateful):
563 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
564 STATEFUL_FILE)
565
566 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800567 else:
568 if board_id:
569 return self.GenerateLatestUpdateImage(board_id,
570 client_version,
571 static_image_dir)
572
Chris Sosa417e55d2011-01-25 16:40:48 -0800573 _LogMessage('Failed to genereate update. '
574 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800575 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700576
577 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800578 """Pre-generates an update and prints out the relative path it.
579
580 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800581 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700582 # Does not work with factory config.
583 assert(not self.factory_config)
584 _LogMessage('Pre-generating the update payload.')
585 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-25 16:40:48 -0800586 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
587 self.board, '0.0.0.0', self.static_dir)
588 if pregenerated_update:
589 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
590
591 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700592
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700593 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700594 """Handles an update ping from an update client.
595
596 Args:
597 data: xml blob from client.
598 label: optional label for the update.
599 Returns:
600 Update payload message for client.
601 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700602 # Set hostname as the hostname that the client is calling to and set up
603 # the url base.
604 self.hostname = cherrypy.request.base
605 if self.urlbase:
606 static_urlbase = self.urlbase
607 elif self.serve_only:
608 static_urlbase = '%s/static/archive' % self.hostname
609 else:
610 static_urlbase = '%s/static' % self.hostname
611
Don Garrett0ad09372010-12-06 16:20:30 -0800612 # If we have a proxy port, adjust the URL we instruct the client to
613 # use to go through the proxy.
614 if self.proxy_port:
615 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
616
Chris Sosa9841e1c2010-10-14 10:51:45 -0700617 _LogMessage('Using static url base %s' % static_urlbase)
618 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700619
620 # Check the client prefix to make sure you can support this type of update.
Chris Sosa9841e1c2010-10-14 10:51:45 -0700621 update_dom = minidom.parseString(data)
622 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700623 if (root.hasAttribute('updaterversion') and
624 not root.getAttribute('updaterversion').startswith(self.client_prefix)):
Chris Sosa7c931362010-10-11 19:49:01 -0700625 _LogMessage('Got update from unsupported updater:' +
Chris Sosa0356d3b2010-09-16 15:46:22 -0700626 root.getAttribute('updaterversion'))
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700627 return self.GetNoUpdatePayload()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700628
629 # We only generate update payloads for updatecheck requests.
630 update_check = root.getElementsByTagName('o:updatecheck')
631 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700632 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700633 # TODO(sosa): Generate correct non-updatecheck payload to better test
634 # update clients.
635 return self.GetNoUpdatePayload()
636
637 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700638 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800639 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700640 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700641 board_id = (query.hasAttribute('board') and query.getAttribute('board')
642 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700643
Chris Sosa0356d3b2010-09-16 15:46:22 -0700644 # Separate logic as Factory requests have static url's that override
645 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700646 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700647 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700648 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700649 static_image_dir = self.static_dir
650 if label:
651 static_image_dir = os.path.join(static_image_dir, label)
652
Don Garrettf90edf02010-11-16 17:36:14 -0800653 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
654 client_version,
655 static_image_dir)
656 if payload_path:
657 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700658 hash = self._GetHash(filename)
659 sha256 = self._GetSHA256(filename)
660 size = self._GetSize(filename)
661 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700662 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800663 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700664 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800665 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700666
Chris Sosa7c931362010-10-11 19:49:01 -0700667 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700668 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700669 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700670 return self.GetNoUpdatePayload()