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