blob: 07449664b559a424b8f5695e88607f0ecc5f494b [file] [log] [blame]
Nick Sanders723f3262010-09-16 05:18:41 -07001# Copyright (c) 2009 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
7
8import os
Darin Petkov798fe7d2010-03-22 15:18:13 -07009import shutil
Nick Sanders723f3262010-09-16 05:18:41 -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):
Nick Sanders723f3262010-09-16 05:18:41 -070015 # Basic functionality of handling ChromeOS autoupdate pings
16 # 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,
Nick Sanders723f3262010-09-16 05:18:41 -070020 factory_config_path=None, validate_factory_config=None,
21 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
Nick Sanders723f3262010-09-16 05:18:41 -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
Nick Sanders723f3262010-09-16 05:18:41 -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'):
35 if self.static_dir != os.readlink('static/archive'):
36 web.debug('removing stale symlink to %s' % self.static_dir)
37 os.unlink('static/archive')
38 os.symlink(self.static_dir, 'static/archive')
39 else:
40 os.symlink(self.static_dir, 'static/archive')
41 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
Nick Sanders723f3262010-09-16 05:18:41 -070044 def GetSecondsSinceMidnight(self):
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070045 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 """
Nick Sanders723f3262010-09-16 05:18:41 -070063 return payload % (self.GetSecondsSinceMidnight(),
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070064 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):
Nick Sanders723f3262010-09-16 05:18:41 -070067 payload = """<?xml version="1.0" encoding="UTF-8"?>
68 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
69 <daystart elapsed_seconds="%s"/>
70 <app appid="{%s}" status="ok">
71 <ping status="ok"/>
72 <updatecheck status="noupdate"/>
73 </app>
74 </gupdate>
rtc@google.com21a5ca32009-11-04 18:23:23 +000075 """
Nick Sanders723f3262010-09-16 05:18:41 -070076 return payload % (self.GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +000077
Nick Sanders723f3262010-09-16 05:18:41 -070078 def GetDefaultBoardID(self):
79 board_file = '%s/.default_board' % (self.scripts_dir)
Andrew de los Reyes9a528712010-06-30 10:29:43 -070080 try:
Nick Sanders723f3262010-09-16 05:18:41 -070081 return open(board_file).read()
82 except IOError:
83 return 'x86-generic'
Andrew de los Reyes9a528712010-06-30 10:29:43 -070084
Nick Sanders723f3262010-09-16 05:18:41 -070085 def GetLatestImagePath(self, board_id):
86 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
87 return os.popen(cmd).read().strip()
88
89 def GetLatestVersion(self, latest_image_path):
90 latest_version = latest_image_path.split('/')[-1]
91
92 # Removes the portage build prefix.
93 latest_version = latest_version.lstrip('g-')
94 return latest_version.split('-')[0]
95
96 def CanUpdate(self, client_version, latest_version):
97 """
98 Returns true iff the latest_version is greater than the client_version.
99 """
100 client_tokens = client_version.replace('_', '').split('.')
101 latest_tokens = latest_version.replace('_', '').split('.')
102 web.debug('client version %s latest version %s' \
103 % (client_version, latest_version))
104 for i in range(4):
105 if int(latest_tokens[i]) == int(client_tokens[i]):
106 continue
107 return int(latest_tokens[i]) > int(client_tokens[i])
108 return False
109
110 def UnpackStatefulPartition(self, image_path, image_file, stateful_file):
111 """Given an image, unpacks the stateful partition to stateful_file."""
112 get_offset = '$(cgpt show -b -i 1 %s)' % image_file
113 get_size = '$(cgpt show -s -i 1 %s)' % image_file
114 unpack_command = (
115 'cd %s && '
116 'dd if=%s of=%s bs=512 skip=%s count=%s' % (image_path, image_file,
117 stateful_file, get_offset, get_size))
118 web.debug(unpack_command)
119 return os.system(unpack_command) == 0
120
121 def UnpackZip(self, image_path, image_file):
122 image = os.path.join(image_path, image_file)
123 if os.path.exists(image):
124 return True
125 else:
126 # -n, never clobber an existing file, in case we get invoked
127 # simultaneously by multiple request handlers. This means that
128 # we're assuming each image.zip file lives in a versioned
129 # directory (a la Buildbot).
130 return os.system('cd %s && unzip -n image.zip %s unpack_partitions.sh' %
131 (image_path, image_file)) == 0
132
133 def GetImageBinPath(self, image_path):
134 if self.test_image:
135 image_file = 'chromiumos_test_image.bin'
136 else:
137 image_file = 'chromiumos_image.bin'
138 return image_file
139
140 def BuildUpdateImage(self, image_path):
141 stateful_file = '%s/stateful.image' % image_path
142 image_file = self.GetImageBinPath(image_path)
143 bin_path = os.path.join(image_path, image_file)
144
145 # Get appropriate update.gz to compare timestamps.
146 if self.serve_only:
147 cached_update_file = os.path.join(image_path, 'update.gz')
148 else:
149 cached_update_file = os.path.join(self.static_dir, 'update.gz')
150
151 # If the new chromiumos image is newer, re-create everything.
152 if (os.path.exists(cached_update_file) and
153 os.path.getmtime(cached_update_file) >= os.path.getmtime(bin_path)):
154 web.debug('Using cached update image at %s instead of %s' %
155 (cached_update_file, bin_path))
156 else:
157 # Unpack zip file if we are serving from a directory.
158 if self.serve_only and not self.UnpackZip(image_path, image_file):
159 web.debug('unzip image.zip failed.')
160 return False
161
162 update_file = os.path.join(image_path, 'update.gz')
163 web.debug('Generating update image %s' % update_file)
164 mkupdate_command = (
165 '%s/cros_generate_update_payload --image=%s --output=%s '
166 '--patch_kernel' % (self.scripts_dir, bin_path, update_file))
167 if os.system(mkupdate_command) != 0:
168 web.debug('Failed to create update image')
169 return False
170
171 # Unpack to get stateful partition.
172 if not self.UnpackStatefulPartition(image_path, image_file,
173 stateful_file):
174 web.debug('Failed to unpack stateful partition.')
175 return False
176
177 mkstatefulupdate_command = 'gzip -f %s' % stateful_file
178 if os.system(mkstatefulupdate_command) != 0:
179 web.debug('Failed to create stateful update gz')
180 return False
181
182 # Add gz suffix
183 stateful_file = '%s.gz' % stateful_file
184
185 # Cleanup of image files.
186 if not self.serve_only:
187 try:
188 web.debug('Found a new image to serve, copying it to static')
189 shutil.copy(update_file, self.static_dir)
190 shutil.copy(stateful_file, self.static_dir)
191 os.remove(update_file)
192 os.remove(stateful_file)
193 except Exception, e:
194 web.debug('%s' % e)
195 return False
rtc@google.com21a5ca32009-11-04 18:23:23 +0000196 return True
rtc@google.comded22402009-10-26 22:36:21 +0000197
Nick Sanders723f3262010-09-16 05:18:41 -0700198 def GetSize(self, update_path):
199 return os.path.getsize(update_path)
rtc@google.comded22402009-10-26 22:36:21 +0000200
Nick Sanders723f3262010-09-16 05:18:41 -0700201 def GetHash(self, update_path):
202 cmd = "cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';" \
203 % update_path
204 return os.popen(cmd).read().rstrip()
Darin Petkov8ef83452010-03-23 16:52:29 -0700205
Andrew de los Reyes52620802010-04-12 13:40:07 -0700206 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
207 """Imports a factory-floor server configuration file. The file should
208 be in this format:
209 config = [
210 {
211 'qual_ids': set([1, 2, 3, "x86-generic"]),
212 'factory_image': 'generic-factory.gz',
213 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
214 'release_image': 'generic-release.gz',
215 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
216 'oempartitionimg_image': 'generic-oem.gz',
217 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700218 'efipartitionimg_image': 'generic-efi.gz',
219 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700220 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800221 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800222 'firmware_image': 'generic-firmware.gz',
223 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700224 },
225 {
226 'qual_ids': set([6]),
227 'factory_image': '6-factory.gz',
228 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
229 'release_image': '6-release.gz',
230 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
231 'oempartitionimg_image': '6-oem.gz',
232 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700233 'efipartitionimg_image': '6-efi.gz',
234 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700235 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800236 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800237 'firmware_image': '6-firmware.gz',
238 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700239 },
240 ]
241 The server will look for the files by name in the static files
242 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700243
Andrew de los Reyes52620802010-04-12 13:40:07 -0700244 If validate_checksums is True, validates checksums and exits. If
245 a checksum mismatch is found, it's printed to the screen.
246 """
247 f = open(filename, 'r')
248 output = {}
249 exec(f.read(), output)
250 self.factory_config = output['config']
251 success = True
252 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800253 for key in stanza.copy().iterkeys():
254 suffix = '_image'
255 if key.endswith(suffix):
256 kind = key[:-len(suffix)]
Nick Sanders723f3262010-09-16 05:18:41 -0700257 stanza[kind + '_size'] = \
258 os.path.getsize(self.static_dir + '/' + stanza[kind + '_image'])
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800259 if validate_checksums:
Nick Sanders723f3262010-09-16 05:18:41 -0700260 factory_checksum = self.GetHash(self.static_dir + '/' +
261 stanza[kind + '_image'])
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800262 if factory_checksum != stanza[kind + '_checksum']:
Nick Sanders723f3262010-09-16 05:18:41 -0700263 print 'Error: checksum mismatch for %s. Expected "%s" but file ' \
264 'has checksum "%s".' % (stanza[kind + '_image'],
265 stanza[kind + '_checksum'],
266 factory_checksum)
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800267 success = False
Andrew de los Reyes52620802010-04-12 13:40:07 -0700268 if validate_checksums:
269 if success is False:
270 raise Exception('Checksum mismatch in conf file.')
271 print 'Config file looks good.'
272
273 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700274 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700275 for stanza in self.factory_config:
276 if board_id not in stanza['qual_ids']:
277 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700278 if kind + '_image' not in stanza:
279 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700280 return (stanza[kind + '_image'],
281 stanza[kind + '_checksum'],
282 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700283 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000284
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700285 def HandleUpdatePing(self, data, label=None):
Nick Sanders723f3262010-09-16 05:18:41 -0700286 web.debug('handle update ping: %s' % data)
rtc@google.com21a5ca32009-11-04 18:23:23 +0000287 update_dom = minidom.parseString(data)
288 root = update_dom.firstChild
Nick Sanders723f3262010-09-16 05:18:41 -0700289 if root.hasAttribute('updaterversion') and \
290 not root.getAttribute('updaterversion').startswith(
291 self.client_prefix):
292 web.debug('Got update from unsupported updater:' + \
293 root.getAttribute('updaterversion'))
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700294 return self.GetNoUpdatePayload()
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700295 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800296 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700297 channel = query.getAttribute('track')
Nick Sanders723f3262010-09-16 05:18:41 -0700298 board_id = query.hasAttribute('board') and query.getAttribute('board') \
299 or self.GetDefaultBoardID()
300 latest_image_path = self.GetLatestImagePath(board_id)
301 latest_version = self.GetLatestVersion(latest_image_path)
302 hostname = web.ctx.host
Andrew de los Reyes52620802010-04-12 13:40:07 -0700303
Nick Sanders723f3262010-09-16 05:18:41 -0700304 # If this is a factory floor server, return the image here:
Andrew de los Reyes52620802010-04-12 13:40:07 -0700305 if self.factory_config:
Nick Sanders723f3262010-09-16 05:18:41 -0700306 (filename, checksum, size) = \
307 self.GetFactoryImage(board_id, channel)
308 if filename is None:
309 web.debug('unable to find image for board %s' % board_id)
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700310 return self.GetNoUpdatePayload()
Nick Sanders723f3262010-09-16 05:18:41 -0700311 url = 'http://%s/static/%s' % (hostname, filename)
312 web.debug('returning update payload ' + url)
313 return self.GetUpdatePayload(checksum, size, url)
314
315 if client_version != 'ForcedUpdate' \
316 and not self.CanUpdate(client_version, latest_version):
317 web.debug('no update')
318 return self.GetNoUpdatePayload()
319 if label:
320 web.debug('Client requested version %s' % label)
321 # Check that matching build exists
322 image_path = '%s/%s' % (self.static_dir, label)
323 if not os.path.exists(image_path):
324 web.debug('%s not found.' % image_path)
325 return self.GetNoUpdatePayload()
326 # Construct a response
327 ok = self.BuildUpdateImage(image_path)
328 if ok != True:
329 web.debug('Failed to build an update image')
330 return self.GetNoUpdatePayload()
331 web.debug('serving update: ')
332 hash = self.GetHash('%s/%s/update.gz' % (self.static_dir, label))
333 size = self.GetSize('%s/%s/update.gz' % (self.static_dir, label))
334 # In case we configured images to be hosted elsewhere
335 # (e.g. buildbot's httpd), use that. Otherwise, serve it
336 # ourselves using web.py's static resource handler.
337 if self.static_urlbase:
338 urlbase = self.static_urlbase
339 else:
340 urlbase = 'http://%s/static/archive/' % hostname
341
342 url = '%s/%s/update.gz' % (urlbase, label)
343 return self.GetUpdatePayload(hash, size, url)
344 web.debug('DONE')
345 else:
346 web.debug('update found %s ' % latest_version)
347 ok = self.BuildUpdateImage(latest_image_path)
348 if ok != True:
349 web.debug('Failed to build an update image')
350 return self.GetNoUpdatePayload()
351
352 hash = self.GetHash('%s/update.gz' % self.static_dir)
353 size = self.GetSize('%s/update.gz' % self.static_dir)
354
355 url = 'http://%s/static/update.gz' % hostname
356 return self.GetUpdatePayload(hash, size, url)