blob: fb8d2882c0bcac5f6a34f02149e8809febfb0028 [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
Darin Petkov2b2ff4b2010-07-27 15:02:09 -07008import time
Gilad Arnold0c9c8602012-10-02 23:58:58 -07009import urllib2
Don Garrett0ad09372010-12-06 16:20:30 -080010import urlparse
Chris Sosa7c931362010-10-11 19:49:01 -070011
Gilad Arnoldabb352e2012-09-23 01:24:27 -070012import cherrypy
13
14from build_util import BuildObject
Chris Sosa52148582012-11-15 15:35:58 -080015import autoupdate_lib
Gilad Arnold55a2a372012-10-02 09:46:32 -070016import common_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070017import log_util
Chris Sosa05491b12010-11-08 17:14:16 -080018
Gilad Arnoldc65330c2012-09-20 15:17:48 -070019
20# Module-local log function.
Chris Sosa65d339b2013-01-21 18:59:21 -080021def _Log(message, *args, **kwargs):
22 return log_util.LogWithTag('UPDATE', message, *args, **kwargs)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070023
rtc@google.comded22402009-10-26 22:36:21 +000024
Chris Sosa417e55d2011-01-25 16:40:48 -080025UPDATE_FILE = 'update.gz'
26STATEFUL_FILE = 'stateful.tgz'
27CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 15:46:22 -070028
Don Garrett0ad09372010-12-06 16:20:30 -080029
Gilad Arnold0c9c8602012-10-02 23:58:58 -070030class AutoupdateError(Exception):
31 """Exception classes used by this module."""
32 pass
33
34
Don Garrett0ad09372010-12-06 16:20:30 -080035def _ChangeUrlPort(url, new_port):
36 """Return the URL passed in with a different port"""
37 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
38 host_port = netloc.split(':')
39
40 if len(host_port) == 1:
41 host_port.append(new_port)
42 else:
43 host_port[1] = new_port
44
45 print host_port
46 netloc = "%s:%s" % tuple(host_port)
47
48 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
49
50
Chris Sosa65d339b2013-01-21 18:59:21 -080051class HostInfo:
Gilad Arnold286a0062012-01-12 13:47:02 -080052 """Records information about an individual host.
53
54 Members:
55 attrs: Static attributes (legacy)
56 log: Complete log of recorded client entries
57 """
58
59 def __init__(self):
60 # A dictionary of current attributes pertaining to the host.
61 self.attrs = {}
62
63 # A list of pairs consisting of a timestamp and a dictionary of recorded
64 # attributes.
65 self.log = []
66
67 def __repr__(self):
68 return 'attrs=%s, log=%s' % (self.attrs, self.log)
69
70 def AddLogEntry(self, entry):
71 """Append a new log entry."""
72 # Append a timestamp.
73 assert not 'timestamp' in entry, 'Oops, timestamp field already in use'
74 entry['timestamp'] = time.strftime('%Y-%m-%d %H:%M:%S')
75 # Add entry to hosts' message log.
76 self.log.append(entry)
77
Gilad Arnold286a0062012-01-12 13:47:02 -080078
Chris Sosa65d339b2013-01-21 18:59:21 -080079class HostInfoTable:
Gilad Arnold286a0062012-01-12 13:47:02 -080080 """Records information about a set of hosts who engage in update activity.
81
82 Members:
83 table: Table of information on hosts.
84 """
85
86 def __init__(self):
87 # A dictionary of host information. Keys are normally IP addresses.
88 self.table = {}
89
90 def __repr__(self):
91 return '%s' % self.table
92
93 def GetInitHostInfo(self, host_id):
94 """Return a host's info object, or create a new one if none exists."""
95 return self.table.setdefault(host_id, HostInfo())
96
97 def GetHostInfo(self, host_id):
98 """Return an info object for given host, if such exists."""
Chris Sosa1885d032012-11-29 17:07:27 -080099 return self.table.get(host_id)
Gilad Arnold286a0062012-01-12 13:47:02 -0800100
101
rtc@google.com64244662009-11-12 00:52:08 +0000102class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700103 """Class that contains functionality that handles Chrome OS update pings.
104
105 Members:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700106 serve_only: serve only pre-built updates. static_dir must contain
107 update.gz and stateful.tgz.
Chris Sosa65d339b2013-01-21 18:59:21 -0800108 factory_config: path to the factory config file if handling factory
109 requests.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700110 use_test_image: use chromiumos_test_image.bin rather than the standard.
111 urlbase: base URL, other than devserver, for update images.
112 forced_image: path to an image to use for all updates.
113 payload_path: path to pre-generated payload to serve.
114 src_image: if specified, creates a delta payload from this image.
115 proxy_port: port of local proxy to tell client to connect to you
116 through.
117 vm: set for VM images (doesn't patch kernel)
118 board: board for the image. Needed for pre-generating of updates.
119 copy_to_static_root: copies images generated from the cache to ~/static.
120 private_key: path to private key in PEM format.
Gilad Arnold8318eac2012-10-04 12:52:23 -0700121 critical_update: whether provisioned payload is critical.
122 remote_payload: whether provisioned payload is remotely staged.
123 max_updates: maximum number of updates we'll try to provision.
124 host_log: record full history of host update events.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700125 """
rtc@google.comded22402009-10-26 22:36:21 +0000126
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700127 _PAYLOAD_URL_PREFIX = '/static/'
128 _FILEINFO_URL_PREFIX = '/api/fileinfo/'
129
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700130 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Chris Sosa65d339b2013-01-21 18:59:21 -0800131 factory_config_path=None,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700132 forced_image=None, payload_path=None,
133 proxy_port=None, src_image='', vm=False, board=None,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800134 copy_to_static_root=True, private_key=None,
Chris Sosa52148582012-11-15 15:35:58 -0800135 critical_update=False, remote_payload=False, max_updates= -1,
Chris Sosa65d339b2013-01-21 18:59:21 -0800136 host_log=False,
137 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700138 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700139 self.serve_only = serve_only
Chris Sosa65d339b2013-01-21 18:59:21 -0800140 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700141 self.use_test_image = test_image
Chris Sosa65d339b2013-01-21 18:59:21 -0800142 self.hostname = None
Chris Sosa5d342a22010-09-28 16:54:41 -0700143 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700144 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -0700145 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700146 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -0700147
Chris Sosa0356d3b2010-09-16 15:46:22 -0700148 self.forced_image = forced_image
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700149 self.payload_path = payload_path
Chris Sosa62f720b2010-10-26 21:39:48 -0700150 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -0800151 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -0700152 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -0700153 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -0800154 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-14 16:33:22 -0800155 self.private_key = private_key
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800156 self.critical_update = critical_update
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700157 self.remote_payload = remote_payload
Jay Srinivasanac69d262012-10-30 19:05:53 -0700158 self.max_updates = max_updates
Gilad Arnold8318eac2012-10-04 12:52:23 -0700159 self.host_log = host_log
Don Garrettfff4c322010-11-19 13:37:12 -0800160
Chris Sosa417e55d2011-01-25 16:40:48 -0800161 # Path to pre-generated file.
162 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700163
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700164 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 13:47:02 -0800165 # information about a given host. A host is identified by its IP address.
166 # The info stored for each host includes a complete log of events for this
167 # host, as well as a dictionary of current attributes derived from events.
168 self.host_infos = HostInfoTable()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700169
Chris Sosa0356d3b2010-09-16 15:46:22 -0700170 def _GetDefaultBoardID(self):
171 """Returns the default board id stored in .default_board."""
172 board_file = '%s/.default_board' % (self.scripts_dir)
173 try:
174 return open(board_file).read()
175 except IOError:
176 return 'x86-generic'
177
Chris Sosa65d339b2013-01-21 18:59:21 -0800178 def _GetLatestImageDir(self, board_id):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700179 """Returns the latest image dir based on shell script."""
Chris Sosa65d339b2013-01-21 18:59:21 -0800180 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700181 return os.popen(cmd).read().strip()
182
Chris Sosa52148582012-11-15 15:35:58 -0800183 @staticmethod
184 def _GetVersionFromDir(image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700185 """Returns the version of the image based on the name of the directory."""
186 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700187 parts = latest_version.split('-')
188 if len(parts) == 2:
189 # Old-style, e.g. "0.15.938.2011_08_23_0941-a1".
190 # TODO(derat): Remove the code for old-style versions after 20120101.
191 return parts[0]
192 else:
193 # New-style, e.g. "R16-1102.0.2011_09_30_0806-a1".
194 return parts[1]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700195
Chris Sosa52148582012-11-15 15:35:58 -0800196 @staticmethod
197 def _CanUpdate(client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800198 """Returns true if the latest_version is greater than the client_version.
199 """
Chris Sosa65d339b2013-01-21 18:59:21 -0800200 _Log('client version %s latest version %s'
201 % (client_version, latest_version))
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700202
203 client_tokens = client_version.replace('_', '').split('.')
204 # If the client has an old four-token version like "0.16.892.0", drop the
205 # first two tokens -- we use versions like "892.0.0" now.
206 # TODO(derat): Remove the code for old-style versions after 20120101.
207 if len(client_tokens) == 4:
208 client_tokens = client_tokens[2:]
209
210 latest_tokens = latest_version.replace('_', '').split('.')
211 if len(latest_tokens) == 4:
212 latest_tokens = latest_tokens[2:]
213
214 for i in range(min(len(client_tokens), len(latest_tokens))):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700215 if int(latest_tokens[i]) == int(client_tokens[i]):
216 continue
217 return int(latest_tokens[i]) > int(client_tokens[i])
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700218
219 # Favor four-token new-style versions on the server over old-style versions
220 # on the client if everything else matches.
221 return len(latest_tokens) > len(client_tokens)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700222
Chris Sosa0356d3b2010-09-16 15:46:22 -0700223 def _GetImageName(self):
224 """Returns the name of the image that should be used."""
225 if self.use_test_image:
226 image_name = 'chromiumos_test_image.bin'
227 else:
228 image_name = 'chromiumos_image.bin'
229 return image_name
230
Chris Sosa52148582012-11-15 15:35:58 -0800231 @staticmethod
232 def _IsDeltaFormatFile(filename):
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700233 try:
234 file_handle = open(filename, 'r')
235 delta_magic = 'CrAU'
236 magic = file_handle.read(len(delta_magic))
237 return magic == delta_magic
Jay Srinivasanac69d262012-10-30 19:05:53 -0700238 except IOError:
239 # For unit tests, we may not have real files, so it's ok to
240 # ignore these IOErrors. In any case, this value is not being
241 # used in update_engine at all as of now.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700242 return False
243
Don Garrettf90edf02010-11-16 17:36:14 -0800244 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700245 """Generates an update gz given a full path to an image.
246
247 Args:
248 image_path: Full path to image.
Chris Sosa65d339b2013-01-21 18:59:21 -0800249 Returns:
250 Path to created update_payload or None on error.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700251 """
Don Garrettfff4c322010-11-19 13:37:12 -0800252 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa65d339b2013-01-21 18:59:21 -0800253 _Log('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700254
Chris Sosa0f1ec842011-02-14 16:33:22 -0800255 update_command = [
Chris Sosa5b8b5eb2012-03-27 11:15:27 -0700256 'cros_generate_update_payload',
Chris Sosa65d339b2013-01-21 18:59:21 -0800257 '--image="%s"' % image_path,
258 '--output="%s"' % update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800259 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700260
Chris Sosa52148582012-11-15 15:35:58 -0800261 if src_image:
Chris Sosa65d339b2013-01-21 18:59:21 -0800262 update_command.append('--src_image="%s"' % src_image)
Chris Sosa52148582012-11-15 15:35:58 -0800263
264 if not self.vm:
265 update_command.append('--patch_kernel')
266
267 if self.private_key:
Chris Sosa65d339b2013-01-21 18:59:21 -0800268 update_command.append('--private_key="%s"' % self.private_key)
Chris Sosa0f1ec842011-02-14 16:33:22 -0800269
Chris Sosa65d339b2013-01-21 18:59:21 -0800270 update_string = ' '.join(update_command)
271 _Log('Running ' + update_string)
272 if os.system(update_string) != 0:
273 _Log('Failed to create update payload')
274 return None
275
276 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700277
Chris Sosa52148582012-11-15 15:35:58 -0800278 @staticmethod
279 def GenerateStatefulFile(image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800280 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700281
282 Args:
283 image_path: Full path to image.
Chris Sosa65d339b2013-01-21 18:59:21 -0800284 Returns:
285 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800286 Raises:
Chris Sosa65d339b2013-01-21 18:59:21 -0800287 A subprocess exception if the update generator fails to generate a
Chris Sosa908fd6f2010-11-10 17:31:18 -0800288 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700289 """
Chris Sosa65d339b2013-01-21 18:59:21 -0800290 subprocess.check_call(
291 ['cros_generate_stateful_update_payload',
292 '--image=%s' % image_path,
293 '--output_dir=%s' % output_dir,
294 ])
295 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700296
Don Garrettf90edf02010-11-16 17:36:14 -0800297 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
298 """Find directory to store a cached update.
299
Gilad Arnold55a2a372012-10-02 09:46:32 -0700300 Given one, or two images for an update, this finds which cache directory
301 should hold the update files, even if they don't exist yet.
Don Garrettf90edf02010-11-16 17:36:14 -0800302
Gilad Arnold55a2a372012-10-02 09:46:32 -0700303 Returns:
304 A directory path for storing a cached update, of the following form:
305 Non-delta updates:
306 CACHE_DIR/<dest_hash>
307 Delta updates:
308 CACHE_DIR/<src_hash>_<dest_hash>
309 Signed updates (self.private_key):
310 CACHE_DIR/<src_hash>_<dest_hash>+<private_key_hash>
Chris Sosa744e1472011-09-07 19:32:50 -0700311 """
Gilad Arnold55a2a372012-10-02 09:46:32 -0700312 update_dir = ''
Chris Sosa744e1472011-09-07 19:32:50 -0700313 if src_image:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700314 update_dir += common_util.GetFileMd5(src_image) + '_'
Don Garrettf90edf02010-11-16 17:36:14 -0800315
Gilad Arnold55a2a372012-10-02 09:46:32 -0700316 update_dir += common_util.GetFileMd5(dest_image)
Chris Sosa744e1472011-09-07 19:32:50 -0700317 if self.private_key:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700318 update_dir += '+' + common_util.GetFileMd5(self.private_key)
Chris Sosa744e1472011-09-07 19:32:50 -0700319
Chris Sosa9fba7562012-01-31 10:15:47 -0800320 if not self.vm:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700321 update_dir += '+patched_kernel'
Chris Sosa9fba7562012-01-31 10:15:47 -0800322
Gilad Arnold55a2a372012-10-02 09:46:32 -0700323 return os.path.join(CACHE_DIR, update_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800324
Don Garrettfff4c322010-11-19 13:37:12 -0800325 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800326 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700327
Chris Sosade91f672010-11-16 10:05:44 -0800328 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800329 src_image: image we are updating from (Null/empty for non-delta)
330 image_path: full path to the image.
Chris Sosa65d339b2013-01-21 18:59:21 -0800331 output_dir: the directory to write the update payloads in
332 Returns:
333 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800334 """
Chris Sosa65d339b2013-01-21 18:59:21 -0800335 update_file = None
336 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700337
Chris Sosa65d339b2013-01-21 18:59:21 -0800338 # Actually do the generation
339 _Log('Generating update for image %s' % image_path)
340 update_file = self.GenerateUpdateFile(self.src_image,
341 image_path,
342 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000343
Chris Sosa65d339b2013-01-21 18:59:21 -0800344 if update_file:
345 stateful_update_file = self.GenerateStatefulFile(image_path,
346 output_dir)
347
348 if update_file and stateful_update_file:
349 return update_file
350 else:
351 _Log('Failed to generate update.')
352 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800353
354 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
355 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000356
Chris Sosa0356d3b2010-09-16 15:46:22 -0700357 Args:
358 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700359 static_image_dir: the directory to move images to after generating.
360 Returns:
Chris Sosa65d339b2013-01-21 18:59:21 -0800361 update filename (not directory) relative to static_image_dir on success,
362 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700363 """
Chris Sosa65d339b2013-01-21 18:59:21 -0800364 _Log('Generating update for src %s image %s' % (self.src_image, image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700365
Chris Sosa417e55d2011-01-25 16:40:48 -0800366 # If it was pregenerated_path, don't regenerate
367 if self.pregenerated_path:
368 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800369
Don Garrettf90edf02010-11-16 17:36:14 -0800370 # Which sub_dir of static_image_dir should hold our cached update image
371 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
Chris Sosa65d339b2013-01-21 18:59:21 -0800372 _Log('Caching in sub_dir "%s"' % cache_sub_dir)
373
374 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
Chris Sosa417e55d2011-01-25 16:40:48 -0800375
Don Garrettf90edf02010-11-16 17:36:14 -0800376 # The cached payloads exist in a cache dir
377 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa65d339b2013-01-21 18:59:21 -0800378 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800379 cache_stateful_payload = os.path.join(static_image_dir,
Chris Sosa65d339b2013-01-21 18:59:21 -0800380 cache_sub_dir,
381 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800382
Chris Sosa417e55d2011-01-25 16:40:48 -0800383 # Check to see if this cache directory is valid.
384 if not os.path.exists(cache_update_payload) or not os.path.exists(
385 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800386 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa65d339b2013-01-21 18:59:21 -0800387 # Clean up stale state.
388 os.system('rm -rf "%s"' % full_cache_dir)
389 os.makedirs(full_cache_dir)
390 return_path = self.GenerateUpdateImage(image_path,
391 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800392
Chris Sosa65d339b2013-01-21 18:59:21 -0800393 # Clean up cache dir since it's not valid.
394 if not return_path:
395 os.system('rm -rf "%s"' % full_cache_dir)
396 return None
397
398 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800399
Chris Sosa08d55a22011-01-19 16:08:02 -0800400 # Generation complete, copy if requested.
401 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800402 # The final results exist directly in static
403 update_payload = os.path.join(static_image_dir,
404 UPDATE_FILE)
405 stateful_payload = os.path.join(static_image_dir,
406 STATEFUL_FILE)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700407 common_util.CopyFile(cache_update_payload, update_payload)
408 common_util.CopyFile(cache_stateful_payload, stateful_payload)
Chris Sosa65d339b2013-01-21 18:59:21 -0800409 return UPDATE_FILE
Chris Sosa417e55d2011-01-25 16:40:48 -0800410 else:
411 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700412
Chris Sosa65d339b2013-01-21 18:59:21 -0800413 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800414 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700415 """Generates an update using the latest image that has been built.
416
417 This will only generate an update if the newest update is newer than that
418 on the client or client_version is 'ForcedUpdate'.
419
420 Args:
Chris Sosa65d339b2013-01-21 18:59:21 -0800421 board_id: Name of the board.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700422 client_version: Current version of the client or 'ForcedUpdate'
423 static_image_dir: the directory to move images to after generating.
424 Returns:
Chris Sosa65d339b2013-01-21 18:59:21 -0800425 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700426 """
Chris Sosa65d339b2013-01-21 18:59:21 -0800427 latest_image_dir = self._GetLatestImageDir(board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700428 latest_version = self._GetVersionFromDir(latest_image_dir)
429 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
430
Chris Sosa65d339b2013-01-21 18:59:21 -0800431 _Log('Preparing to generate update from latest built image %s.' %
432 latest_image_path)
433
Chris Sosa0356d3b2010-09-16 15:46:22 -0700434 # Check to see whether or not we should update.
435 if client_version != 'ForcedUpdate' and not self._CanUpdate(
436 client_version, latest_version):
Chris Sosa65d339b2013-01-21 18:59:21 -0800437 _Log('no update')
438 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700439
Don Garrettf90edf02010-11-16 17:36:14 -0800440 return self.GenerateUpdateImageWithCache(latest_image_path,
441 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700442
Chris Sosa65d339b2013-01-21 18:59:21 -0800443 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
444 """Imports a factory-floor server configuration file. The file should
445 be in this format:
446 config = [
447 {
448 'qual_ids': set([1, 2, 3, "x86-generic"]),
449 'factory_image': 'generic-factory.gz',
450 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
451 'release_image': 'generic-release.gz',
452 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
453 'oempartitionimg_image': 'generic-oem.gz',
454 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
455 'efipartitionimg_image': 'generic-efi.gz',
456 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
457 'stateimg_image': 'generic-state.gz',
458 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
459 'firmware_image': 'generic-firmware.gz',
460 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
461 },
462 {
463 'qual_ids': set([6]),
464 'factory_image': '6-factory.gz',
465 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
466 'release_image': '6-release.gz',
467 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
468 'oempartitionimg_image': '6-oem.gz',
469 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
470 'efipartitionimg_image': '6-efi.gz',
471 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
472 'stateimg_image': '6-state.gz',
473 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
474 'firmware_image': '6-firmware.gz',
475 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
476 },
477 ]
478 The server will look for the files by name in the static files
479 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700480
Chris Sosa65d339b2013-01-21 18:59:21 -0800481 If validate_checksums is True, validates checksums and exits. If
482 a checksum mismatch is found, it's printed to the screen.
483 """
484 f = open(filename, 'r')
485 output = {}
486 exec(f.read(), output)
487 self.factory_config = output['config']
488 success = True
489 for stanza in self.factory_config:
490 for key in stanza.copy().iterkeys():
491 suffix = '_image'
492 if key.endswith(suffix):
493 kind = key[:-len(suffix)]
494 stanza[kind + '_size'] = common_util.GetFileSize(os.path.join(
495 self.static_dir, stanza[kind + '_image']))
496 if validate_checksums:
497 factory_checksum = common_util.GetFileSha1(
498 os.path.join(self.static_dir, stanza[kind + '_image']))
499 if factory_checksum != stanza[kind + '_checksum']:
500 print ('Error: checksum mismatch for %s. Expected "%s" but file '
501 'has checksum "%s".' % (stanza[kind + '_image'],
502 stanza[kind + '_checksum'],
503 factory_checksum))
504 success = False
505
506 if validate_checksums:
507 if success is False:
508 raise AutoupdateError('Checksum mismatch in conf file.')
509
510 print 'Config file looks good.'
511
512 def GetFactoryImage(self, board_id, channel):
513 kind = channel.rsplit('-', 1)[0]
514 for stanza in self.factory_config:
515 if board_id not in stanza['qual_ids']:
516 continue
517 if kind + '_image' not in stanza:
518 break
519 return (stanza[kind + '_image'],
520 stanza[kind + '_checksum'],
521 stanza[kind + '_size'])
522 return None, None, None
523
524 def HandleFactoryRequest(self, board_id, channel, protocol):
525 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
526 if filename is None:
527 _Log('unable to find image for board %s' % board_id)
528 return autoupdate_lib.GetNoUpdateResponse(protocol)
529 url = '%s/static/%s' % (self.hostname, filename)
530 is_delta_format = self._IsDeltaFormatFile(filename)
531 _Log('returning update payload ' + url)
532 # Factory install is using memento updater which is using the sha-1 hash so
533 # setting sha-256 to an empty string.
534 return autoupdate_lib.GetUpdateResponse(checksum, '', size, url,
535 is_delta_format, protocol,
536 self.critical_update)
537
538 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
539 static_image_dir):
540 """Generates an update for non-factory image.
541
542 Returns:
543 file name relative to static_image_dir on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800544 """
Dale Curtis723ec472010-11-30 14:06:47 -0800545 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
546 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
547
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700548 if self.payload_path:
Don Garrett0c880e22010-11-17 18:13:37 -0800549 # If the forced payload is not already in our static_image_dir,
550 # copy it there.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700551 src_path = os.path.abspath(self.payload_path)
Chris Sosa65d339b2013-01-21 18:59:21 -0800552 src_stateful = os.path.join(os.path.dirname(src_path),
553 STATEFUL_FILE)
554
Don Garrettee25e552010-11-23 12:09:35 -0800555 # Only copy the files if the source directory is different from dest.
556 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700557 common_util.CopyFile(src_path, dest_path)
Don Garrettee25e552010-11-23 12:09:35 -0800558
559 # The stateful payload is optional.
560 if os.path.exists(src_stateful):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700561 common_util.CopyFile(src_stateful, dest_stateful)
Don Garrettee25e552010-11-23 12:09:35 -0800562 else:
Chris Sosa65d339b2013-01-21 18:59:21 -0800563 _Log('WARN: %s not found. Expected for dev and test builds.' %
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700564 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800565 if os.path.exists(dest_stateful):
566 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800567
Chris Sosa65d339b2013-01-21 18:59:21 -0800568 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800569 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800570 return self.GenerateUpdateImageWithCache(
571 self.forced_image,
572 static_image_dir=static_image_dir)
Chris Sosa65d339b2013-01-21 18:59:21 -0800573 elif self.serve_only:
574 # Warn if update or stateful files can't be found.
575 if not os.path.exists(dest_path):
576 _Log('WARN: %s not found. Expected for dev and test builds.' %
577 UPDATE_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800578
Chris Sosa65d339b2013-01-21 18:59:21 -0800579 if not os.path.exists(dest_stateful):
580 _Log('WARN: %s not found. Expected for dev and test builds.' %
581 STATEFUL_FILE)
582
583 return UPDATE_FILE
584 else:
585 if board_id:
586 return self.GenerateLatestUpdateImage(board_id,
587 client_version,
588 static_image_dir)
589
590 _Log('Failed to genereate update. '
591 'You must set --board when pre-generating latest update.')
592 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700593
594 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800595 """Pre-generates an update and prints out the relative path it.
596
Chris Sosa65d339b2013-01-21 18:59:21 -0800597 Returns relative path of the update on success.
Chris Sosa8dd80092012-12-10 13:39:11 -0800598 """
Chris Sosa65d339b2013-01-21 18:59:21 -0800599 # Does not work with factory config.
600 assert(not self.factory_config)
601 _Log('Pre-generating the update payload.')
Chris Sosa8dd80092012-12-10 13:39:11 -0800602 # Does not work with labels so just use static dir.
Chris Sosa65d339b2013-01-21 18:59:21 -0800603 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
604 self.board, '0.0.0.0', self.static_dir)
605 if pregenerated_update:
606 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
607
Chris Sosa417e55d2011-01-25 16:40:48 -0800608 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700609
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700610 def _GetRemotePayloadAttrs(self, url):
611 """Returns hashes, size and delta flag of a remote update payload.
612
613 Obtain attributes of a payload file available on a remote devserver. This
614 is based on the assumption that the payload URL uses the /static prefix. We
615 need to make sure that both clients (requests) and remote devserver
616 (provisioning) preserve this invariant.
617
618 Args:
619 url: URL of statically staged remote file (http://host:port/static/...)
620 Returns:
621 A tuple containing the SHA1, SHA256, file size and whether or not it's a
622 delta payload (Boolean).
623 """
624 if self._PAYLOAD_URL_PREFIX not in url:
625 raise AutoupdateError(
626 'Payload URL does not have the expected prefix (%s)' %
627 self._PAYLOAD_URL_PREFIX)
628 fileinfo_url = url.replace(self._PAYLOAD_URL_PREFIX,
629 self._FILEINFO_URL_PREFIX)
Chris Sosa65d339b2013-01-21 18:59:21 -0800630 _Log('retrieving file info for remote payload via %s' % fileinfo_url)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700631 try:
632 conn = urllib2.urlopen(fileinfo_url)
Chris Sosa65d339b2013-01-21 18:59:21 -0800633 file_attr_dict = json.loads(conn.read())
634 sha1 = file_attr_dict['sha1']
635 sha256 = file_attr_dict['sha256']
636 size = file_attr_dict['size']
637 except Exception, e:
638 _Log('failed to obtain remote payload info: %s' % str(e))
639 raise
640 is_delta_format = ('_mton' in url) or ('_nton' in url)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700641
Chris Sosa65d339b2013-01-21 18:59:21 -0800642 return sha1, sha256, size, is_delta_format
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700643
Chris Sosa65d339b2013-01-21 18:59:21 -0800644 def _GetLocalPayloadAttrs(self, static_image_dir, payload_path):
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700645 """Returns hashes, size and delta flag of a local update payload.
646
647 Args:
Chris Sosa65d339b2013-01-21 18:59:21 -0800648 static_image_dir: directory where static files are being staged
649 payload_path: path to the payload file inside the static directory
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700650 Returns:
651 A tuple containing the SHA1, SHA256, file size and whether or not it's a
652 delta payload (Boolean).
653 """
Chris Sosa65d339b2013-01-21 18:59:21 -0800654 filename = os.path.join(static_image_dir, payload_path)
655 sha1 = common_util.GetFileSha1(filename)
656 sha256 = common_util.GetFileSha256(filename)
657 size = common_util.GetFileSize(filename)
658 is_delta_format = self._IsDeltaFormatFile(filename)
659 return sha1, sha256, size, is_delta_format
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700660
Chris Sosa65d339b2013-01-21 18:59:21 -0800661 def HandleUpdatePing(self, data, label=None):
662 """Handles an update ping from an update client.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700663
Chris Sosa65d339b2013-01-21 18:59:21 -0800664 Args:
665 data: xml blob from client.
666 label: optional label for the update.
667 Returns:
668 Update payload message for client.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700669 """
Chris Sosa65d339b2013-01-21 18:59:21 -0800670 # Set hostname as the hostname that the client is calling to and set up
671 # the url base. If behind apache mod_proxy | mod_rewrite, the hostname will
672 # be in X-Forwarded-Host.
673 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
674 if x_forwarded_host:
675 self.hostname = 'http://' + x_forwarded_host
676 else:
677 self.hostname = cherrypy.request.base
678
679 if self.urlbase:
680 static_urlbase = self.urlbase
681 elif self.serve_only:
682 static_urlbase = '%s/static/archive' % self.hostname
683 else:
684 static_urlbase = '%s/static' % self.hostname
685
686 # If we have a proxy port, adjust the URL we instruct the client to
687 # use to go through the proxy.
688 if self.proxy_port:
689 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
690
691 _Log('Using static url base %s' % static_urlbase)
692 _Log('Handling update ping as %s: %s' % (self.hostname, data))
693
694 protocol, app, event, update_check = autoupdate_lib.ParseUpdateRequest(data)
695 _Log('Client is using protocol version: %s' % protocol)
Jay Srinivasanac69d262012-10-30 19:05:53 -0700696
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700697 # Determine request IP, strip any IPv6 data for simplicity.
698 client_ip = cherrypy.request.remote.ip.split(':')[-1]
Chris Sosa65d339b2013-01-21 18:59:21 -0800699
Gilad Arnold286a0062012-01-12 13:47:02 -0800700 # Obtain (or init) info object for this client.
701 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
702
Chris Sosa65d339b2013-01-21 18:59:21 -0800703 # Initialize an empty dictionary for event attributes.
704 log_message = {}
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700705
Chris Sosa65d339b2013-01-21 18:59:21 -0800706 # Store event details in the host info dictionary for API usage.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700707 if event:
Gilad Arnold286a0062012-01-12 13:47:02 -0800708 event_result = int(event[0].getAttribute('eventresult'))
709 event_type = int(event[0].getAttribute('eventtype'))
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700710 client_previous_version = (event[0].getAttribute('previousversion')
711 if event[0].hasAttribute('previousversion')
712 else None)
Gilad Arnold286a0062012-01-12 13:47:02 -0800713 # Store attributes to legacy host info structure
714 curr_host_info.attrs['last_event_status'] = event_result
715 curr_host_info.attrs['last_event_type'] = event_type
716 # Add attributes to log message
717 log_message['event_result'] = event_result
718 log_message['event_type'] = event_type
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700719 if client_previous_version is not None:
720 log_message['previous_version'] = client_previous_version
Gilad Arnold286a0062012-01-12 13:47:02 -0800721
Chris Sosa65d339b2013-01-21 18:59:21 -0800722 # Get information about the requester.
723 if app:
724 client_version = app.getAttribute('version')
725 channel = app.getAttribute('track')
726 board_id = (app.hasAttribute('board') and app.getAttribute('board')
727 or self._GetDefaultBoardID())
728 # Add attributes to log message
729 log_message['version'] = client_version
730 log_message['track'] = channel
731 log_message['board'] = board_id
732
Gilad Arnold8318eac2012-10-04 12:52:23 -0700733 # Log host event, if so instructed.
734 if self.host_log:
735 curr_host_info.AddLogEntry(log_message)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700736
Chris Sosa65d339b2013-01-21 18:59:21 -0800737 # We only generate update payloads for updatecheck requests.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700738 if not update_check:
Chris Sosa65d339b2013-01-21 18:59:21 -0800739 _Log('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700740 # TODO(sosa): Generate correct non-updatecheck payload to better test
741 # update clients.
Chris Sosa52148582012-11-15 15:35:58 -0800742 return autoupdate_lib.GetNoUpdateResponse(protocol)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700743
Chris Sosa65d339b2013-01-21 18:59:21 -0800744 # Store version for this host in the cache.
745 curr_host_info.attrs['last_known_version'] = client_version
746
747 # If maximum number of updates already requested, refuse.
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700748 if self.max_updates > 0:
749 self.max_updates -= 1
750 elif self.max_updates == 0:
Chris Sosa52148582012-11-15 15:35:58 -0800751 return autoupdate_lib.GetNoUpdateResponse(protocol)
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700752
Chris Sosa65d339b2013-01-21 18:59:21 -0800753 # Check if an update has been forced for this client.
754 forced_update = curr_host_info.attrs.pop('forced_update_label', None)
755 if forced_update:
756 label = forced_update
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700757
Chris Sosa65d339b2013-01-21 18:59:21 -0800758 # Separate logic as Factory requests have static url's that override
759 # other options.
760 if self.factory_config:
761 return self.HandleFactoryRequest(board_id, channel, protocol)
762 else:
763 url = ''
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700764 # Are we provisioning a remote or local payload?
765 if self.remote_payload:
766 # If no explicit label was provided, use the value of --payload.
Chris Sosa65d339b2013-01-21 18:59:21 -0800767 if not label and self.payload_path:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700768 label = self.payload_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700769
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700770 # Form the URL of the update payload. This assumes that the payload
771 # file name is a devserver constant (which currently is the case).
772 url = '/'.join(filter(None, [static_urlbase, label, UPDATE_FILE]))
Chris Sosa5d342a22010-09-28 16:54:41 -0700773
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700774 # Get remote payload attributes.
Chris Sosa65d339b2013-01-21 18:59:21 -0800775 sha1, sha256, file_size, is_delta_format = \
776 self._GetRemotePayloadAttrs(url)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700777 else:
Chris Sosa65d339b2013-01-21 18:59:21 -0800778 # Generate payload.
779 static_image_dir = os.path.join(*filter(None, [self.static_dir, label]))
780 payload_path = self.GenerateUpdatePayloadForNonFactory(
781 board_id, client_version, static_image_dir)
782 # If properly generated, obtain the payload URL and attributes.
783 if payload_path:
784 url = '/'.join(filter(None, [static_urlbase, label, payload_path]))
785 sha1, sha256, file_size, is_delta_format = \
786 self._GetLocalPayloadAttrs(static_image_dir, payload_path)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700787
Chris Sosa65d339b2013-01-21 18:59:21 -0800788 # If we end up with an actual payload path, generate a response.
789 if url:
790 _Log('Responding to client to use url %s to get image.' % url)
791 return autoupdate_lib.GetUpdateResponse(
792 sha1, sha256, file_size, url, is_delta_format, protocol,
793 self.critical_update)
794 else:
795 return autoupdate_lib.GetNoUpdateResponse(protocol)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700796
797 def HandleHostInfoPing(self, ip):
798 """Returns host info dictionary for the given IP in JSON format."""
799 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800800 if ip in self.host_infos.table:
801 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
802
803 def HandleHostLogPing(self, ip):
804 """Returns a complete log of events for host in JSON format."""
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700805 # If all events requested, return a dictionary of logs keyed by IP address.
Gilad Arnold286a0062012-01-12 13:47:02 -0800806 if ip == 'all':
807 return json.dumps(
808 dict([(key, self.host_infos.table[key].log)
809 for key in self.host_infos.table]))
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700810
811 # Otherwise we're looking for a specific IP address, so find its log.
Gilad Arnold286a0062012-01-12 13:47:02 -0800812 if ip in self.host_infos.table:
813 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700814
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700815 # If no events were logged for this IP, return an empty log.
816 return json.dumps([])
817
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700818 def HandleSetUpdatePing(self, ip, label):
819 """Sets forced_update_label for a given host."""
820 assert ip, 'No ip provided.'
821 assert label, 'No label provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800822 self.host_infos.GetInitHostInfo(ip).attrs['forced_update_label'] = label