blob: cd52442669592c62634f6a0d40d3bdd6ddf43713 [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
rtc@google.com64244662009-11-12 00:52:08 +00005from buildutil import BuildObject
rtc@google.comded22402009-10-26 22:36:21 +00006from xml.dom import minidom
Don Garrettf90edf02010-11-16 17:36:14 -08007
Chris Sosa7c931362010-10-11 19:49:01 -07008import cherrypy
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08009import datetime
Dale Curtisc9aaf3a2011-08-09 15:47:40 -070010import json
rtc@google.comded22402009-10-26 22:36:21 +000011import os
Darin Petkov798fe7d2010-03-22 15:18:13 -070012import shutil
Chris Sosa05491b12010-11-08 17:14:16 -080013import subprocess
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070014import time
Don Garrett0ad09372010-12-06 16:20:30 -080015import urlparse
Chris Sosa7c931362010-10-11 19:49:01 -070016
Chris Sosa05491b12010-11-08 17:14:16 -080017
Chris Sosa7c931362010-10-11 19:49:01 -070018def _LogMessage(message):
19 cherrypy.log(message, 'UPDATE')
rtc@google.comded22402009-10-26 22:36:21 +000020
Chris Sosa417e55d2011-01-25 16:40:48 -080021UPDATE_FILE = 'update.gz'
22STATEFUL_FILE = 'stateful.tgz'
23CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 15:46:22 -070024
Don Garrett0ad09372010-12-06 16:20:30 -080025
26def _ChangeUrlPort(url, new_port):
27 """Return the URL passed in with a different port"""
28 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
29 host_port = netloc.split(':')
30
31 if len(host_port) == 1:
32 host_port.append(new_port)
33 else:
34 host_port[1] = new_port
35
36 print host_port
37 netloc = "%s:%s" % tuple(host_port)
38
39 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
40
41
Gilad Arnold286a0062012-01-12 13:47:02 -080042class HostInfo:
43 """Records information about an individual host.
44
45 Members:
46 attrs: Static attributes (legacy)
47 log: Complete log of recorded client entries
48 """
49
50 def __init__(self):
51 # A dictionary of current attributes pertaining to the host.
52 self.attrs = {}
53
54 # A list of pairs consisting of a timestamp and a dictionary of recorded
55 # attributes.
56 self.log = []
57
58 def __repr__(self):
59 return 'attrs=%s, log=%s' % (self.attrs, self.log)
60
61 def AddLogEntry(self, entry):
62 """Append a new log entry."""
63 # Append a timestamp.
64 assert not 'timestamp' in entry, 'Oops, timestamp field already in use'
65 entry['timestamp'] = time.strftime('%Y-%m-%d %H:%M:%S')
66 # Add entry to hosts' message log.
67 self.log.append(entry)
68
69 def SetAttr(self, attr, value):
70 """Set an attribute value."""
71 self.attrs[attr] = value
72
73 def GetAttr(self, attr):
74 """Returns the value of an attribute."""
75 if attr in self.attrs:
76 return self.attrs[attr]
77
78 def PopAttr(self, attr, default):
79 """Returns and deletes a particular attribute."""
80 return self.attrs.pop(attr, default)
81
82
83class HostInfoTable:
84 """Records information about a set of hosts who engage in update activity.
85
86 Members:
87 table: Table of information on hosts.
88 """
89
90 def __init__(self):
91 # A dictionary of host information. Keys are normally IP addresses.
92 self.table = {}
93
94 def __repr__(self):
95 return '%s' % self.table
96
97 def GetInitHostInfo(self, host_id):
98 """Return a host's info object, or create a new one if none exists."""
99 return self.table.setdefault(host_id, HostInfo())
100
101 def GetHostInfo(self, host_id):
102 """Return an info object for given host, if such exists."""
103 if host_id in self.table:
104 return self.table[host_id]
105
106
rtc@google.com64244662009-11-12 00:52:08 +0000107class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700108 """Class that contains functionality that handles Chrome OS update pings.
109
110 Members:
Dale Curtis723ec472010-11-30 14:06:47 -0800111 serve_only: Serve only pre-built updates. static_dir must contain update.gz
112 and stateful.tgz.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700113 factory_config: Path to the factory config file if handling factory
114 requests.
115 use_test_image: Use chromiumos_test_image.bin rather than the standard.
116 static_url_base: base URL, other than devserver, for update images.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700117 forced_image: Path to an image to use for all updates.
Chris Sosa08d55a22011-01-19 16:08:02 -0800118 forced_payload: Path to pre-generated payload to serve.
119 port: port to host devserver
120 proxy_port: port of local proxy to tell client to connect to you through.
121 src_image: If specified, creates a delta payload from this image.
122 vm: Set for VM images (doesn't patch kernel)
123 board: board for the image. Needed for pre-generating of updates.
124 copy_to_static_root: Copies images generated from the cache to
125 ~/static.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700126 """
rtc@google.comded22402009-10-26 22:36:21 +0000127
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700128 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Greg Spencerc8b59b22011-03-15 14:15:23 -0700129 factory_config_path=None,
Don Garrett0c880e22010-11-17 18:13:37 -0800130 forced_image=None, forced_payload=None,
Don Garrett0ad09372010-12-06 16:20:30 -0800131 port=8080, proxy_port=None, src_image='', vm=False, board=None,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800132 copy_to_static_root=True, private_key=None,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800133 critical_update=False,
Chris Sosae67b78f2010-11-04 17:33:16 -0700134 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700135 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700136 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -0700137 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700138 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -0700139 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700140 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -0700141 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700142 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -0700143
Chris Sosa0356d3b2010-09-16 15:46:22 -0700144 self.forced_image = forced_image
Don Garrett0c880e22010-11-17 18:13:37 -0800145 self.forced_payload = forced_payload
Chris Sosa62f720b2010-10-26 21:39:48 -0700146 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -0800147 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -0700148 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -0700149 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -0800150 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-14 16:33:22 -0800151 self.private_key = private_key
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800152 self.critical_update = critical_update
Don Garrettfff4c322010-11-19 13:37:12 -0800153
Chris Sosa417e55d2011-01-25 16:40:48 -0800154 # Path to pre-generated file.
155 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700156
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700157 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 13:47:02 -0800158 # information about a given host. A host is identified by its IP address.
159 # The info stored for each host includes a complete log of events for this
160 # host, as well as a dictionary of current attributes derived from events.
161 self.host_infos = HostInfoTable()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700162
Chris Sosa0356d3b2010-09-16 15:46:22 -0700163 def _GetSecondsSinceMidnight(self):
164 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700165 now = time.localtime()
166 return now[3] * 3600 + now[4] * 60 + now[5]
167
Chris Sosa0356d3b2010-09-16 15:46:22 -0700168 def _GetDefaultBoardID(self):
169 """Returns the default board id stored in .default_board."""
170 board_file = '%s/.default_board' % (self.scripts_dir)
171 try:
172 return open(board_file).read()
173 except IOError:
174 return 'x86-generic'
175
176 def _GetLatestImageDir(self, board_id):
177 """Returns the latest image dir based on shell script."""
178 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
179 return os.popen(cmd).read().strip()
180
181 def _GetVersionFromDir(self, image_dir):
182 """Returns the version of the image based on the name of the directory."""
183 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700184 parts = latest_version.split('-')
185 if len(parts) == 2:
186 # Old-style, e.g. "0.15.938.2011_08_23_0941-a1".
187 # TODO(derat): Remove the code for old-style versions after 20120101.
188 return parts[0]
189 else:
190 # New-style, e.g. "R16-1102.0.2011_09_30_0806-a1".
191 return parts[1]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700192
193 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800194 """Returns true if the latest_version is greater than the client_version.
195 """
Chris Sosa7c931362010-10-11 19:49:01 -0700196 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -0800197 % (client_version, latest_version))
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700198
199 client_tokens = client_version.replace('_', '').split('.')
200 # If the client has an old four-token version like "0.16.892.0", drop the
201 # first two tokens -- we use versions like "892.0.0" now.
202 # TODO(derat): Remove the code for old-style versions after 20120101.
203 if len(client_tokens) == 4:
204 client_tokens = client_tokens[2:]
205
206 latest_tokens = latest_version.replace('_', '').split('.')
207 if len(latest_tokens) == 4:
208 latest_tokens = latest_tokens[2:]
209
210 for i in range(min(len(client_tokens), len(latest_tokens))):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700211 if int(latest_tokens[i]) == int(client_tokens[i]):
212 continue
213 return int(latest_tokens[i]) > int(client_tokens[i])
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700214
215 # Favor four-token new-style versions on the server over old-style versions
216 # on the client if everything else matches.
217 return len(latest_tokens) > len(client_tokens)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700218
Chris Sosa0356d3b2010-09-16 15:46:22 -0700219 def _UnpackZip(self, image_dir):
220 """Unpacks an image.zip into a given directory."""
221 image = os.path.join(image_dir, self._GetImageName())
222 if os.path.exists(image):
223 return True
224 else:
225 # -n, never clobber an existing file, in case we get invoked
226 # simultaneously by multiple request handlers. This means that
227 # we're assuming each image.zip file lives in a versioned
228 # directory (a la Buildbot).
229 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
230
231 def _GetImageName(self):
232 """Returns the name of the image that should be used."""
233 if self.use_test_image:
234 image_name = 'chromiumos_test_image.bin'
235 else:
236 image_name = 'chromiumos_image.bin'
237 return image_name
238
Chris Sosa0356d3b2010-09-16 15:46:22 -0700239 def _GetSize(self, update_path):
240 """Returns the size of the file given."""
241 return os.path.getsize(update_path)
242
243 def _GetHash(self, update_path):
244 """Returns the sha1 of the file given."""
245 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
246 % update_path)
247 return os.popen(cmd).read().rstrip()
248
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700249 def _IsDeltaFormatFile(self, filename):
250 try:
251 file_handle = open(filename, 'r')
252 delta_magic = 'CrAU'
253 magic = file_handle.read(len(delta_magic))
254 return magic == delta_magic
255 except Exception:
256 return False
257
Darin Petkov91436cb2010-09-28 08:52:17 -0700258 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
259 # it takes advantage of reduced I/O and multiple processors. Something like:
260 # % tee < FILE > /dev/null \
261 # >( openssl dgst -sha256 -binary | openssl base64 ) \
262 # >( openssl sha1 -binary | openssl base64 )
263 def _GetSHA256(self, update_path):
264 """Returns the sha256 of the file given."""
265 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
266 update_path)
267 return os.popen(cmd).read().rstrip()
268
Don Garrettf90edf02010-11-16 17:36:14 -0800269 def _GetMd5(self, update_path):
270 """Returns the md5 checksum of the file given."""
271 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
272 return os.popen(cmd).read().rstrip()
273
Don Garrett0c880e22010-11-17 18:13:37 -0800274 def _Copy(self, source, dest):
275 """Copies a file from dest to source (if different)"""
276 _LogMessage('Copy File %s -> %s' % (source, dest))
277 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800278 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800279 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800280
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700281 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700282 """Returns a payload to the client corresponding to a new update.
283
284 Args:
285 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700286 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700287 size: size of update blob
288 url: where to find update blob
289 Returns:
290 Xml string to be passed back to client.
291 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700292 delta = 'false'
293 if is_delta_format:
294 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000295 payload = """<?xml version="1.0" encoding="UTF-8"?>
296 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700297 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000298 <app appid="{%s}" status="ok">
299 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700300 <updatecheck
301 codebase="%s"
302 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700303 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700304 needsadmin="false"
305 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700306 IsDelta="%s"
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800307 status="ok"
308 %s/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000309 </app>
310 </gupdate>
311 """
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800312 extra_attributes = []
313 if self.critical_update:
314 # The date string looks like '20111115' (2011-11-15). As of writing,
315 # there's no particular format for the deadline value that the
316 # client expects -- it's just empty vs. non-empty.
317 date_str = datetime.date.today().strftime('%Y%m%d')
318 extra_attributes.append('deadline="%s"' % date_str)
319 xml = payload % (self._GetSecondsSinceMidnight(),
320 self.app_id, url, hash, sha256, size, delta,
321 ' '.join(extra_attributes))
322 _LogMessage('Generated update payload: %s' % xml)
323 return xml
rtc@google.comded22402009-10-26 22:36:21 +0000324
rtc@google.com21a5ca32009-11-04 18:23:23 +0000325 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700326 """Returns a payload to the client corresponding to no update."""
Darin Petkov845f1172011-01-05 14:45:24 -0800327 payload = """<?xml version="1.0" encoding="UTF-8"?>
328 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
329 <daystart elapsed_seconds="%s"/>
330 <app appid="{%s}" status="ok">
331 <ping status="ok"/>
332 <updatecheck status="noupdate"/>
333 </app>
334 </gupdate>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000335 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700336 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000337
Don Garrettf90edf02010-11-16 17:36:14 -0800338 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700339 """Generates an update gz given a full path to an image.
340
341 Args:
342 image_path: Full path to image.
343 Returns:
344 Path to created update_payload or None on error.
345 """
Don Garrettfff4c322010-11-19 13:37:12 -0800346 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa7c931362010-10-11 19:49:01 -0700347 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700348
Chris Sosa0f1ec842011-02-14 16:33:22 -0800349 update_command = [
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100350 '%s/cros_generate_update_payload' % self.devserver_dir,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800351 '--image="%s"' % image_path,
352 '--output="%s"' % update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800353 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700354
Chris Sosa0f1ec842011-02-14 16:33:22 -0800355 if src_image: update_command.append('--src_image="%s"' % src_image)
356 if not self.vm: update_command.append('--patch_kernel')
357 if self.private_key: update_command.append('--private_key="%s"' %
358 self.private_key)
359
360 update_string = ' '.join(update_command)
361 _LogMessage('Running ' + update_string)
362 if os.system(update_string) != 0:
Chris Sosa417e55d2011-01-25 16:40:48 -0800363 _LogMessage('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700364 return None
365
Don Garrettfff4c322010-11-19 13:37:12 -0800366 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700367
Don Garrettf90edf02010-11-16 17:36:14 -0800368 def GenerateStatefulFile(self, image_path, output_dir):
369 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700370
371 Args:
372 image_path: Full path to image.
373 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800374 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800375 Raises:
376 A subprocess exception if the update generator fails to generate a
377 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700378 """
Don Garrettfff4c322010-11-19 13:37:12 -0800379 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800380 subprocess.check_call(
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100381 ['%s/cros_generate_stateful_update_payload' % self.devserver_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800382 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800383 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800384 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800385 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700386
Don Garrettf90edf02010-11-16 17:36:14 -0800387 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
388 """Find directory to store a cached update.
389
Chris Sosa744e1472011-09-07 19:32:50 -0700390 Given one, or two images for an update, this finds which
391 cache directory should hold the update files, even if they don't exist
392 yet. The directory will be inside static_image_dir, and of the form:
Don Garrettf90edf02010-11-16 17:36:14 -0800393
Chris Sosa744e1472011-09-07 19:32:50 -0700394 Non-delta updates:
395 CACHE_DIR/12345678
396 Delta updates:
397 CACHE_DIR/12345678_12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800398
Chris Sosa744e1472011-09-07 19:32:50 -0700399 If self.private_key -- Signed updates:
400 CACHE_DIR/from_above+12345678
401 """
402 sub_dir = self._GetMd5(dest_image)
403 if src_image:
404 sub_dir = '%s_%s' % (self._GetMd5(src_image), sub_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800405
Chris Sosa744e1472011-09-07 19:32:50 -0700406 if self.private_key:
407 sub_dir = '%s+%s' % (sub_dir, self._GetMd5(self.private_key))
408
409 return os.path.join(CACHE_DIR, sub_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800410
Don Garrettfff4c322010-11-19 13:37:12 -0800411 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800412 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700413
Chris Sosade91f672010-11-16 10:05:44 -0800414 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800415 src_image: image we are updating from (Null/empty for non-delta)
416 image_path: full path to the image.
417 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800418 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800419 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800420 """
Don Garrettf90edf02010-11-16 17:36:14 -0800421 update_file = None
422 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700423
Don Garrettf90edf02010-11-16 17:36:14 -0800424 # Actually do the generation
425 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800426 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800427 image_path,
428 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000429
Don Garrettf90edf02010-11-16 17:36:14 -0800430 if update_file:
431 stateful_update_file = self.GenerateStatefulFile(image_path,
432 output_dir)
433
434 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800435 return update_file
Chris Sosa417e55d2011-01-25 16:40:48 -0800436 else:
437 _LogMessage('Failed to generate update.')
438 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800439
440 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
441 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000442
Chris Sosa0356d3b2010-09-16 15:46:22 -0700443 Args:
444 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700445 static_image_dir: the directory to move images to after generating.
446 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800447 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-25 16:40:48 -0800448 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700449 """
Don Garrettf90edf02010-11-16 17:36:14 -0800450 _LogMessage('Generating update for src %s image %s' % (self.src_image,
451 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700452
Chris Sosa417e55d2011-01-25 16:40:48 -0800453 # If it was pregenerated_path, don't regenerate
454 if self.pregenerated_path:
455 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800456
Don Garrettf90edf02010-11-16 17:36:14 -0800457 # Which sub_dir of static_image_dir should hold our cached update image
458 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
459 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
460
Chris Sosa417e55d2011-01-25 16:40:48 -0800461 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
462
Don Garrettf90edf02010-11-16 17:36:14 -0800463 # The cached payloads exist in a cache dir
464 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-25 16:40:48 -0800465 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800466 cache_stateful_payload = os.path.join(static_image_dir,
467 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800468 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800469
Chris Sosa417e55d2011-01-25 16:40:48 -0800470 # Check to see if this cache directory is valid.
471 if not os.path.exists(cache_update_payload) or not os.path.exists(
472 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800473 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800474 # Clean up stale state.
475 os.system('rm -rf "%s"' % full_cache_dir)
476 os.makedirs(full_cache_dir)
477 return_path = self.GenerateUpdateImage(image_path,
478 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800479
Chris Sosa417e55d2011-01-25 16:40:48 -0800480 # Clean up cache dir since it's not valid.
481 if not return_path:
482 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800483 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800484
485 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800486
Chris Sosa08d55a22011-01-19 16:08:02 -0800487 # Generation complete, copy if requested.
488 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800489 # The final results exist directly in static
490 update_payload = os.path.join(static_image_dir,
491 UPDATE_FILE)
492 stateful_payload = os.path.join(static_image_dir,
493 STATEFUL_FILE)
Chris Sosa08d55a22011-01-19 16:08:02 -0800494 self._Copy(cache_update_payload, update_payload)
495 self._Copy(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-25 16:40:48 -0800496 return UPDATE_FILE
497 else:
498 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700499
500 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800501 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700502 """Generates an update using the latest image that has been built.
503
504 This will only generate an update if the newest update is newer than that
505 on the client or client_version is 'ForcedUpdate'.
506
507 Args:
508 board_id: Name of the board.
509 client_version: Current version of the client or 'ForcedUpdate'
510 static_image_dir: the directory to move images to after generating.
511 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800512 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700513 """
514 latest_image_dir = self._GetLatestImageDir(board_id)
515 latest_version = self._GetVersionFromDir(latest_image_dir)
516 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
517
Chris Sosa7c931362010-10-11 19:49:01 -0700518 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700519 latest_image_path)
520
521 # Check to see whether or not we should update.
522 if client_version != 'ForcedUpdate' and not self._CanUpdate(
523 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700524 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800525 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700526
Don Garrettf90edf02010-11-16 17:36:14 -0800527 return self.GenerateUpdateImageWithCache(latest_image_path,
528 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700529
Andrew de los Reyes52620802010-04-12 13:40:07 -0700530 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
531 """Imports a factory-floor server configuration file. The file should
532 be in this format:
533 config = [
534 {
535 'qual_ids': set([1, 2, 3, "x86-generic"]),
536 'factory_image': 'generic-factory.gz',
537 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
538 'release_image': 'generic-release.gz',
539 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
540 'oempartitionimg_image': 'generic-oem.gz',
541 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700542 'efipartitionimg_image': 'generic-efi.gz',
543 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700544 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800545 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800546 'firmware_image': 'generic-firmware.gz',
547 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700548 },
549 {
550 'qual_ids': set([6]),
551 'factory_image': '6-factory.gz',
552 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
553 'release_image': '6-release.gz',
554 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
555 'oempartitionimg_image': '6-oem.gz',
556 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700557 'efipartitionimg_image': '6-efi.gz',
558 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700559 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800560 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800561 'firmware_image': '6-firmware.gz',
562 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700563 },
564 ]
565 The server will look for the files by name in the static files
566 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700567
Andrew de los Reyes52620802010-04-12 13:40:07 -0700568 If validate_checksums is True, validates checksums and exits. If
569 a checksum mismatch is found, it's printed to the screen.
570 """
571 f = open(filename, 'r')
572 output = {}
573 exec(f.read(), output)
574 self.factory_config = output['config']
575 success = True
576 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800577 for key in stanza.copy().iterkeys():
578 suffix = '_image'
579 if key.endswith(suffix):
580 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700581 stanza[kind + '_size'] = self._GetSize(os.path.join(
582 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800583 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700584 factory_checksum = self._GetHash(os.path.join(self.static_dir,
585 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800586 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700587 print ('Error: checksum mismatch for %s. Expected "%s" but file '
588 'has checksum "%s".' % (stanza[kind + '_image'],
589 stanza[kind + '_checksum'],
590 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800591 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700592
Andrew de los Reyes52620802010-04-12 13:40:07 -0700593 if validate_checksums:
594 if success is False:
595 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700596
Andrew de los Reyes52620802010-04-12 13:40:07 -0700597 print 'Config file looks good.'
598
599 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700600 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700601 for stanza in self.factory_config:
602 if board_id not in stanza['qual_ids']:
603 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700604 if kind + '_image' not in stanza:
605 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700606 return (stanza[kind + '_image'],
607 stanza[kind + '_checksum'],
608 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700609 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000610
Chris Sosa7c931362010-10-11 19:49:01 -0700611 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700612 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
613 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700614 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700615 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700616 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700617 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700618 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700619 # Factory install is using memento updater which is using the sha-1 hash so
620 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700621 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700622
Chris Sosa151643e2010-10-28 14:40:57 -0700623 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
624 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800625 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800626
Don Garrettf90edf02010-11-16 17:36:14 -0800627 Returns:
628 file name relative to static_image_dir on success.
629 """
Dale Curtis723ec472010-11-30 14:06:47 -0800630 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
631 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
632
Don Garrett0c880e22010-11-17 18:13:37 -0800633 if self.forced_payload:
634 # If the forced payload is not already in our static_image_dir,
635 # copy it there.
Don Garrettee25e552010-11-23 12:09:35 -0800636 src_path = os.path.abspath(self.forced_payload)
Don Garrettee25e552010-11-23 12:09:35 -0800637 src_stateful = os.path.join(os.path.dirname(src_path),
638 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800639
640 # Only copy the files if the source directory is different from dest.
641 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
642 self._Copy(src_path, dest_path)
643
644 # The stateful payload is optional.
645 if os.path.exists(src_stateful):
646 self._Copy(src_stateful, dest_stateful)
647 else:
648 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
649 STATEFUL_FILE)
650 if os.path.exists(dest_stateful):
651 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800652
Don Garrettfff4c322010-11-19 13:37:12 -0800653 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800654 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800655 return self.GenerateUpdateImageWithCache(
656 self.forced_image,
657 static_image_dir=static_image_dir)
658 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800659 # Warn if update or stateful files can't be found.
660 if not os.path.exists(dest_path):
661 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
662 UPDATE_FILE)
663
664 if not os.path.exists(dest_stateful):
665 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
666 STATEFUL_FILE)
667
668 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800669 else:
670 if board_id:
671 return self.GenerateLatestUpdateImage(board_id,
672 client_version,
673 static_image_dir)
674
Chris Sosa417e55d2011-01-25 16:40:48 -0800675 _LogMessage('Failed to genereate update. '
676 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800677 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700678
679 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800680 """Pre-generates an update and prints out the relative path it.
681
682 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800683 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700684 # Does not work with factory config.
685 assert(not self.factory_config)
686 _LogMessage('Pre-generating the update payload.')
687 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-25 16:40:48 -0800688 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
689 self.board, '0.0.0.0', self.static_dir)
690 if pregenerated_update:
691 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
692
693 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700694
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700695 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700696 """Handles an update ping from an update client.
697
698 Args:
699 data: xml blob from client.
700 label: optional label for the update.
701 Returns:
702 Update payload message for client.
703 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700704 # Set hostname as the hostname that the client is calling to and set up
705 # the url base.
706 self.hostname = cherrypy.request.base
707 if self.urlbase:
708 static_urlbase = self.urlbase
709 elif self.serve_only:
710 static_urlbase = '%s/static/archive' % self.hostname
711 else:
712 static_urlbase = '%s/static' % self.hostname
713
Don Garrett0ad09372010-12-06 16:20:30 -0800714 # If we have a proxy port, adjust the URL we instruct the client to
715 # use to go through the proxy.
716 if self.proxy_port:
717 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
718
Chris Sosa9841e1c2010-10-14 10:51:45 -0700719 _LogMessage('Using static url base %s' % static_urlbase)
720 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700721
Chris Sosa9841e1c2010-10-14 10:51:45 -0700722 update_dom = minidom.parseString(data)
723 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700724
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700725 # Determine request IP, strip any IPv6 data for simplicity.
726 client_ip = cherrypy.request.remote.ip.split(':')[-1]
727
Gilad Arnold286a0062012-01-12 13:47:02 -0800728 # Obtain (or init) info object for this client.
729 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
730
731 # Initialize an empty dictionary for event attributes.
732 log_message = {}
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700733
734 # Store event details in the host info dictionary for API usage.
735 event = root.getElementsByTagName('o:event')
736 if event:
Gilad Arnold286a0062012-01-12 13:47:02 -0800737 event_result = int(event[0].getAttribute('eventresult'))
738 event_type = int(event[0].getAttribute('eventtype'))
739 # Store attributes to legacy host info structure
740 curr_host_info.attrs['last_event_status'] = event_result
741 curr_host_info.attrs['last_event_type'] = event_type
742 # Add attributes to log message
743 log_message['event_result'] = event_result
744 log_message['event_type'] = event_type
745
746 # Get information about the requester.
747 query = root.getElementsByTagName('o:app')[0]
748 if query:
749 client_version = query.getAttribute('version')
750 channel = query.getAttribute('track')
751 board_id = (query.hasAttribute('board') and query.getAttribute('board')
752 or self._GetDefaultBoardID())
753 # Add attributes to log message
754 log_message['version'] = client_version
755 log_message['track'] = channel
756 log_message['board'] = board_id
757
758 # Log client's message
759 curr_host_info.AddLogEntry(log_message)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700760
Chris Sosa0356d3b2010-09-16 15:46:22 -0700761 # We only generate update payloads for updatecheck requests.
762 update_check = root.getElementsByTagName('o:updatecheck')
763 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700764 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700765 # TODO(sosa): Generate correct non-updatecheck payload to better test
766 # update clients.
767 return self.GetNoUpdatePayload()
768
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700769 # Store version for this host in the cache.
Gilad Arnold286a0062012-01-12 13:47:02 -0800770 curr_host_info.attrs['last_known_version'] = client_version
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700771
772 # Check if an update has been forced for this client.
Gilad Arnold286a0062012-01-12 13:47:02 -0800773 forced_update = curr_host_info.PopAttr('forced_update_label', None)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700774 if forced_update:
775 label = forced_update
776
Chris Sosa0356d3b2010-09-16 15:46:22 -0700777 # Separate logic as Factory requests have static url's that override
778 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700779 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700780 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700781 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700782 static_image_dir = self.static_dir
783 if label:
784 static_image_dir = os.path.join(static_image_dir, label)
785
Don Garrettf90edf02010-11-16 17:36:14 -0800786 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
787 client_version,
788 static_image_dir)
789 if payload_path:
790 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700791 hash = self._GetHash(filename)
792 sha256 = self._GetSHA256(filename)
793 size = self._GetSize(filename)
794 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700795 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800796 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700797 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800798 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700799
Chris Sosa7c931362010-10-11 19:49:01 -0700800 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700801 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700802 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700803 return self.GetNoUpdatePayload()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700804
805 def HandleHostInfoPing(self, ip):
806 """Returns host info dictionary for the given IP in JSON format."""
807 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800808 if ip in self.host_infos.table:
809 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
810
811 def HandleHostLogPing(self, ip):
812 """Returns a complete log of events for host in JSON format."""
813 if ip == 'all':
814 return json.dumps(
815 dict([(key, self.host_infos.table[key].log)
816 for key in self.host_infos.table]))
817 if ip in self.host_infos.table:
818 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700819
820 def HandleSetUpdatePing(self, ip, label):
821 """Sets forced_update_label for a given host."""
822 assert ip, 'No ip provided.'
823 assert label, 'No label provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800824 self.host_infos.GetInitHostInfo(ip).attrs['forced_update_label'] = label