blob: f8fb57b92fa449a82f5e5863e47599ca6c9a6be9 [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
Don Garrettfff4c322010-11-19 13:37:12 -080019UPDATE_FILE='update.gz'
20STATEFUL_FILE='stateful.tgz'
Chris Sosa0356d3b2010-09-16 15:46:22 -070021
Don Garrett0ad09372010-12-06 16:20:30 -080022
23def _ChangeUrlPort(url, new_port):
24 """Return the URL passed in with a different port"""
25 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
26 host_port = netloc.split(':')
27
28 if len(host_port) == 1:
29 host_port.append(new_port)
30 else:
31 host_port[1] = new_port
32
33 print host_port
34 netloc = "%s:%s" % tuple(host_port)
35
36 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
37
38
rtc@google.com64244662009-11-12 00:52:08 +000039class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -070040 """Class that contains functionality that handles Chrome OS update pings.
41
42 Members:
Dale Curtis723ec472010-11-30 14:06:47 -080043 serve_only: Serve only pre-built updates. static_dir must contain update.gz
44 and stateful.tgz.
Chris Sosa0356d3b2010-09-16 15:46:22 -070045 factory_config: Path to the factory config file if handling factory
46 requests.
47 use_test_image: Use chromiumos_test_image.bin rather than the standard.
48 static_url_base: base URL, other than devserver, for update images.
49 client_prefix: The prefix for the update engine client.
50 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,
Don Garrett0c880e22010-11-17 18:13:37 -080062 factory_config_path=None, client_prefix=None,
63 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 Sosa08d55a22011-01-19 16:08:02 -080065 copy_to_static_root=True,
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 Sosab63a9282010-09-02 10:43:23 -070076 self.client_prefix = client_prefix
Chris Sosa0356d3b2010-09-16 15:46:22 -070077 self.forced_image = forced_image
Don Garrett0c880e22010-11-17 18:13:37 -080078 self.forced_payload = forced_payload
Chris Sosa62f720b2010-10-26 21:39:48 -070079 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -080080 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -070081 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -070082 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -080083 self.copy_to_static_root = copy_to_static_root
Don Garrettfff4c322010-11-19 13:37:12 -080084
Don Garrettfff4c322010-11-19 13:37:12 -080085 # Track update pregeneration, so we don't recopy if not needed.
86 self.pregenerated = False
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 Sosa4136e692010-10-28 23:42:37 -0700241 patch_kernel_flag = '--patch_kernel'
Chris Sosa7c931362010-10-11 19:49:01 -0700242 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700243
Chris Sosa4136e692010-10-28 23:42:37 -0700244 # Don't patch the kernel for vm images as they don't need the patch.
245 if self.vm:
246 patch_kernel_flag = ''
247
Chris Sosa0356d3b2010-09-16 15:46:22 -0700248 mkupdate_command = (
Chris Sosa62f720b2010-10-26 21:39:48 -0700249 '%s/cros_generate_update_payload --image="%s" --output="%s" '
Chris Sosa4136e692010-10-28 23:42:37 -0700250 '%s --noold_style --src_image="%s"' % (
251 self.scripts_dir, image_path, update_path, patch_kernel_flag,
Don Garrettf90edf02010-11-16 17:36:14 -0800252 src_image))
Chris Sosa62f720b2010-10-26 21:39:48 -0700253 _LogMessage(mkupdate_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700254 if os.system(mkupdate_command) != 0:
Chris Sosa7c931362010-10-11 19:49:01 -0700255 _LogMessage('Failed to create base update file')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700256 return None
257
Don Garrettfff4c322010-11-19 13:37:12 -0800258 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700259
Don Garrettf90edf02010-11-16 17:36:14 -0800260 def GenerateStatefulFile(self, image_path, output_dir):
261 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700262
263 Args:
264 image_path: Full path to image.
265 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800266 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800267 Raises:
268 A subprocess exception if the update generator fails to generate a
269 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700270 """
Don Garrettfff4c322010-11-19 13:37:12 -0800271 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800272 subprocess.check_call(
Don Garrettfff4c322010-11-19 13:37:12 -0800273 ['%s/cros_generate_stateful_update_payload' % self.scripts_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800274 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800275 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800276 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800277 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700278
Don Garrettf90edf02010-11-16 17:36:14 -0800279 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
280 """Find directory to store a cached update.
281
282 Given one, or two images for an update, this finds which
283 cache directory should hold the update files, even if they don't exist
284 yet. The directory will be inside static_image_dir, and of the form:
285
286 Non-delta updates:
287 cache/12345678
288
289 Delta updates:
290 cache/12345678_12345678
291 """
292 # If there is no src, we only have an image file, check image for changes
293 if not src_image:
294 return os.path.join('cache', self._GetMd5(dest_image))
295
296 # If we have src and dest, we are a delta, and check both for changes
297 return os.path.join('cache',
298 "%s_%s" % (self._GetMd5(src_image),
299 self._GetMd5(dest_image)))
300
Don Garrettfff4c322010-11-19 13:37:12 -0800301 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800302 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700303
Chris Sosade91f672010-11-16 10:05:44 -0800304 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800305 src_image: image we are updating from (Null/empty for non-delta)
306 image_path: full path to the image.
307 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800308 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800309 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800310 """
Don Garrettf90edf02010-11-16 17:36:14 -0800311 update_file = None
312 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700313
Don Garrettf90edf02010-11-16 17:36:14 -0800314 # Actually do the generation
315 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800316 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800317 image_path,
318 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000319
Don Garrettf90edf02010-11-16 17:36:14 -0800320 if update_file:
321 stateful_update_file = self.GenerateStatefulFile(image_path,
322 output_dir)
323
324 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800325 return update_file
Don Garrettf90edf02010-11-16 17:36:14 -0800326
327 _LogMessage('Failed to generate update')
328
329 # Cleanup incomplete files, if they exist
330 if update_file and os.path.exists(update_file):
331 os.remove(update_file)
332
333 return None
334
335 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
336 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000337
Chris Sosa0356d3b2010-09-16 15:46:22 -0700338 Args:
339 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700340 static_image_dir: the directory to move images to after generating.
341 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800342 update filename (not directory) relative to static_image_dir on success,
343 or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700344 """
Don Garrettf90edf02010-11-16 17:36:14 -0800345 _LogMessage('Generating update for src %s image %s' % (self.src_image,
346 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700347
Don Garrettfff4c322010-11-19 13:37:12 -0800348 # If it was pregenerated, don't regenerate
349 if self.pregenerated:
350 return UPDATE_FILE
351
Don Garrettf90edf02010-11-16 17:36:14 -0800352 # Which sub_dir of static_image_dir should hold our cached update image
353 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
354 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
355
356 # The cached payloads exist in a cache dir
357 cache_update_payload = os.path.join(static_image_dir,
358 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800359 UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800360 cache_stateful_payload = os.path.join(static_image_dir,
361 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800362 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800363
Don Garrettfff4c322010-11-19 13:37:12 -0800364 # The final results exist directly in static
Don Garrettf90edf02010-11-16 17:36:14 -0800365 update_payload = os.path.join(static_image_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800366 UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800367 stateful_payload = os.path.join(static_image_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800368 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800369
370 # If there isn't a cached payload, make one
371 if not os.path.exists(cache_update_payload):
372 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
373
374 # Create the directory for the cache values
375 if not os.path.exists(full_cache_dir):
376 os.makedirs(full_cache_dir)
377
Don Garrettfff4c322010-11-19 13:37:12 -0800378 result = self.GenerateUpdateImage(image_path,
379 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800380
Don Garrettfff4c322010-11-19 13:37:12 -0800381 if not result:
Don Garrettf90edf02010-11-16 17:36:14 -0800382 # Clean up cache dir if it's not valid
383 os.system("rm -rf %s" % os.path.join(static_image_dir, cache_sub_dir))
384 return None
385
Chris Sosa08d55a22011-01-19 16:08:02 -0800386 # Generation complete, copy if requested.
387 if self.copy_to_static_root:
388 self._Copy(cache_update_payload, update_payload)
389 self._Copy(cache_stateful_payload, stateful_payload)
Don Garrettf90edf02010-11-16 17:36:14 -0800390
Don Garrettfff4c322010-11-19 13:37:12 -0800391 # Return just the filename in static_image_dir.
392 return UPDATE_FILE
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 Garrett0c880e22010-11-17 18:13:37 -0800531
Don Garrettee25e552010-11-23 12:09:35 -0800532 src_stateful = os.path.join(os.path.dirname(src_path),
533 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800534
535 # Only copy the files if the source directory is different from dest.
536 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
537 self._Copy(src_path, dest_path)
538
539 # The stateful payload is optional.
540 if os.path.exists(src_stateful):
541 self._Copy(src_stateful, dest_stateful)
542 else:
543 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
544 STATEFUL_FILE)
545 if os.path.exists(dest_stateful):
546 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800547
Don Garrettfff4c322010-11-19 13:37:12 -0800548 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800549 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800550 return self.GenerateUpdateImageWithCache(
551 self.forced_image,
552 static_image_dir=static_image_dir)
553 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800554 # Warn if update or stateful files can't be found.
555 if not os.path.exists(dest_path):
556 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
557 UPDATE_FILE)
558
559 if not os.path.exists(dest_stateful):
560 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
561 STATEFUL_FILE)
562
563 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800564 else:
565 if board_id:
566 return self.GenerateLatestUpdateImage(board_id,
567 client_version,
568 static_image_dir)
569
570 _LogMessage('You must set --board for pre-generating latest update.')
571 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700572
573 def PreGenerateUpdate(self):
Don Garrettf90edf02010-11-16 17:36:14 -0800574 """Pre-generates an update. Returns True on success.
575 """
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 Sosae67b78f2010-11-04 17:33:16 -0700580 if self.GenerateUpdatePayloadForNonFactory(self.board, '0.0.0.0',
581 self.static_dir):
Chris Sosae67b78f2010-11-04 17:33:16 -0700582 _LogMessage('Pre-generated update successfully.')
Don Garrettfff4c322010-11-19 13:37:12 -0800583 self.pregenerated = True
Chris Sosae67b78f2010-11-04 17:33:16 -0700584 return True
Chris Sosa2c048f12010-10-27 16:05:27 -0700585 else:
586 _LogMessage('Failed to pre-generate update.')
Chris Sosae67b78f2010-11-04 17:33:16 -0700587 return False
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()