blob: 451c139584fdef37321fa62f6a462c45d0a86232 [file] [log] [blame]
rtc@google.comded22402009-10-26 22:36:21 +00001# Copyright (c) 2009 The Chromium OS Authors. All rights reserved.
2# 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
7
8import os
Darin Petkov798fe7d2010-03-22 15:18:13 -07009import shutil
Andrew de los Reyes52620802010-04-12 13:40:07 -070010import sys
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070011import time
rtc@google.comded22402009-10-26 22:36:21 +000012import web
13
rtc@google.com64244662009-11-12 00:52:08 +000014class Autoupdate(BuildObject):
Darin Petkov798fe7d2010-03-22 15:18:13 -070015 # Basic functionality of handling ChromeOS autoupdate pings
rtc@google.com21a5ca32009-11-04 18:23:23 +000016 # and building/serving update images.
17 # TODO(rtc): Clean this code up and write some tests.
rtc@google.comded22402009-10-26 22:36:21 +000018
Sean O'Connor1f7fd362010-04-07 16:34:52 -070019 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Andrew de los Reyes52620802010-04-12 13:40:07 -070020 factory_config_path=None, validate_factory_config=None,
Andrew de los Reyesfb4444b2010-06-29 18:11:28 -070021 client_prefix=None,
Sean O'Connor1f7fd362010-04-07 16:34:52 -070022 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070023 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070024 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -070025 self.factory_config = factory_config_path
Chris Sosaa73ec162010-05-03 20:18:02 -070026 self.test_image = test_image
Sean O'Connor1f7fd362010-04-07 16:34:52 -070027 self.static_urlbase = urlbase
Chris Sosab63a9282010-09-02 10:43:23 -070028 self.client_prefix = client_prefix
Sean O'Connor1f7fd362010-04-07 16:34:52 -070029 if serve_only:
30 # If we're serving out of an archived build dir (e.g. a
31 # buildbot), prepare this webserver's magic 'static/' dir with a
32 # link to the build archive.
33 web.debug('Autoupdate in "serve update images only" mode.')
34 if os.path.exists('static/archive'):
Sean O'Connor1b4b0762010-06-02 17:37:32 -070035 if self.static_dir != os.readlink('static/archive'):
Sean O'Connor1f7fd362010-04-07 16:34:52 -070036 web.debug('removing stale symlink to %s' % self.static_dir)
37 os.unlink('static/archive')
Sean O'Connor1b4b0762010-06-02 17:37:32 -070038 os.symlink(self.static_dir, 'static/archive')
Sean O'Connor1f7fd362010-04-07 16:34:52 -070039 else:
Sean O'Connor1b4b0762010-06-02 17:37:32 -070040 os.symlink(self.static_dir, 'static/archive')
Andrew de los Reyes52620802010-04-12 13:40:07 -070041 if factory_config_path is not None:
42 self.ImportFactoryConfigFile(factory_config_path, validate_factory_config)
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070043
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070044 def GetSecondsSinceMidnight(self):
45 now = time.localtime()
46 return now[3] * 3600 + now[4] * 60 + now[5]
47
rtc@google.com21a5ca32009-11-04 18:23:23 +000048 def GetUpdatePayload(self, hash, size, url):
49 payload = """<?xml version="1.0" encoding="UTF-8"?>
50 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070051 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +000052 <app appid="{%s}" status="ok">
53 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070054 <updatecheck
55 codebase="%s"
56 hash="%s"
57 needsadmin="false"
58 size="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +000059 status="ok"/>
60 </app>
61 </gupdate>
62 """
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070063 return payload % (self.GetSecondsSinceMidnight(),
64 self.app_id, url, hash, size)
rtc@google.comded22402009-10-26 22:36:21 +000065
rtc@google.com21a5ca32009-11-04 18:23:23 +000066 def GetNoUpdatePayload(self):
67 payload = """<?xml version="1.0" encoding="UTF-8"?>
68 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070069 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +000070 <app appid="{%s}" status="ok">
71 <ping status="ok"/>
72 <updatecheck status="noupdate"/>
73 </app>
74 </gupdate>
75 """
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070076 return payload % (self.GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +000077
Andrew de los Reyes9a528712010-06-30 10:29:43 -070078 def GetDefaultBoardID(self):
79 board_file = '%s/.default_board' % (self.scripts_dir)
80 try:
81 return open(board_file).read()
82 except IOError:
83 return 'x86-generic'
84
Sam Leffler76382042010-02-18 09:58:42 -080085 def GetLatestImagePath(self, board_id):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070086 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
rtc@google.com21a5ca32009-11-04 18:23:23 +000087 return os.popen(cmd).read().strip()
rtc@google.comded22402009-10-26 22:36:21 +000088
rtc@google.com21a5ca32009-11-04 18:23:23 +000089 def GetLatestVersion(self, latest_image_path):
90 latest_version = latest_image_path.split('/')[-1]
Ryan Cairns1b05beb2010-02-05 17:05:24 -080091
92 # Removes the portage build prefix.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070093 latest_version = latest_version.lstrip('g-')
rtc@google.com21a5ca32009-11-04 18:23:23 +000094 return latest_version.split('-')[0]
rtc@google.comded22402009-10-26 22:36:21 +000095
rtc@google.com21a5ca32009-11-04 18:23:23 +000096 def CanUpdate(self, client_version, latest_version):
97 """
98 Returns true iff the latest_version is greater than the client_version.
99 """
Chris Sosab63a9282010-09-02 10:43:23 -0700100 client_tokens = client_version.replace('_', '').split('.')
101 latest_tokens = latest_version.replace('_', '').split('.')
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700102 web.debug('client version %s latest version %s' \
Charlie Lee8c993082010-02-24 13:27:37 -0800103 % (client_version, latest_version))
Chris Sosaa73ec162010-05-03 20:18:02 -0700104 for i in range(4):
rtc@google.com21a5ca32009-11-04 18:23:23 +0000105 if int(latest_tokens[i]) == int(client_tokens[i]):
106 continue
107 return int(latest_tokens[i]) > int(client_tokens[i])
rtc@google.comded22402009-10-26 22:36:21 +0000108 return False
rtc@google.comded22402009-10-26 22:36:21 +0000109
Chris Sosab63a9282010-09-02 10:43:23 -0700110 def UnpackStatefulPartition(self, image_path, image_file, stateful_file):
111 """Given an image, unpacks the stateful partition to stateful_file."""
112 stateful_part = "part_1"
113 unpack_command = (
114 'cd %s && '
115 '$(grep %s\ unpack_partitions.sh | sed s/\\"\$TARGET\\"/%s/)' %
116 (image_path, stateful_part, image_file))
117 web.debug(unpack_command)
Chris Sosaa73ec162010-05-03 20:18:02 -0700118 if os.system(unpack_command) == 0:
Chris Sosab63a9282010-09-02 10:43:23 -0700119 shutil.move(os.path.join(image_path, stateful_part), stateful_file)
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700120 return True
Chris Sosaa73ec162010-05-03 20:18:02 -0700121 return False
122
123 def UnpackZip(self, image_path, image_file):
Sean O'Connora7f867e2010-05-27 17:53:32 -0700124 image = os.path.join(image_path, image_file)
125 if os.path.exists(image):
126 return True
127 else:
Sean O'Connor1b4b0762010-06-02 17:37:32 -0700128 # -n, never clobber an existing file, in case we get invoked
129 # simultaneously by multiple request handlers. This means that
130 # we're assuming each image.zip file lives in a versioned
131 # directory (a la Buildbot).
132 return os.system('cd %s && unzip -n image.zip %s unpack_partitions.sh' %
Sean O'Connora7f867e2010-05-27 17:53:32 -0700133 (image_path, image_file)) == 0
Chris Sosaa73ec162010-05-03 20:18:02 -0700134
135 def GetImageBinPath(self, image_path):
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700136 if self.test_image:
137 image_file = 'chromiumos_test_image.bin'
138 else:
139 image_file = 'chromiumos_image.bin'
Chris Sosaa73ec162010-05-03 20:18:02 -0700140 return image_file
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700141
rtc@google.com21a5ca32009-11-04 18:23:23 +0000142 def BuildUpdateImage(self, image_path):
Chris Sosaa73ec162010-05-03 20:18:02 -0700143 stateful_file = '%s/stateful.image' % image_path
Chris Sosaa73ec162010-05-03 20:18:02 -0700144 image_file = self.GetImageBinPath(image_path)
145 bin_path = os.path.join(image_path, image_file)
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700146
Chris Sosaa73ec162010-05-03 20:18:02 -0700147 # Get appropriate update.gz to compare timestamps.
148 if self.serve_only:
149 cached_update_file = os.path.join(image_path, 'update.gz')
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700150 else:
Chris Sosaa73ec162010-05-03 20:18:02 -0700151 cached_update_file = os.path.join(self.static_dir, 'update.gz')
152
Chris Sosab63a9282010-09-02 10:43:23 -0700153 # If the new chromiumos image is newer, re-create everything.
Chris Sosaa73ec162010-05-03 20:18:02 -0700154 if (os.path.exists(cached_update_file) and
155 os.path.getmtime(cached_update_file) >= os.path.getmtime(bin_path)):
156 web.debug('Using cached update image at %s instead of %s' %
157 (cached_update_file, bin_path))
158 else:
159 # Unpack zip file if we are serving from a directory.
160 if self.serve_only and not self.UnpackZip(image_path, image_file):
161 web.debug('unzip image.zip failed.')
rtc@google.com21a5ca32009-11-04 18:23:23 +0000162 return False
Chris Sosaa73ec162010-05-03 20:18:02 -0700163
Chris Sosaa73ec162010-05-03 20:18:02 -0700164 update_file = os.path.join(image_path, 'update.gz')
165 web.debug('Generating update image %s' % update_file)
Chris Sosab63a9282010-09-02 10:43:23 -0700166 mkupdate_command = (
167 '%s/cros_generate_update_payload --image=%s --output=%s' %
168 (self.scripts_dir, bin_path, update_file))
Chris Sosaa73ec162010-05-03 20:18:02 -0700169 if os.system(mkupdate_command) != 0:
170 web.debug('Failed to create update image')
171 return False
172
Chris Sosab63a9282010-09-02 10:43:23 -0700173 # Unpack to get stateful partition.
174 if not self.UnpackStatefulPartition(image_path, image_file,
175 stateful_file):
176 web.debug('Failed to unpack stateful partition.')
177 return False
178
Sean O'Connora7f867e2010-05-27 17:53:32 -0700179 mkstatefulupdate_command = 'gzip -f %s' % stateful_file
Chris Sosaa73ec162010-05-03 20:18:02 -0700180 if os.system(mkstatefulupdate_command) != 0:
Chris Sosab63a9282010-09-02 10:43:23 -0700181 web.debug('Failed to create stateful update gz')
Chris Sosaa73ec162010-05-03 20:18:02 -0700182 return False
183
184 # Add gz suffix
185 stateful_file = '%s.gz' % stateful_file
186
Chris Sosab63a9282010-09-02 10:43:23 -0700187 # Cleanup of image files.
Chris Sosaa73ec162010-05-03 20:18:02 -0700188 if not self.serve_only:
189 try:
190 web.debug('Found a new image to serve, copying it to static')
191 shutil.copy(update_file, self.static_dir)
192 shutil.copy(stateful_file, self.static_dir)
193 os.remove(update_file)
194 os.remove(stateful_file)
195 except Exception, e:
196 web.debug('%s' % e)
197 return False
rtc@google.com21a5ca32009-11-04 18:23:23 +0000198 return True
rtc@google.comded22402009-10-26 22:36:21 +0000199
rtc@google.com21a5ca32009-11-04 18:23:23 +0000200 def GetSize(self, update_path):
201 return os.path.getsize(update_path)
rtc@google.comded22402009-10-26 22:36:21 +0000202
rtc@google.com21a5ca32009-11-04 18:23:23 +0000203 def GetHash(self, update_path):
Darin Petkov8ef83452010-03-23 16:52:29 -0700204 cmd = "cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';" \
205 % update_path
Andrew de los Reyes52620802010-04-12 13:40:07 -0700206 return os.popen(cmd).read().rstrip()
Darin Petkov8ef83452010-03-23 16:52:29 -0700207
Andrew de los Reyes52620802010-04-12 13:40:07 -0700208 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
209 """Imports a factory-floor server configuration file. The file should
210 be in this format:
211 config = [
212 {
213 'qual_ids': set([1, 2, 3, "x86-generic"]),
214 'factory_image': 'generic-factory.gz',
215 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
216 'release_image': 'generic-release.gz',
217 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
218 'oempartitionimg_image': 'generic-oem.gz',
219 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700220 'efipartitionimg_image': 'generic-efi.gz',
221 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700222 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800223 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800224 'firmware_image': 'generic-firmware.gz',
225 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700226 },
227 {
228 'qual_ids': set([6]),
229 'factory_image': '6-factory.gz',
230 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
231 'release_image': '6-release.gz',
232 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
233 'oempartitionimg_image': '6-oem.gz',
234 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700235 'efipartitionimg_image': '6-efi.gz',
236 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700237 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800238 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800239 'firmware_image': '6-firmware.gz',
240 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700241 },
242 ]
243 The server will look for the files by name in the static files
244 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700245
Andrew de los Reyes52620802010-04-12 13:40:07 -0700246 If validate_checksums is True, validates checksums and exits. If
247 a checksum mismatch is found, it's printed to the screen.
248 """
249 f = open(filename, 'r')
250 output = {}
251 exec(f.read(), output)
252 self.factory_config = output['config']
253 success = True
254 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800255 for key in stanza.copy().iterkeys():
256 suffix = '_image'
257 if key.endswith(suffix):
258 kind = key[:-len(suffix)]
259 stanza[kind + '_size'] = \
260 os.path.getsize(self.static_dir + '/' + stanza[kind + '_image'])
261 if validate_checksums:
262 factory_checksum = self.GetHash(self.static_dir + '/' +
263 stanza[kind + '_image'])
264 if factory_checksum != stanza[kind + '_checksum']:
265 print 'Error: checksum mismatch for %s. Expected "%s" but file ' \
266 'has checksum "%s".' % (stanza[kind + '_image'],
267 stanza[kind + '_checksum'],
268 factory_checksum)
269 success = False
Andrew de los Reyes52620802010-04-12 13:40:07 -0700270 if validate_checksums:
271 if success is False:
272 raise Exception('Checksum mismatch in conf file.')
273 print 'Config file looks good.'
274
275 def GetFactoryImage(self, board_id, channel):
276 kind = channel.rsplit('-', 1)[0]
277 for stanza in self.factory_config:
278 if board_id not in stanza['qual_ids']:
279 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700280 if kind + '_image' not in stanza:
281 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700282 return (stanza[kind + '_image'],
283 stanza[kind + '_checksum'],
284 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700285 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000286
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700287 def HandleUpdatePing(self, data, label=None):
Darin Petkovd35ee242010-07-14 16:45:31 -0700288 web.debug('handle update ping: %s' % data)
rtc@google.com21a5ca32009-11-04 18:23:23 +0000289 update_dom = minidom.parseString(data)
290 root = update_dom.firstChild
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700291 if root.hasAttribute('updaterversion') and \
292 not root.getAttribute('updaterversion').startswith(
Andrew de los Reyesfb4444b2010-06-29 18:11:28 -0700293 self.client_prefix):
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700294 web.debug('Got update from unsupported updater:' + \
295 root.getAttribute('updaterversion'))
296 return self.GetNoUpdatePayload()
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700297 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800298 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700299 channel = query.getAttribute('track')
Charlie Lee8c993082010-02-24 13:27:37 -0800300 board_id = query.hasAttribute('board') and query.getAttribute('board') \
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700301 or self.GetDefaultBoardID()
Charlie Lee8c993082010-02-24 13:27:37 -0800302 latest_image_path = self.GetLatestImagePath(board_id)
303 latest_version = self.GetLatestVersion(latest_image_path)
Andrew de los Reyes52620802010-04-12 13:40:07 -0700304 hostname = web.ctx.host
305
306 # If this is a factory floor server, return the image here:
307 if self.factory_config:
308 (filename, checksum, size) = \
309 self.GetFactoryImage(board_id, channel)
310 if filename is None:
311 web.debug('unable to find image for board %s' % board_id)
312 return self.GetNoUpdatePayload()
313 url = 'http://%s/static/%s' % (hostname, filename)
314 web.debug('returning update payload ' + url)
315 return self.GetUpdatePayload(checksum, size, url)
316
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700317 if client_version != 'ForcedUpdate' \
Charlie Lee8c993082010-02-24 13:27:37 -0800318 and not self.CanUpdate(client_version, latest_version):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700319 web.debug('no update')
rtc@google.com21a5ca32009-11-04 18:23:23 +0000320 return self.GetNoUpdatePayload()
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700321 if label:
322 web.debug('Client requested version %s' % label)
323 # Check that matching build exists
324 image_path = '%s/%s' % (self.static_dir, label)
325 if not os.path.exists(image_path):
326 web.debug('%s not found.' % image_path)
327 return self.GetNoUpdatePayload()
328 # Construct a response
329 ok = self.BuildUpdateImage(image_path)
330 if ok != True:
331 web.debug('Failed to build an update image')
332 return self.GetNoUpdatePayload()
333 web.debug('serving update: ')
334 hash = self.GetHash('%s/%s/update.gz' % (self.static_dir, label))
335 size = self.GetSize('%s/%s/update.gz' % (self.static_dir, label))
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700336 # In case we configured images to be hosted elsewhere
337 # (e.g. buildbot's httpd), use that. Otherwise, serve it
338 # ourselves using web.py's static resource handler.
339 if self.static_urlbase:
340 urlbase = self.static_urlbase
341 else:
342 urlbase = 'http://%s/static/archive/' % hostname
343
344 url = '%s/%s/update.gz' % (urlbase, label)
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700345 return self.GetUpdatePayload(hash, size, url)
Chris Sosaa73ec162010-05-03 20:18:02 -0700346 web.debug('DONE')
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700347 else:
348 web.debug('update found %s ' % latest_version)
349 ok = self.BuildUpdateImage(latest_image_path)
350 if ok != True:
351 web.debug('Failed to build an update image')
352 return self.GetNoUpdatePayload()
rtc@google.comded22402009-10-26 22:36:21 +0000353
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700354 hash = self.GetHash('%s/update.gz' % self.static_dir)
355 size = self.GetSize('%s/update.gz' % self.static_dir)
356
357 url = 'http://%s/static/update.gz' % hostname
358 return self.GetUpdatePayload(hash, size, url)