blob: d575d9a640c1bcee50cb65939603495c5c6060c3 [file] [log] [blame]
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -07001# -*- coding: utf-8 -*-
joychen3cb228e2013-06-12 12:13:13 -07002# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Chris Sosa0eecf962014-02-03 14:14:39 -08006"""Main module for parsing and interpreting XBuddy paths for the devserver."""
7
Gilad Arnold5f46d8e2015-02-19 12:17:55 -08008from __future__ import print_function
9
Yiming Chenaab488e2014-11-17 14:49:31 -080010import cherrypy
joychen562699a2013-08-13 15:22:14 -070011import ConfigParser
joychen3cb228e2013-06-12 12:13:13 -070012import datetime
xixuan44b55452016-09-06 15:35:56 -070013import distutils.version
joychen3cb228e2013-06-12 12:13:13 -070014import operator
15import os
joychenf8f07e22013-07-12 17:45:51 -070016import re
joychen3cb228e2013-06-12 12:13:13 -070017import shutil
joychenf8f07e22013-07-12 17:45:51 -070018import time
joychen3cb228e2013-06-12 12:13:13 -070019import threading
20
21import artifact_info
Gabe Black3b567202015-09-23 14:07:59 -070022import build_artifact
23import build_util
joychen3cb228e2013-06-12 12:13:13 -070024import common_util
25import devserver_constants
26import downloader
27import log_util
28
xixuan44b55452016-09-06 15:35:56 -070029# Make sure that chromite is available to import.
30import setup_chromite # pylint: disable=unused-import
31
32try:
33 from chromite.lib import gs
Gwendal Grignouad4cb982017-03-31 11:36:19 -070034except ImportError:
xixuan44b55452016-09-06 15:35:56 -070035 gs = None
36
joychen3cb228e2013-06-12 12:13:13 -070037# Module-local log function.
38def _Log(message, *args):
39 return log_util.LogWithTag('XBUDDY', message, *args)
40
joychen562699a2013-08-13 15:22:14 -070041# xBuddy config constants
42CONFIG_FILE = 'xbuddy_config.ini'
43SHADOW_CONFIG_FILE = 'shadow_xbuddy_config.ini'
44PATH_REWRITES = 'PATH_REWRITES'
45GENERAL = 'GENERAL'
Gilad Arnold896c6d82015-03-13 16:20:29 -070046LOCATION_SUFFIXES = 'LOCATION_SUFFIXES'
joychen921e1fb2013-06-28 11:12:20 -070047
Chris Sosac2abc722013-08-26 17:11:22 -070048# Path for shadow config in chroot.
49CHROOT_SHADOW_DIR = '/mnt/host/source/src/platform/dev'
50
joychen25d25972013-07-30 14:54:16 -070051# XBuddy aliases
52TEST = 'test'
53BASE = 'base'
54DEV = 'dev'
55FULL = 'full_payload'
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -070056SIGNED = 'signed'
joychen25d25972013-07-30 14:54:16 -070057RECOVERY = 'recovery'
58STATEFUL = 'stateful'
59AUTOTEST = 'autotest'
Mike Frysingera0e6a282016-09-01 17:29:08 -040060FACTORY_SHIM = 'factory_shim'
joychen25d25972013-07-30 14:54:16 -070061
joychen921e1fb2013-06-28 11:12:20 -070062# Local build constants
joychenc3944cb2013-08-19 10:42:07 -070063ANY = "ANY"
joychen7df67f72013-07-18 14:21:12 -070064LATEST = "latest"
65LOCAL = "local"
66REMOTE = "remote"
Chris Sosa75490802013-09-30 17:21:45 -070067
68# TODO(sosa): Fix a lot of assumptions about these aliases. There is too much
69# implicit logic here that's unnecessary. What should be done:
70# 1) Collapse Alias logic to one set of aliases for xbuddy (not local/remote).
71# 2) Do not use zip when creating these dicts. Better to not rely on ordering.
72# 3) Move alias/artifact mapping to a central module rather than having it here.
73# 4) Be explicit when things are missing i.e. no dev images in image.zip.
74
joychen921e1fb2013-06-28 11:12:20 -070075LOCAL_ALIASES = [
Gilad Arnold5f46d8e2015-02-19 12:17:55 -080076 TEST,
77 DEV,
78 BASE,
79 RECOVERY,
Mike Frysingera0e6a282016-09-01 17:29:08 -040080 FACTORY_SHIM,
Gilad Arnold5f46d8e2015-02-19 12:17:55 -080081 FULL,
82 STATEFUL,
83 ANY,
joychen921e1fb2013-06-28 11:12:20 -070084]
85
86LOCAL_FILE_NAMES = [
Gilad Arnold5f46d8e2015-02-19 12:17:55 -080087 devserver_constants.TEST_IMAGE_FILE,
88 devserver_constants.IMAGE_FILE,
89 devserver_constants.BASE_IMAGE_FILE,
90 devserver_constants.RECOVERY_IMAGE_FILE,
Mike Frysingera0e6a282016-09-01 17:29:08 -040091 devserver_constants.FACTORY_SHIM_IMAGE_FILE,
Gilad Arnold5f46d8e2015-02-19 12:17:55 -080092 devserver_constants.UPDATE_FILE,
93 devserver_constants.STATEFUL_FILE,
94 None, # For ANY.
joychen921e1fb2013-06-28 11:12:20 -070095]
96
97LOCAL_ALIAS_TO_FILENAME = dict(zip(LOCAL_ALIASES, LOCAL_FILE_NAMES))
98
99# Google Storage constants
100GS_ALIASES = [
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800101 TEST,
102 BASE,
103 RECOVERY,
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700104 SIGNED,
Mike Frysingera0e6a282016-09-01 17:29:08 -0400105 FACTORY_SHIM,
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800106 FULL,
107 STATEFUL,
108 AUTOTEST,
joychen3cb228e2013-06-12 12:13:13 -0700109]
110
joychen921e1fb2013-06-28 11:12:20 -0700111GS_FILE_NAMES = [
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800112 devserver_constants.TEST_IMAGE_FILE,
113 devserver_constants.BASE_IMAGE_FILE,
114 devserver_constants.RECOVERY_IMAGE_FILE,
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700115 devserver_constants.SIGNED_IMAGE_FILE,
Mike Frysingera0e6a282016-09-01 17:29:08 -0400116 devserver_constants.FACTORY_SHIM_IMAGE_FILE,
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800117 devserver_constants.UPDATE_FILE,
118 devserver_constants.STATEFUL_FILE,
119 devserver_constants.AUTOTEST_DIR,
joychen3cb228e2013-06-12 12:13:13 -0700120]
121
122ARTIFACTS = [
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800123 artifact_info.TEST_IMAGE,
124 artifact_info.BASE_IMAGE,
125 artifact_info.RECOVERY_IMAGE,
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700126 artifact_info.SIGNED_IMAGE,
Mike Frysingera0e6a282016-09-01 17:29:08 -0400127 artifact_info.FACTORY_SHIM_IMAGE,
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800128 artifact_info.FULL_PAYLOAD,
129 artifact_info.STATEFUL_PAYLOAD,
130 artifact_info.AUTOTEST,
joychen3cb228e2013-06-12 12:13:13 -0700131]
132
joychen921e1fb2013-06-28 11:12:20 -0700133GS_ALIAS_TO_FILENAME = dict(zip(GS_ALIASES, GS_FILE_NAMES))
134GS_ALIAS_TO_ARTIFACT = dict(zip(GS_ALIASES, ARTIFACTS))
joychen3cb228e2013-06-12 12:13:13 -0700135
joychen921e1fb2013-06-28 11:12:20 -0700136LATEST_OFFICIAL = "latest-official"
joychen3cb228e2013-06-12 12:13:13 -0700137
Chris Sosaea734d92013-10-11 11:28:58 -0700138RELEASE = "-release"
joychen3cb228e2013-06-12 12:13:13 -0700139
joychen3cb228e2013-06-12 12:13:13 -0700140
141class XBuddyException(Exception):
142 """Exception classes used by this module."""
143 pass
144
145
146# no __init__ method
147#pylint: disable=W0232
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800148class Timestamp(object):
joychen3cb228e2013-06-12 12:13:13 -0700149 """Class to translate build path strings and timestamp filenames."""
150
151 _TIMESTAMP_DELIMITER = 'SLASH'
152 XBUDDY_TIMESTAMP_DIR = 'xbuddy_UpdateTimestamps'
153
154 @staticmethod
155 def TimestampToBuild(timestamp_filename):
156 return timestamp_filename.replace(Timestamp._TIMESTAMP_DELIMITER, '/')
157
158 @staticmethod
159 def BuildToTimestamp(build_path):
160 return build_path.replace('/', Timestamp._TIMESTAMP_DELIMITER)
joychen921e1fb2013-06-28 11:12:20 -0700161
162 @staticmethod
163 def UpdateTimestamp(timestamp_dir, build_id):
164 """Update timestamp file of build with build_id."""
165 common_util.MkDirP(timestamp_dir)
joychen562699a2013-08-13 15:22:14 -0700166 _Log("Updating timestamp for %s", build_id)
joychen921e1fb2013-06-28 11:12:20 -0700167 time_file = os.path.join(timestamp_dir,
168 Timestamp.BuildToTimestamp(build_id))
169 with file(time_file, 'a'):
170 os.utime(time_file, None)
joychen3cb228e2013-06-12 12:13:13 -0700171#pylint: enable=W0232
172
173
joychen921e1fb2013-06-28 11:12:20 -0700174class XBuddy(build_util.BuildObject):
joychen3cb228e2013-06-12 12:13:13 -0700175 """Class that manages image retrieval and caching by the devserver.
176
177 Image retrieval by xBuddy path:
178 XBuddy accesses images and artifacts that it stores using an xBuddy
179 path of the form: board/version/alias
180 The primary xbuddy.Get call retrieves the correct artifact or url to where
181 the artifacts can be found.
182
183 Image caching:
184 Images and other artifacts are stored identically to how they would have
185 been if devserver's stage rpc was called and the xBuddy cache replaces
186 build versions on a LRU basis. Timestamps are maintained by last accessed
187 times of representative files in the a directory in the static serve
188 directory (XBUDDY_TIMESTAMP_DIR).
189
190 Private class members:
joychen121fc9b2013-08-02 14:30:30 -0700191 _true_values: used for interpreting boolean values
192 _staging_thread_count: track download requests
193 _timestamp_folder: directory with empty files standing in as timestamps
joychen921e1fb2013-06-28 11:12:20 -0700194 for each image currently cached by xBuddy
joychen3cb228e2013-06-12 12:13:13 -0700195 """
196 _true_values = ['true', 't', 'yes', 'y']
197
198 # Number of threads that are staging images.
199 _staging_thread_count = 0
200 # Lock used to lock increasing/decreasing count.
201 _staging_thread_count_lock = threading.Lock()
202
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800203 def __init__(self, manage_builds=False, board=None, version=None,
204 images_dir=None, log_screen=True, **kwargs):
joychen921e1fb2013-06-28 11:12:20 -0700205 super(XBuddy, self).__init__(**kwargs)
joychenb0dfe552013-07-30 10:02:06 -0700206
Yiming Chenaab488e2014-11-17 14:49:31 -0800207 if not log_screen:
208 cherrypy.config.update({'log.screen': False})
209
joychen562699a2013-08-13 15:22:14 -0700210 self.config = self._ReadConfig()
211 self._manage_builds = manage_builds or self._ManageBuilds()
Chris Sosa75490802013-09-30 17:21:45 -0700212 self._board = board
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800213 self._version = version
joychen921e1fb2013-06-28 11:12:20 -0700214 self._timestamp_folder = os.path.join(self.static_dir,
joychen3cb228e2013-06-12 12:13:13 -0700215 Timestamp.XBUDDY_TIMESTAMP_DIR)
Chris Sosa7cd23202013-10-15 17:22:57 -0700216 if images_dir:
217 self.images_dir = images_dir
218 else:
219 self.images_dir = os.path.join(self.GetSourceRoot(), 'src/build/images')
220
xixuan178263c2017-03-22 09:10:25 -0700221 if common_util.IsRunningOnMoblab():
222 self._ctx = gs.GSContext(cache_user='chronos') if gs else None
223 else:
224 self._ctx = gs.GSContext() if gs else None
xixuan44b55452016-09-06 15:35:56 -0700225
joychen7df67f72013-07-18 14:21:12 -0700226 common_util.MkDirP(self._timestamp_folder)
joychen3cb228e2013-06-12 12:13:13 -0700227
228 @classmethod
229 def ParseBoolean(cls, boolean_string):
230 """Evaluate a string to a boolean value"""
231 if boolean_string:
232 return boolean_string.lower() in cls._true_values
233 else:
234 return False
235
joychen562699a2013-08-13 15:22:14 -0700236 def _ReadConfig(self):
237 """Read xbuddy config from ini files.
238
239 Reads the base config from xbuddy_config.ini, and then merges in the
240 shadow config from shadow_xbuddy_config.ini
241
242 Returns:
243 The merged configuration.
Yu-Ju Hongc54658c2014-01-22 09:18:07 -0800244
joychen562699a2013-08-13 15:22:14 -0700245 Raises:
246 XBuddyException if the config file is missing.
247 """
248 xbuddy_config = ConfigParser.ConfigParser()
249 config_file = os.path.join(self.devserver_dir, CONFIG_FILE)
250 if os.path.exists(config_file):
251 xbuddy_config.read(config_file)
252 else:
Yiming Chend9202142014-11-07 14:56:52 -0800253 # Get the directory of xbuddy.py file.
254 file_dir = os.path.dirname(os.path.realpath(__file__))
255 # Read the default xbuddy_config.ini from the directory.
256 xbuddy_config.read(os.path.join(file_dir, CONFIG_FILE))
joychen562699a2013-08-13 15:22:14 -0700257
258 # Read the shadow file if there is one.
Chris Sosac2abc722013-08-26 17:11:22 -0700259 if os.path.isdir(CHROOT_SHADOW_DIR):
260 shadow_config_file = os.path.join(CHROOT_SHADOW_DIR, SHADOW_CONFIG_FILE)
261 else:
262 shadow_config_file = os.path.join(self.devserver_dir, SHADOW_CONFIG_FILE)
263
264 _Log('Using shadow config file stored at %s', shadow_config_file)
joychen562699a2013-08-13 15:22:14 -0700265 if os.path.exists(shadow_config_file):
266 shadow_xbuddy_config = ConfigParser.ConfigParser()
267 shadow_xbuddy_config.read(shadow_config_file)
268
269 # Merge shadow config in.
270 sections = shadow_xbuddy_config.sections()
271 for s in sections:
272 if not xbuddy_config.has_section(s):
273 xbuddy_config.add_section(s)
274 options = shadow_xbuddy_config.options(s)
275 for o in options:
276 val = shadow_xbuddy_config.get(s, o)
277 xbuddy_config.set(s, o, val)
278
279 return xbuddy_config
280
281 def _ManageBuilds(self):
282 """Checks if xBuddy is managing local builds using the current config."""
283 try:
284 return self.ParseBoolean(self.config.get(GENERAL, 'manage_builds'))
285 except ConfigParser.Error:
286 return False
287
288 def _Capacity(self):
289 """Gets the xbuddy capacity from the current config."""
290 try:
291 return int(self.config.get(GENERAL, 'capacity'))
292 except ConfigParser.Error:
293 return 5
294
Gilad Arnold38e828c2015-04-24 13:52:07 -0700295 def LookupAlias(self, alias, board=None, version=None):
joychen562699a2013-08-13 15:22:14 -0700296 """Given the full xbuddy config, look up an alias for path rewrite.
297
298 Args:
299 alias: The xbuddy path that could be one of the aliases in the
300 rewrite table.
301 board: The board to fill in with when paths are rewritten. Can be from
Gilad Arnold38e828c2015-04-24 13:52:07 -0700302 the update request xml or the default board from devserver. If None,
303 defers to the value given during XBuddy initialization.
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800304 version: The version to fill in when rewriting paths. Could be a specific
Gilad Arnold38e828c2015-04-24 13:52:07 -0700305 version number or a version alias like LATEST. If None, defers to the
306 value given during XBuddy initialization, or LATEST.
Yu-Ju Hongc54658c2014-01-22 09:18:07 -0800307
joychen562699a2013-08-13 15:22:14 -0700308 Returns:
Gilad Arnold896c6d82015-03-13 16:20:29 -0700309 A pair (val, suffix) where val is the rewritten path, or the original
310 string if no rewrite was found; and suffix is the assigned location
311 suffix, or the default suffix if none was found.
joychen562699a2013-08-13 15:22:14 -0700312 """
joychen562699a2013-08-13 15:22:14 -0700313 try:
Gilad Arnold896c6d82015-03-13 16:20:29 -0700314 suffix = self.config.get(LOCATION_SUFFIXES, alias)
315 except ConfigParser.Error:
316 suffix = RELEASE
317
318 try:
joychen562699a2013-08-13 15:22:14 -0700319 val = self.config.get(PATH_REWRITES, alias)
320 except ConfigParser.Error:
321 # No alias lookup found. Return original path.
Gilad Arnold896c6d82015-03-13 16:20:29 -0700322 val = None
joychen562699a2013-08-13 15:22:14 -0700323
Gilad Arnold896c6d82015-03-13 16:20:29 -0700324 if not (val and val.strip()):
325 val = alias
joychen562699a2013-08-13 15:22:14 -0700326 else:
Gilad Arnold896c6d82015-03-13 16:20:29 -0700327 # The found value is not an empty string.
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800328 # Fill in the board and version.
Gilad Arnold896c6d82015-03-13 16:20:29 -0700329 val = val.replace("BOARD", "%(board)s")
330 val = val.replace("VERSION", "%(version)s")
Gilad Arnold38e828c2015-04-24 13:52:07 -0700331 val = val % {'board': board or self._board,
332 'version': version or self._version or LATEST}
Gilad Arnold896c6d82015-03-13 16:20:29 -0700333
334 _Log("Path is %s, location suffix is %s", val, suffix)
335 return val, suffix
joychen562699a2013-08-13 15:22:14 -0700336
Simran Basi99e63c02014-05-20 10:39:52 -0700337 @staticmethod
338 def _ResolveImageDir(image_dir):
339 """Clean up and return the image dir to use.
340
341 Args:
342 image_dir: directory in Google Storage to use.
343
344 Returns:
345 |image_dir| if |image_dir| is not None. Otherwise, returns
346 devserver_constants.GS_IMAGE_DIR
347 """
348 image_dir = image_dir or devserver_constants.GS_IMAGE_DIR
349 # Remove trailing slashes.
350 return image_dir.rstrip('/')
351
Gilad Arnold896c6d82015-03-13 16:20:29 -0700352 def _LookupOfficial(self, board, suffix, image_dir=None):
joychenf8f07e22013-07-12 17:45:51 -0700353 """Check LATEST-master for the version number of interest."""
354 _Log("Checking gs for latest %s-%s image", board, suffix)
Simran Basi99e63c02014-05-20 10:39:52 -0700355 image_dir = XBuddy._ResolveImageDir(image_dir)
356 latest_addr = (devserver_constants.GS_LATEST_MASTER %
357 {'image_dir': image_dir,
358 'board': board,
359 'suffix': suffix})
joychen121fc9b2013-08-02 14:30:30 -0700360 # Full release + version is in the LATEST file.
xixuan44b55452016-09-06 15:35:56 -0700361 version = self._ctx.Cat(latest_addr)
joychen3cb228e2013-06-12 12:13:13 -0700362
joychenf8f07e22013-07-12 17:45:51 -0700363 return devserver_constants.IMAGE_DIR % {'board':board,
364 'suffix':suffix,
365 'version':version}
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800366
xixuan44b55452016-09-06 15:35:56 -0700367 def _LS(self, path, list_subdirectory=False):
368 """Does a directory listing of the given gs path.
369
370 Args:
371 path: directory location on google storage to check.
372 list_subdirectory: whether to only list subdirectory for |path|.
373
374 Returns:
375 A list of paths that matched |path|.
376 """
377 if list_subdirectory:
378 return self._ctx.DoCommand(
Gwendal Grignou76d08792017-03-31 11:32:16 -0700379 ['ls', '-d', '--', path], redirect_stdout=True).output.splitlines()
xixuan44b55452016-09-06 15:35:56 -0700380 else:
381 return self._ctx.LS(path)
382
383 def _GetLatestVersionFromGsDir(self, path, list_subdirectory=False,
384 with_release=True):
385 """Returns most recent version number found in a google storage directory.
386
387 This lists out the contents of the given GS bucket or regex to GS buckets,
388 and tries to grab the newest version found in the directory names.
389
390 Args:
391 path: directory location on google storage to check.
392 list_subdirectory: whether to only list subdirectory for |path|.
393 with_release: whether versions include a release milestone (e.g. R12).
394
395 Returns:
396 The most recent version number found.
397 """
398 list_result = self._LS(path, list_subdirectory=list_subdirectory)
399 dir_names = [os.path.basename(p.rstrip('/')) for p in list_result]
400 try:
401 filter_re = re.compile(devserver_constants.VERSION_RE if with_release
402 else devserver_constants.VERSION)
403 versions = filter(filter_re.match, dir_names)
404 latest_version = max(versions, key=distutils.version.LooseVersion)
405 except ValueError:
406 raise gs.GSContextException(
407 'Failed to find most recent builds at %s' % path)
408
409 return latest_version
410
Gilad Arnold896c6d82015-03-13 16:20:29 -0700411 def _LookupChannel(self, board, suffix, channel='stable',
412 image_dir=None):
joychenf8f07e22013-07-12 17:45:51 -0700413 """Check the channel folder for the version number of interest."""
joychen121fc9b2013-08-02 14:30:30 -0700414 # Get all names in channel dir. Get 10 highest directories by version.
joychen7df67f72013-07-18 14:21:12 -0700415 _Log("Checking channel '%s' for latest '%s' image", channel, board)
Yu-Ju Hongc54658c2014-01-22 09:18:07 -0800416 # Due to historical reasons, gs://chromeos-releases uses
417 # daisy-spring as opposed to the board name daisy_spring. Convert
xixuan44b55452016-09-06 15:35:56 -0700418 # he board name for the lookup.
Yu-Ju Hongc54658c2014-01-22 09:18:07 -0800419 channel_dir = devserver_constants.GS_CHANNEL_DIR % {
420 'channel':channel,
421 'board':re.sub('_', '-', board)}
xixuan44b55452016-09-06 15:35:56 -0700422 latest_version = self._GetLatestVersionFromGsDir(channel_dir,
423 with_release=False)
joychenf8f07e22013-07-12 17:45:51 -0700424
joychen121fc9b2013-08-02 14:30:30 -0700425 # Figure out release number from the version number.
joychenc3944cb2013-08-19 10:42:07 -0700426 image_url = devserver_constants.IMAGE_DIR % {
Gilad Arnold896c6d82015-03-13 16:20:29 -0700427 'board': board,
428 'suffix': suffix,
429 'version': 'R*' + latest_version}
Simran Basi99e63c02014-05-20 10:39:52 -0700430 image_dir = XBuddy._ResolveImageDir(image_dir)
431 gs_url = os.path.join(image_dir, image_url)
joychenf8f07e22013-07-12 17:45:51 -0700432
433 # There should only be one match on cros-image-archive.
xixuan44b55452016-09-06 15:35:56 -0700434 full_version = self._GetLatestVersionFromGsDir(gs_url,
435 list_subdirectory=True)
joychenf8f07e22013-07-12 17:45:51 -0700436
Gilad Arnold896c6d82015-03-13 16:20:29 -0700437 return devserver_constants.IMAGE_DIR % {'board': board,
438 'suffix': suffix,
439 'version': full_version}
joychenf8f07e22013-07-12 17:45:51 -0700440
Gilad Arnold896c6d82015-03-13 16:20:29 -0700441 def _LookupVersion(self, board, suffix, version):
joychenf8f07e22013-07-12 17:45:51 -0700442 """Search GS image releases for the highest match to a version prefix."""
joychen121fc9b2013-08-02 14:30:30 -0700443 # Build the pattern for GS to match.
joychen7df67f72013-07-18 14:21:12 -0700444 _Log("Checking gs for latest '%s' image with prefix '%s'", board, version)
Gilad Arnold896c6d82015-03-13 16:20:29 -0700445 image_url = devserver_constants.IMAGE_DIR % {'board': board,
446 'suffix': suffix,
447 'version': version + '*'}
joychenf8f07e22013-07-12 17:45:51 -0700448 image_dir = os.path.join(devserver_constants.GS_IMAGE_DIR, image_url)
449
joychen121fc9b2013-08-02 14:30:30 -0700450 # Grab the newest version of the ones matched.
xixuan44b55452016-09-06 15:35:56 -0700451 full_version = self._GetLatestVersionFromGsDir(image_dir,
452 list_subdirectory=True)
Gilad Arnold896c6d82015-03-13 16:20:29 -0700453 return devserver_constants.IMAGE_DIR % {'board': board,
454 'suffix': suffix,
455 'version': full_version}
joychenf8f07e22013-07-12 17:45:51 -0700456
Gilad Arnold896c6d82015-03-13 16:20:29 -0700457 def _RemoteBuildId(self, board, suffix, version):
Chris Sosaea734d92013-10-11 11:28:58 -0700458 """Returns the remote build_id for the given board and version.
459
460 Raises:
461 XBuddyException: If we failed to resolve the version to a valid build_id.
462 """
Gilad Arnold896c6d82015-03-13 16:20:29 -0700463 build_id_as_is = devserver_constants.IMAGE_DIR % {'board': board,
464 'suffix': '',
465 'version': version}
466 build_id_suffix = devserver_constants.IMAGE_DIR % {'board': board,
467 'suffix': suffix,
468 'version': version}
Chris Sosaea734d92013-10-11 11:28:58 -0700469 # Return the first path that exists. We assume that what the user typed
470 # is better than with a default suffix added i.e. x86-generic/blah is
471 # more valuable than x86-generic-release/blah.
Gilad Arnold896c6d82015-03-13 16:20:29 -0700472 for build_id in build_id_as_is, build_id_suffix:
Chris Sosaea734d92013-10-11 11:28:58 -0700473 try:
xixuan44b55452016-09-06 15:35:56 -0700474 version = self._ctx.LS(
475 '%s/%s' % (devserver_constants.GS_IMAGE_DIR, build_id))
Chris Sosaea734d92013-10-11 11:28:58 -0700476 return build_id
xixuan44b55452016-09-06 15:35:56 -0700477 except (gs.GSCommandError, gs.GSContextException, gs.GSNoSuchKey):
Chris Sosaea734d92013-10-11 11:28:58 -0700478 continue
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800479
480 raise XBuddyException('Could not find remote build_id for %s %s' % (
481 board, version))
Chris Sosaea734d92013-10-11 11:28:58 -0700482
Gilad Arnold896c6d82015-03-13 16:20:29 -0700483 def _ResolveBuildVersion(self, board, suffix, base_version):
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800484 """Check LATEST-<base_version> and returns a full build version."""
485 _Log('Checking gs for full version for %s of %s', base_version, board)
486 # TODO(garnold) We might want to accommodate version prefixes and pick the
487 # most recent found, as done in _LookupVersion().
488 latest_addr = (devserver_constants.GS_LATEST_BASE_VERSION %
489 {'image_dir': devserver_constants.GS_IMAGE_DIR,
490 'board': board,
Gilad Arnold896c6d82015-03-13 16:20:29 -0700491 'suffix': suffix,
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800492 'base_version': base_version})
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800493 # Full release + version is in the LATEST file.
xixuan44b55452016-09-06 15:35:56 -0700494 return self._ctx.Cat(latest_addr)
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800495
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700496 def _ResolveVersionToBuildIdAndChannel(self, board, suffix, version,
497 image_dir=None):
joychen121fc9b2013-08-02 14:30:30 -0700498 """Handle version aliases for remote payloads in GS.
joychen3cb228e2013-06-12 12:13:13 -0700499
500 Args:
501 board: as specified in the original call. (i.e. x86-generic, parrot)
Gilad Arnold896c6d82015-03-13 16:20:29 -0700502 suffix: The location suffix, to be added to board name.
joychen3cb228e2013-06-12 12:13:13 -0700503 version: as entered in the original call. can be
504 {TBD, 0. some custom alias as defined in a config file}
Ningning Xiab2a1af52016-04-22 11:14:42 -0700505 1. fully qualified build version.
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800506 2. latest
507 3. latest-{channel}
508 4. latest-official-{board suffix}
509 5. version prefix (i.e. RX-Y.X, RX-Y, RX)
Simran Basi99e63c02014-05-20 10:39:52 -0700510 image_dir: image directory to check in Google Storage. If none,
511 the default bucket is used.
joychen3cb228e2013-06-12 12:13:13 -0700512
513 Returns:
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700514 Tuple of (Location where the image dir is actually found on GS (build_id),
515 best guess for the channel).
joychen3cb228e2013-06-12 12:13:13 -0700516
Chris Sosaea734d92013-10-11 11:28:58 -0700517 Raises:
518 XBuddyException: If we failed to resolve the version to a valid url.
joychen3cb228e2013-06-12 12:13:13 -0700519 """
joychenf8f07e22013-07-12 17:45:51 -0700520 # Only the last segment of the alias is variable relative to the rest.
521 version_tuple = version.rsplit('-', 1)
joychen3cb228e2013-06-12 12:13:13 -0700522
joychenf8f07e22013-07-12 17:45:51 -0700523 if re.match(devserver_constants.VERSION_RE, version):
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700524 return self._RemoteBuildId(board, suffix, version), None
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800525 elif re.match(devserver_constants.VERSION, version):
Ningning Xiab2a1af52016-04-22 11:14:42 -0700526 raise XBuddyException('\'%s\' is not valid. Should provide the fully '
527 'qualified version with a version prefix \'RX-\' '
528 'due to crbug.com/585914' % version)
joychenf8f07e22013-07-12 17:45:51 -0700529 elif version == LATEST_OFFICIAL:
530 # latest-official --> LATEST build in board-release
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700531 return self._LookupOfficial(board, suffix, image_dir=image_dir), None
joychenf8f07e22013-07-12 17:45:51 -0700532 elif version_tuple[0] == LATEST_OFFICIAL:
533 # latest-official-{suffix} --> LATEST build in board-{suffix}
Gilad Arnold896c6d82015-03-13 16:20:29 -0700534 return self._LookupOfficial(board, version_tuple[1],
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700535 image_dir=image_dir), None
joychenf8f07e22013-07-12 17:45:51 -0700536 elif version == LATEST:
537 # latest --> latest build on stable channel
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700538 return self._LookupChannel(board, suffix, image_dir=image_dir), 'stable'
joychenf8f07e22013-07-12 17:45:51 -0700539 elif version_tuple[0] == LATEST:
540 if re.match(devserver_constants.VERSION_RE, version_tuple[1]):
541 # latest-R* --> most recent qualifying build
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700542 return self._LookupVersion(board, suffix, version_tuple[1]), None
joychenf8f07e22013-07-12 17:45:51 -0700543 else:
544 # latest-{channel} --> latest build within that channel
Gilad Arnold896c6d82015-03-13 16:20:29 -0700545 return self._LookupChannel(board, suffix, channel=version_tuple[1],
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700546 image_dir=image_dir), version_tuple[1]
joychen3cb228e2013-06-12 12:13:13 -0700547 else:
548 # The given version doesn't match any known patterns.
joychen921e1fb2013-06-28 11:12:20 -0700549 raise XBuddyException("Version %s unknown. Can't find on GS." % version)
joychen3cb228e2013-06-12 12:13:13 -0700550
joychen5260b9a2013-07-16 14:48:01 -0700551 @staticmethod
552 def _Symlink(link, target):
553 """Symlinks link to target, and removes whatever link was there before."""
554 _Log("Linking to %s from %s", link, target)
555 if os.path.lexists(link):
556 os.unlink(link)
557 os.symlink(target, link)
558
joychen121fc9b2013-08-02 14:30:30 -0700559 def _GetLatestLocalVersion(self, board):
joychen921e1fb2013-06-28 11:12:20 -0700560 """Get the version of the latest image built for board by build_image
561
562 Updates the symlink reference within the xBuddy static dir to point to
563 the real image dir in the local /build/images directory.
564
565 Args:
joychenc3944cb2013-08-19 10:42:07 -0700566 board: board that image was built for.
joychen921e1fb2013-06-28 11:12:20 -0700567
568 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700569 The discovered version of the image.
joychenc3944cb2013-08-19 10:42:07 -0700570
571 Raises:
572 XBuddyException if neither test nor dev image was found in latest built
573 directory.
joychen3cb228e2013-06-12 12:13:13 -0700574 """
Alex Klein77df9352018-10-30 12:01:06 -0600575 latest_local_dir = self.GetLatestImageLink(board)
joychenb0dfe552013-07-30 10:02:06 -0700576 if not latest_local_dir or not os.path.exists(latest_local_dir):
joychen921e1fb2013-06-28 11:12:20 -0700577 raise XBuddyException('No builds found for %s. Did you run build_image?' %
578 board)
579
joychen121fc9b2013-08-02 14:30:30 -0700580 # Assume that the version number is the name of the directory.
Alex Klein77df9352018-10-30 12:01:06 -0600581 return os.path.basename(os.path.realpath(latest_local_dir))
joychen921e1fb2013-06-28 11:12:20 -0700582
joychenc3944cb2013-08-19 10:42:07 -0700583 @staticmethod
584 def _FindAny(local_dir):
585 """Returns the image_type for ANY given the local_dir."""
joychenc3944cb2013-08-19 10:42:07 -0700586 test_image = os.path.join(local_dir, devserver_constants.TEST_IMAGE_FILE)
Yu-Ju Hongc23c79b2014-03-17 12:40:33 -0700587 dev_image = os.path.join(local_dir, devserver_constants.IMAGE_FILE)
588 # Prioritize test images over dev images.
joychenc3944cb2013-08-19 10:42:07 -0700589 if os.path.exists(test_image):
590 return 'test'
591
Yu-Ju Hongc23c79b2014-03-17 12:40:33 -0700592 if os.path.exists(dev_image):
593 return 'dev'
594
joychenc3944cb2013-08-19 10:42:07 -0700595 raise XBuddyException('No images found in %s' % local_dir)
596
597 @staticmethod
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800598 def _InterpretPath(path, default_board=None, default_version=None):
joychen121fc9b2013-08-02 14:30:30 -0700599 """Split and return the pieces of an xBuddy path name
joychen921e1fb2013-06-28 11:12:20 -0700600
joychen121fc9b2013-08-02 14:30:30 -0700601 Args:
602 path: the path xBuddy Get was called with.
Chris Sosa0eecf962014-02-03 14:14:39 -0800603 default_board: board to use in case board isn't in path.
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800604 default_version: Version to use in case version isn't in path.
joychen3cb228e2013-06-12 12:13:13 -0700605
Yu-Ju Hongc54658c2014-01-22 09:18:07 -0800606 Returns:
Chris Sosa75490802013-09-30 17:21:45 -0700607 tuple of (image_type, board, version, whether the path is local)
joychen3cb228e2013-06-12 12:13:13 -0700608
609 Raises:
610 XBuddyException: if the path can't be resolved into valid components
611 """
joychen121fc9b2013-08-02 14:30:30 -0700612 path_list = filter(None, path.split('/'))
joychen7df67f72013-07-18 14:21:12 -0700613
Chris Sosa0eecf962014-02-03 14:14:39 -0800614 # Do the stuff that is well known first. We know that if paths have a
615 # image_type, it must be one of the GS/LOCAL aliases and it must be at the
616 # end. Similarly, local/remote are well-known and must start the path list.
617 is_local = True
618 if path_list and path_list[0] in (REMOTE, LOCAL):
619 is_local = (path_list.pop(0) == LOCAL)
joychen7df67f72013-07-18 14:21:12 -0700620
Chris Sosa0eecf962014-02-03 14:14:39 -0800621 # Default image type is determined by remote vs. local.
622 if is_local:
623 image_type = ANY
624 else:
625 image_type = TEST
joychen7df67f72013-07-18 14:21:12 -0700626
Chris Sosa0eecf962014-02-03 14:14:39 -0800627 if path_list and path_list[-1] in GS_ALIASES + LOCAL_ALIASES:
628 image_type = path_list.pop(-1)
joychen3cb228e2013-06-12 12:13:13 -0700629
Chris Sosa0eecf962014-02-03 14:14:39 -0800630 # Now for the tricky part. We don't actually know at this point if the rest
631 # of the path is just a board | version (like R33-2341.0.0) or just a board
632 # or just a version. So we do our best to do the right thing.
633 board = default_board
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800634 version = default_version or LATEST
Chris Sosa0eecf962014-02-03 14:14:39 -0800635 if len(path_list) == 1:
636 path = path_list.pop(0)
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800637 # Treat this as a version if it's one we know (contains default or
638 # latest), or we were given an actual default board.
639 if default_version in path or LATEST in path or default_board is not None:
Chris Sosa0eecf962014-02-03 14:14:39 -0800640 version = path
joychen7df67f72013-07-18 14:21:12 -0700641 else:
Chris Sosa0eecf962014-02-03 14:14:39 -0800642 board = path
joychen7df67f72013-07-18 14:21:12 -0700643
Chris Sosa0eecf962014-02-03 14:14:39 -0800644 elif len(path_list) == 2:
645 # Assumes board/version.
646 board = path_list.pop(0)
647 version = path_list.pop(0)
648
649 if path_list:
650 raise XBuddyException("Path isn't valid. Could not figure out how to "
651 "parse remaining components: %s." % path_list)
652
653 _Log("Get artifact '%s' with board %s and version %s'. Locally? %s",
joychen7df67f72013-07-18 14:21:12 -0700654 image_type, board, version, is_local)
655
656 return image_type, board, version, is_local
joychen3cb228e2013-06-12 12:13:13 -0700657
joychen921e1fb2013-06-28 11:12:20 -0700658 def _SyncRegistryWithBuildImages(self):
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800659 """Crawl images_dir for build_ids of images generated from build_image.
joychen5260b9a2013-07-16 14:48:01 -0700660
661 This will find images and symlink them in xBuddy's static dir so that
662 xBuddy's cache can serve them.
663 If xBuddy's _manage_builds option is on, then a timestamp will also be
664 generated, and xBuddy will clear them from the directory they are in, as
665 necessary.
666 """
Yu-Ju Hong235d1b52014-04-16 11:01:47 -0700667 if not os.path.isdir(self.images_dir):
668 # Skip syncing if images_dir does not exist.
669 _Log('Cannot find %s; skip syncing image registry.', self.images_dir)
670 return
671
joychen921e1fb2013-06-28 11:12:20 -0700672 build_ids = []
673 for b in os.listdir(self.images_dir):
Mike Frysingera2c24252017-11-28 19:14:45 -0500674 # Ignore random files in the build dir.
675 board_dir = os.path.join(self.images_dir, b)
676 if not os.path.isdir(board_dir):
677 continue
678
joychen5260b9a2013-07-16 14:48:01 -0700679 # Ensure we have directories to track all boards in build/images
680 common_util.MkDirP(os.path.join(self.static_dir, b))
joychen921e1fb2013-06-28 11:12:20 -0700681 build_ids.extend(['/'.join([b, v]) for v
joychenc3944cb2013-08-19 10:42:07 -0700682 in os.listdir(board_dir) if not v == LATEST])
joychen921e1fb2013-06-28 11:12:20 -0700683
joychen121fc9b2013-08-02 14:30:30 -0700684 # Symlink undiscovered images, and update timestamps if manage_builds is on.
joychen5260b9a2013-07-16 14:48:01 -0700685 for build_id in build_ids:
686 link = os.path.join(self.static_dir, build_id)
687 target = os.path.join(self.images_dir, build_id)
688 XBuddy._Symlink(link, target)
689 if self._manage_builds:
690 Timestamp.UpdateTimestamp(self._timestamp_folder, build_id)
joychen921e1fb2013-06-28 11:12:20 -0700691
692 def _ListBuildTimes(self):
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800693 """Returns the currently cached builds and their last access timestamp.
joychen3cb228e2013-06-12 12:13:13 -0700694
695 Returns:
696 list of tuples that matches xBuddy build/version to timestamps in long
697 """
joychen121fc9b2013-08-02 14:30:30 -0700698 # Update currently cached builds.
joychen3cb228e2013-06-12 12:13:13 -0700699 build_dict = {}
700
joychen7df67f72013-07-18 14:21:12 -0700701 for f in os.listdir(self._timestamp_folder):
joychen3cb228e2013-06-12 12:13:13 -0700702 last_accessed = os.path.getmtime(os.path.join(self._timestamp_folder, f))
703 build_id = Timestamp.TimestampToBuild(f)
joychenc3944cb2013-08-19 10:42:07 -0700704 stale_time = datetime.timedelta(seconds=(time.time() - last_accessed))
joychen921e1fb2013-06-28 11:12:20 -0700705 build_dict[build_id] = stale_time
joychen3cb228e2013-06-12 12:13:13 -0700706 return_tup = sorted(build_dict.iteritems(), key=operator.itemgetter(1))
707 return return_tup
708
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700709 def _Download(self, gs_url, artifacts, build_id):
Chris Sosa75490802013-09-30 17:21:45 -0700710 """Download the artifacts from the given gs_url.
711
712 Raises:
713 build_artifact.ArtifactDownloadError: If we failed to download the
714 artifact.
715 """
joychen3cb228e2013-06-12 12:13:13 -0700716 with XBuddy._staging_thread_count_lock:
717 XBuddy._staging_thread_count += 1
718 try:
Chris Sosa75490802013-09-30 17:21:45 -0700719 _Log("Downloading %s from %s", artifacts, gs_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700720 dl = downloader.GoogleStorageDownloader(self.static_dir, gs_url, build_id)
Gabe Black3b567202015-09-23 14:07:59 -0700721 factory = build_artifact.ChromeOSArtifactFactory(
722 dl.GetBuildDir(), artifacts, [], dl.GetBuild())
723 dl.Download(factory)
joychen3cb228e2013-06-12 12:13:13 -0700724 finally:
725 with XBuddy._staging_thread_count_lock:
726 XBuddy._staging_thread_count -= 1
727
Chris Sosa75490802013-09-30 17:21:45 -0700728 def CleanCache(self):
joychen562699a2013-08-13 15:22:14 -0700729 """Delete all builds besides the newest N builds"""
joychen121fc9b2013-08-02 14:30:30 -0700730 if not self._manage_builds:
731 return
joychen921e1fb2013-06-28 11:12:20 -0700732 cached_builds = [e[0] for e in self._ListBuildTimes()]
joychen3cb228e2013-06-12 12:13:13 -0700733 _Log('In cache now: %s', cached_builds)
734
joychen562699a2013-08-13 15:22:14 -0700735 for b in range(self._Capacity(), len(cached_builds)):
joychen3cb228e2013-06-12 12:13:13 -0700736 b_path = cached_builds[b]
joychen7df67f72013-07-18 14:21:12 -0700737 _Log("Clearing '%s' from cache", b_path)
joychen3cb228e2013-06-12 12:13:13 -0700738
739 time_file = os.path.join(self._timestamp_folder,
740 Timestamp.BuildToTimestamp(b_path))
joychen921e1fb2013-06-28 11:12:20 -0700741 os.unlink(time_file)
742 clear_dir = os.path.join(self.static_dir, b_path)
joychen3cb228e2013-06-12 12:13:13 -0700743 try:
joychen121fc9b2013-08-02 14:30:30 -0700744 # Handle symlinks, in the case of links to local builds if enabled.
745 if os.path.islink(clear_dir):
joychen5260b9a2013-07-16 14:48:01 -0700746 target = os.readlink(clear_dir)
747 _Log('Deleting locally built image at %s', target)
joychen921e1fb2013-06-28 11:12:20 -0700748
749 os.unlink(clear_dir)
joychen5260b9a2013-07-16 14:48:01 -0700750 if os.path.exists(target):
joychen921e1fb2013-06-28 11:12:20 -0700751 shutil.rmtree(target)
752 elif os.path.exists(clear_dir):
joychen5260b9a2013-07-16 14:48:01 -0700753 _Log('Deleting downloaded image at %s', clear_dir)
joychen3cb228e2013-06-12 12:13:13 -0700754 shutil.rmtree(clear_dir)
joychen921e1fb2013-06-28 11:12:20 -0700755
joychen121fc9b2013-08-02 14:30:30 -0700756 except Exception as err:
757 raise XBuddyException('Failed to clear %s: %s' % (clear_dir, err))
joychen3cb228e2013-06-12 12:13:13 -0700758
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700759 def _TranslateSignedGSUrl(self, build_id, channel=None):
760 """Translate the GS URL to be able to find signed images.
761
762 Args:
763 build_id: Path to the image or update directory on the devserver or
764 in Google Storage. e.g. 'x86-generic/R26-4000.0.0'
765 channel: The channel for the image. If none, it tries to guess it in
766 order of stability.
767
768 Returns:
769 The GS URL for the directory where the signed image can be found.
770
771 Raises:
772 build_artifact.ArtifactDownloadError: If we failed to download the
773 artifact.
774 """
775 match = re.match(r'^([^/]+?)(?:-release)?/R\d+-(.*)$', build_id)
776
777 channels = []
778 if channel:
779 channels.append(channel)
780 else:
781 # Attempt to enumerate all channels, in order of stability.
782 channels.extend(devserver_constants.CHANNELS[::-1])
783
784 for channel in channels:
785 image_dir = devserver_constants.GS_CHANNEL_DIR % {
786 'channel': channel,
787 'board': match.group(1),
788 }
789 gs_url = os.path.join(image_dir, match.group(2))
790 try:
791 self._LS(gs_url)
792 return gs_url
793 except gs.GSNoSuchKey:
794 continue
795 raise build_artifact.ArtifactDownloadError(
796 'Could not find signed image URL for %s in Google Storage' %
797 build_id)
798
799 def _GetFromGS(self, build_id, image_type, image_dir=None, channel=None):
Chris Sosa75490802013-09-30 17:21:45 -0700800 """Check if the artifact is available locally. Download from GS if not.
801
Simran Basi99e63c02014-05-20 10:39:52 -0700802 Args:
803 build_id: Path to the image or update directory on the devserver or
804 in Google Storage. e.g. 'x86-generic/R26-4000.0.0'
805 image_type: Image type to download. Look at aliases at top of file for
806 options.
807 image_dir: Google Storage image archive to search in if requesting a
808 remote artifact. If none uses the default bucket.
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700809 channel: The channel for the image. If none, it tries to guess it in
810 order of stability.
Simran Basi99e63c02014-05-20 10:39:52 -0700811
Chris Sosa75490802013-09-30 17:21:45 -0700812 Raises:
Alex Klein77df9352018-10-30 12:01:06 -0600813 build_artifact.ArtifactDownloadError: If we failed to download the
814 artifact.
Chris Sosa75490802013-09-30 17:21:45 -0700815 """
joychen121fc9b2013-08-02 14:30:30 -0700816 # Stage image if not found in cache.
joychen921e1fb2013-06-28 11:12:20 -0700817 file_name = GS_ALIAS_TO_FILENAME[image_type]
joychen346531c2013-07-24 16:55:56 -0700818 file_loc = os.path.join(self.static_dir, build_id, file_name)
819 cached = os.path.exists(file_loc)
820
joychen921e1fb2013-06-28 11:12:20 -0700821 if not cached:
Chris Sosa75490802013-09-30 17:21:45 -0700822 artifact = GS_ALIAS_TO_ARTIFACT[image_type]
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700823 if image_type == SIGNED:
824 gs_url = self._TranslateSignedGSUrl(build_id, channel=channel)
825 else:
826 image_dir = XBuddy._ResolveImageDir(image_dir)
827 gs_url = os.path.join(image_dir, build_id)
828 self._Download(gs_url, [artifact], build_id)
joychen921e1fb2013-06-28 11:12:20 -0700829 else:
830 _Log('Image already cached.')
831
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800832 def _GetArtifact(self, path_list, board=None, version=None,
833 lookup_only=False, image_dir=None):
joychen346531c2013-07-24 16:55:56 -0700834 """Interpret an xBuddy path and return directory/file_name to resource.
835
Chris Sosa75490802013-09-30 17:21:45 -0700836 Note board can be passed that in but by default if self._board is set,
837 that is used rather than board.
838
Simran Basi99e63c02014-05-20 10:39:52 -0700839 Args:
840 path_list: [board, version, alias] as split from the xbuddy call url.
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800841 board: Board whos artifacts we are looking for. Only used if no board was
842 given during XBuddy initialization.
843 version: Version whose artifacts we are looking for. Used if no version
844 was given during XBuddy initialization. If None, defers to LATEST.
Simran Basi99e63c02014-05-20 10:39:52 -0700845 lookup_only: If true just look up the artifact, if False stage it on
846 the devserver as well.
847 image_dir: Google Storage image archive to search in if requesting a
848 remote artifact. If none uses the default bucket.
849
joychen346531c2013-07-24 16:55:56 -0700850 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -0700851 build_id: Path to the image or update directory on the devserver or
852 in Google Storage. e.g. 'x86-generic/R26-4000.0.0'
853 file_name: of the artifact in the build_id directory.
joychen346531c2013-07-24 16:55:56 -0700854
855 Raises:
joychen121fc9b2013-08-02 14:30:30 -0700856 XBuddyException: if the path could not be translated
Chris Sosa75490802013-09-30 17:21:45 -0700857 build_artifact.ArtifactDownloadError: if we failed to download the
858 artifact.
joychen346531c2013-07-24 16:55:56 -0700859 """
joychen121fc9b2013-08-02 14:30:30 -0700860 path = '/'.join(path_list)
Chris Sosa0eecf962014-02-03 14:14:39 -0800861 default_board = self._board if self._board else board
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800862 default_version = self._version or version or LATEST
joychenb0dfe552013-07-30 10:02:06 -0700863 # Rewrite the path if there is an appropriate default.
Gilad Arnold38e828c2015-04-24 13:52:07 -0700864 path, suffix = self.LookupAlias(path, board=default_board,
865 version=default_version)
joychen121fc9b2013-08-02 14:30:30 -0700866 # Parse the path.
Chris Sosa0eecf962014-02-03 14:14:39 -0800867 image_type, board, version, is_local = self._InterpretPath(
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800868 path, default_board, default_version)
joychen7df67f72013-07-18 14:21:12 -0700869 if is_local:
joychen121fc9b2013-08-02 14:30:30 -0700870 # Get a local image.
joychen7df67f72013-07-18 14:21:12 -0700871 if version == LATEST:
joychen121fc9b2013-08-02 14:30:30 -0700872 # Get the latest local image for the given board.
873 version = self._GetLatestLocalVersion(board)
joychen7df67f72013-07-18 14:21:12 -0700874
joychenc3944cb2013-08-19 10:42:07 -0700875 build_id = os.path.join(board, version)
876 artifact_dir = os.path.join(self.static_dir, build_id)
877 if image_type == ANY:
878 image_type = self._FindAny(artifact_dir)
joychen121fc9b2013-08-02 14:30:30 -0700879
joychenc3944cb2013-08-19 10:42:07 -0700880 file_name = LOCAL_ALIAS_TO_FILENAME[image_type]
881 artifact_path = os.path.join(artifact_dir, file_name)
882 if not os.path.exists(artifact_path):
883 raise XBuddyException('Local %s artifact not in static_dir at %s' %
884 (image_type, artifact_path))
joychen121fc9b2013-08-02 14:30:30 -0700885
joychen921e1fb2013-06-28 11:12:20 -0700886 else:
joychen121fc9b2013-08-02 14:30:30 -0700887 # Get a remote image.
joychen921e1fb2013-06-28 11:12:20 -0700888 if image_type not in GS_ALIASES:
joychen7df67f72013-07-18 14:21:12 -0700889 raise XBuddyException('Bad remote image type: %s. Use one of: %s' %
joychen921e1fb2013-06-28 11:12:20 -0700890 (image_type, GS_ALIASES))
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700891 build_id, channel = self._ResolveVersionToBuildIdAndChannel(
892 board, suffix, version, image_dir=image_dir)
Chris Sosa75490802013-09-30 17:21:45 -0700893 _Log('Resolved version %s to %s.', version, build_id)
894 file_name = GS_ALIAS_TO_FILENAME[image_type]
895 if not lookup_only:
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700896 self._GetFromGS(build_id, image_type, image_dir=image_dir,
897 channel=channel)
joychenf8f07e22013-07-12 17:45:51 -0700898
joychenc3944cb2013-08-19 10:42:07 -0700899 return build_id, file_name
joychen3cb228e2013-06-12 12:13:13 -0700900
901 ############################ BEGIN PUBLIC METHODS
902
903 def List(self):
904 """Lists the currently available images & time since last access."""
joychen921e1fb2013-06-28 11:12:20 -0700905 self._SyncRegistryWithBuildImages()
906 builds = self._ListBuildTimes()
907 return_string = ''
908 for build, timestamp in builds:
909 return_string += '<b>' + build + '</b> '
910 return_string += '(time since last access: ' + str(timestamp) + ')<br>'
911 return return_string
joychen3cb228e2013-06-12 12:13:13 -0700912
913 def Capacity(self):
914 """Returns the number of images cached by xBuddy."""
joychen562699a2013-08-13 15:22:14 -0700915 return str(self._Capacity())
joychen3cb228e2013-06-12 12:13:13 -0700916
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800917 def Translate(self, path_list, board=None, version=None, image_dir=None):
joychen346531c2013-07-24 16:55:56 -0700918 """Translates an xBuddy path to a real path to artifact if it exists.
919
joychen121fc9b2013-08-02 14:30:30 -0700920 Equivalent to the Get call, minus downloading and updating timestamps,
joychen346531c2013-07-24 16:55:56 -0700921
Simran Basi99e63c02014-05-20 10:39:52 -0700922 Args:
923 path_list: [board, version, alias] as split from the xbuddy call url.
924 board: Board whos artifacts we are looking for. If None, use the board
925 XBuddy was initialized to use.
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800926 version: Version whose artifacts we are looking for. If None, use the
927 version XBuddy was initialized with, or LATEST.
Simran Basi99e63c02014-05-20 10:39:52 -0700928 image_dir: image directory to check in Google Storage. If none,
929 the default bucket is used.
930
joychen7c2054a2013-07-25 11:14:07 -0700931 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700932 build_id: Path to the image or update directory on the devserver.
933 e.g. 'x86-generic/R26-4000.0.0'
934 The returned path is always the path to the directory within
935 static_dir, so it is always the build_id of the image.
936 file_name: The file name of the artifact. Can take any of the file
937 values in devserver_constants.
938 e.g. 'chromiumos_test_image.bin' or 'update.gz' if the path list
939 specified 'test' or 'full_payload' artifacts, respectively.
joychen7c2054a2013-07-25 11:14:07 -0700940
joychen121fc9b2013-08-02 14:30:30 -0700941 Raises:
942 XBuddyException: if the path couldn't be translated
joychen346531c2013-07-24 16:55:56 -0700943 """
944 self._SyncRegistryWithBuildImages()
Chris Sosa75490802013-09-30 17:21:45 -0700945 build_id, file_name = self._GetArtifact(path_list, board=board,
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800946 version=version,
Simran Basi99e63c02014-05-20 10:39:52 -0700947 lookup_only=True,
948 image_dir=image_dir)
joychen346531c2013-07-24 16:55:56 -0700949
joychen121fc9b2013-08-02 14:30:30 -0700950 _Log('Returning path to payload: %s/%s', build_id, file_name)
951 return build_id, file_name
joychen346531c2013-07-24 16:55:56 -0700952
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700953 def StageTestArtifactsForUpdate(self, path_list):
Chris Sosa75490802013-09-30 17:21:45 -0700954 """Stages test artifacts for update and returns build_id.
955
956 Raises:
957 XBuddyException: if the path could not be translated
958 build_artifact.ArtifactDownloadError: if we failed to download the test
959 artifacts.
960 """
961 build_id, file_name = self.Translate(path_list)
962 if file_name == devserver_constants.TEST_IMAGE_FILE:
963 gs_url = os.path.join(devserver_constants.GS_IMAGE_DIR,
964 build_id)
965 artifacts = [FULL, STATEFUL]
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700966 self._Download(gs_url, artifacts, build_id)
Chris Sosa75490802013-09-30 17:21:45 -0700967 return build_id
968
Simran Basi99e63c02014-05-20 10:39:52 -0700969 def Get(self, path_list, image_dir=None):
joychen921e1fb2013-06-28 11:12:20 -0700970 """The full xBuddy call, returns resource specified by path_list.
joychen3cb228e2013-06-12 12:13:13 -0700971
972 Please see devserver.py:xbuddy for full documentation.
joychen121fc9b2013-08-02 14:30:30 -0700973
joychen3cb228e2013-06-12 12:13:13 -0700974 Args:
Simran Basi99e63c02014-05-20 10:39:52 -0700975 path_list: [board, version, alias] as split from the xbuddy call url.
976 image_dir: image directory to check in Google Storage. If none,
977 the default bucket is used.
joychen3cb228e2013-06-12 12:13:13 -0700978
979 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700980 build_id: Path to the image or update directory on the devserver.
Simran Basi99e63c02014-05-20 10:39:52 -0700981 e.g. 'x86-generic/R26-4000.0.0'
982 The returned path is always the path to the directory within
983 static_dir, so it is always the build_id of the image.
joychen121fc9b2013-08-02 14:30:30 -0700984 file_name: The file name of the artifact. Can take any of the file
Simran Basi99e63c02014-05-20 10:39:52 -0700985 values in devserver_constants.
986 e.g. 'chromiumos_test_image.bin' or 'update.gz' if the path list
987 specified 'test' or 'full_payload' artifacts, respectively.
joychen3cb228e2013-06-12 12:13:13 -0700988
989 Raises:
Chris Sosa75490802013-09-30 17:21:45 -0700990 XBuddyException: if the path could not be translated
991 build_artifact.ArtifactDownloadError: if we failed to download the
992 artifact.
joychen3cb228e2013-06-12 12:13:13 -0700993 """
joychen7df67f72013-07-18 14:21:12 -0700994 self._SyncRegistryWithBuildImages()
Simran Basi99e63c02014-05-20 10:39:52 -0700995 build_id, file_name = self._GetArtifact(path_list, image_dir=image_dir)
joychen921e1fb2013-06-28 11:12:20 -0700996 Timestamp.UpdateTimestamp(self._timestamp_folder, build_id)
joychen3cb228e2013-06-12 12:13:13 -0700997 #TODO (joyc): run in sep thread
Chris Sosa75490802013-09-30 17:21:45 -0700998 self.CleanCache()
joychen3cb228e2013-06-12 12:13:13 -0700999
joychen121fc9b2013-08-02 14:30:30 -07001000 _Log('Returning path to payload: %s/%s', build_id, file_name)
1001 return build_id, file_name