blob: 5f34f299d9fe0b26d57731435525ed8beb57553c [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
5from xml.dom import minidom
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08006import datetime
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07007import json
rtc@google.comded22402009-10-26 22:36:21 +00008import os
Darin Petkov798fe7d2010-03-22 15:18:13 -07009import shutil
Chris Sosa05491b12010-11-08 17:14:16 -080010import subprocess
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070011import time
Gilad Arnold0c9c8602012-10-02 23:58:58 -070012import urllib2
Don Garrett0ad09372010-12-06 16:20:30 -080013import urlparse
Chris Sosa7c931362010-10-11 19:49:01 -070014
Gilad Arnoldabb352e2012-09-23 01:24:27 -070015import cherrypy
16
17from build_util import BuildObject
Gilad Arnold55a2a372012-10-02 09:46:32 -070018import common_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070019import log_util
Chris Sosa05491b12010-11-08 17:14:16 -080020
Gilad Arnoldc65330c2012-09-20 15:17:48 -070021
22# Module-local log function.
23def _Log(message, *args, **kwargs):
24 return log_util.LogWithTag('UPDATE', message, *args, **kwargs)
25
rtc@google.comded22402009-10-26 22:36:21 +000026
Chris Sosa417e55d2011-01-25 16:40:48 -080027UPDATE_FILE = 'update.gz'
28STATEFUL_FILE = 'stateful.tgz'
29CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 15:46:22 -070030
Don Garrett0ad09372010-12-06 16:20:30 -080031
Gilad Arnold0c9c8602012-10-02 23:58:58 -070032class AutoupdateError(Exception):
33 """Exception classes used by this module."""
34 pass
35
36
Don Garrett0ad09372010-12-06 16:20:30 -080037def _ChangeUrlPort(url, new_port):
38 """Return the URL passed in with a different port"""
39 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
40 host_port = netloc.split(':')
41
42 if len(host_port) == 1:
43 host_port.append(new_port)
44 else:
45 host_port[1] = new_port
46
47 print host_port
48 netloc = "%s:%s" % tuple(host_port)
49
50 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
51
52
Gilad Arnold286a0062012-01-12 13:47:02 -080053class HostInfo:
54 """Records information about an individual host.
55
56 Members:
57 attrs: Static attributes (legacy)
58 log: Complete log of recorded client entries
59 """
60
61 def __init__(self):
62 # A dictionary of current attributes pertaining to the host.
63 self.attrs = {}
64
65 # A list of pairs consisting of a timestamp and a dictionary of recorded
66 # attributes.
67 self.log = []
68
69 def __repr__(self):
70 return 'attrs=%s, log=%s' % (self.attrs, self.log)
71
72 def AddLogEntry(self, entry):
73 """Append a new log entry."""
74 # Append a timestamp.
75 assert not 'timestamp' in entry, 'Oops, timestamp field already in use'
76 entry['timestamp'] = time.strftime('%Y-%m-%d %H:%M:%S')
77 # Add entry to hosts' message log.
78 self.log.append(entry)
79
80 def SetAttr(self, attr, value):
81 """Set an attribute value."""
82 self.attrs[attr] = value
83
84 def GetAttr(self, attr):
85 """Returns the value of an attribute."""
86 if attr in self.attrs:
87 return self.attrs[attr]
88
89 def PopAttr(self, attr, default):
90 """Returns and deletes a particular attribute."""
91 return self.attrs.pop(attr, default)
92
93
94class HostInfoTable:
95 """Records information about a set of hosts who engage in update activity.
96
97 Members:
98 table: Table of information on hosts.
99 """
100
101 def __init__(self):
102 # A dictionary of host information. Keys are normally IP addresses.
103 self.table = {}
104
105 def __repr__(self):
106 return '%s' % self.table
107
108 def GetInitHostInfo(self, host_id):
109 """Return a host's info object, or create a new one if none exists."""
110 return self.table.setdefault(host_id, HostInfo())
111
112 def GetHostInfo(self, host_id):
113 """Return an info object for given host, if such exists."""
114 if host_id in self.table:
115 return self.table[host_id]
116
117
rtc@google.com64244662009-11-12 00:52:08 +0000118class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700119 """Class that contains functionality that handles Chrome OS update pings.
120
121 Members:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700122 serve_only: serve only pre-built updates. static_dir must contain
123 update.gz and stateful.tgz.
124 factory_config: path to the factory config file if handling factory
125 requests.
126 use_test_image: use chromiumos_test_image.bin rather than the standard.
127 urlbase: base URL, other than devserver, for update images.
128 forced_image: path to an image to use for all updates.
129 payload_path: path to pre-generated payload to serve.
130 src_image: if specified, creates a delta payload from this image.
131 proxy_port: port of local proxy to tell client to connect to you
132 through.
133 vm: set for VM images (doesn't patch kernel)
134 board: board for the image. Needed for pre-generating of updates.
135 copy_to_static_root: copies images generated from the cache to ~/static.
136 private_key: path to private key in PEM format.
Gilad Arnold8318eac2012-10-04 12:52:23 -0700137 critical_update: whether provisioned payload is critical.
138 remote_payload: whether provisioned payload is remotely staged.
139 max_updates: maximum number of updates we'll try to provision.
140 host_log: record full history of host update events.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700141 """
rtc@google.comded22402009-10-26 22:36:21 +0000142
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700143 _PAYLOAD_URL_PREFIX = '/static/'
144 _FILEINFO_URL_PREFIX = '/api/fileinfo/'
145
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700146 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Greg Spencerc8b59b22011-03-15 14:15:23 -0700147 factory_config_path=None,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700148 forced_image=None, payload_path=None,
149 proxy_port=None, src_image='', vm=False, board=None,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800150 copy_to_static_root=True, private_key=None,
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700151 critical_update=False, remote_payload=False, max_updates=-1,
Gilad Arnold8318eac2012-10-04 12:52:23 -0700152 host_log=False,
Chris Sosae67b78f2010-11-04 17:33:16 -0700153 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700154 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700155 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -0700156 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700157 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -0700158 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700159 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -0700160 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700161 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -0700162
Chris Sosa0356d3b2010-09-16 15:46:22 -0700163 self.forced_image = forced_image
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700164 self.payload_path = payload_path
Chris Sosa62f720b2010-10-26 21:39:48 -0700165 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -0800166 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -0700167 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -0700168 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -0800169 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-14 16:33:22 -0800170 self.private_key = private_key
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800171 self.critical_update = critical_update
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700172 self.remote_payload = remote_payload
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700173 self.max_updates=max_updates
Gilad Arnold8318eac2012-10-04 12:52:23 -0700174 self.host_log = host_log
Don Garrettfff4c322010-11-19 13:37:12 -0800175
Chris Sosa417e55d2011-01-25 16:40:48 -0800176 # Path to pre-generated file.
177 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700178
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700179 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 13:47:02 -0800180 # information about a given host. A host is identified by its IP address.
181 # The info stored for each host includes a complete log of events for this
182 # host, as well as a dictionary of current attributes derived from events.
183 self.host_infos = HostInfoTable()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700184
Chris Sosa0356d3b2010-09-16 15:46:22 -0700185 def _GetSecondsSinceMidnight(self):
186 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700187 now = time.localtime()
188 return now[3] * 3600 + now[4] * 60 + now[5]
189
Chris Sosa0356d3b2010-09-16 15:46:22 -0700190 def _GetDefaultBoardID(self):
191 """Returns the default board id stored in .default_board."""
192 board_file = '%s/.default_board' % (self.scripts_dir)
193 try:
194 return open(board_file).read()
195 except IOError:
196 return 'x86-generic'
197
198 def _GetLatestImageDir(self, board_id):
199 """Returns the latest image dir based on shell script."""
200 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
201 return os.popen(cmd).read().strip()
202
203 def _GetVersionFromDir(self, image_dir):
204 """Returns the version of the image based on the name of the directory."""
205 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700206 parts = latest_version.split('-')
207 if len(parts) == 2:
208 # Old-style, e.g. "0.15.938.2011_08_23_0941-a1".
209 # TODO(derat): Remove the code for old-style versions after 20120101.
210 return parts[0]
211 else:
212 # New-style, e.g. "R16-1102.0.2011_09_30_0806-a1".
213 return parts[1]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700214
215 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800216 """Returns true if the latest_version is greater than the client_version.
217 """
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700218 _Log('client version %s latest version %s'
219 % (client_version, latest_version))
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700220
221 client_tokens = client_version.replace('_', '').split('.')
222 # If the client has an old four-token version like "0.16.892.0", drop the
223 # first two tokens -- we use versions like "892.0.0" now.
224 # TODO(derat): Remove the code for old-style versions after 20120101.
225 if len(client_tokens) == 4:
226 client_tokens = client_tokens[2:]
227
228 latest_tokens = latest_version.replace('_', '').split('.')
229 if len(latest_tokens) == 4:
230 latest_tokens = latest_tokens[2:]
231
232 for i in range(min(len(client_tokens), len(latest_tokens))):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700233 if int(latest_tokens[i]) == int(client_tokens[i]):
234 continue
235 return int(latest_tokens[i]) > int(client_tokens[i])
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700236
237 # Favor four-token new-style versions on the server over old-style versions
238 # on the client if everything else matches.
239 return len(latest_tokens) > len(client_tokens)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700240
Chris Sosa0356d3b2010-09-16 15:46:22 -0700241 def _UnpackZip(self, image_dir):
242 """Unpacks an image.zip into a given directory."""
243 image = os.path.join(image_dir, self._GetImageName())
244 if os.path.exists(image):
245 return True
246 else:
247 # -n, never clobber an existing file, in case we get invoked
248 # simultaneously by multiple request handlers. This means that
249 # we're assuming each image.zip file lives in a versioned
250 # directory (a la Buildbot).
251 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
252
253 def _GetImageName(self):
254 """Returns the name of the image that should be used."""
255 if self.use_test_image:
256 image_name = 'chromiumos_test_image.bin'
257 else:
258 image_name = 'chromiumos_image.bin'
259 return image_name
260
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700261 def _IsDeltaFormatFile(self, filename):
262 try:
263 file_handle = open(filename, 'r')
264 delta_magic = 'CrAU'
265 magic = file_handle.read(len(delta_magic))
266 return magic == delta_magic
267 except Exception:
268 return False
269
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700270 def GetUpdatePayload(self, sha1, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700271 """Returns a payload to the client corresponding to a new update.
272
273 Args:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700274 sha1: SHA1 hash of update blob
275 sha256: SHA256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700276 size: size of update blob
277 url: where to find update blob
278 Returns:
279 Xml string to be passed back to client.
280 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700281 delta = 'false'
282 if is_delta_format:
283 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000284 payload = """<?xml version="1.0" encoding="UTF-8"?>
285 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700286 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000287 <app appid="{%s}" status="ok">
288 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700289 <updatecheck
Jay Srinivasan9a1c4572012-03-16 19:16:58 -0700290 ChromeOSVersion="9999.0.0"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700291 codebase="%s"
292 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700293 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700294 needsadmin="false"
295 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700296 IsDelta="%s"
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800297 status="ok"
298 %s/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000299 </app>
300 </gupdate>
301 """
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800302 extra_attributes = []
303 if self.critical_update:
304 # The date string looks like '20111115' (2011-11-15). As of writing,
305 # there's no particular format for the deadline value that the
306 # client expects -- it's just empty vs. non-empty.
307 date_str = datetime.date.today().strftime('%Y%m%d')
308 extra_attributes.append('deadline="%s"' % date_str)
309 xml = payload % (self._GetSecondsSinceMidnight(),
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700310 self.app_id, url, sha1, sha256, size, delta,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800311 ' '.join(extra_attributes))
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700312 _Log('Generated update payload: %s' % xml)
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800313 return xml
rtc@google.comded22402009-10-26 22:36:21 +0000314
rtc@google.com21a5ca32009-11-04 18:23:23 +0000315 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700316 """Returns a payload to the client corresponding to no update."""
Darin Petkov845f1172011-01-05 14:45:24 -0800317 payload = """<?xml version="1.0" encoding="UTF-8"?>
318 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
319 <daystart elapsed_seconds="%s"/>
320 <app appid="{%s}" status="ok">
321 <ping status="ok"/>
322 <updatecheck status="noupdate"/>
323 </app>
324 </gupdate>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000325 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700326 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000327
Don Garrettf90edf02010-11-16 17:36:14 -0800328 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700329 """Generates an update gz given a full path to an image.
330
331 Args:
332 image_path: Full path to image.
333 Returns:
334 Path to created update_payload or None on error.
335 """
Don Garrettfff4c322010-11-19 13:37:12 -0800336 update_path = os.path.join(output_dir, UPDATE_FILE)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700337 _Log('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700338
Chris Sosa0f1ec842011-02-14 16:33:22 -0800339 update_command = [
Chris Sosa5b8b5eb2012-03-27 11:15:27 -0700340 'cros_generate_update_payload',
Chris Sosa0f1ec842011-02-14 16:33:22 -0800341 '--image="%s"' % image_path,
342 '--output="%s"' % update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800343 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700344
Chris Sosa0f1ec842011-02-14 16:33:22 -0800345 if src_image: update_command.append('--src_image="%s"' % src_image)
346 if not self.vm: update_command.append('--patch_kernel')
347 if self.private_key: update_command.append('--private_key="%s"' %
348 self.private_key)
349
350 update_string = ' '.join(update_command)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700351 _Log('Running ' + update_string)
Chris Sosa0f1ec842011-02-14 16:33:22 -0800352 if os.system(update_string) != 0:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700353 _Log('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700354 return None
355
Don Garrettfff4c322010-11-19 13:37:12 -0800356 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700357
Don Garrettf90edf02010-11-16 17:36:14 -0800358 def GenerateStatefulFile(self, image_path, output_dir):
359 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700360
361 Args:
362 image_path: Full path to image.
363 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800364 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800365 Raises:
366 A subprocess exception if the update generator fails to generate a
367 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700368 """
Don Garrettfff4c322010-11-19 13:37:12 -0800369 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800370 subprocess.check_call(
Chris Sosa5b8b5eb2012-03-27 11:15:27 -0700371 ['cros_generate_stateful_update_payload',
Chris Sosa908fd6f2010-11-10 17:31:18 -0800372 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800373 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800374 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800375 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700376
Don Garrettf90edf02010-11-16 17:36:14 -0800377 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
378 """Find directory to store a cached update.
379
Gilad Arnold55a2a372012-10-02 09:46:32 -0700380 Given one, or two images for an update, this finds which cache directory
381 should hold the update files, even if they don't exist yet.
Don Garrettf90edf02010-11-16 17:36:14 -0800382
Gilad Arnold55a2a372012-10-02 09:46:32 -0700383 Returns:
384 A directory path for storing a cached update, of the following form:
385 Non-delta updates:
386 CACHE_DIR/<dest_hash>
387 Delta updates:
388 CACHE_DIR/<src_hash>_<dest_hash>
389 Signed updates (self.private_key):
390 CACHE_DIR/<src_hash>_<dest_hash>+<private_key_hash>
Chris Sosa744e1472011-09-07 19:32:50 -0700391 """
Gilad Arnold55a2a372012-10-02 09:46:32 -0700392 update_dir = ''
Chris Sosa744e1472011-09-07 19:32:50 -0700393 if src_image:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700394 update_dir += common_util.GetFileMd5(src_image) + '_'
Don Garrettf90edf02010-11-16 17:36:14 -0800395
Gilad Arnold55a2a372012-10-02 09:46:32 -0700396 update_dir += common_util.GetFileMd5(dest_image)
Chris Sosa744e1472011-09-07 19:32:50 -0700397 if self.private_key:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700398 update_dir += '+' + common_util.GetFileMd5(self.private_key)
Chris Sosa744e1472011-09-07 19:32:50 -0700399
Chris Sosa9fba7562012-01-31 10:15:47 -0800400 if not self.vm:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700401 update_dir += '+patched_kernel'
Chris Sosa9fba7562012-01-31 10:15:47 -0800402
Gilad Arnold55a2a372012-10-02 09:46:32 -0700403 return os.path.join(CACHE_DIR, update_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800404
Don Garrettfff4c322010-11-19 13:37:12 -0800405 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800406 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700407
Chris Sosade91f672010-11-16 10:05:44 -0800408 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800409 src_image: image we are updating from (Null/empty for non-delta)
410 image_path: full path to the image.
411 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800412 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800413 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800414 """
Don Garrettf90edf02010-11-16 17:36:14 -0800415 update_file = None
416 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700417
Don Garrettf90edf02010-11-16 17:36:14 -0800418 # Actually do the generation
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700419 _Log('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800420 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800421 image_path,
422 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000423
Don Garrettf90edf02010-11-16 17:36:14 -0800424 if update_file:
425 stateful_update_file = self.GenerateStatefulFile(image_path,
426 output_dir)
427
428 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800429 return update_file
Chris Sosa417e55d2011-01-25 16:40:48 -0800430 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700431 _Log('Failed to generate update.')
Chris Sosa417e55d2011-01-25 16:40:48 -0800432 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800433
434 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
435 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000436
Chris Sosa0356d3b2010-09-16 15:46:22 -0700437 Args:
438 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700439 static_image_dir: the directory to move images to after generating.
440 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800441 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-25 16:40:48 -0800442 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700443 """
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700444 _Log('Generating update for src %s image %s' % (self.src_image, image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700445
Chris Sosa417e55d2011-01-25 16:40:48 -0800446 # If it was pregenerated_path, don't regenerate
447 if self.pregenerated_path:
448 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800449
Don Garrettf90edf02010-11-16 17:36:14 -0800450 # Which sub_dir of static_image_dir should hold our cached update image
451 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700452 _Log('Caching in sub_dir "%s"' % cache_sub_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800453
Chris Sosa417e55d2011-01-25 16:40:48 -0800454 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
455
Don Garrettf90edf02010-11-16 17:36:14 -0800456 # The cached payloads exist in a cache dir
457 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-25 16:40:48 -0800458 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800459 cache_stateful_payload = os.path.join(static_image_dir,
460 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800461 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800462
Chris Sosa417e55d2011-01-25 16:40:48 -0800463 # Check to see if this cache directory is valid.
464 if not os.path.exists(cache_update_payload) or not os.path.exists(
465 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800466 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800467 # Clean up stale state.
468 os.system('rm -rf "%s"' % full_cache_dir)
469 os.makedirs(full_cache_dir)
470 return_path = self.GenerateUpdateImage(image_path,
471 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800472
Chris Sosa417e55d2011-01-25 16:40:48 -0800473 # Clean up cache dir since it's not valid.
474 if not return_path:
475 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800476 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800477
478 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800479
Chris Sosa08d55a22011-01-19 16:08:02 -0800480 # Generation complete, copy if requested.
481 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800482 # The final results exist directly in static
483 update_payload = os.path.join(static_image_dir,
484 UPDATE_FILE)
485 stateful_payload = os.path.join(static_image_dir,
486 STATEFUL_FILE)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700487 common_util.CopyFile(cache_update_payload, update_payload)
488 common_util.CopyFile(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-25 16:40:48 -0800489 return UPDATE_FILE
490 else:
491 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700492
493 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800494 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700495 """Generates an update using the latest image that has been built.
496
497 This will only generate an update if the newest update is newer than that
498 on the client or client_version is 'ForcedUpdate'.
499
500 Args:
501 board_id: Name of the board.
502 client_version: Current version of the client or 'ForcedUpdate'
503 static_image_dir: the directory to move images to after generating.
504 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800505 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700506 """
507 latest_image_dir = self._GetLatestImageDir(board_id)
508 latest_version = self._GetVersionFromDir(latest_image_dir)
509 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
510
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700511 _Log('Preparing to generate update from latest built image %s.' %
512 latest_image_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700513
514 # Check to see whether or not we should update.
515 if client_version != 'ForcedUpdate' and not self._CanUpdate(
516 client_version, latest_version):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700517 _Log('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800518 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700519
Don Garrettf90edf02010-11-16 17:36:14 -0800520 return self.GenerateUpdateImageWithCache(latest_image_path,
521 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700522
Andrew de los Reyes52620802010-04-12 13:40:07 -0700523 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
524 """Imports a factory-floor server configuration file. The file should
525 be in this format:
526 config = [
527 {
528 'qual_ids': set([1, 2, 3, "x86-generic"]),
529 'factory_image': 'generic-factory.gz',
530 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
531 'release_image': 'generic-release.gz',
532 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
533 'oempartitionimg_image': 'generic-oem.gz',
534 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700535 'efipartitionimg_image': 'generic-efi.gz',
536 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700537 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800538 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800539 'firmware_image': 'generic-firmware.gz',
540 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700541 },
542 {
543 'qual_ids': set([6]),
544 'factory_image': '6-factory.gz',
545 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
546 'release_image': '6-release.gz',
547 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
548 'oempartitionimg_image': '6-oem.gz',
549 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700550 'efipartitionimg_image': '6-efi.gz',
551 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700552 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800553 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800554 'firmware_image': '6-firmware.gz',
555 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700556 },
557 ]
558 The server will look for the files by name in the static files
559 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700560
Andrew de los Reyes52620802010-04-12 13:40:07 -0700561 If validate_checksums is True, validates checksums and exits. If
562 a checksum mismatch is found, it's printed to the screen.
563 """
564 f = open(filename, 'r')
565 output = {}
566 exec(f.read(), output)
567 self.factory_config = output['config']
568 success = True
569 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800570 for key in stanza.copy().iterkeys():
571 suffix = '_image'
572 if key.endswith(suffix):
573 kind = key[:-len(suffix)]
Gilad Arnold55a2a372012-10-02 09:46:32 -0700574 stanza[kind + '_size'] = common_util.GetFileSize(os.path.join(
Chris Sosa0356d3b2010-09-16 15:46:22 -0700575 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800576 if validate_checksums:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700577 factory_checksum = common_util.GetFileSha1(
578 os.path.join(self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800579 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700580 print ('Error: checksum mismatch for %s. Expected "%s" but file '
581 'has checksum "%s".' % (stanza[kind + '_image'],
582 stanza[kind + '_checksum'],
583 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800584 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700585
Andrew de los Reyes52620802010-04-12 13:40:07 -0700586 if validate_checksums:
587 if success is False:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700588 raise AutoupdateError('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700589
Andrew de los Reyes52620802010-04-12 13:40:07 -0700590 print 'Config file looks good.'
591
592 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700593 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700594 for stanza in self.factory_config:
595 if board_id not in stanza['qual_ids']:
596 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700597 if kind + '_image' not in stanza:
598 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700599 return (stanza[kind + '_image'],
600 stanza[kind + '_checksum'],
601 stanza[kind + '_size'])
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700602 return None, None, None
rtc@google.comded22402009-10-26 22:36:21 +0000603
Chris Sosa7c931362010-10-11 19:49:01 -0700604 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700605 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
606 if filename is None:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700607 _Log('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700608 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700609 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700610 is_delta_format = self._IsDeltaFormatFile(filename)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700611 _Log('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700612 # Factory install is using memento updater which is using the sha-1 hash so
613 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700614 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700615
Chris Sosa151643e2010-10-28 14:40:57 -0700616 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
617 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800618 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800619
Don Garrettf90edf02010-11-16 17:36:14 -0800620 Returns:
621 file name relative to static_image_dir on success.
622 """
Dale Curtis723ec472010-11-30 14:06:47 -0800623 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
624 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
625
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700626 if self.payload_path:
Don Garrett0c880e22010-11-17 18:13:37 -0800627 # If the forced payload is not already in our static_image_dir,
628 # copy it there.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700629 src_path = os.path.abspath(self.payload_path)
Don Garrettee25e552010-11-23 12:09:35 -0800630 src_stateful = os.path.join(os.path.dirname(src_path),
631 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800632
633 # Only copy the files if the source directory is different from dest.
634 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700635 common_util.CopyFile(src_path, dest_path)
Don Garrettee25e552010-11-23 12:09:35 -0800636
637 # The stateful payload is optional.
638 if os.path.exists(src_stateful):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700639 common_util.CopyFile(src_stateful, dest_stateful)
Don Garrettee25e552010-11-23 12:09:35 -0800640 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700641 _Log('WARN: %s not found. Expected for dev and test builds.' %
642 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800643 if os.path.exists(dest_stateful):
644 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800645
Don Garrettfff4c322010-11-19 13:37:12 -0800646 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800647 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800648 return self.GenerateUpdateImageWithCache(
649 self.forced_image,
650 static_image_dir=static_image_dir)
651 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800652 # Warn if update or stateful files can't be found.
653 if not os.path.exists(dest_path):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700654 _Log('WARN: %s not found. Expected for dev and test builds.' %
655 UPDATE_FILE)
Dale Curtis723ec472010-11-30 14:06:47 -0800656
657 if not os.path.exists(dest_stateful):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700658 _Log('WARN: %s not found. Expected for dev and test builds.' %
659 STATEFUL_FILE)
Dale Curtis723ec472010-11-30 14:06:47 -0800660
661 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800662 else:
663 if board_id:
664 return self.GenerateLatestUpdateImage(board_id,
665 client_version,
666 static_image_dir)
667
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700668 _Log('Failed to genereate update. '
669 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800670 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700671
672 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800673 """Pre-generates an update and prints out the relative path it.
674
675 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800676 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700677 # Does not work with factory config.
678 assert(not self.factory_config)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700679 _Log('Pre-generating the update payload.')
Chris Sosa2c048f12010-10-27 16:05:27 -0700680 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-25 16:40:48 -0800681 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
682 self.board, '0.0.0.0', self.static_dir)
683 if pregenerated_update:
684 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
685
686 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700687
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700688 def _GetRemotePayloadAttrs(self, url):
689 """Returns hashes, size and delta flag of a remote update payload.
690
691 Obtain attributes of a payload file available on a remote devserver. This
692 is based on the assumption that the payload URL uses the /static prefix. We
693 need to make sure that both clients (requests) and remote devserver
694 (provisioning) preserve this invariant.
695
696 Args:
697 url: URL of statically staged remote file (http://host:port/static/...)
698 Returns:
699 A tuple containing the SHA1, SHA256, file size and whether or not it's a
700 delta payload (Boolean).
701 """
702 if self._PAYLOAD_URL_PREFIX not in url:
703 raise AutoupdateError(
704 'Payload URL does not have the expected prefix (%s)' %
705 self._PAYLOAD_URL_PREFIX)
706 fileinfo_url = url.replace(self._PAYLOAD_URL_PREFIX,
707 self._FILEINFO_URL_PREFIX)
708 _Log('retrieving file info for remote payload via %s' % fileinfo_url)
709 try:
710 conn = urllib2.urlopen(fileinfo_url)
711 file_attr_dict = json.loads(conn.read())
712 sha1 = file_attr_dict['sha1']
713 sha256 = file_attr_dict['sha256']
714 size = file_attr_dict['size']
715 except Exception, e:
716 _Log('failed to obtain remote payload info: %s' % str(e))
717 raise
718 is_delta_format = ('_mton' in url) or ('_nton' in url)
719
720 return sha1, sha256, size, is_delta_format
721
722 def _GetLocalPayloadAttrs(self, static_image_dir, payload_path):
723 """Returns hashes, size and delta flag of a local update payload.
724
725 Args:
726 static_image_dir: directory where static files are being staged
727 payload_path: path to the payload file inside the static directory
728 Returns:
729 A tuple containing the SHA1, SHA256, file size and whether or not it's a
730 delta payload (Boolean).
731 """
732 filename = os.path.join(static_image_dir, payload_path)
733 sha1 = common_util.GetFileSha1(filename)
734 sha256 = common_util.GetFileSha256(filename)
735 size = common_util.GetFileSize(filename)
736 is_delta_format = self._IsDeltaFormatFile(filename)
737 return sha1, sha256, size, is_delta_format
738
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700739 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700740 """Handles an update ping from an update client.
741
742 Args:
743 data: xml blob from client.
744 label: optional label for the update.
745 Returns:
746 Update payload message for client.
747 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700748 # Set hostname as the hostname that the client is calling to and set up
Chris Sosa28be7db2012-06-13 16:26:10 -0700749 # the url base. If behind apache mod_proxy | mod_rewrite, the hostname will
750 # be in X-Forwarded-Host.
751 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
752 if x_forwarded_host:
753 self.hostname = 'http://' + x_forwarded_host
754 else:
755 self.hostname = cherrypy.request.base
756
Chris Sosa9841e1c2010-10-14 10:51:45 -0700757 if self.urlbase:
758 static_urlbase = self.urlbase
759 elif self.serve_only:
760 static_urlbase = '%s/static/archive' % self.hostname
761 else:
762 static_urlbase = '%s/static' % self.hostname
763
Don Garrett0ad09372010-12-06 16:20:30 -0800764 # If we have a proxy port, adjust the URL we instruct the client to
765 # use to go through the proxy.
766 if self.proxy_port:
767 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
768
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700769 _Log('Using static url base %s' % static_urlbase)
770 _Log('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700771
Chris Sosa9841e1c2010-10-14 10:51:45 -0700772 update_dom = minidom.parseString(data)
773 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700774
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700775 # Determine request IP, strip any IPv6 data for simplicity.
776 client_ip = cherrypy.request.remote.ip.split(':')[-1]
777
Gilad Arnold286a0062012-01-12 13:47:02 -0800778 # Obtain (or init) info object for this client.
779 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
780
781 # Initialize an empty dictionary for event attributes.
782 log_message = {}
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700783
784 # Store event details in the host info dictionary for API usage.
785 event = root.getElementsByTagName('o:event')
786 if event:
Gilad Arnold286a0062012-01-12 13:47:02 -0800787 event_result = int(event[0].getAttribute('eventresult'))
788 event_type = int(event[0].getAttribute('eventtype'))
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700789 client_previous_version = (event[0].getAttribute('previousversion')
790 if event[0].hasAttribute('previousversion')
791 else None)
Gilad Arnold286a0062012-01-12 13:47:02 -0800792 # Store attributes to legacy host info structure
793 curr_host_info.attrs['last_event_status'] = event_result
794 curr_host_info.attrs['last_event_type'] = event_type
795 # Add attributes to log message
796 log_message['event_result'] = event_result
797 log_message['event_type'] = event_type
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700798 if client_previous_version is not None:
799 log_message['previous_version'] = client_previous_version
Gilad Arnold286a0062012-01-12 13:47:02 -0800800
801 # Get information about the requester.
802 query = root.getElementsByTagName('o:app')[0]
803 if query:
804 client_version = query.getAttribute('version')
805 channel = query.getAttribute('track')
806 board_id = (query.hasAttribute('board') and query.getAttribute('board')
807 or self._GetDefaultBoardID())
808 # Add attributes to log message
809 log_message['version'] = client_version
810 log_message['track'] = channel
811 log_message['board'] = board_id
812
Gilad Arnold8318eac2012-10-04 12:52:23 -0700813 # Log host event, if so instructed.
814 if self.host_log:
815 curr_host_info.AddLogEntry(log_message)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700816
Chris Sosa0356d3b2010-09-16 15:46:22 -0700817 # We only generate update payloads for updatecheck requests.
818 update_check = root.getElementsByTagName('o:updatecheck')
819 if not update_check:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700820 _Log('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700821 # TODO(sosa): Generate correct non-updatecheck payload to better test
822 # update clients.
823 return self.GetNoUpdatePayload()
824
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700825 # Store version for this host in the cache.
Gilad Arnold286a0062012-01-12 13:47:02 -0800826 curr_host_info.attrs['last_known_version'] = client_version
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700827
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700828 # If maximum number of updates already requested, refuse.
829 if self.max_updates > 0:
830 self.max_updates -= 1
831 elif self.max_updates == 0:
832 return self.GetNoUpdatePayload()
833
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700834 # Check if an update has been forced for this client.
Gilad Arnold286a0062012-01-12 13:47:02 -0800835 forced_update = curr_host_info.PopAttr('forced_update_label', None)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700836 if forced_update:
837 label = forced_update
838
Chris Sosa0356d3b2010-09-16 15:46:22 -0700839 # Separate logic as Factory requests have static url's that override
840 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700841 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700842 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700843 else:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700844 url = ''
845 # Are we provisioning a remote or local payload?
846 if self.remote_payload:
847 # If no explicit label was provided, use the value of --payload.
848 if not label and self.payload_path:
849 label = self.payload_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700850
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700851 # Form the URL of the update payload. This assumes that the payload
852 # file name is a devserver constant (which currently is the case).
853 url = '/'.join(filter(None, [static_urlbase, label, UPDATE_FILE]))
Chris Sosa5d342a22010-09-28 16:54:41 -0700854
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700855 # Get remote payload attributes.
856 sha1, sha256, file_size, is_delta_format = \
857 self._GetRemotePayloadAttrs(url)
858 else:
859 # Generate payload.
860 static_image_dir = os.path.join(*filter(None, [self.static_dir, label]))
861 payload_path = self.GenerateUpdatePayloadForNonFactory(
862 board_id, client_version, static_image_dir)
863 # If properly generated, obtain the payload URL and attributes.
864 if payload_path:
865 url = '/'.join(filter(None, [static_urlbase, label, payload_path]))
866 sha1, sha256, file_size, is_delta_format = \
867 self._GetLocalPayloadAttrs(static_image_dir, payload_path)
868
869 # If we end up with an actual payload path, generate a response.
870 if url:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700871 _Log('Responding to client to use url %s to get image.' % url)
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700872 return self.GetUpdatePayload(
873 sha1, sha256, file_size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700874 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700875 return self.GetNoUpdatePayload()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700876
877 def HandleHostInfoPing(self, ip):
878 """Returns host info dictionary for the given IP in JSON format."""
879 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800880 if ip in self.host_infos.table:
881 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
882
883 def HandleHostLogPing(self, ip):
884 """Returns a complete log of events for host in JSON format."""
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700885 # If all events requested, return a dictionary of logs keyed by IP address.
Gilad Arnold286a0062012-01-12 13:47:02 -0800886 if ip == 'all':
887 return json.dumps(
888 dict([(key, self.host_infos.table[key].log)
889 for key in self.host_infos.table]))
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700890
891 # Otherwise we're looking for a specific IP address, so find its log.
Gilad Arnold286a0062012-01-12 13:47:02 -0800892 if ip in self.host_infos.table:
893 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700894
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700895 # If no events were logged for this IP, return an empty log.
896 return json.dumps([])
897
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700898 def HandleSetUpdatePing(self, ip, label):
899 """Sets forced_update_label for a given host."""
900 assert ip, 'No ip provided.'
901 assert label, 'No label provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800902 self.host_infos.GetInitHostInfo(ip).attrs['forced_update_label'] = label