blob: 5666ca3b887e5f7ebffb7898868395c8c0b2561f [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)
114 return latest_version.split('-')[0]
115
116 def _CanUpdate(self, client_version, latest_version):
Don Garrettf90edf02010-11-16 17:36:14 -0800117 """Returns true if the latest_version is greater than the client_version.
118 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700119 client_tokens = client_version.replace('_', '').split('.')
120 latest_tokens = latest_version.replace('_', '').split('.')
Chris Sosa7c931362010-10-11 19:49:01 -0700121 _LogMessage('client version %s latest version %s'
Don Garrettf90edf02010-11-16 17:36:14 -0800122 % (client_version, latest_version))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700123 for i in range(4):
124 if int(latest_tokens[i]) == int(client_tokens[i]):
125 continue
126 return int(latest_tokens[i]) > int(client_tokens[i])
127 return False
128
Chris Sosa0356d3b2010-09-16 15:46:22 -0700129 def _UnpackZip(self, image_dir):
130 """Unpacks an image.zip into a given directory."""
131 image = os.path.join(image_dir, self._GetImageName())
132 if os.path.exists(image):
133 return True
134 else:
135 # -n, never clobber an existing file, in case we get invoked
136 # simultaneously by multiple request handlers. This means that
137 # we're assuming each image.zip file lives in a versioned
138 # directory (a la Buildbot).
139 return os.system('cd %s && unzip -n image.zip' % image_dir) == 0
140
141 def _GetImageName(self):
142 """Returns the name of the image that should be used."""
143 if self.use_test_image:
144 image_name = 'chromiumos_test_image.bin'
145 else:
146 image_name = 'chromiumos_image.bin'
147 return image_name
148
Chris Sosa0356d3b2010-09-16 15:46:22 -0700149 def _GetSize(self, update_path):
150 """Returns the size of the file given."""
151 return os.path.getsize(update_path)
152
153 def _GetHash(self, update_path):
154 """Returns the sha1 of the file given."""
155 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
156 % update_path)
157 return os.popen(cmd).read().rstrip()
158
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700159 def _IsDeltaFormatFile(self, filename):
160 try:
161 file_handle = open(filename, 'r')
162 delta_magic = 'CrAU'
163 magic = file_handle.read(len(delta_magic))
164 return magic == delta_magic
165 except Exception:
166 return False
167
Darin Petkov91436cb2010-09-28 08:52:17 -0700168 # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
169 # it takes advantage of reduced I/O and multiple processors. Something like:
170 # % tee < FILE > /dev/null \
171 # >( openssl dgst -sha256 -binary | openssl base64 ) \
172 # >( openssl sha1 -binary | openssl base64 )
173 def _GetSHA256(self, update_path):
174 """Returns the sha256 of the file given."""
175 cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
176 update_path)
177 return os.popen(cmd).read().rstrip()
178
Don Garrettf90edf02010-11-16 17:36:14 -0800179 def _GetMd5(self, update_path):
180 """Returns the md5 checksum of the file given."""
181 cmd = ("md5sum %s | awk '{print $1}'" % update_path)
182 return os.popen(cmd).read().rstrip()
183
Don Garrett0c880e22010-11-17 18:13:37 -0800184 def _Copy(self, source, dest):
185 """Copies a file from dest to source (if different)"""
186 _LogMessage('Copy File %s -> %s' % (source, dest))
187 if os.path.lexists(dest):
Don Garrettf90edf02010-11-16 17:36:14 -0800188 os.remove(dest)
Don Garrett0c880e22010-11-17 18:13:37 -0800189 shutil.copy(source, dest)
Don Garrettf90edf02010-11-16 17:36:14 -0800190
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700191 def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700192 """Returns a payload to the client corresponding to a new update.
193
194 Args:
195 hash: hash of update blob
Darin Petkov91436cb2010-09-28 08:52:17 -0700196 sha256: SHA-256 hash of update blob
Chris Sosa0356d3b2010-09-16 15:46:22 -0700197 size: size of update blob
198 url: where to find update blob
199 Returns:
200 Xml string to be passed back to client.
201 """
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700202 delta = 'false'
203 if is_delta_format:
204 delta = 'true'
rtc@google.com21a5ca32009-11-04 18:23:23 +0000205 payload = """<?xml version="1.0" encoding="UTF-8"?>
206 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700207 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000208 <app appid="{%s}" status="ok">
209 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700210 <updatecheck
211 codebase="%s"
212 hash="%s"
Darin Petkov91436cb2010-09-28 08:52:17 -0700213 sha256="%s"
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700214 needsadmin="false"
215 size="%s"
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700216 IsDelta="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000217 status="ok"/>
218 </app>
219 </gupdate>
220 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700221 return payload % (self._GetSecondsSinceMidnight(),
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700222 self.app_id, url, hash, sha256, size, delta)
rtc@google.comded22402009-10-26 22:36:21 +0000223
rtc@google.com21a5ca32009-11-04 18:23:23 +0000224 def GetNoUpdatePayload(self):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700225 """Returns a payload to the client corresponding to no update."""
Darin Petkov845f1172011-01-05 14:45:24 -0800226 payload = """<?xml version="1.0" encoding="UTF-8"?>
227 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
228 <daystart elapsed_seconds="%s"/>
229 <app appid="{%s}" status="ok">
230 <ping status="ok"/>
231 <updatecheck status="noupdate"/>
232 </app>
233 </gupdate>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000234 """
Chris Sosa0356d3b2010-09-16 15:46:22 -0700235 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000236
Don Garrettf90edf02010-11-16 17:36:14 -0800237 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700238 """Generates an update gz given a full path to an image.
239
240 Args:
241 image_path: Full path to image.
242 Returns:
243 Path to created update_payload or None on error.
244 """
Don Garrettfff4c322010-11-19 13:37:12 -0800245 update_path = os.path.join(output_dir, UPDATE_FILE)
Chris Sosa7c931362010-10-11 19:49:01 -0700246 _LogMessage('Generating update image %s' % update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700247
Chris Sosa0f1ec842011-02-14 16:33:22 -0800248 update_command = [
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100249 '%s/cros_generate_update_payload' % self.devserver_dir,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800250 '--image="%s"' % image_path,
251 '--output="%s"' % update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800252 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700253
Chris Sosa0f1ec842011-02-14 16:33:22 -0800254 if src_image: update_command.append('--src_image="%s"' % src_image)
255 if not self.vm: update_command.append('--patch_kernel')
256 if self.private_key: update_command.append('--private_key="%s"' %
257 self.private_key)
258
259 update_string = ' '.join(update_command)
260 _LogMessage('Running ' + update_string)
261 if os.system(update_string) != 0:
Chris Sosa417e55d2011-01-25 16:40:48 -0800262 _LogMessage('Failed to create update payload')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700263 return None
264
Don Garrettfff4c322010-11-19 13:37:12 -0800265 return UPDATE_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700266
Don Garrettf90edf02010-11-16 17:36:14 -0800267 def GenerateStatefulFile(self, image_path, output_dir):
268 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700269
270 Args:
271 image_path: Full path to image.
272 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800273 Path to created stateful update_payload or None on error.
Chris Sosa908fd6f2010-11-10 17:31:18 -0800274 Raises:
275 A subprocess exception if the update generator fails to generate a
276 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700277 """
Don Garrettfff4c322010-11-19 13:37:12 -0800278 output_gz = os.path.join(output_dir, STATEFUL_FILE)
Chris Sosa908fd6f2010-11-10 17:31:18 -0800279 subprocess.check_call(
Zdenek Behan59d8aa72011-02-24 01:09:02 +0100280 ['%s/cros_generate_stateful_update_payload' % self.devserver_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800281 '--image=%s' % image_path,
Don Garrettf90edf02010-11-16 17:36:14 -0800282 '--output_dir=%s' % output_dir,
Chris Sosa908fd6f2010-11-10 17:31:18 -0800283 ])
Don Garrettfff4c322010-11-19 13:37:12 -0800284 return STATEFUL_FILE
Chris Sosa0356d3b2010-09-16 15:46:22 -0700285
Don Garrettf90edf02010-11-16 17:36:14 -0800286 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
287 """Find directory to store a cached update.
288
Chris Sosaa791cb72011-09-06 16:01:09 -0700289 Given one, or two images for an update, this finds which
290 cache directory should hold the update files, even if they don't exist
291 yet. The directory will be inside static_image_dir, and of the form:
Don Garrettf90edf02010-11-16 17:36:14 -0800292
Chris Sosaa791cb72011-09-06 16:01:09 -0700293 Non-delta updates:
294 CACHE_DIR/12345678
295 Delta updates:
296 CACHE_DIR/12345678_12345678
Don Garrettf90edf02010-11-16 17:36:14 -0800297
Chris Sosaa791cb72011-09-06 16:01:09 -0700298 If self.private_key -- Signed updates:
299 CACHE_DIR/from_above+12345678
300 """
301 sub_dir = self._GetMd5(dest_image)
302 if src_image:
303 sub_dir = '%s_%s' % (self._GetMd5(src_image), sub_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800304
Chris Sosaa791cb72011-09-06 16:01:09 -0700305 if self.private_key:
306 sub_dir = '%s+%s' % (sub_dir, self._GetMd5(self.private_key))
307
308 return os.path.join(CACHE_DIR, sub_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800309
Don Garrettfff4c322010-11-19 13:37:12 -0800310 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800311 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700312
Chris Sosade91f672010-11-16 10:05:44 -0800313 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800314 src_image: image we are updating from (Null/empty for non-delta)
315 image_path: full path to the image.
316 output_dir: the directory to write the update payloads in
Chris Sosade91f672010-11-16 10:05:44 -0800317 Returns:
Don Garrettfff4c322010-11-19 13:37:12 -0800318 update payload name relative to output_dir
Chris Sosade91f672010-11-16 10:05:44 -0800319 """
Don Garrettf90edf02010-11-16 17:36:14 -0800320 update_file = None
321 stateful_update_file = None
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700322
Don Garrettf90edf02010-11-16 17:36:14 -0800323 # Actually do the generation
324 _LogMessage('Generating update for image %s' % image_path)
Don Garrettfff4c322010-11-19 13:37:12 -0800325 update_file = self.GenerateUpdateFile(self.src_image,
Don Garrettf90edf02010-11-16 17:36:14 -0800326 image_path,
327 output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000328
Don Garrettf90edf02010-11-16 17:36:14 -0800329 if update_file:
330 stateful_update_file = self.GenerateStatefulFile(image_path,
331 output_dir)
332
333 if update_file and stateful_update_file:
Don Garrettfff4c322010-11-19 13:37:12 -0800334 return update_file
Chris Sosa417e55d2011-01-25 16:40:48 -0800335 else:
336 _LogMessage('Failed to generate update.')
337 return None
Don Garrettf90edf02010-11-16 17:36:14 -0800338
339 def GenerateUpdateImageWithCache(self, image_path, static_image_dir):
340 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000341
Chris Sosa0356d3b2010-09-16 15:46:22 -0700342 Args:
343 image_path: full path to the image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700344 static_image_dir: the directory to move images to after generating.
345 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800346 update filename (not directory) relative to static_image_dir on success,
Chris Sosa417e55d2011-01-25 16:40:48 -0800347 or None.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700348 """
Don Garrettf90edf02010-11-16 17:36:14 -0800349 _LogMessage('Generating update for src %s image %s' % (self.src_image,
350 image_path))
Chris Sosae67b78f2010-11-04 17:33:16 -0700351
Chris Sosa417e55d2011-01-25 16:40:48 -0800352 # If it was pregenerated_path, don't regenerate
353 if self.pregenerated_path:
354 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800355
Don Garrettf90edf02010-11-16 17:36:14 -0800356 # Which sub_dir of static_image_dir should hold our cached update image
357 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
358 _LogMessage('Caching in sub_dir "%s"' % cache_sub_dir)
359
Chris Sosa417e55d2011-01-25 16:40:48 -0800360 update_path = os.path.join(cache_sub_dir, UPDATE_FILE)
361
Don Garrettf90edf02010-11-16 17:36:14 -0800362 # The cached payloads exist in a cache dir
363 cache_update_payload = os.path.join(static_image_dir,
Chris Sosa417e55d2011-01-25 16:40:48 -0800364 update_path)
Don Garrettf90edf02010-11-16 17:36:14 -0800365 cache_stateful_payload = os.path.join(static_image_dir,
366 cache_sub_dir,
Don Garrettfff4c322010-11-19 13:37:12 -0800367 STATEFUL_FILE)
Don Garrettf90edf02010-11-16 17:36:14 -0800368
Chris Sosa417e55d2011-01-25 16:40:48 -0800369 # Check to see if this cache directory is valid.
370 if not os.path.exists(cache_update_payload) or not os.path.exists(
371 cache_stateful_payload):
Don Garrettf90edf02010-11-16 17:36:14 -0800372 full_cache_dir = os.path.join(static_image_dir, cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800373 # Clean up stale state.
374 os.system('rm -rf "%s"' % full_cache_dir)
375 os.makedirs(full_cache_dir)
376 return_path = self.GenerateUpdateImage(image_path,
377 full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800378
Chris Sosa417e55d2011-01-25 16:40:48 -0800379 # Clean up cache dir since it's not valid.
380 if not return_path:
381 os.system('rm -rf "%s"' % full_cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800382 return None
Chris Sosa417e55d2011-01-25 16:40:48 -0800383
384 self.pregenerated_path = update_path
Don Garrettf90edf02010-11-16 17:36:14 -0800385
Chris Sosa08d55a22011-01-19 16:08:02 -0800386 # Generation complete, copy if requested.
387 if self.copy_to_static_root:
Chris Sosa417e55d2011-01-25 16:40:48 -0800388 # The final results exist directly in static
389 update_payload = os.path.join(static_image_dir,
390 UPDATE_FILE)
391 stateful_payload = os.path.join(static_image_dir,
392 STATEFUL_FILE)
Chris Sosa08d55a22011-01-19 16:08:02 -0800393 self._Copy(cache_update_payload, update_payload)
394 self._Copy(cache_stateful_payload, stateful_payload)
Chris Sosa417e55d2011-01-25 16:40:48 -0800395 return UPDATE_FILE
396 else:
397 return self.pregenerated_path
Chris Sosa0356d3b2010-09-16 15:46:22 -0700398
399 def GenerateLatestUpdateImage(self, board_id, client_version,
Don Garrettf90edf02010-11-16 17:36:14 -0800400 static_image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700401 """Generates an update using the latest image that has been built.
402
403 This will only generate an update if the newest update is newer than that
404 on the client or client_version is 'ForcedUpdate'.
405
406 Args:
407 board_id: Name of the board.
408 client_version: Current version of the client or 'ForcedUpdate'
409 static_image_dir: the directory to move images to after generating.
410 Returns:
Don Garrettf90edf02010-11-16 17:36:14 -0800411 Name of the update image relative to static_image_dir or None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700412 """
413 latest_image_dir = self._GetLatestImageDir(board_id)
414 latest_version = self._GetVersionFromDir(latest_image_dir)
415 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
416
Chris Sosa7c931362010-10-11 19:49:01 -0700417 _LogMessage('Preparing to generate update from latest built image %s.' %
Chris Sosa0356d3b2010-09-16 15:46:22 -0700418 latest_image_path)
419
420 # Check to see whether or not we should update.
421 if client_version != 'ForcedUpdate' and not self._CanUpdate(
422 client_version, latest_version):
Chris Sosa7c931362010-10-11 19:49:01 -0700423 _LogMessage('no update')
Don Garrettf90edf02010-11-16 17:36:14 -0800424 return None
Chris Sosa0356d3b2010-09-16 15:46:22 -0700425
Don Garrettf90edf02010-11-16 17:36:14 -0800426 return self.GenerateUpdateImageWithCache(latest_image_path,
427 static_image_dir=static_image_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700428
Andrew de los Reyes52620802010-04-12 13:40:07 -0700429 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
430 """Imports a factory-floor server configuration file. The file should
431 be in this format:
432 config = [
433 {
434 'qual_ids': set([1, 2, 3, "x86-generic"]),
435 'factory_image': 'generic-factory.gz',
436 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
437 'release_image': 'generic-release.gz',
438 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
439 'oempartitionimg_image': 'generic-oem.gz',
440 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700441 'efipartitionimg_image': 'generic-efi.gz',
442 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700443 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800444 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800445 'firmware_image': 'generic-firmware.gz',
446 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700447 },
448 {
449 'qual_ids': set([6]),
450 'factory_image': '6-factory.gz',
451 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
452 'release_image': '6-release.gz',
453 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
454 'oempartitionimg_image': '6-oem.gz',
455 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700456 'efipartitionimg_image': '6-efi.gz',
457 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700458 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800459 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800460 'firmware_image': '6-firmware.gz',
461 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700462 },
463 ]
464 The server will look for the files by name in the static files
465 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700466
Andrew de los Reyes52620802010-04-12 13:40:07 -0700467 If validate_checksums is True, validates checksums and exits. If
468 a checksum mismatch is found, it's printed to the screen.
469 """
470 f = open(filename, 'r')
471 output = {}
472 exec(f.read(), output)
473 self.factory_config = output['config']
474 success = True
475 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800476 for key in stanza.copy().iterkeys():
477 suffix = '_image'
478 if key.endswith(suffix):
479 kind = key[:-len(suffix)]
Chris Sosa0356d3b2010-09-16 15:46:22 -0700480 stanza[kind + '_size'] = self._GetSize(os.path.join(
481 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800482 if validate_checksums:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700483 factory_checksum = self._GetHash(os.path.join(self.static_dir,
484 stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800485 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700486 print ('Error: checksum mismatch for %s. Expected "%s" but file '
487 'has checksum "%s".' % (stanza[kind + '_image'],
488 stanza[kind + '_checksum'],
489 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800490 success = False
Chris Sosa0356d3b2010-09-16 15:46:22 -0700491
Andrew de los Reyes52620802010-04-12 13:40:07 -0700492 if validate_checksums:
493 if success is False:
494 raise Exception('Checksum mismatch in conf file.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700495
Andrew de los Reyes52620802010-04-12 13:40:07 -0700496 print 'Config file looks good.'
497
498 def GetFactoryImage(self, board_id, channel):
Nick Sanders723f3262010-09-16 05:18:41 -0700499 kind = channel.rsplit('-', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700500 for stanza in self.factory_config:
501 if board_id not in stanza['qual_ids']:
502 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700503 if kind + '_image' not in stanza:
504 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700505 return (stanza[kind + '_image'],
506 stanza[kind + '_checksum'],
507 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700508 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000509
Chris Sosa7c931362010-10-11 19:49:01 -0700510 def HandleFactoryRequest(self, board_id, channel):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700511 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
512 if filename is None:
Chris Sosa7c931362010-10-11 19:49:01 -0700513 _LogMessage('unable to find image for board %s' % board_id)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700514 return self.GetNoUpdatePayload()
Chris Sosa05f95162010-10-14 18:01:52 -0700515 url = '%s/static/%s' % (self.hostname, filename)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700516 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa7c931362010-10-11 19:49:01 -0700517 _LogMessage('returning update payload ' + url)
Darin Petkov91436cb2010-09-28 08:52:17 -0700518 # Factory install is using memento updater which is using the sha-1 hash so
519 # setting sha-256 to an empty string.
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700520 return self.GetUpdatePayload(checksum, '', size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700521
Chris Sosa151643e2010-10-28 14:40:57 -0700522 def GenerateUpdatePayloadForNonFactory(self, board_id, client_version,
523 static_image_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800524 """Generates an update for non-factory image.
Don Garrett710470d2010-11-15 17:43:44 -0800525
Don Garrettf90edf02010-11-16 17:36:14 -0800526 Returns:
527 file name relative to static_image_dir on success.
528 """
Dale Curtis723ec472010-11-30 14:06:47 -0800529 dest_path = os.path.join(static_image_dir, UPDATE_FILE)
530 dest_stateful = os.path.join(static_image_dir, STATEFUL_FILE)
531
Don Garrett0c880e22010-11-17 18:13:37 -0800532 if self.forced_payload:
533 # If the forced payload is not already in our static_image_dir,
534 # copy it there.
Don Garrettee25e552010-11-23 12:09:35 -0800535 src_path = os.path.abspath(self.forced_payload)
Don Garrettee25e552010-11-23 12:09:35 -0800536 src_stateful = os.path.join(os.path.dirname(src_path),
537 STATEFUL_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800538
539 # Only copy the files if the source directory is different from dest.
540 if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
541 self._Copy(src_path, dest_path)
542
543 # The stateful payload is optional.
544 if os.path.exists(src_stateful):
545 self._Copy(src_stateful, dest_stateful)
546 else:
547 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
548 STATEFUL_FILE)
549 if os.path.exists(dest_stateful):
550 os.remove(dest_stateful)
Don Garrett0c880e22010-11-17 18:13:37 -0800551
Don Garrettfff4c322010-11-19 13:37:12 -0800552 return UPDATE_FILE
Don Garrett0c880e22010-11-17 18:13:37 -0800553 elif self.forced_image:
Don Garrettf90edf02010-11-16 17:36:14 -0800554 return self.GenerateUpdateImageWithCache(
555 self.forced_image,
556 static_image_dir=static_image_dir)
557 elif self.serve_only:
Dale Curtis723ec472010-11-30 14:06:47 -0800558 # Warn if update or stateful files can't be found.
559 if not os.path.exists(dest_path):
560 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
561 UPDATE_FILE)
562
563 if not os.path.exists(dest_stateful):
564 _LogMessage('WARN: %s not found. Expected for dev and test builds.' %
565 STATEFUL_FILE)
566
567 return UPDATE_FILE
Don Garrettf90edf02010-11-16 17:36:14 -0800568 else:
569 if board_id:
570 return self.GenerateLatestUpdateImage(board_id,
571 client_version,
572 static_image_dir)
573
Chris Sosa417e55d2011-01-25 16:40:48 -0800574 _LogMessage('Failed to genereate update. '
575 'You must set --board when pre-generating latest update.')
Don Garrettf90edf02010-11-16 17:36:14 -0800576 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700577
578 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800579 """Pre-generates an update and prints out the relative path it.
580
581 Returns relative path of the update on success.
Don Garrettf90edf02010-11-16 17:36:14 -0800582 """
Chris Sosa2c048f12010-10-27 16:05:27 -0700583 # Does not work with factory config.
584 assert(not self.factory_config)
585 _LogMessage('Pre-generating the update payload.')
586 # Does not work with labels so just use static dir.
Chris Sosa417e55d2011-01-25 16:40:48 -0800587 pregenerated_update = self.GenerateUpdatePayloadForNonFactory(
588 self.board, '0.0.0.0', self.static_dir)
589 if pregenerated_update:
590 print 'PREGENERATED_UPDATE=%s' % pregenerated_update
591
592 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700593
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700594 def HandleUpdatePing(self, data, label=None):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700595 """Handles an update ping from an update client.
596
597 Args:
598 data: xml blob from client.
599 label: optional label for the update.
600 Returns:
601 Update payload message for client.
602 """
Chris Sosa9841e1c2010-10-14 10:51:45 -0700603 # Set hostname as the hostname that the client is calling to and set up
604 # the url base.
605 self.hostname = cherrypy.request.base
606 if self.urlbase:
607 static_urlbase = self.urlbase
608 elif self.serve_only:
609 static_urlbase = '%s/static/archive' % self.hostname
610 else:
611 static_urlbase = '%s/static' % self.hostname
612
Don Garrett0ad09372010-12-06 16:20:30 -0800613 # If we have a proxy port, adjust the URL we instruct the client to
614 # use to go through the proxy.
615 if self.proxy_port:
616 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
617
Chris Sosa9841e1c2010-10-14 10:51:45 -0700618 _LogMessage('Using static url base %s' % static_urlbase)
619 _LogMessage('Handling update ping as %s: %s' % (self.hostname, data))
Chris Sosa0356d3b2010-09-16 15:46:22 -0700620
Chris Sosa9841e1c2010-10-14 10:51:45 -0700621 update_dom = minidom.parseString(data)
622 root = update_dom.firstChild
Chris Sosa0356d3b2010-09-16 15:46:22 -0700623
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700624 # Determine request IP, strip any IPv6 data for simplicity.
625 client_ip = cherrypy.request.remote.ip.split(':')[-1]
626
627 # Initialize host info dictionary for this client if it doesn't exist.
628 self.host_info.setdefault(client_ip, {})
629
630 # Store event details in the host info dictionary for API usage.
631 event = root.getElementsByTagName('o:event')
632 if event:
633 self.host_info[client_ip]['last_event_status'] = (
634 int(event[0].getAttribute('eventresult')))
635 self.host_info[client_ip]['last_event_type'] = (
636 int(event[0].getAttribute('eventtype')))
637
Chris Sosa0356d3b2010-09-16 15:46:22 -0700638 # We only generate update payloads for updatecheck requests.
639 update_check = root.getElementsByTagName('o:updatecheck')
640 if not update_check:
Chris Sosa7c931362010-10-11 19:49:01 -0700641 _LogMessage('Non-update check received. Returning blank payload.')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700642 # TODO(sosa): Generate correct non-updatecheck payload to better test
643 # update clients.
644 return self.GetNoUpdatePayload()
645
646 # Since this is an updatecheck, get information about the requester.
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700647 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800648 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700649 channel = query.getAttribute('track')
Chris Sosa0356d3b2010-09-16 15:46:22 -0700650 board_id = (query.hasAttribute('board') and query.getAttribute('board')
651 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700652
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700653 # Store version for this host in the cache.
654 self.host_info[client_ip]['last_known_version'] = client_version
655
656 # Check if an update has been forced for this client.
657 forced_update = self.host_info[client_ip].pop('forced_update_label', None)
658 if forced_update:
659 label = forced_update
660
Chris Sosa0356d3b2010-09-16 15:46:22 -0700661 # Separate logic as Factory requests have static url's that override
662 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700663 if self.factory_config:
Chris Sosa7c931362010-10-11 19:49:01 -0700664 return self.HandleFactoryRequest(board_id, channel)
Nick Sanders723f3262010-09-16 05:18:41 -0700665 else:
Chris Sosa0356d3b2010-09-16 15:46:22 -0700666 static_image_dir = self.static_dir
667 if label:
668 static_image_dir = os.path.join(static_image_dir, label)
669
Don Garrettf90edf02010-11-16 17:36:14 -0800670 payload_path = self.GenerateUpdatePayloadForNonFactory(board_id,
671 client_version,
672 static_image_dir)
673 if payload_path:
674 filename = os.path.join(static_image_dir, payload_path)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700675 hash = self._GetHash(filename)
676 sha256 = self._GetSHA256(filename)
677 size = self._GetSize(filename)
678 is_delta_format = self._IsDeltaFormatFile(filename)
Chris Sosa5d342a22010-09-28 16:54:41 -0700679 if label:
Don Garrettf90edf02010-11-16 17:36:14 -0800680 url = '%s/%s/%s' % (static_urlbase, label, payload_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700681 else:
Don Garrettf90edf02010-11-16 17:36:14 -0800682 url = '%s/%s' % (static_urlbase, payload_path)
Chris Sosa5d342a22010-09-28 16:54:41 -0700683
Chris Sosa7c931362010-10-11 19:49:01 -0700684 _LogMessage('Responding to client to use url %s to get image.' % url)
Andrew de los Reyes5679b972010-10-25 17:34:49 -0700685 return self.GetUpdatePayload(hash, sha256, size, url, is_delta_format)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700686 else:
Nick Sanders723f3262010-09-16 05:18:41 -0700687 return self.GetNoUpdatePayload()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700688
689 def HandleHostInfoPing(self, ip):
690 """Returns host info dictionary for the given IP in JSON format."""
691 assert ip, 'No ip provided.'
692 if ip in self.host_info:
693 return json.dumps(self.host_info[ip])
694
695 def HandleSetUpdatePing(self, ip, label):
696 """Sets forced_update_label for a given host."""
697 assert ip, 'No ip provided.'
698 assert label, 'No label provided.'
699 self.host_info.setdefault(ip, {})['forced_update_label'] = label