blob: 5054aaeaae665bb7bc87e0dd0d9a534494058606 [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
joychen921e1fb2013-06-28 11:12:20 -0700157 def __init__(self, **kwargs):
158 super(XBuddy, self).__init__(**kwargs)
159 self._timestamp_folder = os.path.join(self.static_dir,
joychen3cb228e2013-06-12 12:13:13 -0700160 Timestamp.XBUDDY_TIMESTAMP_DIR)
161
162 @classmethod
163 def ParseBoolean(cls, boolean_string):
164 """Evaluate a string to a boolean value"""
165 if boolean_string:
166 return boolean_string.lower() in cls._true_values
167 else:
168 return False
169
170 @staticmethod
171 def _TryIndex(alias_chunks, index):
172 """Attempt to access an index of an alias. Default None if not found."""
173 try:
174 return alias_chunks[index]
175 except IndexError:
176 return None
177
joychen921e1fb2013-06-28 11:12:20 -0700178 #pylint: disable=W0613
joychen3cb228e2013-06-12 12:13:13 -0700179 def _ResolveVersion(self, board, version):
180 """
joychen921e1fb2013-06-28 11:12:20 -0700181 Handle version aliases for remote payloads in GS.
joychen3cb228e2013-06-12 12:13:13 -0700182
183 Args:
184 board: as specified in the original call. (i.e. x86-generic, parrot)
185 version: as entered in the original call. can be
186 {TBD, 0. some custom alias as defined in a config file}
187 1. latest
188 2. latest-{channel}
189 3. latest-official-{board suffix}
190 4. version prefix (i.e. RX-Y.X, RX-Y, RX)
joychen3cb228e2013-06-12 12:13:13 -0700191
192 Returns:
193 Version number that is compatible with google storage (i.e. RX-X.X.X)
194
195 """
196 # TODO (joyc) read from a config file
197
joychen921e1fb2013-06-28 11:12:20 -0700198 if version.startswith(VERSION_PREFIX):
joychen3cb228e2013-06-12 12:13:13 -0700199 # TODO (joyc) Find complete version if it's only a prefix.
200 return version
201
202 else:
203 # The given version doesn't match any known patterns.
joychen921e1fb2013-06-28 11:12:20 -0700204 raise XBuddyException("Version %s unknown. Can't find on GS." % version)
205 #pylint: enable=W0613
joychen3cb228e2013-06-12 12:13:13 -0700206
joychen921e1fb2013-06-28 11:12:20 -0700207 def _GetLatestLocalVersion(self, board, file_name):
208 """Get the version of the latest image built for board by build_image
209
210 Updates the symlink reference within the xBuddy static dir to point to
211 the real image dir in the local /build/images directory.
212
213 Args:
214 board - board-suffix
215 file_name - the filename of the image we have cached
216
217 Returns:
218 version - the discovered version of the image.
joychen3cb228e2013-06-12 12:13:13 -0700219 """
joychen921e1fb2013-06-28 11:12:20 -0700220 latest_local_dir = self.GetLatestImageDir(board)
221 if not (latest_local_dir and os.path.exists(latest_local_dir)):
222 raise XBuddyException('No builds found for %s. Did you run build_image?' %
223 board)
224
225 # assume that the version number is the name of the directory
226 version = os.path.basename(latest_local_dir)
227
228 path_to_image = os.path.join(latest_local_dir, file_name)
229 if not os.path.exists(path_to_image):
230 raise XBuddyException('%s not found in %s. Did you run build_image?' %
231 (file_name, latest_local_dir))
232
233 # symlink the directories
234 common_util.MkDirP(os.path.join(self.static_dir, board))
235 link = os.path.join(self.static_dir, board, version)
236 _Log("Linking to %s from %s", link, latest_local_dir)
237 if os.path.lexists(link):
238 os.unlink(link)
239 os.symlink(latest_local_dir, link)
240
241 return version
242
243 def _InterpretPath(self, path_list):
244 """
245 Split and return the pieces of an xBuddy path name
joychen3cb228e2013-06-12 12:13:13 -0700246
247 input:
joychen921e1fb2013-06-28 11:12:20 -0700248 path_list: the segments of the path xBuddy Get was called with.
249 Documentation of path_list can be found in devserver.py:xbuddy
joychen3cb228e2013-06-12 12:13:13 -0700250
251 Return:
joychen921e1fb2013-06-28 11:12:20 -0700252 tuple of (board, version, image_type)
joychen3cb228e2013-06-12 12:13:13 -0700253
254 Raises:
255 XBuddyException: if the path can't be resolved into valid components
256 """
joychen921e1fb2013-06-28 11:12:20 -0700257 if len(path_list) == 3:
joychen3cb228e2013-06-12 12:13:13 -0700258 # We have a full path, with b/v/a
joychen921e1fb2013-06-28 11:12:20 -0700259 board, version, image_type = path_list
260 elif len(path_list) == 2:
joychen3cb228e2013-06-12 12:13:13 -0700261 # We have only the board and the version, default to test image
joychen921e1fb2013-06-28 11:12:20 -0700262 board, version = path_list
263 image_type = GS_ALIASES[0]
264 elif len(path_list) == 1:
joychen3cb228e2013-06-12 12:13:13 -0700265 # We have only the board. default to latest test image.
joychen921e1fb2013-06-28 11:12:20 -0700266 board = path_list[0]
joychen3cb228e2013-06-12 12:13:13 -0700267 version = LATEST
joychen921e1fb2013-06-28 11:12:20 -0700268 image_type = GS_ALIASES[0]
joychen3cb228e2013-06-12 12:13:13 -0700269 else:
270 # Misshapen beyond recognition
joychen921e1fb2013-06-28 11:12:20 -0700271 raise XBuddyException('Invalid path, %s.' % '/'.join(path_list))
joychen3cb228e2013-06-12 12:13:13 -0700272
273 _Log("board: %s, version: %s, image: %s", board, version, image_type)
274
275 return board, version, image_type
276
277 @staticmethod
joychen921e1fb2013-06-28 11:12:20 -0700278 def _LookupVersion(board, version_type, suffix="release"):
279 """Crawl gs for actual version numbers.
280
281 If all we have is the board, we default to the latest local build. If we
282 have latest-official, we default to latest in the release channel.
283 """
284 # TODO (joyc) latest-official (LATEST-master) on
285 # crawl gs://chromeos-image-archive/
286
287 # TODO (joyc) latest-{dev/beta/stable} on
288 # crawl gs://chromeos-releases/
joychen3cb228e2013-06-12 12:13:13 -0700289 raise NotImplementedError()
290
joychen921e1fb2013-06-28 11:12:20 -0700291 def _SyncRegistryWithBuildImages(self):
292 # crawl images_dir for build_ids of images generated from build_image
293 build_ids = []
294 for b in os.listdir(self.images_dir):
295 board_dir = os.path.join(self.images_dir, b)
296 build_ids.extend(['/'.join([b, v]) for v
297 in os.listdir(board_dir) if not v==LATEST])
298
299 # Check currently registered images
300 for f in os.listdir(self._timestamp_folder):
301 build_id = Timestamp.TimestampToBuild(f)
302 if build_id in build_ids:
303 build_ids.remove(build_id)
304
305 # Add undiscovered images if there are any
306 for b in build_ids:
307 Timestamp.UpdateTimestamp(self._timestamp_folder, b)
308
309 def _ListBuildTimes(self):
joychen3cb228e2013-06-12 12:13:13 -0700310 """ Returns the currently cached builds and their last access timestamp.
311
312 Returns:
313 list of tuples that matches xBuddy build/version to timestamps in long
314 """
315 # update currently cached builds
316 build_dict = {}
317
joychen921e1fb2013-06-28 11:12:20 -0700318 common_util.MkDirP(self._timestamp_folder)
joychen3cb228e2013-06-12 12:13:13 -0700319 filenames = os.listdir(self._timestamp_folder)
320 for f in filenames:
321 last_accessed = os.path.getmtime(os.path.join(self._timestamp_folder, f))
322 build_id = Timestamp.TimestampToBuild(f)
323 stale_time = datetime.timedelta(seconds = (time.time()-last_accessed))
joychen921e1fb2013-06-28 11:12:20 -0700324 build_dict[build_id] = stale_time
joychen3cb228e2013-06-12 12:13:13 -0700325 return_tup = sorted(build_dict.iteritems(), key=operator.itemgetter(1))
326 return return_tup
327
joychen3cb228e2013-06-12 12:13:13 -0700328 def _Download(self, gs_url, artifact):
329 """Download the single artifact from the given gs_url."""
330 with XBuddy._staging_thread_count_lock:
331 XBuddy._staging_thread_count += 1
332 try:
joychen921e1fb2013-06-28 11:12:20 -0700333 downloader.Downloader(self.static_dir, gs_url).Download(
joychen3cb228e2013-06-12 12:13:13 -0700334 [artifact])
335 finally:
336 with XBuddy._staging_thread_count_lock:
337 XBuddy._staging_thread_count -= 1
338
339 def _CleanCache(self):
340 """Delete all builds besides the first _XBUDDY_CAPACITY builds"""
joychen921e1fb2013-06-28 11:12:20 -0700341 self._SyncRegistryWithBuildImages()
342 cached_builds = [e[0] for e in self._ListBuildTimes()]
joychen3cb228e2013-06-12 12:13:13 -0700343 _Log('In cache now: %s', cached_builds)
344
345 for b in range(_XBUDDY_CAPACITY, len(cached_builds)):
346 b_path = cached_builds[b]
347 _Log('Clearing %s from cache', b_path)
348
349 time_file = os.path.join(self._timestamp_folder,
350 Timestamp.BuildToTimestamp(b_path))
joychen921e1fb2013-06-28 11:12:20 -0700351 os.unlink(time_file)
352 clear_dir = os.path.join(self.static_dir, b_path)
joychen3cb228e2013-06-12 12:13:13 -0700353 try:
joychen921e1fb2013-06-28 11:12:20 -0700354 # handle symlinks, in the case of links to local builds
355 if (os.path.islink(clear_dir)):
356 target = os.path.readlink(clear_dir)
357 _Log('Deleting image at %s', target)
358
359 os.unlink(clear_dir)
360 if (os.path.exists(target)):
361 shutil.rmtree(target)
362 elif os.path.exists(clear_dir):
joychen3cb228e2013-06-12 12:13:13 -0700363 shutil.rmtree(clear_dir)
joychen921e1fb2013-06-28 11:12:20 -0700364
joychen3cb228e2013-06-12 12:13:13 -0700365 except Exception:
366 raise XBuddyException('Failed to clear build in %s.' % clear_dir)
367
joychen921e1fb2013-06-28 11:12:20 -0700368 def _GetFromGS(self, build_id, image_type):
369 """Check if the artifact is available locally. Download from GS if not."""
370 gs_url = os.path.join(devserver_constants.GOOGLE_STORAGE_IMAGE_DIR,
371 build_id)
372
373 # stage image if not found in cache
374 file_name = GS_ALIAS_TO_FILENAME[image_type]
375 cached = os.path.exists(os.path.join(self.static_dir,
376 build_id,
377 file_name))
378 if not cached:
379 artifact = GS_ALIAS_TO_ARTIFACT[image_type]
380 _Log('Artifact to stage: %s', artifact)
381
382 _Log('Staging %s image from: %s', image_type, gs_url)
383 self._Download(gs_url, artifact)
384 else:
385 _Log('Image already cached.')
386
387 def _GetArtifact(self, path):
388 """Interpret an xBuddy path and return directory/file_name to resource."""
389 board, version, image_type = self._InterpretPath(path)
390
391 if version in [LATEST_LOCAL, '']:
392 # Get a local image
393 if image_type not in LOCAL_ALIASES:
394 raise XBuddyException('Bad image type: %s. Use one of: %s' %
395 (image_type, LOCAL_ALIASES))
396
397 file_name = LOCAL_ALIAS_TO_FILENAME[image_type]
398 version = self._GetLatestLocalVersion(board, file_name)
399 build_id = os.path.join(board, version)
400 else:
401 # Get a remote image
402 if image_type not in GS_ALIASES:
403 raise XBuddyException('Bad image type: %s. Use one of: %s' %
404 (image_type, GS_ALIASES))
405
406 # Clean up board
407 # TODO(joyc) decide what to do with the board suffix
408
409 # Clean up version
410 file_name = GS_ALIAS_TO_FILENAME[image_type]
411 version = self._ResolveVersion(board, version)
412 build_id = os.path.join(board, version)
413
414 self._GetFromGS(build_id, image_type)
415
416 return build_id, file_name
joychen3cb228e2013-06-12 12:13:13 -0700417
418 ############################ BEGIN PUBLIC METHODS
419
420 def List(self):
421 """Lists the currently available images & time since last access."""
joychen921e1fb2013-06-28 11:12:20 -0700422 self._SyncRegistryWithBuildImages()
423 builds = self._ListBuildTimes()
424 return_string = ''
425 for build, timestamp in builds:
426 return_string += '<b>' + build + '</b> '
427 return_string += '(time since last access: ' + str(timestamp) + ')<br>'
428 return return_string
joychen3cb228e2013-06-12 12:13:13 -0700429
430 def Capacity(self):
431 """Returns the number of images cached by xBuddy."""
432 return str(_XBUDDY_CAPACITY)
433
joychen921e1fb2013-06-28 11:12:20 -0700434 def Get(self, path_list, return_dir=False):
435 """The full xBuddy call, returns resource specified by path_list.
joychen3cb228e2013-06-12 12:13:13 -0700436
437 Please see devserver.py:xbuddy for full documentation.
438 Args:
joychen921e1fb2013-06-28 11:12:20 -0700439 path_list: [board, version, alias] as split from the xbuddy call url
joychen3cb228e2013-06-12 12:13:13 -0700440 return_dir: boolean, if set to true, returns the dir name instead.
441
442 Returns:
443 Path to the image or update directory on the devserver.
444 e.g. http://host/static/x86-generic-release/
445 R26-4000.0.0/chromium-test-image.bin
446 or
447 http://host/static/x86-generic-release/R26-4000.0.0/
448
449 Raises:
450 XBuddyException if path is invalid or XBuddy's cache fails
451 """
joychen921e1fb2013-06-28 11:12:20 -0700452 build_id, file_name = self._GetArtifact(path_list)
joychen3cb228e2013-06-12 12:13:13 -0700453
joychen921e1fb2013-06-28 11:12:20 -0700454 Timestamp.UpdateTimestamp(self._timestamp_folder, build_id)
joychen3cb228e2013-06-12 12:13:13 -0700455
456 #TODO (joyc): run in sep thread
457 self._CleanCache()
458
459 #TODO (joyc) static dir dependent on bug id: 214373
joychen921e1fb2013-06-28 11:12:20 -0700460 return_url = os.path.join('static', build_id)
joychen3cb228e2013-06-12 12:13:13 -0700461 if not return_dir:
462 return_url = os.path.join(return_url, file_name)
463
464 _Log('Returning path to payload: %s', return_url)
465 return return_url