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