Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | # Copyright 2020 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 | |
| 6 | """A class that sets up the environment for telemetry testing.""" |
| 7 | |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 8 | from __future__ import print_function |
| 9 | |
Congbin Guo | 3691441 | 2020-07-31 14:13:53 -0700 | [diff] [blame] | 10 | import contextlib |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 11 | import errno |
Congbin Guo | 3691441 | 2020-07-31 14:13:53 -0700 | [diff] [blame] | 12 | import fcntl |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 13 | import os |
| 14 | import shutil |
| 15 | import subprocess |
| 16 | import tempfile |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 17 | |
| 18 | import requests |
| 19 | |
| 20 | import cherrypy # pylint: disable=import-error |
| 21 | |
Congbin Guo | 3691441 | 2020-07-31 14:13:53 -0700 | [diff] [blame] | 22 | import constants |
| 23 | |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 24 | from chromite.lib import cros_logging as logging |
| 25 | |
| 26 | |
| 27 | # Define module logger. |
| 28 | _logger = logging.getLogger(__file__) |
| 29 | |
| 30 | # Define all GS Cache related constants. |
| 31 | GS_CACHE_HOSTNAME = '127.0.0.1' |
| 32 | GS_CACHE_PORT = '8888' |
| 33 | GS_CACHE_EXRTACT_RPC = 'extract' |
| 34 | GS_CACHE_BASE_URL = ('http://%s:%s/%s' % |
| 35 | (GS_CACHE_HOSTNAME, GS_CACHE_PORT, GS_CACHE_EXRTACT_RPC)) |
Congbin Guo | db597f3 | 2020-08-27 14:12:50 -0700 | [diff] [blame] | 36 | _TIMESTAMP_FILE = 'staged.timestamp' |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 37 | |
| 38 | |
| 39 | def _log(*args, **kwargs): |
| 40 | """A wrapper function of logging.debug/info, etc.""" |
| 41 | level = kwargs.pop('level', logging.DEBUG) |
| 42 | _logger.log(level, extra=cherrypy.request.headers, *args, **kwargs) |
| 43 | |
| 44 | |
Congbin Guo | db597f3 | 2020-08-27 14:12:50 -0700 | [diff] [blame] | 45 | def _touch_timestamp(dir_name): |
| 46 | """Timestamp the directory to allow other jobs to clean it.""" |
| 47 | file_name = os.path.join(dir_name, _TIMESTAMP_FILE) |
| 48 | # Easiest python version of |touch file_name|. |
| 49 | with open(file_name, 'a'): |
| 50 | os.utime(file_name, None) |
| 51 | |
| 52 | |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 53 | def _GetBucketAndBuild(archive_url): |
| 54 | """Gets the build name from the archive_url. |
| 55 | |
| 56 | Args: |
| 57 | archive_url: The archive_url is typically in the format |
| 58 | gs://<gs_bucket>/<build_name>. Deduce the bucket and build name from |
| 59 | this URL by splitting at the appropriate '/'. |
| 60 | |
| 61 | Returns: |
| 62 | Name of the GS bucket as a string. |
| 63 | Name of the build as a string. |
| 64 | """ |
| 65 | clean_url = archive_url.strip('gs://') |
| 66 | parts = clean_url.split('/') |
| 67 | return parts[0], '/'.join(parts[1:]) |
| 68 | |
| 69 | |
Congbin Guo | 3691441 | 2020-07-31 14:13:53 -0700 | [diff] [blame] | 70 | @contextlib.contextmanager |
| 71 | def lock_dir(dir_name): |
| 72 | """Lock a directory exclusively by placing a file lock in it. |
| 73 | |
| 74 | Args: |
| 75 | dir_name: the directory name to be locked. |
| 76 | """ |
| 77 | lock_file = os.path.join(dir_name, '.lock') |
| 78 | with open(lock_file, 'w+') as f: |
| 79 | fcntl.flock(f, fcntl.LOCK_EX) |
| 80 | try: |
| 81 | yield |
| 82 | finally: |
| 83 | fcntl.flock(f, fcntl.LOCK_UN) |
| 84 | |
| 85 | |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 86 | class TelemetrySetupError(Exception): |
| 87 | """Exception class used by this module.""" |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 88 | |
| 89 | |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 90 | class TelemetrySetup(object): |
| 91 | """Class that sets up the environment for telemetry testing.""" |
| 92 | |
| 93 | # Relevant directory paths. |
| 94 | _BASE_DIR_PATH = '/home/chromeos-test/images' |
| 95 | _PARTIAL_DEPENDENCY_DIR_PATH = 'autotest/packages' |
| 96 | |
| 97 | # Relevant directory names. |
| 98 | _TELEMETRY_SRC_DIR_NAME = 'telemetry_src' |
| 99 | _TEST_SRC_DIR_NAME = 'test_src' |
| 100 | _SRC_DIR_NAME = 'src' |
| 101 | |
| 102 | # Names of the telemetry dependency tarballs. |
| 103 | _DEPENDENCIES = [ |
| 104 | 'dep-telemetry_dep.tar.bz2', |
| 105 | 'dep-page_cycler_dep.tar.bz2', |
| 106 | 'dep-chrome_test.tar.bz2', |
| 107 | 'dep-perf_data_dep.tar.bz2', |
| 108 | ] |
| 109 | |
| 110 | def __init__(self, archive_url): |
| 111 | """Initializes the TelemetrySetup class. |
| 112 | |
| 113 | Args: |
| 114 | archive_url: The URL of the archive supplied through the /setup_telemetry |
| 115 | request. It is typically in the format gs://<gs_bucket>/<build_name> |
| 116 | """ |
| 117 | self._bucket, self._build = _GetBucketAndBuild(archive_url) |
| 118 | self._build_dir = os.path.join(self._BASE_DIR_PATH, self._build) |
| 119 | self._temp_dir_path = tempfile.mkdtemp(prefix='gsc-telemetry') |
| 120 | self._tlm_src_dir_path = os.path.join(self._build_dir, |
| 121 | self._TELEMETRY_SRC_DIR_NAME) |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 122 | |
| 123 | def __enter__(self): |
| 124 | """Called while entering context manager; does nothing.""" |
| 125 | return self |
| 126 | |
| 127 | def __exit__(self, exc_type, exc_value, traceback): |
| 128 | """Called while exiting context manager; cleans up temp dirs.""" |
| 129 | try: |
| 130 | shutil.rmtree(self._temp_dir_path) |
| 131 | except Exception as e: |
| 132 | _log('Something went wrong. Could not delete %s due to exception: %s', |
| 133 | self._temp_dir_path, e, level=logging.WARNING) |
| 134 | |
| 135 | def Setup(self): |
| 136 | """Sets up the environment for telemetry testing. |
| 137 | |
| 138 | This method downloads the telemetry dependency tarballs and extracts them |
| 139 | into a 'src' directory. |
| 140 | |
| 141 | Returns: |
| 142 | Path to the src directry where the telemetry dependencies have been |
| 143 | downloaded and extracted. |
| 144 | """ |
| 145 | src_folder = os.path.join(self._tlm_src_dir_path, self._SRC_DIR_NAME) |
| 146 | test_src = os.path.join(self._tlm_src_dir_path, self._TEST_SRC_DIR_NAME) |
| 147 | |
Congbin Guo | 3691441 | 2020-07-31 14:13:53 -0700 | [diff] [blame] | 148 | self._MkDirP(self._tlm_src_dir_path) |
Congbin Guo | db597f3 | 2020-08-27 14:12:50 -0700 | [diff] [blame] | 149 | _touch_timestamp(self._build_dir) |
Congbin Guo | 3691441 | 2020-07-31 14:13:53 -0700 | [diff] [blame] | 150 | with lock_dir(self._tlm_src_dir_path): |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 151 | if not os.path.exists(src_folder): |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 152 | |
| 153 | # Download the required dependency tarballs. |
| 154 | for dep in self._DEPENDENCIES: |
| 155 | dep_path = self._DownloadFilesFromTar(dep, self._temp_dir_path) |
| 156 | if os.path.exists(dep_path): |
| 157 | self._ExtractTarball(dep_path, self._tlm_src_dir_path) |
| 158 | |
| 159 | # By default all the tarballs extract to test_src but some parts of |
| 160 | # the telemetry code specifically hardcoded to exist inside of 'src'. |
| 161 | try: |
| 162 | shutil.move(test_src, src_folder) |
| 163 | except shutil.Error: |
| 164 | raise TelemetrySetupError( |
| 165 | 'Failure in telemetry setup for build %s. Appears that the ' |
| 166 | 'test_src to src move failed.' % self._build) |
| 167 | |
| 168 | return src_folder |
| 169 | |
| 170 | def _DownloadFilesFromTar(self, filename, dest_path): |
| 171 | """Downloads the given tar.bz2 file. |
| 172 | |
| 173 | The given tar.bz2 file is downloaded by calling the 'extract' RPC of |
| 174 | gs_archive_server. |
| 175 | |
| 176 | Args: |
| 177 | filename: Name of the tar.bz2 file to be downloaded. |
| 178 | dest_path: Full path to the directory where it should be downloaded. |
| 179 | |
| 180 | Returns: |
| 181 | Full path to the downloaded file. |
| 182 | |
| 183 | Raises: |
| 184 | TelemetrySetupError when the download cannot be completed for any reason. |
| 185 | """ |
| 186 | dep_path = os.path.join(dest_path, filename) |
| 187 | params = 'file=%s/%s' % (self._PARTIAL_DEPENDENCY_DIR_PATH, filename) |
| 188 | partial_url = ('%s/%s/%s/autotest_packages.tar' % |
| 189 | (GS_CACHE_BASE_URL, self._bucket, self._build)) |
| 190 | url = '%s?%s' % (partial_url, params) |
| 191 | resp = requests.get(url) |
| 192 | try: |
| 193 | resp.raise_for_status() |
Sanika Kulkarni | 7883402 | 2021-02-25 15:18:25 -0800 | [diff] [blame] | 194 | with open(dep_path, 'wb') as f: |
Congbin Guo | 3691441 | 2020-07-31 14:13:53 -0700 | [diff] [blame] | 195 | for content in resp.iter_content(constants.READ_BUFFER_SIZE_BYTES): |
Sanika Kulkarni | 80e5bd7 | 2020-07-21 18:34:34 -0700 | [diff] [blame] | 196 | f.write(content) |
| 197 | except Exception as e: |
| 198 | if (isinstance(e, requests.exceptions.HTTPError) |
| 199 | and resp.status_code == 404): |
| 200 | _log('The request %s returned a 404 Not Found status. This dependency ' |
| 201 | 'could be new and therefore does not exist in this specific ' |
| 202 | 'tarball. Hence, squashing the exception and proceeding.', |
| 203 | url, level=logging.ERROR) |
| 204 | else: |
| 205 | raise TelemetrySetupError('An error occurred while trying to complete ' |
| 206 | 'the extract request %s: %s' % (url, str(e))) |
| 207 | return dep_path |
| 208 | |
| 209 | def _ExtractTarball(self, tarball_path, dest_path): |
| 210 | """Extracts the given tarball into the destination directory. |
| 211 | |
| 212 | Args: |
| 213 | tarball_path: Full path to the tarball to be extracted. |
| 214 | dest_path: Full path to the directory where the tarball should be |
| 215 | extracted. |
| 216 | |
| 217 | Raises: |
| 218 | TelemetrySetupError if the method is unable to extract the tarball for |
| 219 | any reason. |
| 220 | """ |
| 221 | cmd = ['tar', 'xf', tarball_path, '--directory', dest_path] |
| 222 | try: |
| 223 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, |
| 224 | stderr=subprocess.PIPE) |
| 225 | proc.communicate() |
| 226 | except Exception as e: |
| 227 | shutil.rmtree(dest_path) |
| 228 | raise TelemetrySetupError( |
| 229 | 'An exception occurred while trying to untar %s into %s: %s' % |
| 230 | (tarball_path, dest_path, str(e))) |
| 231 | |
| 232 | def _MkDirP(self, path): |
| 233 | """Recursively creates the given directory. |
| 234 | |
| 235 | Args: |
| 236 | path: Full path to the directory that needs to the created. |
| 237 | |
| 238 | Raises: |
| 239 | TelemetrySetupError is the method is unable to create directories for any |
| 240 | reason except OSError EEXIST which indicates that the directory |
| 241 | already exists. |
| 242 | """ |
| 243 | try: |
| 244 | os.makedirs(path) |
| 245 | except Exception as e: |
| 246 | if not isinstance(e, OSError) or e.errno != errno.EEXIST: |
| 247 | raise TelemetrySetupError( |
| 248 | 'Could not create directory %s due to %s.' % (path, str(e))) |