autoupdater.py revision fb2b9f2c2a93db2093f5363a6650dd6b7a833e33
1# Copyright (c) 2010 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 5import httplib 6import logging 7import re 8import socket 9import urlparse 10 11from autotest_lib.client.common_lib import error 12from autotest_lib.client.cros import constants as chromeos_constants 13 14STATEFULDEV_UPDATER = '/usr/local/bin/stateful_update' 15UPDATER_BIN = '/usr/bin/update_engine_client' 16UPDATER_IDLE = 'UPDATE_STATUS_IDLE' 17UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT' 18UPDATED_MARKER = '/var/run/update_engine_autoupdate_completed' 19 20 21class ChromiumOSError(error.InstallError): 22 """Generic error for ChromiumOS-specific exceptions.""" 23 pass 24 25 26def url_to_version(update_url): 27 # The ChromiumOS updater respects the last element in the path as 28 # the requested version. Parse it out. 29 return urlparse.urlparse(update_url).path.split('/')[-1] 30 31 32class ChromiumOSUpdater(): 33 def __init__(self, host=None, update_url=None): 34 self.host = host 35 self.update_url = update_url 36 self.update_version = url_to_version(update_url) 37 38 39 def check_update_status(self): 40 update_status_cmd = ' '.join([UPDATER_BIN, '-status', '2>&1', 41 '| grep CURRENT_OP']) 42 update_status = self._run(update_status_cmd) 43 return update_status.stdout.strip().split('=')[-1] 44 45 46 def reset_update_engine(self): 47 logging.info('Resetting update-engine.') 48 self._run('rm -f %s' % UPDATED_MARKER) 49 try: 50 self._run('initctl stop update-engine') 51 except error.AutoservRunError, e: 52 logging.warn('Stopping update-engine service failed. Already dead?') 53 self._run('initctl start update-engine') 54 # May need to wait if service becomes slow to restart. 55 if self.check_update_status() != UPDATER_IDLE: 56 raise ChromiumOSError('%s is not in an installable state' % 57 self.host.hostname) 58 59 60 def _run(self, cmd, *args, **kwargs): 61 return self.host.run(cmd, *args, **kwargs) 62 63 64 def rootdev(self): 65 return self._run('rootdev').stdout.strip() 66 67 68 def revert_boot_partition(self): 69 part = self.rootdev() 70 logging.warn('Reverting update; Boot partition will be %s', part) 71 return self._run('/postinst %s 2>&1' % part) 72 73 74 def run_update(self): 75 if not self.update_url: 76 return False 77 78 # Check that devserver is accepting connections (from autoserv's host) 79 # If we can't talk to it, the machine host probably can't either. 80 auserver_host = urlparse.urlparse(self.update_url)[1] 81 try: 82 httplib.HTTPConnection(auserver_host).connect() 83 except socket.error: 84 raise ChromiumOSError('Update server at %s not available' % 85 auserver_host) 86 87 logging.info('Installing from %s to: %s' % (self.update_url, 88 self.host.hostname)) 89 # Reset update_engine's state & check that update_engine is idle. 90 self.reset_update_engine() 91 92 # Run autoupdate command. This tells the autoupdate process on 93 # the host to look for an update at a specific URL and version 94 # string. 95 autoupdate_cmd = ' '.join([UPDATER_BIN, 96 '--update', 97 '--omaha_url=%s' % self.update_url, 98 ' 2>&1']) 99 logging.info(autoupdate_cmd) 100 try: 101 self._run(autoupdate_cmd, timeout=900) 102 except error.AutoservRunError, e: 103 # Either a runtime error occurred on the host, or 104 # update_engine_client exited with > 0. 105 raise ChromiumOSError('update_engine failed on %s' % 106 self.host.hostname) 107 108 # Check that the installer completed as expected. 109 status = self.check_update_status() 110 if status != UPDATER_NEED_REBOOT: 111 raise ChromiumOSError('update-engine error on %s: ' 112 '"%s" from update-engine' % 113 (self.host.hostname, status)) 114 115 # Attempt dev & test tools update (which don't live on the 116 # rootfs). This must succeed so that the newly installed host 117 # is testable after we run the autoupdater. 118 statefuldev_url = self.update_url.replace('update', 'static/archive') 119 120 statefuldev_cmd = ' '.join([STATEFULDEV_UPDATER, statefuldev_url, 121 '2>&1']) 122 logging.info(statefuldev_cmd) 123 try: 124 self._run(statefuldev_cmd, timeout=600) 125 except error.AutoservRunError, e: 126 # TODO(seano): If statefuldev update failed, we must mark 127 # the update as failed, and keep the same rootfs after 128 # reboot. 129 self.revert_boot_partition() 130 raise ChromiumOSError('stateful_update failed on %s.' % 131 self.host.hostname) 132 return True 133 134 135 def check_version(self): 136 booted_version = self.get_build_id() 137 if not booted_version: 138 booted_version = self.get_dev_build_id() 139 if not booted_version in self.update_version: 140 logging.error('Expected Chromium OS version: %s.' 141 'Found Chromium OS %s', 142 self.update_version, booted_version) 143 raise ChromiumOSError('Updater failed on host %s' % 144 self.host.hostname) 145 else: 146 return True 147 148 149 def get_build_id(self): 150 """Turns the CHROMEOS_RELEASE_DESCRIPTION into a string that 151 matches the build ID.""" 152 version = self._run('grep CHROMEOS_RELEASE_DESCRIPTION' 153 ' /etc/lsb-release').stdout 154 build_re = (r'CHROMEOS_RELEASE_DESCRIPTION=' 155 '(\d+\.\d+\.\d+\.\d+) \(\w+ \w+ (\w+)(.*)\)') 156 version_match = re.match(build_re, version) 157 if version_match: 158 version, build_id, builder = version_match.groups() 159 build_match = re.match(r'.*: (\d+)', builder) 160 if build_match: 161 builder_num = '-b%s' % build_match.group(1) 162 else: 163 builder_num = '' 164 return '%s-r%s%s' % (version, build_id, builder_num) 165 166 167 def get_dev_build_id(self): 168 """Pulls the CHROMEOS_RELEASE_VERSION string from /etc/lsb-release.""" 169 return self._run('grep CHROMEOS_RELEASE_VERSION' 170 ' /etc/lsb-release').stdout.split('=')[1].strip() 171