blob: 4a88659fe6befc38397ba2c60c5c18e98941d78e [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 Arnolde74b3812013-04-22 11:27:38 -07008import 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 Arnolde74b3812013-04-22 11:27:38 -070015# 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
joychen921e1fb2013-06-28 11:12:20 -070020import build_util
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 Arnolde74b3812013-04-22 11:27:38 -070024# 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
joychen921e1fb2013-06-28 11:12:20 -0700125class Autoupdate(build_util.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.
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700138 patch_kernel: Patch the kernel when generating updates
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700139 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,
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700158 proxy_port=None, src_image='', patch_kernel=True, 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 Sosa3ae4dc12013-03-29 11:47:00 -0700174 self.patch_kernel = patch_kernel
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 Sosa52148582012-11-15 15:35:58 -0800234 @staticmethod
235 def _GetVersionFromDir(image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700236 """Returns the version of the image based on the name of the directory."""
237 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700238 parts = latest_version.split('-')
239 if len(parts) == 2:
240 # Old-style, e.g. "0.15.938.2011_08_23_0941-a1".
241 # TODO(derat): Remove the code for old-style versions after 20120101.
242 return parts[0]
243 else:
244 # New-style, e.g. "R16-1102.0.2011_09_30_0806-a1".
245 return parts[1]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700246
Chris Sosa52148582012-11-15 15:35:58 -0800247 @staticmethod
248 def _CanUpdate(client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800249 """Returns true if the latest_version is greater than the client_version.
250 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800251 _Log('client version %s latest version %s', client_version, latest_version)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700252
253 client_tokens = client_version.replace('_', '').split('.')
254 # If the client has an old four-token version like "0.16.892.0", drop the
255 # first two tokens -- we use versions like "892.0.0" now.
256 # TODO(derat): Remove the code for old-style versions after 20120101.
257 if len(client_tokens) == 4:
258 client_tokens = client_tokens[2:]
259
260 latest_tokens = latest_version.replace('_', '').split('.')
261 if len(latest_tokens) == 4:
262 latest_tokens = latest_tokens[2:]
263
264 for i in range(min(len(client_tokens), len(latest_tokens))):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700265 if int(latest_tokens[i]) == int(client_tokens[i]):
266 continue
267 return int(latest_tokens[i]) > int(client_tokens[i])
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700268
269 # Favor four-token new-style versions on the server over old-style versions
270 # on the client if everything else matches.
271 return len(latest_tokens) > len(client_tokens)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700272
Chris Sosa0356d3b2010-09-16 15:46:22 -0700273 def _GetImageName(self):
274 """Returns the name of the image that should be used."""
275 if self.use_test_image:
276 image_name = 'chromiumos_test_image.bin'
277 else:
278 image_name = 'chromiumos_image.bin'
Chris Sosa6a3697f2013-01-29 16:44:43 -0800279
Chris Sosa0356d3b2010-09-16 15:46:22 -0700280 return image_name
281
Chris Sosa52148582012-11-15 15:35:58 -0800282 @staticmethod
Gilad Arnolde74b3812013-04-22 11:27:38 -0700283 def IsDeltaFormatFile(filename):
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700284 try:
Gilad Arnolde74b3812013-04-22 11:27:38 -0700285 with open(filename) as payload_file:
286 payload = update_payload.Payload(payload_file)
287 payload.Init()
288 return payload.IsDelta()
289 except (IOError, update_payload.PayloadError):
290 # For unit tests we may not have real files, so it's ok to ignore these
291 # errors.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700292 return False
293
Don Garrettf90edf02010-11-16 17:36:14 -0800294 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700295 """Generates an update gz given a full path to an image.
296
297 Args:
298 image_path: Full path to image.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800299 Raises:
300 subprocess.CalledProcessError if the update generator fails to generate a
301 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700302 """
Don Garrettfff4c322010-11-19 13:37:12 -0800303 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800304 _Log('Generating update image %s', update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700305
Chris Sosa0f1ec842011-02-14 16:33:22 -0800306 update_command = [
Chris Sosa5b8b5eb2012-03-27 11:15:27 -0700307 'cros_generate_update_payload',
Chris Sosa6a3697f2013-01-29 16:44:43 -0800308 '--image', image_path,
309 '--output', update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800310 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700311
Chris Sosa52148582012-11-15 15:35:58 -0800312 if src_image:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800313 update_command.extend(['--src_image', src_image])
Chris Sosa52148582012-11-15 15:35:58 -0800314
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700315 if self.patch_kernel:
Chris Sosa52148582012-11-15 15:35:58 -0800316 update_command.append('--patch_kernel')
317
318 if self.private_key:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800319 update_command.extend(['--private_key', self.private_key])
Chris Sosa0f1ec842011-02-14 16:33:22 -0800320
Chris Sosa6a3697f2013-01-29 16:44:43 -0800321 _Log('Running %s', ' '.join(update_command))
322 subprocess.check_call(update_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700323
Chris Sosa52148582012-11-15 15:35:58 -0800324 @staticmethod
325 def GenerateStatefulFile(image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800326 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700327
328 Args:
329 image_path: Full path to image.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800330 Raises:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800331 subprocess.CalledProcessError if the update generator fails to generate a
Chris Sosa908fd6f2010-11-10 17:31:18 -0800332 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700333 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800334 update_command = [
335 'cros_generate_stateful_update_payload',
336 '--image', image_path,
337 '--output_dir', output_dir,
338 ]
339 _Log('Running %s', ' '.join(update_command))
340 subprocess.check_call(update_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700341
Don Garrettf90edf02010-11-16 17:36:14 -0800342 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
343 """Find directory to store a cached update.
344
Gilad Arnold55a2a372012-10-02 09:46:32 -0700345 Given one, or two images for an update, this finds which cache directory
346 should hold the update files, even if they don't exist yet.
Don Garrettf90edf02010-11-16 17:36:14 -0800347
Gilad Arnold55a2a372012-10-02 09:46:32 -0700348 Returns:
349 A directory path for storing a cached update, of the following form:
350 Non-delta updates:
351 CACHE_DIR/<dest_hash>
352 Delta updates:
353 CACHE_DIR/<src_hash>_<dest_hash>
354 Signed updates (self.private_key):
355 CACHE_DIR/<src_hash>_<dest_hash>+<private_key_hash>
Chris Sosa744e1472011-09-07 19:32:50 -0700356 """
Gilad Arnold55a2a372012-10-02 09:46:32 -0700357 update_dir = ''
Chris Sosa744e1472011-09-07 19:32:50 -0700358 if src_image:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700359 update_dir += common_util.GetFileMd5(src_image) + '_'
Don Garrettf90edf02010-11-16 17:36:14 -0800360
Gilad Arnold55a2a372012-10-02 09:46:32 -0700361 update_dir += common_util.GetFileMd5(dest_image)
Chris Sosa744e1472011-09-07 19:32:50 -0700362 if self.private_key:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700363 update_dir += '+' + common_util.GetFileMd5(self.private_key)
Chris Sosa744e1472011-09-07 19:32:50 -0700364
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700365 if self.patch_kernel:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700366 update_dir += '+patched_kernel'
Chris Sosa9fba7562012-01-31 10:15:47 -0800367
Gilad Arnold55a2a372012-10-02 09:46:32 -0700368 return os.path.join(CACHE_DIR, update_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800369
Don Garrettfff4c322010-11-19 13:37:12 -0800370 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800371 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700372
Chris Sosade91f672010-11-16 10:05:44 -0800373 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800374 src_image: image we are updating from (Null/empty for non-delta)
375 image_path: full path to the image.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800376 output_dir: the directory to write the update payloads to
377 Raises:
378 AutoupdateError if it failed to generate either update or stateful
379 payload.
Chris Sosade91f672010-11-16 10:05:44 -0800380 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800381 _Log('Generating update for image %s', image_path)
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700382
Chris Sosa6a3697f2013-01-29 16:44:43 -0800383 # Delete any previous state in this directory.
384 os.system('rm -rf "%s"' % output_dir)
385 os.makedirs(output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000386
Chris Sosa6a3697f2013-01-29 16:44:43 -0800387 try:
388 self.GenerateUpdateFile(self.src_image, image_path, output_dir)
389 self.GenerateStatefulFile(image_path, output_dir)
390 except subprocess.CalledProcessError:
391 os.system('rm -rf "%s"' % output_dir)
392 raise AutoupdateError('Failed to generate update in %s' % output_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800393
394 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
395 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000396
Chris Sosa0356d3b2010-09-16 15:46:22 -0700397 Args:
398 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700399 static_image_dir: the directory to move images to after generating.
400 Returns:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800401 update directory relative to static_image_dir. None if it should
402 serve from the static_image_dir.
403 Raises:
404 AutoupdateError if it we need to generate a payload and fail to do so.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700405 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800406 _Log('Generating update for src %s image %s', self.src_image, image_path)
Chris Sosae67b78f2010-11-04 17:33:16 -0700407
Chris Sosa417e55d2011-01-25 16:40:48 -0800408 # If it was pregenerated_path, don't regenerate
409 if self.pregenerated_path:
410 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800411
Don Garrettf90edf02010-11-16 17:36:14 -0800412 # Which sub_dir of static_image_dir should hold our cached update image
413 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800414 _Log('Caching in sub_dir "%s"', cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800415
Don Garrettf90edf02010-11-16 17:36:14 -0800416 # The cached payloads exist in a cache dir
417 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa6a3697f2013-01-29 16:44:43 -0800418 cache_sub_dir, UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800419 cache_stateful_payload = os.path.join(static_image_dir,
Chris Sosa6a3697f2013-01-29 16:44:43 -0800420 cache_sub_dir, STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800421
Chris Sosa6a3697f2013-01-29 16:44:43 -0800422 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800423 # Check to see if this cache directory is valid.
424 if not os.path.exists(cache_update_payload) or not os.path.exists(
425 cache_stateful_payload):
Chris Sosa6a3697f2013-01-29 16:44:43 -0800426 self.GenerateUpdateImage(image_path, full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800427
Chris Sosa6a3697f2013-01-29 16:44:43 -0800428 self.pregenerated_path = cache_sub_dir
Chris Sosa65d339b2013-01-21 18:59:21 -0800429
Chris Sosa6a3697f2013-01-29 16:44:43 -0800430 # Generate the cache file.
431 self.GetLocalPayloadAttrs(full_cache_dir)
432 cache_metadata_file = os.path.join(full_cache_dir, METADATA_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800433
Chris Sosa08d55a22011-01-19 16:08:02 -0800434 # Generation complete, copy if requested.
435 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800436 # The final results exist directly in static
Gilad Arnolde74b3812013-04-22 11:27:38 -0700437 cros_update_payload = os.path.join(static_image_dir, UPDATE_FILE)
438 stateful_payload = os.path.join(static_image_dir, STATEFUL_FILE)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800439 metadata_file = os.path.join(static_image_dir, METADATA_FILE)
Gilad Arnolde74b3812013-04-22 11:27:38 -0700440 common_util.CopyFile(cache_update_payload, cros_update_payload)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700441 common_util.CopyFile(cache_stateful_payload, stateful_payload)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800442 common_util.CopyFile(cache_metadata_file, metadata_file)
443 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800444 else:
445 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700446
Chris Sosa6a3697f2013-01-29 16:44:43 -0800447 def GenerateLatestUpdateImage(self, board, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800448 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700449 """Generates an update using the latest image that has been built.
450
451 This will only generate an update if the newest update is newer than that
452 on the client or client_version is 'ForcedUpdate'.
453
454 Args:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800455 board: Name of the board.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700456 client_version: Current version of the client or 'ForcedUpdate'
457 static_image_dir: the directory to move images to after generating.
458 Returns:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800459 Name of the update directory relative to the static dir. None if it should
460 serve from the static_image_dir.
461 Raises:
462 AutoupdateError if it failed to generate the payload or can't update
463 the given client_version.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700464 """
joychen921e1fb2013-06-28 11:12:20 -0700465 latest_image_dir = self.GetLatestImageDir(board)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700466 latest_version = self._GetVersionFromDir(latest_image_dir)
467 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
468
Chris Sosa0356d3b2010-09-16 15:46:22 -0700469 # Check to see whether or not we should update.
470 if client_version != 'ForcedUpdate' and not self._CanUpdate(
471 client_version, latest_version):
Chris Sosa6a3697f2013-01-29 16:44:43 -0800472 raise AutoupdateError('Update check received but no update available '
473 'for client')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700474
Don Garrettf90edf02010-11-16 17:36:14 -0800475 return self.GenerateUpdateImageWithCache(latest_image_path,
476 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700477
Chris Sosa6a3697f2013-01-29 16:44:43 -0800478 def GenerateUpdatePayload(self, board, client_version, static_image_dir):
479 """Generates an update for an image and returns the relative payload dir.
Chris Sosaa73ec162010-05-03 20:18:02 -0700480
Chris Sosa6a3697f2013-01-29 16:44:43 -0800481 Returns:
482 payload dir relative to static_image_dir. None if it should
483 serve from the static_image_dir.
484 Raises:
485 AutoupdateError if it failed to generate the payload.
Don Garrettf90edf02010-11-16 17:36:14 -0800486 """
Dale Curtis723ec472010-11-30 14:06:47 -0800487 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
488 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
489
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700490 if self.payload_path:
Don Garrett0c880e22010-11-17 18:13:37 -0800491 # If the forced payload is not already in our static_image_dir,
492 # copy it there.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700493 src_path = os.path.abspath(self.payload_path)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800494 src_stateful = os.path.join(os.path.dirname(src_path), STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800495 # Only copy the files if the source directory is different from dest.
496 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700497 common_util.CopyFile(src_path, dest_path)
Don Garrettee25e552010-11-23 12:09:35 -0800498
499 # The stateful payload is optional.
500 if os.path.exists(src_stateful):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700501 common_util.CopyFile(src_stateful, dest_stateful)
Don Garrettee25e552010-11-23 12:09:35 -0800502 else:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800503 _Log('WARN: %s not found. Expected for dev and test builds',
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700504 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800505 if os.path.exists(dest_stateful):
506 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800507
Chris Sosa6a3697f2013-01-29 16:44:43 -0800508 # Serve from the main directory so rel_path is None.
509 return None
Don Garrett0c880e22010-11-17 18:13:37 -0800510 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800511 return self.GenerateUpdateImageWithCache(
512 self.forced_image,
513 static_image_dir=static_image_dir)
Chris Sosa65d339b2013-01-21 18:59:21 -0800514 else:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800515 if not board:
516 raise AutoupdateError(
517 'Failed to generate update. '
518 'You must set --board when pre-generating latest update.')
Chris Sosa65d339b2013-01-21 18:59:21 -0800519
Chris Sosa6a3697f2013-01-29 16:44:43 -0800520 return self.GenerateLatestUpdateImage(board, client_version,
521 static_image_dir)
Chris Sosa2c048f12010-10-27 16:05:27 -0700522
523 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800524 """Pre-generates an update and prints out the relative path it.
525
Chris Sosa6a3697f2013-01-29 16:44:43 -0800526 Returns relative path of the update.
Chris Sosa65d339b2013-01-21 18:59:21 -0800527
Chris Sosa6a3697f2013-01-29 16:44:43 -0800528 Raises:
529 AutoupdateError if it failed to generate the payload.
530 """
531 _Log('Pre-generating the update payload')
532 # Does not work with labels so just use static dir.
533 pregenerated_update = self.GenerateUpdatePayload(self.board, '0.0.0.0',
534 self.static_dir)
535 print 'PREGENERATED_UPDATE=%s' % _NonePathJoin(pregenerated_update,
536 UPDATE_FILE)
Chris Sosa417e55d2011-01-25 16:40:48 -0800537 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700538
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700539 def _GetRemotePayloadAttrs(self, url):
540 """Returns hashes, size and delta flag of a remote update payload.
541
542 Obtain attributes of a payload file available on a remote devserver. This
543 is based on the assumption that the payload URL uses the /static prefix. We
544 need to make sure that both clients (requests) and remote devserver
545 (provisioning) preserve this invariant.
546
547 Args:
548 url: URL of statically staged remote file (http://host:port/static/...)
549 Returns:
550 A tuple containing the SHA1, SHA256, file size and whether or not it's a
551 delta payload (Boolean).
552 """
553 if self._PAYLOAD_URL_PREFIX not in url:
554 raise AutoupdateError(
555 'Payload URL does not have the expected prefix (%s)' %
556 self._PAYLOAD_URL_PREFIX)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800557
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700558 fileinfo_url = url.replace(self._PAYLOAD_URL_PREFIX,
559 self._FILEINFO_URL_PREFIX)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800560 _Log('Retrieving file info for remote payload via %s', fileinfo_url)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700561 try:
562 conn = urllib2.urlopen(fileinfo_url)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800563 metadata_obj = Autoupdate._ReadMetadataFromStream(conn)
564 # These fields are required for remote calls.
565 if not metadata_obj:
566 raise AutoupdateError('Failed to obtain remote payload info')
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700567
Chris Sosa6a3697f2013-01-29 16:44:43 -0800568 return metadata_obj
569 except IOError as e:
570 raise AutoupdateError('Failed to obtain remote payload info: %s', e)
571
572 def GetLocalPayloadAttrs(self, payload_dir):
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700573 """Returns hashes, size and delta flag of a local update payload.
574
575 Args:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800576 payload_dir: Path to the directory the payload is in.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700577 Returns:
578 A tuple containing the SHA1, SHA256, file size and whether or not it's a
579 delta payload (Boolean).
580 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800581 filename = os.path.join(payload_dir, UPDATE_FILE)
582 if not os.path.exists(filename):
583 raise AutoupdateError('update.gz not present in payload dir %s' %
584 payload_dir)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700585
Chris Sosa6a3697f2013-01-29 16:44:43 -0800586 metadata_obj = Autoupdate._ReadMetadataFromFile(payload_dir)
587 if not metadata_obj or not (metadata_obj.sha1 and
588 metadata_obj.sha256 and
589 metadata_obj.size):
590 sha1 = common_util.GetFileSha1(filename)
591 sha256 = common_util.GetFileSha256(filename)
592 size = common_util.GetFileSize(filename)
Gilad Arnolde74b3812013-04-22 11:27:38 -0700593 is_delta_format = self.IsDeltaFormatFile(filename)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800594 metadata_obj = UpdateMetadata(sha1, sha256, size, is_delta_format)
595 Autoupdate._StoreMetadataToFile(payload_dir, metadata_obj)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700596
Chris Sosa6a3697f2013-01-29 16:44:43 -0800597 return metadata_obj
598
599 def _ProcessUpdateComponents(self, app, event):
600 """Processes the app and event components of an update request.
601
602 Returns tuple containing forced_update_label, client_version, and board.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700603 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800604 # Initialize an empty dictionary for event attributes to log.
605 log_message = {}
Jay Srinivasanac69d262012-10-30 19:05:53 -0700606
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700607 # Determine request IP, strip any IPv6 data for simplicity.
608 client_ip = cherrypy.request.remote.ip.split(':')[-1]
Gilad Arnold286a0062012-01-12 13:47:02 -0800609 # Obtain (or init) info object for this client.
610 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
611
Chris Sosa6a3697f2013-01-29 16:44:43 -0800612 client_version = 'ForcedUpdate'
613 board = None
614 if app:
615 client_version = app.getAttribute('version')
616 channel = app.getAttribute('track')
617 board = (app.hasAttribute('board') and app.getAttribute('board')
618 or self._GetDefaultBoardID())
619 # Add attributes to log message
620 log_message['version'] = client_version
621 log_message['track'] = channel
622 log_message['board'] = board
623 curr_host_info.attrs['last_known_version'] = client_version
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700624
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700625 if event:
Gilad Arnold286a0062012-01-12 13:47:02 -0800626 event_result = int(event[0].getAttribute('eventresult'))
627 event_type = int(event[0].getAttribute('eventtype'))
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700628 client_previous_version = (event[0].getAttribute('previousversion')
629 if event[0].hasAttribute('previousversion')
630 else None)
Gilad Arnold286a0062012-01-12 13:47:02 -0800631 # Store attributes to legacy host info structure
632 curr_host_info.attrs['last_event_status'] = event_result
633 curr_host_info.attrs['last_event_type'] = event_type
634 # Add attributes to log message
635 log_message['event_result'] = event_result
636 log_message['event_type'] = event_type
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700637 if client_previous_version is not None:
638 log_message['previous_version'] = client_previous_version
Gilad Arnold286a0062012-01-12 13:47:02 -0800639
Gilad Arnold8318eac2012-10-04 12:52:23 -0700640 # Log host event, if so instructed.
641 if self.host_log:
642 curr_host_info.AddLogEntry(log_message)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700643
Chris Sosa6a3697f2013-01-29 16:44:43 -0800644 return (curr_host_info.attrs.pop('forced_update_label', None),
645 client_version, board)
646
647 def _GetStaticUrl(self):
648 """Returns the static url base that should prefix all payload responses."""
649 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
650 if x_forwarded_host:
651 hostname = 'http://' + x_forwarded_host
652 else:
653 hostname = cherrypy.request.base
654
655 if self.urlbase:
656 static_urlbase = self.urlbase
657 elif self.serve_only:
658 static_urlbase = '%s/static/archive' % hostname
659 else:
660 static_urlbase = '%s/static' % hostname
661
662 # If we have a proxy port, adjust the URL we instruct the client to
663 # use to go through the proxy.
664 if self.proxy_port:
665 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
666
667 _Log('Using static url base %s', static_urlbase)
668 _Log('Handling update ping as %s', hostname)
669 return static_urlbase
670
671 def HandleUpdatePing(self, data, label=None):
672 """Handles an update ping from an update client.
673
674 Args:
675 data: XML blob from client.
676 label: optional label for the update.
677 Returns:
678 Update payload message for client.
679 """
680 # Get the static url base that will form that base of our update url e.g.
681 # http://hostname:8080/static/update.gz.
682 static_urlbase = self._GetStaticUrl()
683
684 # Parse the XML we got into the components we care about.
685 protocol, app, event, update_check = autoupdate_lib.ParseUpdateRequest(data)
686
687 # #########################################################################
688 # Process attributes of the update check.
689 forced_update_label, client_version, board = self._ProcessUpdateComponents(
690 app, event)
691
692 # We only process update_checks in the update rpc.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700693 if not update_check:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800694 _Log('Non-update check received. Returning blank payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700695 # TODO(sosa): Generate correct non-updatecheck payload to better test
696 # update clients.
Chris Sosa52148582012-11-15 15:35:58 -0800697 return autoupdate_lib.GetNoUpdateResponse(protocol)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700698
Chris Sosa6a3697f2013-01-29 16:44:43 -0800699 # In case max_updates is used, return no response if max reached.
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700700 if self.max_updates > 0:
701 self.max_updates -= 1
702 elif self.max_updates == 0:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800703 _Log('Request received but max number of updates handled')
Chris Sosa52148582012-11-15 15:35:58 -0800704 return autoupdate_lib.GetNoUpdateResponse(protocol)
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700705
Chris Sosa6a3697f2013-01-29 16:44:43 -0800706 _Log('Update Check Received. Client is using protocol version: %s',
707 protocol)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700708
Chris Sosa6a3697f2013-01-29 16:44:43 -0800709 if forced_update_label:
710 if label:
711 _Log('Label: %s set but being overwritten to %s by request', label,
712 forced_update_label)
713
714 label = forced_update_label
715
716 # #########################################################################
717 # Finally its time to generate the omaha response to give to client that
718 # lets them know where to find the payload and its associated metadata.
719 metadata_obj = None
720
721 try:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700722 # Are we provisioning a remote or local payload?
723 if self.remote_payload:
724 # If no explicit label was provided, use the value of --payload.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800725 if not label:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700726 label = self.payload_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700727
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700728 # Form the URL of the update payload. This assumes that the payload
729 # file name is a devserver constant (which currently is the case).
730 url = '/'.join(filter(None, [static_urlbase, label, UPDATE_FILE]))
Chris Sosa5d342a22010-09-28 16:54:41 -0700731
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700732 # Get remote payload attributes.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800733 metadata_obj = self._GetRemotePayloadAttrs(url)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700734 else:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800735 static_image_dir = _NonePathJoin(self.static_dir, label)
736 rel_path = None
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700737
Chris Sosa6a3697f2013-01-29 16:44:43 -0800738 # Serving files only, don't generate an update.
739 if not self.serve_only:
740 # Generate payload if necessary.
741 rel_path = self.GenerateUpdatePayload(board, client_version,
742 static_image_dir)
743
744 url = '/'.join(filter(None, [static_urlbase, label, rel_path,
745 UPDATE_FILE]))
746 local_payload_dir = _NonePathJoin(static_image_dir, rel_path)
747 metadata_obj = self.GetLocalPayloadAttrs(local_payload_dir)
748
749 except AutoupdateError as e:
750 # Raised if we fail to generate an update payload.
751 _Log('Failed to process an update: %r', e)
752 return autoupdate_lib.GetNoUpdateResponse(protocol)
753
754 _Log('Responding to client to use url %s to get image', url)
755 return autoupdate_lib.GetUpdateResponse(
756 metadata_obj.sha1, metadata_obj.sha256, metadata_obj.size, url,
757 metadata_obj.is_delta_format, protocol, self.critical_update)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700758
759 def HandleHostInfoPing(self, ip):
760 """Returns host info dictionary for the given IP in JSON format."""
761 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800762 if ip in self.host_infos.table:
763 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
764
765 def HandleHostLogPing(self, ip):
766 """Returns a complete log of events for host in JSON format."""
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700767 # If all events requested, return a dictionary of logs keyed by IP address.
Gilad Arnold286a0062012-01-12 13:47:02 -0800768 if ip == 'all':
769 return json.dumps(
770 dict([(key, self.host_infos.table[key].log)
771 for key in self.host_infos.table]))
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700772
773 # Otherwise we're looking for a specific IP address, so find its log.
Gilad Arnold286a0062012-01-12 13:47:02 -0800774 if ip in self.host_infos.table:
775 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700776
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700777 # If no events were logged for this IP, return an empty log.
778 return json.dumps([])
779
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700780 def HandleSetUpdatePing(self, ip, label):
781 """Sets forced_update_label for a given host."""
782 assert ip, 'No ip provided.'
783 assert label, 'No label provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800784 self.host_infos.GetInitHostInfo(ip).attrs['forced_update_label'] = label