blob: 30f3b625e0b6e9cc2cbd284e25a19b1f7ffd670b [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.
51 """
rtc@google.comded22402009-10-26 22:36:21 +000052
Sean O'Connor1f7fd362010-04-07 16:34:52 -070053 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Don Garrett0c880e22010-11-17 18:13:37 -080054 factory_config_path=None, client_prefix=None,
55 forced_image=None, forced_payload=None,
Don Garrett0ad09372010-12-06 16:20:30 -080056 port=8080, proxy_port=None, src_image='', vm=False, board=None,
Chris Sosae67b78f2010-11-04 17:33:16 -070057 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070058 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070059 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -070060 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -070061 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -070062 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -070063 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -070064 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -070065 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -070066
Chris Sosab63a9282010-09-02 10:43:23 -070067 self.client_prefix = client_prefix
Chris Sosa0356d3b2010-09-16 15:46:22 -070068 self.forced_image = forced_image
Don Garrett0c880e22010-11-17 18:13:37 -080069 self.forced_payload = forced_payload
Chris Sosa62f720b2010-10-26 21:39:48 -070070 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -080071 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -070072 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -070073 self.board = board
Don Garrettfff4c322010-11-19 13:37:12 -080074
Don Garrettfff4c322010-11-19 13:37:12 -080075 # Track update pregeneration, so we don't recopy if not needed.
76 self.pregenerated = False
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070077
Chris Sosa0356d3b2010-09-16 15:46:22 -070078 def _GetSecondsSinceMidnight(self):
79 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070080 now = time.localtime()
81 return now[3] * 3600 + now[4] * 60 + now[5]
82
Chris Sosa0356d3b2010-09-16 15:46:22 -070083 def _GetDefaultBoardID(self):
84 """Returns the default board id stored in .default_board."""
85 board_file = '%s/.default_board' % (self.scripts_dir)
86 try:
87 return open(board_file).read()
88 except IOError:
89 return 'x86-generic'
90
91 def _GetLatestImageDir(self, board_id):
92 """Returns the latest image dir based on shell script."""
93 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
94 return os.popen(cmd).read().strip()
95
96 def _GetVersionFromDir(self, image_dir):
97 """Returns the version of the image based on the name of the directory."""
98 latest_version = os.path.basename(image_dir)
99 return latest_version.split('-')[0]
100
101 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800102 """Returns true if the latest_version is greater than the client_version.
103 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700104 client_tokens = client_version.replace('_', '').split('.')
105 latest_tokens = latest_version.replace('_', '').split('.')
Chris Sosa7c931362010-10-11 19:49:01 -0700106 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -0800107 % (client_version, latest_version))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700108 for i in range(4):
109 if int(latest_tokens[i]) == int(client_tokens[i]):
110 continue
111 return int(latest_tokens[i]) > int(client_tokens[i])
112 return False
113
Chris Sosa0356d3b2010-09-16 15:46:22 -0700114 def _UnpackZip(self, image_dir):
115 """Unpacks an image.zip into a given directory."""
116 image = os.path.join(image_dir, self._GetImageName())
117 if os.path.exists(image):
118 return True
119 else:
120 # -n, never clobber an existing file, in case we get invoked
121 # simultaneously by multiple request handlers. This means that
122 # we're assuming each image.zip file lives in a versioned
123 # directory (a la Buildbot).
124 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
125
126 def _GetImageName(self):
127 """Returns the name of the image that should be used."""
128 if self.use_test_image:
129 image_name = 'chromiumos_test_image.bin'
130 else:
131 image_name = 'chromiumos_image.bin'
132 return image_name
133
Chris Sosa0356d3b2010-09-16 15:46:22 -0700134 def _GetSize(self, update_path):
135 """Returns the size of the file given."""
136 return os.path.getsize(update_path)
137
138 def _GetHash(self, update_path):
139 """Returns the sha1 of the file given."""
140 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
141 % update_path)
142 return os.popen(cmd).read().rstrip()
143
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700144 def _IsDeltaFormatFile(self, filename):
145 try:
146 file_handle = open(filename, 'r')
147 delta_magic = 'CrAU'
148 magic = file_handle.read(len(delta_magic))
149 return magic == delta_magic
150 except Exception:
151 return False
152
Darin Petkov91436cb2010-09-28 08:52:17 -0700153 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
154 # it takes advantage of reduced I/O and multiple processors. Something like:
155 # % tee < FILE > /dev/null \
156 # >( openssl dgst -sha256 -binary | openssl base64 ) \
157 # >( openssl sha1 -binary | openssl base64 )
158 def _GetSHA256(self, update_path):
159 """Returns the sha256 of the file given."""
160 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
161 update_path)
162 return os.popen(cmd).read().rstrip()
163
Don Garrettf90edf02010-11-16 17:36:14 -0800164 def _GetMd5(self, update_path):
165 """Returns the md5 checksum of the file given."""
166 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
167 return os.popen(cmd).read().rstrip()
168
Don Garrett0c880e22010-11-17 18:13:37 -0800169 def _Copy(self, source, dest):
170 """Copies a file from dest to source (if different)"""
171 _LogMessage('Copy File %s -> %s' % (source, dest))
172 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800173 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800174 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800175
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700176 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700177 """Returns a payload to the client corresponding to a new update.
178
179 Args:
180 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700181 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700182 size: size of update blob
183 url: where to find update blob
184 Returns:
185 Xml string to be passed back to client.
186 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700187 delta = 'false'
188 if is_delta_format:
189 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000190 payload = """<?xml version="1.0" encoding="UTF-8"?>
191 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700192 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000193 <app appid="{%s}" status="ok">
194 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700195 <updatecheck
196 codebase="%s"
197 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700198 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700199 needsadmin="false"
200 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700201 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000202 status="ok"/>
203 </app>
204 </gupdate>
205 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700206 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700207 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000208
rtc@google.com21a5ca32009-11-04 18:23:23 +0000209 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700210 """Returns a payload to the client corresponding to no update."""
211 payload = """ < ?xml version = "1.0" encoding = "UTF-8"? >
212 < gupdate xmlns = "http://www.google.com/update2/response" protocol = "2.0" >
213 < daystart elapsed_seconds = "%s" />
214 < app appid = "{%s}" status = "ok" >
215 < ping status = "ok" />
216 < updatecheck status = "noupdate" />
217 </ app >
218 </ gupdate >
rtc@google.com21a5ca32009-11-04 18:23:23 +0000219 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700220 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000221
Don Garrettf90edf02010-11-16 17:36:14 -0800222 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700223 """Generates an update gz given a full path to an image.
224
225 Args:
226 image_path: Full path to image.
227 Returns:
228 Path to created update_payload or None on error.
229 """
Don Garrettfff4c322010-11-19 13:37:12 -0800230 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa4136e692010-10-28 23:42:37 -0700231 patch_kernel_flag = '--patch_kernel'
Chris Sosa7c931362010-10-11 19:49:01 -0700232 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700233
Chris Sosa4136e692010-10-28 23:42:37 -0700234 # Don't patch the kernel for vm images as they don't need the patch.
235 if self.vm:
236 patch_kernel_flag = ''
237
Chris Sosa0356d3b2010-09-16 15:46:22 -0700238 mkupdate_command = (
Chris Sosa62f720b2010-10-26 21:39:48 -0700239 '%s/cros_generate_update_payload --image="%s" --output="%s" '
Chris Sosa4136e692010-10-28 23:42:37 -0700240 '%s --noold_style --src_image="%s"' % (
241 self.scripts_dir, image_path, update_path, patch_kernel_flag,
Don Garrettf90edf02010-11-16 17:36:14 -0800242 src_image))
Chris Sosa62f720b2010-10-26 21:39:48 -0700243 _LogMessage(mkupdate_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700244 if os.system(mkupdate_command) != 0:
Chris Sosa7c931362010-10-11 19:49:01 -0700245 _LogMessage('Failed to create base update file')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700246 return None
247
Don Garrettfff4c322010-11-19 13:37:12 -0800248 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700249
Don Garrettf90edf02010-11-16 17:36:14 -0800250 def GenerateStatefulFile(self, image_path, output_dir):
251 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700252
253 Args:
254 image_path: Full path to image.
255 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800256 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800257 Raises:
258 A subprocess exception if the update generator fails to generate a
259 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700260 """
Don Garrettfff4c322010-11-19 13:37:12 -0800261 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800262 subprocess.check_call(
Don Garrettfff4c322010-11-19 13:37:12 -0800263 ['%s/cros_generate_stateful_update_payload' % self.scripts_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800264 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800265 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800266 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800267 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700268
Don Garrettf90edf02010-11-16 17:36:14 -0800269 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
270 """Find directory to store a cached update.
271
272 Given one, or two images for an update, this finds which
273 cache directory should hold the update files, even if they don't exist
274 yet. The directory will be inside static_image_dir, and of the form:
275
276 Non-delta updates:
277 cache/12345678
278
279 Delta updates:
280 cache/12345678_12345678
281 """
282 # If there is no src, we only have an image file, check image for changes
283 if not src_image:
284 return os.path.join('cache', self._GetMd5(dest_image))
285
286 # If we have src and dest, we are a delta, and check both for changes
287 return os.path.join('cache',
288 "%s_%s" % (self._GetMd5(src_image),
289 self._GetMd5(dest_image)))
290
Don Garrettfff4c322010-11-19 13:37:12 -0800291 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800292 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700293
Chris Sosade91f672010-11-16 10:05:44 -0800294 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800295 src_image: image we are updating from (Null/empty for non-delta)
296 image_path: full path to the image.
297 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800298 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800299 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800300 """
Don Garrettf90edf02010-11-16 17:36:14 -0800301 update_file = None
302 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700303
Don Garrettf90edf02010-11-16 17:36:14 -0800304 # Actually do the generation
305 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800306 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800307 image_path,
308 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000309
Don Garrettf90edf02010-11-16 17:36:14 -0800310 if update_file:
311 stateful_update_file = self.GenerateStatefulFile(image_path,
312 output_dir)
313
314 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800315 return update_file
Don Garrettf90edf02010-11-16 17:36:14 -0800316
317 _LogMessage('Failed to generate update')
318
319 # Cleanup incomplete files, if they exist
320 if update_file and os.path.exists(update_file):
321 os.remove(update_file)
322
323 return None
324
325 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
326 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000327
Chris Sosa0356d3b2010-09-16 15:46:22 -0700328 Args:
329 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700330 static_image_dir: the directory to move images to after generating.
331 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800332 update filename (not directory) relative to static_image_dir on success,
333 or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700334 """
Don Garrettf90edf02010-11-16 17:36:14 -0800335 _LogMessage('Generating update for src %s image %s' % (self.src_image,
336 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700337
Don Garrettfff4c322010-11-19 13:37:12 -0800338 # If it was pregenerated, don't regenerate
339 if self.pregenerated:
340 return UPDATE_FILE
341
Don Garrettf90edf02010-11-16 17:36:14 -0800342 # Which sub_dir of static_image_dir should hold our cached update image
343 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
344 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
345
346 # The cached payloads exist in a cache dir
347 cache_update_payload = os.path.join(static_image_dir,
348 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800349 UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800350 cache_stateful_payload = os.path.join(static_image_dir,
351 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800352 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800353
Don Garrettfff4c322010-11-19 13:37:12 -0800354 # The final results exist directly in static
Don Garrettf90edf02010-11-16 17:36:14 -0800355 update_payload = os.path.join(static_image_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800356 UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800357 stateful_payload = os.path.join(static_image_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800358 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800359
360 # If there isn't a cached payload, make one
361 if not os.path.exists(cache_update_payload):
362 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
363
364 # Create the directory for the cache values
365 if not os.path.exists(full_cache_dir):
366 os.makedirs(full_cache_dir)
367
Don Garrettfff4c322010-11-19 13:37:12 -0800368 result = self.GenerateUpdateImage(image_path,
369 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800370
Don Garrettfff4c322010-11-19 13:37:12 -0800371 if not result:
Don Garrettf90edf02010-11-16 17:36:14 -0800372 # Clean up cache dir if it's not valid
373 os.system("rm -rf %s" % os.path.join(static_image_dir, cache_sub_dir))
374 return None
375
Don Garrett0c880e22010-11-17 18:13:37 -0800376 # If the generation worked, copy files
377 self._Copy(cache_update_payload, update_payload)
378 self._Copy(cache_stateful_payload, stateful_payload)
Don Garrettf90edf02010-11-16 17:36:14 -0800379
Don Garrettfff4c322010-11-19 13:37:12 -0800380 # Return just the filename in static_image_dir.
381 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700382
383 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800384 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700385 """Generates an update using the latest image that has been built.
386
387 This will only generate an update if the newest update is newer than that
388 on the client or client_version is 'ForcedUpdate'.
389
390 Args:
391 board_id: Name of the board.
392 client_version: Current version of the client or 'ForcedUpdate'
393 static_image_dir: the directory to move images to after generating.
394 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800395 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700396 """
397 latest_image_dir = self._GetLatestImageDir(board_id)
398 latest_version = self._GetVersionFromDir(latest_image_dir)
399 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
400
Chris Sosa7c931362010-10-11 19:49:01 -0700401 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700402 latest_image_path)
403
404 # Check to see whether or not we should update.
405 if client_version != 'ForcedUpdate' and not self._CanUpdate(
406 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700407 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800408 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700409
Don Garrettf90edf02010-11-16 17:36:14 -0800410 return self.GenerateUpdateImageWithCache(latest_image_path,
411 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700412
Andrew de los Reyes52620802010-04-12 13:40:07 -0700413 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
414 """Imports a factory-floor server configuration file. The file should
415 be in this format:
416 config = [
417 {
418 'qual_ids': set([1, 2, 3, "x86-generic"]),
419 'factory_image': 'generic-factory.gz',
420 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
421 'release_image': 'generic-release.gz',
422 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
423 'oempartitionimg_image': 'generic-oem.gz',
424 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700425 'efipartitionimg_image': 'generic-efi.gz',
426 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700427 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800428 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800429 'firmware_image': 'generic-firmware.gz',
430 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700431 },
432 {
433 'qual_ids': set([6]),
434 'factory_image': '6-factory.gz',
435 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
436 'release_image': '6-release.gz',
437 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
438 'oempartitionimg_image': '6-oem.gz',
439 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700440 'efipartitionimg_image': '6-efi.gz',
441 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700442 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800443 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800444 'firmware_image': '6-firmware.gz',
445 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700446 },
447 ]
448 The server will look for the files by name in the static files
449 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700450
Andrew de los Reyes52620802010-04-12 13:40:07 -0700451 If validate_checksums is True, validates checksums and exits. If
452 a checksum mismatch is found, it's printed to the screen.
453 """
454 f = open(filename, 'r')
455 output = {}
456 exec(f.read(), output)
457 self.factory_config = output['config']
458 success = True
459 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800460 for key in stanza.copy().iterkeys():
461 suffix = '_image'
462 if key.endswith(suffix):
463 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700464 stanza[kind + '_size'] = self._GetSize(os.path.join(
465 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800466 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700467 factory_checksum = self._GetHash(os.path.join(self.static_dir,
468 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800469 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700470 print ('Error: checksum mismatch for %s. Expected "%s" but file '
471 'has checksum "%s".' % (stanza[kind + '_image'],
472 stanza[kind + '_checksum'],
473 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800474 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700475
Andrew de los Reyes52620802010-04-12 13:40:07 -0700476 if validate_checksums:
477 if success is False:
478 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700479
Andrew de los Reyes52620802010-04-12 13:40:07 -0700480 print 'Config file looks good.'
481
482 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700483 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700484 for stanza in self.factory_config:
485 if board_id not in stanza['qual_ids']:
486 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700487 if kind + '_image' not in stanza:
488 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700489 return (stanza[kind + '_image'],
490 stanza[kind + '_checksum'],
491 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700492 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000493
Chris Sosa7c931362010-10-11 19:49:01 -0700494 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700495 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
496 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700497 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700498 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700499 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700500 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700501 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700502 # Factory install is using memento updater which is using the sha-1 hash so
503 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700504 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700505
Chris Sosa151643e2010-10-28 14:40:57 -0700506 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
507 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800508 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800509
Don Garrettf90edf02010-11-16 17:36:14 -0800510 Returns:
511 file name relative to static_image_dir on success.
512 """
Dale Curtis723ec472010-11-30 14:06:47 -0800513 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
514 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
515
Don Garrett0c880e22010-11-17 18:13:37 -0800516 if self.forced_payload:
517 # If the forced payload is not already in our static_image_dir,
518 # copy it there.
Don Garrettee25e552010-11-23 12:09:35 -0800519 src_path = os.path.abspath(self.forced_payload)
Don Garrett0c880e22010-11-17 18:13:37 -0800520
Don Garrettee25e552010-11-23 12:09:35 -0800521 src_stateful = os.path.join(os.path.dirname(src_path),
522 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800523
524 # Only copy the files if the source directory is different from dest.
525 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
526 self._Copy(src_path, dest_path)
527
528 # The stateful payload is optional.
529 if os.path.exists(src_stateful):
530 self._Copy(src_stateful, dest_stateful)
531 else:
532 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
533 STATEFUL_FILE)
534 if os.path.exists(dest_stateful):
535 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800536
Don Garrettfff4c322010-11-19 13:37:12 -0800537 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800538 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800539 return self.GenerateUpdateImageWithCache(
540 self.forced_image,
541 static_image_dir=static_image_dir)
542 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800543 # Warn if update or stateful files can't be found.
544 if not os.path.exists(dest_path):
545 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
546 UPDATE_FILE)
547
548 if not os.path.exists(dest_stateful):
549 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
550 STATEFUL_FILE)
551
552 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800553 else:
554 if board_id:
555 return self.GenerateLatestUpdateImage(board_id,
556 client_version,
557 static_image_dir)
558
559 _LogMessage('You must set --board for pre-generating latest update.')
560 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700561
562 def PreGenerateUpdate(self):
Don Garrettf90edf02010-11-16 17:36:14 -0800563 """Pre-generates an update. Returns True on success.
564 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700565 # Does not work with factory config.
566 assert(not self.factory_config)
567 _LogMessage('Pre-generating the update payload.')
568 # Does not work with labels so just use static dir.
Chris Sosae67b78f2010-11-04 17:33:16 -0700569 if self.GenerateUpdatePayloadForNonFactory(self.board, '0.0.0.0',
570 self.static_dir):
Chris Sosae67b78f2010-11-04 17:33:16 -0700571 _LogMessage('Pre-generated update successfully.')
Don Garrettfff4c322010-11-19 13:37:12 -0800572 self.pregenerated = True
Chris Sosae67b78f2010-11-04 17:33:16 -0700573 return True
Chris Sosa2c048f12010-10-27 16:05:27 -0700574 else:
575 _LogMessage('Failed to pre-generate update.')
Chris Sosae67b78f2010-11-04 17:33:16 -0700576 return False
Chris Sosa2c048f12010-10-27 16:05:27 -0700577
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700578 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700579 """Handles an update ping from an update client.
580
581 Args:
582 data: xml blob from client.
583 label: optional label for the update.
584 Returns:
585 Update payload message for client.
586 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700587 # Set hostname as the hostname that the client is calling to and set up
588 # the url base.
589 self.hostname = cherrypy.request.base
590 if self.urlbase:
591 static_urlbase = self.urlbase
592 elif self.serve_only:
593 static_urlbase = '%s/static/archive' % self.hostname
594 else:
595 static_urlbase = '%s/static' % self.hostname
596
Don Garrett0ad09372010-12-06 16:20:30 -0800597 # If we have a proxy port, adjust the URL we instruct the client to
598 # use to go through the proxy.
599 if self.proxy_port:
600 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
601
Chris Sosa9841e1c2010-10-14 10:51:45 -0700602 _LogMessage('Using static url base %s' % static_urlbase)
603 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700604
605 # Check the client prefix to make sure you can support this type of update.
Chris Sosa9841e1c2010-10-14 10:51:45 -0700606 update_dom = minidom.parseString(data)
607 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700608 if (root.hasAttribute('updaterversion') and
609 not root.getAttribute('updaterversion').startswith(self.client_prefix)):
Chris Sosa7c931362010-10-11 19:49:01 -0700610 _LogMessage('Got update from unsupported updater:' +
Chris Sosa0356d3b2010-09-16 15:46:22 -0700611 root.getAttribute('updaterversion'))
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700612 return self.GetNoUpdatePayload()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700613
614 # We only generate update payloads for updatecheck requests.
615 update_check = root.getElementsByTagName('o:updatecheck')
616 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700617 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700618 # TODO(sosa): Generate correct non-updatecheck payload to better test
619 # update clients.
620 return self.GetNoUpdatePayload()
621
622 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700623 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800624 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700625 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700626 board_id = (query.hasAttribute('board') and query.getAttribute('board')
627 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700628
Chris Sosa0356d3b2010-09-16 15:46:22 -0700629 # Separate logic as Factory requests have static url's that override
630 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700631 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700632 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700633 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700634 static_image_dir = self.static_dir
635 if label:
636 static_image_dir = os.path.join(static_image_dir, label)
637
Don Garrettf90edf02010-11-16 17:36:14 -0800638 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
639 client_version,
640 static_image_dir)
641 if payload_path:
642 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700643 hash = self._GetHash(filename)
644 sha256 = self._GetSHA256(filename)
645 size = self._GetSize(filename)
646 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700647 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800648 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700649 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800650 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700651
Chris Sosa7c931362010-10-11 19:49:01 -0700652 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700653 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700654 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700655 return self.GetNoUpdatePayload()