OLD | NEW |
1 # Copyright (c) 2009 The Chromium OS Authors. All rights reserved. | 1 # 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 from buildutil import BuildObject | 5 from buildutil import BuildObject |
6 from xml.dom import minidom | 6 from xml.dom import minidom |
7 | 7 |
8 import os | 8 import os |
9 import shutil | 9 import shutil |
| 10 import sys |
10 import web | 11 import web |
11 | 12 |
12 class Autoupdate(BuildObject): | 13 class Autoupdate(BuildObject): |
13 # Basic functionality of handling ChromeOS autoupdate pings | 14 # Basic functionality of handling ChromeOS autoupdate pings |
14 # and building/serving update images. | 15 # and building/serving update images. |
15 # TODO(rtc): Clean this code up and write some tests. | 16 # TODO(rtc): Clean this code up and write some tests. |
16 | 17 |
17 def __init__(self, serve_only=None, test_image=False, urlbase=None, | 18 def __init__(self, serve_only=None, test_image=False, urlbase=None, |
| 19 factory_config_path=None, validate_factory_config=None, |
18 *args, **kwargs): | 20 *args, **kwargs): |
19 super(Autoupdate, self).__init__(*args, **kwargs) | 21 super(Autoupdate, self).__init__(*args, **kwargs) |
20 self.serve_only = serve_only | 22 self.serve_only = serve_only |
21 self.test_image=test_image | 23 self.test_image=test_image |
22 self.static_urlbase = urlbase | 24 self.static_urlbase = urlbase |
23 if serve_only: | 25 if serve_only: |
24 # If we're serving out of an archived build dir (e.g. a | 26 # If we're serving out of an archived build dir (e.g. a |
25 # buildbot), prepare this webserver's magic 'static/' dir with a | 27 # buildbot), prepare this webserver's magic 'static/' dir with a |
26 # link to the build archive. | 28 # link to the build archive. |
27 web.debug('Autoupdate in "serve update images only" mode.') | 29 web.debug('Autoupdate in "serve update images only" mode.') |
28 if os.path.exists('static/archive'): | 30 if os.path.exists('static/archive'): |
29 archive_symlink = os.readlink('static/archive') | 31 archive_symlink = os.readlink('static/archive') |
30 if archive_symlink != self.static_dir: | 32 if archive_symlink != self.static_dir: |
31 web.debug('removing stale symlink to %s' % self.static_dir) | 33 web.debug('removing stale symlink to %s' % self.static_dir) |
32 os.unlink('static/archive') | 34 os.unlink('static/archive') |
33 else: | 35 else: |
34 os.symlink(self.static_dir, 'static/archive') | 36 os.symlink(self.static_dir, 'static/archive') |
| 37 if factory_config_path is not None: |
| 38 self.ImportFactoryConfigFile(factory_config_path, validate_factory_config) |
35 | 39 |
36 def GetUpdatePayload(self, hash, size, url): | 40 def GetUpdatePayload(self, hash, size, url): |
37 payload = """<?xml version="1.0" encoding="UTF-8"?> | 41 payload = """<?xml version="1.0" encoding="UTF-8"?> |
38 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0"> | 42 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0"> |
39 <app appid="{%s}" status="ok"> | 43 <app appid="{%s}" status="ok"> |
40 <ping status="ok"/> | 44 <ping status="ok"/> |
41 <updatecheck | 45 <updatecheck |
42 codebase="%s" | 46 codebase="%s" |
43 hash="%s" | 47 hash="%s" |
44 needsadmin="false" | 48 needsadmin="false" |
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
131 web.debug('Unable to copy %s to %s' % (update_file, self.static_dir)) | 135 web.debug('Unable to copy %s to %s' % (update_file, self.static_dir)) |
132 return False | 136 return False |
133 return True | 137 return True |
134 | 138 |
135 def GetSize(self, update_path): | 139 def GetSize(self, update_path): |
136 return os.path.getsize(update_path) | 140 return os.path.getsize(update_path) |
137 | 141 |
138 def GetHash(self, update_path): | 142 def GetHash(self, update_path): |
139 cmd = "cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';" \ | 143 cmd = "cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';" \ |
140 % update_path | 144 % update_path |
141 web.debug(cmd) | 145 return os.popen(cmd).read().rstrip() |
142 return os.popen(cmd).read() | |
143 | 146 |
| 147 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']) |
144 | 211 |
145 def HandleUpdatePing(self, data, label=None): | 212 def HandleUpdatePing(self, data, label=None): |
| 213 web.debug('handle update ping') |
146 update_dom = minidom.parseString(data) | 214 update_dom = minidom.parseString(data) |
147 root = update_dom.firstChild | 215 root = update_dom.firstChild |
148 query = root.getElementsByTagName('o:app')[0] | 216 query = root.getElementsByTagName('o:app')[0] |
149 client_version = query.getAttribute('version') | 217 client_version = query.getAttribute('version') |
| 218 channel = query.getAttribute('track') |
150 board_id = query.hasAttribute('board') and query.getAttribute('board') \ | 219 board_id = query.hasAttribute('board') and query.getAttribute('board') \ |
151 or 'x86-generic' | 220 or 'x86-generic' |
152 latest_image_path = self.GetLatestImagePath(board_id) | 221 latest_image_path = self.GetLatestImagePath(board_id) |
153 latest_version = self.GetLatestVersion(latest_image_path) | 222 latest_version = self.GetLatestVersion(latest_image_path) |
| 223 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 |
154 if client_version != 'ForcedUpdate' \ | 236 if client_version != 'ForcedUpdate' \ |
155 and not self.CanUpdate(client_version, latest_version): | 237 and not self.CanUpdate(client_version, latest_version): |
156 web.debug('no update') | 238 web.debug('no update') |
157 return self.GetNoUpdatePayload() | 239 return self.GetNoUpdatePayload() |
158 hostname = web.ctx.host | |
159 if label: | 240 if label: |
160 web.debug('Client requested version %s' % label) | 241 web.debug('Client requested version %s' % label) |
161 # Check that matching build exists | 242 # Check that matching build exists |
162 image_path = '%s/%s' % (self.static_dir, label) | 243 image_path = '%s/%s' % (self.static_dir, label) |
163 if not os.path.exists(image_path): | 244 if not os.path.exists(image_path): |
164 web.debug('%s not found.' % image_path) | 245 web.debug('%s not found.' % image_path) |
165 return self.GetNoUpdatePayload() | 246 return self.GetNoUpdatePayload() |
166 # Construct a response | 247 # Construct a response |
167 ok = self.BuildUpdateImage(image_path) | 248 ok = self.BuildUpdateImage(image_path) |
168 if ok != True: | 249 if ok != True: |
(...skipping 18 matching lines...) Expand all Loading... |
187 ok = self.BuildUpdateImage(latest_image_path) | 268 ok = self.BuildUpdateImage(latest_image_path) |
188 if ok != True: | 269 if ok != True: |
189 web.debug('Failed to build an update image') | 270 web.debug('Failed to build an update image') |
190 return self.GetNoUpdatePayload() | 271 return self.GetNoUpdatePayload() |
191 | 272 |
192 hash = self.GetHash('%s/update.gz' % self.static_dir) | 273 hash = self.GetHash('%s/update.gz' % self.static_dir) |
193 size = self.GetSize('%s/update.gz' % self.static_dir) | 274 size = self.GetSize('%s/update.gz' % self.static_dir) |
194 | 275 |
195 url = 'http://%s/static/update.gz' % hostname | 276 url = 'http://%s/static/update.gz' % hostname |
196 return self.GetUpdatePayload(hash, size, url) | 277 return self.GetUpdatePayload(hash, size, url) |
OLD | NEW |