blob: f4adf167938885a1efd6d5daafe1c1688ee14938 [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
23 self.test_image=test_image
24 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')
Andrew de los Reyes52620802010-04-12 13:40:07 -070037 if factory_config_path is not None:
38 self.ImportFactoryConfigFile(factory_config_path, validate_factory_config)
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070039
rtc@google.com21a5ca32009-11-04 18:23:23 +000040 def GetUpdatePayload(self, hash, size, url):
41 payload = """<?xml version="1.0" encoding="UTF-8"?>
42 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
43 <app appid="{%s}" status="ok">
44 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070045 <updatecheck
46 codebase="%s"
47 hash="%s"
48 needsadmin="false"
49 size="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +000050 status="ok"/>
51 </app>
52 </gupdate>
53 """
54 return payload % (self.app_id, url, hash, size)
rtc@google.comded22402009-10-26 22:36:21 +000055
rtc@google.com21a5ca32009-11-04 18:23:23 +000056 def GetNoUpdatePayload(self):
57 payload = """<?xml version="1.0" encoding="UTF-8"?>
58 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
59 <app appid="{%s}" status="ok">
60 <ping status="ok"/>
61 <updatecheck status="noupdate"/>
62 </app>
63 </gupdate>
64 """
65 return payload % self.app_id
rtc@google.comded22402009-10-26 22:36:21 +000066
Sam Leffler76382042010-02-18 09:58:42 -080067 def GetLatestImagePath(self, board_id):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070068 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
rtc@google.com21a5ca32009-11-04 18:23:23 +000069 return os.popen(cmd).read().strip()
rtc@google.comded22402009-10-26 22:36:21 +000070
rtc@google.com21a5ca32009-11-04 18:23:23 +000071 def GetLatestVersion(self, latest_image_path):
72 latest_version = latest_image_path.split('/')[-1]
Ryan Cairns1b05beb2010-02-05 17:05:24 -080073
74 # Removes the portage build prefix.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070075 latest_version = latest_version.lstrip('g-')
rtc@google.com21a5ca32009-11-04 18:23:23 +000076 return latest_version.split('-')[0]
rtc@google.comded22402009-10-26 22:36:21 +000077
rtc@google.com21a5ca32009-11-04 18:23:23 +000078 def CanUpdate(self, client_version, latest_version):
79 """
80 Returns true iff the latest_version is greater than the client_version.
81 """
82 client_tokens = client_version.split('.')
83 latest_tokens = latest_version.split('.')
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070084 web.debug('client version %s latest version %s' \
Charlie Lee8c993082010-02-24 13:27:37 -080085 % (client_version, latest_version))
rtc@google.com21a5ca32009-11-04 18:23:23 +000086 for i in range(0,4):
87 if int(latest_tokens[i]) == int(client_tokens[i]):
88 continue
89 return int(latest_tokens[i]) > int(client_tokens[i])
rtc@google.comded22402009-10-26 22:36:21 +000090 return False
rtc@google.comded22402009-10-26 22:36:21 +000091
Darin Petkov55604f12010-04-12 11:09:25 -070092 def UnpackImage(self, image_path, kernel_file, rootfs_file):
93 if os.path.exists(rootfs_file) and os.path.exists(kernel_file):
Darin Petkovcbcd2bd2010-04-06 10:14:08 -070094 return True
95 if self.test_image:
96 image_file = 'chromiumos_test_image.bin'
97 else:
98 image_file = 'chromiumos_image.bin'
Sean O'Connor1f7fd362010-04-07 16:34:52 -070099 if self.serve_only:
Darin Petkov55604f12010-04-12 11:09:25 -0700100 os.system('cd %s && unzip -o image.zip' %
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700101 (image_path, image_file))
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700102 os.system('rm -f %s/part_*' % image_path)
103 os.system('cd %s && ./unpack_partitions.sh %s' % (image_path, image_file))
Darin Petkov55604f12010-04-12 11:09:25 -0700104 shutil.move(os.path.join(image_path, 'part_2'), kernel_file)
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700105 shutil.move(os.path.join(image_path, 'part_3'), rootfs_file)
106 os.system('rm -f %s/part_*' % image_path)
107 return True
108
rtc@google.com21a5ca32009-11-04 18:23:23 +0000109 def BuildUpdateImage(self, image_path):
Darin Petkov55604f12010-04-12 11:09:25 -0700110 kernel_file = '%s/kernel.image' % image_path
111 rootfs_file = '%s/rootfs.image' % image_path
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700112
Darin Petkov55604f12010-04-12 11:09:25 -0700113 if not self.UnpackImage(image_path, kernel_file, rootfs_file):
114 web.debug('failed to unpack image.')
Darin Petkovcbcd2bd2010-04-06 10:14:08 -0700115 return False
116
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700117 update_file = '%s/update.gz' % image_path
118 if (os.path.exists(update_file) and
Darin Petkov55604f12010-04-12 11:09:25 -0700119 os.path.getmtime(update_file) >= os.path.getmtime(rootfs_file)):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700120 web.debug('Found cached update image %s/update.gz' % image_path)
121 else:
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700122 web.debug('generating update image %s' % update_file)
Darin Petkov55604f12010-04-12 11:09:25 -0700123 mkupdate = ('%s/mk_memento_images.sh %s %s' %
124 (self.scripts_dir, kernel_file, rootfs_file))
rtc@google.com21a5ca32009-11-04 18:23:23 +0000125 web.debug(mkupdate)
126 err = os.system(mkupdate)
127 if err != 0:
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700128 web.debug('failed to create update image')
rtc@google.com21a5ca32009-11-04 18:23:23 +0000129 return False
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700130 if not self.serve_only:
131 web.debug('Found an image, copying it to static')
132 try:
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700133 shutil.copy(update_file, self.static_dir)
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700134 except Exception, e:
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700135 web.debug('Unable to copy %s to %s' % (update_file, self.static_dir))
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700136 return False
rtc@google.com21a5ca32009-11-04 18:23:23 +0000137 return True
rtc@google.comded22402009-10-26 22:36:21 +0000138
rtc@google.com21a5ca32009-11-04 18:23:23 +0000139 def GetSize(self, update_path):
140 return os.path.getsize(update_path)
rtc@google.comded22402009-10-26 22:36:21 +0000141
rtc@google.com21a5ca32009-11-04 18:23:23 +0000142 def GetHash(self, update_path):
Darin Petkov8ef83452010-03-23 16:52:29 -0700143 cmd = "cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';" \
144 % update_path
Andrew de los Reyes52620802010-04-12 13:40:07 -0700145 return os.popen(cmd).read().rstrip()
Darin Petkov8ef83452010-03-23 16:52:29 -0700146
Andrew de los Reyes52620802010-04-12 13:40:07 -0700147 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
148 """Imports a factory-floor server configuration file. The file should
149 be in this format:
150 config = [
151 {
152 'qual_ids': set([1, 2, 3, "x86-generic"]),
153 'factory_image': 'generic-factory.gz',
154 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
155 'release_image': 'generic-release.gz',
156 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
157 'oempartitionimg_image': 'generic-oem.gz',
158 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
159 'stateimg_image': 'generic-state.gz',
160 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM='
161 },
162 {
163 'qual_ids': set([6]),
164 'factory_image': '6-factory.gz',
165 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
166 'release_image': '6-release.gz',
167 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
168 'oempartitionimg_image': '6-oem.gz',
169 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
170 'stateimg_image': '6-state.gz',
171 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM='
172 },
173 ]
174 The server will look for the files by name in the static files
175 directory.
176
177 If validate_checksums is True, validates checksums and exits. If
178 a checksum mismatch is found, it's printed to the screen.
179 """
180 f = open(filename, 'r')
181 output = {}
182 exec(f.read(), output)
183 self.factory_config = output['config']
184 success = True
185 for stanza in self.factory_config:
186 for kind in ('factory', 'oempartitionimg', 'release', 'stateimg'):
187 stanza[kind + '_size'] = \
188 os.path.getsize(self.static_dir + '/' + stanza[kind + '_image'])
189 if validate_checksums:
190 factory_checksum = self.GetHash(self.static_dir + '/' +
191 stanza[kind + '_image'])
192 if factory_checksum != stanza[kind + '_checksum']:
193 print 'Error: checksum mismatch for %s. Expected "%s" but file ' \
194 'has checksum "%s".' % (stanza[kind + '_image'],
195 stanza[kind + '_checksum'],
196 factory_checksum)
197 success = False
198 if validate_checksums:
199 if success is False:
200 raise Exception('Checksum mismatch in conf file.')
201 print 'Config file looks good.'
202
203 def GetFactoryImage(self, board_id, channel):
204 kind = channel.rsplit('-', 1)[0]
205 for stanza in self.factory_config:
206 if board_id not in stanza['qual_ids']:
207 continue
208 return (stanza[kind + '_image'],
209 stanza[kind + '_checksum'],
210 stanza[kind + '_size'])
rtc@google.comded22402009-10-26 22:36:21 +0000211
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700212 def HandleUpdatePing(self, data, label=None):
Andrew de los Reyes52620802010-04-12 13:40:07 -0700213 web.debug('handle update ping')
rtc@google.com21a5ca32009-11-04 18:23:23 +0000214 update_dom = minidom.parseString(data)
215 root = update_dom.firstChild
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700216 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800217 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700218 channel = query.getAttribute('track')
Charlie Lee8c993082010-02-24 13:27:37 -0800219 board_id = query.hasAttribute('board') and query.getAttribute('board') \
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700220 or 'x86-generic'
Charlie Lee8c993082010-02-24 13:27:37 -0800221 latest_image_path = self.GetLatestImagePath(board_id)
222 latest_version = self.GetLatestVersion(latest_image_path)
Andrew de los Reyes52620802010-04-12 13:40:07 -0700223 hostname = web.ctx.host
224
225 # If this is a factory floor server, return the image here:
226 if self.factory_config:
227 (filename, checksum, size) = \
228 self.GetFactoryImage(board_id, channel)
229 if filename is None:
230 web.debug('unable to find image for board %s' % board_id)
231 return self.GetNoUpdatePayload()
232 url = 'http://%s/static/%s' % (hostname, filename)
233 web.debug('returning update payload ' + url)
234 return self.GetUpdatePayload(checksum, size, url)
235
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700236 if client_version != 'ForcedUpdate' \
Charlie Lee8c993082010-02-24 13:27:37 -0800237 and not self.CanUpdate(client_version, latest_version):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700238 web.debug('no update')
rtc@google.com21a5ca32009-11-04 18:23:23 +0000239 return self.GetNoUpdatePayload()
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700240 if label:
241 web.debug('Client requested version %s' % label)
242 # Check that matching build exists
243 image_path = '%s/%s' % (self.static_dir, label)
244 if not os.path.exists(image_path):
245 web.debug('%s not found.' % image_path)
246 return self.GetNoUpdatePayload()
247 # Construct a response
248 ok = self.BuildUpdateImage(image_path)
249 if ok != True:
250 web.debug('Failed to build an update image')
251 return self.GetNoUpdatePayload()
252 web.debug('serving update: ')
253 hash = self.GetHash('%s/%s/update.gz' % (self.static_dir, label))
254 size = self.GetSize('%s/%s/update.gz' % (self.static_dir, label))
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700255 # In case we configured images to be hosted elsewhere
256 # (e.g. buildbot's httpd), use that. Otherwise, serve it
257 # ourselves using web.py's static resource handler.
258 if self.static_urlbase:
259 urlbase = self.static_urlbase
260 else:
261 urlbase = 'http://%s/static/archive/' % hostname
262
263 url = '%s/%s/update.gz' % (urlbase, label)
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700264 return self.GetUpdatePayload(hash, size, url)
265 web.debug( 'DONE')
266 else:
267 web.debug('update found %s ' % latest_version)
268 ok = self.BuildUpdateImage(latest_image_path)
269 if ok != True:
270 web.debug('Failed to build an update image')
271 return self.GetNoUpdatePayload()
rtc@google.comded22402009-10-26 22:36:21 +0000272
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700273 hash = self.GetHash('%s/update.gz' % self.static_dir)
274 size = self.GetSize('%s/update.gz' % self.static_dir)
275
276 url = 'http://%s/static/update.gz' % hostname
277 return self.GetUpdatePayload(hash, size, url)