blob: 1ded29e5d0729d574bae61a101e1af59fefa2774 [file] [log] [blame]
# Copyright (c) 2009 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from buildutil import BuildObject
from xml.dom import minidom
import os
import shutil
import sys
import web
class Autoupdate(BuildObject):
# Basic functionality of handling ChromeOS autoupdate pings
# and building/serving update images.
# TODO(rtc): Clean this code up and write some tests.
def __init__(self, serve_only=None, test_image=False, urlbase=None,
factory_config_path=None, validate_factory_config=None,
client_prefix=None,
*args, **kwargs):
super(Autoupdate, self).__init__(*args, **kwargs)
self.serve_only = serve_only
self.factory_config = factory_config_path
self.test_image = test_image
self.static_urlbase = urlbase
self.client_prefix=client_prefix
if serve_only:
# If we're serving out of an archived build dir (e.g. a
# buildbot), prepare this webserver's magic 'static/' dir with a
# link to the build archive.
web.debug('Autoupdate in "serve update images only" mode.')
if os.path.exists('static/archive'):
if self.static_dir != os.readlink('static/archive'):
web.debug('removing stale symlink to %s' % self.static_dir)
os.unlink('static/archive')
os.symlink(self.static_dir, 'static/archive')
else:
os.symlink(self.static_dir, 'static/archive')
if factory_config_path is not None:
self.ImportFactoryConfigFile(factory_config_path, validate_factory_config)
def GetUpdatePayload(self, hash, size, url):
payload = """<?xml version="1.0" encoding="UTF-8"?>
<gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
<app appid="{%s}" status="ok">
<ping status="ok"/>
<updatecheck
codebase="%s"
hash="%s"
needsadmin="false"
size="%s"
status="ok"/>
</app>
</gupdate>
"""
return payload % (self.app_id, url, hash, size)
def GetNoUpdatePayload(self):
payload = """<?xml version="1.0" encoding="UTF-8"?>
<gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
<app appid="{%s}" status="ok">
<ping status="ok"/>
<updatecheck status="noupdate"/>
</app>
</gupdate>
"""
return payload % self.app_id
def GetDefaultBoardID(self):
board_file = '%s/.default_board' % (self.scripts_dir)
try:
return open(board_file).read()
except IOError:
return 'x86-generic'
def GetLatestImagePath(self, board_id):
cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
return os.popen(cmd).read().strip()
def GetLatestVersion(self, latest_image_path):
latest_version = latest_image_path.split('/')[-1]
# Removes the portage build prefix.
latest_version = latest_version.lstrip('g-')
return latest_version.split('-')[0]
def CanUpdate(self, client_version, latest_version):
"""
Returns true iff the latest_version is greater than the client_version.
"""
client_tokens = client_version.replace('_','').split('.')
latest_tokens = latest_version.replace('_','').split('.')
web.debug('client version %s latest version %s' \
% (client_version, latest_version))
for i in range(4):
if int(latest_tokens[i]) == int(client_tokens[i]):
continue
return int(latest_tokens[i]) > int(client_tokens[i])
return False
def UnpackImage(self, image_path, image_file, stateful_file,
kernel_file, rootfs_file):
unpack_command = 'cd %s && ./unpack_partitions.sh %s' % \
(image_path, image_file)
if os.system(unpack_command) == 0:
shutil.move(os.path.join(image_path, 'part_1'), stateful_file)
shutil.move(os.path.join(image_path, 'part_2'), kernel_file)
shutil.move(os.path.join(image_path, 'part_3'), rootfs_file)
os.system('cd %s && rm part_*' % image_path)
return True
return False
def UnpackZip(self, image_path, image_file):
image = os.path.join(image_path, image_file)
if os.path.exists(image):
return True
else:
# -n, never clobber an existing file, in case we get invoked
# simultaneously by multiple request handlers. This means that
# we're assuming each image.zip file lives in a versioned
# directory (a la Buildbot).
return os.system('cd %s && unzip -n image.zip %s unpack_partitions.sh' %
(image_path, image_file)) == 0
def GetImageBinPath(self, image_path):
if self.test_image:
image_file = 'chromiumos_test_image.bin'
else:
image_file = 'chromiumos_image.bin'
return image_file
def BuildUpdateImage(self, image_path):
stateful_file = '%s/stateful.image' % image_path
kernel_file = '%s/kernel.image' % image_path
rootfs_file = '%s/rootfs.image' % image_path
image_file = self.GetImageBinPath(image_path)
bin_path = os.path.join(image_path, image_file)
# Get appropriate update.gz to compare timestamps.
if self.serve_only:
cached_update_file = os.path.join(image_path, 'update.gz')
else:
cached_update_file = os.path.join(self.static_dir, 'update.gz')
# If the rootfs image is newer, re-create everything.
if (os.path.exists(cached_update_file) and
os.path.getmtime(cached_update_file) >= os.path.getmtime(bin_path)):
web.debug('Using cached update image at %s instead of %s' %
(cached_update_file, bin_path))
else:
# Unpack zip file if we are serving from a directory.
if self.serve_only and not self.UnpackZip(image_path, image_file):
web.debug('unzip image.zip failed.')
return False
if not self.UnpackImage(image_path, image_file, stateful_file,
kernel_file, rootfs_file):
web.debug('Failed to unpack image.')
return False
update_file = os.path.join(image_path, 'update.gz')
web.debug('Generating update image %s' % update_file)
mkupdate_command = '%s/mk_memento_images.sh %s %s' % \
(self.scripts_dir, kernel_file, rootfs_file)
if os.system(mkupdate_command) != 0:
web.debug('Failed to create update image')
return False
mkstatefulupdate_command = 'gzip -f %s' % stateful_file
if os.system(mkstatefulupdate_command) != 0:
web.debug('Failed to create stateful update image')
return False
# Add gz suffix
stateful_file = '%s.gz' % stateful_file
# Cleanup of image files
os.remove(kernel_file)
os.remove(rootfs_file)
if not self.serve_only:
try:
web.debug('Found a new image to serve, copying it to static')
shutil.copy(update_file, self.static_dir)
shutil.copy(stateful_file, self.static_dir)
os.remove(update_file)
os.remove(stateful_file)
except Exception, e:
web.debug('%s' % e)
return False
return True
def GetSize(self, update_path):
return os.path.getsize(update_path)
def GetHash(self, update_path):
cmd = "cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';" \
% update_path
return os.popen(cmd).read().rstrip()
def ImportFactoryConfigFile(self, filename, validate_checksums=False):
"""Imports a factory-floor server configuration file. The file should
be in this format:
config = [
{
'qual_ids': set([1, 2, 3, "x86-generic"]),
'factory_image': 'generic-factory.gz',
'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
'release_image': 'generic-release.gz',
'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
'oempartitionimg_image': 'generic-oem.gz',
'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
'efipartitionimg_image': 'generic-efi.gz',
'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
'stateimg_image': 'generic-state.gz',
'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
'firmware_image': 'generic-firmware.gz',
'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
},
{
'qual_ids': set([6]),
'factory_image': '6-factory.gz',
'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
'release_image': '6-release.gz',
'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
'oempartitionimg_image': '6-oem.gz',
'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
'efipartitionimg_image': '6-efi.gz',
'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
'stateimg_image': '6-state.gz',
'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
'firmware_image': '6-firmware.gz',
'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
},
]
The server will look for the files by name in the static files
directory.
If validate_checksums is True, validates checksums and exits. If
a checksum mismatch is found, it's printed to the screen.
"""
f = open(filename, 'r')
output = {}
exec(f.read(), output)
self.factory_config = output['config']
success = True
for stanza in self.factory_config:
for key in stanza.copy().iterkeys():
suffix = '_image'
if key.endswith(suffix):
kind = key[:-len(suffix)]
stanza[kind + '_size'] = \
os.path.getsize(self.static_dir + '/' + stanza[kind + '_image'])
if validate_checksums:
factory_checksum = self.GetHash(self.static_dir + '/' +
stanza[kind + '_image'])
if factory_checksum != stanza[kind + '_checksum']:
print 'Error: checksum mismatch for %s. Expected "%s" but file ' \
'has checksum "%s".' % (stanza[kind + '_image'],
stanza[kind + '_checksum'],
factory_checksum)
success = False
if validate_checksums:
if success is False:
raise Exception('Checksum mismatch in conf file.')
print 'Config file looks good.'
def GetFactoryImage(self, board_id, channel):
kind = channel.rsplit('-', 1)[0]
for stanza in self.factory_config:
if board_id not in stanza['qual_ids']:
continue
return (stanza[kind + '_image'],
stanza[kind + '_checksum'],
stanza[kind + '_size'])
def HandleUpdatePing(self, data, label=None):
web.debug('handle update ping')
update_dom = minidom.parseString(data)
root = update_dom.firstChild
if root.hasAttribute('updaterversion') and \
not root.getAttribute('updaterversion').startswith(
self.client_prefix):
web.debug('Got update from unsupported updater:' + \
root.getAttribute('updaterversion'))
return self.GetNoUpdatePayload()
query = root.getElementsByTagName('o:app')[0]
client_version = query.getAttribute('version')
channel = query.getAttribute('track')
board_id = query.hasAttribute('board') and query.getAttribute('board') \
or self.GetDefaultBoardID()
latest_image_path = self.GetLatestImagePath(board_id)
latest_version = self.GetLatestVersion(latest_image_path)
hostname = web.ctx.host
# If this is a factory floor server, return the image here:
if self.factory_config:
(filename, checksum, size) = \
self.GetFactoryImage(board_id, channel)
if filename is None:
web.debug('unable to find image for board %s' % board_id)
return self.GetNoUpdatePayload()
url = 'http://%s/static/%s' % (hostname, filename)
web.debug('returning update payload ' + url)
return self.GetUpdatePayload(checksum, size, url)
if client_version != 'ForcedUpdate' \
and not self.CanUpdate(client_version, latest_version):
web.debug('no update')
return self.GetNoUpdatePayload()
if label:
web.debug('Client requested version %s' % label)
# Check that matching build exists
image_path = '%s/%s' % (self.static_dir, label)
if not os.path.exists(image_path):
web.debug('%s not found.' % image_path)
return self.GetNoUpdatePayload()
# Construct a response
ok = self.BuildUpdateImage(image_path)
if ok != True:
web.debug('Failed to build an update image')
return self.GetNoUpdatePayload()
web.debug('serving update: ')
hash = self.GetHash('%s/%s/update.gz' % (self.static_dir, label))
size = self.GetSize('%s/%s/update.gz' % (self.static_dir, label))
# In case we configured images to be hosted elsewhere
# (e.g. buildbot's httpd), use that. Otherwise, serve it
# ourselves using web.py's static resource handler.
if self.static_urlbase:
urlbase = self.static_urlbase
else:
urlbase = 'http://%s/static/archive/' % hostname
url = '%s/%s/update.gz' % (urlbase, label)
return self.GetUpdatePayload(hash, size, url)
web.debug('DONE')
else:
web.debug('update found %s ' % latest_version)
ok = self.BuildUpdateImage(latest_image_path)
if ok != True:
web.debug('Failed to build an update image')
return self.GetNoUpdatePayload()
hash = self.GetHash('%s/update.gz' % self.static_dir)
size = self.GetSize('%s/update.gz' % self.static_dir)
url = 'http://%s/static/update.gz' % hostname
return self.GetUpdatePayload(hash, size, url)