blob: 31003e832e4b3a121b525834545451bf9f3583ce [file] [log] [blame]
Darin Petkovc3fd90c2011-05-11 14:23:00 -07001# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00002# 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 15:47:40 -07005import json
rtc@google.comded22402009-10-26 22:36:21 +00006import os
Chris Sosa05491b12010-11-08 17:14:16 -08007import subprocess
Gilad Arnold29021592013-02-15 16:19:17 -08008import sys
Darin Petkov2b2ff4b2010-07-27 15:02:09 -07009import time
Gilad Arnold0c9c8602012-10-02 23:58:58 -070010import urllib2
Don Garrett0ad09372010-12-06 16:20:30 -080011import urlparse
Chris Sosa7c931362010-10-11 19:49:01 -070012
Gilad Arnoldabb352e2012-09-23 01:24:27 -070013import cherrypy
14
Gilad Arnold29021592013-02-15 16:19:17 -080015# 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 01:24:27 -070020from build_util import BuildObject
Chris Sosa52148582012-11-15 15:35:58 -080021import autoupdate_lib
Gilad Arnold55a2a372012-10-02 09:46:32 -070022import common_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070023import log_util
Gilad Arnold29021592013-02-15 16:19:17 -080024# pylint: disable=F0401
25import update_payload
Chris Sosa05491b12010-11-08 17:14:16 -080026
Gilad Arnoldc65330c2012-09-20 15:17:48 -070027
28# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080029def _Log(message, *args):
30 return log_util.LogWithTag('UPDATE', message, *args)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070031
rtc@google.comded22402009-10-26 22:36:21 +000032
Chris Sosa417e55d2011-01-25 16:40:48 -080033UPDATE_FILE = 'update.gz'
Chris Sosa6a3697f2013-01-29 16:44:43 -080034METADATA_FILE = 'update.meta'
Chris Sosa417e55d2011-01-25 16:40:48 -080035STATEFUL_FILE = 'stateful.tgz'
36CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 15:46:22 -070037
Don Garrett0ad09372010-12-06 16:20:30 -080038
Gilad Arnold0c9c8602012-10-02 23:58:58 -070039class AutoupdateError(Exception):
40 """Exception classes used by this module."""
41 pass
42
43
Don Garrett0ad09372010-12-06 16:20:30 -080044def _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-29 16:44:43 -080059def _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-06 16:20:30 -080062
Chris Sosa6a3697f2013-01-29 16:44:43 -080063
64class HostInfo(object):
Gilad Arnold286a0062012-01-12 13:47:02 -080065 """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 13:47:02 -080091
Chris Sosa6a3697f2013-01-29 16:44:43 -080092class HostInfoTable(object):
Gilad Arnold286a0062012-01-12 13:47:02 -080093 """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-29 17:07:27 -0800112 return self.table.get(host_id)
Gilad Arnold286a0062012-01-12 13:47:02 -0800113
114
Chris Sosa6a3697f2013-01-29 16:44:43 -0800115class 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
rtc@google.com64244662009-11-12 00:52:08 +0000125class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700126 """Class that contains functionality that handles Chrome OS update pings.
127
128 Members:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700129 serve_only: serve only pre-built updates. static_dir must contain
130 update.gz and stateful.tgz.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700131 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.
138 vm: set for VM images (doesn't patch kernel)
139 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 12:52:23 -0700142 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 15:46:22 -0700146 """
rtc@google.comded22402009-10-26 22:36:21 +0000147
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700148 _PAYLOAD_URL_PREFIX = '/static/'
149 _FILEINFO_URL_PREFIX = '/api/fileinfo/'
150
Chris Sosa6a3697f2013-01-29 16:44:43 -0800151 SHA1_ATTR = 'sha1'
152 SHA256_ATTR = 'sha256'
153 SIZE_ATTR = 'size'
154 ISDELTA_ATTR = 'is_delta'
155
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700156 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700157 forced_image=None, payload_path=None,
158 proxy_port=None, src_image='', vm=False, board=None,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800159 copy_to_static_root=True, private_key=None,
Chris Sosa52148582012-11-15 15:35:58 -0800160 critical_update=False, remote_payload=False, max_updates= -1,
Chris Sosa6a3697f2013-01-29 16:44:43 -0800161 host_log=False, *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700162 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700163 self.serve_only = serve_only
Chris Sosa0356d3b2010-09-16 15:46:22 -0700164 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -0700165 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700166 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -0700167 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700168 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -0700169
Chris Sosa0356d3b2010-09-16 15:46:22 -0700170 self.forced_image = forced_image
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700171 self.payload_path = payload_path
Chris Sosa62f720b2010-10-26 21:39:48 -0700172 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -0800173 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -0700174 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -0700175 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -0800176 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-14 16:33:22 -0800177 self.private_key = private_key
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800178 self.critical_update = critical_update
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700179 self.remote_payload = remote_payload
Jay Srinivasanac69d262012-10-30 19:05:53 -0700180 self.max_updates = max_updates
Gilad Arnold8318eac2012-10-04 12:52:23 -0700181 self.host_log = host_log
Don Garrettfff4c322010-11-19 13:37:12 -0800182
Chris Sosa417e55d2011-01-25 16:40:48 -0800183 # Path to pre-generated file.
184 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700185
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700186 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 13:47:02 -0800187 # 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 15:47:40 -0700191
Chris Sosa6a3697f2013-01-29 16:44:43 -0800192 @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 15:46:22 -0700226 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-29 16:44:43 -0800234 def _GetLatestImageDir(self, board):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700235 """Returns the latest image dir based on shell script."""
Chris Sosa6a3697f2013-01-29 16:44:43 -0800236 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700237 return os.popen(cmd).read().strip()
238
Chris Sosa52148582012-11-15 15:35:58 -0800239 @staticmethod
240 def _GetVersionFromDir(image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700241 """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 08:52:52 -0700243 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 15:46:22 -0700251
Chris Sosa52148582012-11-15 15:35:58 -0800252 @staticmethod
253 def _CanUpdate(client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800254 """Returns true if the latest_version is greater than the client_version.
255 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800256 _Log('client version %s latest version %s', client_version, latest_version)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700257
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 15:46:22 -0700270 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 08:52:52 -0700273
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 15:46:22 -0700277
Chris Sosa0356d3b2010-09-16 15:46:22 -0700278 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-29 16:44:43 -0800284
Chris Sosa0356d3b2010-09-16 15:46:22 -0700285 return image_name
286
Chris Sosa52148582012-11-15 15:35:58 -0800287 @staticmethod
Gilad Arnold29021592013-02-15 16:19:17 -0800288 def IsDeltaFormatFile(filename):
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700289 try:
Gilad Arnold29021592013-02-15 16:19:17 -0800290 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-25 17:34:49 -0700297 return False
298
Don Garrettf90edf02010-11-16 17:36:14 -0800299 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700300 """Generates an update gz given a full path to an image.
301
302 Args:
303 image_path: Full path to image.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800304 Raises:
305 subprocess.CalledProcessError if the update generator fails to generate a
306 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700307 """
Don Garrettfff4c322010-11-19 13:37:12 -0800308 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800309 _Log('Generating update image %s', update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700310
Chris Sosa0f1ec842011-02-14 16:33:22 -0800311 update_command = [
Chris Sosa5b8b5eb2012-03-27 11:15:27 -0700312 'cros_generate_update_payload',
Chris Sosa6a3697f2013-01-29 16:44:43 -0800313 '--image', image_path,
314 '--output', update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800315 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700316
Chris Sosa52148582012-11-15 15:35:58 -0800317 if src_image:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800318 update_command.extend(['--src_image', src_image])
Chris Sosa52148582012-11-15 15:35:58 -0800319
320 if not self.vm:
321 update_command.append('--patch_kernel')
322
323 if self.private_key:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800324 update_command.extend(['--private_key', self.private_key])
Chris Sosa0f1ec842011-02-14 16:33:22 -0800325
Chris Sosa6a3697f2013-01-29 16:44:43 -0800326 _Log('Running %s', ' '.join(update_command))
327 subprocess.check_call(update_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700328
Chris Sosa52148582012-11-15 15:35:58 -0800329 @staticmethod
330 def GenerateStatefulFile(image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800331 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700332
333 Args:
334 image_path: Full path to image.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800335 Raises:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800336 subprocess.CalledProcessError if the update generator fails to generate a
Chris Sosa908fd6f2010-11-10 17:31:18 -0800337 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700338 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800339 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 15:46:22 -0700346
Don Garrettf90edf02010-11-16 17:36:14 -0800347 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
348 """Find directory to store a cached update.
349
Gilad Arnold55a2a372012-10-02 09:46:32 -0700350 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-16 17:36:14 -0800352
Gilad Arnold55a2a372012-10-02 09:46:32 -0700353 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-07 19:32:50 -0700361 """
Gilad Arnold55a2a372012-10-02 09:46:32 -0700362 update_dir = ''
Chris Sosa744e1472011-09-07 19:32:50 -0700363 if src_image:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700364 update_dir += common_util.GetFileMd5(src_image) + '_'
Don Garrettf90edf02010-11-16 17:36:14 -0800365
Gilad Arnold55a2a372012-10-02 09:46:32 -0700366 update_dir += common_util.GetFileMd5(dest_image)
Chris Sosa744e1472011-09-07 19:32:50 -0700367 if self.private_key:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700368 update_dir += '+' + common_util.GetFileMd5(self.private_key)
Chris Sosa744e1472011-09-07 19:32:50 -0700369
Chris Sosa9fba7562012-01-31 10:15:47 -0800370 if not self.vm:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700371 update_dir += '+patched_kernel'
Chris Sosa9fba7562012-01-31 10:15:47 -0800372
Gilad Arnold55a2a372012-10-02 09:46:32 -0700373 return os.path.join(CACHE_DIR, update_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800374
Don Garrettfff4c322010-11-19 13:37:12 -0800375 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800376 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700377
Chris Sosade91f672010-11-16 10:05:44 -0800378 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800379 src_image: image we are updating from (Null/empty for non-delta)
380 image_path: full path to the image.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800381 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 10:05:44 -0800385 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800386 _Log('Generating update for image %s', image_path)
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700387
Chris Sosa6a3697f2013-01-29 16:44:43 -0800388 # Delete any previous state in this directory.
389 os.system('rm -rf "%s"' % output_dir)
390 os.makedirs(output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000391
Chris Sosa6a3697f2013-01-29 16:44:43 -0800392 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-16 17:36:14 -0800398
399 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
400 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000401
Chris Sosa0356d3b2010-09-16 15:46:22 -0700402 Args:
403 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700404 static_image_dir: the directory to move images to after generating.
405 Returns:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800406 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 15:46:22 -0700410 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800411 _Log('Generating update for src %s image %s', self.src_image, image_path)
Chris Sosae67b78f2010-11-04 17:33:16 -0700412
Chris Sosa417e55d2011-01-25 16:40:48 -0800413 # If it was pregenerated_path, don't regenerate
414 if self.pregenerated_path:
415 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800416
Don Garrettf90edf02010-11-16 17:36:14 -0800417 # 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-29 16:44:43 -0800419 _Log('Caching in sub_dir "%s"', cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800420
Don Garrettf90edf02010-11-16 17:36:14 -0800421 # The cached payloads exist in a cache dir
422 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa6a3697f2013-01-29 16:44:43 -0800423 cache_sub_dir, UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800424 cache_stateful_payload = os.path.join(static_image_dir,
Chris Sosa6a3697f2013-01-29 16:44:43 -0800425 cache_sub_dir, STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800426
Chris Sosa6a3697f2013-01-29 16:44:43 -0800427 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800428 # 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-29 16:44:43 -0800431 self.GenerateUpdateImage(image_path, full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800432
Chris Sosa6a3697f2013-01-29 16:44:43 -0800433 self.pregenerated_path = cache_sub_dir
Chris Sosa65d339b2013-01-21 18:59:21 -0800434
Chris Sosa6a3697f2013-01-29 16:44:43 -0800435 # 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-16 17:36:14 -0800438
Chris Sosa08d55a22011-01-19 16:08:02 -0800439 # Generation complete, copy if requested.
440 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800441 # The final results exist directly in static
Gilad Arnold29021592013-02-15 16:19:17 -0800442 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-29 16:44:43 -0800444 metadata_file = os.path.join(static_image_dir, METADATA_FILE)
Gilad Arnold29021592013-02-15 16:19:17 -0800445 common_util.CopyFile(cache_update_payload, cros_update_payload)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700446 common_util.CopyFile(cache_stateful_payload, stateful_payload)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800447 common_util.CopyFile(cache_metadata_file, metadata_file)
448 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800449 else:
450 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700451
Chris Sosa6a3697f2013-01-29 16:44:43 -0800452 def GenerateLatestUpdateImage(self, board, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800453 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700454 """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-29 16:44:43 -0800460 board: Name of the board.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700461 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-29 16:44:43 -0800464 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 15:46:22 -0700469 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800470 latest_image_dir = self._GetLatestImageDir(board)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700471 latest_version = self._GetVersionFromDir(latest_image_dir)
472 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
473
Chris Sosa0356d3b2010-09-16 15:46:22 -0700474 # 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-29 16:44:43 -0800477 raise AutoupdateError('Update check received but no update available '
478 'for client')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700479
Don Garrettf90edf02010-11-16 17:36:14 -0800480 return self.GenerateUpdateImageWithCache(latest_image_path,
481 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700482
Chris Sosa6a3697f2013-01-29 16:44:43 -0800483 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-03 20:18:02 -0700485
Chris Sosa6a3697f2013-01-29 16:44:43 -0800486 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-16 17:36:14 -0800491 """
Dale Curtis723ec472010-11-30 14:06:47 -0800492 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-02 23:58:58 -0700495 if self.payload_path:
Don Garrett0c880e22010-11-17 18:13:37 -0800496 # If the forced payload is not already in our static_image_dir,
497 # copy it there.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700498 src_path = os.path.abspath(self.payload_path)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800499 src_stateful = os.path.join(os.path.dirname(src_path), STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800500 # 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 09:46:32 -0700502 common_util.CopyFile(src_path, dest_path)
Don Garrettee25e552010-11-23 12:09:35 -0800503
504 # The stateful payload is optional.
505 if os.path.exists(src_stateful):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700506 common_util.CopyFile(src_stateful, dest_stateful)
Don Garrettee25e552010-11-23 12:09:35 -0800507 else:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800508 _Log('WARN: %s not found. Expected for dev and test builds',
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700509 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800510 if os.path.exists(dest_stateful):
511 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800512
Chris Sosa6a3697f2013-01-29 16:44:43 -0800513 # Serve from the main directory so rel_path is None.
514 return None
Don Garrett0c880e22010-11-17 18:13:37 -0800515 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800516 return self.GenerateUpdateImageWithCache(
517 self.forced_image,
518 static_image_dir=static_image_dir)
Chris Sosa65d339b2013-01-21 18:59:21 -0800519 else:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800520 if not board:
521 raise AutoupdateError(
522 'Failed to generate update. '
523 'You must set --board when pre-generating latest update.')
Chris Sosa65d339b2013-01-21 18:59:21 -0800524
Chris Sosa6a3697f2013-01-29 16:44:43 -0800525 return self.GenerateLatestUpdateImage(board, client_version,
526 static_image_dir)
Chris Sosa2c048f12010-10-27 16:05:27 -0700527
528 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800529 """Pre-generates an update and prints out the relative path it.
530
Chris Sosa6a3697f2013-01-29 16:44:43 -0800531 Returns relative path of the update.
Chris Sosa65d339b2013-01-21 18:59:21 -0800532
Chris Sosa6a3697f2013-01-29 16:44:43 -0800533 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-25 16:40:48 -0800542 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700543
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700544 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-29 16:44:43 -0800562
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700563 fileinfo_url = url.replace(self._PAYLOAD_URL_PREFIX,
564 self._FILEINFO_URL_PREFIX)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800565 _Log('Retrieving file info for remote payload via %s', fileinfo_url)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700566 try:
567 conn = urllib2.urlopen(fileinfo_url)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800568 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-02 23:58:58 -0700572
Chris Sosa6a3697f2013-01-29 16:44:43 -0800573 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-02 23:58:58 -0700578 """Returns hashes, size and delta flag of a local update payload.
579
580 Args:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800581 payload_dir: Path to the directory the payload is in.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700582 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-29 16:44:43 -0800586 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-02 23:58:58 -0700590
Chris Sosa6a3697f2013-01-29 16:44:43 -0800591 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 Arnold29021592013-02-15 16:19:17 -0800598 is_delta_format = self.IsDeltaFormatFile(filename)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800599 metadata_obj = UpdateMetadata(sha1, sha256, size, is_delta_format)
600 Autoupdate._StoreMetadataToFile(payload_dir, metadata_obj)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700601
Chris Sosa6a3697f2013-01-29 16:44:43 -0800602 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 15:46:22 -0700608 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800609 # Initialize an empty dictionary for event attributes to log.
610 log_message = {}
Jay Srinivasanac69d262012-10-30 19:05:53 -0700611
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700612 # Determine request IP, strip any IPv6 data for simplicity.
613 client_ip = cherrypy.request.remote.ip.split(':')[-1]
Gilad Arnold286a0062012-01-12 13:47:02 -0800614 # Obtain (or init) info object for this client.
615 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
616
Chris Sosa6a3697f2013-01-29 16:44:43 -0800617 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 15:47:40 -0700629
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700630 if event:
Gilad Arnold286a0062012-01-12 13:47:02 -0800631 event_result = int(event[0].getAttribute('eventresult'))
632 event_type = int(event[0].getAttribute('eventtype'))
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700633 client_previous_version = (event[0].getAttribute('previousversion')
634 if event[0].hasAttribute('previousversion')
635 else None)
Gilad Arnold286a0062012-01-12 13:47:02 -0800636 # 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 15:33:21 -0700642 if client_previous_version is not None:
643 log_message['previous_version'] = client_previous_version
Gilad Arnold286a0062012-01-12 13:47:02 -0800644
Gilad Arnold8318eac2012-10-04 12:52:23 -0700645 # Log host event, if so instructed.
646 if self.host_log:
647 curr_host_info.AddLogEntry(log_message)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700648
Chris Sosa6a3697f2013-01-29 16:44:43 -0800649 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 15:46:22 -0700698 if not update_check:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800699 _Log('Non-update check received. Returning blank payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700700 # TODO(sosa): Generate correct non-updatecheck payload to better test
701 # update clients.
Chris Sosa52148582012-11-15 15:35:58 -0800702 return autoupdate_lib.GetNoUpdateResponse(protocol)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700703
Chris Sosa6a3697f2013-01-29 16:44:43 -0800704 # In case max_updates is used, return no response if max reached.
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700705 if self.max_updates > 0:
706 self.max_updates -= 1
707 elif self.max_updates == 0:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800708 _Log('Request received but max number of updates handled')
Chris Sosa52148582012-11-15 15:35:58 -0800709 return autoupdate_lib.GetNoUpdateResponse(protocol)
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700710
Chris Sosa6a3697f2013-01-29 16:44:43 -0800711 _Log('Update Check Received. Client is using protocol version: %s',
712 protocol)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700713
Chris Sosa6a3697f2013-01-29 16:44:43 -0800714 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-02 23:58:58 -0700727 # 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-29 16:44:43 -0800730 if not label:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700731 label = self.payload_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700732
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700733 # 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 16:54:41 -0700736
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700737 # Get remote payload attributes.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800738 metadata_obj = self._GetRemotePayloadAttrs(url)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700739 else:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800740 static_image_dir = _NonePathJoin(self.static_dir, label)
741 rel_path = None
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700742
Chris Sosa6a3697f2013-01-29 16:44:43 -0800743 # 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 15:47:40 -0700763
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 13:47:02 -0800767 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 15:28:27 -0700772 # If all events requested, return a dictionary of logs keyed by IP address.
Gilad Arnold286a0062012-01-12 13:47:02 -0800773 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 15:28:27 -0700777
778 # Otherwise we're looking for a specific IP address, so find its log.
Gilad Arnold286a0062012-01-12 13:47:02 -0800779 if ip in self.host_infos.table:
780 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700781
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700782 # If no events were logged for this IP, return an empty log.
783 return json.dumps([])
784
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700785 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 13:47:02 -0800789 self.host_infos.GetInitHostInfo(ip).attrs['forced_update_label'] = label