blob: 4647db3fc743bfb005eaceed43a4053d4694e1d7 [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.
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,
247 '--noold_style',
248 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700249
Chris Sosa0f1ec842011-02-14 16:33:22 -0800250 if src_image: update_command.append('--src_image="%s"' % src_image)
251 if not self.vm: update_command.append('--patch_kernel')
252 if self.private_key: update_command.append('--private_key="%s"' %
253 self.private_key)
254
255 update_string = ' '.join(update_command)
256 _LogMessage('Running ' + update_string)
257 if os.system(update_string) != 0:
Chris Sosa417e55d2011-01-25 16:40:48 -0800258 _LogMessage('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700259 return None
260
Don Garrettfff4c322010-11-19 13:37:12 -0800261 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700262
Don Garrettf90edf02010-11-16 17:36:14 -0800263 def GenerateStatefulFile(self, image_path, output_dir):
264 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700265
266 Args:
267 image_path: Full path to image.
268 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800269 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800270 Raises:
271 A subprocess exception if the update generator fails to generate a
272 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700273 """
Don Garrettfff4c322010-11-19 13:37:12 -0800274 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800275 subprocess.check_call(
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100276 ['%s/cros_generate_stateful_update_payload' % self.devserver_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800277 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800278 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800279 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800280 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700281
Don Garrettf90edf02010-11-16 17:36:14 -0800282 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
283 """Find directory to store a cached update.
284
285 Given one, or two images for an update, this finds which
286 cache directory should hold the update files, even if they don't exist
287 yet. The directory will be inside static_image_dir, and of the form:
288
289 Non-delta updates:
Chris Sosa417e55d2011-01-25 16:40:48 -0800290 CACHE_DIR/12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800291
292 Delta updates:
Chris Sosa417e55d2011-01-25 16:40:48 -0800293 CACHE_DIR/12345678_12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800294 """
295 # If there is no src, we only have an image file, check image for changes
296 if not src_image:
Chris Sosa417e55d2011-01-25 16:40:48 -0800297 return os.path.join(CACHE_DIR, self._GetMd5(dest_image))
Don Garrettf90edf02010-11-16 17:36:14 -0800298
299 # If we have src and dest, we are a delta, and check both for changes
Chris Sosa417e55d2011-01-25 16:40:48 -0800300 return os.path.join(CACHE_DIR,
Don Garrettf90edf02010-11-16 17:36:14 -0800301 "%s_%s" % (self._GetMd5(src_image),
302 self._GetMd5(dest_image)))
303
Don Garrettfff4c322010-11-19 13:37:12 -0800304 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800305 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700306
Chris Sosade91f672010-11-16 10:05:44 -0800307 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800308 src_image: image we are updating from (Null/empty for non-delta)
309 image_path: full path to the image.
310 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800311 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800312 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800313 """
Don Garrettf90edf02010-11-16 17:36:14 -0800314 update_file = None
315 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700316
Don Garrettf90edf02010-11-16 17:36:14 -0800317 # Actually do the generation
318 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800319 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800320 image_path,
321 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000322
Don Garrettf90edf02010-11-16 17:36:14 -0800323 if update_file:
324 stateful_update_file = self.GenerateStatefulFile(image_path,
325 output_dir)
326
327 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800328 return update_file
Chris Sosa417e55d2011-01-25 16:40:48 -0800329 else:
330 _LogMessage('Failed to generate update.')
331 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800332
333 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
334 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000335
Chris Sosa0356d3b2010-09-16 15:46:22 -0700336 Args:
337 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700338 static_image_dir: the directory to move images to after generating.
339 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800340 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-25 16:40:48 -0800341 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700342 """
Don Garrettf90edf02010-11-16 17:36:14 -0800343 _LogMessage('Generating update for src %s image %s' % (self.src_image,
344 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700345
Chris Sosa417e55d2011-01-25 16:40:48 -0800346 # If it was pregenerated_path, don't regenerate
347 if self.pregenerated_path:
348 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800349
Don Garrettf90edf02010-11-16 17:36:14 -0800350 # Which sub_dir of static_image_dir should hold our cached update image
351 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
352 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
353
Chris Sosa417e55d2011-01-25 16:40:48 -0800354 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
355
Don Garrettf90edf02010-11-16 17:36:14 -0800356 # The cached payloads exist in a cache dir
357 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-25 16:40:48 -0800358 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800359 cache_stateful_payload = os.path.join(static_image_dir,
360 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800361 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800362
Chris Sosa417e55d2011-01-25 16:40:48 -0800363 # Check to see if this cache directory is valid.
364 if not os.path.exists(cache_update_payload) or not os.path.exists(
365 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800366 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800367 # Clean up stale state.
368 os.system('rm -rf "%s"' % full_cache_dir)
369 os.makedirs(full_cache_dir)
370 return_path = self.GenerateUpdateImage(image_path,
371 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800372
Chris Sosa417e55d2011-01-25 16:40:48 -0800373 # Clean up cache dir since it's not valid.
374 if not return_path:
375 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800376 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800377
378 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800379
Chris Sosa08d55a22011-01-19 16:08:02 -0800380 # Generation complete, copy if requested.
381 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800382 # The final results exist directly in static
383 update_payload = os.path.join(static_image_dir,
384 UPDATE_FILE)
385 stateful_payload = os.path.join(static_image_dir,
386 STATEFUL_FILE)
Chris Sosa08d55a22011-01-19 16:08:02 -0800387 self._Copy(cache_update_payload, update_payload)
388 self._Copy(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-25 16:40:48 -0800389 return UPDATE_FILE
390 else:
391 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700392
393 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800394 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700395 """Generates an update using the latest image that has been built.
396
397 This will only generate an update if the newest update is newer than that
398 on the client or client_version is 'ForcedUpdate'.
399
400 Args:
401 board_id: Name of the board.
402 client_version: Current version of the client or 'ForcedUpdate'
403 static_image_dir: the directory to move images to after generating.
404 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800405 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700406 """
407 latest_image_dir = self._GetLatestImageDir(board_id)
408 latest_version = self._GetVersionFromDir(latest_image_dir)
409 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
410
Chris Sosa7c931362010-10-11 19:49:01 -0700411 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700412 latest_image_path)
413
414 # Check to see whether or not we should update.
415 if client_version != 'ForcedUpdate' and not self._CanUpdate(
416 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700417 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800418 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700419
Don Garrettf90edf02010-11-16 17:36:14 -0800420 return self.GenerateUpdateImageWithCache(latest_image_path,
421 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700422
Andrew de los Reyes52620802010-04-12 13:40:07 -0700423 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
424 """Imports a factory-floor server configuration file. The file should
425 be in this format:
426 config = [
427 {
428 'qual_ids': set([1, 2, 3, "x86-generic"]),
429 'factory_image': 'generic-factory.gz',
430 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
431 'release_image': 'generic-release.gz',
432 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
433 'oempartitionimg_image': 'generic-oem.gz',
434 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700435 'efipartitionimg_image': 'generic-efi.gz',
436 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700437 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800438 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800439 'firmware_image': 'generic-firmware.gz',
440 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700441 },
442 {
443 'qual_ids': set([6]),
444 'factory_image': '6-factory.gz',
445 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
446 'release_image': '6-release.gz',
447 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
448 'oempartitionimg_image': '6-oem.gz',
449 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700450 'efipartitionimg_image': '6-efi.gz',
451 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700452 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800453 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800454 'firmware_image': '6-firmware.gz',
455 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700456 },
457 ]
458 The server will look for the files by name in the static files
459 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700460
Andrew de los Reyes52620802010-04-12 13:40:07 -0700461 If validate_checksums is True, validates checksums and exits. If
462 a checksum mismatch is found, it's printed to the screen.
463 """
464 f = open(filename, 'r')
465 output = {}
466 exec(f.read(), output)
467 self.factory_config = output['config']
468 success = True
469 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800470 for key in stanza.copy().iterkeys():
471 suffix = '_image'
472 if key.endswith(suffix):
473 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700474 stanza[kind + '_size'] = self._GetSize(os.path.join(
475 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800476 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700477 factory_checksum = self._GetHash(os.path.join(self.static_dir,
478 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800479 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700480 print ('Error: checksum mismatch for %s. Expected "%s" but file '
481 'has checksum "%s".' % (stanza[kind + '_image'],
482 stanza[kind + '_checksum'],
483 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800484 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700485
Andrew de los Reyes52620802010-04-12 13:40:07 -0700486 if validate_checksums:
487 if success is False:
488 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700489
Andrew de los Reyes52620802010-04-12 13:40:07 -0700490 print 'Config file looks good.'
491
492 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700493 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700494 for stanza in self.factory_config:
495 if board_id not in stanza['qual_ids']:
496 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700497 if kind + '_image' not in stanza:
498 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700499 return (stanza[kind + '_image'],
500 stanza[kind + '_checksum'],
501 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700502 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000503
Chris Sosa7c931362010-10-11 19:49:01 -0700504 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700505 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
506 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700507 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700508 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700509 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700510 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700511 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700512 # Factory install is using memento updater which is using the sha-1 hash so
513 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700514 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700515
Chris Sosa151643e2010-10-28 14:40:57 -0700516 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
517 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800518 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800519
Don Garrettf90edf02010-11-16 17:36:14 -0800520 Returns:
521 file name relative to static_image_dir on success.
522 """
Dale Curtis723ec472010-11-30 14:06:47 -0800523 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
524 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
525
Don Garrett0c880e22010-11-17 18:13:37 -0800526 if self.forced_payload:
527 # If the forced payload is not already in our static_image_dir,
528 # copy it there.
Don Garrettee25e552010-11-23 12:09:35 -0800529 src_path = os.path.abspath(self.forced_payload)
Don Garrettee25e552010-11-23 12:09:35 -0800530 src_stateful = os.path.join(os.path.dirname(src_path),
531 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800532
533 # Only copy the files if the source directory is different from dest.
534 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
535 self._Copy(src_path, dest_path)
536
537 # The stateful payload is optional.
538 if os.path.exists(src_stateful):
539 self._Copy(src_stateful, dest_stateful)
540 else:
541 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
542 STATEFUL_FILE)
543 if os.path.exists(dest_stateful):
544 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800545
Don Garrettfff4c322010-11-19 13:37:12 -0800546 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800547 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800548 return self.GenerateUpdateImageWithCache(
549 self.forced_image,
550 static_image_dir=static_image_dir)
551 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800552 # Warn if update or stateful files can't be found.
553 if not os.path.exists(dest_path):
554 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
555 UPDATE_FILE)
556
557 if not os.path.exists(dest_stateful):
558 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
559 STATEFUL_FILE)
560
561 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800562 else:
563 if board_id:
564 return self.GenerateLatestUpdateImage(board_id,
565 client_version,
566 static_image_dir)
567
Chris Sosa417e55d2011-01-25 16:40:48 -0800568 _LogMessage('Failed to genereate update. '
569 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800570 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700571
572 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800573 """Pre-generates an update and prints out the relative path it.
574
575 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800576 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700577 # Does not work with factory config.
578 assert(not self.factory_config)
579 _LogMessage('Pre-generating the update payload.')
580 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-25 16:40:48 -0800581 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
582 self.board, '0.0.0.0', self.static_dir)
583 if pregenerated_update:
584 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
585
586 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700587
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700588 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700589 """Handles an update ping from an update client.
590
591 Args:
592 data: xml blob from client.
593 label: optional label for the update.
594 Returns:
595 Update payload message for client.
596 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700597 # Set hostname as the hostname that the client is calling to and set up
598 # the url base.
599 self.hostname = cherrypy.request.base
600 if self.urlbase:
601 static_urlbase = self.urlbase
602 elif self.serve_only:
603 static_urlbase = '%s/static/archive' % self.hostname
604 else:
605 static_urlbase = '%s/static' % self.hostname
606
Don Garrett0ad09372010-12-06 16:20:30 -0800607 # If we have a proxy port, adjust the URL we instruct the client to
608 # use to go through the proxy.
609 if self.proxy_port:
610 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
611
Chris Sosa9841e1c2010-10-14 10:51:45 -0700612 _LogMessage('Using static url base %s' % static_urlbase)
613 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700614
Chris Sosa9841e1c2010-10-14 10:51:45 -0700615 update_dom = minidom.parseString(data)
616 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700617
618 # We only generate update payloads for updatecheck requests.
619 update_check = root.getElementsByTagName('o:updatecheck')
620 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700621 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700622 # TODO(sosa): Generate correct non-updatecheck payload to better test
623 # update clients.
624 return self.GetNoUpdatePayload()
625
626 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700627 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800628 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700629 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700630 board_id = (query.hasAttribute('board') and query.getAttribute('board')
631 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700632
Chris Sosa0356d3b2010-09-16 15:46:22 -0700633 # Separate logic as Factory requests have static url's that override
634 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700635 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700636 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700637 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700638 static_image_dir = self.static_dir
639 if label:
640 static_image_dir = os.path.join(static_image_dir, label)
641
Don Garrettf90edf02010-11-16 17:36:14 -0800642 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
643 client_version,
644 static_image_dir)
645 if payload_path:
646 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700647 hash = self._GetHash(filename)
648 sha256 = self._GetSHA256(filename)
649 size = self._GetSize(filename)
650 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700651 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800652 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700653 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800654 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700655
Chris Sosa7c931362010-10-11 19:49:01 -0700656 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700657 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700658 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700659 return self.GetNoUpdatePayload()