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