blob: 642a74e189830137953f4b8ea2a51639d2b8bd12 [file] [log] [blame]
Sanika Kulkarni80e5bd72020-07-21 18:34:34 -07001# -*- 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 Kulkarni80e5bd72020-07-21 18:34:34 -07008from __future__ import print_function
9
Congbin Guo36914412020-07-31 14:13:53 -070010import contextlib
Sanika Kulkarni80e5bd72020-07-21 18:34:34 -070011import errno
Congbin Guo36914412020-07-31 14:13:53 -070012import fcntl
Sanika Kulkarni80e5bd72020-07-21 18:34:34 -070013import os
14import shutil
15import subprocess
16import tempfile
Sanika Kulkarni80e5bd72020-07-21 18:34:34 -070017
18import requests
19
20import cherrypy # pylint: disable=import-error
21
Congbin Guo36914412020-07-31 14:13:53 -070022import constants
23
Sanika Kulkarni80e5bd72020-07-21 18:34:34 -070024from 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.
31GS_CACHE_HOSTNAME = '127.0.0.1'
32GS_CACHE_PORT = '8888'
33GS_CACHE_EXRTACT_RPC = 'extract'
34GS_CACHE_BASE_URL = ('http://%s:%s/%s' %
35 (GS_CACHE_HOSTNAME, GS_CACHE_PORT, GS_CACHE_EXRTACT_RPC))
Congbin Guodb597f32020-08-27 14:12:50 -070036_TIMESTAMP_FILE = 'staged.timestamp'
Sanika Kulkarni80e5bd72020-07-21 18:34:34 -070037
38
39def _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 Guodb597f32020-08-27 14:12:50 -070045def _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 Kulkarni80e5bd72020-07-21 18:34:34 -070053def _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 Guo36914412020-07-31 14:13:53 -070070@contextlib.contextmanager
71def 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 Kulkarni80e5bd72020-07-21 18:34:34 -070086class TelemetrySetupError(Exception):
87 """Exception class used by this module."""
Sanika Kulkarni80e5bd72020-07-21 18:34:34 -070088
89
Sanika Kulkarni80e5bd72020-07-21 18:34:34 -070090class 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 Kulkarni80e5bd72020-07-21 18:34:34 -0700122
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 Guo36914412020-07-31 14:13:53 -0700148 self._MkDirP(self._tlm_src_dir_path)
Congbin Guodb597f32020-08-27 14:12:50 -0700149 _touch_timestamp(self._build_dir)
Congbin Guo36914412020-07-31 14:13:53 -0700150 with lock_dir(self._tlm_src_dir_path):
Sanika Kulkarni80e5bd72020-07-21 18:34:34 -0700151 if not os.path.exists(src_folder):
Sanika Kulkarni80e5bd72020-07-21 18:34:34 -0700152
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 Kulkarni78834022021-02-25 15:18:25 -0800194 with open(dep_path, 'wb') as f:
Congbin Guo36914412020-07-31 14:13:53 -0700195 for content in resp.iter_content(constants.READ_BUFFER_SIZE_BYTES):
Sanika Kulkarni80e5bd72020-07-21 18:34:34 -0700196 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)))