blob: 0dd296c12d7fd181284d33dfb3ae7b4a138b2246 [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
rtc@google.comded22402009-10-26 22:36:21 +000011import web
12
rtc@google.com64244662009-11-12 00:52:08 +000013class Autoupdate(BuildObject):
Darin Petkov798fe7d2010-03-22 15:18:13 -070014 # Basic functionality of handling ChromeOS autoupdate pings
rtc@google.com21a5ca32009-11-04 18:23:23 +000015 # and building/serving update images.
16 # TODO(rtc): Clean this code up and write some tests.
rtc@google.comded22402009-10-26 22:36:21 +000017
Sean O'Connor1f7fd362010-04-07 16:34:52 -070018 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Andrew de los Reyes52620802010-04-12 13:40:07 -070019 factory_config_path=None, validate_factory_config=None,
Sean O'Connor1f7fd362010-04-07 16:34:52 -070020 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070021 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070022 self.serve_only = serve_only
Chris Sosaa73ec162010-05-03 20:18:02 -070023 self.test_image = test_image
Sean O'Connor1f7fd362010-04-07 16:34:52 -070024 self.static_urlbase = urlbase
25 if serve_only:
26 # If we're serving out of an archived build dir (e.g. a
27 # buildbot), prepare this webserver's magic 'static/' dir with a
28 # link to the build archive.
29 web.debug('Autoupdate in "serve update images only" mode.')
30 if os.path.exists('static/archive'):
31 archive_symlink = os.readlink('static/archive')
32 if archive_symlink != self.static_dir:
33 web.debug('removing stale symlink to %s' % self.static_dir)
34 os.unlink('static/archive')
35 else:
36 os.symlink(self.static_dir, 'static/archive')
Darin Petkov98da5db2010-04-13 10:11:40 -070037 self.factory_config = None
Andrew de los Reyes52620802010-04-12 13:40:07 -070038 if factory_config_path is not None:
39 self.ImportFactoryConfigFile(factory_config_path, validate_factory_config)
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070040
rtc@google.com21a5ca32009-11-04 18:23:23 +000041 def GetUpdatePayload(self, hash, size, url):
42 payload = """<?xml version="1.0" encoding="UTF-8"?>
43 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
44 <app appid="{%s}" status="ok">
45 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070046 <updatecheck
47 codebase="%s"
48 hash="%s"
49 needsadmin="false"
50 size="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +000051 status="ok"/>
52 </app>
53 </gupdate>
54 """
55 return payload % (self.app_id, url, hash, size)
rtc@google.comded22402009-10-26 22:36:21 +000056
rtc@google.com21a5ca32009-11-04 18:23:23 +000057 def GetNoUpdatePayload(self):
58 payload = """<?xml version="1.0" encoding="UTF-8"?>
59 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
60 <app appid="{%s}" status="ok">
61 <ping status="ok"/>
62 <updatecheck status="noupdate"/>
63 </app>
64 </gupdate>
65 """
66 return payload % self.app_id
rtc@google.comded22402009-10-26 22:36:21 +000067
Sam Leffler76382042010-02-18 09:58:42 -080068 def GetLatestImagePath(self, board_id):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070069 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
rtc@google.com21a5ca32009-11-04 18:23:23 +000070 return os.popen(cmd).read().strip()
rtc@google.comded22402009-10-26 22:36:21 +000071
rtc@google.com21a5ca32009-11-04 18:23:23 +000072 def GetLatestVersion(self, latest_image_path):
73 latest_version = latest_image_path.split('/')[-1]
Ryan Cairns1b05beb2010-02-05 17:05:24 -080074
75 # Removes the portage build prefix.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070076 latest_version = latest_version.lstrip('g-')
rtc@google.com21a5ca32009-11-04 18:23:23 +000077 return latest_version.split('-')[0]
rtc@google.comded22402009-10-26 22:36:21 +000078
rtc@google.com21a5ca32009-11-04 18:23:23 +000079 def CanUpdate(self, client_version, latest_version):
80 """
81 Returns true iff the latest_version is greater than the client_version.
82 """
Vincent Scheib904c6642010-05-18 14:57:39 -070083 client_tokens = client_version.replace('_','').split('.')
84 latest_tokens = latest_version.replace('_','').split('.')
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070085 web.debug('client version %s latest version %s' \
Charlie Lee8c993082010-02-24 13:27:37 -080086 % (client_version, latest_version))
Chris Sosaa73ec162010-05-03 20:18:02 -070087 for i in range(4):
rtc@google.com21a5ca32009-11-04 18:23:23 +000088 if int(latest_tokens[i]) == int(client_tokens[i]):
89 continue
90 return int(latest_tokens[i]) > int(client_tokens[i])
rtc@google.comded22402009-10-26 22:36:21 +000091 return False
rtc@google.comded22402009-10-26 22:36:21 +000092
Chris Sosaa73ec162010-05-03 20:18:02 -070093 def UnpackImage(self, image_path, image_file, stateful_file, kernel_file, rootfs_file):
94 unpack_command = 'cd %s && ./unpack_partitions.sh %s' % \
95 (image_path, image_file)
96 if os.system(unpack_command) == 0:
97 shutil.move(os.path.join(image_path, 'part_1'), stateful_file)
98 shutil.move(os.path.join(image_path, 'part_2'), kernel_file)
99 shutil.move(os.path.join(image_path, 'part_3'), rootfs_file)
100 os.system('cd %s && rm part_*' % image_path)
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700101 return True
Chris Sosaa73ec162010-05-03 20:18:02 -0700102 return False
103
104 def UnpackZip(self, image_path, image_file):
105 return os.system('cd %s && unzip -o image.zip %s unpack_partitions.sh' % \
106 (image_path, image_file)) == 0
107
108 def GetImageBinPath(self, image_path):
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700109 if self.test_image:
110 image_file = 'chromiumos_test_image.bin'
111 else:
112 image_file = 'chromiumos_image.bin'
Chris Sosaa73ec162010-05-03 20:18:02 -0700113 return image_file
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700114
rtc@google.com21a5ca32009-11-04 18:23:23 +0000115 def BuildUpdateImage(self, image_path):
Chris Sosaa73ec162010-05-03 20:18:02 -0700116 stateful_file = '%s/stateful.image' % image_path
Darin Petkov55604f12010-04-12 11:09:25 -0700117 kernel_file = '%s/kernel.image' % image_path
118 rootfs_file = '%s/rootfs.image' % image_path
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700119
Chris Sosaa73ec162010-05-03 20:18:02 -0700120 image_file = self.GetImageBinPath(image_path)
121 bin_path = os.path.join(image_path, image_file)
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700122
Chris Sosaa73ec162010-05-03 20:18:02 -0700123 # Get appropriate update.gz to compare timestamps.
124 if self.serve_only:
125 cached_update_file = os.path.join(image_path, 'update.gz')
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700126 else:
Chris Sosaa73ec162010-05-03 20:18:02 -0700127 cached_update_file = os.path.join(self.static_dir, 'update.gz')
128
129 # Check whether we need to re-create if the original image is newer.
130 if (os.path.exists(cached_update_file) and
131 os.path.getmtime(cached_update_file) >= os.path.getmtime(bin_path)):
132 web.debug('Using cached update image at %s instead of %s' %
133 (cached_update_file, bin_path))
134 else:
135 # Unpack zip file if we are serving from a directory.
136 if self.serve_only and not self.UnpackZip(image_path, image_file):
137 web.debug('unzip image.zip failed.')
rtc@google.com21a5ca32009-11-04 18:23:23 +0000138 return False
Chris Sosaa73ec162010-05-03 20:18:02 -0700139
140 if not self.UnpackImage(image_path, image_file, stateful_file,
141 kernel_file, rootfs_file):
142 web.debug('Failed to unpack image.')
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700143 return False
Chris Sosaa73ec162010-05-03 20:18:02 -0700144
145 update_file = os.path.join(image_path, 'update.gz')
146 web.debug('Generating update image %s' % update_file)
147 mkupdate_command = '%s/mk_memento_images.sh %s %s' % \
148 (self.scripts_dir, kernel_file, rootfs_file)
149 if os.system(mkupdate_command) != 0:
150 web.debug('Failed to create update image')
151 return False
152
153 mkstatefulupdate_command = 'gzip %s' % stateful_file
154 if os.system(mkstatefulupdate_command) != 0:
155 web.debug('Failed to create stateful update image')
156 return False
157
158 # Add gz suffix
159 stateful_file = '%s.gz' % stateful_file
160
161 # Cleanup of image files
162 os.remove(kernel_file)
163 os.remove(rootfs_file)
164 if not self.serve_only:
165 try:
166 web.debug('Found a new image to serve, copying it to static')
167 shutil.copy(update_file, self.static_dir)
168 shutil.copy(stateful_file, self.static_dir)
169 os.remove(update_file)
170 os.remove(stateful_file)
171 except Exception, e:
172 web.debug('%s' % e)
173 return False
rtc@google.com21a5ca32009-11-04 18:23:23 +0000174 return True
rtc@google.comded22402009-10-26 22:36:21 +0000175
rtc@google.com21a5ca32009-11-04 18:23:23 +0000176 def GetSize(self, update_path):
177 return os.path.getsize(update_path)
rtc@google.comded22402009-10-26 22:36:21 +0000178
rtc@google.com21a5ca32009-11-04 18:23:23 +0000179 def GetHash(self, update_path):
Darin Petkov8ef83452010-03-23 16:52:29 -0700180 cmd = "cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';" \
181 % update_path
Andrew de los Reyes52620802010-04-12 13:40:07 -0700182 return os.popen(cmd).read().rstrip()
Darin Petkov8ef83452010-03-23 16:52:29 -0700183
Andrew de los Reyes52620802010-04-12 13:40:07 -0700184 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
185 """Imports a factory-floor server configuration file. The file should
186 be in this format:
187 config = [
188 {
189 'qual_ids': set([1, 2, 3, "x86-generic"]),
190 'factory_image': 'generic-factory.gz',
191 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
192 'release_image': 'generic-release.gz',
193 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
194 'oempartitionimg_image': 'generic-oem.gz',
195 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
196 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800197 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
198 'systemrom_image': 'generic-systemrom.gz',
199 'systemrom_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
200 'ecrom_image': 'generic-ecrom.gz',
201 'ecrom_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700202 },
203 {
204 'qual_ids': set([6]),
205 'factory_image': '6-factory.gz',
206 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
207 'release_image': '6-release.gz',
208 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
209 'oempartitionimg_image': '6-oem.gz',
210 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
211 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800212 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
213 'systemrom_image': '6-systemrom.gz',
214 'systemrom_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
215 'ecrom_image': '6-ecrom.gz',
216 'ecrom_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700217 },
218 ]
219 The server will look for the files by name in the static files
220 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700221
Andrew de los Reyes52620802010-04-12 13:40:07 -0700222 If validate_checksums is True, validates checksums and exits. If
223 a checksum mismatch is found, it's printed to the screen.
224 """
225 f = open(filename, 'r')
226 output = {}
227 exec(f.read(), output)
228 self.factory_config = output['config']
229 success = True
230 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800231 for key in stanza.copy().iterkeys():
232 suffix = '_image'
233 if key.endswith(suffix):
234 kind = key[:-len(suffix)]
235 stanza[kind + '_size'] = \
236 os.path.getsize(self.static_dir + '/' + stanza[kind + '_image'])
237 if validate_checksums:
238 factory_checksum = self.GetHash(self.static_dir + '/' +
239 stanza[kind + '_image'])
240 if factory_checksum != stanza[kind + '_checksum']:
241 print 'Error: checksum mismatch for %s. Expected "%s" but file ' \
242 'has checksum "%s".' % (stanza[kind + '_image'],
243 stanza[kind + '_checksum'],
244 factory_checksum)
245 success = False
Andrew de los Reyes52620802010-04-12 13:40:07 -0700246 if validate_checksums:
247 if success is False:
248 raise Exception('Checksum mismatch in conf file.')
249 print 'Config file looks good.'
250
251 def GetFactoryImage(self, board_id, channel):
252 kind = channel.rsplit('-', 1)[0]
253 for stanza in self.factory_config:
254 if board_id not in stanza['qual_ids']:
255 continue
256 return (stanza[kind + '_image'],
257 stanza[kind + '_checksum'],
258 stanza[kind + '_size'])
rtc@google.comded22402009-10-26 22:36:21 +0000259
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700260 def HandleUpdatePing(self, data, label=None):
Andrew de los Reyes52620802010-04-12 13:40:07 -0700261 web.debug('handle update ping')
rtc@google.com21a5ca32009-11-04 18:23:23 +0000262 update_dom = minidom.parseString(data)
263 root = update_dom.firstChild
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700264 if root.hasAttribute('updaterversion') and \
265 not root.getAttribute('updaterversion').startswith(
266 'MementoSoftwareUpdate'):
267 web.debug('Got update from unsupported updater:' + \
268 root.getAttribute('updaterversion'))
269 return self.GetNoUpdatePayload()
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700270 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800271 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700272 channel = query.getAttribute('track')
Charlie Lee8c993082010-02-24 13:27:37 -0800273 board_id = query.hasAttribute('board') and query.getAttribute('board') \
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700274 or 'x86-generic'
Charlie Lee8c993082010-02-24 13:27:37 -0800275 latest_image_path = self.GetLatestImagePath(board_id)
276 latest_version = self.GetLatestVersion(latest_image_path)
Andrew de los Reyes52620802010-04-12 13:40:07 -0700277 hostname = web.ctx.host
278
279 # If this is a factory floor server, return the image here:
280 if self.factory_config:
281 (filename, checksum, size) = \
282 self.GetFactoryImage(board_id, channel)
283 if filename is None:
284 web.debug('unable to find image for board %s' % board_id)
285 return self.GetNoUpdatePayload()
286 url = 'http://%s/static/%s' % (hostname, filename)
287 web.debug('returning update payload ' + url)
288 return self.GetUpdatePayload(checksum, size, url)
289
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700290 if client_version != 'ForcedUpdate' \
Charlie Lee8c993082010-02-24 13:27:37 -0800291 and not self.CanUpdate(client_version, latest_version):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700292 web.debug('no update')
rtc@google.com21a5ca32009-11-04 18:23:23 +0000293 return self.GetNoUpdatePayload()
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700294 if label:
295 web.debug('Client requested version %s' % label)
296 # Check that matching build exists
297 image_path = '%s/%s' % (self.static_dir, label)
298 if not os.path.exists(image_path):
299 web.debug('%s not found.' % image_path)
300 return self.GetNoUpdatePayload()
301 # Construct a response
302 ok = self.BuildUpdateImage(image_path)
303 if ok != True:
304 web.debug('Failed to build an update image')
305 return self.GetNoUpdatePayload()
306 web.debug('serving update: ')
307 hash = self.GetHash('%s/%s/update.gz' % (self.static_dir, label))
308 size = self.GetSize('%s/%s/update.gz' % (self.static_dir, label))
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700309 # In case we configured images to be hosted elsewhere
310 # (e.g. buildbot's httpd), use that. Otherwise, serve it
311 # ourselves using web.py's static resource handler.
312 if self.static_urlbase:
313 urlbase = self.static_urlbase
314 else:
315 urlbase = 'http://%s/static/archive/' % hostname
316
317 url = '%s/%s/update.gz' % (urlbase, label)
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700318 return self.GetUpdatePayload(hash, size, url)
Chris Sosaa73ec162010-05-03 20:18:02 -0700319 web.debug('DONE')
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700320 else:
321 web.debug('update found %s ' % latest_version)
322 ok = self.BuildUpdateImage(latest_image_path)
323 if ok != True:
324 web.debug('Failed to build an update image')
325 return self.GetNoUpdatePayload()
rtc@google.comded22402009-10-26 22:36:21 +0000326
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700327 hash = self.GetHash('%s/update.gz' % self.static_dir)
328 size = self.GetSize('%s/update.gz' % self.static_dir)
329
330 url = 'http://%s/static/update.gz' % hostname
331 return self.GetUpdatePayload(hash, size, url)