blob: 7b1e5b2fbfd86770dfb75888511a8da04b0c6e49 [file] [log] [blame]
joychen3cb228e2013-06-12 12:13:13 -07001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import datetime
6import operator
7import os
8import time
joychen3cb228e2013-06-12 12:13:13 -07009import shutil
10import threading
11
joychen921e1fb2013-06-28 11:12:20 -070012import build_util
joychen3cb228e2013-06-12 12:13:13 -070013import artifact_info
14import build_artifact
15import common_util
16import devserver_constants
17import downloader
18import log_util
19
20# Module-local log function.
21def _Log(message, *args):
22 return log_util.LogWithTag('XBUDDY', message, *args)
23
joychen3cb228e2013-06-12 12:13:13 -070024_XBUDDY_CAPACITY = 5
joychen921e1fb2013-06-28 11:12:20 -070025
26# Local build constants
27LATEST_LOCAL = "latest-local"
28LOCAL_ALIASES = [
29 'test',
30 'base',
31 'dev',
32]
33
34LOCAL_FILE_NAMES = [
35 devserver_constants.TEST_IMAGE_FILE,
36 devserver_constants.BASE_IMAGE_FILE,
37 devserver_constants.IMAGE_FILE,
38]
39
40LOCAL_ALIAS_TO_FILENAME = dict(zip(LOCAL_ALIASES, LOCAL_FILE_NAMES))
41
42# Google Storage constants
43GS_ALIASES = [
joychen3cb228e2013-06-12 12:13:13 -070044 'test',
45 'base',
46 'recovery',
47 'full_payload',
48 'stateful',
49 'autotest',
50]
51
52# TODO(joyc) these should become devserver constants.
53# currently, storage locations are embedded in the artifact classes defined in
54# build_artifact
55
joychen921e1fb2013-06-28 11:12:20 -070056GS_FILE_NAMES = [
57 devserver_constants.TEST_IMAGE_FILE,
58 devserver_constants.BASE_IMAGE_FILE,
59 devserver_constants.RECOVERY_IMAGE_FILE,
joychen3cb228e2013-06-12 12:13:13 -070060 devserver_constants.ROOT_UPDATE_FILE,
61 build_artifact.STATEFUL_UPDATE_FILE,
62 devserver_constants.AUTOTEST_DIR,
63]
64
65ARTIFACTS = [
66 artifact_info.TEST_IMAGE,
67 artifact_info.BASE_IMAGE,
68 artifact_info.RECOVERY_IMAGE,
69 artifact_info.FULL_PAYLOAD,
70 artifact_info.STATEFUL_PAYLOAD,
71 artifact_info.AUTOTEST,
72]
73
joychen921e1fb2013-06-28 11:12:20 -070074GS_ALIAS_TO_FILENAME = dict(zip(GS_ALIASES, GS_FILE_NAMES))
75GS_ALIAS_TO_ARTIFACT = dict(zip(GS_ALIASES, ARTIFACTS))
joychen3cb228e2013-06-12 12:13:13 -070076
joychen921e1fb2013-06-28 11:12:20 -070077LATEST_OFFICIAL = "latest-official"
78VERSION_PREFIX = "R"
joychen3cb228e2013-06-12 12:13:13 -070079
80LATEST = "latest"
81
82CHANNEL = [
83 'stable',
84 'beta',
85 'dev',
86 'canary',
87]
88
89# only paired with official
90SUFFIX = [
91 'release',
92 'paladin',
93 'factory',
94]
95
96class XBuddyException(Exception):
97 """Exception classes used by this module."""
98 pass
99
100
101# no __init__ method
102#pylint: disable=W0232
103class Timestamp():
104 """Class to translate build path strings and timestamp filenames."""
105
106 _TIMESTAMP_DELIMITER = 'SLASH'
107 XBUDDY_TIMESTAMP_DIR = 'xbuddy_UpdateTimestamps'
108
109 @staticmethod
110 def TimestampToBuild(timestamp_filename):
111 return timestamp_filename.replace(Timestamp._TIMESTAMP_DELIMITER, '/')
112
113 @staticmethod
114 def BuildToTimestamp(build_path):
115 return build_path.replace('/', Timestamp._TIMESTAMP_DELIMITER)
joychen921e1fb2013-06-28 11:12:20 -0700116
117 @staticmethod
118 def UpdateTimestamp(timestamp_dir, build_id):
119 """Update timestamp file of build with build_id."""
120 common_util.MkDirP(timestamp_dir)
121 time_file = os.path.join(timestamp_dir,
122 Timestamp.BuildToTimestamp(build_id))
123 with file(time_file, 'a'):
124 os.utime(time_file, None)
joychen3cb228e2013-06-12 12:13:13 -0700125#pylint: enable=W0232
126
127
joychen921e1fb2013-06-28 11:12:20 -0700128class XBuddy(build_util.BuildObject):
joychen3cb228e2013-06-12 12:13:13 -0700129 """Class that manages image retrieval and caching by the devserver.
130
131 Image retrieval by xBuddy path:
132 XBuddy accesses images and artifacts that it stores using an xBuddy
133 path of the form: board/version/alias
134 The primary xbuddy.Get call retrieves the correct artifact or url to where
135 the artifacts can be found.
136
137 Image caching:
138 Images and other artifacts are stored identically to how they would have
139 been if devserver's stage rpc was called and the xBuddy cache replaces
140 build versions on a LRU basis. Timestamps are maintained by last accessed
141 times of representative files in the a directory in the static serve
142 directory (XBUDDY_TIMESTAMP_DIR).
143
144 Private class members:
145 _true_values - used for interpreting boolean values
146 _staging_thread_count - track download requests
joychen921e1fb2013-06-28 11:12:20 -0700147 _timestamp_folder - directory with empty files standing in as timestamps
148 for each image currently cached by xBuddy
joychen3cb228e2013-06-12 12:13:13 -0700149 """
150 _true_values = ['true', 't', 'yes', 'y']
151
152 # Number of threads that are staging images.
153 _staging_thread_count = 0
154 # Lock used to lock increasing/decreasing count.
155 _staging_thread_count_lock = threading.Lock()
156
joychen5260b9a2013-07-16 14:48:01 -0700157 def __init__(self, manage_builds=False, **kwargs):
joychen921e1fb2013-06-28 11:12:20 -0700158 super(XBuddy, self).__init__(**kwargs)
joychen5260b9a2013-07-16 14:48:01 -0700159 self._manage_builds = manage_builds
joychen921e1fb2013-06-28 11:12:20 -0700160 self._timestamp_folder = os.path.join(self.static_dir,
joychen3cb228e2013-06-12 12:13:13 -0700161 Timestamp.XBUDDY_TIMESTAMP_DIR)
162
163 @classmethod
164 def ParseBoolean(cls, boolean_string):
165 """Evaluate a string to a boolean value"""
166 if boolean_string:
167 return boolean_string.lower() in cls._true_values
168 else:
169 return False
170
171 @staticmethod
172 def _TryIndex(alias_chunks, index):
173 """Attempt to access an index of an alias. Default None if not found."""
174 try:
175 return alias_chunks[index]
176 except IndexError:
177 return None
178
joychen921e1fb2013-06-28 11:12:20 -0700179 #pylint: disable=W0613
joychen3cb228e2013-06-12 12:13:13 -0700180 def _ResolveVersion(self, board, version):
181 """
joychen921e1fb2013-06-28 11:12:20 -0700182 Handle version aliases for remote payloads in GS.
joychen3cb228e2013-06-12 12:13:13 -0700183
184 Args:
185 board: as specified in the original call. (i.e. x86-generic, parrot)
186 version: as entered in the original call. can be
187 {TBD, 0. some custom alias as defined in a config file}
188 1. latest
189 2. latest-{channel}
190 3. latest-official-{board suffix}
191 4. version prefix (i.e. RX-Y.X, RX-Y, RX)
joychen3cb228e2013-06-12 12:13:13 -0700192
193 Returns:
194 Version number that is compatible with google storage (i.e. RX-X.X.X)
195
196 """
197 # TODO (joyc) read from a config file
198
joychen921e1fb2013-06-28 11:12:20 -0700199 if version.startswith(VERSION_PREFIX):
joychen3cb228e2013-06-12 12:13:13 -0700200 # TODO (joyc) Find complete version if it's only a prefix.
201 return version
202
203 else:
204 # The given version doesn't match any known patterns.
joychen921e1fb2013-06-28 11:12:20 -0700205 raise XBuddyException("Version %s unknown. Can't find on GS." % version)
206 #pylint: enable=W0613
joychen3cb228e2013-06-12 12:13:13 -0700207
joychen5260b9a2013-07-16 14:48:01 -0700208 @staticmethod
209 def _Symlink(link, target):
210 """Symlinks link to target, and removes whatever link was there before."""
211 _Log("Linking to %s from %s", link, target)
212 if os.path.lexists(link):
213 os.unlink(link)
214 os.symlink(target, link)
215
joychen921e1fb2013-06-28 11:12:20 -0700216 def _GetLatestLocalVersion(self, board, file_name):
217 """Get the version of the latest image built for board by build_image
218
219 Updates the symlink reference within the xBuddy static dir to point to
220 the real image dir in the local /build/images directory.
221
222 Args:
223 board - board-suffix
224 file_name - the filename of the image we have cached
225
226 Returns:
227 version - the discovered version of the image.
joychen3cb228e2013-06-12 12:13:13 -0700228 """
joychen921e1fb2013-06-28 11:12:20 -0700229 latest_local_dir = self.GetLatestImageDir(board)
230 if not (latest_local_dir and os.path.exists(latest_local_dir)):
231 raise XBuddyException('No builds found for %s. Did you run build_image?' %
232 board)
233
234 # assume that the version number is the name of the directory
235 version = os.path.basename(latest_local_dir)
236
237 path_to_image = os.path.join(latest_local_dir, file_name)
238 if not os.path.exists(path_to_image):
239 raise XBuddyException('%s not found in %s. Did you run build_image?' %
240 (file_name, latest_local_dir))
241
242 # symlink the directories
243 common_util.MkDirP(os.path.join(self.static_dir, board))
244 link = os.path.join(self.static_dir, board, version)
joychen5260b9a2013-07-16 14:48:01 -0700245 XBuddy._Symlink(link, latest_local_dir)
joychen921e1fb2013-06-28 11:12:20 -0700246
247 return version
248
249 def _InterpretPath(self, path_list):
250 """
251 Split and return the pieces of an xBuddy path name
joychen3cb228e2013-06-12 12:13:13 -0700252
253 input:
joychen921e1fb2013-06-28 11:12:20 -0700254 path_list: the segments of the path xBuddy Get was called with.
255 Documentation of path_list can be found in devserver.py:xbuddy
joychen3cb228e2013-06-12 12:13:13 -0700256
257 Return:
joychen921e1fb2013-06-28 11:12:20 -0700258 tuple of (board, version, image_type)
joychen3cb228e2013-06-12 12:13:13 -0700259
260 Raises:
261 XBuddyException: if the path can't be resolved into valid components
262 """
joychen921e1fb2013-06-28 11:12:20 -0700263 if len(path_list) == 3:
joychen3cb228e2013-06-12 12:13:13 -0700264 # We have a full path, with b/v/a
joychen921e1fb2013-06-28 11:12:20 -0700265 board, version, image_type = path_list
266 elif len(path_list) == 2:
joychen3cb228e2013-06-12 12:13:13 -0700267 # We have only the board and the version, default to test image
joychen921e1fb2013-06-28 11:12:20 -0700268 board, version = path_list
269 image_type = GS_ALIASES[0]
270 elif len(path_list) == 1:
joychen3cb228e2013-06-12 12:13:13 -0700271 # We have only the board. default to latest test image.
joychen921e1fb2013-06-28 11:12:20 -0700272 board = path_list[0]
joychen3cb228e2013-06-12 12:13:13 -0700273 version = LATEST
joychen921e1fb2013-06-28 11:12:20 -0700274 image_type = GS_ALIASES[0]
joychen3cb228e2013-06-12 12:13:13 -0700275 else:
276 # Misshapen beyond recognition
joychen921e1fb2013-06-28 11:12:20 -0700277 raise XBuddyException('Invalid path, %s.' % '/'.join(path_list))
joychen3cb228e2013-06-12 12:13:13 -0700278
279 _Log("board: %s, version: %s, image: %s", board, version, image_type)
280
281 return board, version, image_type
282
283 @staticmethod
joychen921e1fb2013-06-28 11:12:20 -0700284 def _LookupVersion(board, version_type, suffix="release"):
285 """Crawl gs for actual version numbers.
286
287 If all we have is the board, we default to the latest local build. If we
288 have latest-official, we default to latest in the release channel.
289 """
290 # TODO (joyc) latest-official (LATEST-master) on
291 # crawl gs://chromeos-image-archive/
292
293 # TODO (joyc) latest-{dev/beta/stable} on
294 # crawl gs://chromeos-releases/
joychen3cb228e2013-06-12 12:13:13 -0700295 raise NotImplementedError()
296
joychen921e1fb2013-06-28 11:12:20 -0700297 def _SyncRegistryWithBuildImages(self):
joychen5260b9a2013-07-16 14:48:01 -0700298 """ Crawl images_dir for build_ids of images generated from build_image.
299
300 This will find images and symlink them in xBuddy's static dir so that
301 xBuddy's cache can serve them.
302 If xBuddy's _manage_builds option is on, then a timestamp will also be
303 generated, and xBuddy will clear them from the directory they are in, as
304 necessary.
305 """
joychen921e1fb2013-06-28 11:12:20 -0700306 build_ids = []
307 for b in os.listdir(self.images_dir):
joychen5260b9a2013-07-16 14:48:01 -0700308 # Ensure we have directories to track all boards in build/images
309 common_util.MkDirP(os.path.join(self.static_dir, b))
joychen921e1fb2013-06-28 11:12:20 -0700310 board_dir = os.path.join(self.images_dir, b)
311 build_ids.extend(['/'.join([b, v]) for v
312 in os.listdir(board_dir) if not v==LATEST])
313
314 # Check currently registered images
315 for f in os.listdir(self._timestamp_folder):
316 build_id = Timestamp.TimestampToBuild(f)
317 if build_id in build_ids:
318 build_ids.remove(build_id)
319
joychen5260b9a2013-07-16 14:48:01 -0700320 # Symlink undiscovered images, and update timestamps if manage_builds is on
321 for build_id in build_ids:
322 link = os.path.join(self.static_dir, build_id)
323 target = os.path.join(self.images_dir, build_id)
324 XBuddy._Symlink(link, target)
325 if self._manage_builds:
326 Timestamp.UpdateTimestamp(self._timestamp_folder, build_id)
joychen921e1fb2013-06-28 11:12:20 -0700327
328 def _ListBuildTimes(self):
joychen3cb228e2013-06-12 12:13:13 -0700329 """ Returns the currently cached builds and their last access timestamp.
330
331 Returns:
332 list of tuples that matches xBuddy build/version to timestamps in long
333 """
334 # update currently cached builds
335 build_dict = {}
336
joychen921e1fb2013-06-28 11:12:20 -0700337 common_util.MkDirP(self._timestamp_folder)
joychen3cb228e2013-06-12 12:13:13 -0700338 filenames = os.listdir(self._timestamp_folder)
339 for f in filenames:
340 last_accessed = os.path.getmtime(os.path.join(self._timestamp_folder, f))
341 build_id = Timestamp.TimestampToBuild(f)
342 stale_time = datetime.timedelta(seconds = (time.time()-last_accessed))
joychen921e1fb2013-06-28 11:12:20 -0700343 build_dict[build_id] = stale_time
joychen3cb228e2013-06-12 12:13:13 -0700344 return_tup = sorted(build_dict.iteritems(), key=operator.itemgetter(1))
345 return return_tup
346
joychen3cb228e2013-06-12 12:13:13 -0700347 def _Download(self, gs_url, artifact):
348 """Download the single artifact from the given gs_url."""
349 with XBuddy._staging_thread_count_lock:
350 XBuddy._staging_thread_count += 1
351 try:
joychen921e1fb2013-06-28 11:12:20 -0700352 downloader.Downloader(self.static_dir, gs_url).Download(
joychen3cb228e2013-06-12 12:13:13 -0700353 [artifact])
354 finally:
355 with XBuddy._staging_thread_count_lock:
356 XBuddy._staging_thread_count -= 1
357
358 def _CleanCache(self):
359 """Delete all builds besides the first _XBUDDY_CAPACITY builds"""
joychen921e1fb2013-06-28 11:12:20 -0700360 self._SyncRegistryWithBuildImages()
361 cached_builds = [e[0] for e in self._ListBuildTimes()]
joychen3cb228e2013-06-12 12:13:13 -0700362 _Log('In cache now: %s', cached_builds)
363
364 for b in range(_XBUDDY_CAPACITY, len(cached_builds)):
365 b_path = cached_builds[b]
366 _Log('Clearing %s from cache', b_path)
367
368 time_file = os.path.join(self._timestamp_folder,
369 Timestamp.BuildToTimestamp(b_path))
joychen921e1fb2013-06-28 11:12:20 -0700370 os.unlink(time_file)
371 clear_dir = os.path.join(self.static_dir, b_path)
joychen3cb228e2013-06-12 12:13:13 -0700372 try:
joychen5260b9a2013-07-16 14:48:01 -0700373 # handle symlinks, in the case of links to local builds if enabled
374 if self._manage_builds and os.path.islink(clear_dir):
375 target = os.readlink(clear_dir)
376 _Log('Deleting locally built image at %s', target)
joychen921e1fb2013-06-28 11:12:20 -0700377
378 os.unlink(clear_dir)
joychen5260b9a2013-07-16 14:48:01 -0700379 if os.path.exists(target):
joychen921e1fb2013-06-28 11:12:20 -0700380 shutil.rmtree(target)
381 elif os.path.exists(clear_dir):
joychen5260b9a2013-07-16 14:48:01 -0700382 _Log('Deleting downloaded image at %s', clear_dir)
joychen3cb228e2013-06-12 12:13:13 -0700383 shutil.rmtree(clear_dir)
joychen921e1fb2013-06-28 11:12:20 -0700384
joychen3cb228e2013-06-12 12:13:13 -0700385 except Exception:
386 raise XBuddyException('Failed to clear build in %s.' % clear_dir)
387
joychen921e1fb2013-06-28 11:12:20 -0700388 def _GetFromGS(self, build_id, image_type):
389 """Check if the artifact is available locally. Download from GS if not."""
390 gs_url = os.path.join(devserver_constants.GOOGLE_STORAGE_IMAGE_DIR,
391 build_id)
392
393 # stage image if not found in cache
394 file_name = GS_ALIAS_TO_FILENAME[image_type]
395 cached = os.path.exists(os.path.join(self.static_dir,
396 build_id,
397 file_name))
398 if not cached:
399 artifact = GS_ALIAS_TO_ARTIFACT[image_type]
400 _Log('Artifact to stage: %s', artifact)
401
402 _Log('Staging %s image from: %s', image_type, gs_url)
403 self._Download(gs_url, artifact)
404 else:
405 _Log('Image already cached.')
406
407 def _GetArtifact(self, path):
408 """Interpret an xBuddy path and return directory/file_name to resource."""
409 board, version, image_type = self._InterpretPath(path)
410
411 if version in [LATEST_LOCAL, '']:
412 # Get a local image
413 if image_type not in LOCAL_ALIASES:
414 raise XBuddyException('Bad image type: %s. Use one of: %s' %
415 (image_type, LOCAL_ALIASES))
416
417 file_name = LOCAL_ALIAS_TO_FILENAME[image_type]
418 version = self._GetLatestLocalVersion(board, file_name)
419 build_id = os.path.join(board, version)
420 else:
421 # Get a remote image
422 if image_type not in GS_ALIASES:
423 raise XBuddyException('Bad image type: %s. Use one of: %s' %
424 (image_type, GS_ALIASES))
425
426 # Clean up board
427 # TODO(joyc) decide what to do with the board suffix
428
429 # Clean up version
430 file_name = GS_ALIAS_TO_FILENAME[image_type]
431 version = self._ResolveVersion(board, version)
432 build_id = os.path.join(board, version)
433
434 self._GetFromGS(build_id, image_type)
435
436 return build_id, file_name
joychen3cb228e2013-06-12 12:13:13 -0700437
438 ############################ BEGIN PUBLIC METHODS
439
440 def List(self):
441 """Lists the currently available images & time since last access."""
joychen921e1fb2013-06-28 11:12:20 -0700442 self._SyncRegistryWithBuildImages()
443 builds = self._ListBuildTimes()
444 return_string = ''
445 for build, timestamp in builds:
446 return_string += '<b>' + build + '</b> '
447 return_string += '(time since last access: ' + str(timestamp) + ')<br>'
448 return return_string
joychen3cb228e2013-06-12 12:13:13 -0700449
450 def Capacity(self):
451 """Returns the number of images cached by xBuddy."""
452 return str(_XBUDDY_CAPACITY)
453
joychen921e1fb2013-06-28 11:12:20 -0700454 def Get(self, path_list, return_dir=False):
455 """The full xBuddy call, returns resource specified by path_list.
joychen3cb228e2013-06-12 12:13:13 -0700456
457 Please see devserver.py:xbuddy for full documentation.
458 Args:
joychen921e1fb2013-06-28 11:12:20 -0700459 path_list: [board, version, alias] as split from the xbuddy call url
joychen3cb228e2013-06-12 12:13:13 -0700460 return_dir: boolean, if set to true, returns the dir name instead.
461
462 Returns:
463 Path to the image or update directory on the devserver.
464 e.g. http://host/static/x86-generic-release/
465 R26-4000.0.0/chromium-test-image.bin
466 or
467 http://host/static/x86-generic-release/R26-4000.0.0/
468
469 Raises:
470 XBuddyException if path is invalid or XBuddy's cache fails
471 """
joychen921e1fb2013-06-28 11:12:20 -0700472 build_id, file_name = self._GetArtifact(path_list)
joychen3cb228e2013-06-12 12:13:13 -0700473
joychen921e1fb2013-06-28 11:12:20 -0700474 Timestamp.UpdateTimestamp(self._timestamp_folder, build_id)
joychen3cb228e2013-06-12 12:13:13 -0700475
476 #TODO (joyc): run in sep thread
477 self._CleanCache()
478
479 #TODO (joyc) static dir dependent on bug id: 214373
joychen921e1fb2013-06-28 11:12:20 -0700480 return_url = os.path.join('static', build_id)
joychen3cb228e2013-06-12 12:13:13 -0700481 if not return_dir:
482 return_url = os.path.join(return_url, file_name)
483
484 _Log('Returning path to payload: %s', return_url)
485 return return_url