blob: 1d7e0a9dcde402be724bd30ca1f156e6462af032 [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
joycheneaf4cfc2013-07-02 08:38:57 -0700196 def _InterpretPath(self, path_parts):
joychen3cb228e2013-06-12 12:13:13 -0700197 """
198 Split and translate the pieces of an xBuddy path name
199
200 input:
joycheneaf4cfc2013-07-02 08:38:57 -0700201 path_parts: the segments of the path xBuddy Get was called with.
202 Documentation of path_parts can be found in devserver.py:xbuddy
joychen3cb228e2013-06-12 12:13:13 -0700203
204 Return:
205 tuple of (board, version, image_type), as verified exist on gs
206
207 Raises:
208 XBuddyException: if the path can't be resolved into valid components
209 """
joychen3cb228e2013-06-12 12:13:13 -0700210 if len(path_parts) == 3:
211 # We have a full path, with b/v/a
212 board, version, image_type = path_parts
213 elif len(path_parts) == 2:
214 # We have only the board and the version, default to test image
215 board, version = path_parts
216 image_type = ALIASES[0]
217 elif len(path_parts) == 1:
218 # We have only the board. default to latest test image.
219 board = path_parts[0]
220 version = LATEST
221 image_type = ALIASES[0]
222 else:
223 # Misshapen beyond recognition
joycheneaf4cfc2013-07-02 08:38:57 -0700224 raise XBuddyException('Invalid path, %s.' % '/'.join(path_parts))
joychen3cb228e2013-06-12 12:13:13 -0700225
226 # Clean up board
227 # TODO(joyc) decide what to do with the board suffix
228
229 # Clean up version
230 version = self._ResolveVersion(board, version)
231
232 # clean up image_type
233 if image_type not in ALIASES:
234 raise XBuddyException('Image type %s unknown.' % image_type)
235
236 _Log("board: %s, version: %s, image: %s", board, version, image_type)
237
238 return board, version, image_type
239
240 @staticmethod
241 def _LookupVersion(board, version_type=None, suffix=None):
242 """Crawl gs for actual version numbers."""
243 # TODO (joyc)
244 raise NotImplementedError()
245
246 def _ListBuilds(self):
247 """ Returns the currently cached builds and their last access timestamp.
248
249 Returns:
250 list of tuples that matches xBuddy build/version to timestamps in long
251 """
252 # update currently cached builds
253 build_dict = {}
254
255 filenames = os.listdir(self._timestamp_folder)
256 for f in filenames:
257 last_accessed = os.path.getmtime(os.path.join(self._timestamp_folder, f))
258 build_id = Timestamp.TimestampToBuild(f)
259 stale_time = datetime.timedelta(seconds = (time.time()-last_accessed))
260 build_dict[build_id] = str(stale_time)
261 return_tup = sorted(build_dict.iteritems(), key=operator.itemgetter(1))
262 return return_tup
263
264 def _UpdateTimestamp(self, board_id):
265 """Update timestamp file of build with build_id."""
266 common_util.MkDirP(self._timestamp_folder)
267 time_file = os.path.join(self._timestamp_folder,
268 Timestamp.BuildToTimestamp(board_id))
269 with file(time_file, 'a'):
270 os.utime(time_file, None)
271
272 def _Download(self, gs_url, artifact):
273 """Download the single artifact from the given gs_url."""
274 with XBuddy._staging_thread_count_lock:
275 XBuddy._staging_thread_count += 1
276 try:
277 downloader.Downloader(self._static_dir, gs_url).Download(
278 [artifact])
279 finally:
280 with XBuddy._staging_thread_count_lock:
281 XBuddy._staging_thread_count -= 1
282
283 def _CleanCache(self):
284 """Delete all builds besides the first _XBUDDY_CAPACITY builds"""
285 cached_builds = [e[0] for e in self._ListBuilds()]
286 _Log('In cache now: %s', cached_builds)
287
288 for b in range(_XBUDDY_CAPACITY, len(cached_builds)):
289 b_path = cached_builds[b]
290 _Log('Clearing %s from cache', b_path)
291
292 time_file = os.path.join(self._timestamp_folder,
293 Timestamp.BuildToTimestamp(b_path))
294 os.remove(time_file)
295 clear_dir = os.path.join(self._static_dir, b_path)
296 try:
297 if os.path.exists(clear_dir):
298 shutil.rmtree(clear_dir)
299 except Exception:
300 raise XBuddyException('Failed to clear build in %s.' % clear_dir)
301
302
303 ############################ BEGIN PUBLIC METHODS
304
305 def List(self):
306 """Lists the currently available images & time since last access."""
307 return str(self._ListBuilds())
308
309 def Capacity(self):
310 """Returns the number of images cached by xBuddy."""
311 return str(_XBUDDY_CAPACITY)
312
joycheneaf4cfc2013-07-02 08:38:57 -0700313 def Get(self, path_parts, return_dir=False):
314 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700315
316 Please see devserver.py:xbuddy for full documentation.
317 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700318 path_parts: [board, version, alias] as split from the xbuddy call url
joychen3cb228e2013-06-12 12:13:13 -0700319 return_dir: boolean, if set to true, returns the dir name instead.
320
321 Returns:
322 Path to the image or update directory on the devserver.
323 e.g. http://host/static/x86-generic-release/
324 R26-4000.0.0/chromium-test-image.bin
325 or
326 http://host/static/x86-generic-release/R26-4000.0.0/
327
328 Raises:
329 XBuddyException if path is invalid or XBuddy's cache fails
330 """
joycheneaf4cfc2013-07-02 08:38:57 -0700331 board, version, image_type = self._InterpretPath(path_parts)
joychen3cb228e2013-06-12 12:13:13 -0700332 file_name = IMAGE_TYPE_TO_FILENAME[image_type]
333
334 gs_url = os.path.join(devserver_constants.GOOGLE_STORAGE_IMAGE_DIR,
335 board, version)
336 serve_dir = os.path.join(board, version)
337
338 # stage image if not found in cache
339 cached = os.path.exists(os.path.join(self._static_dir,
340 serve_dir,
341 file_name))
342 if not cached:
343 artifact = IMAGE_TYPE_TO_ARTIFACT[image_type]
344 _Log('Artifact to stage: %s', artifact)
345
346 _Log('Staging %s image from: %s', image_type, gs_url)
347 self._Download(gs_url, artifact)
348 else:
349 _Log('Image already cached.')
350
351 self._UpdateTimestamp('/'.join([board, version]))
352
353 #TODO (joyc): run in sep thread
354 self._CleanCache()
355
356 #TODO (joyc) static dir dependent on bug id: 214373
357 return_url = os.path.join('static', serve_dir)
358 if not return_dir:
359 return_url = os.path.join(return_url, file_name)
360
361 _Log('Returning path to payload: %s', return_url)
362 return return_url