xref: /aosp_15_r20/external/autotest/server/cros/telemetry_setup.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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