blob: b02b7f50ea9b2167b435918c30fd52447a6f6d24 [file] [log] [blame]
Darin Petkovc3fd90c2011-05-11 14:23:00 -07001# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
rtc@google.com64244662009-11-12 00:52:08 +00005from buildutil import BuildObject
rtc@google.comded22402009-10-26 22:36:21 +00006from xml.dom import minidom
Don Garrettf90edf02010-11-16 17:36:14 -08007
Chris Sosa7c931362010-10-11 19:49:01 -07008import cherrypy
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07009import json
rtc@google.comded22402009-10-26 22:36:21 +000010import os
Darin Petkov798fe7d2010-03-22 15:18:13 -070011import shutil
Chris Sosa05491b12010-11-08 17:14:16 -080012import subprocess
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070013import time
Don Garrett0ad09372010-12-06 16:20:30 -080014import urlparse
Chris Sosa7c931362010-10-11 19:49:01 -070015
Chris Sosa05491b12010-11-08 17:14:16 -080016
Chris Sosa7c931362010-10-11 19:49:01 -070017def _LogMessage(message):
18 cherrypy.log(message, 'UPDATE')
rtc@google.comded22402009-10-26 22:36:21 +000019
Chris Sosa417e55d2011-01-25 16:40:48 -080020UPDATE_FILE = 'update.gz'
21STATEFUL_FILE = 'stateful.tgz'
22CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 15:46:22 -070023
Don Garrett0ad09372010-12-06 16:20:30 -080024
25def _ChangeUrlPort(url, new_port):
26 """Return the URL passed in with a different port"""
27 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
28 host_port = netloc.split(':')
29
30 if len(host_port) == 1:
31 host_port.append(new_port)
32 else:
33 host_port[1] = new_port
34
35 print host_port
36 netloc = "%s:%s" % tuple(host_port)
37
38 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
39
40
rtc@google.com64244662009-11-12 00:52:08 +000041class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -070042 """Class that contains functionality that handles Chrome OS update pings.
43
44 Members:
Dale Curtis723ec472010-11-30 14:06:47 -080045 serve_only: Serve only pre-built updates. static_dir must contain update.gz
46 and stateful.tgz.
Chris Sosa0356d3b2010-09-16 15:46:22 -070047 factory_config: Path to the factory config file if handling factory
48 requests.
49 use_test_image: Use chromiumos_test_image.bin rather than the standard.
50 static_url_base: base URL, other than devserver, for update images.
Chris Sosa0356d3b2010-09-16 15:46:22 -070051 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,
Greg Spencerc8b59b22011-03-15 14:15:23 -070063 factory_config_path=None,
Don Garrett0c880e22010-11-17 18:13:37 -080064 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 Sosa0f1ec842011-02-14 16:33:22 -080066 copy_to_static_root=True, private_key=None,
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 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
Chris Sosa0f1ec842011-02-14 16:33:22 -080084 self.private_key = private_key
Don Garrettfff4c322010-11-19 13:37:12 -080085
Chris Sosa417e55d2011-01-25 16:40:48 -080086 # Path to pre-generated file.
87 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070088
Dale Curtisc9aaf3a2011-08-09 15:47:40 -070089 # Initialize empty host info cache. Used to keep track of various bits of
90 # information about a given host.
91 self.host_info = {}
92
Chris Sosa0356d3b2010-09-16 15:46:22 -070093 def _GetSecondsSinceMidnight(self):
94 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070095 now = time.localtime()
96 return now[3] * 3600 + now[4] * 60 + now[5]
97
Chris Sosa0356d3b2010-09-16 15:46:22 -070098 def _GetDefaultBoardID(self):
99 """Returns the default board id stored in .default_board."""
100 board_file = '%s/.default_board' % (self.scripts_dir)
101 try:
102 return open(board_file).read()
103 except IOError:
104 return 'x86-generic'
105
106 def _GetLatestImageDir(self, board_id):
107 """Returns the latest image dir based on shell script."""
108 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
109 return os.popen(cmd).read().strip()
110
111 def _GetVersionFromDir(self, image_dir):
112 """Returns the version of the image based on the name of the directory."""
113 latest_version = os.path.basename(image_dir)
114 return latest_version.split('-')[0]
115
116 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800117 """Returns true if the latest_version is greater than the client_version.
118 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700119 client_tokens = client_version.replace('_', '').split('.')
120 latest_tokens = latest_version.replace('_', '').split('.')
Chris Sosa7c931362010-10-11 19:49:01 -0700121 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -0800122 % (client_version, latest_version))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700123 for i in range(4):
124 if int(latest_tokens[i]) == int(client_tokens[i]):
125 continue
126 return int(latest_tokens[i]) > int(client_tokens[i])
127 return False
128
Chris Sosa0356d3b2010-09-16 15:46:22 -0700129 def _UnpackZip(self, image_dir):
130 """Unpacks an image.zip into a given directory."""
131 image = os.path.join(image_dir, self._GetImageName())
132 if os.path.exists(image):
133 return True
134 else:
135 # -n, never clobber an existing file, in case we get invoked
136 # simultaneously by multiple request handlers. This means that
137 # we're assuming each image.zip file lives in a versioned
138 # directory (a la Buildbot).
139 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
140
141 def _GetImageName(self):
142 """Returns the name of the image that should be used."""
143 if self.use_test_image:
144 image_name = 'chromiumos_test_image.bin'
145 else:
146 image_name = 'chromiumos_image.bin'
147 return image_name
148
Chris Sosa0356d3b2010-09-16 15:46:22 -0700149 def _GetSize(self, update_path):
150 """Returns the size of the file given."""
151 return os.path.getsize(update_path)
152
153 def _GetHash(self, update_path):
154 """Returns the sha1 of the file given."""
155 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
156 % update_path)
157 return os.popen(cmd).read().rstrip()
158
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700159 def _IsDeltaFormatFile(self, filename):
160 try:
161 file_handle = open(filename, 'r')
162 delta_magic = 'CrAU'
163 magic = file_handle.read(len(delta_magic))
164 return magic == delta_magic
165 except Exception:
166 return False
167
Darin Petkov91436cb2010-09-28 08:52:17 -0700168 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
169 # it takes advantage of reduced I/O and multiple processors. Something like:
170 # % tee < FILE > /dev/null \
171 # >( openssl dgst -sha256 -binary | openssl base64 ) \
172 # >( openssl sha1 -binary | openssl base64 )
173 def _GetSHA256(self, update_path):
174 """Returns the sha256 of the file given."""
175 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
176 update_path)
177 return os.popen(cmd).read().rstrip()
178
Don Garrettf90edf02010-11-16 17:36:14 -0800179 def _GetMd5(self, update_path):
180 """Returns the md5 checksum of the file given."""
181 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
182 return os.popen(cmd).read().rstrip()
183
Don Garrett0c880e22010-11-17 18:13:37 -0800184 def _Copy(self, source, dest):
185 """Copies a file from dest to source (if different)"""
186 _LogMessage('Copy File %s -> %s' % (source, dest))
187 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800188 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800189 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800190
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700191 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700192 """Returns a payload to the client corresponding to a new update.
193
194 Args:
195 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700196 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700197 size: size of update blob
198 url: where to find update blob
199 Returns:
200 Xml string to be passed back to client.
201 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700202 delta = 'false'
203 if is_delta_format:
204 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000205 payload = """<?xml version="1.0" encoding="UTF-8"?>
206 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700207 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000208 <app appid="{%s}" status="ok">
209 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700210 <updatecheck
211 codebase="%s"
212 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700213 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700214 needsadmin="false"
215 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700216 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000217 status="ok"/>
218 </app>
219 </gupdate>
220 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700221 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700222 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000223
rtc@google.com21a5ca32009-11-04 18:23:23 +0000224 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700225 """Returns a payload to the client corresponding to no update."""
Darin Petkov845f1172011-01-05 14:45:24 -0800226 payload = """<?xml version="1.0" encoding="UTF-8"?>
227 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
228 <daystart elapsed_seconds="%s"/>
229 <app appid="{%s}" status="ok">
230 <ping status="ok"/>
231 <updatecheck status="noupdate"/>
232 </app>
233 </gupdate>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000234 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700235 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000236
Don Garrettf90edf02010-11-16 17:36:14 -0800237 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700238 """Generates an update gz given a full path to an image.
239
240 Args:
241 image_path: Full path to image.
242 Returns:
243 Path to created update_payload or None on error.
244 """
Don Garrettfff4c322010-11-19 13:37:12 -0800245 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa7c931362010-10-11 19:49:01 -0700246 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700247
Chris Sosa0f1ec842011-02-14 16:33:22 -0800248 update_command = [
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100249 '%s/cros_generate_update_payload' % self.devserver_dir,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800250 '--image="%s"' % image_path,
251 '--output="%s"' % update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800252 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700253
Chris Sosa0f1ec842011-02-14 16:33:22 -0800254 if src_image: update_command.append('--src_image="%s"' % src_image)
255 if not self.vm: update_command.append('--patch_kernel')
256 if self.private_key: update_command.append('--private_key="%s"' %
257 self.private_key)
258
259 update_string = ' '.join(update_command)
260 _LogMessage('Running ' + update_string)
261 if os.system(update_string) != 0:
Chris Sosa417e55d2011-01-25 16:40:48 -0800262 _LogMessage('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700263 return None
264
Don Garrettfff4c322010-11-19 13:37:12 -0800265 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700266
Don Garrettf90edf02010-11-16 17:36:14 -0800267 def GenerateStatefulFile(self, image_path, output_dir):
268 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700269
270 Args:
271 image_path: Full path to image.
272 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800273 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800274 Raises:
275 A subprocess exception if the update generator fails to generate a
276 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700277 """
Don Garrettfff4c322010-11-19 13:37:12 -0800278 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800279 subprocess.check_call(
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100280 ['%s/cros_generate_stateful_update_payload' % self.devserver_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800281 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800282 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800283 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800284 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700285
Don Garrettf90edf02010-11-16 17:36:14 -0800286 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
287 """Find directory to store a cached update.
288
289 Given one, or two images for an update, this finds which
290 cache directory should hold the update files, even if they don't exist
291 yet. The directory will be inside static_image_dir, and of the form:
292
293 Non-delta updates:
Chris Sosa417e55d2011-01-25 16:40:48 -0800294 CACHE_DIR/12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800295
296 Delta updates:
Chris Sosa417e55d2011-01-25 16:40:48 -0800297 CACHE_DIR/12345678_12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800298 """
299 # If there is no src, we only have an image file, check image for changes
300 if not src_image:
Chris Sosa417e55d2011-01-25 16:40:48 -0800301 return os.path.join(CACHE_DIR, self._GetMd5(dest_image))
Don Garrettf90edf02010-11-16 17:36:14 -0800302
303 # If we have src and dest, we are a delta, and check both for changes
Chris Sosa417e55d2011-01-25 16:40:48 -0800304 return os.path.join(CACHE_DIR,
Don Garrettf90edf02010-11-16 17:36:14 -0800305 "%s_%s" % (self._GetMd5(src_image),
306 self._GetMd5(dest_image)))
307
Don Garrettfff4c322010-11-19 13:37:12 -0800308 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800309 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700310
Chris Sosade91f672010-11-16 10:05:44 -0800311 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800312 src_image: image we are updating from (Null/empty for non-delta)
313 image_path: full path to the image.
314 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800315 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800316 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800317 """
Don Garrettf90edf02010-11-16 17:36:14 -0800318 update_file = None
319 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700320
Don Garrettf90edf02010-11-16 17:36:14 -0800321 # Actually do the generation
322 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800323 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800324 image_path,
325 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000326
Don Garrettf90edf02010-11-16 17:36:14 -0800327 if update_file:
328 stateful_update_file = self.GenerateStatefulFile(image_path,
329 output_dir)
330
331 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800332 return update_file
Chris Sosa417e55d2011-01-25 16:40:48 -0800333 else:
334 _LogMessage('Failed to generate update.')
335 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800336
337 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
338 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000339
Chris Sosa0356d3b2010-09-16 15:46:22 -0700340 Args:
341 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700342 static_image_dir: the directory to move images to after generating.
343 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800344 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-25 16:40:48 -0800345 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700346 """
Don Garrettf90edf02010-11-16 17:36:14 -0800347 _LogMessage('Generating update for src %s image %s' % (self.src_image,
348 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700349
Chris Sosa417e55d2011-01-25 16:40:48 -0800350 # If it was pregenerated_path, don't regenerate
351 if self.pregenerated_path:
352 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800353
Don Garrettf90edf02010-11-16 17:36:14 -0800354 # Which sub_dir of static_image_dir should hold our cached update image
355 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
356 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
357
Chris Sosa417e55d2011-01-25 16:40:48 -0800358 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
359
Don Garrettf90edf02010-11-16 17:36:14 -0800360 # The cached payloads exist in a cache dir
361 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-25 16:40:48 -0800362 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800363 cache_stateful_payload = os.path.join(static_image_dir,
364 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800365 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800366
Chris Sosa417e55d2011-01-25 16:40:48 -0800367 # Check to see if this cache directory is valid.
368 if not os.path.exists(cache_update_payload) or not os.path.exists(
369 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800370 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800371 # Clean up stale state.
372 os.system('rm -rf "%s"' % full_cache_dir)
373 os.makedirs(full_cache_dir)
374 return_path = self.GenerateUpdateImage(image_path,
375 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800376
Chris Sosa417e55d2011-01-25 16:40:48 -0800377 # Clean up cache dir since it's not valid.
378 if not return_path:
379 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800380 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800381
382 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800383
Chris Sosa08d55a22011-01-19 16:08:02 -0800384 # Generation complete, copy if requested.
385 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800386 # The final results exist directly in static
387 update_payload = os.path.join(static_image_dir,
388 UPDATE_FILE)
389 stateful_payload = os.path.join(static_image_dir,
390 STATEFUL_FILE)
Chris Sosa08d55a22011-01-19 16:08:02 -0800391 self._Copy(cache_update_payload, update_payload)
392 self._Copy(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-25 16:40:48 -0800393 return UPDATE_FILE
394 else:
395 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700396
397 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800398 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700399 """Generates an update using the latest image that has been built.
400
401 This will only generate an update if the newest update is newer than that
402 on the client or client_version is 'ForcedUpdate'.
403
404 Args:
405 board_id: Name of the board.
406 client_version: Current version of the client or 'ForcedUpdate'
407 static_image_dir: the directory to move images to after generating.
408 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800409 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700410 """
411 latest_image_dir = self._GetLatestImageDir(board_id)
412 latest_version = self._GetVersionFromDir(latest_image_dir)
413 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
414
Chris Sosa7c931362010-10-11 19:49:01 -0700415 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700416 latest_image_path)
417
418 # Check to see whether or not we should update.
419 if client_version != 'ForcedUpdate' and not self._CanUpdate(
420 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700421 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800422 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700423
Don Garrettf90edf02010-11-16 17:36:14 -0800424 return self.GenerateUpdateImageWithCache(latest_image_path,
425 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700426
Andrew de los Reyes52620802010-04-12 13:40:07 -0700427 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
428 """Imports a factory-floor server configuration file. The file should
429 be in this format:
430 config = [
431 {
432 'qual_ids': set([1, 2, 3, "x86-generic"]),
433 'factory_image': 'generic-factory.gz',
434 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
435 'release_image': 'generic-release.gz',
436 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
437 'oempartitionimg_image': 'generic-oem.gz',
438 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700439 'efipartitionimg_image': 'generic-efi.gz',
440 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700441 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800442 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800443 'firmware_image': 'generic-firmware.gz',
444 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700445 },
446 {
447 'qual_ids': set([6]),
448 'factory_image': '6-factory.gz',
449 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
450 'release_image': '6-release.gz',
451 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
452 'oempartitionimg_image': '6-oem.gz',
453 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700454 'efipartitionimg_image': '6-efi.gz',
455 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700456 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800457 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800458 'firmware_image': '6-firmware.gz',
459 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700460 },
461 ]
462 The server will look for the files by name in the static files
463 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700464
Andrew de los Reyes52620802010-04-12 13:40:07 -0700465 If validate_checksums is True, validates checksums and exits. If
466 a checksum mismatch is found, it's printed to the screen.
467 """
468 f = open(filename, 'r')
469 output = {}
470 exec(f.read(), output)
471 self.factory_config = output['config']
472 success = True
473 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800474 for key in stanza.copy().iterkeys():
475 suffix = '_image'
476 if key.endswith(suffix):
477 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700478 stanza[kind + '_size'] = self._GetSize(os.path.join(
479 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800480 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700481 factory_checksum = self._GetHash(os.path.join(self.static_dir,
482 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800483 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700484 print ('Error: checksum mismatch for %s. Expected "%s" but file '
485 'has checksum "%s".' % (stanza[kind + '_image'],
486 stanza[kind + '_checksum'],
487 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800488 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700489
Andrew de los Reyes52620802010-04-12 13:40:07 -0700490 if validate_checksums:
491 if success is False:
492 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700493
Andrew de los Reyes52620802010-04-12 13:40:07 -0700494 print 'Config file looks good.'
495
496 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700497 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700498 for stanza in self.factory_config:
499 if board_id not in stanza['qual_ids']:
500 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700501 if kind + '_image' not in stanza:
502 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700503 return (stanza[kind + '_image'],
504 stanza[kind + '_checksum'],
505 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700506 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000507
Chris Sosa7c931362010-10-11 19:49:01 -0700508 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700509 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
510 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700511 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700512 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700513 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700514 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700515 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700516 # Factory install is using memento updater which is using the sha-1 hash so
517 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700518 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700519
Chris Sosa151643e2010-10-28 14:40:57 -0700520 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
521 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800522 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800523
Don Garrettf90edf02010-11-16 17:36:14 -0800524 Returns:
525 file name relative to static_image_dir on success.
526 """
Dale Curtis723ec472010-11-30 14:06:47 -0800527 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
528 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
529
Don Garrett0c880e22010-11-17 18:13:37 -0800530 if self.forced_payload:
531 # If the forced payload is not already in our static_image_dir,
532 # copy it there.
Don Garrettee25e552010-11-23 12:09:35 -0800533 src_path = os.path.abspath(self.forced_payload)
Don Garrettee25e552010-11-23 12:09:35 -0800534 src_stateful = os.path.join(os.path.dirname(src_path),
535 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800536
537 # Only copy the files if the source directory is different from dest.
538 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
539 self._Copy(src_path, dest_path)
540
541 # The stateful payload is optional.
542 if os.path.exists(src_stateful):
543 self._Copy(src_stateful, dest_stateful)
544 else:
545 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
546 STATEFUL_FILE)
547 if os.path.exists(dest_stateful):
548 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800549
Don Garrettfff4c322010-11-19 13:37:12 -0800550 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800551 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800552 return self.GenerateUpdateImageWithCache(
553 self.forced_image,
554 static_image_dir=static_image_dir)
555 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800556 # Warn if update or stateful files can't be found.
557 if not os.path.exists(dest_path):
558 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
559 UPDATE_FILE)
560
561 if not os.path.exists(dest_stateful):
562 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
563 STATEFUL_FILE)
564
565 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800566 else:
567 if board_id:
568 return self.GenerateLatestUpdateImage(board_id,
569 client_version,
570 static_image_dir)
571
Chris Sosa417e55d2011-01-25 16:40:48 -0800572 _LogMessage('Failed to genereate update. '
573 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800574 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700575
576 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800577 """Pre-generates an update and prints out the relative path it.
578
579 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800580 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700581 # Does not work with factory config.
582 assert(not self.factory_config)
583 _LogMessage('Pre-generating the update payload.')
584 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-25 16:40:48 -0800585 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
586 self.board, '0.0.0.0', self.static_dir)
587 if pregenerated_update:
588 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
589
590 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700591
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700592 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700593 """Handles an update ping from an update client.
594
595 Args:
596 data: xml blob from client.
597 label: optional label for the update.
598 Returns:
599 Update payload message for client.
600 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700601 # Set hostname as the hostname that the client is calling to and set up
602 # the url base.
603 self.hostname = cherrypy.request.base
604 if self.urlbase:
605 static_urlbase = self.urlbase
606 elif self.serve_only:
607 static_urlbase = '%s/static/archive' % self.hostname
608 else:
609 static_urlbase = '%s/static' % self.hostname
610
Don Garrett0ad09372010-12-06 16:20:30 -0800611 # If we have a proxy port, adjust the URL we instruct the client to
612 # use to go through the proxy.
613 if self.proxy_port:
614 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
615
Chris Sosa9841e1c2010-10-14 10:51:45 -0700616 _LogMessage('Using static url base %s' % static_urlbase)
617 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700618
Chris Sosa9841e1c2010-10-14 10:51:45 -0700619 update_dom = minidom.parseString(data)
620 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700621
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700622 # Determine request IP, strip any IPv6 data for simplicity.
623 client_ip = cherrypy.request.remote.ip.split(':')[-1]
624
625 # Initialize host info dictionary for this client if it doesn't exist.
626 self.host_info.setdefault(client_ip, {})
627
628 # Store event details in the host info dictionary for API usage.
629 event = root.getElementsByTagName('o:event')
630 if event:
631 self.host_info[client_ip]['last_event_status'] = (
632 int(event[0].getAttribute('eventresult')))
633 self.host_info[client_ip]['last_event_type'] = (
634 int(event[0].getAttribute('eventtype')))
635
Chris Sosa0356d3b2010-09-16 15:46:22 -0700636 # We only generate update payloads for updatecheck requests.
637 update_check = root.getElementsByTagName('o:updatecheck')
638 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700639 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700640 # TODO(sosa): Generate correct non-updatecheck payload to better test
641 # update clients.
642 return self.GetNoUpdatePayload()
643
644 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700645 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800646 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700647 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700648 board_id = (query.hasAttribute('board') and query.getAttribute('board')
649 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700650
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700651 # Store version for this host in the cache.
652 self.host_info[client_ip]['last_known_version'] = client_version
653
654 # Check if an update has been forced for this client.
655 forced_update = self.host_info[client_ip].pop('forced_update_label', None)
656 if forced_update:
657 label = forced_update
658
Chris Sosa0356d3b2010-09-16 15:46:22 -0700659 # Separate logic as Factory requests have static url's that override
660 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700661 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700662 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700663 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700664 static_image_dir = self.static_dir
665 if label:
666 static_image_dir = os.path.join(static_image_dir, label)
667
Don Garrettf90edf02010-11-16 17:36:14 -0800668 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
669 client_version,
670 static_image_dir)
671 if payload_path:
672 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700673 hash = self._GetHash(filename)
674 sha256 = self._GetSHA256(filename)
675 size = self._GetSize(filename)
676 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700677 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800678 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700679 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800680 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700681
Chris Sosa7c931362010-10-11 19:49:01 -0700682 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700683 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700684 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700685 return self.GetNoUpdatePayload()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700686
687 def HandleHostInfoPing(self, ip):
688 """Returns host info dictionary for the given IP in JSON format."""
689 assert ip, 'No ip provided.'
690 if ip in self.host_info:
691 return json.dumps(self.host_info[ip])
692
693 def HandleSetUpdatePing(self, ip, label):
694 """Sets forced_update_label for a given host."""
695 assert ip, 'No ip provided.'
696 assert label, 'No label provided.'
697 self.host_info.setdefault(ip, {})['forced_update_label'] = label