blob: 7747ef7a8a91ea1a0c4892379350d2ebb38bf04e [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
joychen3cb228e2013-06-12 12:13:13 -070010import datetime
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +020011import distutils.version # pylint: disable=import-error,no-name-in-module
joychen3cb228e2013-06-12 12:13:13 -070012import operator
13import os
joychenf8f07e22013-07-12 17:45:51 -070014import re
joychen3cb228e2013-06-12 12:13:13 -070015import shutil
joychenf8f07e22013-07-12 17:45:51 -070016import time
joychen3cb228e2013-06-12 12:13:13 -070017import threading
18
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +020019import cherrypy # pylint: disable=import-error
20from six.moves import configparser
21
joychen3cb228e2013-06-12 12:13:13 -070022import artifact_info
Gabe Black3b567202015-09-23 14:07:59 -070023import build_artifact
24import build_util
joychen3cb228e2013-06-12 12:13:13 -070025import common_util
26import devserver_constants
27import downloader
28import log_util
29
xixuan44b55452016-09-06 15:35:56 -070030# Make sure that chromite is available to import.
31import setup_chromite # pylint: disable=unused-import
32
33try:
34 from chromite.lib import gs
Gwendal Grignouad4cb982017-03-31 11:36:19 -070035except ImportError:
xixuan44b55452016-09-06 15:35:56 -070036 gs = None
37
joychen3cb228e2013-06-12 12:13:13 -070038# Module-local log function.
39def _Log(message, *args):
40 return log_util.LogWithTag('XBUDDY', message, *args)
41
joychen562699a2013-08-13 15:22:14 -070042# xBuddy config constants
43CONFIG_FILE = 'xbuddy_config.ini'
44SHADOW_CONFIG_FILE = 'shadow_xbuddy_config.ini'
45PATH_REWRITES = 'PATH_REWRITES'
46GENERAL = 'GENERAL'
Gilad Arnold896c6d82015-03-13 16:20:29 -070047LOCATION_SUFFIXES = 'LOCATION_SUFFIXES'
joychen921e1fb2013-06-28 11:12:20 -070048
Chris Sosac2abc722013-08-26 17:11:22 -070049# Path for shadow config in chroot.
50CHROOT_SHADOW_DIR = '/mnt/host/source/src/platform/dev'
51
joychen25d25972013-07-30 14:54:16 -070052# XBuddy aliases
53TEST = 'test'
54BASE = 'base'
55DEV = 'dev'
56FULL = 'full_payload'
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -070057SIGNED = 'signed'
joychen25d25972013-07-30 14:54:16 -070058RECOVERY = 'recovery'
59STATEFUL = 'stateful'
60AUTOTEST = 'autotest'
Mike Frysingera0e6a282016-09-01 17:29:08 -040061FACTORY_SHIM = 'factory_shim'
joychen25d25972013-07-30 14:54:16 -070062
joychen921e1fb2013-06-28 11:12:20 -070063# Local build constants
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +020064ANY = 'ANY'
65LATEST = 'latest'
66LOCAL = 'local'
67REMOTE = 'remote'
Chris Sosa75490802013-09-30 17:21:45 -070068
69# TODO(sosa): Fix a lot of assumptions about these aliases. There is too much
70# implicit logic here that's unnecessary. What should be done:
71# 1) Collapse Alias logic to one set of aliases for xbuddy (not local/remote).
72# 2) Do not use zip when creating these dicts. Better to not rely on ordering.
73# 3) Move alias/artifact mapping to a central module rather than having it here.
74# 4) Be explicit when things are missing i.e. no dev images in image.zip.
75
joychen921e1fb2013-06-28 11:12:20 -070076LOCAL_ALIASES = [
Gilad Arnold5f46d8e2015-02-19 12:17:55 -080077 TEST,
78 DEV,
79 BASE,
80 RECOVERY,
Mike Frysingera0e6a282016-09-01 17:29:08 -040081 FACTORY_SHIM,
Gilad Arnold5f46d8e2015-02-19 12:17:55 -080082 FULL,
83 STATEFUL,
84 ANY,
joychen921e1fb2013-06-28 11:12:20 -070085]
86
87LOCAL_FILE_NAMES = [
Gilad Arnold5f46d8e2015-02-19 12:17:55 -080088 devserver_constants.TEST_IMAGE_FILE,
89 devserver_constants.IMAGE_FILE,
90 devserver_constants.BASE_IMAGE_FILE,
91 devserver_constants.RECOVERY_IMAGE_FILE,
Mike Frysingera0e6a282016-09-01 17:29:08 -040092 devserver_constants.FACTORY_SHIM_IMAGE_FILE,
Gilad Arnold5f46d8e2015-02-19 12:17:55 -080093 devserver_constants.UPDATE_FILE,
94 devserver_constants.STATEFUL_FILE,
95 None, # For ANY.
joychen921e1fb2013-06-28 11:12:20 -070096]
97
98LOCAL_ALIAS_TO_FILENAME = dict(zip(LOCAL_ALIASES, LOCAL_FILE_NAMES))
99
100# Google Storage constants
101GS_ALIASES = [
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800102 TEST,
103 BASE,
104 RECOVERY,
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700105 SIGNED,
Mike Frysingera0e6a282016-09-01 17:29:08 -0400106 FACTORY_SHIM,
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800107 FULL,
108 STATEFUL,
109 AUTOTEST,
joychen3cb228e2013-06-12 12:13:13 -0700110]
111
joychen921e1fb2013-06-28 11:12:20 -0700112GS_FILE_NAMES = [
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800113 devserver_constants.TEST_IMAGE_FILE,
114 devserver_constants.BASE_IMAGE_FILE,
115 devserver_constants.RECOVERY_IMAGE_FILE,
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700116 devserver_constants.SIGNED_IMAGE_FILE,
Mike Frysingera0e6a282016-09-01 17:29:08 -0400117 devserver_constants.FACTORY_SHIM_IMAGE_FILE,
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800118 devserver_constants.UPDATE_FILE,
119 devserver_constants.STATEFUL_FILE,
120 devserver_constants.AUTOTEST_DIR,
joychen3cb228e2013-06-12 12:13:13 -0700121]
122
123ARTIFACTS = [
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800124 artifact_info.TEST_IMAGE,
125 artifact_info.BASE_IMAGE,
126 artifact_info.RECOVERY_IMAGE,
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700127 artifact_info.SIGNED_IMAGE,
Mike Frysingera0e6a282016-09-01 17:29:08 -0400128 artifact_info.FACTORY_SHIM_IMAGE,
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800129 artifact_info.FULL_PAYLOAD,
130 artifact_info.STATEFUL_PAYLOAD,
131 artifact_info.AUTOTEST,
joychen3cb228e2013-06-12 12:13:13 -0700132]
133
joychen921e1fb2013-06-28 11:12:20 -0700134GS_ALIAS_TO_FILENAME = dict(zip(GS_ALIASES, GS_FILE_NAMES))
135GS_ALIAS_TO_ARTIFACT = dict(zip(GS_ALIASES, ARTIFACTS))
joychen3cb228e2013-06-12 12:13:13 -0700136
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200137LATEST_OFFICIAL = 'latest-official'
joychen3cb228e2013-06-12 12:13:13 -0700138
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200139RELEASE = '-release'
joychen3cb228e2013-06-12 12:13:13 -0700140
joychen3cb228e2013-06-12 12:13:13 -0700141
142class XBuddyException(Exception):
143 """Exception classes used by this module."""
144 pass
145
146
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800147class Timestamp(object):
joychen3cb228e2013-06-12 12:13:13 -0700148 """Class to translate build path strings and timestamp filenames."""
149
150 _TIMESTAMP_DELIMITER = 'SLASH'
151 XBUDDY_TIMESTAMP_DIR = 'xbuddy_UpdateTimestamps'
152
153 @staticmethod
154 def TimestampToBuild(timestamp_filename):
155 return timestamp_filename.replace(Timestamp._TIMESTAMP_DELIMITER, '/')
156
157 @staticmethod
158 def BuildToTimestamp(build_path):
159 return build_path.replace('/', Timestamp._TIMESTAMP_DELIMITER)
joychen921e1fb2013-06-28 11:12:20 -0700160
161 @staticmethod
162 def UpdateTimestamp(timestamp_dir, build_id):
163 """Update timestamp file of build with build_id."""
164 common_util.MkDirP(timestamp_dir)
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200165 _Log('Updating timestamp for %s', build_id)
joychen921e1fb2013-06-28 11:12:20 -0700166 time_file = os.path.join(timestamp_dir,
167 Timestamp.BuildToTimestamp(build_id))
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200168 with open(time_file, 'a'):
joychen921e1fb2013-06-28 11:12:20 -0700169 os.utime(time_file, None)
joychen3cb228e2013-06-12 12:13:13 -0700170
171
joychen921e1fb2013-06-28 11:12:20 -0700172class XBuddy(build_util.BuildObject):
joychen3cb228e2013-06-12 12:13:13 -0700173 """Class that manages image retrieval and caching by the devserver.
174
175 Image retrieval by xBuddy path:
176 XBuddy accesses images and artifacts that it stores using an xBuddy
177 path of the form: board/version/alias
178 The primary xbuddy.Get call retrieves the correct artifact or url to where
179 the artifacts can be found.
180
181 Image caching:
182 Images and other artifacts are stored identically to how they would have
183 been if devserver's stage rpc was called and the xBuddy cache replaces
184 build versions on a LRU basis. Timestamps are maintained by last accessed
185 times of representative files in the a directory in the static serve
186 directory (XBUDDY_TIMESTAMP_DIR).
187
188 Private class members:
joychen121fc9b2013-08-02 14:30:30 -0700189 _true_values: used for interpreting boolean values
190 _staging_thread_count: track download requests
191 _timestamp_folder: directory with empty files standing in as timestamps
joychen921e1fb2013-06-28 11:12:20 -0700192 for each image currently cached by xBuddy
joychen3cb228e2013-06-12 12:13:13 -0700193 """
194 _true_values = ['true', 't', 'yes', 'y']
195
196 # Number of threads that are staging images.
197 _staging_thread_count = 0
198 # Lock used to lock increasing/decreasing count.
199 _staging_thread_count_lock = threading.Lock()
200
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800201 def __init__(self, manage_builds=False, board=None, version=None,
202 images_dir=None, log_screen=True, **kwargs):
joychen921e1fb2013-06-28 11:12:20 -0700203 super(XBuddy, self).__init__(**kwargs)
joychenb0dfe552013-07-30 10:02:06 -0700204
Yiming Chenaab488e2014-11-17 14:49:31 -0800205 if not log_screen:
206 cherrypy.config.update({'log.screen': False})
207
joychen562699a2013-08-13 15:22:14 -0700208 self.config = self._ReadConfig()
209 self._manage_builds = manage_builds or self._ManageBuilds()
Chris Sosa75490802013-09-30 17:21:45 -0700210 self._board = board
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800211 self._version = version
joychen921e1fb2013-06-28 11:12:20 -0700212 self._timestamp_folder = os.path.join(self.static_dir,
joychen3cb228e2013-06-12 12:13:13 -0700213 Timestamp.XBUDDY_TIMESTAMP_DIR)
Chris Sosa7cd23202013-10-15 17:22:57 -0700214 if images_dir:
215 self.images_dir = images_dir
216 else:
217 self.images_dir = os.path.join(self.GetSourceRoot(), 'src/build/images')
218
xixuan178263c2017-03-22 09:10:25 -0700219 if common_util.IsRunningOnMoblab():
220 self._ctx = gs.GSContext(cache_user='chronos') if gs else None
221 else:
222 self._ctx = gs.GSContext() if gs else None
xixuan44b55452016-09-06 15:35:56 -0700223
joychen7df67f72013-07-18 14:21:12 -0700224 common_util.MkDirP(self._timestamp_folder)
joychen3cb228e2013-06-12 12:13:13 -0700225
226 @classmethod
227 def ParseBoolean(cls, boolean_string):
228 """Evaluate a string to a boolean value"""
229 if boolean_string:
230 return boolean_string.lower() in cls._true_values
231 else:
232 return False
233
joychen562699a2013-08-13 15:22:14 -0700234 def _ReadConfig(self):
235 """Read xbuddy config from ini files.
236
237 Reads the base config from xbuddy_config.ini, and then merges in the
238 shadow config from shadow_xbuddy_config.ini
239
240 Returns:
241 The merged configuration.
Yu-Ju Hongc54658c2014-01-22 09:18:07 -0800242
joychen562699a2013-08-13 15:22:14 -0700243 Raises:
244 XBuddyException if the config file is missing.
245 """
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200246 xbuddy_config = configparser.ConfigParser()
joychen562699a2013-08-13 15:22:14 -0700247 config_file = os.path.join(self.devserver_dir, CONFIG_FILE)
248 if os.path.exists(config_file):
249 xbuddy_config.read(config_file)
250 else:
Yiming Chend9202142014-11-07 14:56:52 -0800251 # Get the directory of xbuddy.py file.
252 file_dir = os.path.dirname(os.path.realpath(__file__))
253 # Read the default xbuddy_config.ini from the directory.
254 xbuddy_config.read(os.path.join(file_dir, CONFIG_FILE))
joychen562699a2013-08-13 15:22:14 -0700255
256 # Read the shadow file if there is one.
Chris Sosac2abc722013-08-26 17:11:22 -0700257 if os.path.isdir(CHROOT_SHADOW_DIR):
258 shadow_config_file = os.path.join(CHROOT_SHADOW_DIR, SHADOW_CONFIG_FILE)
259 else:
260 shadow_config_file = os.path.join(self.devserver_dir, SHADOW_CONFIG_FILE)
261
262 _Log('Using shadow config file stored at %s', shadow_config_file)
joychen562699a2013-08-13 15:22:14 -0700263 if os.path.exists(shadow_config_file):
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200264 shadow_xbuddy_config = configparser.ConfigParser()
joychen562699a2013-08-13 15:22:14 -0700265 shadow_xbuddy_config.read(shadow_config_file)
266
267 # Merge shadow config in.
268 sections = shadow_xbuddy_config.sections()
269 for s in sections:
270 if not xbuddy_config.has_section(s):
271 xbuddy_config.add_section(s)
272 options = shadow_xbuddy_config.options(s)
273 for o in options:
274 val = shadow_xbuddy_config.get(s, o)
275 xbuddy_config.set(s, o, val)
276
277 return xbuddy_config
278
279 def _ManageBuilds(self):
280 """Checks if xBuddy is managing local builds using the current config."""
281 try:
282 return self.ParseBoolean(self.config.get(GENERAL, 'manage_builds'))
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200283 except configparser.Error:
joychen562699a2013-08-13 15:22:14 -0700284 return False
285
286 def _Capacity(self):
287 """Gets the xbuddy capacity from the current config."""
288 try:
289 return int(self.config.get(GENERAL, 'capacity'))
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200290 except configparser.Error:
joychen562699a2013-08-13 15:22:14 -0700291 return 5
292
Gilad Arnold38e828c2015-04-24 13:52:07 -0700293 def LookupAlias(self, alias, board=None, version=None):
joychen562699a2013-08-13 15:22:14 -0700294 """Given the full xbuddy config, look up an alias for path rewrite.
295
296 Args:
297 alias: The xbuddy path that could be one of the aliases in the
298 rewrite table.
299 board: The board to fill in with when paths are rewritten. Can be from
Gilad Arnold38e828c2015-04-24 13:52:07 -0700300 the update request xml or the default board from devserver. If None,
301 defers to the value given during XBuddy initialization.
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800302 version: The version to fill in when rewriting paths. Could be a specific
Gilad Arnold38e828c2015-04-24 13:52:07 -0700303 version number or a version alias like LATEST. If None, defers to the
304 value given during XBuddy initialization, or LATEST.
Yu-Ju Hongc54658c2014-01-22 09:18:07 -0800305
joychen562699a2013-08-13 15:22:14 -0700306 Returns:
Gilad Arnold896c6d82015-03-13 16:20:29 -0700307 A pair (val, suffix) where val is the rewritten path, or the original
308 string if no rewrite was found; and suffix is the assigned location
309 suffix, or the default suffix if none was found.
joychen562699a2013-08-13 15:22:14 -0700310 """
joychen562699a2013-08-13 15:22:14 -0700311 try:
Gilad Arnold896c6d82015-03-13 16:20:29 -0700312 suffix = self.config.get(LOCATION_SUFFIXES, alias)
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200313 except configparser.Error:
Gilad Arnold896c6d82015-03-13 16:20:29 -0700314 suffix = RELEASE
315
316 try:
joychen562699a2013-08-13 15:22:14 -0700317 val = self.config.get(PATH_REWRITES, alias)
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200318 except configparser.Error:
joychen562699a2013-08-13 15:22:14 -0700319 # No alias lookup found. Return original path.
Gilad Arnold896c6d82015-03-13 16:20:29 -0700320 val = None
joychen562699a2013-08-13 15:22:14 -0700321
Gilad Arnold896c6d82015-03-13 16:20:29 -0700322 if not (val and val.strip()):
323 val = alias
joychen562699a2013-08-13 15:22:14 -0700324 else:
Gilad Arnold896c6d82015-03-13 16:20:29 -0700325 # The found value is not an empty string.
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800326 # Fill in the board and version.
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200327 val = val.replace('BOARD', '%(board)s')
328 val = val.replace('VERSION', '%(version)s')
Gilad Arnold38e828c2015-04-24 13:52:07 -0700329 val = val % {'board': board or self._board,
330 'version': version or self._version or LATEST}
Gilad Arnold896c6d82015-03-13 16:20:29 -0700331
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200332 _Log('Path is %s, location suffix is %s', val, suffix)
Gilad Arnold896c6d82015-03-13 16:20:29 -0700333 return val, suffix
joychen562699a2013-08-13 15:22:14 -0700334
Simran Basi99e63c02014-05-20 10:39:52 -0700335 @staticmethod
336 def _ResolveImageDir(image_dir):
337 """Clean up and return the image dir to use.
338
339 Args:
340 image_dir: directory in Google Storage to use.
341
342 Returns:
343 |image_dir| if |image_dir| is not None. Otherwise, returns
344 devserver_constants.GS_IMAGE_DIR
345 """
346 image_dir = image_dir or devserver_constants.GS_IMAGE_DIR
347 # Remove trailing slashes.
348 return image_dir.rstrip('/')
349
Gilad Arnold896c6d82015-03-13 16:20:29 -0700350 def _LookupOfficial(self, board, suffix, image_dir=None):
joychenf8f07e22013-07-12 17:45:51 -0700351 """Check LATEST-master for the version number of interest."""
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200352 _Log('Checking gs for latest %s-%s image', board, suffix)
Simran Basi99e63c02014-05-20 10:39:52 -0700353 image_dir = XBuddy._ResolveImageDir(image_dir)
354 latest_addr = (devserver_constants.GS_LATEST_MASTER %
355 {'image_dir': image_dir,
356 'board': board,
357 'suffix': suffix})
joychen121fc9b2013-08-02 14:30:30 -0700358 # Full release + version is in the LATEST file.
xixuan44b55452016-09-06 15:35:56 -0700359 version = self._ctx.Cat(latest_addr)
joychen3cb228e2013-06-12 12:13:13 -0700360
joychenf8f07e22013-07-12 17:45:51 -0700361 return devserver_constants.IMAGE_DIR % {'board':board,
362 'suffix':suffix,
363 'version':version}
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800364
xixuan44b55452016-09-06 15:35:56 -0700365 def _LS(self, path, list_subdirectory=False):
366 """Does a directory listing of the given gs path.
367
368 Args:
369 path: directory location on google storage to check.
370 list_subdirectory: whether to only list subdirectory for |path|.
371
372 Returns:
373 A list of paths that matched |path|.
374 """
375 if list_subdirectory:
376 return self._ctx.DoCommand(
Gwendal Grignou76d08792017-03-31 11:32:16 -0700377 ['ls', '-d', '--', path], redirect_stdout=True).output.splitlines()
xixuan44b55452016-09-06 15:35:56 -0700378 else:
379 return self._ctx.LS(path)
380
381 def _GetLatestVersionFromGsDir(self, path, list_subdirectory=False,
382 with_release=True):
383 """Returns most recent version number found in a google storage directory.
384
385 This lists out the contents of the given GS bucket or regex to GS buckets,
386 and tries to grab the newest version found in the directory names.
387
388 Args:
389 path: directory location on google storage to check.
390 list_subdirectory: whether to only list subdirectory for |path|.
391 with_release: whether versions include a release milestone (e.g. R12).
392
393 Returns:
394 The most recent version number found.
395 """
396 list_result = self._LS(path, list_subdirectory=list_subdirectory)
397 dir_names = [os.path.basename(p.rstrip('/')) for p in list_result]
398 try:
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200399 versions_re = re.compile(devserver_constants.VERSION_RE if with_release
400 else devserver_constants.VERSION)
401 versions = [d for d in dir_names if versions_re.match(d)]
xixuan44b55452016-09-06 15:35:56 -0700402 latest_version = max(versions, key=distutils.version.LooseVersion)
403 except ValueError:
404 raise gs.GSContextException(
405 'Failed to find most recent builds at %s' % path)
406
407 return latest_version
408
Gilad Arnold896c6d82015-03-13 16:20:29 -0700409 def _LookupChannel(self, board, suffix, channel='stable',
410 image_dir=None):
joychenf8f07e22013-07-12 17:45:51 -0700411 """Check the channel folder for the version number of interest."""
joychen121fc9b2013-08-02 14:30:30 -0700412 # Get all names in channel dir. Get 10 highest directories by version.
joychen7df67f72013-07-18 14:21:12 -0700413 _Log("Checking channel '%s' for latest '%s' image", channel, board)
Yu-Ju Hongc54658c2014-01-22 09:18:07 -0800414 # Due to historical reasons, gs://chromeos-releases uses
415 # daisy-spring as opposed to the board name daisy_spring. Convert
xixuan44b55452016-09-06 15:35:56 -0700416 # he board name for the lookup.
Yu-Ju Hongc54658c2014-01-22 09:18:07 -0800417 channel_dir = devserver_constants.GS_CHANNEL_DIR % {
418 'channel':channel,
419 'board':re.sub('_', '-', board)}
xixuan44b55452016-09-06 15:35:56 -0700420 latest_version = self._GetLatestVersionFromGsDir(channel_dir,
421 with_release=False)
joychenf8f07e22013-07-12 17:45:51 -0700422
joychen121fc9b2013-08-02 14:30:30 -0700423 # Figure out release number from the version number.
joychenc3944cb2013-08-19 10:42:07 -0700424 image_url = devserver_constants.IMAGE_DIR % {
Gilad Arnold896c6d82015-03-13 16:20:29 -0700425 'board': board,
426 'suffix': suffix,
427 'version': 'R*' + latest_version}
Simran Basi99e63c02014-05-20 10:39:52 -0700428 image_dir = XBuddy._ResolveImageDir(image_dir)
429 gs_url = os.path.join(image_dir, image_url)
joychenf8f07e22013-07-12 17:45:51 -0700430
431 # There should only be one match on cros-image-archive.
xixuan44b55452016-09-06 15:35:56 -0700432 full_version = self._GetLatestVersionFromGsDir(gs_url,
433 list_subdirectory=True)
joychenf8f07e22013-07-12 17:45:51 -0700434
Gilad Arnold896c6d82015-03-13 16:20:29 -0700435 return devserver_constants.IMAGE_DIR % {'board': board,
436 'suffix': suffix,
437 'version': full_version}
joychenf8f07e22013-07-12 17:45:51 -0700438
Gilad Arnold896c6d82015-03-13 16:20:29 -0700439 def _LookupVersion(self, board, suffix, version):
joychenf8f07e22013-07-12 17:45:51 -0700440 """Search GS image releases for the highest match to a version prefix."""
joychen121fc9b2013-08-02 14:30:30 -0700441 # Build the pattern for GS to match.
joychen7df67f72013-07-18 14:21:12 -0700442 _Log("Checking gs for latest '%s' image with prefix '%s'", board, version)
Gilad Arnold896c6d82015-03-13 16:20:29 -0700443 image_url = devserver_constants.IMAGE_DIR % {'board': board,
444 'suffix': suffix,
445 'version': version + '*'}
joychenf8f07e22013-07-12 17:45:51 -0700446 image_dir = os.path.join(devserver_constants.GS_IMAGE_DIR, image_url)
447
joychen121fc9b2013-08-02 14:30:30 -0700448 # Grab the newest version of the ones matched.
xixuan44b55452016-09-06 15:35:56 -0700449 full_version = self._GetLatestVersionFromGsDir(image_dir,
450 list_subdirectory=True)
Gilad Arnold896c6d82015-03-13 16:20:29 -0700451 return devserver_constants.IMAGE_DIR % {'board': board,
452 'suffix': suffix,
453 'version': full_version}
joychenf8f07e22013-07-12 17:45:51 -0700454
Gilad Arnold896c6d82015-03-13 16:20:29 -0700455 def _RemoteBuildId(self, board, suffix, version):
Chris Sosaea734d92013-10-11 11:28:58 -0700456 """Returns the remote build_id for the given board and version.
457
458 Raises:
459 XBuddyException: If we failed to resolve the version to a valid build_id.
460 """
Gilad Arnold896c6d82015-03-13 16:20:29 -0700461 build_id_as_is = devserver_constants.IMAGE_DIR % {'board': board,
462 'suffix': '',
463 'version': version}
464 build_id_suffix = devserver_constants.IMAGE_DIR % {'board': board,
465 'suffix': suffix,
466 'version': version}
Chris Sosaea734d92013-10-11 11:28:58 -0700467 # Return the first path that exists. We assume that what the user typed
468 # is better than with a default suffix added i.e. x86-generic/blah is
469 # more valuable than x86-generic-release/blah.
Gilad Arnold896c6d82015-03-13 16:20:29 -0700470 for build_id in build_id_as_is, build_id_suffix:
Chris Sosaea734d92013-10-11 11:28:58 -0700471 try:
xixuan44b55452016-09-06 15:35:56 -0700472 version = self._ctx.LS(
473 '%s/%s' % (devserver_constants.GS_IMAGE_DIR, build_id))
Chris Sosaea734d92013-10-11 11:28:58 -0700474 return build_id
xixuan44b55452016-09-06 15:35:56 -0700475 except (gs.GSCommandError, gs.GSContextException, gs.GSNoSuchKey):
Chris Sosaea734d92013-10-11 11:28:58 -0700476 continue
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800477
478 raise XBuddyException('Could not find remote build_id for %s %s' % (
479 board, version))
Chris Sosaea734d92013-10-11 11:28:58 -0700480
Gilad Arnold896c6d82015-03-13 16:20:29 -0700481 def _ResolveBuildVersion(self, board, suffix, base_version):
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800482 """Check LATEST-<base_version> and returns a full build version."""
483 _Log('Checking gs for full version for %s of %s', base_version, board)
484 # TODO(garnold) We might want to accommodate version prefixes and pick the
485 # most recent found, as done in _LookupVersion().
486 latest_addr = (devserver_constants.GS_LATEST_BASE_VERSION %
487 {'image_dir': devserver_constants.GS_IMAGE_DIR,
488 'board': board,
Gilad Arnold896c6d82015-03-13 16:20:29 -0700489 'suffix': suffix,
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800490 'base_version': base_version})
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800491 # Full release + version is in the LATEST file.
xixuan44b55452016-09-06 15:35:56 -0700492 return self._ctx.Cat(latest_addr)
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800493
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700494 def _ResolveVersionToBuildIdAndChannel(self, board, suffix, version,
495 image_dir=None):
joychen121fc9b2013-08-02 14:30:30 -0700496 """Handle version aliases for remote payloads in GS.
joychen3cb228e2013-06-12 12:13:13 -0700497
498 Args:
499 board: as specified in the original call. (i.e. x86-generic, parrot)
Gilad Arnold896c6d82015-03-13 16:20:29 -0700500 suffix: The location suffix, to be added to board name.
joychen3cb228e2013-06-12 12:13:13 -0700501 version: as entered in the original call. can be
502 {TBD, 0. some custom alias as defined in a config file}
Ningning Xiab2a1af52016-04-22 11:14:42 -0700503 1. fully qualified build version.
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800504 2. latest
505 3. latest-{channel}
506 4. latest-official-{board suffix}
507 5. version prefix (i.e. RX-Y.X, RX-Y, RX)
Simran Basi99e63c02014-05-20 10:39:52 -0700508 image_dir: image directory to check in Google Storage. If none,
509 the default bucket is used.
joychen3cb228e2013-06-12 12:13:13 -0700510
511 Returns:
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700512 Tuple of (Location where the image dir is actually found on GS (build_id),
513 best guess for the channel).
joychen3cb228e2013-06-12 12:13:13 -0700514
Chris Sosaea734d92013-10-11 11:28:58 -0700515 Raises:
516 XBuddyException: If we failed to resolve the version to a valid url.
joychen3cb228e2013-06-12 12:13:13 -0700517 """
joychenf8f07e22013-07-12 17:45:51 -0700518 # Only the last segment of the alias is variable relative to the rest.
519 version_tuple = version.rsplit('-', 1)
joychen3cb228e2013-06-12 12:13:13 -0700520
joychenf8f07e22013-07-12 17:45:51 -0700521 if re.match(devserver_constants.VERSION_RE, version):
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700522 return self._RemoteBuildId(board, suffix, version), None
Gilad Arnold869e8ab2015-02-19 23:34:49 -0800523 elif re.match(devserver_constants.VERSION, version):
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200524 raise XBuddyException('"%s" is not valid. Should provide the fully '
525 'qualified version with a version prefix "RX-" '
Ningning Xiab2a1af52016-04-22 11:14:42 -0700526 'due to crbug.com/585914' % version)
joychenf8f07e22013-07-12 17:45:51 -0700527 elif version == LATEST_OFFICIAL:
528 # latest-official --> LATEST build in board-release
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700529 return self._LookupOfficial(board, suffix, image_dir=image_dir), None
joychenf8f07e22013-07-12 17:45:51 -0700530 elif version_tuple[0] == LATEST_OFFICIAL:
531 # latest-official-{suffix} --> LATEST build in board-{suffix}
Gilad Arnold896c6d82015-03-13 16:20:29 -0700532 return self._LookupOfficial(board, version_tuple[1],
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700533 image_dir=image_dir), None
joychenf8f07e22013-07-12 17:45:51 -0700534 elif version == LATEST:
535 # latest --> latest build on stable channel
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700536 return self._LookupChannel(board, suffix, image_dir=image_dir), 'stable'
joychenf8f07e22013-07-12 17:45:51 -0700537 elif version_tuple[0] == LATEST:
538 if re.match(devserver_constants.VERSION_RE, version_tuple[1]):
539 # latest-R* --> most recent qualifying build
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700540 return self._LookupVersion(board, suffix, version_tuple[1]), None
joychenf8f07e22013-07-12 17:45:51 -0700541 else:
542 # latest-{channel} --> latest build within that channel
Gilad Arnold896c6d82015-03-13 16:20:29 -0700543 return self._LookupChannel(board, suffix, channel=version_tuple[1],
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700544 image_dir=image_dir), version_tuple[1]
joychen3cb228e2013-06-12 12:13:13 -0700545 else:
546 # The given version doesn't match any known patterns.
joychen921e1fb2013-06-28 11:12:20 -0700547 raise XBuddyException("Version %s unknown. Can't find on GS." % version)
joychen3cb228e2013-06-12 12:13:13 -0700548
joychen5260b9a2013-07-16 14:48:01 -0700549 @staticmethod
550 def _Symlink(link, target):
551 """Symlinks link to target, and removes whatever link was there before."""
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200552 _Log('Linking to %s from %s', link, target)
joychen5260b9a2013-07-16 14:48:01 -0700553 if os.path.lexists(link):
554 os.unlink(link)
555 os.symlink(target, link)
556
joychen121fc9b2013-08-02 14:30:30 -0700557 def _GetLatestLocalVersion(self, board):
joychen921e1fb2013-06-28 11:12:20 -0700558 """Get the version of the latest image built for board by build_image
559
560 Updates the symlink reference within the xBuddy static dir to point to
561 the real image dir in the local /build/images directory.
562
563 Args:
joychenc3944cb2013-08-19 10:42:07 -0700564 board: board that image was built for.
joychen921e1fb2013-06-28 11:12:20 -0700565
566 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700567 The discovered version of the image.
joychenc3944cb2013-08-19 10:42:07 -0700568
569 Raises:
570 XBuddyException if neither test nor dev image was found in latest built
571 directory.
joychen3cb228e2013-06-12 12:13:13 -0700572 """
Alex Klein77df9352018-10-30 12:01:06 -0600573 latest_local_dir = self.GetLatestImageLink(board)
joychenb0dfe552013-07-30 10:02:06 -0700574 if not latest_local_dir or not os.path.exists(latest_local_dir):
joychen921e1fb2013-06-28 11:12:20 -0700575 raise XBuddyException('No builds found for %s. Did you run build_image?' %
576 board)
577
joychen121fc9b2013-08-02 14:30:30 -0700578 # Assume that the version number is the name of the directory.
Alex Klein77df9352018-10-30 12:01:06 -0600579 return os.path.basename(os.path.realpath(latest_local_dir))
joychen921e1fb2013-06-28 11:12:20 -0700580
joychenc3944cb2013-08-19 10:42:07 -0700581 @staticmethod
582 def _FindAny(local_dir):
583 """Returns the image_type for ANY given the local_dir."""
joychenc3944cb2013-08-19 10:42:07 -0700584 test_image = os.path.join(local_dir, devserver_constants.TEST_IMAGE_FILE)
Yu-Ju Hongc23c79b2014-03-17 12:40:33 -0700585 dev_image = os.path.join(local_dir, devserver_constants.IMAGE_FILE)
586 # Prioritize test images over dev images.
joychenc3944cb2013-08-19 10:42:07 -0700587 if os.path.exists(test_image):
588 return 'test'
589
Yu-Ju Hongc23c79b2014-03-17 12:40:33 -0700590 if os.path.exists(dev_image):
591 return 'dev'
592
joychenc3944cb2013-08-19 10:42:07 -0700593 raise XBuddyException('No images found in %s' % local_dir)
594
595 @staticmethod
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800596 def _InterpretPath(path, default_board=None, default_version=None):
joychen121fc9b2013-08-02 14:30:30 -0700597 """Split and return the pieces of an xBuddy path name
joychen921e1fb2013-06-28 11:12:20 -0700598
joychen121fc9b2013-08-02 14:30:30 -0700599 Args:
600 path: the path xBuddy Get was called with.
Chris Sosa0eecf962014-02-03 14:14:39 -0800601 default_board: board to use in case board isn't in path.
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800602 default_version: Version to use in case version isn't in path.
joychen3cb228e2013-06-12 12:13:13 -0700603
Yu-Ju Hongc54658c2014-01-22 09:18:07 -0800604 Returns:
Chris Sosa75490802013-09-30 17:21:45 -0700605 tuple of (image_type, board, version, whether the path is local)
joychen3cb228e2013-06-12 12:13:13 -0700606
607 Raises:
608 XBuddyException: if the path can't be resolved into valid components
609 """
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200610 path_list = [p for p in path.split('/') if p]
joychen7df67f72013-07-18 14:21:12 -0700611
Chris Sosa0eecf962014-02-03 14:14:39 -0800612 # Do the stuff that is well known first. We know that if paths have a
613 # image_type, it must be one of the GS/LOCAL aliases and it must be at the
614 # end. Similarly, local/remote are well-known and must start the path list.
615 is_local = True
616 if path_list and path_list[0] in (REMOTE, LOCAL):
617 is_local = (path_list.pop(0) == LOCAL)
joychen7df67f72013-07-18 14:21:12 -0700618
Chris Sosa0eecf962014-02-03 14:14:39 -0800619 # Default image type is determined by remote vs. local.
620 if is_local:
621 image_type = ANY
622 else:
623 image_type = TEST
joychen7df67f72013-07-18 14:21:12 -0700624
Chris Sosa0eecf962014-02-03 14:14:39 -0800625 if path_list and path_list[-1] in GS_ALIASES + LOCAL_ALIASES:
626 image_type = path_list.pop(-1)
joychen3cb228e2013-06-12 12:13:13 -0700627
Chris Sosa0eecf962014-02-03 14:14:39 -0800628 # Now for the tricky part. We don't actually know at this point if the rest
629 # of the path is just a board | version (like R33-2341.0.0) or just a board
630 # or just a version. So we do our best to do the right thing.
631 board = default_board
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800632 version = default_version or LATEST
Chris Sosa0eecf962014-02-03 14:14:39 -0800633 if len(path_list) == 1:
634 path = path_list.pop(0)
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800635 # Treat this as a version if it's one we know (contains default or
636 # latest), or we were given an actual default board.
637 if default_version in path or LATEST in path or default_board is not None:
Chris Sosa0eecf962014-02-03 14:14:39 -0800638 version = path
joychen7df67f72013-07-18 14:21:12 -0700639 else:
Chris Sosa0eecf962014-02-03 14:14:39 -0800640 board = path
joychen7df67f72013-07-18 14:21:12 -0700641
Chris Sosa0eecf962014-02-03 14:14:39 -0800642 elif len(path_list) == 2:
643 # Assumes board/version.
644 board = path_list.pop(0)
645 version = path_list.pop(0)
646
647 if path_list:
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200648 raise XBuddyException('Path is not valid. Could not figure out how to '
649 'parse remaining components: %s.' % path_list)
Chris Sosa0eecf962014-02-03 14:14:39 -0800650
651 _Log("Get artifact '%s' with board %s and version %s'. Locally? %s",
joychen7df67f72013-07-18 14:21:12 -0700652 image_type, board, version, is_local)
653
654 return image_type, board, version, is_local
joychen3cb228e2013-06-12 12:13:13 -0700655
joychen921e1fb2013-06-28 11:12:20 -0700656 def _SyncRegistryWithBuildImages(self):
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800657 """Crawl images_dir for build_ids of images generated from build_image.
joychen5260b9a2013-07-16 14:48:01 -0700658
659 This will find images and symlink them in xBuddy's static dir so that
660 xBuddy's cache can serve them.
661 If xBuddy's _manage_builds option is on, then a timestamp will also be
662 generated, and xBuddy will clear them from the directory they are in, as
663 necessary.
664 """
Yu-Ju Hong235d1b52014-04-16 11:01:47 -0700665 if not os.path.isdir(self.images_dir):
666 # Skip syncing if images_dir does not exist.
667 _Log('Cannot find %s; skip syncing image registry.', self.images_dir)
668 return
669
joychen921e1fb2013-06-28 11:12:20 -0700670 build_ids = []
671 for b in os.listdir(self.images_dir):
Mike Frysingera2c24252017-11-28 19:14:45 -0500672 # Ignore random files in the build dir.
673 board_dir = os.path.join(self.images_dir, b)
674 if not os.path.isdir(board_dir):
675 continue
676
joychen5260b9a2013-07-16 14:48:01 -0700677 # Ensure we have directories to track all boards in build/images
678 common_util.MkDirP(os.path.join(self.static_dir, b))
joychen921e1fb2013-06-28 11:12:20 -0700679 build_ids.extend(['/'.join([b, v]) for v
joychenc3944cb2013-08-19 10:42:07 -0700680 in os.listdir(board_dir) if not v == LATEST])
joychen921e1fb2013-06-28 11:12:20 -0700681
joychen121fc9b2013-08-02 14:30:30 -0700682 # Symlink undiscovered images, and update timestamps if manage_builds is on.
joychen5260b9a2013-07-16 14:48:01 -0700683 for build_id in build_ids:
684 link = os.path.join(self.static_dir, build_id)
685 target = os.path.join(self.images_dir, build_id)
686 XBuddy._Symlink(link, target)
687 if self._manage_builds:
688 Timestamp.UpdateTimestamp(self._timestamp_folder, build_id)
joychen921e1fb2013-06-28 11:12:20 -0700689
690 def _ListBuildTimes(self):
Gilad Arnold5f46d8e2015-02-19 12:17:55 -0800691 """Returns the currently cached builds and their last access timestamp.
joychen3cb228e2013-06-12 12:13:13 -0700692
693 Returns:
694 list of tuples that matches xBuddy build/version to timestamps in long
695 """
joychen121fc9b2013-08-02 14:30:30 -0700696 # Update currently cached builds.
joychen3cb228e2013-06-12 12:13:13 -0700697 build_dict = {}
698
joychen7df67f72013-07-18 14:21:12 -0700699 for f in os.listdir(self._timestamp_folder):
joychen3cb228e2013-06-12 12:13:13 -0700700 last_accessed = os.path.getmtime(os.path.join(self._timestamp_folder, f))
701 build_id = Timestamp.TimestampToBuild(f)
joychenc3944cb2013-08-19 10:42:07 -0700702 stale_time = datetime.timedelta(seconds=(time.time() - last_accessed))
joychen921e1fb2013-06-28 11:12:20 -0700703 build_dict[build_id] = stale_time
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200704 return_tup = sorted(build_dict.items(), key=operator.itemgetter(1))
joychen3cb228e2013-06-12 12:13:13 -0700705 return return_tup
706
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700707 def _Download(self, gs_url, artifacts, build_id):
Chris Sosa75490802013-09-30 17:21:45 -0700708 """Download the artifacts from the given gs_url.
709
710 Raises:
711 build_artifact.ArtifactDownloadError: If we failed to download the
712 artifact.
713 """
joychen3cb228e2013-06-12 12:13:13 -0700714 with XBuddy._staging_thread_count_lock:
715 XBuddy._staging_thread_count += 1
716 try:
Achuith Bhandarkar46b0c6e2019-09-19 15:46:16 +0200717 _Log('Downloading %s from %s', artifacts, gs_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700718 dl = downloader.GoogleStorageDownloader(self.static_dir, gs_url, build_id)
Gabe Black3b567202015-09-23 14:07:59 -0700719 factory = build_artifact.ChromeOSArtifactFactory(
720 dl.GetBuildDir(), artifacts, [], dl.GetBuild())
721 dl.Download(factory)
joychen3cb228e2013-06-12 12:13:13 -0700722 finally:
723 with XBuddy._staging_thread_count_lock:
724 XBuddy._staging_thread_count -= 1
725
Chris Sosa75490802013-09-30 17:21:45 -0700726 def CleanCache(self):
joychen562699a2013-08-13 15:22:14 -0700727 """Delete all builds besides the newest N builds"""
joychen121fc9b2013-08-02 14:30:30 -0700728 if not self._manage_builds:
729 return
joychen921e1fb2013-06-28 11:12:20 -0700730 cached_builds = [e[0] for e in self._ListBuildTimes()]
joychen3cb228e2013-06-12 12:13:13 -0700731 _Log('In cache now: %s', cached_builds)
732
joychen562699a2013-08-13 15:22:14 -0700733 for b in range(self._Capacity(), len(cached_builds)):
joychen3cb228e2013-06-12 12:13:13 -0700734 b_path = cached_builds[b]
joychen7df67f72013-07-18 14:21:12 -0700735 _Log("Clearing '%s' from cache", b_path)
joychen3cb228e2013-06-12 12:13:13 -0700736
737 time_file = os.path.join(self._timestamp_folder,
738 Timestamp.BuildToTimestamp(b_path))
joychen921e1fb2013-06-28 11:12:20 -0700739 os.unlink(time_file)
740 clear_dir = os.path.join(self.static_dir, b_path)
joychen3cb228e2013-06-12 12:13:13 -0700741 try:
joychen121fc9b2013-08-02 14:30:30 -0700742 # Handle symlinks, in the case of links to local builds if enabled.
743 if os.path.islink(clear_dir):
joychen5260b9a2013-07-16 14:48:01 -0700744 target = os.readlink(clear_dir)
745 _Log('Deleting locally built image at %s', target)
joychen921e1fb2013-06-28 11:12:20 -0700746
747 os.unlink(clear_dir)
joychen5260b9a2013-07-16 14:48:01 -0700748 if os.path.exists(target):
joychen921e1fb2013-06-28 11:12:20 -0700749 shutil.rmtree(target)
750 elif os.path.exists(clear_dir):
joychen5260b9a2013-07-16 14:48:01 -0700751 _Log('Deleting downloaded image at %s', clear_dir)
joychen3cb228e2013-06-12 12:13:13 -0700752 shutil.rmtree(clear_dir)
joychen921e1fb2013-06-28 11:12:20 -0700753
joychen121fc9b2013-08-02 14:30:30 -0700754 except Exception as err:
755 raise XBuddyException('Failed to clear %s: %s' % (clear_dir, err))
joychen3cb228e2013-06-12 12:13:13 -0700756
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700757 def _TranslateSignedGSUrl(self, build_id, channel=None):
758 """Translate the GS URL to be able to find signed images.
759
760 Args:
761 build_id: Path to the image or update directory on the devserver or
762 in Google Storage. e.g. 'x86-generic/R26-4000.0.0'
763 channel: The channel for the image. If none, it tries to guess it in
764 order of stability.
765
766 Returns:
767 The GS URL for the directory where the signed image can be found.
768
769 Raises:
770 build_artifact.ArtifactDownloadError: If we failed to download the
771 artifact.
772 """
773 match = re.match(r'^([^/]+?)(?:-release)?/R\d+-(.*)$', build_id)
774
775 channels = []
776 if channel:
777 channels.append(channel)
778 else:
779 # Attempt to enumerate all channels, in order of stability.
780 channels.extend(devserver_constants.CHANNELS[::-1])
781
782 for channel in channels:
783 image_dir = devserver_constants.GS_CHANNEL_DIR % {
784 'channel': channel,
785 'board': match.group(1),
786 }
787 gs_url = os.path.join(image_dir, match.group(2))
788 try:
789 self._LS(gs_url)
790 return gs_url
791 except gs.GSNoSuchKey:
792 continue
793 raise build_artifact.ArtifactDownloadError(
794 'Could not find signed image URL for %s in Google Storage' %
795 build_id)
796
797 def _GetFromGS(self, build_id, image_type, image_dir=None, channel=None):
Chris Sosa75490802013-09-30 17:21:45 -0700798 """Check if the artifact is available locally. Download from GS if not.
799
Simran Basi99e63c02014-05-20 10:39:52 -0700800 Args:
801 build_id: Path to the image or update directory on the devserver or
802 in Google Storage. e.g. 'x86-generic/R26-4000.0.0'
803 image_type: Image type to download. Look at aliases at top of file for
804 options.
805 image_dir: Google Storage image archive to search in if requesting a
806 remote artifact. If none uses the default bucket.
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700807 channel: The channel for the image. If none, it tries to guess it in
808 order of stability.
Simran Basi99e63c02014-05-20 10:39:52 -0700809
Chris Sosa75490802013-09-30 17:21:45 -0700810 Raises:
Alex Klein77df9352018-10-30 12:01:06 -0600811 build_artifact.ArtifactDownloadError: If we failed to download the
812 artifact.
Chris Sosa75490802013-09-30 17:21:45 -0700813 """
joychen121fc9b2013-08-02 14:30:30 -0700814 # Stage image if not found in cache.
joychen921e1fb2013-06-28 11:12:20 -0700815 file_name = GS_ALIAS_TO_FILENAME[image_type]
joychen346531c2013-07-24 16:55:56 -0700816 file_loc = os.path.join(self.static_dir, build_id, file_name)
817 cached = os.path.exists(file_loc)
818
joychen921e1fb2013-06-28 11:12:20 -0700819 if not cached:
Chris Sosa75490802013-09-30 17:21:45 -0700820 artifact = GS_ALIAS_TO_ARTIFACT[image_type]
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700821 if image_type == SIGNED:
822 gs_url = self._TranslateSignedGSUrl(build_id, channel=channel)
823 else:
824 image_dir = XBuddy._ResolveImageDir(image_dir)
825 gs_url = os.path.join(image_dir, build_id)
826 self._Download(gs_url, [artifact], build_id)
joychen921e1fb2013-06-28 11:12:20 -0700827 else:
828 _Log('Image already cached.')
829
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800830 def _GetArtifact(self, path_list, board=None, version=None,
831 lookup_only=False, image_dir=None):
joychen346531c2013-07-24 16:55:56 -0700832 """Interpret an xBuddy path and return directory/file_name to resource.
833
Chris Sosa75490802013-09-30 17:21:45 -0700834 Note board can be passed that in but by default if self._board is set,
835 that is used rather than board.
836
Simran Basi99e63c02014-05-20 10:39:52 -0700837 Args:
838 path_list: [board, version, alias] as split from the xbuddy call url.
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800839 board: Board whos artifacts we are looking for. Only used if no board was
840 given during XBuddy initialization.
841 version: Version whose artifacts we are looking for. Used if no version
842 was given during XBuddy initialization. If None, defers to LATEST.
Simran Basi99e63c02014-05-20 10:39:52 -0700843 lookup_only: If true just look up the artifact, if False stage it on
844 the devserver as well.
845 image_dir: Google Storage image archive to search in if requesting a
846 remote artifact. If none uses the default bucket.
847
joychen346531c2013-07-24 16:55:56 -0700848 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -0700849 build_id: Path to the image or update directory on the devserver or
850 in Google Storage. e.g. 'x86-generic/R26-4000.0.0'
851 file_name: of the artifact in the build_id directory.
joychen346531c2013-07-24 16:55:56 -0700852
853 Raises:
joychen121fc9b2013-08-02 14:30:30 -0700854 XBuddyException: if the path could not be translated
Chris Sosa75490802013-09-30 17:21:45 -0700855 build_artifact.ArtifactDownloadError: if we failed to download the
856 artifact.
joychen346531c2013-07-24 16:55:56 -0700857 """
joychen121fc9b2013-08-02 14:30:30 -0700858 path = '/'.join(path_list)
Chris Sosa0eecf962014-02-03 14:14:39 -0800859 default_board = self._board if self._board else board
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800860 default_version = self._version or version or LATEST
joychenb0dfe552013-07-30 10:02:06 -0700861 # Rewrite the path if there is an appropriate default.
Gilad Arnold38e828c2015-04-24 13:52:07 -0700862 path, suffix = self.LookupAlias(path, board=default_board,
863 version=default_version)
joychen121fc9b2013-08-02 14:30:30 -0700864 # Parse the path.
Chris Sosa0eecf962014-02-03 14:14:39 -0800865 image_type, board, version, is_local = self._InterpretPath(
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800866 path, default_board, default_version)
joychen7df67f72013-07-18 14:21:12 -0700867 if is_local:
joychen121fc9b2013-08-02 14:30:30 -0700868 # Get a local image.
joychen7df67f72013-07-18 14:21:12 -0700869 if version == LATEST:
joychen121fc9b2013-08-02 14:30:30 -0700870 # Get the latest local image for the given board.
871 version = self._GetLatestLocalVersion(board)
joychen7df67f72013-07-18 14:21:12 -0700872
joychenc3944cb2013-08-19 10:42:07 -0700873 build_id = os.path.join(board, version)
874 artifact_dir = os.path.join(self.static_dir, build_id)
875 if image_type == ANY:
876 image_type = self._FindAny(artifact_dir)
joychen121fc9b2013-08-02 14:30:30 -0700877
joychenc3944cb2013-08-19 10:42:07 -0700878 file_name = LOCAL_ALIAS_TO_FILENAME[image_type]
879 artifact_path = os.path.join(artifact_dir, file_name)
880 if not os.path.exists(artifact_path):
881 raise XBuddyException('Local %s artifact not in static_dir at %s' %
882 (image_type, artifact_path))
joychen121fc9b2013-08-02 14:30:30 -0700883
joychen921e1fb2013-06-28 11:12:20 -0700884 else:
joychen121fc9b2013-08-02 14:30:30 -0700885 # Get a remote image.
joychen921e1fb2013-06-28 11:12:20 -0700886 if image_type not in GS_ALIASES:
joychen7df67f72013-07-18 14:21:12 -0700887 raise XBuddyException('Bad remote image type: %s. Use one of: %s' %
joychen921e1fb2013-06-28 11:12:20 -0700888 (image_type, GS_ALIASES))
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700889 build_id, channel = self._ResolveVersionToBuildIdAndChannel(
890 board, suffix, version, image_dir=image_dir)
Chris Sosa75490802013-09-30 17:21:45 -0700891 _Log('Resolved version %s to %s.', version, build_id)
892 file_name = GS_ALIAS_TO_FILENAME[image_type]
893 if not lookup_only:
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700894 self._GetFromGS(build_id, image_type, image_dir=image_dir,
895 channel=channel)
joychenf8f07e22013-07-12 17:45:51 -0700896
joychenc3944cb2013-08-19 10:42:07 -0700897 return build_id, file_name
joychen3cb228e2013-06-12 12:13:13 -0700898
899 ############################ BEGIN PUBLIC METHODS
900
901 def List(self):
902 """Lists the currently available images & time since last access."""
joychen921e1fb2013-06-28 11:12:20 -0700903 self._SyncRegistryWithBuildImages()
904 builds = self._ListBuildTimes()
905 return_string = ''
906 for build, timestamp in builds:
907 return_string += '<b>' + build + '</b> '
908 return_string += '(time since last access: ' + str(timestamp) + ')<br>'
909 return return_string
joychen3cb228e2013-06-12 12:13:13 -0700910
911 def Capacity(self):
912 """Returns the number of images cached by xBuddy."""
joychen562699a2013-08-13 15:22:14 -0700913 return str(self._Capacity())
joychen3cb228e2013-06-12 12:13:13 -0700914
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800915 def Translate(self, path_list, board=None, version=None, image_dir=None):
joychen346531c2013-07-24 16:55:56 -0700916 """Translates an xBuddy path to a real path to artifact if it exists.
917
joychen121fc9b2013-08-02 14:30:30 -0700918 Equivalent to the Get call, minus downloading and updating timestamps,
joychen346531c2013-07-24 16:55:56 -0700919
Simran Basi99e63c02014-05-20 10:39:52 -0700920 Args:
921 path_list: [board, version, alias] as split from the xbuddy call url.
922 board: Board whos artifacts we are looking for. If None, use the board
923 XBuddy was initialized to use.
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800924 version: Version whose artifacts we are looking for. If None, use the
925 version XBuddy was initialized with, or LATEST.
Simran Basi99e63c02014-05-20 10:39:52 -0700926 image_dir: image directory to check in Google Storage. If none,
927 the default bucket is used.
928
joychen7c2054a2013-07-25 11:14:07 -0700929 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700930 build_id: Path to the image or update directory on the devserver.
931 e.g. 'x86-generic/R26-4000.0.0'
932 The returned path is always the path to the directory within
933 static_dir, so it is always the build_id of the image.
934 file_name: The file name of the artifact. Can take any of the file
935 values in devserver_constants.
936 e.g. 'chromiumos_test_image.bin' or 'update.gz' if the path list
937 specified 'test' or 'full_payload' artifacts, respectively.
joychen7c2054a2013-07-25 11:14:07 -0700938
joychen121fc9b2013-08-02 14:30:30 -0700939 Raises:
940 XBuddyException: if the path couldn't be translated
joychen346531c2013-07-24 16:55:56 -0700941 """
942 self._SyncRegistryWithBuildImages()
Chris Sosa75490802013-09-30 17:21:45 -0700943 build_id, file_name = self._GetArtifact(path_list, board=board,
Gilad Arnoldd04fcab2015-02-19 12:00:45 -0800944 version=version,
Simran Basi99e63c02014-05-20 10:39:52 -0700945 lookup_only=True,
946 image_dir=image_dir)
joychen346531c2013-07-24 16:55:56 -0700947
joychen121fc9b2013-08-02 14:30:30 -0700948 _Log('Returning path to payload: %s/%s', build_id, file_name)
949 return build_id, file_name
joychen346531c2013-07-24 16:55:56 -0700950
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700951 def StageTestArtifactsForUpdate(self, path_list):
Chris Sosa75490802013-09-30 17:21:45 -0700952 """Stages test artifacts for update and returns build_id.
953
954 Raises:
955 XBuddyException: if the path could not be translated
956 build_artifact.ArtifactDownloadError: if we failed to download the test
957 artifacts.
958 """
959 build_id, file_name = self.Translate(path_list)
960 if file_name == devserver_constants.TEST_IMAGE_FILE:
961 gs_url = os.path.join(devserver_constants.GS_IMAGE_DIR,
962 build_id)
963 artifacts = [FULL, STATEFUL]
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700964 self._Download(gs_url, artifacts, build_id)
Chris Sosa75490802013-09-30 17:21:45 -0700965 return build_id
966
Simran Basi99e63c02014-05-20 10:39:52 -0700967 def Get(self, path_list, image_dir=None):
joychen921e1fb2013-06-28 11:12:20 -0700968 """The full xBuddy call, returns resource specified by path_list.
joychen3cb228e2013-06-12 12:13:13 -0700969
970 Please see devserver.py:xbuddy for full documentation.
joychen121fc9b2013-08-02 14:30:30 -0700971
joychen3cb228e2013-06-12 12:13:13 -0700972 Args:
Simran Basi99e63c02014-05-20 10:39:52 -0700973 path_list: [board, version, alias] as split from the xbuddy call url.
974 image_dir: image directory to check in Google Storage. If none,
975 the default bucket is used.
joychen3cb228e2013-06-12 12:13:13 -0700976
977 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700978 build_id: Path to the image or update directory on the devserver.
Simran Basi99e63c02014-05-20 10:39:52 -0700979 e.g. 'x86-generic/R26-4000.0.0'
980 The returned path is always the path to the directory within
981 static_dir, so it is always the build_id of the image.
joychen121fc9b2013-08-02 14:30:30 -0700982 file_name: The file name of the artifact. Can take any of the file
Simran Basi99e63c02014-05-20 10:39:52 -0700983 values in devserver_constants.
984 e.g. 'chromiumos_test_image.bin' or 'update.gz' if the path list
985 specified 'test' or 'full_payload' artifacts, respectively.
joychen3cb228e2013-06-12 12:13:13 -0700986
987 Raises:
Chris Sosa75490802013-09-30 17:21:45 -0700988 XBuddyException: if the path could not be translated
989 build_artifact.ArtifactDownloadError: if we failed to download the
990 artifact.
joychen3cb228e2013-06-12 12:13:13 -0700991 """
joychen7df67f72013-07-18 14:21:12 -0700992 self._SyncRegistryWithBuildImages()
Simran Basi99e63c02014-05-20 10:39:52 -0700993 build_id, file_name = self._GetArtifact(path_list, image_dir=image_dir)
joychen921e1fb2013-06-28 11:12:20 -0700994 Timestamp.UpdateTimestamp(self._timestamp_folder, build_id)
joychen3cb228e2013-06-12 12:13:13 -0700995 #TODO (joyc): run in sep thread
Chris Sosa75490802013-09-30 17:21:45 -0700996 self.CleanCache()
joychen3cb228e2013-06-12 12:13:13 -0700997
joychen121fc9b2013-08-02 14:30:30 -0700998 _Log('Returning path to payload: %s/%s', build_id, file_name)
999 return build_id, file_name