blob: 17160a9ef1c693f7f4e3368df66342d9ffe3c1d3 [file] [log] [blame]
Chris Sosaaae36452010-09-15 17:06:05 -07001# Copyright (c) 2009-2010 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
7
8import os
Darin Petkov798fe7d2010-03-22 15:18:13 -07009import shutil
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070010import time
rtc@google.comded22402009-10-26 22:36:21 +000011import web
12
Chris Sosaaae36452010-09-15 17:06:05 -070013
rtc@google.com64244662009-11-12 00:52:08 +000014class Autoupdate(BuildObject):
Chris Sosaaae36452010-09-15 17:06:05 -070015 """Class that contains functionality that handles Chrome OS update pings.
16
17 Members:
18 serve_only: Serve images from a pre-built image.zip file. static_dir
19 must be set to the location of the image.zip.
20 factory_config: Path to the factory config file if handling factory
21 requests.
22 use_test_image: Use chromiumos_test_image.bin rather than the standard.
23 static_url_base: base URL, other than devserver, for update images.
24 client_prefix: The prefix for the update engine client.
25 forced_image: Path to an image to use for all updates.
26 """
rtc@google.comded22402009-10-26 22:36:21 +000027
Sean O'Connor1f7fd362010-04-07 16:34:52 -070028 def __init__(self, serve_only=None, test_image=False, urlbase=None,
Chris Sosaaae36452010-09-15 17:06:05 -070029 factory_config_path=None, client_prefix=None, forced_image=None,
Sean O'Connor1f7fd362010-04-07 16:34:52 -070030 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070031 super(Autoupdate, self).__init__(*args, **kwargs)
Sean O'Connor1f7fd362010-04-07 16:34:52 -070032 self.serve_only = serve_only
Sean O'Connor1b4b0762010-06-02 17:37:32 -070033 self.factory_config = factory_config_path
Chris Sosaaae36452010-09-15 17:06:05 -070034 self.use_test_image = test_image
Sean O'Connor1f7fd362010-04-07 16:34:52 -070035 self.static_urlbase = urlbase
Chris Sosab63a9282010-09-02 10:43:23 -070036 self.client_prefix = client_prefix
Chris Sosaaae36452010-09-15 17:06:05 -070037 self.forced_image = forced_image
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070038
Chris Sosaaae36452010-09-15 17:06:05 -070039 def _GetSecondsSinceMidnight(self):
40 """Returns the seconds since midnight as a decimal value."""
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070041 now = time.localtime()
42 return now[3] * 3600 + now[4] * 60 + now[5]
43
Chris Sosaaae36452010-09-15 17:06:05 -070044 def _GetDefaultBoardID(self):
45 """Returns the default board id stored in .default_board."""
46 board_file = '%s/.default_board' % (self.scripts_dir)
47 try:
48 return open(board_file).read()
49 except IOError:
50 return 'x86-generic'
51
52 def _GetLatestImageDir(self, board_id):
53 """Returns the latest image dir based on shell script."""
54 cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
55 return os.popen(cmd).read().strip()
56
57 def _GetVersionFromDir(self, image_dir):
58 """Returns the version of the image based on the name of the directory."""
59 latest_version = os.path.basename(image_dir)
60 return latest_version.split('-')[0]
61
62 def _CanUpdate(self, client_version, latest_version):
63 """Returns true if the latest_version is greater than the client_version."""
64 client_tokens = client_version.replace('_', '').split('.')
65 latest_tokens = latest_version.replace('_', '').split('.')
66 web.debug('client version %s latest version %s'
67 % (client_version, latest_version))
68 for i in range(4):
69 if int(latest_tokens[i]) == int(client_tokens[i]):
70 continue
71 return int(latest_tokens[i]) > int(client_tokens[i])
72 return False
73
74 def _UnpackStatefulPartition(self, image_path, stateful_file):
75 """Given an image, unpacks its stateful partition to stateful_file."""
76 image_dir = os.path.dirname(image_path)
77 image_file = os.path.basename(image_path)
78
79 get_offset = '$(cgpt show -b -i 1 %s)' % image_file
80 get_size = '$(cgpt show -s -i 1 %s)' % image_file
81 unpack_command = (
82 'cd %s && '
83 'dd if=%s of=%s bs=512 skip=%s count=%s' % (image_dir, image_file,
84 stateful_file, get_offset,
85 get_size))
86 web.debug(unpack_command)
87 return os.system(unpack_command) == 0
88
89 def _UnpackZip(self, image_dir):
90 """Unpacks an image.zip into a given directory."""
91 image = os.path.join(image_dir, self._GetImageName())
92 if os.path.exists(image):
93 return True
94 else:
95 # -n, never clobber an existing file, in case we get invoked
96 # simultaneously by multiple request handlers. This means that
97 # we're assuming each image.zip file lives in a versioned
98 # directory (a la Buildbot).
99 return os.system('cd %s && unzip -n image.zip %s unpack_partitions.sh' %
100 (image_dir, self._GetImageName())) == 0
101
102 def _GetImageName(self):
103 """Returns the name of the image that should be used."""
104 if self.use_test_image:
105 image_name = 'chromiumos_test_image.bin'
106 else:
107 image_name = 'chromiumos_image.bin'
108 return image_name
109
110 def _IsImageNewerThanCached(self, image_path, cached_file_path):
111 """Returns true if the image is newer than the cached image."""
112 # No image to compare against.
113 if not os.path.exists(image_path) and os.path.exists(cached_file_path):
114 return True
115
116 if (os.path.exists(cached_file_path) and
117 os.path.getmtime(cached_file_path) < os.path.getmtime(image_path)):
118 return True
119 else:
120 web.debug('Found usable cached update image at %s instead of %s' %
121 (cached_file_path, image_path))
122 return False
123
124 def _GetSize(self, update_path):
125 """Returns the size of the file given."""
126 return os.path.getsize(update_path)
127
128 def _GetHash(self, update_path):
129 """Returns the sha1 of the file given."""
130 cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
131 % update_path)
132 return os.popen(cmd).read().rstrip()
133
rtc@google.com21a5ca32009-11-04 18:23:23 +0000134 def GetUpdatePayload(self, hash, size, url):
Chris Sosaaae36452010-09-15 17:06:05 -0700135 """Returns a payload to the client corresponding to a new update.
136
137 Args:
138 hash: hash of update blob
139 size: size of update blob
140 url: where to find update blob
141 Returns:
142 Xml string to be passed back to client.
143 """
rtc@google.com21a5ca32009-11-04 18:23:23 +0000144 payload = """<?xml version="1.0" encoding="UTF-8"?>
145 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700146 <daystart elapsed_seconds="%s"/>
rtc@google.com21a5ca32009-11-04 18:23:23 +0000147 <app appid="{%s}" status="ok">
148 <ping status="ok"/>
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700149 <updatecheck
150 codebase="%s"
151 hash="%s"
152 needsadmin="false"
153 size="%s"
rtc@google.com21a5ca32009-11-04 18:23:23 +0000154 status="ok"/>
155 </app>
156 </gupdate>
157 """
Chris Sosaaae36452010-09-15 17:06:05 -0700158 return payload % (self._GetSecondsSinceMidnight(),
Darin Petkov2b2ff4b2010-07-27 15:02:09 -0700159 self.app_id, url, hash, size)
rtc@google.comded22402009-10-26 22:36:21 +0000160
rtc@google.com21a5ca32009-11-04 18:23:23 +0000161 def GetNoUpdatePayload(self):
Chris Sosaaae36452010-09-15 17:06:05 -0700162 """Returns a payload to the client corresponding to no update."""
163 payload = """ < ?xml version = "1.0" encoding = "UTF-8"? >
164 < gupdate xmlns = "http://www.google.com/update2/response" protocol = "2.0" >
165 < daystart elapsed_seconds = "%s" />
166 < app appid = "{%s}" status = "ok" >
167 < ping status = "ok" />
168 < updatecheck status = "noupdate" />
169 </ app >
170 </ gupdate >
rtc@google.com21a5ca32009-11-04 18:23:23 +0000171 """
Chris Sosaaae36452010-09-15 17:06:05 -0700172 return payload % (self._GetSecondsSinceMidnight(), self.app_id)
rtc@google.comded22402009-10-26 22:36:21 +0000173
Chris Sosaaae36452010-09-15 17:06:05 -0700174 def GenerateUpdateFile(self, image_path):
175 """Generates an update gz given a full path to an image.
176
177 Args:
178 image_path: Full path to image.
179 Returns:
180 Path to created update_payload or None on error.
181 """
182 image_dir = os.path.dirname(image_path)
183 update_path = os.path.join(image_dir, 'update.gz')
184 web.debug('Generating update image %s' % update_path)
185
186 mkupdate_command = (
187 '%s/cros_generate_update_payload --image=%s --output=%s '
188 '--patch_kernel' % (self.scripts_dir, image_path, update_path))
189 if os.system(mkupdate_command) != 0:
190 web.debug('Failed to create base update file')
191 return None
192
193 return update_path
194
195 def GenerateStatefulFile(self, image_path):
196 """Generates a stateful update gz given a full path to an image.
197
198 Args:
199 image_path: Full path to image.
200 Returns:
201 Path to created stateful update_payload or None on error.
202 """
203 stateful_partition_path = '%s/stateful.image' % os.path.dirname(image_path)
204
205 # Unpack to get stateful partition.
206 if self._UnpackStatefulPartition(image_path, stateful_partition_path):
207 mkstatefulupdate_command = 'gzip -f %s' % stateful_partition_path
208 if os.system(mkstatefulupdate_command) == 0:
209 web.debug('Successfully generated %s.gz' % stateful_partition_path)
210 return '%s.gz' % stateful_partition_path
211
212 web.debug('Failed to create stateful update file')
213 return None
214
215 def MoveImagesToStaticDir(self, update_path, stateful_update_path,
216 static_image_dir):
217 """Moves gz files from their directories to serving directories.
218
219 Args:
220 update_path: full path to main update gz.
221 stateful_update_path: full path to stateful partition gz.
222 static_image_dir: where to put files.
223 Returns:
224 Returns True if the files were moved over successfully.
225 """
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700226 try:
Chris Sosaaae36452010-09-15 17:06:05 -0700227 shutil.copy(update_path, static_image_dir)
228 shutil.copy(stateful_update_path, static_image_dir)
229 os.remove(update_path)
230 os.remove(stateful_update_path)
231 except Exception:
232 web.debug('Failed to move %s and %s to %s' % (update_path,
233 stateful_update_path,
234 static_image_dir))
235 return False
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700236
rtc@google.com21a5ca32009-11-04 18:23:23 +0000237 return True
rtc@google.comded22402009-10-26 22:36:21 +0000238
Chris Sosaaae36452010-09-15 17:06:05 -0700239 def GenerateUpdateImage(self, image_path, move_to_static_dir=False,
240 static_image_dir=None):
241 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000242
Chris Sosaaae36452010-09-15 17:06:05 -0700243 Args:
244 image_path: full path to the image.
245 move_to_static_dir: Moves the files from their dir to the static dir.
246 static_image_dir: the directory to move images to after generating.
247 Returns:
248 True if the update payload was created successfully.
249 """
250 web.debug('Generating update for image %s' % image_path)
251 update_path = self.GenerateUpdateFile(image_path)
252 stateful_update_path = self.GenerateStatefulFile(image_path)
253 if not update_path or not stateful_update_path:
254 web.debug('Failed to generate update')
255 return False
256
257 if move_to_static_dir:
258 return self.MoveImagesToStaticDir(update_path, stateful_update_path,
259 static_image_dir)
260 else:
261 return True
262
263 def GenerateLatestUpdateImage(self, board_id, client_version,
264 static_image_dir=None):
265 """Generates an update using the latest image that has been built.
266
267 This will only generate an update if the newest update is newer than that
268 on the client or client_version is 'ForcedUpdate'.
269
270 Args:
271 board_id: Name of the board.
272 client_version: Current version of the client or 'ForcedUpdate'
273 static_image_dir: the directory to move images to after generating.
274 Returns:
275 True if the update payload was created successfully.
276 """
277 latest_image_dir = self._GetLatestImageDir(board_id)
278 latest_version = self._GetVersionFromDir(latest_image_dir)
279 latest_image_path = os.path.join(latest_image_dir, self._GetImageName())
280
281 web.debug('Preparing to generate update from latest built image %s.' %
282 latest_image_path)
283
284 # Check to see whether or not we should update.
285 if client_version != 'ForcedUpdate' and not self._CanUpdate(
286 client_version, latest_version):
287 web.debug('no update')
288 return False
289
290 cached_file_path = os.path.join(static_image_dir, 'update.gz')
291 if (os.path.exists(cached_file_path) and
292 not self._IsImageNewerThanCached(latest_image_path, cached_file_path)):
293 return True
294
295 return self.GenerateUpdateImage(latest_image_path, move_to_static_dir=True,
296 static_image_dir=static_image_dir)
297
298 def GenerateImageFromZip(self, static_image_dir):
299 """Generates an update from an image zip file.
300
301 This method assumes you have an image.zip in directory you are serving
302 from. If this file is newer than a previously cached file, it will unzip
303 this file, create a payload and serve it.
304
305 Args:
306 static_image_dir: Directory where the zip file exists.
307 Returns:
308 True if the update payload was created successfully.
309 """
310 web.debug('Preparing to generate update from zip in %s.' % static_image_dir)
311 image_path = os.path.join(static_image_dir, self._GetImageName())
312 cached_file_path = os.path.join(static_image_dir, 'update.gz')
313 if not self._IsImageNewerThanCached(image_path, cached_file_path):
314 return True
315
316 if self._UnpackZip(static_image_dir):
317 web.debug('unzip image.zip failed.')
318 return False
319
320 return self.GenerateUpdateImage(image_path, move_to_static_dir=False,
321 static_image_dir=None)
Darin Petkov8ef83452010-03-23 16:52:29 -0700322
Andrew de los Reyes52620802010-04-12 13:40:07 -0700323 def ImportFactoryConfigFile(self, filename, validate_checksums=False):
324 """Imports a factory-floor server configuration file. The file should
325 be in this format:
326 config = [
327 {
328 'qual_ids': set([1, 2, 3, "x86-generic"]),
329 'factory_image': 'generic-factory.gz',
330 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
331 'release_image': 'generic-release.gz',
332 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
333 'oempartitionimg_image': 'generic-oem.gz',
334 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700335 'efipartitionimg_image': 'generic-efi.gz',
336 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700337 'stateimg_image': 'generic-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800338 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800339 'firmware_image': 'generic-firmware.gz',
340 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700341 },
342 {
343 'qual_ids': set([6]),
344 'factory_image': '6-factory.gz',
345 'factory_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
346 'release_image': '6-release.gz',
347 'release_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
348 'oempartitionimg_image': '6-oem.gz',
349 'oempartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Nick Sanderse1eea922010-05-19 22:17:08 -0700350 'efipartitionimg_image': '6-efi.gz',
351 'efipartitionimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700352 'stateimg_image': '6-state.gz',
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800353 'stateimg_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Tom Wai-Hong Tamdac3df12010-06-14 09:56:15 +0800354 'firmware_image': '6-firmware.gz',
355 'firmware_checksum': 'AtiI8B64agHVN+yeBAyiNMX3+HM=',
Andrew de los Reyes52620802010-04-12 13:40:07 -0700356 },
357 ]
358 The server will look for the files by name in the static files
359 directory.
Chris Sosaa73ec162010-05-03 20:18:02 -0700360
Andrew de los Reyes52620802010-04-12 13:40:07 -0700361 If validate_checksums is True, validates checksums and exits. If
362 a checksum mismatch is found, it's printed to the screen.
363 """
364 f = open(filename, 'r')
365 output = {}
366 exec(f.read(), output)
367 self.factory_config = output['config']
368 success = True
369 for stanza in self.factory_config:
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800370 for key in stanza.copy().iterkeys():
371 suffix = '_image'
372 if key.endswith(suffix):
373 kind = key[:-len(suffix)]
Chris Sosaaae36452010-09-15 17:06:05 -0700374 stanza[kind + '_size'] = os.path.getsize(os.path.join(
375 self.static_dir, stanza[kind + '_image']))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800376 if validate_checksums:
Chris Sosaaae36452010-09-15 17:06:05 -0700377 factory_checksum = self._GetHash(self.static_dir + ' / ' +
378 stanza[kind + '_image'])
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800379 if factory_checksum != stanza[kind + '_checksum']:
Chris Sosaaae36452010-09-15 17:06:05 -0700380 print ('Error: checksum mismatch for %s. Expected "%s" but file '
381 'has checksum "%s".' % (stanza[kind + '_image'],
382 stanza[kind + '_checksum'],
383 factory_checksum))
Tom Wai-Hong Tam65fc6072010-05-20 11:44:26 +0800384 success = False
Chris Sosaaae36452010-09-15 17:06:05 -0700385
Andrew de los Reyes52620802010-04-12 13:40:07 -0700386 if validate_checksums:
387 if success is False:
388 raise Exception('Checksum mismatch in conf file.')
Chris Sosaaae36452010-09-15 17:06:05 -0700389
Andrew de los Reyes52620802010-04-12 13:40:07 -0700390 print 'Config file looks good.'
391
392 def GetFactoryImage(self, board_id, channel):
Chris Sosaaae36452010-09-15 17:06:05 -0700393 kind = channel.rsplit(' - ', 1)[0]
Andrew de los Reyes52620802010-04-12 13:40:07 -0700394 for stanza in self.factory_config:
395 if board_id not in stanza['qual_ids']:
396 continue
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700397 if kind + '_image' not in stanza:
398 break
Andrew de los Reyes52620802010-04-12 13:40:07 -0700399 return (stanza[kind + '_image'],
400 stanza[kind + '_checksum'],
401 stanza[kind + '_size'])
Nick Sanders15cd6ae2010-06-30 12:30:56 -0700402 return (None, None, None)
rtc@google.comded22402009-10-26 22:36:21 +0000403
Chris Sosaaae36452010-09-15 17:06:05 -0700404 def HandleFactoryRequest(self, hostname, board_id, channel):
405 (filename, checksum, size) = self.GetFactoryImage(board_id, channel)
406 if filename is None:
407 web.debug('unable to find image for board %s' % board_id)
408 return self.GetNoUpdatePayload()
409 url = 'http://%s/static/%s' % (hostname, filename)
410 web.debug('returning update payload ' + url)
411 return self.GetUpdatePayload(checksum, size, url)
412
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700413 def HandleUpdatePing(self, data, label=None):
Chris Sosaaae36452010-09-15 17:06:05 -0700414 """Handles an update ping from an update client.
415
416 Args:
417 data: xml blob from client.
418 label: optional label for the update.
419 Returns:
420 Update payload message for client.
421 """
422 web.debug('handling update ping: %s' % data)
rtc@google.com21a5ca32009-11-04 18:23:23 +0000423 update_dom = minidom.parseString(data)
424 root = update_dom.firstChild
Chris Sosaaae36452010-09-15 17:06:05 -0700425
426 # Check the client prefix to make sure you can support this type of update.
427 if (root.hasAttribute('updaterversion') and
428 not root.getAttribute('updaterversion').startswith(self.client_prefix)):
429 web.debug('Got update from unsupported updater:' +
430 root.getAttribute('updaterversion'))
Andrew de los Reyes9223f132010-05-07 17:08:17 -0700431 return self.GetNoUpdatePayload()
Chris Sosaaae36452010-09-15 17:06:05 -0700432
433 # We only generate update payloads for updatecheck requests.
434 update_check = root.getElementsByTagName('o:updatecheck')
435 if not update_check:
436 web.debug('Non-update check received. Returning blank payload.')
437 # TODO(sosa): Generate correct non-updatecheck payload to better test
438 # update clients.
439 return self.GetNoUpdatePayload()
440
441 # Since this is an updatecheck, get information about the requester.
442 hostname = web.ctx.host
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700443 query = root.getElementsByTagName('o:app')[0]
Charlie Lee8c993082010-02-24 13:27:37 -0800444 client_version = query.getAttribute('version')
Andrew de los Reyes52620802010-04-12 13:40:07 -0700445 channel = query.getAttribute('track')
Chris Sosaaae36452010-09-15 17:06:05 -0700446 board_id = (query.hasAttribute('board') and query.getAttribute('board')
447 or self._GetDefaultBoardID())
Andrew de los Reyes52620802010-04-12 13:40:07 -0700448
Chris Sosaaae36452010-09-15 17:06:05 -0700449 # Separate logic as Factory requests have static url's that override
450 # other options.
Andrew de los Reyes52620802010-04-12 13:40:07 -0700451 if self.factory_config:
Chris Sosaaae36452010-09-15 17:06:05 -0700452 return self.HandleFactoryRequest(hostname, board_id, channel)
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700453 else:
Chris Sosaaae36452010-09-15 17:06:05 -0700454 static_image_dir = self.static_dir
455 if label:
456 static_image_dir = os.path.join(static_image_dir, label)
457
458 # Not for factory, find and serve the correct image given the options.
459 if self.forced_image:
460 has_built_image = self.GenerateUpdateImage(
461 self.forced_image, move_to_static_dir=True,
462 static_image_dir=static_image_dir)
463 # Now that we've generated it, clear out so that other pings of same
464 # devserver instance do not generate new images.
465 self.forced_image = None
466 elif self.serve_only:
467 has_built_image = self.GenerateImageFromZip(static_image_dir)
468 else:
469 has_built_image = self.GenerateLatestUpdateImage(board_id,
470 client_version,
471 static_image_dir)
472
473 if has_built_image:
474 hash = self._GetHash(os.path.join(static_image_dir, 'update.gz'))
475 size = self._GetSize(os.path.join(static_image_dir, 'update.gz'))
476 if self.static_urlbase and label:
477 url = '%s/%s/update.gz' % (self.static_urlbase, label)
478 elif self.serve_only:
479 url = 'http://%s/static/archive/update.gz' % hostname
480 else:
481 url = 'http://%s/static/update.gz' % hostname
482 return self.GetUpdatePayload(hash, size, url)
483 else:
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700484 return self.GetNoUpdatePayload()