blob: a43f1e729a549cc2879a5259c48e944eb54d1186 [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
9import re
10import shutil
11import threading
12
13import 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
24# xBuddy globals
25_XBUDDY_CAPACITY = 5
26ALIASES = [
27 'test',
28 'base',
29 'recovery',
30 'full_payload',
31 'stateful',
32 'autotest',
33]
34
35# TODO(joyc) these should become devserver constants.
36# currently, storage locations are embedded in the artifact classes defined in
37# build_artifact
38
39PATH_TO = [
40 build_artifact.TEST_IMAGE_FILE,
41 build_artifact.BASE_IMAGE_FILE,
42 build_artifact.RECOVERY_IMAGE_FILE,
43 devserver_constants.ROOT_UPDATE_FILE,
44 build_artifact.STATEFUL_UPDATE_FILE,
45 devserver_constants.AUTOTEST_DIR,
46]
47
48ARTIFACTS = [
49 artifact_info.TEST_IMAGE,
50 artifact_info.BASE_IMAGE,
51 artifact_info.RECOVERY_IMAGE,
52 artifact_info.FULL_PAYLOAD,
53 artifact_info.STATEFUL_PAYLOAD,
54 artifact_info.AUTOTEST,
55]
56
57IMAGE_TYPE_TO_FILENAME = dict(zip(ALIASES, PATH_TO))
58IMAGE_TYPE_TO_ARTIFACT = dict(zip(ALIASES, ARTIFACTS))
59
60# local, official, prefix storage locations
61# TODO figure out how to access channels
62OFFICIAL_RE = "latest-official.*"
63LATEST_RE = "latest.*"
64VERSION_PREFIX_RE = "R.*"
65
66LATEST = "latest"
67
68CHANNEL = [
69 'stable',
70 'beta',
71 'dev',
72 'canary',
73]
74
75# only paired with official
76SUFFIX = [
77 'release',
78 'paladin',
79 'factory',
80]
81
82class XBuddyException(Exception):
83 """Exception classes used by this module."""
84 pass
85
86
87# no __init__ method
88#pylint: disable=W0232
89class Timestamp():
90 """Class to translate build path strings and timestamp filenames."""
91
92 _TIMESTAMP_DELIMITER = 'SLASH'
93 XBUDDY_TIMESTAMP_DIR = 'xbuddy_UpdateTimestamps'
94
95 @staticmethod
96 def TimestampToBuild(timestamp_filename):
97 return timestamp_filename.replace(Timestamp._TIMESTAMP_DELIMITER, '/')
98
99 @staticmethod
100 def BuildToTimestamp(build_path):
101 return build_path.replace('/', Timestamp._TIMESTAMP_DELIMITER)
102#pylint: enable=W0232
103
104
105class XBuddy():
106 """Class that manages image retrieval and caching by the devserver.
107
108 Image retrieval by xBuddy path:
109 XBuddy accesses images and artifacts that it stores using an xBuddy
110 path of the form: board/version/alias
111 The primary xbuddy.Get call retrieves the correct artifact or url to where
112 the artifacts can be found.
113
114 Image caching:
115 Images and other artifacts are stored identically to how they would have
116 been if devserver's stage rpc was called and the xBuddy cache replaces
117 build versions on a LRU basis. Timestamps are maintained by last accessed
118 times of representative files in the a directory in the static serve
119 directory (XBUDDY_TIMESTAMP_DIR).
120
121 Private class members:
122 _true_values - used for interpreting boolean values
123 _staging_thread_count - track download requests
124 _static_dir - where all the artifacts are served from
125 """
126 _true_values = ['true', 't', 'yes', 'y']
127
128 # Number of threads that are staging images.
129 _staging_thread_count = 0
130 # Lock used to lock increasing/decreasing count.
131 _staging_thread_count_lock = threading.Lock()
132
133 def __init__(self, static_dir):
134 self._static_dir = static_dir
135 self._timestamp_folder = os.path.join(self._static_dir,
136 Timestamp.XBUDDY_TIMESTAMP_DIR)
137
138 @classmethod
139 def ParseBoolean(cls, boolean_string):
140 """Evaluate a string to a boolean value"""
141 if boolean_string:
142 return boolean_string.lower() in cls._true_values
143 else:
144 return False
145
146 @staticmethod
147 def _TryIndex(alias_chunks, index):
148 """Attempt to access an index of an alias. Default None if not found."""
149 try:
150 return alias_chunks[index]
151 except IndexError:
152 return None
153
154 def _ResolveVersion(self, board, version):
155 """
156 Handle version aliases.
157
158 Args:
159 board: as specified in the original call. (i.e. x86-generic, parrot)
160 version: as entered in the original call. can be
161 {TBD, 0. some custom alias as defined in a config file}
162 1. latest
163 2. latest-{channel}
164 3. latest-official-{board suffix}
165 4. version prefix (i.e. RX-Y.X, RX-Y, RX)
166 5. defaults to latest-local build
167
168 Returns:
169 Version number that is compatible with google storage (i.e. RX-X.X.X)
170
171 """
172 # TODO (joyc) read from a config file
173
174 version_tuple = version.split('-')
175
176 if re.match(OFFICIAL_RE, version):
177 # want most recent official build
178 return self._LookupVersion(board,
179 version_type='official',
180 suffix=self._TryIndex(version_tuple, 2))
181
182 elif re.match(LATEST_RE, version):
183 # want most recent build
184 return self._LookupVersion(board,
185 version_type=self._TryIndex(version_tuple, 1))
186
187 elif re.match(VERSION_PREFIX_RE, version):
188 # TODO (joyc) Find complete version if it's only a prefix.
189 return version
190
191 else:
192 # The given version doesn't match any known patterns.
193 # Default to most recent build.
194 return self._LookupVersion(board)
195
196 def _InterpretPath(self, path):
197 """
198 Split and translate the pieces of an xBuddy path name
199
200 input:
201 path: board/version/artifact
202 board must be specified, in the board of board, board-suffix
203 version can be the version number, or any of the xBuddy defined
204 version aliases
205 artifact is the devserver name for what to be downloaded
206
207 Return:
208 tuple of (board, version, image_type), as verified exist on gs
209
210 Raises:
211 XBuddyException: if the path can't be resolved into valid components
212 """
213 path_parts = path.rstrip('/').split('/')
214 if len(path_parts) == 3:
215 # We have a full path, with b/v/a
216 board, version, image_type = path_parts
217 elif len(path_parts) == 2:
218 # We have only the board and the version, default to test image
219 board, version = path_parts
220 image_type = ALIASES[0]
221 elif len(path_parts) == 1:
222 # We have only the board. default to latest test image.
223 board = path_parts[0]
224 version = LATEST
225 image_type = ALIASES[0]
226 else:
227 # Misshapen beyond recognition
228 raise XBuddyException('Invalid path, %s.' % path)
229
230 # Clean up board
231 # TODO(joyc) decide what to do with the board suffix
232
233 # Clean up version
234 version = self._ResolveVersion(board, version)
235
236 # clean up image_type
237 if image_type not in ALIASES:
238 raise XBuddyException('Image type %s unknown.' % image_type)
239
240 _Log("board: %s, version: %s, image: %s", board, version, image_type)
241
242 return board, version, image_type
243
244 @staticmethod
245 def _LookupVersion(board, version_type=None, suffix=None):
246 """Crawl gs for actual version numbers."""
247 # TODO (joyc)
248 raise NotImplementedError()
249
250 def _ListBuilds(self):
251 """ Returns the currently cached builds and their last access timestamp.
252
253 Returns:
254 list of tuples that matches xBuddy build/version to timestamps in long
255 """
256 # update currently cached builds
257 build_dict = {}
258
259 filenames = os.listdir(self._timestamp_folder)
260 for f in filenames:
261 last_accessed = os.path.getmtime(os.path.join(self._timestamp_folder, f))
262 build_id = Timestamp.TimestampToBuild(f)
263 stale_time = datetime.timedelta(seconds = (time.time()-last_accessed))
264 build_dict[build_id] = str(stale_time)
265 return_tup = sorted(build_dict.iteritems(), key=operator.itemgetter(1))
266 return return_tup
267
268 def _UpdateTimestamp(self, board_id):
269 """Update timestamp file of build with build_id."""
270 common_util.MkDirP(self._timestamp_folder)
271 time_file = os.path.join(self._timestamp_folder,
272 Timestamp.BuildToTimestamp(board_id))
273 with file(time_file, 'a'):
274 os.utime(time_file, None)
275
276 def _Download(self, gs_url, artifact):
277 """Download the single artifact from the given gs_url."""
278 with XBuddy._staging_thread_count_lock:
279 XBuddy._staging_thread_count += 1
280 try:
281 downloader.Downloader(self._static_dir, gs_url).Download(
282 [artifact])
283 finally:
284 with XBuddy._staging_thread_count_lock:
285 XBuddy._staging_thread_count -= 1
286
287 def _CleanCache(self):
288 """Delete all builds besides the first _XBUDDY_CAPACITY builds"""
289 cached_builds = [e[0] for e in self._ListBuilds()]
290 _Log('In cache now: %s', cached_builds)
291
292 for b in range(_XBUDDY_CAPACITY, len(cached_builds)):
293 b_path = cached_builds[b]
294 _Log('Clearing %s from cache', b_path)
295
296 time_file = os.path.join(self._timestamp_folder,
297 Timestamp.BuildToTimestamp(b_path))
298 os.remove(time_file)
299 clear_dir = os.path.join(self._static_dir, b_path)
300 try:
301 if os.path.exists(clear_dir):
302 shutil.rmtree(clear_dir)
303 except Exception:
304 raise XBuddyException('Failed to clear build in %s.' % clear_dir)
305
306
307 ############################ BEGIN PUBLIC METHODS
308
309 def List(self):
310 """Lists the currently available images & time since last access."""
311 return str(self._ListBuilds())
312
313 def Capacity(self):
314 """Returns the number of images cached by xBuddy."""
315 return str(_XBUDDY_CAPACITY)
316
317 def Get(self, path, return_dir=False):
318 """The full xBuddy call, returns path to resource on this devserver.
319
320 Please see devserver.py:xbuddy for full documentation.
321 Args:
322 path: board/version/alias
323 return_dir: boolean, if set to true, returns the dir name instead.
324
325 Returns:
326 Path to the image or update directory on the devserver.
327 e.g. http://host/static/x86-generic-release/
328 R26-4000.0.0/chromium-test-image.bin
329 or
330 http://host/static/x86-generic-release/R26-4000.0.0/
331
332 Raises:
333 XBuddyException if path is invalid or XBuddy's cache fails
334 """
335 board, version, image_type = self._InterpretPath(path)
336 file_name = IMAGE_TYPE_TO_FILENAME[image_type]
337
338 gs_url = os.path.join(devserver_constants.GOOGLE_STORAGE_IMAGE_DIR,
339 board, version)
340 serve_dir = os.path.join(board, version)
341
342 # stage image if not found in cache
343 cached = os.path.exists(os.path.join(self._static_dir,
344 serve_dir,
345 file_name))
346 if not cached:
347 artifact = IMAGE_TYPE_TO_ARTIFACT[image_type]
348 _Log('Artifact to stage: %s', artifact)
349
350 _Log('Staging %s image from: %s', image_type, gs_url)
351 self._Download(gs_url, artifact)
352 else:
353 _Log('Image already cached.')
354
355 self._UpdateTimestamp('/'.join([board, version]))
356
357 #TODO (joyc): run in sep thread
358 self._CleanCache()
359
360 #TODO (joyc) static dir dependent on bug id: 214373
361 return_url = os.path.join('static', serve_dir)
362 if not return_dir:
363 return_url = os.path.join(return_url, file_name)
364
365 _Log('Returning path to payload: %s', return_url)
366 return return_url