blob: d1a0126ac7a03ca292b0fa39e93d0d69d92bd9e4 [file] [log] [blame]
Darin Petkovc3fd90c2011-05-11 14:23:00 -07001# Copyright (c) 2011 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.
Chris Sosa0356d3b2010-09-16 15:46:22 -070050 forced_image: Path to an image to use for all updates.
Chris Sosa08d55a22011-01-19 16:08:02 -080051 forced_payload: Path to pre-generated payload to serve.
52 port: port to host devserver
53 proxy_port: port of local proxy to tell client to connect to you through.
54 src_image: If specified, creates a delta payload from this image.
55 vm: Set for VM images (doesn't patch kernel)
56 board: board for the image. Needed for pre-generating of updates.
57 copy_to_static_root: Copies images generated from the cache to
58 ~/static.
Chris Sosa0356d3b2010-09-16 15:46:22 -070059 """
rtc@google.comded22402009-10-26 22:36:21 +000060
Sean O'Connor1f7fd362010-04-07 16:34:52 -070061 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Greg Spencerc8b59b22011-03-15 14:15:23 -070062 factory_config_path=None,
Don Garrett0c880e22010-11-17 18:13:37 -080063 forced_image=None, forced_payload=None,
Don Garrett0ad09372010-12-06 16:20:30 -080064 port=8080, proxy_port=None, src_image='', vm=False, board=None,
Chris Sosa0f1ec842011-02-14 16:33:22 -080065 copy_to_static_root=True, private_key=None,
Chris Sosae67b78f2010-11-04 17:33:16 -070066 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070067 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070068 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -070069 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -070070 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -070071 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -070072 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -070073 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -070074 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -070075
Chris Sosa0356d3b2010-09-16 15:46:22 -070076 self.forced_image = forced_image
Don Garrett0c880e22010-11-17 18:13:37 -080077 self.forced_payload = forced_payload
Chris Sosa62f720b2010-10-26 21:39:48 -070078 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -080079 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -070080 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -070081 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -080082 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-14 16:33:22 -080083 self.private_key = private_key
Don Garrettfff4c322010-11-19 13:37:12 -080084
Chris Sosa417e55d2011-01-25 16:40:48 -080085 # Path to pre-generated file.
86 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070087
Chris Sosa0356d3b2010-09-16 15:46:22 -070088 def _GetSecondsSinceMidnight(self):
89 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070090 now = time.localtime()
91 return now[3] * 3600 + now[4] * 60 + now[5]
92
Chris Sosa0356d3b2010-09-16 15:46:22 -070093 def _GetDefaultBoardID(self):
94 """Returns the default board id stored in .default_board."""
95 board_file = '%s/.default_board' % (self.scripts_dir)
96 try:
97 return open(board_file).read()
98 except IOError:
99 return 'x86-generic'
100
101 def _GetLatestImageDir(self, board_id):
102 """Returns the latest image dir based on shell script."""
103 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
104 return os.popen(cmd).read().strip()
105
106 def _GetVersionFromDir(self, image_dir):
107 """Returns the version of the image based on the name of the directory."""
108 latest_version = os.path.basename(image_dir)
109 return latest_version.split('-')[0]
110
111 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800112 """Returns true if the latest_version is greater than the client_version.
113 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700114 client_tokens = client_version.replace('_', '').split('.')
115 latest_tokens = latest_version.replace('_', '').split('.')
Chris Sosa7c931362010-10-11 19:49:01 -0700116 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -0800117 % (client_version, latest_version))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700118 for i in range(4):
119 if int(latest_tokens[i]) == int(client_tokens[i]):
120 continue
121 return int(latest_tokens[i]) > int(client_tokens[i])
122 return False
123
Chris Sosa0356d3b2010-09-16 15:46:22 -0700124 def _UnpackZip(self, image_dir):
125 """Unpacks an image.zip into a given directory."""
126 image = os.path.join(image_dir, self._GetImageName())
127 if os.path.exists(image):
128 return True
129 else:
130 # -n, never clobber an existing file, in case we get invoked
131 # simultaneously by multiple request handlers. This means that
132 # we're assuming each image.zip file lives in a versioned
133 # directory (a la Buildbot).
134 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
135
136 def _GetImageName(self):
137 """Returns the name of the image that should be used."""
138 if self.use_test_image:
139 image_name = 'chromiumos_test_image.bin'
140 else:
141 image_name = 'chromiumos_image.bin'
142 return image_name
143
Chris Sosa0356d3b2010-09-16 15:46:22 -0700144 def _GetSize(self, update_path):
145 """Returns the size of the file given."""
146 return os.path.getsize(update_path)
147
148 def _GetHash(self, update_path):
149 """Returns the sha1 of the file given."""
150 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
151 % update_path)
152 return os.popen(cmd).read().rstrip()
153
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700154 def _IsDeltaFormatFile(self, filename):
155 try:
156 file_handle = open(filename, 'r')
157 delta_magic = 'CrAU'
158 magic = file_handle.read(len(delta_magic))
159 return magic == delta_magic
160 except Exception:
161 return False
162
Darin Petkov91436cb2010-09-28 08:52:17 -0700163 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
164 # it takes advantage of reduced I/O and multiple processors. Something like:
165 # % tee < FILE > /dev/null \
166 # >( openssl dgst -sha256 -binary | openssl base64 ) \
167 # >( openssl sha1 -binary | openssl base64 )
168 def _GetSHA256(self, update_path):
169 """Returns the sha256 of the file given."""
170 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
171 update_path)
172 return os.popen(cmd).read().rstrip()
173
Don Garrettf90edf02010-11-16 17:36:14 -0800174 def _GetMd5(self, update_path):
175 """Returns the md5 checksum of the file given."""
176 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
177 return os.popen(cmd).read().rstrip()
178
Don Garrett0c880e22010-11-17 18:13:37 -0800179 def _Copy(self, source, dest):
180 """Copies a file from dest to source (if different)"""
181 _LogMessage('Copy File %s -> %s' % (source, dest))
182 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800183 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800184 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800185
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700186 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700187 """Returns a payload to the client corresponding to a new update.
188
189 Args:
190 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700191 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700192 size: size of update blob
193 url: where to find update blob
194 Returns:
195 Xml string to be passed back to client.
196 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700197 delta = 'false'
198 if is_delta_format:
199 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000200 payload = """<?xml version="1.0" encoding="UTF-8"?>
201 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700202 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000203 <app appid="{%s}" status="ok">
204 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700205 <updatecheck
206 codebase="%s"
207 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700208 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700209 needsadmin="false"
210 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700211 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000212 status="ok"/>
213 </app>
214 </gupdate>
215 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700216 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700217 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000218
rtc@google.com21a5ca32009-11-04 18:23:23 +0000219 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700220 """Returns a payload to the client corresponding to no update."""
Darin Petkov845f1172011-01-05 14:45:24 -0800221 payload = """<?xml version="1.0" encoding="UTF-8"?>
222 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
223 <daystart elapsed_seconds="%s"/>
224 <app appid="{%s}" status="ok">
225 <ping status="ok"/>
226 <updatecheck status="noupdate"/>
227 </app>
228 </gupdate>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000229 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700230 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000231
Don Garrettf90edf02010-11-16 17:36:14 -0800232 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700233 """Generates an update gz given a full path to an image.
234
235 Args:
236 image_path: Full path to image.
237 Returns:
238 Path to created update_payload or None on error.
239 """
Don Garrettfff4c322010-11-19 13:37:12 -0800240 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa7c931362010-10-11 19:49:01 -0700241 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700242
Chris Sosa0f1ec842011-02-14 16:33:22 -0800243 update_command = [
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100244 '%s/cros_generate_update_payload' % self.devserver_dir,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800245 '--image="%s"' % image_path,
246 '--output="%s"' % update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800247 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700248
Chris Sosa0f1ec842011-02-14 16:33:22 -0800249 if src_image: update_command.append('--src_image="%s"' % src_image)
250 if not self.vm: update_command.append('--patch_kernel')
251 if self.private_key: update_command.append('--private_key="%s"' %
252 self.private_key)
253
254 update_string = ' '.join(update_command)
255 _LogMessage('Running ' + update_string)
256 if os.system(update_string) != 0:
Chris Sosa417e55d2011-01-25 16:40:48 -0800257 _LogMessage('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700258 return None
259
Don Garrettfff4c322010-11-19 13:37:12 -0800260 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700261
Don Garrettf90edf02010-11-16 17:36:14 -0800262 def GenerateStatefulFile(self, image_path, output_dir):
263 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700264
265 Args:
266 image_path: Full path to image.
267 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800268 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800269 Raises:
270 A subprocess exception if the update generator fails to generate a
271 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700272 """
Don Garrettfff4c322010-11-19 13:37:12 -0800273 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800274 subprocess.check_call(
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100275 ['%s/cros_generate_stateful_update_payload' % self.devserver_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800276 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800277 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800278 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800279 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700280
Don Garrettf90edf02010-11-16 17:36:14 -0800281 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
282 """Find directory to store a cached update.
283
284 Given one, or two images for an update, this finds which
285 cache directory should hold the update files, even if they don't exist
286 yet. The directory will be inside static_image_dir, and of the form:
287
288 Non-delta updates:
Chris Sosa417e55d2011-01-25 16:40:48 -0800289 CACHE_DIR/12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800290
291 Delta updates:
Chris Sosa417e55d2011-01-25 16:40:48 -0800292 CACHE_DIR/12345678_12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800293 """
294 # If there is no src, we only have an image file, check image for changes
295 if not src_image:
Chris Sosa417e55d2011-01-25 16:40:48 -0800296 return os.path.join(CACHE_DIR, self._GetMd5(dest_image))
Don Garrettf90edf02010-11-16 17:36:14 -0800297
298 # If we have src and dest, we are a delta, and check both for changes
Chris Sosa417e55d2011-01-25 16:40:48 -0800299 return os.path.join(CACHE_DIR,
Don Garrettf90edf02010-11-16 17:36:14 -0800300 "%s_%s" % (self._GetMd5(src_image),
301 self._GetMd5(dest_image)))
302
Don Garrettfff4c322010-11-19 13:37:12 -0800303 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800304 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700305
Chris Sosade91f672010-11-16 10:05:44 -0800306 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800307 src_image: image we are updating from (Null/empty for non-delta)
308 image_path: full path to the image.
309 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800310 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800311 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800312 """
Don Garrettf90edf02010-11-16 17:36:14 -0800313 update_file = None
314 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700315
Don Garrettf90edf02010-11-16 17:36:14 -0800316 # Actually do the generation
317 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800318 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800319 image_path,
320 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000321
Don Garrettf90edf02010-11-16 17:36:14 -0800322 if update_file:
323 stateful_update_file = self.GenerateStatefulFile(image_path,
324 output_dir)
325
326 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800327 return update_file
Chris Sosa417e55d2011-01-25 16:40:48 -0800328 else:
329 _LogMessage('Failed to generate update.')
330 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800331
332 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
333 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000334
Chris Sosa0356d3b2010-09-16 15:46:22 -0700335 Args:
336 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700337 static_image_dir: the directory to move images to after generating.
338 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800339 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-25 16:40:48 -0800340 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700341 """
Don Garrettf90edf02010-11-16 17:36:14 -0800342 _LogMessage('Generating update for src %s image %s' % (self.src_image,
343 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700344
Chris Sosa417e55d2011-01-25 16:40:48 -0800345 # If it was pregenerated_path, don't regenerate
346 if self.pregenerated_path:
347 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800348
Don Garrettf90edf02010-11-16 17:36:14 -0800349 # Which sub_dir of static_image_dir should hold our cached update image
350 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
351 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
352
Chris Sosa417e55d2011-01-25 16:40:48 -0800353 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
354
Don Garrettf90edf02010-11-16 17:36:14 -0800355 # The cached payloads exist in a cache dir
356 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-25 16:40:48 -0800357 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800358 cache_stateful_payload = os.path.join(static_image_dir,
359 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800360 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800361
Chris Sosa417e55d2011-01-25 16:40:48 -0800362 # Check to see if this cache directory is valid.
363 if not os.path.exists(cache_update_payload) or not os.path.exists(
364 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800365 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800366 # Clean up stale state.
367 os.system('rm -rf "%s"' % full_cache_dir)
368 os.makedirs(full_cache_dir)
369 return_path = self.GenerateUpdateImage(image_path,
370 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800371
Chris Sosa417e55d2011-01-25 16:40:48 -0800372 # Clean up cache dir since it's not valid.
373 if not return_path:
374 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800375 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800376
377 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800378
Chris Sosa08d55a22011-01-19 16:08:02 -0800379 # Generation complete, copy if requested.
380 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800381 # The final results exist directly in static
382 update_payload = os.path.join(static_image_dir,
383 UPDATE_FILE)
384 stateful_payload = os.path.join(static_image_dir,
385 STATEFUL_FILE)
Chris Sosa08d55a22011-01-19 16:08:02 -0800386 self._Copy(cache_update_payload, update_payload)
387 self._Copy(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-25 16:40:48 -0800388 return UPDATE_FILE
389 else:
390 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700391
392 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800393 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700394 """Generates an update using the latest image that has been built.
395
396 This will only generate an update if the newest update is newer than that
397 on the client or client_version is 'ForcedUpdate'.
398
399 Args:
400 board_id: Name of the board.
401 client_version: Current version of the client or 'ForcedUpdate'
402 static_image_dir: the directory to move images to after generating.
403 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800404 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700405 """
406 latest_image_dir = self._GetLatestImageDir(board_id)
407 latest_version = self._GetVersionFromDir(latest_image_dir)
408 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
409
Chris Sosa7c931362010-10-11 19:49:01 -0700410 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700411 latest_image_path)
412
413 # Check to see whether or not we should update.
414 if client_version != 'ForcedUpdate' and not self._CanUpdate(
415 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700416 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800417 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700418
Don Garrettf90edf02010-11-16 17:36:14 -0800419 return self.GenerateUpdateImageWithCache(latest_image_path,
420 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700421
Andrew de los Reyes52620802010-04-12 13:40:07 -0700422 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
423 """Imports a factory-floor server configuration file. The file should
424 be in this format:
425 config = [
426 {
427 'qual_ids': set([1, 2, 3, "x86-generic"]),
428 'factory_image': 'generic-factory.gz',
429 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
430 'release_image': 'generic-release.gz',
431 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
432 'oempartitionimg_image': 'generic-oem.gz',
433 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700434 'efipartitionimg_image': 'generic-efi.gz',
435 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700436 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800437 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800438 'firmware_image': 'generic-firmware.gz',
439 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700440 },
441 {
442 'qual_ids': set([6]),
443 'factory_image': '6-factory.gz',
444 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
445 'release_image': '6-release.gz',
446 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
447 'oempartitionimg_image': '6-oem.gz',
448 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700449 'efipartitionimg_image': '6-efi.gz',
450 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700451 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800452 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800453 'firmware_image': '6-firmware.gz',
454 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700455 },
456 ]
457 The server will look for the files by name in the static files
458 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700459
Andrew de los Reyes52620802010-04-12 13:40:07 -0700460 If validate_checksums is True, validates checksums and exits. If
461 a checksum mismatch is found, it's printed to the screen.
462 """
463 f = open(filename, 'r')
464 output = {}
465 exec(f.read(), output)
466 self.factory_config = output['config']
467 success = True
468 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800469 for key in stanza.copy().iterkeys():
470 suffix = '_image'
471 if key.endswith(suffix):
472 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700473 stanza[kind + '_size'] = self._GetSize(os.path.join(
474 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800475 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700476 factory_checksum = self._GetHash(os.path.join(self.static_dir,
477 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800478 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700479 print ('Error: checksum mismatch for %s. Expected "%s" but file '
480 'has checksum "%s".' % (stanza[kind + '_image'],
481 stanza[kind + '_checksum'],
482 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800483 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700484
Andrew de los Reyes52620802010-04-12 13:40:07 -0700485 if validate_checksums:
486 if success is False:
487 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700488
Andrew de los Reyes52620802010-04-12 13:40:07 -0700489 print 'Config file looks good.'
490
491 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700492 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700493 for stanza in self.factory_config:
494 if board_id not in stanza['qual_ids']:
495 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700496 if kind + '_image' not in stanza:
497 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700498 return (stanza[kind + '_image'],
499 stanza[kind + '_checksum'],
500 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700501 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000502
Chris Sosa7c931362010-10-11 19:49:01 -0700503 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700504 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
505 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700506 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700507 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700508 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700509 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700510 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700511 # Factory install is using memento updater which is using the sha-1 hash so
512 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700513 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700514
Chris Sosa151643e2010-10-28 14:40:57 -0700515 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
516 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800517 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800518
Don Garrettf90edf02010-11-16 17:36:14 -0800519 Returns:
520 file name relative to static_image_dir on success.
521 """
Dale Curtis723ec472010-11-30 14:06:47 -0800522 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
523 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
524
Don Garrett0c880e22010-11-17 18:13:37 -0800525 if self.forced_payload:
526 # If the forced payload is not already in our static_image_dir,
527 # copy it there.
Don Garrettee25e552010-11-23 12:09:35 -0800528 src_path = os.path.abspath(self.forced_payload)
Don Garrettee25e552010-11-23 12:09:35 -0800529 src_stateful = os.path.join(os.path.dirname(src_path),
530 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800531
532 # Only copy the files if the source directory is different from dest.
533 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
534 self._Copy(src_path, dest_path)
535
536 # The stateful payload is optional.
537 if os.path.exists(src_stateful):
538 self._Copy(src_stateful, dest_stateful)
539 else:
540 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
541 STATEFUL_FILE)
542 if os.path.exists(dest_stateful):
543 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800544
Don Garrettfff4c322010-11-19 13:37:12 -0800545 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800546 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800547 return self.GenerateUpdateImageWithCache(
548 self.forced_image,
549 static_image_dir=static_image_dir)
550 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800551 # Warn if update or stateful files can't be found.
552 if not os.path.exists(dest_path):
553 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
554 UPDATE_FILE)
555
556 if not os.path.exists(dest_stateful):
557 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
558 STATEFUL_FILE)
559
560 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800561 else:
562 if board_id:
563 return self.GenerateLatestUpdateImage(board_id,
564 client_version,
565 static_image_dir)
566
Chris Sosa417e55d2011-01-25 16:40:48 -0800567 _LogMessage('Failed to genereate update. '
568 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800569 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700570
571 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800572 """Pre-generates an update and prints out the relative path it.
573
574 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800575 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700576 # Does not work with factory config.
577 assert(not self.factory_config)
578 _LogMessage('Pre-generating the update payload.')
579 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-25 16:40:48 -0800580 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
581 self.board, '0.0.0.0', self.static_dir)
582 if pregenerated_update:
583 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
584
585 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700586
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700587 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700588 """Handles an update ping from an update client.
589
590 Args:
591 data: xml blob from client.
592 label: optional label for the update.
593 Returns:
594 Update payload message for client.
595 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700596 # Set hostname as the hostname that the client is calling to and set up
597 # the url base.
598 self.hostname = cherrypy.request.base
599 if self.urlbase:
600 static_urlbase = self.urlbase
601 elif self.serve_only:
602 static_urlbase = '%s/static/archive' % self.hostname
603 else:
604 static_urlbase = '%s/static' % self.hostname
605
Don Garrett0ad09372010-12-06 16:20:30 -0800606 # If we have a proxy port, adjust the URL we instruct the client to
607 # use to go through the proxy.
608 if self.proxy_port:
609 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
610
Chris Sosa9841e1c2010-10-14 10:51:45 -0700611 _LogMessage('Using static url base %s' % static_urlbase)
612 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700613
Chris Sosa9841e1c2010-10-14 10:51:45 -0700614 update_dom = minidom.parseString(data)
615 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700616
617 # We only generate update payloads for updatecheck requests.
618 update_check = root.getElementsByTagName('o:updatecheck')
619 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700620 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700621 # TODO(sosa): Generate correct non-updatecheck payload to better test
622 # update clients.
623 return self.GetNoUpdatePayload()
624
625 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700626 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800627 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700628 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700629 board_id = (query.hasAttribute('board') and query.getAttribute('board')
630 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700631
Chris Sosa0356d3b2010-09-16 15:46:22 -0700632 # Separate logic as Factory requests have static url's that override
633 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700634 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700635 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700636 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700637 static_image_dir = self.static_dir
638 if label:
639 static_image_dir = os.path.join(static_image_dir, label)
640
Don Garrettf90edf02010-11-16 17:36:14 -0800641 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
642 client_version,
643 static_image_dir)
644 if payload_path:
645 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700646 hash = self._GetHash(filename)
647 sha256 = self._GetSHA256(filename)
648 size = self._GetSize(filename)
649 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700650 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800651 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700652 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800653 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700654
Chris Sosa7c931362010-10-11 19:49:01 -0700655 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700656 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700657 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700658 return self.GetNoUpdatePayload()