xref: /aosp_15_r20/external/autotest/site_utils/lxc/lxc.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Copyright 2015 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import absolute_import
6from __future__ import division
7from __future__ import print_function
8
9import logging
10import os
11import tempfile
12
13import common
14from autotest_lib.client.bin import utils as common_utils
15from autotest_lib.client.common_lib import error
16from autotest_lib.client.common_lib.cros import dev_server
17from autotest_lib.client.common_lib.cros import retry
18from autotest_lib.server import utils as server_utils
19from autotest_lib.site_utils.lxc import constants
20import six
21from six.moves import zip
22
23try:
24    from autotest_lib.utils.frozen_chromite.lib import metrics
25except ImportError:
26    metrics = common_utils.metrics_mock
27
28
29def get_container_info(container_path, **filters):
30    """Get a collection of container information in the given container path.
31
32    This method parse the output of lxc-ls to get a list of container
33    information. The lxc-ls command output looks like:
34    NAME      STATE    IPV4       IPV6  AUTOSTART  PID   MEMORY  RAM     SWAP
35    --------------------------------------------------------------------------
36    base      STOPPED  -          -     NO         -     -       -       -
37    test_123  RUNNING  10.0.3.27  -     NO         8359  6.28MB  6.28MB  0.0MB
38
39    @param container_path: Path to look for containers.
40    @param filters: Key value to filter the containers, e.g., name='base'
41
42    @return: A list of dictionaries that each dictionary has the information of
43             a container. The keys are defined in ATTRIBUTES.
44    """
45    cmd = 'sudo lxc-ls -P %s -f -F %s' % (os.path.realpath(container_path),
46                                          ','.join(constants.ATTRIBUTES))
47    output = common_utils.run(cmd).stdout
48    info_collection = []
49
50    logging.info('cmd [%s] output:\n%s', cmd, output)
51
52    for line in output.splitlines()[1:]:
53        # Only LXC 1.x has the second line of '-' as a separator.
54        if line.startswith('------'):
55            continue
56        info_collection.append(
57                dict(list(zip(constants.ATTRIBUTES, line.split()))))
58    if filters:
59        filtered_collection = []
60        for key, value in six.iteritems(filters):
61            for info in info_collection:
62                if key in info and info[key] == value:
63                    filtered_collection.append(info)
64        info_collection = filtered_collection
65    return info_collection
66
67
68def download_extract(url, target, extract_dir):
69    """Download the file from given url and save it to the target, then extract.
70
71    @param url: Url to download the file.
72    @param target: Path of the file to save to.
73    @param extract_dir: Directory to extract the content of the file to.
74    """
75    remote_url = dev_server.DevServer.get_server_url(url)
76    # This can be run in multiple threads, pick a unique tmp_file.name.
77    with tempfile.NamedTemporaryFile(prefix=os.path.basename(target) + '_',
78                                     delete=False) as tmp_file:
79        if remote_url in dev_server.ImageServerBase.servers():
80            _download_via_devserver(url, tmp_file.name)
81        else:
82            _download_via_curl(url, tmp_file.name)
83        common_utils.run('sudo mv %s %s' % (tmp_file.name, target))
84    common_utils.run('sudo tar -xvf %s -C %s' % (target, extract_dir))
85
86
87# Make sure retries only happen in the non-timeout case.
88@retry.retry((error.CmdError),
89             raiselist=[error.CmdTimeoutError],
90             timeout_min=3*2,
91             delay_sec=10)
92def _download_via_curl(url, target_file_path):
93    # We do not want to retry on CmdTimeoutError but still retry on
94    # CmdError. Hence we can't use curl --timeout=...
95    common_utils.run('sudo curl -s %s -o %s' % (url, target_file_path),
96                     stderr_tee=common_utils.TEE_TO_LOGS, timeout=3*60)
97
98
99# Make sure retries only happen in the non-timeout case.
100@retry.retry((error.CmdError),
101             raiselist=[error.CmdTimeoutError],
102             timeout_min=(constants.DEVSERVER_CALL_TIMEOUT *
103                          constants.DEVSERVER_CALL_RETRY / 60),
104             delay_sec=constants.DEVSERVER_CALL_DELAY)
105def _download_via_devserver(url, target_file_path):
106    dev_server.ImageServerBase.download_file(
107        url, target_file_path, timeout=constants.DEVSERVER_CALL_TIMEOUT)
108
109
110def _install_package_precheck(packages):
111    """If SSP is not enabled or the test is running in chroot (using test_that),
112    packages installation should be skipped.
113
114    The check does not raise exception so tests started by test_that or running
115    in an Autotest setup with SSP disabled can continue. That assume the running
116    environment, chroot or a machine, has the desired packages installed
117    already.
118
119    @param packages: A list of names of the packages to install.
120
121    @return: True if package installation can continue. False if it should be
122             skipped.
123
124    """
125    if server_utils.is_inside_chroot():
126        logging.info('Test is running inside chroot. Install package %s is '
127                     'skipped.', packages)
128        return False
129
130    if not common_utils.is_in_container():
131        raise error.ContainerError('Package installation is only supported '
132                                   'when test is running inside container.')
133
134    return True
135
136
137def _remove_banned_packages(packages, banned_packages):
138    """Filter out packages.
139
140    @param packages: A set of packages names that have been requested.
141    @param items: A list of package names that are not to be installed.
142
143    @return: A sanatized set of packages names to install.
144    """
145    return {package for package in packages if package not in banned_packages}
146
147
148def _ensure_pip(target_setting):
149    """ Ensure pip is installed, if not install it.
150
151    @param target_setting: target command param specifying the path to where
152                           python packages should be installed.
153    """
154    try:
155        import pip
156    except ImportError:
157        common_utils.run(
158            'wget https://bootstrap.pypa.io/get-pip.py -O /tmp/get-pip.py')
159        common_utils.run('python /tmp/get-pip.py %s' % target_setting)
160
161
162@metrics.SecondsTimerDecorator(
163    '%s/install_packages_duration' % constants.STATS_KEY)
164@retry.retry(error.CmdError, timeout_min=30)
165def install_packages(packages=[], python_packages=[], force_latest=False):
166    """Install the given package inside container.
167
168    !!! WARNING !!!
169    This call may introduce several minutes of delay in test run. The best way
170    to avoid such delay is to update the base container used for the test run.
171    File a bug for infra deputy to update the base container with the new
172    package a test requires.
173
174    @param packages: A list of names of the packages to install.
175    @param python_packages: A list of names of the python packages to install
176                            using pip.
177    @param force_latest: True to force to install the latest version of the
178                         package. Default to False, which means skip installing
179                         the package if it's installed already, even with an old
180                         version.
181
182    @raise error.ContainerError: If package is attempted to be installed outside
183                                 a container.
184    @raise error.CmdError: If the package doesn't exist or failed to install.
185
186    """
187    if not _install_package_precheck(packages or python_packages):
188        return
189
190    # If force_latest is False, only install packages that are not already
191    # installed.
192    if not force_latest:
193        packages = [p for p in packages
194                    if not common_utils.is_package_installed(p)]
195        python_packages = [p for p in python_packages
196                           if not common_utils.is_python_package_installed(p)]
197        if not packages and not python_packages:
198            logging.debug(
199                'All packages are installed already, skip reinstall.')
200            return
201
202    # Always run apt-get update before installing any container. The base
203    # container may have outdated cache.
204    common_utils.run('sudo apt-get update')
205
206    # Make sure the lists are not None for iteration.
207    packages = [] if not packages else packages
208    # Remove duplicates.
209    packages = set(packages)
210
211    # Ubuntu distribution of pip is very old, do not use it as it causes
212    # segmentation faults.  Some tests request these packages, ensure they
213    # are not installed.
214    packages = _remove_banned_packages(packages, ['python-pip', 'python-dev'])
215
216    if packages:
217        common_utils.run(
218            'sudo DEBIAN_FRONTEND=noninteractive apt-get install %s -y '
219            '--force-yes' % ' '.join(packages))
220        logging.debug('Packages are installed: %s.', packages)
221
222    target_setting = ''
223    # For containers running in Moblab, /usr/local/lib/python2.7/dist-packages/
224    # is a readonly mount from the host. Therefore, new python modules have to
225    # be installed in /usr/lib/python2.7/dist-packages/
226    # Containers created in Moblab does not have autotest/site-packages folder.
227    if not os.path.exists('/usr/local/autotest/site-packages'):
228        target_setting = '--target="/usr/lib/python2.7/dist-packages/"'
229    # Pip should be installed in the base container, if not install it.
230    if python_packages:
231        _ensure_pip(target_setting)
232        common_utils.run('python -m pip install pip --upgrade')
233        common_utils.run('python -m pip install %s %s' % (target_setting,
234                                                          ' '.join(python_packages)))
235        logging.debug('Python packages are installed: %s.', python_packages)
236