blob: 4a1e8932504c2bfe60e9134063de14372a189516 [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
Chris Sosa05491b12010-11-08 17:14:16 -08009import subprocess
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070010import time
Gilad Arnold0c9c8602012-10-02 23:58:58 -070011import urllib2
Don Garrett0ad09372010-12-06 16:20:30 -080012import urlparse
Chris Sosa7c931362010-10-11 19:49:01 -070013
Gilad Arnoldabb352e2012-09-23 01:24:27 -070014import cherrypy
15
16from build_util import BuildObject
Gilad Arnold55a2a372012-10-02 09:46:32 -070017import common_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070018import log_util
Chris Sosa05491b12010-11-08 17:14:16 -080019
Gilad Arnoldc65330c2012-09-20 15:17:48 -070020
21# Module-local log function.
22def _Log(message, *args, **kwargs):
23 return log_util.LogWithTag('UPDATE', message, *args, **kwargs)
24
rtc@google.comded22402009-10-26 22:36:21 +000025
Chris Sosa417e55d2011-01-25 16:40:48 -080026UPDATE_FILE = 'update.gz'
27STATEFUL_FILE = 'stateful.tgz'
28CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 15:46:22 -070029
Jay Srinivasanac69d262012-10-30 19:05:53 -070030# Responses for the various Omaha protocols indexed by the protocol version.
31UPDATE_RESPONSE = {}
32UPDATE_RESPONSE['2.0'] = """<?xml version="1.0" encoding="UTF-8"?>
33 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
34 <daystart elapsed_seconds="%(time_elapsed)s"/>
35 <app appid="{%(appid)s}" status="ok">
36 <ping status="ok"/>
37 <updatecheck
38 ChromeOSVersion="9999.0.0"
39 codebase="%(url)s"
40 hash="%(sha1)s"
41 sha256="%(sha256)s"
42 needsadmin="false"
43 size="%(size)s"
44 IsDelta="%(is_delta_format)s"
45 status="ok"
46 %(extra_attr)s/>
47 </app>
48 </gupdate>
49 """
50UPDATE_RESPONSE['3.0'] = """<?xml version="1.0" encoding="UTF-8"?>
51 <response protocol="3.0">
52 <daystart elapsed_seconds="%(time_elapsed)s"/>
53 <app appid="{%(appid)s}" status="ok">
54 <ping status="ok"/>
55 <updatecheck status="ok">
56 <urls>
57 <url codebase="%(codebase)s/"/>
58 </urls>
59 <manifest version="9999.0.0">
60 <packages>
61 <package hash="%(sha1)s" name="%(filename)s" size="%(size)s"
62 required="true"/>
63 </packages>
64 <actions>
65 <action event="postinstall"
66 ChromeOSVersion="9999.0.0"
67 sha256="%(sha256)s"
68 needsadmin="false"
69 IsDelta="%(is_delta_format)s"
70 %(extra_attr)s />
71 </actions>
72 </manifest>
73 </updatecheck>
74 </app>
75 </response>
76 """
77# Responses for the various Omaha protocols indexed by the protocol version
78# when there's no update to be served.
79NO_UPDATE_RESPONSE = {}
80NO_UPDATE_RESPONSE['2.0'] = """<?xml version="1.0" encoding="UTF-8"?>
81 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
82 <daystart elapsed_seconds="%(time_elapsed)s"/>
83 <app appid="{%(appid)s}" status="ok">
84 <ping status="ok"/>
85 <updatecheck status="noupdate"/>
86 </app>
87 </gupdate>
88 """
89NO_UPDATE_RESPONSE['3.0'] = """<?xml version="1.0" encoding="UTF-8"?>
90 <response" protocol="3.0">
91 <daystart elapsed_seconds="%(time_elapsed)s"/>
92 <app appid="{%(appid)s}" status="ok">
93 <ping status="ok"/>
94 <updatecheck status="noupdate"/>
95 </app>
96 </response>
97 """
Don Garrett0ad09372010-12-06 16:20:30 -080098
Gilad Arnold0c9c8602012-10-02 23:58:58 -070099class AutoupdateError(Exception):
100 """Exception classes used by this module."""
101 pass
102
103
Don Garrett0ad09372010-12-06 16:20:30 -0800104def _ChangeUrlPort(url, new_port):
105 """Return the URL passed in with a different port"""
106 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
107 host_port = netloc.split(':')
108
109 if len(host_port) == 1:
110 host_port.append(new_port)
111 else:
112 host_port[1] = new_port
113
114 print host_port
115 netloc = "%s:%s" % tuple(host_port)
116
117 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
118
119
Gilad Arnold286a0062012-01-12 13:47:02 -0800120class HostInfo:
121 """Records information about an individual host.
122
123 Members:
124 attrs: Static attributes (legacy)
125 log: Complete log of recorded client entries
126 """
127
128 def __init__(self):
129 # A dictionary of current attributes pertaining to the host.
130 self.attrs = {}
131
132 # A list of pairs consisting of a timestamp and a dictionary of recorded
133 # attributes.
134 self.log = []
135
136 def __repr__(self):
137 return 'attrs=%s, log=%s' % (self.attrs, self.log)
138
139 def AddLogEntry(self, entry):
140 """Append a new log entry."""
141 # Append a timestamp.
142 assert not 'timestamp' in entry, 'Oops, timestamp field already in use'
143 entry['timestamp'] = time.strftime('%Y-%m-%d %H:%M:%S')
144 # Add entry to hosts' message log.
145 self.log.append(entry)
146
147 def SetAttr(self, attr, value):
148 """Set an attribute value."""
149 self.attrs[attr] = value
150
151 def GetAttr(self, attr):
152 """Returns the value of an attribute."""
153 if attr in self.attrs:
154 return self.attrs[attr]
155
156 def PopAttr(self, attr, default):
157 """Returns and deletes a particular attribute."""
158 return self.attrs.pop(attr, default)
159
160
161class HostInfoTable:
162 """Records information about a set of hosts who engage in update activity.
163
164 Members:
165 table: Table of information on hosts.
166 """
167
168 def __init__(self):
169 # A dictionary of host information. Keys are normally IP addresses.
170 self.table = {}
171
172 def __repr__(self):
173 return '%s' % self.table
174
175 def GetInitHostInfo(self, host_id):
176 """Return a host's info object, or create a new one if none exists."""
177 return self.table.setdefault(host_id, HostInfo())
178
179 def GetHostInfo(self, host_id):
180 """Return an info object for given host, if such exists."""
181 if host_id in self.table:
182 return self.table[host_id]
183
184
rtc@google.com64244662009-11-12 00:52:08 +0000185class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700186 """Class that contains functionality that handles Chrome OS update pings.
187
188 Members:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700189 serve_only: serve only pre-built updates. static_dir must contain
190 update.gz and stateful.tgz.
191 factory_config: path to the factory config file if handling factory
192 requests.
193 use_test_image: use chromiumos_test_image.bin rather than the standard.
194 urlbase: base URL, other than devserver, for update images.
195 forced_image: path to an image to use for all updates.
196 payload_path: path to pre-generated payload to serve.
197 src_image: if specified, creates a delta payload from this image.
198 proxy_port: port of local proxy to tell client to connect to you
199 through.
200 vm: set for VM images (doesn't patch kernel)
201 board: board for the image. Needed for pre-generating of updates.
202 copy_to_static_root: copies images generated from the cache to ~/static.
203 private_key: path to private key in PEM format.
Gilad Arnold8318eac2012-10-04 12:52:23 -0700204 critical_update: whether provisioned payload is critical.
205 remote_payload: whether provisioned payload is remotely staged.
206 max_updates: maximum number of updates we'll try to provision.
207 host_log: record full history of host update events.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700208 """
rtc@google.comded22402009-10-26 22:36:21 +0000209
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700210 _PAYLOAD_URL_PREFIX = '/static/'
211 _FILEINFO_URL_PREFIX = '/api/fileinfo/'
212
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700213 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Greg Spencerc8b59b22011-03-15 14:15:23 -0700214 factory_config_path=None,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700215 forced_image=None, payload_path=None,
216 proxy_port=None, src_image='', vm=False, board=None,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800217 copy_to_static_root=True, private_key=None,
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700218 critical_update=False, remote_payload=False, max_updates=-1,
Gilad Arnold8318eac2012-10-04 12:52:23 -0700219 host_log=False,
Chris Sosae67b78f2010-11-04 17:33:16 -0700220 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700221 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -0700222 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -0700223 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700224 self.use_test_image = test_image
Jay Srinivasanac69d262012-10-30 19:05:53 -0700225 self.hostname = None
Chris Sosa5d342a22010-09-28 16:54:41 -0700226 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700227 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -0700228 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -0700229 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -0700230
Chris Sosa0356d3b2010-09-16 15:46:22 -0700231 self.forced_image = forced_image
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700232 self.payload_path = payload_path
Chris Sosa62f720b2010-10-26 21:39:48 -0700233 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -0800234 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -0700235 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -0700236 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -0800237 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-14 16:33:22 -0800238 self.private_key = private_key
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800239 self.critical_update = critical_update
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700240 self.remote_payload = remote_payload
Jay Srinivasanac69d262012-10-30 19:05:53 -0700241 self.max_updates = max_updates
Gilad Arnold8318eac2012-10-04 12:52:23 -0700242 self.host_log = host_log
Don Garrettfff4c322010-11-19 13:37:12 -0800243
Chris Sosa417e55d2011-01-25 16:40:48 -0800244 # Path to pre-generated file.
245 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700246
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700247 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 13:47:02 -0800248 # information about a given host. A host is identified by its IP address.
249 # The info stored for each host includes a complete log of events for this
250 # host, as well as a dictionary of current attributes derived from events.
251 self.host_infos = HostInfoTable()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700252
Chris Sosa0356d3b2010-09-16 15:46:22 -0700253 def _GetSecondsSinceMidnight(self):
254 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700255 now = time.localtime()
256 return now[3] * 3600 + now[4] * 60 + now[5]
257
Chris Sosa0356d3b2010-09-16 15:46:22 -0700258 def _GetDefaultBoardID(self):
259 """Returns the default board id stored in .default_board."""
260 board_file = '%s/.default_board' % (self.scripts_dir)
261 try:
262 return open(board_file).read()
263 except IOError:
264 return 'x86-generic'
265
266 def _GetLatestImageDir(self, board_id):
267 """Returns the latest image dir based on shell script."""
268 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
269 return os.popen(cmd).read().strip()
270
271 def _GetVersionFromDir(self, image_dir):
272 """Returns the version of the image based on the name of the directory."""
273 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700274 parts = latest_version.split('-')
275 if len(parts) == 2:
276 # Old-style, e.g. "0.15.938.2011_08_23_0941-a1".
277 # TODO(derat): Remove the code for old-style versions after 20120101.
278 return parts[0]
279 else:
280 # New-style, e.g. "R16-1102.0.2011_09_30_0806-a1".
281 return parts[1]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700282
283 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800284 """Returns true if the latest_version is greater than the client_version.
285 """
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700286 _Log('client version %s latest version %s'
287 % (client_version, latest_version))
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700288
289 client_tokens = client_version.replace('_', '').split('.')
290 # If the client has an old four-token version like "0.16.892.0", drop the
291 # first two tokens -- we use versions like "892.0.0" now.
292 # TODO(derat): Remove the code for old-style versions after 20120101.
293 if len(client_tokens) == 4:
294 client_tokens = client_tokens[2:]
295
296 latest_tokens = latest_version.replace('_', '').split('.')
297 if len(latest_tokens) == 4:
298 latest_tokens = latest_tokens[2:]
299
300 for i in range(min(len(client_tokens), len(latest_tokens))):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700301 if int(latest_tokens[i]) == int(client_tokens[i]):
302 continue
303 return int(latest_tokens[i]) > int(client_tokens[i])
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700304
305 # Favor four-token new-style versions on the server over old-style versions
306 # on the client if everything else matches.
307 return len(latest_tokens) > len(client_tokens)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700308
Chris Sosa0356d3b2010-09-16 15:46:22 -0700309 def _UnpackZip(self, image_dir):
310 """Unpacks an image.zip into a given directory."""
311 image = os.path.join(image_dir, self._GetImageName())
312 if os.path.exists(image):
313 return True
314 else:
315 # -n, never clobber an existing file, in case we get invoked
316 # simultaneously by multiple request handlers. This means that
317 # we're assuming each image.zip file lives in a versioned
318 # directory (a la Buildbot).
319 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
320
321 def _GetImageName(self):
322 """Returns the name of the image that should be used."""
323 if self.use_test_image:
324 image_name = 'chromiumos_test_image.bin'
325 else:
326 image_name = 'chromiumos_image.bin'
327 return image_name
328
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700329 def _IsDeltaFormatFile(self, filename):
330 try:
331 file_handle = open(filename, 'r')
332 delta_magic = 'CrAU'
333 magic = file_handle.read(len(delta_magic))
334 return magic == delta_magic
Jay Srinivasanac69d262012-10-30 19:05:53 -0700335 except IOError:
336 # For unit tests, we may not have real files, so it's ok to
337 # ignore these IOErrors. In any case, this value is not being
338 # used in update_engine at all as of now.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700339 return False
340
Jay Srinivasanac69d262012-10-30 19:05:53 -0700341 def GetCommonResponseValues(self):
342 """Returns a dictionary of default values for the response."""
343 response_values = {}
344 response_values['appid'] = self.app_id
345 response_values['time_elapsed'] = self._GetSecondsSinceMidnight()
346 return response_values
347
348 def GetSubstitutedResponse(self, response_dict, protocol, response_values):
349 """Substitutes the protocol-specific response with response_values.
350
351 Args:
352 response_dict: Canned response messages indexed by protocol.
353 protocol: client's protocol version from the request Xml.
354 response_values: Values to be substituted in the canned response.
355 Returns:
356 Xml string to be passed back to client.
357 Raises:
358 AutoupdateError if required response values are not present.
359 """
360 try:
361 response_xml = response_dict[protocol] % response_values
362 _Log('Generated response xml: %s' % response_xml)
363 return response_xml
364 except KeyError as e:
365 raise AutoupdateError('Missing response value/unknown protocol: %s' % e)
366
367 def GetUpdateResponse(self, sha1, sha256, size, url, is_delta_format,
368 protocol):
369 """Returns a protocol-specific response to the client for a new update.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700370
371 Args:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700372 sha1: SHA1 hash of update blob
373 sha256: SHA256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700374 size: size of update blob
375 url: where to find update blob
Jay Srinivasanac69d262012-10-30 19:05:53 -0700376 is_delta_format: true if url refers to a delta payload
377 protocol: client's protocol version from the request Xml.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700378 Returns:
379 Xml string to be passed back to client.
380 """
Jay Srinivasanac69d262012-10-30 19:05:53 -0700381 response_values = self.GetCommonResponseValues()
382 response_values['sha1'] = sha1
383 response_values['sha256'] = sha256
384 response_values['size'] = size
385 response_values['url'] = url
386 (codebase, filename) = os.path.split(url)
387 response_values['codebase'] = codebase
388 response_values['filename'] = filename
389 response_values['is_delta_format'] = is_delta_format
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800390 extra_attributes = []
391 if self.critical_update:
392 # The date string looks like '20111115' (2011-11-15). As of writing,
393 # there's no particular format for the deadline value that the
394 # client expects -- it's just empty vs. non-empty.
395 date_str = datetime.date.today().strftime('%Y%m%d')
396 extra_attributes.append('deadline="%s"' % date_str)
Jay Srinivasanac69d262012-10-30 19:05:53 -0700397 response_values['extra_attr'] = ' '.join(extra_attributes)
398 response_xml = self.GetSubstitutedResponse(UPDATE_RESPONSE, protocol,
399 response_values)
400 return response_xml
rtc@google.comded22402009-10-26 22:36:21 +0000401
Jay Srinivasanac69d262012-10-30 19:05:53 -0700402 def GetNoUpdateResponse(self, protocol):
403 """Returns a protocol-specific response to the client for no update.
404
405 Args:
406 protocol: client's protocol version from the request Xml.
407 Returns:
408 Xml string to be passed back to client.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000409 """
Jay Srinivasanac69d262012-10-30 19:05:53 -0700410 response_values = self.GetCommonResponseValues()
411 response_xml = self.GetSubstitutedResponse(NO_UPDATE_RESPONSE, protocol,
412 response_values)
413 return response_xml
rtc@google.comded22402009-10-26 22:36:21 +0000414
Don Garrettf90edf02010-11-16 17:36:14 -0800415 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700416 """Generates an update gz given a full path to an image.
417
418 Args:
419 image_path: Full path to image.
420 Returns:
421 Path to created update_payload or None on error.
422 """
Don Garrettfff4c322010-11-19 13:37:12 -0800423 update_path = os.path.join(output_dir, UPDATE_FILE)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700424 _Log('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700425
Chris Sosa0f1ec842011-02-14 16:33:22 -0800426 update_command = [
Chris Sosa5b8b5eb2012-03-27 11:15:27 -0700427 'cros_generate_update_payload',
Chris Sosa0f1ec842011-02-14 16:33:22 -0800428 '--image="%s"' % image_path,
429 '--output="%s"' % update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800430 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700431
Chris Sosa0f1ec842011-02-14 16:33:22 -0800432 if src_image: update_command.append('--src_image="%s"' % src_image)
433 if not self.vm: update_command.append('--patch_kernel')
434 if self.private_key: update_command.append('--private_key="%s"' %
435 self.private_key)
436
437 update_string = ' '.join(update_command)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700438 _Log('Running ' + update_string)
Chris Sosa0f1ec842011-02-14 16:33:22 -0800439 if os.system(update_string) != 0:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700440 _Log('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700441 return None
442
Don Garrettfff4c322010-11-19 13:37:12 -0800443 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700444
Don Garrettf90edf02010-11-16 17:36:14 -0800445 def GenerateStatefulFile(self, image_path, output_dir):
446 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700447
448 Args:
449 image_path: Full path to image.
450 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800451 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800452 Raises:
453 A subprocess exception if the update generator fails to generate a
454 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700455 """
Chris Sosa908fd6f2010-11-10 17:31:18 -0800456 subprocess.check_call(
Chris Sosa5b8b5eb2012-03-27 11:15:27 -0700457 ['cros_generate_stateful_update_payload',
Chris Sosa908fd6f2010-11-10 17:31:18 -0800458 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800459 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800460 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800461 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700462
Don Garrettf90edf02010-11-16 17:36:14 -0800463 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
464 """Find directory to store a cached update.
465
Gilad Arnold55a2a372012-10-02 09:46:32 -0700466 Given one, or two images for an update, this finds which cache directory
467 should hold the update files, even if they don't exist yet.
Don Garrettf90edf02010-11-16 17:36:14 -0800468
Gilad Arnold55a2a372012-10-02 09:46:32 -0700469 Returns:
470 A directory path for storing a cached update, of the following form:
471 Non-delta updates:
472 CACHE_DIR/<dest_hash>
473 Delta updates:
474 CACHE_DIR/<src_hash>_<dest_hash>
475 Signed updates (self.private_key):
476 CACHE_DIR/<src_hash>_<dest_hash>+<private_key_hash>
Chris Sosa744e1472011-09-07 19:32:50 -0700477 """
Gilad Arnold55a2a372012-10-02 09:46:32 -0700478 update_dir = ''
Chris Sosa744e1472011-09-07 19:32:50 -0700479 if src_image:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700480 update_dir += common_util.GetFileMd5(src_image) + '_'
Don Garrettf90edf02010-11-16 17:36:14 -0800481
Gilad Arnold55a2a372012-10-02 09:46:32 -0700482 update_dir += common_util.GetFileMd5(dest_image)
Chris Sosa744e1472011-09-07 19:32:50 -0700483 if self.private_key:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700484 update_dir += '+' + common_util.GetFileMd5(self.private_key)
Chris Sosa744e1472011-09-07 19:32:50 -0700485
Chris Sosa9fba7562012-01-31 10:15:47 -0800486 if not self.vm:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700487 update_dir += '+patched_kernel'
Chris Sosa9fba7562012-01-31 10:15:47 -0800488
Gilad Arnold55a2a372012-10-02 09:46:32 -0700489 return os.path.join(CACHE_DIR, update_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800490
Don Garrettfff4c322010-11-19 13:37:12 -0800491 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800492 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700493
Chris Sosade91f672010-11-16 10:05:44 -0800494 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800495 src_image: image we are updating from (Null/empty for non-delta)
496 image_path: full path to the image.
497 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800498 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800499 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800500 """
Don Garrettf90edf02010-11-16 17:36:14 -0800501 update_file = None
502 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700503
Don Garrettf90edf02010-11-16 17:36:14 -0800504 # Actually do the generation
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700505 _Log('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800506 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800507 image_path,
508 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000509
Don Garrettf90edf02010-11-16 17:36:14 -0800510 if update_file:
511 stateful_update_file = self.GenerateStatefulFile(image_path,
512 output_dir)
513
514 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800515 return update_file
Chris Sosa417e55d2011-01-25 16:40:48 -0800516 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700517 _Log('Failed to generate update.')
Chris Sosa417e55d2011-01-25 16:40:48 -0800518 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800519
520 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
521 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000522
Chris Sosa0356d3b2010-09-16 15:46:22 -0700523 Args:
524 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700525 static_image_dir: the directory to move images to after generating.
526 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800527 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-25 16:40:48 -0800528 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700529 """
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700530 _Log('Generating update for src %s image %s' % (self.src_image, image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700531
Chris Sosa417e55d2011-01-25 16:40:48 -0800532 # If it was pregenerated_path, don't regenerate
533 if self.pregenerated_path:
534 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800535
Don Garrettf90edf02010-11-16 17:36:14 -0800536 # Which sub_dir of static_image_dir should hold our cached update image
537 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700538 _Log('Caching in sub_dir "%s"' % cache_sub_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800539
Chris Sosa417e55d2011-01-25 16:40:48 -0800540 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
541
Don Garrettf90edf02010-11-16 17:36:14 -0800542 # The cached payloads exist in a cache dir
543 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-25 16:40:48 -0800544 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800545 cache_stateful_payload = os.path.join(static_image_dir,
546 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800547 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800548
Chris Sosa417e55d2011-01-25 16:40:48 -0800549 # Check to see if this cache directory is valid.
550 if not os.path.exists(cache_update_payload) or not os.path.exists(
551 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800552 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800553 # Clean up stale state.
554 os.system('rm -rf "%s"' % full_cache_dir)
555 os.makedirs(full_cache_dir)
556 return_path = self.GenerateUpdateImage(image_path,
557 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800558
Chris Sosa417e55d2011-01-25 16:40:48 -0800559 # Clean up cache dir since it's not valid.
560 if not return_path:
561 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800562 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800563
564 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800565
Chris Sosa08d55a22011-01-19 16:08:02 -0800566 # Generation complete, copy if requested.
567 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800568 # The final results exist directly in static
569 update_payload = os.path.join(static_image_dir,
570 UPDATE_FILE)
571 stateful_payload = os.path.join(static_image_dir,
572 STATEFUL_FILE)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700573 common_util.CopyFile(cache_update_payload, update_payload)
574 common_util.CopyFile(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-25 16:40:48 -0800575 return UPDATE_FILE
576 else:
577 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700578
579 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800580 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700581 """Generates an update using the latest image that has been built.
582
583 This will only generate an update if the newest update is newer than that
584 on the client or client_version is 'ForcedUpdate'.
585
586 Args:
587 board_id: Name of the board.
588 client_version: Current version of the client or 'ForcedUpdate'
589 static_image_dir: the directory to move images to after generating.
590 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800591 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700592 """
593 latest_image_dir = self._GetLatestImageDir(board_id)
594 latest_version = self._GetVersionFromDir(latest_image_dir)
595 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
596
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700597 _Log('Preparing to generate update from latest built image %s.' %
598 latest_image_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700599
600 # Check to see whether or not we should update.
601 if client_version != 'ForcedUpdate' and not self._CanUpdate(
602 client_version, latest_version):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700603 _Log('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800604 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700605
Don Garrettf90edf02010-11-16 17:36:14 -0800606 return self.GenerateUpdateImageWithCache(latest_image_path,
607 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700608
Andrew de los Reyes52620802010-04-12 13:40:07 -0700609 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
610 """Imports a factory-floor server configuration file. The file should
611 be in this format:
612 config = [
613 {
614 'qual_ids': set([1, 2, 3, "x86-generic"]),
615 'factory_image': 'generic-factory.gz',
616 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
617 'release_image': 'generic-release.gz',
618 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
619 'oempartitionimg_image': 'generic-oem.gz',
620 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700621 'efipartitionimg_image': 'generic-efi.gz',
622 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700623 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800624 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800625 'firmware_image': 'generic-firmware.gz',
626 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700627 },
628 {
629 'qual_ids': set([6]),
630 'factory_image': '6-factory.gz',
631 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
632 'release_image': '6-release.gz',
633 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
634 'oempartitionimg_image': '6-oem.gz',
635 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700636 'efipartitionimg_image': '6-efi.gz',
637 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700638 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800639 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800640 'firmware_image': '6-firmware.gz',
641 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700642 },
643 ]
644 The server will look for the files by name in the static files
645 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700646
Andrew de los Reyes52620802010-04-12 13:40:07 -0700647 If validate_checksums is True, validates checksums and exits. If
648 a checksum mismatch is found, it's printed to the screen.
649 """
650 f = open(filename, 'r')
651 output = {}
652 exec(f.read(), output)
653 self.factory_config = output['config']
654 success = True
655 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800656 for key in stanza.copy().iterkeys():
657 suffix = '_image'
658 if key.endswith(suffix):
659 kind = key[:-len(suffix)]
Gilad Arnold55a2a372012-10-02 09:46:32 -0700660 stanza[kind + '_size'] = common_util.GetFileSize(os.path.join(
Chris Sosa0356d3b2010-09-16 15:46:22 -0700661 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800662 if validate_checksums:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700663 factory_checksum = common_util.GetFileSha1(
664 os.path.join(self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800665 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700666 print ('Error: checksum mismatch for %s. Expected "%s" but file '
667 'has checksum "%s".' % (stanza[kind + '_image'],
668 stanza[kind + '_checksum'],
669 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800670 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700671
Andrew de los Reyes52620802010-04-12 13:40:07 -0700672 if validate_checksums:
673 if success is False:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700674 raise AutoupdateError('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700675
Andrew de los Reyes52620802010-04-12 13:40:07 -0700676 print 'Config file looks good.'
677
678 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700679 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700680 for stanza in self.factory_config:
681 if board_id not in stanza['qual_ids']:
682 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700683 if kind + '_image' not in stanza:
684 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700685 return (stanza[kind + '_image'],
686 stanza[kind + '_checksum'],
687 stanza[kind + '_size'])
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700688 return None, None, None
rtc@google.comded22402009-10-26 22:36:21 +0000689
Jay Srinivasanac69d262012-10-30 19:05:53 -0700690 def HandleFactoryRequest(self, board_id, channel, protocol):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700691 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
692 if filename is None:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700693 _Log('unable to find image for board %s' % board_id)
Jay Srinivasanac69d262012-10-30 19:05:53 -0700694 return self.GetNoUpdateResponse(protocol)
Chris Sosa05f95162010-10-14 18:01:52 -0700695 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700696 is_delta_format = self._IsDeltaFormatFile(filename)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700697 _Log('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700698 # Factory install is using memento updater which is using the sha-1 hash so
699 # setting sha-256 to an empty string.
Jay Srinivasanac69d262012-10-30 19:05:53 -0700700 return self.GetUpdateResponse(checksum, '', size, url, is_delta_format,
701 protocol)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700702
Chris Sosa151643e2010-10-28 14:40:57 -0700703 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
704 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800705 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800706
Don Garrettf90edf02010-11-16 17:36:14 -0800707 Returns:
708 file name relative to static_image_dir on success.
709 """
Dale Curtis723ec472010-11-30 14:06:47 -0800710 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
711 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
712
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700713 if self.payload_path:
Don Garrett0c880e22010-11-17 18:13:37 -0800714 # If the forced payload is not already in our static_image_dir,
715 # copy it there.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700716 src_path = os.path.abspath(self.payload_path)
Don Garrettee25e552010-11-23 12:09:35 -0800717 src_stateful = os.path.join(os.path.dirname(src_path),
718 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800719
720 # Only copy the files if the source directory is different from dest.
721 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700722 common_util.CopyFile(src_path, dest_path)
Don Garrettee25e552010-11-23 12:09:35 -0800723
724 # The stateful payload is optional.
725 if os.path.exists(src_stateful):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700726 common_util.CopyFile(src_stateful, dest_stateful)
Don Garrettee25e552010-11-23 12:09:35 -0800727 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700728 _Log('WARN: %s not found. Expected for dev and test builds.' %
729 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800730 if os.path.exists(dest_stateful):
731 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800732
Don Garrettfff4c322010-11-19 13:37:12 -0800733 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800734 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800735 return self.GenerateUpdateImageWithCache(
736 self.forced_image,
737 static_image_dir=static_image_dir)
738 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800739 # Warn if update or stateful files can't be found.
740 if not os.path.exists(dest_path):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700741 _Log('WARN: %s not found. Expected for dev and test builds.' %
742 UPDATE_FILE)
Dale Curtis723ec472010-11-30 14:06:47 -0800743
744 if not os.path.exists(dest_stateful):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700745 _Log('WARN: %s not found. Expected for dev and test builds.' %
746 STATEFUL_FILE)
Dale Curtis723ec472010-11-30 14:06:47 -0800747
748 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800749 else:
750 if board_id:
751 return self.GenerateLatestUpdateImage(board_id,
752 client_version,
753 static_image_dir)
754
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700755 _Log('Failed to genereate update. '
756 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800757 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700758
759 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800760 """Pre-generates an update and prints out the relative path it.
761
762 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800763 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700764 # Does not work with factory config.
765 assert(not self.factory_config)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700766 _Log('Pre-generating the update payload.')
Chris Sosa2c048f12010-10-27 16:05:27 -0700767 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-25 16:40:48 -0800768 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
769 self.board, '0.0.0.0', self.static_dir)
770 if pregenerated_update:
771 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
772
773 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700774
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700775 def _GetRemotePayloadAttrs(self, url):
776 """Returns hashes, size and delta flag of a remote update payload.
777
778 Obtain attributes of a payload file available on a remote devserver. This
779 is based on the assumption that the payload URL uses the /static prefix. We
780 need to make sure that both clients (requests) and remote devserver
781 (provisioning) preserve this invariant.
782
783 Args:
784 url: URL of statically staged remote file (http://host:port/static/...)
785 Returns:
786 A tuple containing the SHA1, SHA256, file size and whether or not it's a
787 delta payload (Boolean).
788 """
789 if self._PAYLOAD_URL_PREFIX not in url:
790 raise AutoupdateError(
791 'Payload URL does not have the expected prefix (%s)' %
792 self._PAYLOAD_URL_PREFIX)
793 fileinfo_url = url.replace(self._PAYLOAD_URL_PREFIX,
794 self._FILEINFO_URL_PREFIX)
795 _Log('retrieving file info for remote payload via %s' % fileinfo_url)
796 try:
797 conn = urllib2.urlopen(fileinfo_url)
798 file_attr_dict = json.loads(conn.read())
799 sha1 = file_attr_dict['sha1']
800 sha256 = file_attr_dict['sha256']
801 size = file_attr_dict['size']
802 except Exception, e:
803 _Log('failed to obtain remote payload info: %s' % str(e))
804 raise
805 is_delta_format = ('_mton' in url) or ('_nton' in url)
806
807 return sha1, sha256, size, is_delta_format
808
809 def _GetLocalPayloadAttrs(self, static_image_dir, payload_path):
810 """Returns hashes, size and delta flag of a local update payload.
811
812 Args:
813 static_image_dir: directory where static files are being staged
814 payload_path: path to the payload file inside the static directory
815 Returns:
816 A tuple containing the SHA1, SHA256, file size and whether or not it's a
817 delta payload (Boolean).
818 """
819 filename = os.path.join(static_image_dir, payload_path)
820 sha1 = common_util.GetFileSha1(filename)
821 sha256 = common_util.GetFileSha256(filename)
822 size = common_util.GetFileSize(filename)
823 is_delta_format = self._IsDeltaFormatFile(filename)
824 return sha1, sha256, size, is_delta_format
825
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700826 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700827 """Handles an update ping from an update client.
828
829 Args:
830 data: xml blob from client.
831 label: optional label for the update.
832 Returns:
833 Update payload message for client.
834 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700835 # Set hostname as the hostname that the client is calling to and set up
Chris Sosa28be7db2012-06-13 16:26:10 -0700836 # the url base. If behind apache mod_proxy | mod_rewrite, the hostname will
837 # be in X-Forwarded-Host.
838 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
839 if x_forwarded_host:
840 self.hostname = 'http://' + x_forwarded_host
841 else:
842 self.hostname = cherrypy.request.base
843
Chris Sosa9841e1c2010-10-14 10:51:45 -0700844 if self.urlbase:
845 static_urlbase = self.urlbase
846 elif self.serve_only:
847 static_urlbase = '%s/static/archive' % self.hostname
848 else:
849 static_urlbase = '%s/static' % self.hostname
850
Don Garrett0ad09372010-12-06 16:20:30 -0800851 # If we have a proxy port, adjust the URL we instruct the client to
852 # use to go through the proxy.
853 if self.proxy_port:
854 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
855
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700856 _Log('Using static url base %s' % static_urlbase)
857 _Log('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700858
Chris Sosa9841e1c2010-10-14 10:51:45 -0700859 update_dom = minidom.parseString(data)
860 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700861
Jay Srinivasanac69d262012-10-30 19:05:53 -0700862 # Create a dictionary for all strings that are dependent on the client's
863 # protocol and use the entries in the dictionary instead of hardcoding
864 # the element names in the rest of the code.
865 protocol = root.getAttribute('protocol')
866 _Log('Client is using protocol version: %s' % protocol)
867 if protocol not in ('2.0', '3.0'):
868 raise AutoupdateError('Unsupported Omaha protocol %s' % protocol)
869
870 element_dict = {}
871 for name in ['event', 'app', 'updatecheck']:
872 element_dict[name] = 'o:' + name if protocol == '2.0' else name
873
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700874 # Determine request IP, strip any IPv6 data for simplicity.
875 client_ip = cherrypy.request.remote.ip.split(':')[-1]
876
Gilad Arnold286a0062012-01-12 13:47:02 -0800877 # Obtain (or init) info object for this client.
878 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
879
880 # Initialize an empty dictionary for event attributes.
881 log_message = {}
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700882
883 # Store event details in the host info dictionary for API usage.
Jay Srinivasanac69d262012-10-30 19:05:53 -0700884 event = root.getElementsByTagName(element_dict['event'])
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700885 if event:
Gilad Arnold286a0062012-01-12 13:47:02 -0800886 event_result = int(event[0].getAttribute('eventresult'))
887 event_type = int(event[0].getAttribute('eventtype'))
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700888 client_previous_version = (event[0].getAttribute('previousversion')
889 if event[0].hasAttribute('previousversion')
890 else None)
Gilad Arnold286a0062012-01-12 13:47:02 -0800891 # Store attributes to legacy host info structure
892 curr_host_info.attrs['last_event_status'] = event_result
893 curr_host_info.attrs['last_event_type'] = event_type
894 # Add attributes to log message
895 log_message['event_result'] = event_result
896 log_message['event_type'] = event_type
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700897 if client_previous_version is not None:
898 log_message['previous_version'] = client_previous_version
Gilad Arnold286a0062012-01-12 13:47:02 -0800899
900 # Get information about the requester.
Jay Srinivasanac69d262012-10-30 19:05:53 -0700901 query = root.getElementsByTagName(element_dict['app'])[0]
Gilad Arnold286a0062012-01-12 13:47:02 -0800902 if query:
903 client_version = query.getAttribute('version')
904 channel = query.getAttribute('track')
905 board_id = (query.hasAttribute('board') and query.getAttribute('board')
906 or self._GetDefaultBoardID())
907 # Add attributes to log message
908 log_message['version'] = client_version
909 log_message['track'] = channel
910 log_message['board'] = board_id
911
Gilad Arnold8318eac2012-10-04 12:52:23 -0700912 # Log host event, if so instructed.
913 if self.host_log:
914 curr_host_info.AddLogEntry(log_message)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700915
Chris Sosa0356d3b2010-09-16 15:46:22 -0700916 # We only generate update payloads for updatecheck requests.
Jay Srinivasanac69d262012-10-30 19:05:53 -0700917 update_check = root.getElementsByTagName(element_dict['updatecheck'])
Chris Sosa0356d3b2010-09-16 15:46:22 -0700918 if not update_check:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700919 _Log('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700920 # TODO(sosa): Generate correct non-updatecheck payload to better test
921 # update clients.
Jay Srinivasanac69d262012-10-30 19:05:53 -0700922 return self.GetNoUpdateResponse(protocol)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700923
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700924 # Store version for this host in the cache.
Gilad Arnold286a0062012-01-12 13:47:02 -0800925 curr_host_info.attrs['last_known_version'] = client_version
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700926
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700927 # If maximum number of updates already requested, refuse.
928 if self.max_updates > 0:
929 self.max_updates -= 1
930 elif self.max_updates == 0:
Jay Srinivasanac69d262012-10-30 19:05:53 -0700931 return self.GetNoUpdateResponse(protocol)
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700932
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700933 # Check if an update has been forced for this client.
Gilad Arnold286a0062012-01-12 13:47:02 -0800934 forced_update = curr_host_info.PopAttr('forced_update_label', None)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700935 if forced_update:
936 label = forced_update
937
Chris Sosa0356d3b2010-09-16 15:46:22 -0700938 # Separate logic as Factory requests have static url's that override
939 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700940 if self.factory_config:
Jay Srinivasanac69d262012-10-30 19:05:53 -0700941 return self.HandleFactoryRequest(board_id, channel, protocol)
Nick Sanders723f3262010-09-16 05:18:41 -0700942 else:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700943 url = ''
944 # Are we provisioning a remote or local payload?
945 if self.remote_payload:
946 # If no explicit label was provided, use the value of --payload.
947 if not label and self.payload_path:
948 label = self.payload_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700949
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700950 # Form the URL of the update payload. This assumes that the payload
951 # file name is a devserver constant (which currently is the case).
952 url = '/'.join(filter(None, [static_urlbase, label, UPDATE_FILE]))
Chris Sosa5d342a22010-09-28 16:54:41 -0700953
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700954 # Get remote payload attributes.
955 sha1, sha256, file_size, is_delta_format = \
956 self._GetRemotePayloadAttrs(url)
957 else:
958 # Generate payload.
959 static_image_dir = os.path.join(*filter(None, [self.static_dir, label]))
960 payload_path = self.GenerateUpdatePayloadForNonFactory(
961 board_id, client_version, static_image_dir)
962 # If properly generated, obtain the payload URL and attributes.
963 if payload_path:
964 url = '/'.join(filter(None, [static_urlbase, label, payload_path]))
965 sha1, sha256, file_size, is_delta_format = \
966 self._GetLocalPayloadAttrs(static_image_dir, payload_path)
967
968 # If we end up with an actual payload path, generate a response.
969 if url:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700970 _Log('Responding to client to use url %s to get image.' % url)
Jay Srinivasanac69d262012-10-30 19:05:53 -0700971 return self.GetUpdateResponse(
972 sha1, sha256, file_size, url, is_delta_format, protocol)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700973 else:
Jay Srinivasanac69d262012-10-30 19:05:53 -0700974 return self.GetNoUpdateResponse(protocol)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700975
976 def HandleHostInfoPing(self, ip):
977 """Returns host info dictionary for the given IP in JSON format."""
978 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800979 if ip in self.host_infos.table:
980 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
981
982 def HandleHostLogPing(self, ip):
983 """Returns a complete log of events for host in JSON format."""
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700984 # If all events requested, return a dictionary of logs keyed by IP address.
Gilad Arnold286a0062012-01-12 13:47:02 -0800985 if ip == 'all':
986 return json.dumps(
987 dict([(key, self.host_infos.table[key].log)
988 for key in self.host_infos.table]))
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700989
990 # Otherwise we're looking for a specific IP address, so find its log.
Gilad Arnold286a0062012-01-12 13:47:02 -0800991 if ip in self.host_infos.table:
992 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700993
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700994 # If no events were logged for this IP, return an empty log.
995 return json.dumps([])
996
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700997 def HandleSetUpdatePing(self, ip, label):
998 """Sets forced_update_label for a given host."""
999 assert ip, 'No ip provided.'
1000 assert label, 'No label provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -08001001 self.host_infos.GetInitHostInfo(ip).attrs['forced_update_label'] = label