blob: 6bbfa0f493f81440ccf7d682b6e76ff3a91cfe93 [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
Dale Curtisc9aaf3a2011-08-09 22:47:405import json
[email protected]ded22402009-10-26 22:36:216import os
Chris Sosa05491b12010-11-09 01:14:167import subprocess
Gilad Arnolde74b3812013-04-22 18:27:388import sys
Darin Petkov2b2ff4b2010-07-27 22:02:099import time
Gilad Arnold0c9c8602012-10-03 06:58:5810import urllib2
Don Garrett0ad09372010-12-07 00:20:3011import urlparse
Chris Sosa7c931362010-10-12 02:49:0112
Gilad Arnoldabb352e2012-09-23 08:24:2713import cherrypy
14
Gilad Arnolde74b3812013-04-22 18:27:3815# Allow importing from dev/host/lib when running from source tree.
16lib_dir = os.path.join(os.path.dirname(__file__), 'host', 'lib')
17if os.path.exists(lib_dir) and os.path.isdir(lib_dir):
18 sys.path.insert(1, lib_dir)
19
Gilad Arnoldabb352e2012-09-23 08:24:2720from build_util import BuildObject
Chris Sosa52148582012-11-15 23:35:5821import autoupdate_lib
Gilad Arnold55a2a372012-10-02 16:46:3222import common_util
Gilad Arnoldc65330c2012-09-20 22:17:4823import log_util
Gilad Arnolde74b3812013-04-22 18:27:3824# pylint: disable=F0401
25import update_payload
Chris Sosa05491b12010-11-09 01:14:1626
Gilad Arnoldc65330c2012-09-20 22:17:4827
28# Module-local log function.
Chris Sosa6a3697f2013-01-30 00:44:4329def _Log(message, *args):
30 return log_util.LogWithTag('UPDATE', message, *args)
Gilad Arnoldc65330c2012-09-20 22:17:4831
[email protected]ded22402009-10-26 22:36:2132
Chris Sosa417e55d2011-01-26 00:40:4833UPDATE_FILE = 'update.gz'
Chris Sosa6a3697f2013-01-30 00:44:4334METADATA_FILE = 'update.meta'
Chris Sosa417e55d2011-01-26 00:40:4835STATEFUL_FILE = 'stateful.tgz'
36CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 22:46:2237
Don Garrett0ad09372010-12-07 00:20:3038
Gilad Arnold0c9c8602012-10-03 06:58:5839class AutoupdateError(Exception):
40 """Exception classes used by this module."""
41 pass
42
43
Don Garrett0ad09372010-12-07 00:20:3044def _ChangeUrlPort(url, new_port):
45 """Return the URL passed in with a different port"""
46 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
47 host_port = netloc.split(':')
48
49 if len(host_port) == 1:
50 host_port.append(new_port)
51 else:
52 host_port[1] = new_port
53
54 print host_port
55 netloc = "%s:%s" % tuple(host_port)
56
57 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
58
Chris Sosa6a3697f2013-01-30 00:44:4359def _NonePathJoin(*args):
60 """os.path.join that filters None's from the argument list."""
61 return os.path.join(*filter(None, args))
Don Garrett0ad09372010-12-07 00:20:3062
Chris Sosa6a3697f2013-01-30 00:44:4363
64class HostInfo(object):
Gilad Arnold286a0062012-01-12 21:47:0265 """Records information about an individual host.
66
67 Members:
68 attrs: Static attributes (legacy)
69 log: Complete log of recorded client entries
70 """
71
72 def __init__(self):
73 # A dictionary of current attributes pertaining to the host.
74 self.attrs = {}
75
76 # A list of pairs consisting of a timestamp and a dictionary of recorded
77 # attributes.
78 self.log = []
79
80 def __repr__(self):
81 return 'attrs=%s, log=%s' % (self.attrs, self.log)
82
83 def AddLogEntry(self, entry):
84 """Append a new log entry."""
85 # Append a timestamp.
86 assert not 'timestamp' in entry, 'Oops, timestamp field already in use'
87 entry['timestamp'] = time.strftime('%Y-%m-%d %H:%M:%S')
88 # Add entry to hosts' message log.
89 self.log.append(entry)
90
Gilad Arnold286a0062012-01-12 21:47:0291
Chris Sosa6a3697f2013-01-30 00:44:4392class HostInfoTable(object):
Gilad Arnold286a0062012-01-12 21:47:0293 """Records information about a set of hosts who engage in update activity.
94
95 Members:
96 table: Table of information on hosts.
97 """
98
99 def __init__(self):
100 # A dictionary of host information. Keys are normally IP addresses.
101 self.table = {}
102
103 def __repr__(self):
104 return '%s' % self.table
105
106 def GetInitHostInfo(self, host_id):
107 """Return a host's info object, or create a new one if none exists."""
108 return self.table.setdefault(host_id, HostInfo())
109
110 def GetHostInfo(self, host_id):
111 """Return an info object for given host, if such exists."""
Chris Sosa1885d032012-11-30 01:07:27112 return self.table.get(host_id)
Gilad Arnold286a0062012-01-12 21:47:02113
114
Chris Sosa6a3697f2013-01-30 00:44:43115class UpdateMetadata(object):
116 """Object containing metadata about an update payload."""
117
118 def __init__(self, sha1, sha256, size, is_delta_format):
119 self.sha1 = sha1
120 self.sha256 = sha256
121 self.size = size
122 self.is_delta_format = is_delta_format
123
124
[email protected]64244662009-11-12 00:52:08125class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 22:46:22126 """Class that contains functionality that handles Chrome OS update pings.
127
128 Members:
Gilad Arnold0c9c8602012-10-03 06:58:58129 serve_only: serve only pre-built updates. static_dir must contain
130 update.gz and stateful.tgz.
Gilad Arnold0c9c8602012-10-03 06:58:58131 use_test_image: use chromiumos_test_image.bin rather than the standard.
132 urlbase: base URL, other than devserver, for update images.
133 forced_image: path to an image to use for all updates.
134 payload_path: path to pre-generated payload to serve.
135 src_image: if specified, creates a delta payload from this image.
136 proxy_port: port of local proxy to tell client to connect to you
137 through.
Chris Sosa3ae4dc12013-03-29 18:47:00138 patch_kernel: Patch the kernel when generating updates
Gilad Arnold0c9c8602012-10-03 06:58:58139 board: board for the image. Needed for pre-generating of updates.
140 copy_to_static_root: copies images generated from the cache to ~/static.
141 private_key: path to private key in PEM format.
Gilad Arnold8318eac2012-10-04 19:52:23142 critical_update: whether provisioned payload is critical.
143 remote_payload: whether provisioned payload is remotely staged.
144 max_updates: maximum number of updates we'll try to provision.
145 host_log: record full history of host update events.
Chris Sosa0356d3b2010-09-16 22:46:22146 """
[email protected]ded22402009-10-26 22:36:21147
Gilad Arnold0c9c8602012-10-03 06:58:58148 _PAYLOAD_URL_PREFIX = '/static/'
149 _FILEINFO_URL_PREFIX = '/api/fileinfo/'
150
Chris Sosa6a3697f2013-01-30 00:44:43151 SHA1_ATTR = 'sha1'
152 SHA256_ATTR = 'sha256'
153 SIZE_ATTR = 'size'
154 ISDELTA_ATTR = 'is_delta'
155
Sean O'Connor1f7fd362010-04-07 23:34:52156 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Gilad Arnold0c9c8602012-10-03 06:58:58157 forced_image=None, payload_path=None,
Chris Sosa3ae4dc12013-03-29 18:47:00158 proxy_port=None, src_image='', patch_kernel=True, board=None,
Chris Sosa0f1ec842011-02-15 00:33:22159 copy_to_static_root=True, private_key=None,
Chris Sosa52148582012-11-15 23:35:58160 critical_update=False, remote_payload=False, max_updates= -1,
Chris Sosa6a3697f2013-01-30 00:44:43161 host_log=False, *args, **kwargs):
Sean O'Connor14b6a0a2010-03-21 06:23:48162 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 23:34:52163 self.serve_only = serve_only
Chris Sosa0356d3b2010-09-16 22:46:22164 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 23:54:41165 if urlbase:
Chris Sosa9841e1c2010-10-14 17:51:45166 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 23:54:41167 else:
Chris Sosa9841e1c2010-10-14 17:51:45168 self.urlbase = None
Chris Sosa5d342a22010-09-28 23:54:41169
Chris Sosa0356d3b2010-09-16 22:46:22170 self.forced_image = forced_image
Gilad Arnold0c9c8602012-10-03 06:58:58171 self.payload_path = payload_path
Chris Sosa62f720b2010-10-27 04:39:48172 self.src_image = src_image
Don Garrett0ad09372010-12-07 00:20:30173 self.proxy_port = proxy_port
Chris Sosa3ae4dc12013-03-29 18:47:00174 self.patch_kernel = patch_kernel
Chris Sosae67b78f12010-11-05 00:33:16175 self.board = board
Chris Sosa08d55a22011-01-20 00:08:02176 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-15 00:33:22177 self.private_key = private_key
Satoru Takabayashid733cbe2011-11-15 17:36:32178 self.critical_update = critical_update
Gilad Arnold0c9c8602012-10-03 06:58:58179 self.remote_payload = remote_payload
Jay Srinivasanac69d262012-10-31 02:05:53180 self.max_updates = max_updates
Gilad Arnold8318eac2012-10-04 19:52:23181 self.host_log = host_log
Don Garrettfff4c322010-11-19 21:37:12182
Chris Sosa417e55d2011-01-26 00:40:48183 # Path to pre-generated file.
184 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-21 06:23:48185
Dale Curtisc9aaf3a2011-08-09 22:47:40186 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 21:47:02187 # information about a given host. A host is identified by its IP address.
188 # The info stored for each host includes a complete log of events for this
189 # host, as well as a dictionary of current attributes derived from events.
190 self.host_infos = HostInfoTable()
Dale Curtisc9aaf3a2011-08-09 22:47:40191
Chris Sosa6a3697f2013-01-30 00:44:43192 @classmethod
193 def _ReadMetadataFromStream(cls, stream):
194 """Returns metadata obj from input json stream that implements .read()."""
195 file_attr_dict = {}
196 try:
197 file_attr_dict = json.loads(stream.read())
198 except IOError:
199 return None
200
201 sha1 = file_attr_dict.get(cls.SHA1_ATTR)
202 sha256 = file_attr_dict.get(cls.SHA256_ATTR)
203 size = file_attr_dict.get(cls.SIZE_ATTR)
204 is_delta = file_attr_dict.get(cls.ISDELTA_ATTR)
205 return UpdateMetadata(sha1, sha256, size, is_delta)
206
207 @staticmethod
208 def _ReadMetadataFromFile(payload_dir):
209 """Returns metadata object from the metadata_file in the payload_dir"""
210 metadata_file = os.path.join(payload_dir, METADATA_FILE)
211 if os.path.exists(metadata_file):
212 with open(metadata_file, 'r') as metadata_stream:
213 return Autoupdate._ReadMetadataFromStream(metadata_stream)
214
215 @classmethod
216 def _StoreMetadataToFile(cls, payload_dir, metadata_obj):
217 """Stores metadata object into the metadata_file of the payload_dir"""
218 file_dict = {cls.SHA1_ATTR: metadata_obj.sha1,
219 cls.SHA256_ATTR: metadata_obj.sha256,
220 cls.SIZE_ATTR: metadata_obj.size,
221 cls.ISDELTA_ATTR: metadata_obj.is_delta_format}
222 metadata_file = os.path.join(payload_dir, METADATA_FILE)
223 with open(metadata_file, 'w') as file_handle:
224 json.dump(file_dict, file_handle)
225
Chris Sosa0356d3b2010-09-16 22:46:22226 def _GetDefaultBoardID(self):
227 """Returns the default board id stored in .default_board."""
228 board_file = '%s/.default_board' % (self.scripts_dir)
229 try:
230 return open(board_file).read()
231 except IOError:
232 return 'x86-generic'
233
Chris Sosa6a3697f2013-01-30 00:44:43234 def _GetLatestImageDir(self, board):
Chris Sosa0356d3b2010-09-16 22:46:22235 """Returns the latest image dir based on shell script."""
Chris Sosa6a3697f2013-01-30 00:44:43236 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board)
Chris Sosa0356d3b2010-09-16 22:46:22237 return os.popen(cmd).read().strip()
238
Chris Sosa52148582012-11-15 23:35:58239 @staticmethod
240 def _GetVersionFromDir(image_dir):
Chris Sosa0356d3b2010-09-16 22:46:22241 """Returns the version of the image based on the name of the directory."""
242 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 15:52:52243 parts = latest_version.split('-')
244 if len(parts) == 2:
245 # Old-style, e.g. "0.15.938.2011_08_23_0941-a1".
246 # TODO(derat): Remove the code for old-style versions after 20120101.
247 return parts[0]
248 else:
249 # New-style, e.g. "R16-1102.0.2011_09_30_0806-a1".
250 return parts[1]
Chris Sosa0356d3b2010-09-16 22:46:22251
Chris Sosa52148582012-11-15 23:35:58252 @staticmethod
253 def _CanUpdate(client_version, latest_version):
Don Garrettf90edf02010-11-17 01:36:14254 """Returns true if the latest_version is greater than the client_version.
255 """
Chris Sosa6a3697f2013-01-30 00:44:43256 _Log('client version %s latest version %s', client_version, latest_version)
Daniel Erat8a0bc4a2011-09-30 15:52:52257
258 client_tokens = client_version.replace('_', '').split('.')
259 # If the client has an old four-token version like "0.16.892.0", drop the
260 # first two tokens -- we use versions like "892.0.0" now.
261 # TODO(derat): Remove the code for old-style versions after 20120101.
262 if len(client_tokens) == 4:
263 client_tokens = client_tokens[2:]
264
265 latest_tokens = latest_version.replace('_', '').split('.')
266 if len(latest_tokens) == 4:
267 latest_tokens = latest_tokens[2:]
268
269 for i in range(min(len(client_tokens), len(latest_tokens))):
Chris Sosa0356d3b2010-09-16 22:46:22270 if int(latest_tokens[i]) == int(client_tokens[i]):
271 continue
272 return int(latest_tokens[i]) > int(client_tokens[i])
Daniel Erat8a0bc4a2011-09-30 15:52:52273
274 # Favor four-token new-style versions on the server over old-style versions
275 # on the client if everything else matches.
276 return len(latest_tokens) > len(client_tokens)
Chris Sosa0356d3b2010-09-16 22:46:22277
Chris Sosa0356d3b2010-09-16 22:46:22278 def _GetImageName(self):
279 """Returns the name of the image that should be used."""
280 if self.use_test_image:
281 image_name = 'chromiumos_test_image.bin'
282 else:
283 image_name = 'chromiumos_image.bin'
Chris Sosa6a3697f2013-01-30 00:44:43284
Chris Sosa0356d3b2010-09-16 22:46:22285 return image_name
286
Chris Sosa52148582012-11-15 23:35:58287 @staticmethod
Gilad Arnolde74b3812013-04-22 18:27:38288 def IsDeltaFormatFile(filename):
Andrew de los Reyes5679b972010-10-26 00:34:49289 try:
Gilad Arnolde74b3812013-04-22 18:27:38290 with open(filename) as payload_file:
291 payload = update_payload.Payload(payload_file)
292 payload.Init()
293 return payload.IsDelta()
294 except (IOError, update_payload.PayloadError):
295 # For unit tests we may not have real files, so it's ok to ignore these
296 # errors.
Andrew de los Reyes5679b972010-10-26 00:34:49297 return False
298
Don Garrettf90edf02010-11-17 01:36:14299 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 22:46:22300 """Generates an update gz given a full path to an image.
301
302 Args:
303 image_path: Full path to image.
Chris Sosa6a3697f2013-01-30 00:44:43304 Raises:
305 subprocess.CalledProcessError if the update generator fails to generate a
306 stateful payload.
Chris Sosa0356d3b2010-09-16 22:46:22307 """
Don Garrettfff4c322010-11-19 21:37:12308 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa6a3697f2013-01-30 00:44:43309 _Log('Generating update image %s', update_path)
Chris Sosa0356d3b2010-09-16 22:46:22310
Chris Sosa0f1ec842011-02-15 00:33:22311 update_command = [
Chris Sosa5b8b5eb2012-03-27 18:15:27312 'cros_generate_update_payload',
Chris Sosa6a3697f2013-01-30 00:44:43313 '--image', image_path,
314 '--output', update_path,
Chris Sosa0f1ec842011-02-15 00:33:22315 ]
Chris Sosa4136e692010-10-29 06:42:37316
Chris Sosa52148582012-11-15 23:35:58317 if src_image:
Chris Sosa6a3697f2013-01-30 00:44:43318 update_command.extend(['--src_image', src_image])
Chris Sosa52148582012-11-15 23:35:58319
Chris Sosa3ae4dc12013-03-29 18:47:00320 if self.patch_kernel:
Chris Sosa52148582012-11-15 23:35:58321 update_command.append('--patch_kernel')
322
323 if self.private_key:
Chris Sosa6a3697f2013-01-30 00:44:43324 update_command.extend(['--private_key', self.private_key])
Chris Sosa0f1ec842011-02-15 00:33:22325
Chris Sosa6a3697f2013-01-30 00:44:43326 _Log('Running %s', ' '.join(update_command))
327 subprocess.check_call(update_command)
Chris Sosa0356d3b2010-09-16 22:46:22328
Chris Sosa52148582012-11-15 23:35:58329 @staticmethod
330 def GenerateStatefulFile(image_path, output_dir):
Don Garrettf90edf02010-11-17 01:36:14331 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 22:46:22332
333 Args:
334 image_path: Full path to image.
Chris Sosa908fd6f2010-11-11 01:31:18335 Raises:
Chris Sosa6a3697f2013-01-30 00:44:43336 subprocess.CalledProcessError if the update generator fails to generate a
Chris Sosa908fd6f2010-11-11 01:31:18337 stateful payload.
Chris Sosa0356d3b2010-09-16 22:46:22338 """
Chris Sosa6a3697f2013-01-30 00:44:43339 update_command = [
340 'cros_generate_stateful_update_payload',
341 '--image', image_path,
342 '--output_dir', output_dir,
343 ]
344 _Log('Running %s', ' '.join(update_command))
345 subprocess.check_call(update_command)
Chris Sosa0356d3b2010-09-16 22:46:22346
Don Garrettf90edf02010-11-17 01:36:14347 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
348 """Find directory to store a cached update.
349
Gilad Arnold55a2a372012-10-02 16:46:32350 Given one, or two images for an update, this finds which cache directory
351 should hold the update files, even if they don't exist yet.
Don Garrettf90edf02010-11-17 01:36:14352
Gilad Arnold55a2a372012-10-02 16:46:32353 Returns:
354 A directory path for storing a cached update, of the following form:
355 Non-delta updates:
356 CACHE_DIR/<dest_hash>
357 Delta updates:
358 CACHE_DIR/<src_hash>_<dest_hash>
359 Signed updates (self.private_key):
360 CACHE_DIR/<src_hash>_<dest_hash>+<private_key_hash>
Chris Sosa744e1472011-09-08 02:32:50361 """
Gilad Arnold55a2a372012-10-02 16:46:32362 update_dir = ''
Chris Sosa744e1472011-09-08 02:32:50363 if src_image:
Gilad Arnold55a2a372012-10-02 16:46:32364 update_dir += common_util.GetFileMd5(src_image) + '_'
Don Garrettf90edf02010-11-17 01:36:14365
Gilad Arnold55a2a372012-10-02 16:46:32366 update_dir += common_util.GetFileMd5(dest_image)
Chris Sosa744e1472011-09-08 02:32:50367 if self.private_key:
Gilad Arnold55a2a372012-10-02 16:46:32368 update_dir += '+' + common_util.GetFileMd5(self.private_key)
Chris Sosa744e1472011-09-08 02:32:50369
Chris Sosa3ae4dc12013-03-29 18:47:00370 if self.patch_kernel:
Gilad Arnold55a2a372012-10-02 16:46:32371 update_dir += '+patched_kernel'
Chris Sosa9fba7562012-01-31 18:15:47372
Gilad Arnold55a2a372012-10-02 16:46:32373 return os.path.join(CACHE_DIR, update_dir)
Don Garrettf90edf02010-11-17 01:36:14374
Don Garrettfff4c322010-11-19 21:37:12375 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-17 01:36:14376 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 22:46:22377
Chris Sosade91f672010-11-16 18:05:44378 Args:
Don Garrettf90edf02010-11-17 01:36:14379 src_image: image we are updating from (Null/empty for non-delta)
380 image_path: full path to the image.
Chris Sosa6a3697f2013-01-30 00:44:43381 output_dir: the directory to write the update payloads to
382 Raises:
383 AutoupdateError if it failed to generate either update or stateful
384 payload.
Chris Sosade91f672010-11-16 18:05:44385 """
Chris Sosa6a3697f2013-01-30 00:44:43386 _Log('Generating update for image %s', image_path)
Andrew de los Reyes9a528712010-06-30 17:29:43387
Chris Sosa6a3697f2013-01-30 00:44:43388 # Delete any previous state in this directory.
389 os.system('rm -rf "%s"' % output_dir)
390 os.makedirs(output_dir)
[email protected]ded22402009-10-26 22:36:21391
Chris Sosa6a3697f2013-01-30 00:44:43392 try:
393 self.GenerateUpdateFile(self.src_image, image_path, output_dir)
394 self.GenerateStatefulFile(image_path, output_dir)
395 except subprocess.CalledProcessError:
396 os.system('rm -rf "%s"' % output_dir)
397 raise AutoupdateError('Failed to generate update in %s' % output_dir)
Don Garrettf90edf02010-11-17 01:36:14398
399 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
400 """Force generates an update payload based on the given image_path.
[email protected]ded22402009-10-26 22:36:21401
Chris Sosa0356d3b2010-09-16 22:46:22402 Args:
403 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 22:46:22404 static_image_dir: the directory to move images to after generating.
405 Returns:
Chris Sosa6a3697f2013-01-30 00:44:43406 update directory relative to static_image_dir. None if it should
407 serve from the static_image_dir.
408 Raises:
409 AutoupdateError if it we need to generate a payload and fail to do so.
Chris Sosa0356d3b2010-09-16 22:46:22410 """
Chris Sosa6a3697f2013-01-30 00:44:43411 _Log('Generating update for src %s image %s', self.src_image, image_path)
Chris Sosae67b78f12010-11-05 00:33:16412
Chris Sosa417e55d2011-01-26 00:40:48413 # If it was pregenerated_path, don't regenerate
414 if self.pregenerated_path:
415 return self.pregenerated_path
Don Garrettfff4c322010-11-19 21:37:12416
Don Garrettf90edf02010-11-17 01:36:14417 # Which sub_dir of static_image_dir should hold our cached update image
418 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
Chris Sosa6a3697f2013-01-30 00:44:43419 _Log('Caching in sub_dir "%s"', cache_sub_dir)
Chris Sosa417e55d2011-01-26 00:40:48420
Don Garrettf90edf02010-11-17 01:36:14421 # The cached payloads exist in a cache dir
422 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa6a3697f2013-01-30 00:44:43423 cache_sub_dir, UPDATE_FILE)
Don Garrettf90edf02010-11-17 01:36:14424 cache_stateful_payload = os.path.join(static_image_dir,
Chris Sosa6a3697f2013-01-30 00:44:43425 cache_sub_dir, STATEFUL_FILE)
Don Garrettf90edf02010-11-17 01:36:14426
Chris Sosa6a3697f2013-01-30 00:44:43427 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-26 00:40:48428 # Check to see if this cache directory is valid.
429 if not os.path.exists(cache_update_payload) or not os.path.exists(
430 cache_stateful_payload):
Chris Sosa6a3697f2013-01-30 00:44:43431 self.GenerateUpdateImage(image_path, full_cache_dir)
Don Garrettf90edf02010-11-17 01:36:14432
Chris Sosa6a3697f2013-01-30 00:44:43433 self.pregenerated_path = cache_sub_dir
Chris Sosa65d339b2013-01-22 02:59:21434
Chris Sosa6a3697f2013-01-30 00:44:43435 # Generate the cache file.
436 self.GetLocalPayloadAttrs(full_cache_dir)
437 cache_metadata_file = os.path.join(full_cache_dir, METADATA_FILE)
Don Garrettf90edf02010-11-17 01:36:14438
Chris Sosa08d55a22011-01-20 00:08:02439 # Generation complete, copy if requested.
440 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-26 00:40:48441 # The final results exist directly in static
Gilad Arnolde74b3812013-04-22 18:27:38442 cros_update_payload = os.path.join(static_image_dir, UPDATE_FILE)
443 stateful_payload = os.path.join(static_image_dir, STATEFUL_FILE)
Chris Sosa6a3697f2013-01-30 00:44:43444 metadata_file = os.path.join(static_image_dir, METADATA_FILE)
Gilad Arnolde74b3812013-04-22 18:27:38445 common_util.CopyFile(cache_update_payload, cros_update_payload)
Gilad Arnold55a2a372012-10-02 16:46:32446 common_util.CopyFile(cache_stateful_payload, stateful_payload)
Chris Sosa6a3697f2013-01-30 00:44:43447 common_util.CopyFile(cache_metadata_file, metadata_file)
448 return None
Chris Sosa417e55d2011-01-26 00:40:48449 else:
450 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 22:46:22451
Chris Sosa6a3697f2013-01-30 00:44:43452 def GenerateLatestUpdateImage(self, board, client_version,
Don Garrettf90edf02010-11-17 01:36:14453 static_image_dir):
Chris Sosa0356d3b2010-09-16 22:46:22454 """Generates an update using the latest image that has been built.
455
456 This will only generate an update if the newest update is newer than that
457 on the client or client_version is 'ForcedUpdate'.
458
459 Args:
Chris Sosa6a3697f2013-01-30 00:44:43460 board: Name of the board.
Chris Sosa0356d3b2010-09-16 22:46:22461 client_version: Current version of the client or 'ForcedUpdate'
462 static_image_dir: the directory to move images to after generating.
463 Returns:
Chris Sosa6a3697f2013-01-30 00:44:43464 Name of the update directory relative to the static dir. None if it should
465 serve from the static_image_dir.
466 Raises:
467 AutoupdateError if it failed to generate the payload or can't update
468 the given client_version.
Chris Sosa0356d3b2010-09-16 22:46:22469 """
Chris Sosa6a3697f2013-01-30 00:44:43470 latest_image_dir = self._GetLatestImageDir(board)
Chris Sosa0356d3b2010-09-16 22:46:22471 latest_version = self._GetVersionFromDir(latest_image_dir)
472 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
473
Chris Sosa0356d3b2010-09-16 22:46:22474 # Check to see whether or not we should update.
475 if client_version != 'ForcedUpdate' and not self._CanUpdate(
476 client_version, latest_version):
Chris Sosa6a3697f2013-01-30 00:44:43477 raise AutoupdateError('Update check received but no update available '
478 'for client')
Chris Sosa0356d3b2010-09-16 22:46:22479
Don Garrettf90edf02010-11-17 01:36:14480 return self.GenerateUpdateImageWithCache(latest_image_path,
481 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 22:46:22482
Chris Sosa6a3697f2013-01-30 00:44:43483 def GenerateUpdatePayload(self, board, client_version, static_image_dir):
484 """Generates an update for an image and returns the relative payload dir.
Chris Sosaa73ec162010-05-04 03:18:02485
Chris Sosa6a3697f2013-01-30 00:44:43486 Returns:
487 payload dir relative to static_image_dir. None if it should
488 serve from the static_image_dir.
489 Raises:
490 AutoupdateError if it failed to generate the payload.
Don Garrettf90edf02010-11-17 01:36:14491 """
Dale Curtis723ec472010-11-30 22:06:47492 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
493 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
494
Gilad Arnold0c9c8602012-10-03 06:58:58495 if self.payload_path:
Don Garrett0c880e22010-11-18 02:13:37496 # If the forced payload is not already in our static_image_dir,
497 # copy it there.
Gilad Arnold0c9c8602012-10-03 06:58:58498 src_path = os.path.abspath(self.payload_path)
Chris Sosa6a3697f2013-01-30 00:44:43499 src_stateful = os.path.join(os.path.dirname(src_path), STATEFUL_FILE)
Don Garrettee25e552010-11-23 20:09:35500 # Only copy the files if the source directory is different from dest.
501 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
Gilad Arnold55a2a372012-10-02 16:46:32502 common_util.CopyFile(src_path, dest_path)
Don Garrettee25e552010-11-23 20:09:35503
504 # The stateful payload is optional.
505 if os.path.exists(src_stateful):
Gilad Arnold55a2a372012-10-02 16:46:32506 common_util.CopyFile(src_stateful, dest_stateful)
Don Garrettee25e552010-11-23 20:09:35507 else:
Chris Sosa6a3697f2013-01-30 00:44:43508 _Log('WARN: %s not found. Expected for dev and test builds',
Gilad Arnoldc65330c2012-09-20 22:17:48509 STATEFUL_FILE)
Don Garrettee25e552010-11-23 20:09:35510 if os.path.exists(dest_stateful):
511 os.remove(dest_stateful)
Don Garrett0c880e22010-11-18 02:13:37512
Chris Sosa6a3697f2013-01-30 00:44:43513 # Serve from the main directory so rel_path is None.
514 return None
Don Garrett0c880e22010-11-18 02:13:37515 elif self.forced_image:
Don Garrettf90edf02010-11-17 01:36:14516 return self.GenerateUpdateImageWithCache(
517 self.forced_image,
518 static_image_dir=static_image_dir)
Chris Sosa65d339b2013-01-22 02:59:21519 else:
Chris Sosa6a3697f2013-01-30 00:44:43520 if not board:
521 raise AutoupdateError(
522 'Failed to generate update. '
523 'You must set --board when pre-generating latest update.')
Chris Sosa65d339b2013-01-22 02:59:21524
Chris Sosa6a3697f2013-01-30 00:44:43525 return self.GenerateLatestUpdateImage(board, client_version,
526 static_image_dir)
Chris Sosa2c048f12010-10-27 23:05:27527
528 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-26 00:40:48529 """Pre-generates an update and prints out the relative path it.
530
Chris Sosa6a3697f2013-01-30 00:44:43531 Returns relative path of the update.
Chris Sosa65d339b2013-01-22 02:59:21532
Chris Sosa6a3697f2013-01-30 00:44:43533 Raises:
534 AutoupdateError if it failed to generate the payload.
535 """
536 _Log('Pre-generating the update payload')
537 # Does not work with labels so just use static dir.
538 pregenerated_update = self.GenerateUpdatePayload(self.board, '0.0.0.0',
539 self.static_dir)
540 print 'PREGENERATED_UPDATE=%s' % _NonePathJoin(pregenerated_update,
541 UPDATE_FILE)
Chris Sosa417e55d2011-01-26 00:40:48542 return pregenerated_update
Chris Sosa2c048f12010-10-27 23:05:27543
Gilad Arnold0c9c8602012-10-03 06:58:58544 def _GetRemotePayloadAttrs(self, url):
545 """Returns hashes, size and delta flag of a remote update payload.
546
547 Obtain attributes of a payload file available on a remote devserver. This
548 is based on the assumption that the payload URL uses the /static prefix. We
549 need to make sure that both clients (requests) and remote devserver
550 (provisioning) preserve this invariant.
551
552 Args:
553 url: URL of statically staged remote file (http://host:port/static/...)
554 Returns:
555 A tuple containing the SHA1, SHA256, file size and whether or not it's a
556 delta payload (Boolean).
557 """
558 if self._PAYLOAD_URL_PREFIX not in url:
559 raise AutoupdateError(
560 'Payload URL does not have the expected prefix (%s)' %
561 self._PAYLOAD_URL_PREFIX)
Chris Sosa6a3697f2013-01-30 00:44:43562
Gilad Arnold0c9c8602012-10-03 06:58:58563 fileinfo_url = url.replace(self._PAYLOAD_URL_PREFIX,
564 self._FILEINFO_URL_PREFIX)
Chris Sosa6a3697f2013-01-30 00:44:43565 _Log('Retrieving file info for remote payload via %s', fileinfo_url)
Gilad Arnold0c9c8602012-10-03 06:58:58566 try:
567 conn = urllib2.urlopen(fileinfo_url)
Chris Sosa6a3697f2013-01-30 00:44:43568 metadata_obj = Autoupdate._ReadMetadataFromStream(conn)
569 # These fields are required for remote calls.
570 if not metadata_obj:
571 raise AutoupdateError('Failed to obtain remote payload info')
Gilad Arnold0c9c8602012-10-03 06:58:58572
Chris Sosa6a3697f2013-01-30 00:44:43573 return metadata_obj
574 except IOError as e:
575 raise AutoupdateError('Failed to obtain remote payload info: %s', e)
576
577 def GetLocalPayloadAttrs(self, payload_dir):
Gilad Arnold0c9c8602012-10-03 06:58:58578 """Returns hashes, size and delta flag of a local update payload.
579
580 Args:
Chris Sosa6a3697f2013-01-30 00:44:43581 payload_dir: Path to the directory the payload is in.
Gilad Arnold0c9c8602012-10-03 06:58:58582 Returns:
583 A tuple containing the SHA1, SHA256, file size and whether or not it's a
584 delta payload (Boolean).
585 """
Chris Sosa6a3697f2013-01-30 00:44:43586 filename = os.path.join(payload_dir, UPDATE_FILE)
587 if not os.path.exists(filename):
588 raise AutoupdateError('update.gz not present in payload dir %s' %
589 payload_dir)
Gilad Arnold0c9c8602012-10-03 06:58:58590
Chris Sosa6a3697f2013-01-30 00:44:43591 metadata_obj = Autoupdate._ReadMetadataFromFile(payload_dir)
592 if not metadata_obj or not (metadata_obj.sha1 and
593 metadata_obj.sha256 and
594 metadata_obj.size):
595 sha1 = common_util.GetFileSha1(filename)
596 sha256 = common_util.GetFileSha256(filename)
597 size = common_util.GetFileSize(filename)
Gilad Arnolde74b3812013-04-22 18:27:38598 is_delta_format = self.IsDeltaFormatFile(filename)
Chris Sosa6a3697f2013-01-30 00:44:43599 metadata_obj = UpdateMetadata(sha1, sha256, size, is_delta_format)
600 Autoupdate._StoreMetadataToFile(payload_dir, metadata_obj)
Chris Sosa0356d3b2010-09-16 22:46:22601
Chris Sosa6a3697f2013-01-30 00:44:43602 return metadata_obj
603
604 def _ProcessUpdateComponents(self, app, event):
605 """Processes the app and event components of an update request.
606
607 Returns tuple containing forced_update_label, client_version, and board.
Chris Sosa0356d3b2010-09-16 22:46:22608 """
Chris Sosa6a3697f2013-01-30 00:44:43609 # Initialize an empty dictionary for event attributes to log.
610 log_message = {}
Jay Srinivasanac69d262012-10-31 02:05:53611
Dale Curtisc9aaf3a2011-08-09 22:47:40612 # Determine request IP, strip any IPv6 data for simplicity.
613 client_ip = cherrypy.request.remote.ip.split(':')[-1]
Gilad Arnold286a0062012-01-12 21:47:02614 # Obtain (or init) info object for this client.
615 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
616
Chris Sosa6a3697f2013-01-30 00:44:43617 client_version = 'ForcedUpdate'
618 board = None
619 if app:
620 client_version = app.getAttribute('version')
621 channel = app.getAttribute('track')
622 board = (app.hasAttribute('board') and app.getAttribute('board')
623 or self._GetDefaultBoardID())
624 # Add attributes to log message
625 log_message['version'] = client_version
626 log_message['track'] = channel
627 log_message['board'] = board
628 curr_host_info.attrs['last_known_version'] = client_version
Dale Curtisc9aaf3a2011-08-09 22:47:40629
Dale Curtisc9aaf3a2011-08-09 22:47:40630 if event:
Gilad Arnold286a0062012-01-12 21:47:02631 event_result = int(event[0].getAttribute('eventresult'))
632 event_type = int(event[0].getAttribute('eventtype'))
Gilad Arnoldb11a8942012-03-13 22:33:21633 client_previous_version = (event[0].getAttribute('previousversion')
634 if event[0].hasAttribute('previousversion')
635 else None)
Gilad Arnold286a0062012-01-12 21:47:02636 # Store attributes to legacy host info structure
637 curr_host_info.attrs['last_event_status'] = event_result
638 curr_host_info.attrs['last_event_type'] = event_type
639 # Add attributes to log message
640 log_message['event_result'] = event_result
641 log_message['event_type'] = event_type
Gilad Arnoldb11a8942012-03-13 22:33:21642 if client_previous_version is not None:
643 log_message['previous_version'] = client_previous_version
Gilad Arnold286a0062012-01-12 21:47:02644
Gilad Arnold8318eac2012-10-04 19:52:23645 # Log host event, if so instructed.
646 if self.host_log:
647 curr_host_info.AddLogEntry(log_message)
Dale Curtisc9aaf3a2011-08-09 22:47:40648
Chris Sosa6a3697f2013-01-30 00:44:43649 return (curr_host_info.attrs.pop('forced_update_label', None),
650 client_version, board)
651
652 def _GetStaticUrl(self):
653 """Returns the static url base that should prefix all payload responses."""
654 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
655 if x_forwarded_host:
656 hostname = 'http://' + x_forwarded_host
657 else:
658 hostname = cherrypy.request.base
659
660 if self.urlbase:
661 static_urlbase = self.urlbase
662 elif self.serve_only:
663 static_urlbase = '%s/static/archive' % hostname
664 else:
665 static_urlbase = '%s/static' % hostname
666
667 # If we have a proxy port, adjust the URL we instruct the client to
668 # use to go through the proxy.
669 if self.proxy_port:
670 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
671
672 _Log('Using static url base %s', static_urlbase)
673 _Log('Handling update ping as %s', hostname)
674 return static_urlbase
675
676 def HandleUpdatePing(self, data, label=None):
677 """Handles an update ping from an update client.
678
679 Args:
680 data: XML blob from client.
681 label: optional label for the update.
682 Returns:
683 Update payload message for client.
684 """
685 # Get the static url base that will form that base of our update url e.g.
686 # http://hostname:8080/static/update.gz.
687 static_urlbase = self._GetStaticUrl()
688
689 # Parse the XML we got into the components we care about.
690 protocol, app, event, update_check = autoupdate_lib.ParseUpdateRequest(data)
691
692 # #########################################################################
693 # Process attributes of the update check.
694 forced_update_label, client_version, board = self._ProcessUpdateComponents(
695 app, event)
696
697 # We only process update_checks in the update rpc.
Chris Sosa0356d3b2010-09-16 22:46:22698 if not update_check:
Chris Sosa6a3697f2013-01-30 00:44:43699 _Log('Non-update check received. Returning blank payload')
Chris Sosa0356d3b2010-09-16 22:46:22700 # TODO(sosa): Generate correct non-updatecheck payload to better test
701 # update clients.
Chris Sosa52148582012-11-15 23:35:58702 return autoupdate_lib.GetNoUpdateResponse(protocol)
Chris Sosa0356d3b2010-09-16 22:46:22703
Chris Sosa6a3697f2013-01-30 00:44:43704 # In case max_updates is used, return no response if max reached.
Gilad Arnolda564b4b2012-10-04 17:32:44705 if self.max_updates > 0:
706 self.max_updates -= 1
707 elif self.max_updates == 0:
Chris Sosa6a3697f2013-01-30 00:44:43708 _Log('Request received but max number of updates handled')
Chris Sosa52148582012-11-15 23:35:58709 return autoupdate_lib.GetNoUpdateResponse(protocol)
Gilad Arnolda564b4b2012-10-04 17:32:44710
Chris Sosa6a3697f2013-01-30 00:44:43711 _Log('Update Check Received. Client is using protocol version: %s',
712 protocol)
Dale Curtisc9aaf3a2011-08-09 22:47:40713
Chris Sosa6a3697f2013-01-30 00:44:43714 if forced_update_label:
715 if label:
716 _Log('Label: %s set but being overwritten to %s by request', label,
717 forced_update_label)
718
719 label = forced_update_label
720
721 # #########################################################################
722 # Finally its time to generate the omaha response to give to client that
723 # lets them know where to find the payload and its associated metadata.
724 metadata_obj = None
725
726 try:
Gilad Arnold0c9c8602012-10-03 06:58:58727 # Are we provisioning a remote or local payload?
728 if self.remote_payload:
729 # If no explicit label was provided, use the value of --payload.
Chris Sosa6a3697f2013-01-30 00:44:43730 if not label:
Gilad Arnold0c9c8602012-10-03 06:58:58731 label = self.payload_path
Chris Sosa0356d3b2010-09-16 22:46:22732
Gilad Arnold0c9c8602012-10-03 06:58:58733 # Form the URL of the update payload. This assumes that the payload
734 # file name is a devserver constant (which currently is the case).
735 url = '/'.join(filter(None, [static_urlbase, label, UPDATE_FILE]))
Chris Sosa5d342a22010-09-28 23:54:41736
Gilad Arnold0c9c8602012-10-03 06:58:58737 # Get remote payload attributes.
Chris Sosa6a3697f2013-01-30 00:44:43738 metadata_obj = self._GetRemotePayloadAttrs(url)
Gilad Arnold0c9c8602012-10-03 06:58:58739 else:
Chris Sosa6a3697f2013-01-30 00:44:43740 static_image_dir = _NonePathJoin(self.static_dir, label)
741 rel_path = None
Gilad Arnold0c9c8602012-10-03 06:58:58742
Chris Sosa6a3697f2013-01-30 00:44:43743 # Serving files only, don't generate an update.
744 if not self.serve_only:
745 # Generate payload if necessary.
746 rel_path = self.GenerateUpdatePayload(board, client_version,
747 static_image_dir)
748
749 url = '/'.join(filter(None, [static_urlbase, label, rel_path,
750 UPDATE_FILE]))
751 local_payload_dir = _NonePathJoin(static_image_dir, rel_path)
752 metadata_obj = self.GetLocalPayloadAttrs(local_payload_dir)
753
754 except AutoupdateError as e:
755 # Raised if we fail to generate an update payload.
756 _Log('Failed to process an update: %r', e)
757 return autoupdate_lib.GetNoUpdateResponse(protocol)
758
759 _Log('Responding to client to use url %s to get image', url)
760 return autoupdate_lib.GetUpdateResponse(
761 metadata_obj.sha1, metadata_obj.sha256, metadata_obj.size, url,
762 metadata_obj.is_delta_format, protocol, self.critical_update)
Dale Curtisc9aaf3a2011-08-09 22:47:40763
764 def HandleHostInfoPing(self, ip):
765 """Returns host info dictionary for the given IP in JSON format."""
766 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 21:47:02767 if ip in self.host_infos.table:
768 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
769
770 def HandleHostLogPing(self, ip):
771 """Returns a complete log of events for host in JSON format."""
Gilad Arnold4ba437d2012-10-05 22:28:27772 # If all events requested, return a dictionary of logs keyed by IP address.
Gilad Arnold286a0062012-01-12 21:47:02773 if ip == 'all':
774 return json.dumps(
775 dict([(key, self.host_infos.table[key].log)
776 for key in self.host_infos.table]))
Gilad Arnold4ba437d2012-10-05 22:28:27777
778 # Otherwise we're looking for a specific IP address, so find its log.
Gilad Arnold286a0062012-01-12 21:47:02779 if ip in self.host_infos.table:
780 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 22:47:40781
Gilad Arnold4ba437d2012-10-05 22:28:27782 # If no events were logged for this IP, return an empty log.
783 return json.dumps([])
784
Dale Curtisc9aaf3a2011-08-09 22:47:40785 def HandleSetUpdatePing(self, ip, label):
786 """Sets forced_update_label for a given host."""
787 assert ip, 'No ip provided.'
788 assert label, 'No label provided.'
Gilad Arnold286a0062012-01-12 21:47:02789 self.host_infos.GetInitHostInfo(ip).attrs['forced_update_label'] = label