blob: b74f343d99c76c31644e9b81d98c8512cdb31074 [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
joychened64b222013-06-21 16:39:34 -0700148 _OLD_PAYLOAD_URL_PREFIX = '/static/archive'
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700149 _PAYLOAD_URL_PREFIX = '/static/'
150 _FILEINFO_URL_PREFIX = '/api/fileinfo/'
151
Chris Sosa6a3697f2013-01-29 16:44:43 -0800152 SHA1_ATTR = 'sha1'
153 SHA256_ATTR = 'sha256'
154 SIZE_ATTR = 'size'
155 ISDELTA_ATTR = 'is_delta'
156
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700157 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700158 forced_image=None, payload_path=None,
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700159 proxy_port=None, src_image='', patch_kernel=True, board=None,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800160 copy_to_static_root=True, private_key=None,
Chris Sosa52148582012-11-15 15:35:58 -0800161 critical_update=False, remote_payload=False, max_updates= -1,
Chris Sosa6a3697f2013-01-29 16:44:43 -0800162 host_log=False, *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700163 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700164 self.serve_only = serve_only
Chris Sosa0356d3b2010-09-16 15:46:22 -0700165 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -0700166 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700167 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -0700168 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700169 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -0700170
Chris Sosa0356d3b2010-09-16 15:46:22 -0700171 self.forced_image = forced_image
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700172 self.payload_path = payload_path
Chris Sosa62f720b2010-10-26 21:39:48 -0700173 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -0800174 self.proxy_port = proxy_port
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700175 self.patch_kernel = patch_kernel
Chris Sosae67b78f2010-11-04 17:33:16 -0700176 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -0800177 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-14 16:33:22 -0800178 self.private_key = private_key
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800179 self.critical_update = critical_update
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700180 self.remote_payload = remote_payload
Jay Srinivasanac69d262012-10-30 19:05:53 -0700181 self.max_updates = max_updates
Gilad Arnold8318eac2012-10-04 12:52:23 -0700182 self.host_log = host_log
Don Garrettfff4c322010-11-19 13:37:12 -0800183
Chris Sosa417e55d2011-01-25 16:40:48 -0800184 # Path to pre-generated file.
185 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700186
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700187 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 13:47:02 -0800188 # information about a given host. A host is identified by its IP address.
189 # The info stored for each host includes a complete log of events for this
190 # host, as well as a dictionary of current attributes derived from events.
191 self.host_infos = HostInfoTable()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700192
Chris Sosa6a3697f2013-01-29 16:44:43 -0800193 @classmethod
194 def _ReadMetadataFromStream(cls, stream):
195 """Returns metadata obj from input json stream that implements .read()."""
196 file_attr_dict = {}
197 try:
198 file_attr_dict = json.loads(stream.read())
199 except IOError:
200 return None
201
202 sha1 = file_attr_dict.get(cls.SHA1_ATTR)
203 sha256 = file_attr_dict.get(cls.SHA256_ATTR)
204 size = file_attr_dict.get(cls.SIZE_ATTR)
205 is_delta = file_attr_dict.get(cls.ISDELTA_ATTR)
206 return UpdateMetadata(sha1, sha256, size, is_delta)
207
208 @staticmethod
209 def _ReadMetadataFromFile(payload_dir):
210 """Returns metadata object from the metadata_file in the payload_dir"""
211 metadata_file = os.path.join(payload_dir, METADATA_FILE)
212 if os.path.exists(metadata_file):
213 with open(metadata_file, 'r') as metadata_stream:
214 return Autoupdate._ReadMetadataFromStream(metadata_stream)
215
216 @classmethod
217 def _StoreMetadataToFile(cls, payload_dir, metadata_obj):
218 """Stores metadata object into the metadata_file of the payload_dir"""
219 file_dict = {cls.SHA1_ATTR: metadata_obj.sha1,
220 cls.SHA256_ATTR: metadata_obj.sha256,
221 cls.SIZE_ATTR: metadata_obj.size,
222 cls.ISDELTA_ATTR: metadata_obj.is_delta_format}
223 metadata_file = os.path.join(payload_dir, METADATA_FILE)
224 with open(metadata_file, 'w') as file_handle:
225 json.dump(file_dict, file_handle)
226
Chris Sosa0356d3b2010-09-16 15:46:22 -0700227 def _GetDefaultBoardID(self):
228 """Returns the default board id stored in .default_board."""
229 board_file = '%s/.default_board' % (self.scripts_dir)
230 try:
231 return open(board_file).read()
232 except IOError:
233 return 'x86-generic'
234
Chris Sosa52148582012-11-15 15:35:58 -0800235 @staticmethod
236 def _GetVersionFromDir(image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700237 """Returns the version of the image based on the name of the directory."""
238 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700239 parts = latest_version.split('-')
240 if len(parts) == 2:
241 # Old-style, e.g. "0.15.938.2011_08_23_0941-a1".
242 # TODO(derat): Remove the code for old-style versions after 20120101.
243 return parts[0]
244 else:
245 # New-style, e.g. "R16-1102.0.2011_09_30_0806-a1".
246 return parts[1]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700247
Chris Sosa52148582012-11-15 15:35:58 -0800248 @staticmethod
249 def _CanUpdate(client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800250 """Returns true if the latest_version is greater than the client_version.
251 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800252 _Log('client version %s latest version %s', client_version, latest_version)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700253
254 client_tokens = client_version.replace('_', '').split('.')
255 # If the client has an old four-token version like "0.16.892.0", drop the
256 # first two tokens -- we use versions like "892.0.0" now.
257 # TODO(derat): Remove the code for old-style versions after 20120101.
258 if len(client_tokens) == 4:
259 client_tokens = client_tokens[2:]
260
261 latest_tokens = latest_version.replace('_', '').split('.')
262 if len(latest_tokens) == 4:
263 latest_tokens = latest_tokens[2:]
264
265 for i in range(min(len(client_tokens), len(latest_tokens))):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700266 if int(latest_tokens[i]) == int(client_tokens[i]):
267 continue
268 return int(latest_tokens[i]) > int(client_tokens[i])
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700269
270 # Favor four-token new-style versions on the server over old-style versions
271 # on the client if everything else matches.
272 return len(latest_tokens) > len(client_tokens)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700273
Chris Sosa0356d3b2010-09-16 15:46:22 -0700274 def _GetImageName(self):
275 """Returns the name of the image that should be used."""
276 if self.use_test_image:
277 image_name = 'chromiumos_test_image.bin'
278 else:
279 image_name = 'chromiumos_image.bin'
Chris Sosa6a3697f2013-01-29 16:44:43 -0800280
Chris Sosa0356d3b2010-09-16 15:46:22 -0700281 return image_name
282
Chris Sosa52148582012-11-15 15:35:58 -0800283 @staticmethod
Gilad Arnolde74b3812013-04-22 11:27:38 -0700284 def IsDeltaFormatFile(filename):
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700285 try:
Gilad Arnolde74b3812013-04-22 11:27:38 -0700286 with open(filename) as payload_file:
287 payload = update_payload.Payload(payload_file)
288 payload.Init()
289 return payload.IsDelta()
290 except (IOError, update_payload.PayloadError):
291 # For unit tests we may not have real files, so it's ok to ignore these
292 # errors.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700293 return False
294
Don Garrettf90edf02010-11-16 17:36:14 -0800295 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700296 """Generates an update gz given a full path to an image.
297
298 Args:
299 image_path: Full path to image.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800300 Raises:
301 subprocess.CalledProcessError if the update generator fails to generate a
302 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700303 """
Don Garrettfff4c322010-11-19 13:37:12 -0800304 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800305 _Log('Generating update image %s', update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700306
Chris Sosa0f1ec842011-02-14 16:33:22 -0800307 update_command = [
Chris Sosa5b8b5eb2012-03-27 11:15:27 -0700308 'cros_generate_update_payload',
Chris Sosa6a3697f2013-01-29 16:44:43 -0800309 '--image', image_path,
310 '--output', update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800311 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700312
Chris Sosa52148582012-11-15 15:35:58 -0800313 if src_image:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800314 update_command.extend(['--src_image', src_image])
Chris Sosa52148582012-11-15 15:35:58 -0800315
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700316 if self.patch_kernel:
Chris Sosa52148582012-11-15 15:35:58 -0800317 update_command.append('--patch_kernel')
318
319 if self.private_key:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800320 update_command.extend(['--private_key', self.private_key])
Chris Sosa0f1ec842011-02-14 16:33:22 -0800321
Chris Sosa6a3697f2013-01-29 16:44:43 -0800322 _Log('Running %s', ' '.join(update_command))
323 subprocess.check_call(update_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700324
Chris Sosa52148582012-11-15 15:35:58 -0800325 @staticmethod
326 def GenerateStatefulFile(image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800327 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700328
329 Args:
330 image_path: Full path to image.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800331 Raises:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800332 subprocess.CalledProcessError if the update generator fails to generate a
Chris Sosa908fd6f2010-11-10 17:31:18 -0800333 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700334 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800335 update_command = [
336 'cros_generate_stateful_update_payload',
337 '--image', image_path,
338 '--output_dir', output_dir,
339 ]
340 _Log('Running %s', ' '.join(update_command))
341 subprocess.check_call(update_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700342
Don Garrettf90edf02010-11-16 17:36:14 -0800343 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
344 """Find directory to store a cached update.
345
Gilad Arnold55a2a372012-10-02 09:46:32 -0700346 Given one, or two images for an update, this finds which cache directory
347 should hold the update files, even if they don't exist yet.
Don Garrettf90edf02010-11-16 17:36:14 -0800348
Gilad Arnold55a2a372012-10-02 09:46:32 -0700349 Returns:
350 A directory path for storing a cached update, of the following form:
351 Non-delta updates:
352 CACHE_DIR/<dest_hash>
353 Delta updates:
354 CACHE_DIR/<src_hash>_<dest_hash>
355 Signed updates (self.private_key):
356 CACHE_DIR/<src_hash>_<dest_hash>+<private_key_hash>
Chris Sosa744e1472011-09-07 19:32:50 -0700357 """
Gilad Arnold55a2a372012-10-02 09:46:32 -0700358 update_dir = ''
Chris Sosa744e1472011-09-07 19:32:50 -0700359 if src_image:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700360 update_dir += common_util.GetFileMd5(src_image) + '_'
Don Garrettf90edf02010-11-16 17:36:14 -0800361
Gilad Arnold55a2a372012-10-02 09:46:32 -0700362 update_dir += common_util.GetFileMd5(dest_image)
Chris Sosa744e1472011-09-07 19:32:50 -0700363 if self.private_key:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700364 update_dir += '+' + common_util.GetFileMd5(self.private_key)
Chris Sosa744e1472011-09-07 19:32:50 -0700365
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700366 if self.patch_kernel:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700367 update_dir += '+patched_kernel'
Chris Sosa9fba7562012-01-31 10:15:47 -0800368
Gilad Arnold55a2a372012-10-02 09:46:32 -0700369 return os.path.join(CACHE_DIR, update_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800370
Don Garrettfff4c322010-11-19 13:37:12 -0800371 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800372 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700373
Chris Sosade91f672010-11-16 10:05:44 -0800374 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800375 src_image: image we are updating from (Null/empty for non-delta)
376 image_path: full path to the image.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800377 output_dir: the directory to write the update payloads to
378 Raises:
379 AutoupdateError if it failed to generate either update or stateful
380 payload.
Chris Sosade91f672010-11-16 10:05:44 -0800381 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800382 _Log('Generating update for image %s', image_path)
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700383
Chris Sosa6a3697f2013-01-29 16:44:43 -0800384 # Delete any previous state in this directory.
385 os.system('rm -rf "%s"' % output_dir)
386 os.makedirs(output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000387
Chris Sosa6a3697f2013-01-29 16:44:43 -0800388 try:
389 self.GenerateUpdateFile(self.src_image, image_path, output_dir)
390 self.GenerateStatefulFile(image_path, output_dir)
391 except subprocess.CalledProcessError:
392 os.system('rm -rf "%s"' % output_dir)
393 raise AutoupdateError('Failed to generate update in %s' % output_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800394
395 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
396 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000397
Chris Sosa0356d3b2010-09-16 15:46:22 -0700398 Args:
399 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700400 static_image_dir: the directory to move images to after generating.
401 Returns:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800402 update directory relative to static_image_dir. None if it should
403 serve from the static_image_dir.
404 Raises:
405 AutoupdateError if it we need to generate a payload and fail to do so.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700406 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800407 _Log('Generating update for src %s image %s', self.src_image, image_path)
Chris Sosae67b78f2010-11-04 17:33:16 -0700408
Chris Sosa417e55d2011-01-25 16:40:48 -0800409 # If it was pregenerated_path, don't regenerate
410 if self.pregenerated_path:
411 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800412
Don Garrettf90edf02010-11-16 17:36:14 -0800413 # Which sub_dir of static_image_dir should hold our cached update image
414 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800415 _Log('Caching in sub_dir "%s"', cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800416
Don Garrettf90edf02010-11-16 17:36:14 -0800417 # The cached payloads exist in a cache dir
418 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa6a3697f2013-01-29 16:44:43 -0800419 cache_sub_dir, UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800420 cache_stateful_payload = os.path.join(static_image_dir,
Chris Sosa6a3697f2013-01-29 16:44:43 -0800421 cache_sub_dir, STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800422
Chris Sosa6a3697f2013-01-29 16:44:43 -0800423 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800424 # Check to see if this cache directory is valid.
425 if not os.path.exists(cache_update_payload) or not os.path.exists(
426 cache_stateful_payload):
Chris Sosa6a3697f2013-01-29 16:44:43 -0800427 self.GenerateUpdateImage(image_path, full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800428
Chris Sosa6a3697f2013-01-29 16:44:43 -0800429 self.pregenerated_path = cache_sub_dir
Chris Sosa65d339b2013-01-21 18:59:21 -0800430
Chris Sosa6a3697f2013-01-29 16:44:43 -0800431 # Generate the cache file.
432 self.GetLocalPayloadAttrs(full_cache_dir)
433 cache_metadata_file = os.path.join(full_cache_dir, METADATA_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800434
Chris Sosa08d55a22011-01-19 16:08:02 -0800435 # Generation complete, copy if requested.
436 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800437 # The final results exist directly in static
Gilad Arnolde74b3812013-04-22 11:27:38 -0700438 cros_update_payload = os.path.join(static_image_dir, UPDATE_FILE)
439 stateful_payload = os.path.join(static_image_dir, STATEFUL_FILE)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800440 metadata_file = os.path.join(static_image_dir, METADATA_FILE)
Gilad Arnolde74b3812013-04-22 11:27:38 -0700441 common_util.CopyFile(cache_update_payload, cros_update_payload)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700442 common_util.CopyFile(cache_stateful_payload, stateful_payload)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800443 common_util.CopyFile(cache_metadata_file, metadata_file)
444 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800445 else:
446 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700447
Chris Sosa6a3697f2013-01-29 16:44:43 -0800448 def GenerateLatestUpdateImage(self, board, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800449 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700450 """Generates an update using the latest image that has been built.
451
452 This will only generate an update if the newest update is newer than that
453 on the client or client_version is 'ForcedUpdate'.
454
455 Args:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800456 board: Name of the board.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700457 client_version: Current version of the client or 'ForcedUpdate'
458 static_image_dir: the directory to move images to after generating.
459 Returns:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800460 Name of the update directory relative to the static dir. None if it should
461 serve from the static_image_dir.
462 Raises:
463 AutoupdateError if it failed to generate the payload or can't update
464 the given client_version.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700465 """
joychen921e1fb2013-06-28 11:12:20 -0700466 latest_image_dir = self.GetLatestImageDir(board)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700467 latest_version = self._GetVersionFromDir(latest_image_dir)
468 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
469
Chris Sosa0356d3b2010-09-16 15:46:22 -0700470 # Check to see whether or not we should update.
471 if client_version != 'ForcedUpdate' and not self._CanUpdate(
472 client_version, latest_version):
Chris Sosa6a3697f2013-01-29 16:44:43 -0800473 raise AutoupdateError('Update check received but no update available '
474 'for client')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700475
Don Garrettf90edf02010-11-16 17:36:14 -0800476 return self.GenerateUpdateImageWithCache(latest_image_path,
477 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700478
Chris Sosa6a3697f2013-01-29 16:44:43 -0800479 def GenerateUpdatePayload(self, board, client_version, static_image_dir):
480 """Generates an update for an image and returns the relative payload dir.
Chris Sosaa73ec162010-05-03 20:18:02 -0700481
Chris Sosa6a3697f2013-01-29 16:44:43 -0800482 Returns:
483 payload dir relative to static_image_dir. None if it should
484 serve from the static_image_dir.
485 Raises:
486 AutoupdateError if it failed to generate the payload.
Don Garrettf90edf02010-11-16 17:36:14 -0800487 """
Dale Curtis723ec472010-11-30 14:06:47 -0800488 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
489 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
490
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700491 if self.payload_path:
Don Garrett0c880e22010-11-17 18:13:37 -0800492 # If the forced payload is not already in our static_image_dir,
493 # copy it there.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700494 src_path = os.path.abspath(self.payload_path)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800495 src_stateful = os.path.join(os.path.dirname(src_path), STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800496 # Only copy the files if the source directory is different from dest.
497 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700498 common_util.CopyFile(src_path, dest_path)
Don Garrettee25e552010-11-23 12:09:35 -0800499
500 # The stateful payload is optional.
501 if os.path.exists(src_stateful):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700502 common_util.CopyFile(src_stateful, dest_stateful)
Don Garrettee25e552010-11-23 12:09:35 -0800503 else:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800504 _Log('WARN: %s not found. Expected for dev and test builds',
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700505 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800506 if os.path.exists(dest_stateful):
507 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800508
Chris Sosa6a3697f2013-01-29 16:44:43 -0800509 # Serve from the main directory so rel_path is None.
510 return None
Don Garrett0c880e22010-11-17 18:13:37 -0800511 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800512 return self.GenerateUpdateImageWithCache(
513 self.forced_image,
514 static_image_dir=static_image_dir)
Chris Sosa65d339b2013-01-21 18:59:21 -0800515 else:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800516 if not board:
517 raise AutoupdateError(
518 'Failed to generate update. '
519 'You must set --board when pre-generating latest update.')
Chris Sosa65d339b2013-01-21 18:59:21 -0800520
Chris Sosa6a3697f2013-01-29 16:44:43 -0800521 return self.GenerateLatestUpdateImage(board, client_version,
522 static_image_dir)
Chris Sosa2c048f12010-10-27 16:05:27 -0700523
524 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800525 """Pre-generates an update and prints out the relative path it.
526
Chris Sosa6a3697f2013-01-29 16:44:43 -0800527 Returns relative path of the update.
Chris Sosa65d339b2013-01-21 18:59:21 -0800528
Chris Sosa6a3697f2013-01-29 16:44:43 -0800529 Raises:
530 AutoupdateError if it failed to generate the payload.
531 """
532 _Log('Pre-generating the update payload')
533 # Does not work with labels so just use static dir.
534 pregenerated_update = self.GenerateUpdatePayload(self.board, '0.0.0.0',
535 self.static_dir)
536 print 'PREGENERATED_UPDATE=%s' % _NonePathJoin(pregenerated_update,
537 UPDATE_FILE)
Chris Sosa417e55d2011-01-25 16:40:48 -0800538 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700539
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700540 def _GetRemotePayloadAttrs(self, url):
541 """Returns hashes, size and delta flag of a remote update payload.
542
543 Obtain attributes of a payload file available on a remote devserver. This
544 is based on the assumption that the payload URL uses the /static prefix. We
545 need to make sure that both clients (requests) and remote devserver
546 (provisioning) preserve this invariant.
547
548 Args:
549 url: URL of statically staged remote file (http://host:port/static/...)
550 Returns:
551 A tuple containing the SHA1, SHA256, file size and whether or not it's a
552 delta payload (Boolean).
553 """
554 if self._PAYLOAD_URL_PREFIX not in url:
555 raise AutoupdateError(
556 'Payload URL does not have the expected prefix (%s)' %
557 self._PAYLOAD_URL_PREFIX)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800558
joychened64b222013-06-21 16:39:34 -0700559 if self._OLD_PAYLOAD_URL_PREFIX in url:
560 fileinfo_url = url.replace(self._OLD_PAYLOAD_URL_PREFIX,
561 self._FILEINFO_URL_PREFIX)
562 else:
563 fileinfo_url = url.replace(self._PAYLOAD_URL_PREFIX,
564 self._FILEINFO_URL_PREFIX)
565
Chris Sosa6a3697f2013-01-29 16:44:43 -0800566 _Log('Retrieving file info for remote payload via %s', fileinfo_url)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700567 try:
568 conn = urllib2.urlopen(fileinfo_url)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800569 metadata_obj = Autoupdate._ReadMetadataFromStream(conn)
570 # These fields are required for remote calls.
571 if not metadata_obj:
572 raise AutoupdateError('Failed to obtain remote payload info')
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700573
Chris Sosa6a3697f2013-01-29 16:44:43 -0800574 return metadata_obj
575 except IOError as e:
576 raise AutoupdateError('Failed to obtain remote payload info: %s', e)
577
578 def GetLocalPayloadAttrs(self, payload_dir):
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700579 """Returns hashes, size and delta flag of a local update payload.
580
581 Args:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800582 payload_dir: Path to the directory the payload is in.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700583 Returns:
584 A tuple containing the SHA1, SHA256, file size and whether or not it's a
585 delta payload (Boolean).
586 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800587 filename = os.path.join(payload_dir, UPDATE_FILE)
588 if not os.path.exists(filename):
589 raise AutoupdateError('update.gz not present in payload dir %s' %
590 payload_dir)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700591
Chris Sosa6a3697f2013-01-29 16:44:43 -0800592 metadata_obj = Autoupdate._ReadMetadataFromFile(payload_dir)
593 if not metadata_obj or not (metadata_obj.sha1 and
594 metadata_obj.sha256 and
595 metadata_obj.size):
596 sha1 = common_util.GetFileSha1(filename)
597 sha256 = common_util.GetFileSha256(filename)
598 size = common_util.GetFileSize(filename)
Gilad Arnolde74b3812013-04-22 11:27:38 -0700599 is_delta_format = self.IsDeltaFormatFile(filename)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800600 metadata_obj = UpdateMetadata(sha1, sha256, size, is_delta_format)
601 Autoupdate._StoreMetadataToFile(payload_dir, metadata_obj)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700602
Chris Sosa6a3697f2013-01-29 16:44:43 -0800603 return metadata_obj
604
605 def _ProcessUpdateComponents(self, app, event):
606 """Processes the app and event components of an update request.
607
608 Returns tuple containing forced_update_label, client_version, and board.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700609 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800610 # Initialize an empty dictionary for event attributes to log.
611 log_message = {}
Jay Srinivasanac69d262012-10-30 19:05:53 -0700612
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700613 # Determine request IP, strip any IPv6 data for simplicity.
614 client_ip = cherrypy.request.remote.ip.split(':')[-1]
Gilad Arnold286a0062012-01-12 13:47:02 -0800615 # Obtain (or init) info object for this client.
616 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
617
Chris Sosa6a3697f2013-01-29 16:44:43 -0800618 client_version = 'ForcedUpdate'
619 board = None
620 if app:
621 client_version = app.getAttribute('version')
622 channel = app.getAttribute('track')
623 board = (app.hasAttribute('board') and app.getAttribute('board')
624 or self._GetDefaultBoardID())
625 # Add attributes to log message
626 log_message['version'] = client_version
627 log_message['track'] = channel
628 log_message['board'] = board
629 curr_host_info.attrs['last_known_version'] = client_version
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700630
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700631 if event:
Gilad Arnold286a0062012-01-12 13:47:02 -0800632 event_result = int(event[0].getAttribute('eventresult'))
633 event_type = int(event[0].getAttribute('eventtype'))
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700634 client_previous_version = (event[0].getAttribute('previousversion')
635 if event[0].hasAttribute('previousversion')
636 else None)
Gilad Arnold286a0062012-01-12 13:47:02 -0800637 # Store attributes to legacy host info structure
638 curr_host_info.attrs['last_event_status'] = event_result
639 curr_host_info.attrs['last_event_type'] = event_type
640 # Add attributes to log message
641 log_message['event_result'] = event_result
642 log_message['event_type'] = event_type
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700643 if client_previous_version is not None:
644 log_message['previous_version'] = client_previous_version
Gilad Arnold286a0062012-01-12 13:47:02 -0800645
Gilad Arnold8318eac2012-10-04 12:52:23 -0700646 # Log host event, if so instructed.
647 if self.host_log:
648 curr_host_info.AddLogEntry(log_message)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700649
Chris Sosa6a3697f2013-01-29 16:44:43 -0800650 return (curr_host_info.attrs.pop('forced_update_label', None),
651 client_version, board)
652
653 def _GetStaticUrl(self):
654 """Returns the static url base that should prefix all payload responses."""
655 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
656 if x_forwarded_host:
657 hostname = 'http://' + x_forwarded_host
658 else:
659 hostname = cherrypy.request.base
660
661 if self.urlbase:
662 static_urlbase = self.urlbase
Chris Sosa6a3697f2013-01-29 16:44:43 -0800663 else:
664 static_urlbase = '%s/static' % hostname
665
666 # If we have a proxy port, adjust the URL we instruct the client to
667 # use to go through the proxy.
668 if self.proxy_port:
669 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
670
671 _Log('Using static url base %s', static_urlbase)
672 _Log('Handling update ping as %s', hostname)
673 return static_urlbase
674
675 def HandleUpdatePing(self, data, label=None):
676 """Handles an update ping from an update client.
677
678 Args:
679 data: XML blob from client.
680 label: optional label for the update.
681 Returns:
682 Update payload message for client.
683 """
684 # Get the static url base that will form that base of our update url e.g.
685 # http://hostname:8080/static/update.gz.
686 static_urlbase = self._GetStaticUrl()
687
688 # Parse the XML we got into the components we care about.
689 protocol, app, event, update_check = autoupdate_lib.ParseUpdateRequest(data)
690
691 # #########################################################################
692 # Process attributes of the update check.
693 forced_update_label, client_version, board = self._ProcessUpdateComponents(
694 app, event)
695
696 # We only process update_checks in the update rpc.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700697 if not update_check:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800698 _Log('Non-update check received. Returning blank payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700699 # TODO(sosa): Generate correct non-updatecheck payload to better test
700 # update clients.
Chris Sosa52148582012-11-15 15:35:58 -0800701 return autoupdate_lib.GetNoUpdateResponse(protocol)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700702
Chris Sosa6a3697f2013-01-29 16:44:43 -0800703 # In case max_updates is used, return no response if max reached.
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700704 if self.max_updates > 0:
705 self.max_updates -= 1
706 elif self.max_updates == 0:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800707 _Log('Request received but max number of updates handled')
Chris Sosa52148582012-11-15 15:35:58 -0800708 return autoupdate_lib.GetNoUpdateResponse(protocol)
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700709
Chris Sosa6a3697f2013-01-29 16:44:43 -0800710 _Log('Update Check Received. Client is using protocol version: %s',
711 protocol)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700712
Chris Sosa6a3697f2013-01-29 16:44:43 -0800713 if forced_update_label:
714 if label:
715 _Log('Label: %s set but being overwritten to %s by request', label,
716 forced_update_label)
717
718 label = forced_update_label
719
720 # #########################################################################
721 # Finally its time to generate the omaha response to give to client that
722 # lets them know where to find the payload and its associated metadata.
723 metadata_obj = None
724
725 try:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700726 # Are we provisioning a remote or local payload?
727 if self.remote_payload:
728 # If no explicit label was provided, use the value of --payload.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800729 if not label:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700730 label = self.payload_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700731
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700732 # Form the URL of the update payload. This assumes that the payload
733 # file name is a devserver constant (which currently is the case).
734 url = '/'.join(filter(None, [static_urlbase, label, UPDATE_FILE]))
Chris Sosa5d342a22010-09-28 16:54:41 -0700735
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700736 # Get remote payload attributes.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800737 metadata_obj = self._GetRemotePayloadAttrs(url)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700738 else:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800739 static_image_dir = _NonePathJoin(self.static_dir, label)
740 rel_path = None
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700741
Chris Sosa6a3697f2013-01-29 16:44:43 -0800742 # Serving files only, don't generate an update.
743 if not self.serve_only:
744 # Generate payload if necessary.
745 rel_path = self.GenerateUpdatePayload(board, client_version,
746 static_image_dir)
747
748 url = '/'.join(filter(None, [static_urlbase, label, rel_path,
749 UPDATE_FILE]))
750 local_payload_dir = _NonePathJoin(static_image_dir, rel_path)
751 metadata_obj = self.GetLocalPayloadAttrs(local_payload_dir)
752
753 except AutoupdateError as e:
754 # Raised if we fail to generate an update payload.
755 _Log('Failed to process an update: %r', e)
756 return autoupdate_lib.GetNoUpdateResponse(protocol)
757
758 _Log('Responding to client to use url %s to get image', url)
759 return autoupdate_lib.GetUpdateResponse(
760 metadata_obj.sha1, metadata_obj.sha256, metadata_obj.size, url,
761 metadata_obj.is_delta_format, protocol, self.critical_update)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700762
763 def HandleHostInfoPing(self, ip):
764 """Returns host info dictionary for the given IP in JSON format."""
765 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800766 if ip in self.host_infos.table:
767 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
768
769 def HandleHostLogPing(self, ip):
770 """Returns a complete log of events for host in JSON format."""
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700771 # If all events requested, return a dictionary of logs keyed by IP address.
Gilad Arnold286a0062012-01-12 13:47:02 -0800772 if ip == 'all':
773 return json.dumps(
774 dict([(key, self.host_infos.table[key].log)
775 for key in self.host_infos.table]))
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700776
777 # Otherwise we're looking for a specific IP address, so find its log.
Gilad Arnold286a0062012-01-12 13:47:02 -0800778 if ip in self.host_infos.table:
779 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700780
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700781 # If no events were logged for this IP, return an empty log.
782 return json.dumps([])
783
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700784 def HandleSetUpdatePing(self, ip, label):
785 """Sets forced_update_label for a given host."""
786 assert ip, 'No ip provided.'
787 assert label, 'No label provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800788 self.host_infos.GetInitHostInfo(ip).attrs['forced_update_label'] = label