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