blob: 289c2d5c1837faef1650b144708a42d2cc84556c [file] [log] [blame]
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -08001# -*- coding: utf-8 -*-
2# Copyright 2021 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"""A class that sets up the environment for telemetry testing."""
6
7from __future__ import absolute_import
8from __future__ import division
9from __future__ import print_function
10
11from autotest_lib.client.common_lib.cros import dev_server
12
13import contextlib
14import errno
15import fcntl
16import logging
17import os
18import shutil
19import subprocess
20import tempfile
21
22import requests
23
24_READ_BUFFER_SIZE_BYTES = 1024 * 1024 # 1 MB
25
26
27@contextlib.contextmanager
28def lock_dir(dir_name):
29 """Lock a directory exclusively by placing a file lock in it.
30
31 Args:
32 dir_name: the directory name to be locked.
33 """
34 lock_file = os.path.join(dir_name, '.lock')
35 with open(lock_file, 'w+') as f:
36 fcntl.flock(f, fcntl.LOCK_EX)
37 try:
38 yield
39 finally:
40 fcntl.flock(f, fcntl.LOCK_UN)
41
42
43class TelemetrySetupError(Exception):
44 """Exception class used by this module."""
45 pass
46
47
48class TelemetrySetup(object):
49 """Class that sets up the environment for telemetry testing."""
50
51 # Relevant directory paths.
52 _BASE_DIR_PATH = '/tmp/telemetry-workdir'
53 _PARTIAL_DEPENDENCY_DIR_PATH = 'autotest/packages'
54
55 # Relevant directory names.
56 _TELEMETRY_SRC_DIR_NAME = 'telemetry_src'
57 _TEST_SRC_DIR_NAME = 'test_src'
58 _SRC_DIR_NAME = 'src'
59
60 # Names of the telemetry dependency tarballs.
61 _DEPENDENCIES = [
62 'dep-telemetry_dep.tar.bz2',
63 'dep-page_cycler_dep.tar.bz2',
64 'dep-chrome_test.tar.bz2',
65 'dep-perf_data_dep.tar.bz2',
66 ]
67
68 # Partial devserver URLs.
69 _STATIC_URL_TEMPLATE = '%s/static/%s/autotest/packages/%s'
70
Sanika Kulkarni211a4ef2021-01-29 13:23:44 -080071 def __init__(self, hostname, build):
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -080072 """Initializes the TelemetrySetup class.
73
74 Args:
Sanika Kulkarni211a4ef2021-01-29 13:23:44 -080075 hostname: The host for which telemetry environment should be setup. This
76 is important for devserver resolution.
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -080077 build: The build for which telemetry environment should be setup. It is
78 typically in the format <board>/<version>.
79 """
80 self._build = build
Sanika Kulkarni211a4ef2021-01-29 13:23:44 -080081 self._ds = dev_server.ImageServer.resolve(self._build,
82 hostname=hostname)
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -080083 self._setup_dir_path = tempfile.mkdtemp(prefix='telemetry-setupdir_')
84 self._tmp_build_dir = os.path.join(self._BASE_DIR_PATH, self._build)
85 self._tlm_src_dir_path = os.path.join(self._tmp_build_dir,
86 self._TELEMETRY_SRC_DIR_NAME)
87
88 def Setup(self):
89 """Sets up the environment for telemetry testing.
90
91 This method downloads the telemetry dependency tarballs and extracts
92 them into a 'src' directory.
93
94 Returns:
95 Path to the src directory where the telemetry dependencies have been
96 downloaded and extracted.
97 """
98 src_folder = os.path.join(self._tlm_src_dir_path, self._SRC_DIR_NAME)
99 test_src = os.path.join(self._tlm_src_dir_path,
100 self._TEST_SRC_DIR_NAME)
101 self._MkDirP(self._tlm_src_dir_path)
102 with lock_dir(self._tlm_src_dir_path):
103 if not os.path.exists(src_folder):
104 # Download the required dependency tarballs.
105 for dep in self._DEPENDENCIES:
106 dep_path = self._DownloadFilesFromDevserver(
107 dep, self._setup_dir_path)
108 if os.path.exists(dep_path):
109 self._ExtractTarball(dep_path, self._tlm_src_dir_path)
110
111 # By default all the tarballs extract to test_src but some parts
112 # of the telemetry code specifically hardcoded to exist inside
113 # of 'src'.
114 try:
115 shutil.move(test_src, src_folder)
116 except shutil.Error:
117 raise TelemetrySetupError(
118 'Failure in telemetry setup for build %s. Appears '
119 'that the test_src to src move failed.' %
120 self._build)
121 return src_folder
122
123 def _DownloadFilesFromDevserver(self, filename, dest_path):
124 """Downloads the given tar.bz2 file from the devserver.
125
126 Args:
127 filename: Name of the tar.bz2 file to be downloaded.
128 dest_path: Full path to the directory where it should be downloaded.
129
130 Returns:
131 Full path to the downloaded file.
132
133 Raises:
134 TelemetrySetupError when the download cannot be completed for any
135 reason.
136 """
137 dep_path = os.path.join(dest_path, filename)
138 url = (self._STATIC_URL_TEMPLATE %
139 (self._ds.url(), self._build, filename))
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800140 try:
Sanika Kulkarni211a4ef2021-01-29 13:23:44 -0800141 resp = requests.get(url)
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800142 resp.raise_for_status()
143 with open(dep_path, 'w') as f:
144 for content in resp.iter_content(_READ_BUFFER_SIZE_BYTES):
145 f.write(content)
146 except Exception as e:
147 if (isinstance(e, requests.exceptions.HTTPError)
148 and resp.status_code == 404):
149 logging.error(
150 'The request %s returned a 404 Not Found status.'
151 'This dependency could be new and therefore does not '
152 'exist. Hence, squashing the exception and proceeding.',
153 url)
Sanika Kulkarni211a4ef2021-01-29 13:23:44 -0800154 elif isinstance(e, requests.exceptions.ConnectionError):
155 logging.warning(
156 'The request failed because a connection to the devserver '
157 '%s could not be established. Attempting to execute the '
158 'request %s once by SSH-ing into the devserver.',
159 self._ds.url(), url)
160 return self._DownloadFilesFromDevserverViaSSH(url, dep_path)
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800161 else:
162 raise TelemetrySetupError(
163 'An error occurred while trying to complete %s: %s' %
164 (url, e))
165 return dep_path
166
Sanika Kulkarni211a4ef2021-01-29 13:23:44 -0800167 def _DownloadFilesFromDevserverViaSSH(self, url, dep_path):
168 """Downloads the file at the URL from the devserver by SSH-ing into it.
169
170 Args:
171 url: URL of the location of the tar.bz2 file on the devserver.
172 dep_path: Full path to the file where it will be downloaded.
173
174 Returns:
175 Full path to the downloaded file.
176
177 Raises:
178 TelemetrySetupError when the download cannot be completed for any
179 reason.
180 """
181 cmd = ['ssh', self._ds.hostname, 'curl', url]
182 with open(dep_path, 'w') as f:
183 proc = subprocess.Popen(cmd, stdout=f, stderr=subprocess.PIPE)
184 _, err = proc.communicate()
185 if proc.returncode != 0:
186 raise TelemetrySetupError(
187 'The command: %s finished with returncode %s and '
188 'errors as following: %s. The telemetry dependency '
189 'could not be downloaded.' %
190 (' '.join(cmd), proc.returncode, err))
191 return dep_path
192
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800193 def _ExtractTarball(self, tarball_path, dest_path):
194 """Extracts the given tarball into the destination directory.
195
196 Args:
197 tarball_path: Full path to the tarball to be extracted.
198 dest_path: Full path to the directory where the tarball should be
199 extracted.
200
201 Raises:
202 TelemetrySetupError if the method is unable to extract the tarball for
203 any reason.
204 """
205 cmd = ['tar', 'xf', tarball_path, '--directory', dest_path]
206 try:
207 proc = subprocess.Popen(cmd,
208 stdout=subprocess.PIPE,
209 stderr=subprocess.PIPE)
210 proc.communicate()
211 except Exception as e:
212 shutil.rmtree(dest_path)
213 raise TelemetrySetupError(
214 'An exception occurred while trying to untar %s into %s: %s'
215 % (tarball_path, dest_path, str(e)))
216
217 def _MkDirP(self, path):
218 """Recursively creates the given directory.
219
220 Args:
221 path: Full path to the directory that needs to the created.
222
223 Raises:
224 TelemetrySetupError is the method is unable to create directories for
225 any reason except OSError EEXIST which indicates that the
226 directory already exists.
227 """
228 try:
229 os.makedirs(path)
230 except Exception as e:
231 if not isinstance(e, OSError) or e.errno != errno.EEXIST:
232 raise TelemetrySetupError(
233 'Could not create directory %s due to %s.' %
234 (path, str(e)))
235
236 def Cleanup(self):
237 """Cleans up telemetry setup and work environment."""
238 try:
239 shutil.rmtree(self._setup_dir_path)
240 except Exception as e:
241 logging.error('Something went wrong. Could not delete %s: %s',
242 self._setup_dir_path, e)
243 try:
244 shutil.rmtree(self._tlm_src_dir_path)
245 except Exception as e:
246 logging.error('Something went wrong. Could not delete %s: %s',
247 self._tlm_src_dir_path, e)