blob: cbe0a73f83093b4ce0f47069ff74c1abca2bd938 [file] [log] [blame]
Darin Petkovc3fd90c2011-05-11 21:23:001# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
[email protected]ded22402009-10-26 22:36:212# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
[email protected]64244662009-11-12 00:52:085from buildutil import BuildObject
[email protected]ded22402009-10-26 22:36:216from xml.dom import minidom
Don Garrettf90edf02010-11-17 01:36:147
Chris Sosa7c931362010-10-12 02:49:018import cherrypy
Satoru Takabayashid733cbe2011-11-15 17:36:329import datetime
Dale Curtisc9aaf3a2011-08-09 22:47:4010import json
[email protected]ded22402009-10-26 22:36:2111import os
Darin Petkov798fe7d2010-03-22 22:18:1312import shutil
Chris Sosa05491b12010-11-09 01:14:1613import subprocess
Darin Petkov2b2ff4b2010-07-27 22:02:0914import time
Don Garrett0ad09372010-12-07 00:20:3015import urlparse
Chris Sosa7c931362010-10-12 02:49:0116
Chris Sosa05491b12010-11-09 01:14:1617
Chris Sosa7c931362010-10-12 02:49:0118def _LogMessage(message):
19 cherrypy.log(message, 'UPDATE')
[email protected]ded22402009-10-26 22:36:2120
Chris Sosa417e55d2011-01-26 00:40:4821UPDATE_FILE = 'update.gz'
22STATEFUL_FILE = 'stateful.tgz'
23CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 22:46:2224
Don Garrett0ad09372010-12-07 00:20:3025
26def _ChangeUrlPort(url, new_port):
27 """Return the URL passed in with a different port"""
28 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
29 host_port = netloc.split(':')
30
31 if len(host_port) == 1:
32 host_port.append(new_port)
33 else:
34 host_port[1] = new_port
35
36 print host_port
37 netloc = "%s:%s" % tuple(host_port)
38
39 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
40
41
Gilad Arnold286a0062012-01-12 21:47:0242class HostInfo:
43 """Records information about an individual host.
44
45 Members:
46 attrs: Static attributes (legacy)
47 log: Complete log of recorded client entries
48 """
49
50 def __init__(self):
51 # A dictionary of current attributes pertaining to the host.
52 self.attrs = {}
53
54 # A list of pairs consisting of a timestamp and a dictionary of recorded
55 # attributes.
56 self.log = []
57
58 def __repr__(self):
59 return 'attrs=%s, log=%s' % (self.attrs, self.log)
60
61 def AddLogEntry(self, entry):
62 """Append a new log entry."""
63 # Append a timestamp.
64 assert not 'timestamp' in entry, 'Oops, timestamp field already in use'
65 entry['timestamp'] = time.strftime('%Y-%m-%d %H:%M:%S')
66 # Add entry to hosts' message log.
67 self.log.append(entry)
68
69 def SetAttr(self, attr, value):
70 """Set an attribute value."""
71 self.attrs[attr] = value
72
73 def GetAttr(self, attr):
74 """Returns the value of an attribute."""
75 if attr in self.attrs:
76 return self.attrs[attr]
77
78 def PopAttr(self, attr, default):
79 """Returns and deletes a particular attribute."""
80 return self.attrs.pop(attr, default)
81
82
83class HostInfoTable:
84 """Records information about a set of hosts who engage in update activity.
85
86 Members:
87 table: Table of information on hosts.
88 """
89
90 def __init__(self):
91 # A dictionary of host information. Keys are normally IP addresses.
92 self.table = {}
93
94 def __repr__(self):
95 return '%s' % self.table
96
97 def GetInitHostInfo(self, host_id):
98 """Return a host's info object, or create a new one if none exists."""
99 return self.table.setdefault(host_id, HostInfo())
100
101 def GetHostInfo(self, host_id):
102 """Return an info object for given host, if such exists."""
103 if host_id in self.table:
104 return self.table[host_id]
105
106
[email protected]64244662009-11-12 00:52:08107class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 22:46:22108 """Class that contains functionality that handles Chrome OS update pings.
109
110 Members:
Dale Curtis723ec472010-11-30 22:06:47111 serve_only: Serve only pre-built updates. static_dir must contain update.gz
112 and stateful.tgz.
Chris Sosa0356d3b2010-09-16 22:46:22113 factory_config: Path to the factory config file if handling factory
114 requests.
115 use_test_image: Use chromiumos_test_image.bin rather than the standard.
116 static_url_base: base URL, other than devserver, for update images.
Chris Sosa0356d3b2010-09-16 22:46:22117 forced_image: Path to an image to use for all updates.
Chris Sosa08d55a22011-01-20 00:08:02118 forced_payload: Path to pre-generated payload to serve.
119 port: port to host devserver
120 proxy_port: port of local proxy to tell client to connect to you through.
121 src_image: If specified, creates a delta payload from this image.
122 vm: Set for VM images (doesn't patch kernel)
123 board: board for the image. Needed for pre-generating of updates.
124 copy_to_static_root: Copies images generated from the cache to
125 ~/static.
Chris Sosa0356d3b2010-09-16 22:46:22126 """
[email protected]ded22402009-10-26 22:36:21127
Sean O'Connor1f7fd362010-04-07 23:34:52128 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Greg Spencerc8b59b22011-03-15 21:15:23129 factory_config_path=None,
Don Garrett0c880e22010-11-18 02:13:37130 forced_image=None, forced_payload=None,
Don Garrett0ad09372010-12-07 00:20:30131 port=8080, proxy_port=None, src_image='', vm=False, board=None,
Chris Sosa0f1ec842011-02-15 00:33:22132 copy_to_static_root=True, private_key=None,
Satoru Takabayashid733cbe2011-11-15 17:36:32133 critical_update=False,
Chris Sosae67b78f12010-11-05 00:33:16134 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-21 06:23:48135 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 23:34:52136 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-03 00:37:32137 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 22:46:22138 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 23:54:41139 if urlbase:
Chris Sosa9841e1c2010-10-14 17:51:45140 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 23:54:41141 else:
Chris Sosa9841e1c2010-10-14 17:51:45142 self.urlbase = None
Chris Sosa5d342a22010-09-28 23:54:41143
Chris Sosa0356d3b2010-09-16 22:46:22144 self.forced_image = forced_image
Don Garrett0c880e22010-11-18 02:13:37145 self.forced_payload = forced_payload
Chris Sosa62f720b2010-10-27 04:39:48146 self.src_image = src_image
Don Garrett0ad09372010-12-07 00:20:30147 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-29 06:42:37148 self.vm = vm
Chris Sosae67b78f12010-11-05 00:33:16149 self.board = board
Chris Sosa08d55a22011-01-20 00:08:02150 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-15 00:33:22151 self.private_key = private_key
Satoru Takabayashid733cbe2011-11-15 17:36:32152 self.critical_update = critical_update
Don Garrettfff4c322010-11-19 21:37:12153
Chris Sosa417e55d2011-01-26 00:40:48154 # Path to pre-generated file.
155 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-21 06:23:48156
Dale Curtisc9aaf3a2011-08-09 22:47:40157 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 21:47:02158 # information about a given host. A host is identified by its IP address.
159 # The info stored for each host includes a complete log of events for this
160 # host, as well as a dictionary of current attributes derived from events.
161 self.host_infos = HostInfoTable()
Dale Curtisc9aaf3a2011-08-09 22:47:40162
Chris Sosa0356d3b2010-09-16 22:46:22163 def _GetSecondsSinceMidnight(self):
164 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 22:02:09165 now = time.localtime()
166 return now[3] * 3600 + now[4] * 60 + now[5]
167
Chris Sosa0356d3b2010-09-16 22:46:22168 def _GetDefaultBoardID(self):
169 """Returns the default board id stored in .default_board."""
170 board_file = '%s/.default_board' % (self.scripts_dir)
171 try:
172 return open(board_file).read()
173 except IOError:
174 return 'x86-generic'
175
176 def _GetLatestImageDir(self, board_id):
177 """Returns the latest image dir based on shell script."""
178 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
179 return os.popen(cmd).read().strip()
180
181 def _GetVersionFromDir(self, image_dir):
182 """Returns the version of the image based on the name of the directory."""
183 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 15:52:52184 parts = latest_version.split('-')
185 if len(parts) == 2:
186 # Old-style, e.g. "0.15.938.2011_08_23_0941-a1".
187 # TODO(derat): Remove the code for old-style versions after 20120101.
188 return parts[0]
189 else:
190 # New-style, e.g. "R16-1102.0.2011_09_30_0806-a1".
191 return parts[1]
Chris Sosa0356d3b2010-09-16 22:46:22192
193 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-17 01:36:14194 """Returns true if the latest_version is greater than the client_version.
195 """
Chris Sosa7c931362010-10-12 02:49:01196 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-17 01:36:14197 % (client_version, latest_version))
Daniel Erat8a0bc4a2011-09-30 15:52:52198
199 client_tokens = client_version.replace('_', '').split('.')
200 # If the client has an old four-token version like "0.16.892.0", drop the
201 # first two tokens -- we use versions like "892.0.0" now.
202 # TODO(derat): Remove the code for old-style versions after 20120101.
203 if len(client_tokens) == 4:
204 client_tokens = client_tokens[2:]
205
206 latest_tokens = latest_version.replace('_', '').split('.')
207 if len(latest_tokens) == 4:
208 latest_tokens = latest_tokens[2:]
209
210 for i in range(min(len(client_tokens), len(latest_tokens))):
Chris Sosa0356d3b2010-09-16 22:46:22211 if int(latest_tokens[i]) == int(client_tokens[i]):
212 continue
213 return int(latest_tokens[i]) > int(client_tokens[i])
Daniel Erat8a0bc4a2011-09-30 15:52:52214
215 # Favor four-token new-style versions on the server over old-style versions
216 # on the client if everything else matches.
217 return len(latest_tokens) > len(client_tokens)
Chris Sosa0356d3b2010-09-16 22:46:22218
Chris Sosa0356d3b2010-09-16 22:46:22219 def _UnpackZip(self, image_dir):
220 """Unpacks an image.zip into a given directory."""
221 image = os.path.join(image_dir, self._GetImageName())
222 if os.path.exists(image):
223 return True
224 else:
225 # -n, never clobber an existing file, in case we get invoked
226 # simultaneously by multiple request handlers. This means that
227 # we're assuming each image.zip file lives in a versioned
228 # directory (a la Buildbot).
229 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
230
231 def _GetImageName(self):
232 """Returns the name of the image that should be used."""
233 if self.use_test_image:
234 image_name = 'chromiumos_test_image.bin'
235 else:
236 image_name = 'chromiumos_image.bin'
237 return image_name
238
Chris Sosa0356d3b2010-09-16 22:46:22239 def _GetSize(self, update_path):
240 """Returns the size of the file given."""
241 return os.path.getsize(update_path)
242
243 def _GetHash(self, update_path):
244 """Returns the sha1 of the file given."""
245 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
246 % update_path)
247 return os.popen(cmd).read().rstrip()
248
Andrew de los Reyes5679b972010-10-26 00:34:49249 def _IsDeltaFormatFile(self, filename):
250 try:
251 file_handle = open(filename, 'r')
252 delta_magic = 'CrAU'
253 magic = file_handle.read(len(delta_magic))
254 return magic == delta_magic
255 except Exception:
256 return False
257
Darin Petkov91436cb2010-09-28 15:52:17258 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
259 # it takes advantage of reduced I/O and multiple processors. Something like:
260 # % tee < FILE > /dev/null \
261 # >( openssl dgst -sha256 -binary | openssl base64 ) \
262 # >( openssl sha1 -binary | openssl base64 )
263 def _GetSHA256(self, update_path):
264 """Returns the sha256 of the file given."""
265 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
266 update_path)
267 return os.popen(cmd).read().rstrip()
268
Don Garrettf90edf02010-11-17 01:36:14269 def _GetMd5(self, update_path):
270 """Returns the md5 checksum of the file given."""
271 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
272 return os.popen(cmd).read().rstrip()
273
Don Garrett0c880e22010-11-18 02:13:37274 def _Copy(self, source, dest):
275 """Copies a file from dest to source (if different)"""
276 _LogMessage('Copy File %s -> %s' % (source, dest))
277 if os.path.lexists(dest):
Don Garrettf90edf02010-11-17 01:36:14278 os.remove(dest)
Don Garrett0c880e22010-11-18 02:13:37279 shutil.copy(source, dest)
Don Garrettf90edf02010-11-17 01:36:14280
Andrew de los Reyes5679b972010-10-26 00:34:49281 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 22:46:22282 """Returns a payload to the client corresponding to a new update.
283
284 Args:
285 hash: hash of update blob
Darin Petkov91436cb2010-09-28 15:52:17286 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 22:46:22287 size: size of update blob
288 url: where to find update blob
289 Returns:
290 Xml string to be passed back to client.
291 """
Andrew de los Reyes5679b972010-10-26 00:34:49292 delta = 'false'
293 if is_delta_format:
294 delta = 'true'
[email protected]21a5ca32009-11-04 18:23:23295 payload = """<?xml version="1.0" encoding="UTF-8"?>
296 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 22:02:09297 <daystart elapsed_seconds="%s"/>
[email protected]21a5ca32009-11-04 18:23:23298 <app appid="{%s}" status="ok">
299 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-21 06:23:48300 <updatecheck
Jay Srinivasan9a1c4572012-03-17 02:16:58301 ChromeOSVersion="9999.0.0"
Sean O'Connor14b6a0a2010-03-21 06:23:48302 codebase="%s"
303 hash="%s"
Darin Petkov91436cb2010-09-28 15:52:17304 sha256="%s"
Sean O'Connor14b6a0a2010-03-21 06:23:48305 needsadmin="false"
306 size="%s"
Andrew de los Reyes5679b972010-10-26 00:34:49307 IsDelta="%s"
Satoru Takabayashid733cbe2011-11-15 17:36:32308 status="ok"
309 %s/>
[email protected]21a5ca32009-11-04 18:23:23310 </app>
311 </gupdate>
312 """
Satoru Takabayashid733cbe2011-11-15 17:36:32313 extra_attributes = []
314 if self.critical_update:
315 # The date string looks like '20111115' (2011-11-15). As of writing,
316 # there's no particular format for the deadline value that the
317 # client expects -- it's just empty vs. non-empty.
318 date_str = datetime.date.today().strftime('%Y%m%d')
319 extra_attributes.append('deadline="%s"' % date_str)
320 xml = payload % (self._GetSecondsSinceMidnight(),
321 self.app_id, url, hash, sha256, size, delta,
322 ' '.join(extra_attributes))
323 _LogMessage('Generated update payload: %s' % xml)
324 return xml
[email protected]ded22402009-10-26 22:36:21325
[email protected]21a5ca32009-11-04 18:23:23326 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 22:46:22327 """Returns a payload to the client corresponding to no update."""
Darin Petkov845f1172011-01-05 22:45:24328 payload = """<?xml version="1.0" encoding="UTF-8"?>
329 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
330 <daystart elapsed_seconds="%s"/>
331 <app appid="{%s}" status="ok">
332 <ping status="ok"/>
333 <updatecheck status="noupdate"/>
334 </app>
335 </gupdate>
[email protected]21a5ca32009-11-04 18:23:23336 """
Chris Sosa0356d3b2010-09-16 22:46:22337 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
[email protected]ded22402009-10-26 22:36:21338
Don Garrettf90edf02010-11-17 01:36:14339 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 22:46:22340 """Generates an update gz given a full path to an image.
341
342 Args:
343 image_path: Full path to image.
344 Returns:
345 Path to created update_payload or None on error.
346 """
Don Garrettfff4c322010-11-19 21:37:12347 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa7c931362010-10-12 02:49:01348 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 22:46:22349
Chris Sosa0f1ec842011-02-15 00:33:22350 update_command = [
Chris Sosa5b8b5eb2012-03-27 18:15:27351 'cros_generate_update_payload',
Chris Sosa0f1ec842011-02-15 00:33:22352 '--image="%s"' % image_path,
353 '--output="%s"' % update_path,
Chris Sosa0f1ec842011-02-15 00:33:22354 ]
Chris Sosa4136e692010-10-29 06:42:37355
Chris Sosa0f1ec842011-02-15 00:33:22356 if src_image: update_command.append('--src_image="%s"' % src_image)
357 if not self.vm: update_command.append('--patch_kernel')
358 if self.private_key: update_command.append('--private_key="%s"' %
359 self.private_key)
360
361 update_string = ' '.join(update_command)
362 _LogMessage('Running ' + update_string)
363 if os.system(update_string) != 0:
Chris Sosa417e55d2011-01-26 00:40:48364 _LogMessage('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 22:46:22365 return None
366
Don Garrettfff4c322010-11-19 21:37:12367 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 22:46:22368
Don Garrettf90edf02010-11-17 01:36:14369 def GenerateStatefulFile(self, image_path, output_dir):
370 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 22:46:22371
372 Args:
373 image_path: Full path to image.
374 Returns:
Don Garrettf90edf02010-11-17 01:36:14375 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-11 01:31:18376 Raises:
377 A subprocess exception if the update generator fails to generate a
378 stateful payload.
Chris Sosa0356d3b2010-09-16 22:46:22379 """
Don Garrettfff4c322010-11-19 21:37:12380 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-11 01:31:18381 subprocess.check_call(
Chris Sosa5b8b5eb2012-03-27 18:15:27382 ['cros_generate_stateful_update_payload',
Chris Sosa908fd6f2010-11-11 01:31:18383 '--image=%s' % image_path,
Don Garrettf90edf02010-11-17 01:36:14384 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-11 01:31:18385 ])
Don Garrettfff4c322010-11-19 21:37:12386 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 22:46:22387
Don Garrettf90edf02010-11-17 01:36:14388 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
389 """Find directory to store a cached update.
390
Chris Sosa744e1472011-09-08 02:32:50391 Given one, or two images for an update, this finds which
392 cache directory should hold the update files, even if they don't exist
393 yet. The directory will be inside static_image_dir, and of the form:
Don Garrettf90edf02010-11-17 01:36:14394
Chris Sosa744e1472011-09-08 02:32:50395 Non-delta updates:
396 CACHE_DIR/12345678
397 Delta updates:
398 CACHE_DIR/12345678_12345678
Don Garrettf90edf02010-11-17 01:36:14399
Chris Sosa744e1472011-09-08 02:32:50400 If self.private_key -- Signed updates:
401 CACHE_DIR/from_above+12345678
402 """
403 sub_dir = self._GetMd5(dest_image)
404 if src_image:
405 sub_dir = '%s_%s' % (self._GetMd5(src_image), sub_dir)
Don Garrettf90edf02010-11-17 01:36:14406
Chris Sosa744e1472011-09-08 02:32:50407 if self.private_key:
408 sub_dir = '%s+%s' % (sub_dir, self._GetMd5(self.private_key))
409
Chris Sosa9fba7562012-01-31 18:15:47410 if not self.vm:
411 sub_dir = '%s+patched_kernel' % sub_dir
412
Chris Sosa744e1472011-09-08 02:32:50413 return os.path.join(CACHE_DIR, sub_dir)
Don Garrettf90edf02010-11-17 01:36:14414
Don Garrettfff4c322010-11-19 21:37:12415 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-17 01:36:14416 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 22:46:22417
Chris Sosade91f672010-11-16 18:05:44418 Args:
Don Garrettf90edf02010-11-17 01:36:14419 src_image: image we are updating from (Null/empty for non-delta)
420 image_path: full path to the image.
421 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 18:05:44422 Returns:
Don Garrettfff4c322010-11-19 21:37:12423 update payload name relative to output_dir
Chris Sosade91f672010-11-16 18:05:44424 """
Don Garrettf90edf02010-11-17 01:36:14425 update_file = None
426 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 17:29:43427
Don Garrettf90edf02010-11-17 01:36:14428 # Actually do the generation
429 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 21:37:12430 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-17 01:36:14431 image_path,
432 output_dir)
[email protected]ded22402009-10-26 22:36:21433
Don Garrettf90edf02010-11-17 01:36:14434 if update_file:
435 stateful_update_file = self.GenerateStatefulFile(image_path,
436 output_dir)
437
438 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 21:37:12439 return update_file
Chris Sosa417e55d2011-01-26 00:40:48440 else:
441 _LogMessage('Failed to generate update.')
442 return None
Don Garrettf90edf02010-11-17 01:36:14443
444 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
445 """Force generates an update payload based on the given image_path.
[email protected]ded22402009-10-26 22:36:21446
Chris Sosa0356d3b2010-09-16 22:46:22447 Args:
448 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 22:46:22449 static_image_dir: the directory to move images to after generating.
450 Returns:
Don Garrettf90edf02010-11-17 01:36:14451 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-26 00:40:48452 or None.
Chris Sosa0356d3b2010-09-16 22:46:22453 """
Don Garrettf90edf02010-11-17 01:36:14454 _LogMessage('Generating update for src %s image %s' % (self.src_image,
455 image_path))
Chris Sosae67b78f12010-11-05 00:33:16456
Chris Sosa417e55d2011-01-26 00:40:48457 # If it was pregenerated_path, don't regenerate
458 if self.pregenerated_path:
459 return self.pregenerated_path
Don Garrettfff4c322010-11-19 21:37:12460
Don Garrettf90edf02010-11-17 01:36:14461 # Which sub_dir of static_image_dir should hold our cached update image
462 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
463 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
464
Chris Sosa417e55d2011-01-26 00:40:48465 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
466
Don Garrettf90edf02010-11-17 01:36:14467 # The cached payloads exist in a cache dir
468 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-26 00:40:48469 update_path)
Don Garrettf90edf02010-11-17 01:36:14470 cache_stateful_payload = os.path.join(static_image_dir,
471 cache_sub_dir,
Don Garrettfff4c322010-11-19 21:37:12472 STATEFUL_FILE)
Don Garrettf90edf02010-11-17 01:36:14473
Chris Sosa417e55d2011-01-26 00:40:48474 # Check to see if this cache directory is valid.
475 if not os.path.exists(cache_update_payload) or not os.path.exists(
476 cache_stateful_payload):
Don Garrettf90edf02010-11-17 01:36:14477 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-26 00:40:48478 # Clean up stale state.
479 os.system('rm -rf "%s"' % full_cache_dir)
480 os.makedirs(full_cache_dir)
481 return_path = self.GenerateUpdateImage(image_path,
482 full_cache_dir)
Don Garrettf90edf02010-11-17 01:36:14483
Chris Sosa417e55d2011-01-26 00:40:48484 # Clean up cache dir since it's not valid.
485 if not return_path:
486 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-17 01:36:14487 return None
Chris Sosa417e55d2011-01-26 00:40:48488
489 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-17 01:36:14490
Chris Sosa08d55a22011-01-20 00:08:02491 # Generation complete, copy if requested.
492 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-26 00:40:48493 # The final results exist directly in static
494 update_payload = os.path.join(static_image_dir,
495 UPDATE_FILE)
496 stateful_payload = os.path.join(static_image_dir,
497 STATEFUL_FILE)
Chris Sosa08d55a22011-01-20 00:08:02498 self._Copy(cache_update_payload, update_payload)
499 self._Copy(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-26 00:40:48500 return UPDATE_FILE
501 else:
502 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 22:46:22503
504 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-17 01:36:14505 static_image_dir):
Chris Sosa0356d3b2010-09-16 22:46:22506 """Generates an update using the latest image that has been built.
507
508 This will only generate an update if the newest update is newer than that
509 on the client or client_version is 'ForcedUpdate'.
510
511 Args:
512 board_id: Name of the board.
513 client_version: Current version of the client or 'ForcedUpdate'
514 static_image_dir: the directory to move images to after generating.
515 Returns:
Don Garrettf90edf02010-11-17 01:36:14516 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 22:46:22517 """
518 latest_image_dir = self._GetLatestImageDir(board_id)
519 latest_version = self._GetVersionFromDir(latest_image_dir)
520 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
521
Chris Sosa7c931362010-10-12 02:49:01522 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 22:46:22523 latest_image_path)
524
525 # Check to see whether or not we should update.
526 if client_version != 'ForcedUpdate' and not self._CanUpdate(
527 client_version, latest_version):
Chris Sosa7c931362010-10-12 02:49:01528 _LogMessage('no update')
Don Garrettf90edf02010-11-17 01:36:14529 return None
Chris Sosa0356d3b2010-09-16 22:46:22530
Don Garrettf90edf02010-11-17 01:36:14531 return self.GenerateUpdateImageWithCache(latest_image_path,
532 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 22:46:22533
Andrew de los Reyes52620802010-04-12 20:40:07534 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
535 """Imports a factory-floor server configuration file. The file should
536 be in this format:
537 config = [
538 {
539 'qual_ids': set([1, 2, 3, "x86-generic"]),
540 'factory_image': 'generic-factory.gz',
541 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
542 'release_image': 'generic-release.gz',
543 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
544 'oempartitionimg_image': 'generic-oem.gz',
545 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-20 05:17:08546 'efipartitionimg_image': 'generic-efi.gz',
547 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 20:40:07548 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 03:44:26549 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 01:56:15550 'firmware_image': 'generic-firmware.gz',
551 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 20:40:07552 },
553 {
554 'qual_ids': set([6]),
555 'factory_image': '6-factory.gz',
556 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
557 'release_image': '6-release.gz',
558 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
559 'oempartitionimg_image': '6-oem.gz',
560 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-20 05:17:08561 'efipartitionimg_image': '6-efi.gz',
562 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 20:40:07563 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 03:44:26564 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 01:56:15565 'firmware_image': '6-firmware.gz',
566 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 20:40:07567 },
568 ]
569 The server will look for the files by name in the static files
570 directory.
Chris Sosaa73ec162010-05-04 03:18:02571
Andrew de los Reyes52620802010-04-12 20:40:07572 If validate_checksums is True, validates checksums and exits. If
573 a checksum mismatch is found, it's printed to the screen.
574 """
575 f = open(filename, 'r')
576 output = {}
577 exec(f.read(), output)
578 self.factory_config = output['config']
579 success = True
580 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 03:44:26581 for key in stanza.copy().iterkeys():
582 suffix = '_image'
583 if key.endswith(suffix):
584 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 22:46:22585 stanza[kind + '_size'] = self._GetSize(os.path.join(
586 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 03:44:26587 if validate_checksums:
Chris Sosa0356d3b2010-09-16 22:46:22588 factory_checksum = self._GetHash(os.path.join(self.static_dir,
589 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 03:44:26590 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 22:46:22591 print ('Error: checksum mismatch for %s. Expected "%s" but file '
592 'has checksum "%s".' % (stanza[kind + '_image'],
593 stanza[kind + '_checksum'],
594 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 03:44:26595 success = False
Chris Sosa0356d3b2010-09-16 22:46:22596
Andrew de los Reyes52620802010-04-12 20:40:07597 if validate_checksums:
598 if success is False:
599 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 22:46:22600
Andrew de los Reyes52620802010-04-12 20:40:07601 print 'Config file looks good.'
602
603 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 12:18:41604 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 20:40:07605 for stanza in self.factory_config:
606 if board_id not in stanza['qual_ids']:
607 continue
Nick Sanders15cd6ae2010-06-30 19:30:56608 if kind + '_image' not in stanza:
609 break
Andrew de los Reyes52620802010-04-12 20:40:07610 return (stanza[kind + '_image'],
611 stanza[kind + '_checksum'],
612 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 19:30:56613 return (None, None, None)
[email protected]ded22402009-10-26 22:36:21614
Chris Sosa7c931362010-10-12 02:49:01615 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 22:46:22616 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
617 if filename is None:
Chris Sosa7c931362010-10-12 02:49:01618 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 22:46:22619 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-15 01:01:52620 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-26 00:34:49621 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-12 02:49:01622 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 15:52:17623 # Factory install is using memento updater which is using the sha-1 hash so
624 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-26 00:34:49625 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 22:46:22626
Chris Sosa151643e2010-10-28 21:40:57627 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
628 static_image_dir):
Don Garrettf90edf02010-11-17 01:36:14629 """Generates an update for non-factory image.
Don Garrett710470d2010-11-16 01:43:44630
Don Garrettf90edf02010-11-17 01:36:14631 Returns:
632 file name relative to static_image_dir on success.
633 """
Dale Curtis723ec472010-11-30 22:06:47634 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
635 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
636
Don Garrett0c880e22010-11-18 02:13:37637 if self.forced_payload:
638 # If the forced payload is not already in our static_image_dir,
639 # copy it there.
Don Garrettee25e552010-11-23 20:09:35640 src_path = os.path.abspath(self.forced_payload)
Don Garrettee25e552010-11-23 20:09:35641 src_stateful = os.path.join(os.path.dirname(src_path),
642 STATEFUL_FILE)
Don Garrettee25e552010-11-23 20:09:35643
644 # Only copy the files if the source directory is different from dest.
645 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
646 self._Copy(src_path, dest_path)
647
648 # The stateful payload is optional.
649 if os.path.exists(src_stateful):
650 self._Copy(src_stateful, dest_stateful)
651 else:
652 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
653 STATEFUL_FILE)
654 if os.path.exists(dest_stateful):
655 os.remove(dest_stateful)
Don Garrett0c880e22010-11-18 02:13:37656
Don Garrettfff4c322010-11-19 21:37:12657 return UPDATE_FILE
Don Garrett0c880e22010-11-18 02:13:37658 elif self.forced_image:
Don Garrettf90edf02010-11-17 01:36:14659 return self.GenerateUpdateImageWithCache(
660 self.forced_image,
661 static_image_dir=static_image_dir)
662 elif self.serve_only:
Dale Curtis723ec472010-11-30 22:06:47663 # Warn if update or stateful files can't be found.
664 if not os.path.exists(dest_path):
665 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
666 UPDATE_FILE)
667
668 if not os.path.exists(dest_stateful):
669 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
670 STATEFUL_FILE)
671
672 return UPDATE_FILE
Don Garrettf90edf02010-11-17 01:36:14673 else:
674 if board_id:
675 return self.GenerateLatestUpdateImage(board_id,
676 client_version,
677 static_image_dir)
678
Chris Sosa417e55d2011-01-26 00:40:48679 _LogMessage('Failed to genereate update. '
680 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-17 01:36:14681 return None
Chris Sosa2c048f12010-10-27 23:05:27682
683 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-26 00:40:48684 """Pre-generates an update and prints out the relative path it.
685
686 Returns relative path of the update on success.
Don Garrettf90edf02010-11-17 01:36:14687 """
Chris Sosa2c048f12010-10-27 23:05:27688 # Does not work with factory config.
689 assert(not self.factory_config)
690 _LogMessage('Pre-generating the update payload.')
691 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-26 00:40:48692 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
693 self.board, '0.0.0.0', self.static_dir)
694 if pregenerated_update:
695 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
696
697 return pregenerated_update
Chris Sosa2c048f12010-10-27 23:05:27698
Sean O'Connor14b6a0a2010-03-21 06:23:48699 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 22:46:22700 """Handles an update ping from an update client.
701
702 Args:
703 data: xml blob from client.
704 label: optional label for the update.
705 Returns:
706 Update payload message for client.
707 """
Chris Sosa9841e1c2010-10-14 17:51:45708 # Set hostname as the hostname that the client is calling to and set up
Chris Sosa28be7db2012-06-13 23:26:10709 # the url base. If behind apache mod_proxy | mod_rewrite, the hostname will
710 # be in X-Forwarded-Host.
711 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
712 if x_forwarded_host:
713 self.hostname = 'http://' + x_forwarded_host
714 else:
715 self.hostname = cherrypy.request.base
716
Chris Sosa9841e1c2010-10-14 17:51:45717 if self.urlbase:
718 static_urlbase = self.urlbase
719 elif self.serve_only:
720 static_urlbase = '%s/static/archive' % self.hostname
721 else:
722 static_urlbase = '%s/static' % self.hostname
723
Don Garrett0ad09372010-12-07 00:20:30724 # If we have a proxy port, adjust the URL we instruct the client to
725 # use to go through the proxy.
726 if self.proxy_port:
727 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
728
Chris Sosa9841e1c2010-10-14 17:51:45729 _LogMessage('Using static url base %s' % static_urlbase)
730 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 22:46:22731
Chris Sosa9841e1c2010-10-14 17:51:45732 update_dom = minidom.parseString(data)
733 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 22:46:22734
Dale Curtisc9aaf3a2011-08-09 22:47:40735 # Determine request IP, strip any IPv6 data for simplicity.
736 client_ip = cherrypy.request.remote.ip.split(':')[-1]
737
Gilad Arnold286a0062012-01-12 21:47:02738 # Obtain (or init) info object for this client.
739 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
740
741 # Initialize an empty dictionary for event attributes.
742 log_message = {}
Dale Curtisc9aaf3a2011-08-09 22:47:40743
744 # Store event details in the host info dictionary for API usage.
745 event = root.getElementsByTagName('o:event')
746 if event:
Gilad Arnold286a0062012-01-12 21:47:02747 event_result = int(event[0].getAttribute('eventresult'))
748 event_type = int(event[0].getAttribute('eventtype'))
Gilad Arnoldb11a8942012-03-13 22:33:21749 client_previous_version = (event[0].getAttribute('previousversion')
750 if event[0].hasAttribute('previousversion')
751 else None)
Gilad Arnold286a0062012-01-12 21:47:02752 # Store attributes to legacy host info structure
753 curr_host_info.attrs['last_event_status'] = event_result
754 curr_host_info.attrs['last_event_type'] = event_type
755 # Add attributes to log message
756 log_message['event_result'] = event_result
757 log_message['event_type'] = event_type
Gilad Arnoldb11a8942012-03-13 22:33:21758 if client_previous_version is not None:
759 log_message['previous_version'] = client_previous_version
Gilad Arnold286a0062012-01-12 21:47:02760
761 # Get information about the requester.
762 query = root.getElementsByTagName('o:app')[0]
763 if query:
764 client_version = query.getAttribute('version')
765 channel = query.getAttribute('track')
766 board_id = (query.hasAttribute('board') and query.getAttribute('board')
767 or self._GetDefaultBoardID())
768 # Add attributes to log message
769 log_message['version'] = client_version
770 log_message['track'] = channel
771 log_message['board'] = board_id
772
773 # Log client's message
774 curr_host_info.AddLogEntry(log_message)
Dale Curtisc9aaf3a2011-08-09 22:47:40775
Chris Sosa0356d3b2010-09-16 22:46:22776 # We only generate update payloads for updatecheck requests.
777 update_check = root.getElementsByTagName('o:updatecheck')
778 if not update_check:
Chris Sosa7c931362010-10-12 02:49:01779 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 22:46:22780 # TODO(sosa): Generate correct non-updatecheck payload to better test
781 # update clients.
782 return self.GetNoUpdatePayload()
783
Dale Curtisc9aaf3a2011-08-09 22:47:40784 # Store version for this host in the cache.
Gilad Arnold286a0062012-01-12 21:47:02785 curr_host_info.attrs['last_known_version'] = client_version
Dale Curtisc9aaf3a2011-08-09 22:47:40786
787 # Check if an update has been forced for this client.
Gilad Arnold286a0062012-01-12 21:47:02788 forced_update = curr_host_info.PopAttr('forced_update_label', None)
Dale Curtisc9aaf3a2011-08-09 22:47:40789 if forced_update:
790 label = forced_update
791
Chris Sosa0356d3b2010-09-16 22:46:22792 # Separate logic as Factory requests have static url's that override
793 # other options.
Andrew de los Reyes52620802010-04-12 20:40:07794 if self.factory_config:
Chris Sosa7c931362010-10-12 02:49:01795 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 12:18:41796 else:
Chris Sosa0356d3b2010-09-16 22:46:22797 static_image_dir = self.static_dir
798 if label:
799 static_image_dir = os.path.join(static_image_dir, label)
800
Don Garrettf90edf02010-11-17 01:36:14801 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
802 client_version,
803 static_image_dir)
804 if payload_path:
805 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-26 00:34:49806 hash = self._GetHash(filename)
807 sha256 = self._GetSHA256(filename)
808 size = self._GetSize(filename)
809 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 23:54:41810 if label:
Don Garrettf90edf02010-11-17 01:36:14811 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 22:46:22812 else:
Don Garrettf90edf02010-11-17 01:36:14813 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 23:54:41814
Chris Sosa7c931362010-10-12 02:49:01815 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-26 00:34:49816 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 22:46:22817 else:
Nick Sanders723f3262010-09-16 12:18:41818 return self.GetNoUpdatePayload()
Dale Curtisc9aaf3a2011-08-09 22:47:40819
820 def HandleHostInfoPing(self, ip):
821 """Returns host info dictionary for the given IP in JSON format."""
822 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 21:47:02823 if ip in self.host_infos.table:
824 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
825
826 def HandleHostLogPing(self, ip):
827 """Returns a complete log of events for host in JSON format."""
828 if ip == 'all':
829 return json.dumps(
830 dict([(key, self.host_infos.table[key].log)
831 for key in self.host_infos.table]))
832 if ip in self.host_infos.table:
833 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 22:47:40834
835 def HandleSetUpdatePing(self, ip, label):
836 """Sets forced_update_label for a given host."""
837 assert ip, 'No ip provided.'
838 assert label, 'No label provided.'
Gilad Arnold286a0062012-01-12 21:47:02839 self.host_infos.GetInitHostInfo(ip).attrs['forced_update_label'] = label