blob: 8e61dab2995f5bf100ff7801d3e3606ddfe58f64 [file] [log] [blame]
Darin Petkovc3fd90c2011-05-11 14:23:00 -07001# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
rtc@google.com64244662009-11-12 00:52:08 +00005from buildutil import BuildObject
rtc@google.comded22402009-10-26 22:36:21 +00006from xml.dom import minidom
Don Garrettf90edf02010-11-16 17:36:14 -08007
Chris Sosa7c931362010-10-11 19:49:01 -07008import cherrypy
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08009import datetime
Dale Curtisc9aaf3a2011-08-09 15:47:40 -070010import json
rtc@google.comded22402009-10-26 22:36:21 +000011import os
Darin Petkov798fe7d2010-03-22 15:18:13 -070012import shutil
Chris Sosa05491b12010-11-08 17:14:16 -080013import subprocess
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070014import time
Don Garrett0ad09372010-12-06 16:20:30 -080015import urlparse
Chris Sosa7c931362010-10-11 19:49:01 -070016
Chris Sosa05491b12010-11-08 17:14:16 -080017
Chris Sosa7c931362010-10-11 19:49:01 -070018def _LogMessage(message):
19 cherrypy.log(message, 'UPDATE')
rtc@google.comded22402009-10-26 22:36:21 +000020
Chris Sosa417e55d2011-01-25 16:40:48 -080021UPDATE_FILE = 'update.gz'
22STATEFUL_FILE = 'stateful.tgz'
23CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 15:46:22 -070024
Don Garrett0ad09372010-12-06 16:20:30 -080025
26def _ChangeUrlPort(url, new_port):
27 """Return the URL passed in with a different port"""
28 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
29 host_port = netloc.split(':')
30
31 if len(host_port) == 1:
32 host_port.append(new_port)
33 else:
34 host_port[1] = new_port
35
36 print host_port
37 netloc = "%s:%s" % tuple(host_port)
38
39 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
40
41
rtc@google.com64244662009-11-12 00:52:08 +000042class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -070043 """Class that contains functionality that handles Chrome OS update pings.
44
45 Members:
Dale Curtis723ec472010-11-30 14:06:47 -080046 serve_only: Serve only pre-built updates. static_dir must contain update.gz
47 and stateful.tgz.
Chris Sosa0356d3b2010-09-16 15:46:22 -070048 factory_config: Path to the factory config file if handling factory
49 requests.
50 use_test_image: Use chromiumos_test_image.bin rather than the standard.
51 static_url_base: base URL, other than devserver, for update images.
Chris Sosa0356d3b2010-09-16 15:46:22 -070052 forced_image: Path to an image to use for all updates.
Chris Sosa08d55a22011-01-19 16:08:02 -080053 forced_payload: Path to pre-generated payload to serve.
54 port: port to host devserver
55 proxy_port: port of local proxy to tell client to connect to you through.
56 src_image: If specified, creates a delta payload from this image.
57 vm: Set for VM images (doesn't patch kernel)
58 board: board for the image. Needed for pre-generating of updates.
59 copy_to_static_root: Copies images generated from the cache to
60 ~/static.
Chris Sosa0356d3b2010-09-16 15:46:22 -070061 """
rtc@google.comded22402009-10-26 22:36:21 +000062
Sean O'Connor1f7fd362010-04-07 16:34:52 -070063 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Greg Spencerc8b59b22011-03-15 14:15:23 -070064 factory_config_path=None,
Don Garrett0c880e22010-11-17 18:13:37 -080065 forced_image=None, forced_payload=None,
Don Garrett0ad09372010-12-06 16:20:30 -080066 port=8080, proxy_port=None, src_image='', vm=False, board=None,
Chris Sosa0f1ec842011-02-14 16:33:22 -080067 copy_to_static_root=True, private_key=None,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -080068 critical_update=False,
Chris Sosae67b78f2010-11-04 17:33:16 -070069 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070070 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070071 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -070072 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -070073 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -070074 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -070075 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -070076 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -070077 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -070078
Chris Sosa0356d3b2010-09-16 15:46:22 -070079 self.forced_image = forced_image
Don Garrett0c880e22010-11-17 18:13:37 -080080 self.forced_payload = forced_payload
Chris Sosa62f720b2010-10-26 21:39:48 -070081 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -080082 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -070083 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -070084 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -080085 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-14 16:33:22 -080086 self.private_key = private_key
Satoru Takabayashid733cbe2011-11-15 09:36:32 -080087 self.critical_update = critical_update
Don Garrettfff4c322010-11-19 13:37:12 -080088
Chris Sosa417e55d2011-01-25 16:40:48 -080089 # Path to pre-generated file.
90 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070091
Dale Curtisc9aaf3a2011-08-09 15:47:40 -070092 # Initialize empty host info cache. Used to keep track of various bits of
93 # information about a given host.
94 self.host_info = {}
95
Chris Sosa0356d3b2010-09-16 15:46:22 -070096 def _GetSecondsSinceMidnight(self):
97 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070098 now = time.localtime()
99 return now[3] * 3600 + now[4] * 60 + now[5]
100
Chris Sosa0356d3b2010-09-16 15:46:22 -0700101 def _GetDefaultBoardID(self):
102 """Returns the default board id stored in .default_board."""
103 board_file = '%s/.default_board' % (self.scripts_dir)
104 try:
105 return open(board_file).read()
106 except IOError:
107 return 'x86-generic'
108
109 def _GetLatestImageDir(self, board_id):
110 """Returns the latest image dir based on shell script."""
111 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
112 return os.popen(cmd).read().strip()
113
114 def _GetVersionFromDir(self, image_dir):
115 """Returns the version of the image based on the name of the directory."""
116 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700117 parts = latest_version.split('-')
118 if len(parts) == 2:
119 # Old-style, e.g. "0.15.938.2011_08_23_0941-a1".
120 # TODO(derat): Remove the code for old-style versions after 20120101.
121 return parts[0]
122 else:
123 # New-style, e.g. "R16-1102.0.2011_09_30_0806-a1".
124 return parts[1]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700125
126 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800127 """Returns true if the latest_version is greater than the client_version.
128 """
Chris Sosa7c931362010-10-11 19:49:01 -0700129 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -0800130 % (client_version, latest_version))
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700131
132 client_tokens = client_version.replace('_', '').split('.')
133 # If the client has an old four-token version like "0.16.892.0", drop the
134 # first two tokens -- we use versions like "892.0.0" now.
135 # TODO(derat): Remove the code for old-style versions after 20120101.
136 if len(client_tokens) == 4:
137 client_tokens = client_tokens[2:]
138
139 latest_tokens = latest_version.replace('_', '').split('.')
140 if len(latest_tokens) == 4:
141 latest_tokens = latest_tokens[2:]
142
143 for i in range(min(len(client_tokens), len(latest_tokens))):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700144 if int(latest_tokens[i]) == int(client_tokens[i]):
145 continue
146 return int(latest_tokens[i]) > int(client_tokens[i])
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700147
148 # Favor four-token new-style versions on the server over old-style versions
149 # on the client if everything else matches.
150 return len(latest_tokens) > len(client_tokens)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700151
Chris Sosa0356d3b2010-09-16 15:46:22 -0700152 def _UnpackZip(self, image_dir):
153 """Unpacks an image.zip into a given directory."""
154 image = os.path.join(image_dir, self._GetImageName())
155 if os.path.exists(image):
156 return True
157 else:
158 # -n, never clobber an existing file, in case we get invoked
159 # simultaneously by multiple request handlers. This means that
160 # we're assuming each image.zip file lives in a versioned
161 # directory (a la Buildbot).
162 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
163
164 def _GetImageName(self):
165 """Returns the name of the image that should be used."""
166 if self.use_test_image:
167 image_name = 'chromiumos_test_image.bin'
168 else:
169 image_name = 'chromiumos_image.bin'
170 return image_name
171
Chris Sosa0356d3b2010-09-16 15:46:22 -0700172 def _GetSize(self, update_path):
173 """Returns the size of the file given."""
174 return os.path.getsize(update_path)
175
176 def _GetHash(self, update_path):
177 """Returns the sha1 of the file given."""
178 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
179 % update_path)
180 return os.popen(cmd).read().rstrip()
181
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700182 def _IsDeltaFormatFile(self, filename):
183 try:
184 file_handle = open(filename, 'r')
185 delta_magic = 'CrAU'
186 magic = file_handle.read(len(delta_magic))
187 return magic == delta_magic
188 except Exception:
189 return False
190
Darin Petkov91436cb2010-09-28 08:52:17 -0700191 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
192 # it takes advantage of reduced I/O and multiple processors. Something like:
193 # % tee < FILE > /dev/null \
194 # >( openssl dgst -sha256 -binary | openssl base64 ) \
195 # >( openssl sha1 -binary | openssl base64 )
196 def _GetSHA256(self, update_path):
197 """Returns the sha256 of the file given."""
198 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
199 update_path)
200 return os.popen(cmd).read().rstrip()
201
Don Garrettf90edf02010-11-16 17:36:14 -0800202 def _GetMd5(self, update_path):
203 """Returns the md5 checksum of the file given."""
204 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
205 return os.popen(cmd).read().rstrip()
206
Don Garrett0c880e22010-11-17 18:13:37 -0800207 def _Copy(self, source, dest):
208 """Copies a file from dest to source (if different)"""
209 _LogMessage('Copy File %s -> %s' % (source, dest))
210 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800211 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800212 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800213
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700214 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700215 """Returns a payload to the client corresponding to a new update.
216
217 Args:
218 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700219 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700220 size: size of update blob
221 url: where to find update blob
222 Returns:
223 Xml string to be passed back to client.
224 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700225 delta = 'false'
226 if is_delta_format:
227 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000228 payload = """<?xml version="1.0" encoding="UTF-8"?>
229 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700230 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000231 <app appid="{%s}" status="ok">
232 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700233 <updatecheck
234 codebase="%s"
235 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700236 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700237 needsadmin="false"
238 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700239 IsDelta="%s"
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800240 status="ok"
241 %s/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000242 </app>
243 </gupdate>
244 """
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800245 extra_attributes = []
246 if self.critical_update:
247 # The date string looks like '20111115' (2011-11-15). As of writing,
248 # there's no particular format for the deadline value that the
249 # client expects -- it's just empty vs. non-empty.
250 date_str = datetime.date.today().strftime('%Y%m%d')
251 extra_attributes.append('deadline="%s"' % date_str)
252 xml = payload % (self._GetSecondsSinceMidnight(),
253 self.app_id, url, hash, sha256, size, delta,
254 ' '.join(extra_attributes))
255 _LogMessage('Generated update payload: %s' % xml)
256 return xml
rtc@google.comded22402009-10-26 22:36:21 +0000257
rtc@google.com21a5ca32009-11-04 18:23:23 +0000258 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700259 """Returns a payload to the client corresponding to no update."""
Darin Petkov845f1172011-01-05 14:45:24 -0800260 payload = """<?xml version="1.0" encoding="UTF-8"?>
261 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
262 <daystart elapsed_seconds="%s"/>
263 <app appid="{%s}" status="ok">
264 <ping status="ok"/>
265 <updatecheck status="noupdate"/>
266 </app>
267 </gupdate>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000268 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700269 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000270
Don Garrettf90edf02010-11-16 17:36:14 -0800271 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700272 """Generates an update gz given a full path to an image.
273
274 Args:
275 image_path: Full path to image.
276 Returns:
277 Path to created update_payload or None on error.
278 """
Don Garrettfff4c322010-11-19 13:37:12 -0800279 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa7c931362010-10-11 19:49:01 -0700280 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700281
Chris Sosa0f1ec842011-02-14 16:33:22 -0800282 update_command = [
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100283 '%s/cros_generate_update_payload' % self.devserver_dir,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800284 '--image="%s"' % image_path,
285 '--output="%s"' % update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800286 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700287
Chris Sosa0f1ec842011-02-14 16:33:22 -0800288 if src_image: update_command.append('--src_image="%s"' % src_image)
289 if not self.vm: update_command.append('--patch_kernel')
290 if self.private_key: update_command.append('--private_key="%s"' %
291 self.private_key)
292
293 update_string = ' '.join(update_command)
294 _LogMessage('Running ' + update_string)
295 if os.system(update_string) != 0:
Chris Sosa417e55d2011-01-25 16:40:48 -0800296 _LogMessage('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700297 return None
298
Don Garrettfff4c322010-11-19 13:37:12 -0800299 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700300
Don Garrettf90edf02010-11-16 17:36:14 -0800301 def GenerateStatefulFile(self, image_path, output_dir):
302 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700303
304 Args:
305 image_path: Full path to image.
306 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800307 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800308 Raises:
309 A subprocess exception if the update generator fails to generate a
310 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700311 """
Don Garrettfff4c322010-11-19 13:37:12 -0800312 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800313 subprocess.check_call(
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100314 ['%s/cros_generate_stateful_update_payload' % self.devserver_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800315 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800316 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800317 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800318 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700319
Don Garrettf90edf02010-11-16 17:36:14 -0800320 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
321 """Find directory to store a cached update.
322
Chris Sosa744e1472011-09-07 19:32:50 -0700323 Given one, or two images for an update, this finds which
324 cache directory should hold the update files, even if they don't exist
325 yet. The directory will be inside static_image_dir, and of the form:
Don Garrettf90edf02010-11-16 17:36:14 -0800326
Chris Sosa744e1472011-09-07 19:32:50 -0700327 Non-delta updates:
328 CACHE_DIR/12345678
329 Delta updates:
330 CACHE_DIR/12345678_12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800331
Chris Sosa744e1472011-09-07 19:32:50 -0700332 If self.private_key -- Signed updates:
333 CACHE_DIR/from_above+12345678
334 """
335 sub_dir = self._GetMd5(dest_image)
336 if src_image:
337 sub_dir = '%s_%s' % (self._GetMd5(src_image), sub_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800338
Chris Sosa744e1472011-09-07 19:32:50 -0700339 if self.private_key:
340 sub_dir = '%s+%s' % (sub_dir, self._GetMd5(self.private_key))
341
342 return os.path.join(CACHE_DIR, sub_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800343
Don Garrettfff4c322010-11-19 13:37:12 -0800344 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800345 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700346
Chris Sosade91f672010-11-16 10:05:44 -0800347 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800348 src_image: image we are updating from (Null/empty for non-delta)
349 image_path: full path to the image.
350 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800351 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800352 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800353 """
Don Garrettf90edf02010-11-16 17:36:14 -0800354 update_file = None
355 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700356
Don Garrettf90edf02010-11-16 17:36:14 -0800357 # Actually do the generation
358 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800359 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800360 image_path,
361 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000362
Don Garrettf90edf02010-11-16 17:36:14 -0800363 if update_file:
364 stateful_update_file = self.GenerateStatefulFile(image_path,
365 output_dir)
366
367 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800368 return update_file
Chris Sosa417e55d2011-01-25 16:40:48 -0800369 else:
370 _LogMessage('Failed to generate update.')
371 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800372
373 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
374 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000375
Chris Sosa0356d3b2010-09-16 15:46:22 -0700376 Args:
377 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700378 static_image_dir: the directory to move images to after generating.
379 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800380 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-25 16:40:48 -0800381 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700382 """
Don Garrettf90edf02010-11-16 17:36:14 -0800383 _LogMessage('Generating update for src %s image %s' % (self.src_image,
384 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700385
Chris Sosa417e55d2011-01-25 16:40:48 -0800386 # If it was pregenerated_path, don't regenerate
387 if self.pregenerated_path:
388 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800389
Don Garrettf90edf02010-11-16 17:36:14 -0800390 # Which sub_dir of static_image_dir should hold our cached update image
391 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
392 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
393
Chris Sosa417e55d2011-01-25 16:40:48 -0800394 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
395
Don Garrettf90edf02010-11-16 17:36:14 -0800396 # The cached payloads exist in a cache dir
397 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-25 16:40:48 -0800398 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800399 cache_stateful_payload = os.path.join(static_image_dir,
400 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800401 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800402
Chris Sosa417e55d2011-01-25 16:40:48 -0800403 # Check to see if this cache directory is valid.
404 if not os.path.exists(cache_update_payload) or not os.path.exists(
405 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800406 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800407 # Clean up stale state.
408 os.system('rm -rf "%s"' % full_cache_dir)
409 os.makedirs(full_cache_dir)
410 return_path = self.GenerateUpdateImage(image_path,
411 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800412
Chris Sosa417e55d2011-01-25 16:40:48 -0800413 # Clean up cache dir since it's not valid.
414 if not return_path:
415 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800416 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800417
418 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800419
Chris Sosa08d55a22011-01-19 16:08:02 -0800420 # Generation complete, copy if requested.
421 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800422 # The final results exist directly in static
423 update_payload = os.path.join(static_image_dir,
424 UPDATE_FILE)
425 stateful_payload = os.path.join(static_image_dir,
426 STATEFUL_FILE)
Chris Sosa08d55a22011-01-19 16:08:02 -0800427 self._Copy(cache_update_payload, update_payload)
428 self._Copy(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-25 16:40:48 -0800429 return UPDATE_FILE
430 else:
431 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700432
433 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800434 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700435 """Generates an update using the latest image that has been built.
436
437 This will only generate an update if the newest update is newer than that
438 on the client or client_version is 'ForcedUpdate'.
439
440 Args:
441 board_id: Name of the board.
442 client_version: Current version of the client or 'ForcedUpdate'
443 static_image_dir: the directory to move images to after generating.
444 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800445 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700446 """
447 latest_image_dir = self._GetLatestImageDir(board_id)
448 latest_version = self._GetVersionFromDir(latest_image_dir)
449 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
450
Chris Sosa7c931362010-10-11 19:49:01 -0700451 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700452 latest_image_path)
453
454 # Check to see whether or not we should update.
455 if client_version != 'ForcedUpdate' and not self._CanUpdate(
456 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700457 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800458 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700459
Don Garrettf90edf02010-11-16 17:36:14 -0800460 return self.GenerateUpdateImageWithCache(latest_image_path,
461 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700462
Andrew de los Reyes52620802010-04-12 13:40:07 -0700463 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
464 """Imports a factory-floor server configuration file. The file should
465 be in this format:
466 config = [
467 {
468 'qual_ids': set([1, 2, 3, "x86-generic"]),
469 'factory_image': 'generic-factory.gz',
470 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
471 'release_image': 'generic-release.gz',
472 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
473 'oempartitionimg_image': 'generic-oem.gz',
474 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700475 'efipartitionimg_image': 'generic-efi.gz',
476 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700477 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800478 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800479 'firmware_image': 'generic-firmware.gz',
480 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700481 },
482 {
483 'qual_ids': set([6]),
484 'factory_image': '6-factory.gz',
485 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
486 'release_image': '6-release.gz',
487 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
488 'oempartitionimg_image': '6-oem.gz',
489 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700490 'efipartitionimg_image': '6-efi.gz',
491 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700492 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800493 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800494 'firmware_image': '6-firmware.gz',
495 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700496 },
497 ]
498 The server will look for the files by name in the static files
499 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700500
Andrew de los Reyes52620802010-04-12 13:40:07 -0700501 If validate_checksums is True, validates checksums and exits. If
502 a checksum mismatch is found, it's printed to the screen.
503 """
504 f = open(filename, 'r')
505 output = {}
506 exec(f.read(), output)
507 self.factory_config = output['config']
508 success = True
509 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800510 for key in stanza.copy().iterkeys():
511 suffix = '_image'
512 if key.endswith(suffix):
513 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700514 stanza[kind + '_size'] = self._GetSize(os.path.join(
515 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800516 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700517 factory_checksum = self._GetHash(os.path.join(self.static_dir,
518 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800519 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700520 print ('Error: checksum mismatch for %s. Expected "%s" but file '
521 'has checksum "%s".' % (stanza[kind + '_image'],
522 stanza[kind + '_checksum'],
523 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800524 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700525
Andrew de los Reyes52620802010-04-12 13:40:07 -0700526 if validate_checksums:
527 if success is False:
528 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700529
Andrew de los Reyes52620802010-04-12 13:40:07 -0700530 print 'Config file looks good.'
531
532 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700533 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700534 for stanza in self.factory_config:
535 if board_id not in stanza['qual_ids']:
536 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700537 if kind + '_image' not in stanza:
538 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700539 return (stanza[kind + '_image'],
540 stanza[kind + '_checksum'],
541 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700542 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000543
Chris Sosa7c931362010-10-11 19:49:01 -0700544 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700545 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
546 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700547 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700548 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700549 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700550 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700551 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700552 # Factory install is using memento updater which is using the sha-1 hash so
553 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700554 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700555
Chris Sosa151643e2010-10-28 14:40:57 -0700556 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
557 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800558 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800559
Don Garrettf90edf02010-11-16 17:36:14 -0800560 Returns:
561 file name relative to static_image_dir on success.
562 """
Dale Curtis723ec472010-11-30 14:06:47 -0800563 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
564 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
565
Don Garrett0c880e22010-11-17 18:13:37 -0800566 if self.forced_payload:
567 # If the forced payload is not already in our static_image_dir,
568 # copy it there.
Don Garrettee25e552010-11-23 12:09:35 -0800569 src_path = os.path.abspath(self.forced_payload)
Don Garrettee25e552010-11-23 12:09:35 -0800570 src_stateful = os.path.join(os.path.dirname(src_path),
571 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800572
573 # Only copy the files if the source directory is different from dest.
574 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
575 self._Copy(src_path, dest_path)
576
577 # The stateful payload is optional.
578 if os.path.exists(src_stateful):
579 self._Copy(src_stateful, dest_stateful)
580 else:
581 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
582 STATEFUL_FILE)
583 if os.path.exists(dest_stateful):
584 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800585
Don Garrettfff4c322010-11-19 13:37:12 -0800586 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800587 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800588 return self.GenerateUpdateImageWithCache(
589 self.forced_image,
590 static_image_dir=static_image_dir)
591 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800592 # Warn if update or stateful files can't be found.
593 if not os.path.exists(dest_path):
594 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
595 UPDATE_FILE)
596
597 if not os.path.exists(dest_stateful):
598 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
599 STATEFUL_FILE)
600
601 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800602 else:
603 if board_id:
604 return self.GenerateLatestUpdateImage(board_id,
605 client_version,
606 static_image_dir)
607
Chris Sosa417e55d2011-01-25 16:40:48 -0800608 _LogMessage('Failed to genereate update. '
609 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800610 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700611
612 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800613 """Pre-generates an update and prints out the relative path it.
614
615 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800616 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700617 # Does not work with factory config.
618 assert(not self.factory_config)
619 _LogMessage('Pre-generating the update payload.')
620 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-25 16:40:48 -0800621 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
622 self.board, '0.0.0.0', self.static_dir)
623 if pregenerated_update:
624 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
625
626 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700627
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700628 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700629 """Handles an update ping from an update client.
630
631 Args:
632 data: xml blob from client.
633 label: optional label for the update.
634 Returns:
635 Update payload message for client.
636 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700637 # Set hostname as the hostname that the client is calling to and set up
638 # the url base.
639 self.hostname = cherrypy.request.base
640 if self.urlbase:
641 static_urlbase = self.urlbase
642 elif self.serve_only:
643 static_urlbase = '%s/static/archive' % self.hostname
644 else:
645 static_urlbase = '%s/static' % self.hostname
646
Don Garrett0ad09372010-12-06 16:20:30 -0800647 # If we have a proxy port, adjust the URL we instruct the client to
648 # use to go through the proxy.
649 if self.proxy_port:
650 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
651
Chris Sosa9841e1c2010-10-14 10:51:45 -0700652 _LogMessage('Using static url base %s' % static_urlbase)
653 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700654
Chris Sosa9841e1c2010-10-14 10:51:45 -0700655 update_dom = minidom.parseString(data)
656 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700657
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700658 # Determine request IP, strip any IPv6 data for simplicity.
659 client_ip = cherrypy.request.remote.ip.split(':')[-1]
660
661 # Initialize host info dictionary for this client if it doesn't exist.
662 self.host_info.setdefault(client_ip, {})
663
664 # Store event details in the host info dictionary for API usage.
665 event = root.getElementsByTagName('o:event')
666 if event:
667 self.host_info[client_ip]['last_event_status'] = (
668 int(event[0].getAttribute('eventresult')))
669 self.host_info[client_ip]['last_event_type'] = (
670 int(event[0].getAttribute('eventtype')))
671
Chris Sosa0356d3b2010-09-16 15:46:22 -0700672 # We only generate update payloads for updatecheck requests.
673 update_check = root.getElementsByTagName('o:updatecheck')
674 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700675 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700676 # TODO(sosa): Generate correct non-updatecheck payload to better test
677 # update clients.
678 return self.GetNoUpdatePayload()
679
680 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700681 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800682 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700683 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700684 board_id = (query.hasAttribute('board') and query.getAttribute('board')
685 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700686
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700687 # Store version for this host in the cache.
688 self.host_info[client_ip]['last_known_version'] = client_version
689
690 # Check if an update has been forced for this client.
691 forced_update = self.host_info[client_ip].pop('forced_update_label', None)
692 if forced_update:
693 label = forced_update
694
Chris Sosa0356d3b2010-09-16 15:46:22 -0700695 # Separate logic as Factory requests have static url's that override
696 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700697 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700698 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700699 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700700 static_image_dir = self.static_dir
701 if label:
702 static_image_dir = os.path.join(static_image_dir, label)
703
Don Garrettf90edf02010-11-16 17:36:14 -0800704 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
705 client_version,
706 static_image_dir)
707 if payload_path:
708 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700709 hash = self._GetHash(filename)
710 sha256 = self._GetSHA256(filename)
711 size = self._GetSize(filename)
712 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700713 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800714 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700715 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800716 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700717
Chris Sosa7c931362010-10-11 19:49:01 -0700718 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700719 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700720 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700721 return self.GetNoUpdatePayload()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700722
723 def HandleHostInfoPing(self, ip):
724 """Returns host info dictionary for the given IP in JSON format."""
725 assert ip, 'No ip provided.'
726 if ip in self.host_info:
727 return json.dumps(self.host_info[ip])
728
729 def HandleSetUpdatePing(self, ip, label):
730 """Sets forced_update_label for a given host."""
731 assert ip, 'No ip provided.'
732 assert label, 'No label provided.'
733 self.host_info.setdefault(ip, {})['forced_update_label'] = label