blob: d83de48c3cd4d89bb5c25b06d14e2155781703d2 [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 Sosad3de9ef2011-01-25 14:38:59 -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 Sosa08d55a22011-01-19 16:08:02 -080066 copy_to_static_root=True,
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
Don Garrettfff4c322010-11-19 13:37:12 -080085
Chris Sosad3de9ef2011-01-25 14:38:59 -080086 # Path to pre-generated file.
87 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070088
Chris Sosa0356d3b2010-09-16 15:46:22 -070089 def _GetSecondsSinceMidnight(self):
90 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070091 now = time.localtime()
92 return now[3] * 3600 + now[4] * 60 + now[5]
93
Chris Sosa0356d3b2010-09-16 15:46:22 -070094 def _GetDefaultBoardID(self):
95 """Returns the default board id stored in .default_board."""
96 board_file = '%s/.default_board' % (self.scripts_dir)
97 try:
98 return open(board_file).read()
99 except IOError:
100 return 'x86-generic'
101
102 def _GetLatestImageDir(self, board_id):
103 """Returns the latest image dir based on shell script."""
104 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
105 return os.popen(cmd).read().strip()
106
107 def _GetVersionFromDir(self, image_dir):
108 """Returns the version of the image based on the name of the directory."""
109 latest_version = os.path.basename(image_dir)
110 return latest_version.split('-')[0]
111
112 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800113 """Returns true if the latest_version is greater than the client_version.
114 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700115 client_tokens = client_version.replace('_', '').split('.')
116 latest_tokens = latest_version.replace('_', '').split('.')
Chris Sosa7c931362010-10-11 19:49:01 -0700117 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -0800118 % (client_version, latest_version))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700119 for i in range(4):
120 if int(latest_tokens[i]) == int(client_tokens[i]):
121 continue
122 return int(latest_tokens[i]) > int(client_tokens[i])
123 return False
124
Chris Sosa0356d3b2010-09-16 15:46:22 -0700125 def _UnpackZip(self, image_dir):
126 """Unpacks an image.zip into a given directory."""
127 image = os.path.join(image_dir, self._GetImageName())
128 if os.path.exists(image):
129 return True
130 else:
131 # -n, never clobber an existing file, in case we get invoked
132 # simultaneously by multiple request handlers. This means that
133 # we're assuming each image.zip file lives in a versioned
134 # directory (a la Buildbot).
135 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
136
137 def _GetImageName(self):
138 """Returns the name of the image that should be used."""
139 if self.use_test_image:
140 image_name = 'chromiumos_test_image.bin'
141 else:
142 image_name = 'chromiumos_image.bin'
143 return image_name
144
Chris Sosa0356d3b2010-09-16 15:46:22 -0700145 def _GetSize(self, update_path):
146 """Returns the size of the file given."""
147 return os.path.getsize(update_path)
148
149 def _GetHash(self, update_path):
150 """Returns the sha1 of the file given."""
151 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
152 % update_path)
153 return os.popen(cmd).read().rstrip()
154
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700155 def _IsDeltaFormatFile(self, filename):
156 try:
157 file_handle = open(filename, 'r')
158 delta_magic = 'CrAU'
159 magic = file_handle.read(len(delta_magic))
160 return magic == delta_magic
161 except Exception:
162 return False
163
Darin Petkov91436cb2010-09-28 08:52:17 -0700164 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
165 # it takes advantage of reduced I/O and multiple processors. Something like:
166 # % tee < FILE > /dev/null \
167 # >( openssl dgst -sha256 -binary | openssl base64 ) \
168 # >( openssl sha1 -binary | openssl base64 )
169 def _GetSHA256(self, update_path):
170 """Returns the sha256 of the file given."""
171 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
172 update_path)
173 return os.popen(cmd).read().rstrip()
174
Don Garrettf90edf02010-11-16 17:36:14 -0800175 def _GetMd5(self, update_path):
176 """Returns the md5 checksum of the file given."""
177 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
178 return os.popen(cmd).read().rstrip()
179
Don Garrett0c880e22010-11-17 18:13:37 -0800180 def _Copy(self, source, dest):
181 """Copies a file from dest to source (if different)"""
182 _LogMessage('Copy File %s -> %s' % (source, dest))
183 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800184 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800185 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800186
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700187 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700188 """Returns a payload to the client corresponding to a new update.
189
190 Args:
191 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700192 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700193 size: size of update blob
194 url: where to find update blob
195 Returns:
196 Xml string to be passed back to client.
197 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700198 delta = 'false'
199 if is_delta_format:
200 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000201 payload = """<?xml version="1.0" encoding="UTF-8"?>
202 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700203 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000204 <app appid="{%s}" status="ok">
205 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700206 <updatecheck
207 codebase="%s"
208 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700209 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700210 needsadmin="false"
211 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700212 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000213 status="ok"/>
214 </app>
215 </gupdate>
216 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700217 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700218 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000219
rtc@google.com21a5ca32009-11-04 18:23:23 +0000220 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700221 """Returns a payload to the client corresponding to no update."""
Darin Petkov845f1172011-01-05 14:45:24 -0800222 payload = """<?xml version="1.0" encoding="UTF-8"?>
223 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
224 <daystart elapsed_seconds="%s"/>
225 <app appid="{%s}" status="ok">
226 <ping status="ok"/>
227 <updatecheck status="noupdate"/>
228 </app>
229 </gupdate>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000230 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700231 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000232
Don Garrettf90edf02010-11-16 17:36:14 -0800233 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700234 """Generates an update gz given a full path to an image.
235
236 Args:
237 image_path: Full path to image.
238 Returns:
239 Path to created update_payload or None on error.
240 """
Don Garrettfff4c322010-11-19 13:37:12 -0800241 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa4136e692010-10-28 23:42:37 -0700242 patch_kernel_flag = '--patch_kernel'
Chris Sosa7c931362010-10-11 19:49:01 -0700243 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700244
Chris Sosa4136e692010-10-28 23:42:37 -0700245 # Don't patch the kernel for vm images as they don't need the patch.
246 if self.vm:
247 patch_kernel_flag = ''
248
Chris Sosa0356d3b2010-09-16 15:46:22 -0700249 mkupdate_command = (
Chris Sosa62f720b2010-10-26 21:39:48 -0700250 '%s/cros_generate_update_payload --image="%s" --output="%s" '
Chris Sosa4136e692010-10-28 23:42:37 -0700251 '%s --noold_style --src_image="%s"' % (
252 self.scripts_dir, image_path, update_path, patch_kernel_flag,
Don Garrettf90edf02010-11-16 17:36:14 -0800253 src_image))
Chris Sosa62f720b2010-10-26 21:39:48 -0700254 _LogMessage(mkupdate_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700255 if os.system(mkupdate_command) != 0:
Chris Sosad3de9ef2011-01-25 14:38:59 -0800256 _LogMessage('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700257 return None
258
Don Garrettfff4c322010-11-19 13:37:12 -0800259 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700260
Don Garrettf90edf02010-11-16 17:36:14 -0800261 def GenerateStatefulFile(self, image_path, output_dir):
262 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700263
264 Args:
265 image_path: Full path to image.
266 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800267 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800268 Raises:
269 A subprocess exception if the update generator fails to generate a
270 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700271 """
Don Garrettfff4c322010-11-19 13:37:12 -0800272 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800273 subprocess.check_call(
Don Garrettfff4c322010-11-19 13:37:12 -0800274 ['%s/cros_generate_stateful_update_payload' % self.scripts_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800275 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800276 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800277 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800278 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700279
Don Garrettf90edf02010-11-16 17:36:14 -0800280 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
281 """Find directory to store a cached update.
282
283 Given one, or two images for an update, this finds which
284 cache directory should hold the update files, even if they don't exist
285 yet. The directory will be inside static_image_dir, and of the form:
286
287 Non-delta updates:
Chris Sosad3de9ef2011-01-25 14:38:59 -0800288 CACHE_DIR/12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800289
290 Delta updates:
Chris Sosad3de9ef2011-01-25 14:38:59 -0800291 CACHE_DIR/12345678_12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800292 """
293 # If there is no src, we only have an image file, check image for changes
294 if not src_image:
Chris Sosad3de9ef2011-01-25 14:38:59 -0800295 return os.path.join(CACHE_DIR, self._GetMd5(dest_image))
Don Garrettf90edf02010-11-16 17:36:14 -0800296
297 # If we have src and dest, we are a delta, and check both for changes
Chris Sosad3de9ef2011-01-25 14:38:59 -0800298 return os.path.join(CACHE_DIR,
Don Garrettf90edf02010-11-16 17:36:14 -0800299 "%s_%s" % (self._GetMd5(src_image),
300 self._GetMd5(dest_image)))
301
Don Garrettfff4c322010-11-19 13:37:12 -0800302 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800303 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700304
Chris Sosade91f672010-11-16 10:05:44 -0800305 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800306 src_image: image we are updating from (Null/empty for non-delta)
307 image_path: full path to the image.
308 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800309 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800310 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800311 """
Don Garrettf90edf02010-11-16 17:36:14 -0800312 update_file = None
313 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700314
Don Garrettf90edf02010-11-16 17:36:14 -0800315 # Actually do the generation
316 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800317 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800318 image_path,
319 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000320
Don Garrettf90edf02010-11-16 17:36:14 -0800321 if update_file:
322 stateful_update_file = self.GenerateStatefulFile(image_path,
323 output_dir)
324
325 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800326 return update_file
Chris Sosad3de9ef2011-01-25 14:38:59 -0800327 else:
328 _LogMessage('Failed to generate update.')
329 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800330
331 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
332 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000333
Chris Sosa0356d3b2010-09-16 15:46:22 -0700334 Args:
335 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700336 static_image_dir: the directory to move images to after generating.
337 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800338 update filename (not directory) relative to static_image_dir on success,
Chris Sosad3de9ef2011-01-25 14:38:59 -0800339 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700340 """
Don Garrettf90edf02010-11-16 17:36:14 -0800341 _LogMessage('Generating update for src %s image %s' % (self.src_image,
342 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700343
Chris Sosad3de9ef2011-01-25 14:38:59 -0800344 # If it was pregenerated_path, don't regenerate
345 if self.pregenerated_path:
346 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800347
Don Garrettf90edf02010-11-16 17:36:14 -0800348 # Which sub_dir of static_image_dir should hold our cached update image
349 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
350 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
351
Chris Sosad3de9ef2011-01-25 14:38:59 -0800352 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
353
Don Garrettf90edf02010-11-16 17:36:14 -0800354 # The cached payloads exist in a cache dir
355 cache_update_payload = os.path.join(static_image_dir,
Chris Sosad3de9ef2011-01-25 14:38:59 -0800356 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800357 cache_stateful_payload = os.path.join(static_image_dir,
358 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800359 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800360
Chris Sosad3de9ef2011-01-25 14:38:59 -0800361 # Check to see if this cache directory is valid.
362 if not os.path.exists(cache_update_payload) or not os.path.exists(
363 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800364 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosad3de9ef2011-01-25 14:38:59 -0800365 # Clean up stale state.
366 os.system('rm -rf "%s"' % full_cache_dir)
367 os.makedirs(full_cache_dir)
368 update_path = self.GenerateUpdateImage(image_path,
369 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800370
Chris Sosad3de9ef2011-01-25 14:38:59 -0800371 # Clean up cache dir since it's not valid.
372 if not return_path:
373 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800374 return None
Chris Sosad3de9ef2011-01-25 14:38:59 -0800375 else:
376 assert (return_path == update_path,
377 'Returned path %s not equal to %s' % (return_path, update_path))
378
379 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800380
Chris Sosa08d55a22011-01-19 16:08:02 -0800381 # Generation complete, copy if requested.
382 if self.copy_to_static_root:
Chris Sosad3de9ef2011-01-25 14:38:59 -0800383 # The final results exist directly in static
384 update_payload = os.path.join(static_image_dir,
385 UPDATE_FILE)
386 stateful_payload = os.path.join(static_image_dir,
387 STATEFUL_FILE)
Chris Sosa08d55a22011-01-19 16:08:02 -0800388 self._Copy(cache_update_payload, update_payload)
389 self._Copy(cache_stateful_payload, stateful_payload)
Chris Sosad3de9ef2011-01-25 14:38:59 -0800390 return UPDATE_FILE
391 else:
392 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700393
394 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800395 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700396 """Generates an update using the latest image that has been built.
397
398 This will only generate an update if the newest update is newer than that
399 on the client or client_version is 'ForcedUpdate'.
400
401 Args:
402 board_id: Name of the board.
403 client_version: Current version of the client or 'ForcedUpdate'
404 static_image_dir: the directory to move images to after generating.
405 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800406 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700407 """
408 latest_image_dir = self._GetLatestImageDir(board_id)
409 latest_version = self._GetVersionFromDir(latest_image_dir)
410 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
411
Chris Sosa7c931362010-10-11 19:49:01 -0700412 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700413 latest_image_path)
414
415 # Check to see whether or not we should update.
416 if client_version != 'ForcedUpdate' and not self._CanUpdate(
417 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700418 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800419 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700420
Don Garrettf90edf02010-11-16 17:36:14 -0800421 return self.GenerateUpdateImageWithCache(latest_image_path,
422 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700423
Andrew de los Reyes52620802010-04-12 13:40:07 -0700424 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
425 """Imports a factory-floor server configuration file. The file should
426 be in this format:
427 config = [
428 {
429 'qual_ids': set([1, 2, 3, "x86-generic"]),
430 'factory_image': 'generic-factory.gz',
431 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
432 'release_image': 'generic-release.gz',
433 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
434 'oempartitionimg_image': 'generic-oem.gz',
435 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700436 'efipartitionimg_image': 'generic-efi.gz',
437 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700438 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800439 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800440 'firmware_image': 'generic-firmware.gz',
441 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700442 },
443 {
444 'qual_ids': set([6]),
445 'factory_image': '6-factory.gz',
446 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
447 'release_image': '6-release.gz',
448 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
449 'oempartitionimg_image': '6-oem.gz',
450 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700451 'efipartitionimg_image': '6-efi.gz',
452 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700453 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800454 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800455 'firmware_image': '6-firmware.gz',
456 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700457 },
458 ]
459 The server will look for the files by name in the static files
460 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700461
Andrew de los Reyes52620802010-04-12 13:40:07 -0700462 If validate_checksums is True, validates checksums and exits. If
463 a checksum mismatch is found, it's printed to the screen.
464 """
465 f = open(filename, 'r')
466 output = {}
467 exec(f.read(), output)
468 self.factory_config = output['config']
469 success = True
470 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800471 for key in stanza.copy().iterkeys():
472 suffix = '_image'
473 if key.endswith(suffix):
474 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700475 stanza[kind + '_size'] = self._GetSize(os.path.join(
476 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800477 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700478 factory_checksum = self._GetHash(os.path.join(self.static_dir,
479 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800480 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700481 print ('Error: checksum mismatch for %s. Expected "%s" but file '
482 'has checksum "%s".' % (stanza[kind + '_image'],
483 stanza[kind + '_checksum'],
484 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800485 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700486
Andrew de los Reyes52620802010-04-12 13:40:07 -0700487 if validate_checksums:
488 if success is False:
489 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700490
Andrew de los Reyes52620802010-04-12 13:40:07 -0700491 print 'Config file looks good.'
492
493 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700494 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700495 for stanza in self.factory_config:
496 if board_id not in stanza['qual_ids']:
497 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700498 if kind + '_image' not in stanza:
499 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700500 return (stanza[kind + '_image'],
501 stanza[kind + '_checksum'],
502 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700503 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000504
Chris Sosa7c931362010-10-11 19:49:01 -0700505 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700506 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
507 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700508 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700509 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700510 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700511 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700512 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700513 # Factory install is using memento updater which is using the sha-1 hash so
514 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700515 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700516
Chris Sosa151643e2010-10-28 14:40:57 -0700517 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
518 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800519 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800520
Don Garrettf90edf02010-11-16 17:36:14 -0800521 Returns:
522 file name relative to static_image_dir on success.
523 """
Dale Curtis723ec472010-11-30 14:06:47 -0800524 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
525 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
526
Don Garrett0c880e22010-11-17 18:13:37 -0800527 if self.forced_payload:
528 # If the forced payload is not already in our static_image_dir,
529 # copy it there.
Don Garrettee25e552010-11-23 12:09:35 -0800530 src_path = os.path.abspath(self.forced_payload)
Don Garrettee25e552010-11-23 12:09:35 -0800531 src_stateful = os.path.join(os.path.dirname(src_path),
532 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800533
534 # Only copy the files if the source directory is different from dest.
535 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
536 self._Copy(src_path, dest_path)
537
538 # The stateful payload is optional.
539 if os.path.exists(src_stateful):
540 self._Copy(src_stateful, dest_stateful)
541 else:
542 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
543 STATEFUL_FILE)
544 if os.path.exists(dest_stateful):
545 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800546
Don Garrettfff4c322010-11-19 13:37:12 -0800547 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800548 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800549 return self.GenerateUpdateImageWithCache(
550 self.forced_image,
551 static_image_dir=static_image_dir)
552 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800553 # Warn if update or stateful files can't be found.
554 if not os.path.exists(dest_path):
555 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
556 UPDATE_FILE)
557
558 if not os.path.exists(dest_stateful):
559 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
560 STATEFUL_FILE)
561
562 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800563 else:
564 if board_id:
565 return self.GenerateLatestUpdateImage(board_id,
566 client_version,
567 static_image_dir)
568
Chris Sosad3de9ef2011-01-25 14:38:59 -0800569 _LogMessage('Failed to genereate update. '
570 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800571 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700572
573 def PreGenerateUpdate(self):
Chris Sosad3de9ef2011-01-25 14:38:59 -0800574 """Pre-generates an update and prints out the relative path it.
575
576 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800577 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700578 # Does not work with factory config.
579 assert(not self.factory_config)
580 _LogMessage('Pre-generating the update payload.')
581 # Does not work with labels so just use static dir.
Chris Sosad3de9ef2011-01-25 14:38:59 -0800582 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
583 self.board, '0.0.0.0', self.static_dir)
584 if pregenerated_update:
585 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
586
587 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700588
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700589 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700590 """Handles an update ping from an update client.
591
592 Args:
593 data: xml blob from client.
594 label: optional label for the update.
595 Returns:
596 Update payload message for client.
597 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700598 # Set hostname as the hostname that the client is calling to and set up
599 # the url base.
600 self.hostname = cherrypy.request.base
601 if self.urlbase:
602 static_urlbase = self.urlbase
603 elif self.serve_only:
604 static_urlbase = '%s/static/archive' % self.hostname
605 else:
606 static_urlbase = '%s/static' % self.hostname
607
Don Garrett0ad09372010-12-06 16:20:30 -0800608 # If we have a proxy port, adjust the URL we instruct the client to
609 # use to go through the proxy.
610 if self.proxy_port:
611 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
612
Chris Sosa9841e1c2010-10-14 10:51:45 -0700613 _LogMessage('Using static url base %s' % static_urlbase)
614 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700615
616 # Check the client prefix to make sure you can support this type of update.
Chris Sosa9841e1c2010-10-14 10:51:45 -0700617 update_dom = minidom.parseString(data)
618 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700619 if (root.hasAttribute('updaterversion') and
620 not root.getAttribute('updaterversion').startswith(self.client_prefix)):
Chris Sosa7c931362010-10-11 19:49:01 -0700621 _LogMessage('Got update from unsupported updater:' +
Chris Sosa0356d3b2010-09-16 15:46:22 -0700622 root.getAttribute('updaterversion'))
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700623 return self.GetNoUpdatePayload()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700624
625 # We only generate update payloads for updatecheck requests.
626 update_check = root.getElementsByTagName('o:updatecheck')
627 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700628 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700629 # TODO(sosa): Generate correct non-updatecheck payload to better test
630 # update clients.
631 return self.GetNoUpdatePayload()
632
633 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700634 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800635 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700636 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700637 board_id = (query.hasAttribute('board') and query.getAttribute('board')
638 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700639
Chris Sosa0356d3b2010-09-16 15:46:22 -0700640 # Separate logic as Factory requests have static url's that override
641 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700642 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700643 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700644 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700645 static_image_dir = self.static_dir
646 if label:
647 static_image_dir = os.path.join(static_image_dir, label)
648
Don Garrettf90edf02010-11-16 17:36:14 -0800649 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
650 client_version,
651 static_image_dir)
652 if payload_path:
653 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700654 hash = self._GetHash(filename)
655 sha256 = self._GetSHA256(filename)
656 size = self._GetSize(filename)
657 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700658 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800659 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700660 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800661 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700662
Chris Sosa7c931362010-10-11 19:49:01 -0700663 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700664 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700665 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700666 return self.GetNoUpdatePayload()