blob: 719d9bf8957c0fa396ed0d811cefefd7a6d4f50d [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
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07009import json
rtc@google.comded22402009-10-26 22:36:21 +000010import os
Darin Petkov798fe7d2010-03-22 15:18:13 -070011import shutil
Chris Sosa05491b12010-11-08 17:14:16 -080012import subprocess
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070013import time
Don Garrett0ad09372010-12-06 16:20:30 -080014import urlparse
Chris Sosa7c931362010-10-11 19:49:01 -070015
Chris Sosa05491b12010-11-08 17:14:16 -080016
Chris Sosa7c931362010-10-11 19:49:01 -070017def _LogMessage(message):
18 cherrypy.log(message, 'UPDATE')
rtc@google.comded22402009-10-26 22:36:21 +000019
Chris Sosa417e55d2011-01-25 16:40:48 -080020UPDATE_FILE = 'update.gz'
21STATEFUL_FILE = 'stateful.tgz'
22CACHE_DIR = 'cache'
Chris Sosa0356d3b2010-09-16 15:46:22 -070023
Don Garrett0ad09372010-12-06 16:20:30 -080024
25def _ChangeUrlPort(url, new_port):
26 """Return the URL passed in with a different port"""
27 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
28 host_port = netloc.split(':')
29
30 if len(host_port) == 1:
31 host_port.append(new_port)
32 else:
33 host_port[1] = new_port
34
35 print host_port
36 netloc = "%s:%s" % tuple(host_port)
37
38 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
39
40
rtc@google.com64244662009-11-12 00:52:08 +000041class Autoupdate(BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -070042 """Class that contains functionality that handles Chrome OS update pings.
43
44 Members:
Dale Curtis723ec472010-11-30 14:06:47 -080045 serve_only: Serve only pre-built updates. static_dir must contain update.gz
46 and stateful.tgz.
Chris Sosa0356d3b2010-09-16 15:46:22 -070047 factory_config: Path to the factory config file if handling factory
48 requests.
49 use_test_image: Use chromiumos_test_image.bin rather than the standard.
50 static_url_base: base URL, other than devserver, for update images.
Chris Sosa0356d3b2010-09-16 15:46:22 -070051 forced_image: Path to an image to use for all updates.
Chris Sosa08d55a22011-01-19 16:08:02 -080052 forced_payload: Path to pre-generated payload to serve.
53 port: port to host devserver
54 proxy_port: port of local proxy to tell client to connect to you through.
55 src_image: If specified, creates a delta payload from this image.
56 vm: Set for VM images (doesn't patch kernel)
57 board: board for the image. Needed for pre-generating of updates.
58 copy_to_static_root: Copies images generated from the cache to
59 ~/static.
Chris Sosa0356d3b2010-09-16 15:46:22 -070060 """
rtc@google.comded22402009-10-26 22:36:21 +000061
Sean O'Connor1f7fd362010-04-07 16:34:52 -070062 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Greg Spencerc8b59b22011-03-15 14:15:23 -070063 factory_config_path=None,
Don Garrett0c880e22010-11-17 18:13:37 -080064 forced_image=None, forced_payload=None,
Don Garrett0ad09372010-12-06 16:20:30 -080065 port=8080, proxy_port=None, src_image='', vm=False, board=None,
Chris Sosa0f1ec842011-02-14 16:33:22 -080066 copy_to_static_root=True, private_key=None,
Chris Sosae67b78f2010-11-04 17:33:16 -070067 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070068 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070069 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -070070 self.factory_config = factory_config_path
Chris Sosa0356d3b2010-09-16 15:46:22 -070071 self.use_test_image = test_image
Chris Sosa5d342a22010-09-28 16:54:41 -070072 if urlbase:
Chris Sosa9841e1c2010-10-14 10:51:45 -070073 self.urlbase = urlbase
Chris Sosa5d342a22010-09-28 16:54:41 -070074 else:
Chris Sosa9841e1c2010-10-14 10:51:45 -070075 self.urlbase = None
Chris Sosa5d342a22010-09-28 16:54:41 -070076
Chris Sosa0356d3b2010-09-16 15:46:22 -070077 self.forced_image = forced_image
Don Garrett0c880e22010-11-17 18:13:37 -080078 self.forced_payload = forced_payload
Chris Sosa62f720b2010-10-26 21:39:48 -070079 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -080080 self.proxy_port = proxy_port
Chris Sosa4136e692010-10-28 23:42:37 -070081 self.vm = vm
Chris Sosae67b78f2010-11-04 17:33:16 -070082 self.board = board
Chris Sosa08d55a22011-01-19 16:08:02 -080083 self.copy_to_static_root = copy_to_static_root
Chris Sosa0f1ec842011-02-14 16:33:22 -080084 self.private_key = private_key
Don Garrettfff4c322010-11-19 13:37:12 -080085
Chris Sosa417e55d2011-01-25 16:40:48 -080086 # Path to pre-generated file.
87 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070088
Dale Curtisc9aaf3a2011-08-09 15:47:40 -070089 # Initialize empty host info cache. Used to keep track of various bits of
90 # information about a given host.
91 self.host_info = {}
92
Chris Sosa0356d3b2010-09-16 15:46:22 -070093 def _GetSecondsSinceMidnight(self):
94 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070095 now = time.localtime()
96 return now[3] * 3600 + now[4] * 60 + now[5]
97
Chris Sosa0356d3b2010-09-16 15:46:22 -070098 def _GetDefaultBoardID(self):
99 """Returns the default board id stored in .default_board."""
100 board_file = '%s/.default_board' % (self.scripts_dir)
101 try:
102 return open(board_file).read()
103 except IOError:
104 return 'x86-generic'
105
106 def _GetLatestImageDir(self, board_id):
107 """Returns the latest image dir based on shell script."""
108 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
109 return os.popen(cmd).read().strip()
110
111 def _GetVersionFromDir(self, image_dir):
112 """Returns the version of the image based on the name of the directory."""
113 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700114 parts = latest_version.split('-')
115 if len(parts) == 2:
116 # Old-style, e.g. "0.15.938.2011_08_23_0941-a1".
117 # TODO(derat): Remove the code for old-style versions after 20120101.
118 return parts[0]
119 else:
120 # New-style, e.g. "R16-1102.0.2011_09_30_0806-a1".
121 return parts[1]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700122
123 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800124 """Returns true if the latest_version is greater than the client_version.
125 """
Chris Sosa7c931362010-10-11 19:49:01 -0700126 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -0800127 % (client_version, latest_version))
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700128
129 client_tokens = client_version.replace('_', '').split('.')
130 # If the client has an old four-token version like "0.16.892.0", drop the
131 # first two tokens -- we use versions like "892.0.0" now.
132 # TODO(derat): Remove the code for old-style versions after 20120101.
133 if len(client_tokens) == 4:
134 client_tokens = client_tokens[2:]
135
136 latest_tokens = latest_version.replace('_', '').split('.')
137 if len(latest_tokens) == 4:
138 latest_tokens = latest_tokens[2:]
139
140 for i in range(min(len(client_tokens), len(latest_tokens))):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700141 if int(latest_tokens[i]) == int(client_tokens[i]):
142 continue
143 return int(latest_tokens[i]) > int(client_tokens[i])
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700144
145 # Favor four-token new-style versions on the server over old-style versions
146 # on the client if everything else matches.
147 return len(latest_tokens) > len(client_tokens)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700148
Chris Sosa0356d3b2010-09-16 15:46:22 -0700149 def _UnpackZip(self, image_dir):
150 """Unpacks an image.zip into a given directory."""
151 image = os.path.join(image_dir, self._GetImageName())
152 if os.path.exists(image):
153 return True
154 else:
155 # -n, never clobber an existing file, in case we get invoked
156 # simultaneously by multiple request handlers. This means that
157 # we're assuming each image.zip file lives in a versioned
158 # directory (a la Buildbot).
159 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
160
161 def _GetImageName(self):
162 """Returns the name of the image that should be used."""
163 if self.use_test_image:
164 image_name = 'chromiumos_test_image.bin'
165 else:
166 image_name = 'chromiumos_image.bin'
167 return image_name
168
Chris Sosa0356d3b2010-09-16 15:46:22 -0700169 def _GetSize(self, update_path):
170 """Returns the size of the file given."""
171 return os.path.getsize(update_path)
172
173 def _GetHash(self, update_path):
174 """Returns the sha1 of the file given."""
175 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
176 % update_path)
177 return os.popen(cmd).read().rstrip()
178
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700179 def _IsDeltaFormatFile(self, filename):
180 try:
181 file_handle = open(filename, 'r')
182 delta_magic = 'CrAU'
183 magic = file_handle.read(len(delta_magic))
184 return magic == delta_magic
185 except Exception:
186 return False
187
Darin Petkov91436cb2010-09-28 08:52:17 -0700188 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
189 # it takes advantage of reduced I/O and multiple processors. Something like:
190 # % tee < FILE > /dev/null \
191 # >( openssl dgst -sha256 -binary | openssl base64 ) \
192 # >( openssl sha1 -binary | openssl base64 )
193 def _GetSHA256(self, update_path):
194 """Returns the sha256 of the file given."""
195 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
196 update_path)
197 return os.popen(cmd).read().rstrip()
198
Don Garrettf90edf02010-11-16 17:36:14 -0800199 def _GetMd5(self, update_path):
200 """Returns the md5 checksum of the file given."""
201 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
202 return os.popen(cmd).read().rstrip()
203
Don Garrett0c880e22010-11-17 18:13:37 -0800204 def _Copy(self, source, dest):
205 """Copies a file from dest to source (if different)"""
206 _LogMessage('Copy File %s -> %s' % (source, dest))
207 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800208 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800209 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800210
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700211 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700212 """Returns a payload to the client corresponding to a new update.
213
214 Args:
215 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700216 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700217 size: size of update blob
218 url: where to find update blob
219 Returns:
220 Xml string to be passed back to client.
221 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700222 delta = 'false'
223 if is_delta_format:
224 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000225 payload = """<?xml version="1.0" encoding="UTF-8"?>
226 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700227 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000228 <app appid="{%s}" status="ok">
229 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700230 <updatecheck
231 codebase="%s"
232 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700233 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700234 needsadmin="false"
235 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700236 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000237 status="ok"/>
238 </app>
239 </gupdate>
240 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700241 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700242 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000243
rtc@google.com21a5ca32009-11-04 18:23:23 +0000244 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700245 """Returns a payload to the client corresponding to no update."""
Darin Petkov845f1172011-01-05 14:45:24 -0800246 payload = """<?xml version="1.0" encoding="UTF-8"?>
247 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
248 <daystart elapsed_seconds="%s"/>
249 <app appid="{%s}" status="ok">
250 <ping status="ok"/>
251 <updatecheck status="noupdate"/>
252 </app>
253 </gupdate>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000254 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700255 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000256
Don Garrettf90edf02010-11-16 17:36:14 -0800257 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700258 """Generates an update gz given a full path to an image.
259
260 Args:
261 image_path: Full path to image.
262 Returns:
263 Path to created update_payload or None on error.
264 """
Don Garrettfff4c322010-11-19 13:37:12 -0800265 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa7c931362010-10-11 19:49:01 -0700266 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700267
Chris Sosa0f1ec842011-02-14 16:33:22 -0800268 update_command = [
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100269 '%s/cros_generate_update_payload' % self.devserver_dir,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800270 '--image="%s"' % image_path,
271 '--output="%s"' % update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800272 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700273
Chris Sosa0f1ec842011-02-14 16:33:22 -0800274 if src_image: update_command.append('--src_image="%s"' % src_image)
275 if not self.vm: update_command.append('--patch_kernel')
276 if self.private_key: update_command.append('--private_key="%s"' %
277 self.private_key)
278
279 update_string = ' '.join(update_command)
280 _LogMessage('Running ' + update_string)
281 if os.system(update_string) != 0:
Chris Sosa417e55d2011-01-25 16:40:48 -0800282 _LogMessage('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700283 return None
284
Don Garrettfff4c322010-11-19 13:37:12 -0800285 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700286
Don Garrettf90edf02010-11-16 17:36:14 -0800287 def GenerateStatefulFile(self, image_path, output_dir):
288 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700289
290 Args:
291 image_path: Full path to image.
292 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800293 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800294 Raises:
295 A subprocess exception if the update generator fails to generate a
296 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700297 """
Don Garrettfff4c322010-11-19 13:37:12 -0800298 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800299 subprocess.check_call(
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100300 ['%s/cros_generate_stateful_update_payload' % self.devserver_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800301 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800302 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800303 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800304 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700305
Don Garrettf90edf02010-11-16 17:36:14 -0800306 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
307 """Find directory to store a cached update.
308
Chris Sosa744e1472011-09-07 19:32:50 -0700309 Given one, or two images for an update, this finds which
310 cache directory should hold the update files, even if they don't exist
311 yet. The directory will be inside static_image_dir, and of the form:
Don Garrettf90edf02010-11-16 17:36:14 -0800312
Chris Sosa744e1472011-09-07 19:32:50 -0700313 Non-delta updates:
314 CACHE_DIR/12345678
315 Delta updates:
316 CACHE_DIR/12345678_12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800317
Chris Sosa744e1472011-09-07 19:32:50 -0700318 If self.private_key -- Signed updates:
319 CACHE_DIR/from_above+12345678
320 """
321 sub_dir = self._GetMd5(dest_image)
322 if src_image:
323 sub_dir = '%s_%s' % (self._GetMd5(src_image), sub_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800324
Chris Sosa744e1472011-09-07 19:32:50 -0700325 if self.private_key:
326 sub_dir = '%s+%s' % (sub_dir, self._GetMd5(self.private_key))
327
328 return os.path.join(CACHE_DIR, sub_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800329
Don Garrettfff4c322010-11-19 13:37:12 -0800330 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800331 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700332
Chris Sosade91f672010-11-16 10:05:44 -0800333 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800334 src_image: image we are updating from (Null/empty for non-delta)
335 image_path: full path to the image.
336 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800337 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800338 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800339 """
Don Garrettf90edf02010-11-16 17:36:14 -0800340 update_file = None
341 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700342
Don Garrettf90edf02010-11-16 17:36:14 -0800343 # Actually do the generation
344 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800345 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800346 image_path,
347 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000348
Don Garrettf90edf02010-11-16 17:36:14 -0800349 if update_file:
350 stateful_update_file = self.GenerateStatefulFile(image_path,
351 output_dir)
352
353 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800354 return update_file
Chris Sosa417e55d2011-01-25 16:40:48 -0800355 else:
356 _LogMessage('Failed to generate update.')
357 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800358
359 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
360 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000361
Chris Sosa0356d3b2010-09-16 15:46:22 -0700362 Args:
363 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700364 static_image_dir: the directory to move images to after generating.
365 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800366 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-25 16:40:48 -0800367 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700368 """
Don Garrettf90edf02010-11-16 17:36:14 -0800369 _LogMessage('Generating update for src %s image %s' % (self.src_image,
370 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700371
Chris Sosa417e55d2011-01-25 16:40:48 -0800372 # If it was pregenerated_path, don't regenerate
373 if self.pregenerated_path:
374 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800375
Don Garrettf90edf02010-11-16 17:36:14 -0800376 # Which sub_dir of static_image_dir should hold our cached update image
377 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
378 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
379
Chris Sosa417e55d2011-01-25 16:40:48 -0800380 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
381
Don Garrettf90edf02010-11-16 17:36:14 -0800382 # The cached payloads exist in a cache dir
383 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-25 16:40:48 -0800384 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800385 cache_stateful_payload = os.path.join(static_image_dir,
386 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800387 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800388
Chris Sosa417e55d2011-01-25 16:40:48 -0800389 # Check to see if this cache directory is valid.
390 if not os.path.exists(cache_update_payload) or not os.path.exists(
391 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800392 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800393 # Clean up stale state.
394 os.system('rm -rf "%s"' % full_cache_dir)
395 os.makedirs(full_cache_dir)
396 return_path = self.GenerateUpdateImage(image_path,
397 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800398
Chris Sosa417e55d2011-01-25 16:40:48 -0800399 # Clean up cache dir since it's not valid.
400 if not return_path:
401 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800402 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800403
404 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800405
Chris Sosa08d55a22011-01-19 16:08:02 -0800406 # Generation complete, copy if requested.
407 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800408 # The final results exist directly in static
409 update_payload = os.path.join(static_image_dir,
410 UPDATE_FILE)
411 stateful_payload = os.path.join(static_image_dir,
412 STATEFUL_FILE)
Chris Sosa08d55a22011-01-19 16:08:02 -0800413 self._Copy(cache_update_payload, update_payload)
414 self._Copy(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-25 16:40:48 -0800415 return UPDATE_FILE
416 else:
417 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700418
419 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800420 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700421 """Generates an update using the latest image that has been built.
422
423 This will only generate an update if the newest update is newer than that
424 on the client or client_version is 'ForcedUpdate'.
425
426 Args:
427 board_id: Name of the board.
428 client_version: Current version of the client or 'ForcedUpdate'
429 static_image_dir: the directory to move images to after generating.
430 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800431 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700432 """
433 latest_image_dir = self._GetLatestImageDir(board_id)
434 latest_version = self._GetVersionFromDir(latest_image_dir)
435 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
436
Chris Sosa7c931362010-10-11 19:49:01 -0700437 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700438 latest_image_path)
439
440 # Check to see whether or not we should update.
441 if client_version != 'ForcedUpdate' and not self._CanUpdate(
442 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700443 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800444 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700445
Don Garrettf90edf02010-11-16 17:36:14 -0800446 return self.GenerateUpdateImageWithCache(latest_image_path,
447 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700448
Andrew de los Reyes52620802010-04-12 13:40:07 -0700449 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
450 """Imports a factory-floor server configuration file. The file should
451 be in this format:
452 config = [
453 {
454 'qual_ids': set([1, 2, 3, "x86-generic"]),
455 'factory_image': 'generic-factory.gz',
456 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
457 'release_image': 'generic-release.gz',
458 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
459 'oempartitionimg_image': 'generic-oem.gz',
460 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700461 'efipartitionimg_image': 'generic-efi.gz',
462 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700463 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800464 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800465 'firmware_image': 'generic-firmware.gz',
466 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700467 },
468 {
469 'qual_ids': set([6]),
470 'factory_image': '6-factory.gz',
471 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
472 'release_image': '6-release.gz',
473 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
474 'oempartitionimg_image': '6-oem.gz',
475 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700476 'efipartitionimg_image': '6-efi.gz',
477 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700478 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800479 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800480 'firmware_image': '6-firmware.gz',
481 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700482 },
483 ]
484 The server will look for the files by name in the static files
485 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700486
Andrew de los Reyes52620802010-04-12 13:40:07 -0700487 If validate_checksums is True, validates checksums and exits. If
488 a checksum mismatch is found, it's printed to the screen.
489 """
490 f = open(filename, 'r')
491 output = {}
492 exec(f.read(), output)
493 self.factory_config = output['config']
494 success = True
495 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800496 for key in stanza.copy().iterkeys():
497 suffix = '_image'
498 if key.endswith(suffix):
499 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700500 stanza[kind + '_size'] = self._GetSize(os.path.join(
501 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800502 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700503 factory_checksum = self._GetHash(os.path.join(self.static_dir,
504 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800505 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700506 print ('Error: checksum mismatch for %s. Expected "%s" but file '
507 'has checksum "%s".' % (stanza[kind + '_image'],
508 stanza[kind + '_checksum'],
509 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800510 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700511
Andrew de los Reyes52620802010-04-12 13:40:07 -0700512 if validate_checksums:
513 if success is False:
514 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700515
Andrew de los Reyes52620802010-04-12 13:40:07 -0700516 print 'Config file looks good.'
517
518 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700519 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700520 for stanza in self.factory_config:
521 if board_id not in stanza['qual_ids']:
522 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700523 if kind + '_image' not in stanza:
524 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700525 return (stanza[kind + '_image'],
526 stanza[kind + '_checksum'],
527 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700528 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000529
Chris Sosa7c931362010-10-11 19:49:01 -0700530 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700531 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
532 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700533 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700534 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700535 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700536 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700537 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700538 # Factory install is using memento updater which is using the sha-1 hash so
539 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700540 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700541
Chris Sosa151643e2010-10-28 14:40:57 -0700542 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
543 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800544 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800545
Don Garrettf90edf02010-11-16 17:36:14 -0800546 Returns:
547 file name relative to static_image_dir on success.
548 """
Dale Curtis723ec472010-11-30 14:06:47 -0800549 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
550 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
551
Don Garrett0c880e22010-11-17 18:13:37 -0800552 if self.forced_payload:
553 # If the forced payload is not already in our static_image_dir,
554 # copy it there.
Don Garrettee25e552010-11-23 12:09:35 -0800555 src_path = os.path.abspath(self.forced_payload)
Don Garrettee25e552010-11-23 12:09:35 -0800556 src_stateful = os.path.join(os.path.dirname(src_path),
557 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800558
559 # Only copy the files if the source directory is different from dest.
560 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
561 self._Copy(src_path, dest_path)
562
563 # The stateful payload is optional.
564 if os.path.exists(src_stateful):
565 self._Copy(src_stateful, dest_stateful)
566 else:
567 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
568 STATEFUL_FILE)
569 if os.path.exists(dest_stateful):
570 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800571
Don Garrettfff4c322010-11-19 13:37:12 -0800572 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800573 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800574 return self.GenerateUpdateImageWithCache(
575 self.forced_image,
576 static_image_dir=static_image_dir)
577 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800578 # Warn if update or stateful files can't be found.
579 if not os.path.exists(dest_path):
580 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
581 UPDATE_FILE)
582
583 if not os.path.exists(dest_stateful):
584 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
585 STATEFUL_FILE)
586
587 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800588 else:
589 if board_id:
590 return self.GenerateLatestUpdateImage(board_id,
591 client_version,
592 static_image_dir)
593
Chris Sosa417e55d2011-01-25 16:40:48 -0800594 _LogMessage('Failed to genereate update. '
595 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800596 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700597
598 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800599 """Pre-generates an update and prints out the relative path it.
600
601 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800602 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700603 # Does not work with factory config.
604 assert(not self.factory_config)
605 _LogMessage('Pre-generating the update payload.')
606 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-25 16:40:48 -0800607 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
608 self.board, '0.0.0.0', self.static_dir)
609 if pregenerated_update:
610 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
611
612 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700613
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700614 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700615 """Handles an update ping from an update client.
616
617 Args:
618 data: xml blob from client.
619 label: optional label for the update.
620 Returns:
621 Update payload message for client.
622 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700623 # Set hostname as the hostname that the client is calling to and set up
624 # the url base.
625 self.hostname = cherrypy.request.base
626 if self.urlbase:
627 static_urlbase = self.urlbase
628 elif self.serve_only:
629 static_urlbase = '%s/static/archive' % self.hostname
630 else:
631 static_urlbase = '%s/static' % self.hostname
632
Don Garrett0ad09372010-12-06 16:20:30 -0800633 # If we have a proxy port, adjust the URL we instruct the client to
634 # use to go through the proxy.
635 if self.proxy_port:
636 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
637
Chris Sosa9841e1c2010-10-14 10:51:45 -0700638 _LogMessage('Using static url base %s' % static_urlbase)
639 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700640
Chris Sosa9841e1c2010-10-14 10:51:45 -0700641 update_dom = minidom.parseString(data)
642 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700643
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700644 # Determine request IP, strip any IPv6 data for simplicity.
645 client_ip = cherrypy.request.remote.ip.split(':')[-1]
646
647 # Initialize host info dictionary for this client if it doesn't exist.
648 self.host_info.setdefault(client_ip, {})
649
650 # Store event details in the host info dictionary for API usage.
651 event = root.getElementsByTagName('o:event')
652 if event:
653 self.host_info[client_ip]['last_event_status'] = (
654 int(event[0].getAttribute('eventresult')))
655 self.host_info[client_ip]['last_event_type'] = (
656 int(event[0].getAttribute('eventtype')))
657
Chris Sosa0356d3b2010-09-16 15:46:22 -0700658 # We only generate update payloads for updatecheck requests.
659 update_check = root.getElementsByTagName('o:updatecheck')
660 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700661 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700662 # TODO(sosa): Generate correct non-updatecheck payload to better test
663 # update clients.
664 return self.GetNoUpdatePayload()
665
666 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700667 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800668 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700669 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700670 board_id = (query.hasAttribute('board') and query.getAttribute('board')
671 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700672
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700673 # Store version for this host in the cache.
674 self.host_info[client_ip]['last_known_version'] = client_version
675
676 # Check if an update has been forced for this client.
677 forced_update = self.host_info[client_ip].pop('forced_update_label', None)
678 if forced_update:
679 label = forced_update
680
Chris Sosa0356d3b2010-09-16 15:46:22 -0700681 # Separate logic as Factory requests have static url's that override
682 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700683 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700684 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700685 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700686 static_image_dir = self.static_dir
687 if label:
688 static_image_dir = os.path.join(static_image_dir, label)
689
Don Garrettf90edf02010-11-16 17:36:14 -0800690 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
691 client_version,
692 static_image_dir)
693 if payload_path:
694 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700695 hash = self._GetHash(filename)
696 sha256 = self._GetSHA256(filename)
697 size = self._GetSize(filename)
698 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700699 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800700 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700701 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800702 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700703
Chris Sosa7c931362010-10-11 19:49:01 -0700704 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700705 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700706 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700707 return self.GetNoUpdatePayload()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700708
709 def HandleHostInfoPing(self, ip):
710 """Returns host info dictionary for the given IP in JSON format."""
711 assert ip, 'No ip provided.'
712 if ip in self.host_info:
713 return json.dumps(self.host_info[ip])
714
715 def HandleSetUpdatePing(self, ip, label):
716 """Sets forced_update_label for a given host."""
717 assert ip, 'No ip provided.'
718 assert label, 'No label provided.'
719 self.host_info.setdefault(ip, {})['forced_update_label'] = label