xref: /aosp_15_r20/external/autotest/site_utils/lxc/utils.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 Li"""This module provides some utilities used by LXC and its tools.
6*9c5db199SXin Li"""
7*9c5db199SXin Li
8*9c5db199SXin Liimport logging
9*9c5db199SXin Liimport os
10*9c5db199SXin Liimport re
11*9c5db199SXin Liimport shutil
12*9c5db199SXin Liimport tempfile
13*9c5db199SXin Liimport unittest
14*9c5db199SXin Lifrom contextlib import contextmanager
15*9c5db199SXin Li
16*9c5db199SXin Liimport common
17*9c5db199SXin Lifrom autotest_lib.client.bin import utils
18*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
19*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros.network import interface
20*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config
21*9c5db199SXin Lifrom autotest_lib.site_utils.lxc import constants
22*9c5db199SXin Lifrom autotest_lib.site_utils.lxc import unittest_setup
23*9c5db199SXin Li
24*9c5db199SXin Li
25*9c5db199SXin Lidef path_exists(path):
26*9c5db199SXin Li    """Check if path exists.
27*9c5db199SXin Li
28*9c5db199SXin Li    If the process is not running with root user, os.path.exists may fail to
29*9c5db199SXin Li    check if a path owned by root user exists. This function uses command
30*9c5db199SXin Li    `test -e` to check if path exists.
31*9c5db199SXin Li
32*9c5db199SXin Li    @param path: Path to check if it exists.
33*9c5db199SXin Li
34*9c5db199SXin Li    @return: True if path exists, otherwise False.
35*9c5db199SXin Li    """
36*9c5db199SXin Li    try:
37*9c5db199SXin Li        utils.run('sudo test -e "%s"' % path)
38*9c5db199SXin Li        return True
39*9c5db199SXin Li    except error.CmdError:
40*9c5db199SXin Li        return False
41*9c5db199SXin Li
42*9c5db199SXin Li
43*9c5db199SXin Lidef get_host_ip():
44*9c5db199SXin Li    """Get the IP address of the host running containers on lxcbr*.
45*9c5db199SXin Li
46*9c5db199SXin Li    This function gets the IP address on network interface lxcbr*. The
47*9c5db199SXin Li    assumption is that lxc uses the network interface started with "lxcbr".
48*9c5db199SXin Li
49*9c5db199SXin Li    @return: IP address of the host running containers.
50*9c5db199SXin Li    """
51*9c5db199SXin Li    # The kernel publishes symlinks to various network devices in /sys.
52*9c5db199SXin Li    result = utils.run('ls /sys/class/net', ignore_status=True)
53*9c5db199SXin Li    # filter out empty strings
54*9c5db199SXin Li    interface_names = [x for x in result.stdout.split() if x]
55*9c5db199SXin Li
56*9c5db199SXin Li    lxc_network = None
57*9c5db199SXin Li    for name in interface_names:
58*9c5db199SXin Li        if name.startswith('lxcbr'):
59*9c5db199SXin Li            lxc_network = name
60*9c5db199SXin Li            break
61*9c5db199SXin Li    if not lxc_network:
62*9c5db199SXin Li        raise error.ContainerError('Failed to find network interface used by '
63*9c5db199SXin Li                                   'lxc. All existing interfaces are: %s' %
64*9c5db199SXin Li                                   interface_names)
65*9c5db199SXin Li    netif = interface.Interface(lxc_network)
66*9c5db199SXin Li    return netif.ipv4_address
67*9c5db199SXin Li
68*9c5db199SXin Lidef is_vm():
69*9c5db199SXin Li    """Check if the process is running in a virtual machine.
70*9c5db199SXin Li
71*9c5db199SXin Li    @return: True if the process is running in a virtual machine, otherwise
72*9c5db199SXin Li             return False.
73*9c5db199SXin Li    """
74*9c5db199SXin Li    try:
75*9c5db199SXin Li        virt = utils.run('sudo -n virt-what').stdout.strip()
76*9c5db199SXin Li        logging.debug('virt-what output: %s', virt)
77*9c5db199SXin Li        return bool(virt)
78*9c5db199SXin Li    except error.CmdError:
79*9c5db199SXin Li        logging.warning('Package virt-what is not installed, default to assume '
80*9c5db199SXin Li                     'it is not a virtual machine.')
81*9c5db199SXin Li        return False
82*9c5db199SXin Li
83*9c5db199SXin Li
84*9c5db199SXin Lidef destroy(path, name,
85*9c5db199SXin Li            force=True, snapshots=False, ignore_status=False, timeout=-1):
86*9c5db199SXin Li    """
87*9c5db199SXin Li  Destroy an LXC container.
88*9c5db199SXin Li
89*9c5db199SXin Li  @param force: Destroy even if running. Default true.
90*9c5db199SXin Li  @param snapshots: Destroy all snapshots based on the container. Default false.
91*9c5db199SXin Li  @param ignore_status: Ignore return code of command. Default false.
92*9c5db199SXin Li  @param timeout: Seconds to wait for completion. No timeout imposed if the
93*9c5db199SXin Li    value is negative. Default -1 (no timeout).
94*9c5db199SXin Li
95*9c5db199SXin Li  @returns: CmdResult object from the shell command
96*9c5db199SXin Li  """
97*9c5db199SXin Li    cmd = 'sudo lxc-destroy -P %s -n %s' % (path, name)
98*9c5db199SXin Li    if force:
99*9c5db199SXin Li        cmd += ' -f'
100*9c5db199SXin Li    if snapshots:
101*9c5db199SXin Li        cmd += ' -s'
102*9c5db199SXin Li    if timeout >= 0:
103*9c5db199SXin Li        return utils.run(cmd, ignore_status=ignore_status, timeout=timeout)
104*9c5db199SXin Li    else:
105*9c5db199SXin Li        return utils.run(cmd, ignore_status=ignore_status)
106*9c5db199SXin Li
107*9c5db199SXin Li
108*9c5db199SXin Lidef clone(lxc_path, src_name, new_path, dst_name, snapshot):
109*9c5db199SXin Li    """Clones a container.
110*9c5db199SXin Li
111*9c5db199SXin Li    @param lxc_path: The LXC path of the source container.
112*9c5db199SXin Li    @param src_name: The name of the source container.
113*9c5db199SXin Li    @param new_path: The LXC path of the destination container.
114*9c5db199SXin Li    @param dst_name: The name of the destination container.
115*9c5db199SXin Li    @param snapshot: Whether or not to create a snapshot clone.
116*9c5db199SXin Li    """
117*9c5db199SXin Li    snapshot_arg = '-s' if snapshot and constants.SUPPORT_SNAPSHOT_CLONE else ''
118*9c5db199SXin Li    # overlayfs is the default clone backend storage. However it is not
119*9c5db199SXin Li    # supported in Ganeti yet. Use aufs as the alternative.
120*9c5db199SXin Li    aufs_arg = '-B aufs' if is_vm() and snapshot else ''
121*9c5db199SXin Li    cmd = (('sudo lxc-copy --lxcpath {lxcpath} --newpath {newpath} '
122*9c5db199SXin Li                    '--name {name} --newname {newname} {snapshot} {backing}')
123*9c5db199SXin Li           .format(
124*9c5db199SXin Li               lxcpath = lxc_path,
125*9c5db199SXin Li               newpath = new_path,
126*9c5db199SXin Li               name = src_name,
127*9c5db199SXin Li               newname = dst_name,
128*9c5db199SXin Li               snapshot = snapshot_arg,
129*9c5db199SXin Li               backing = aufs_arg
130*9c5db199SXin Li           ))
131*9c5db199SXin Li    utils.run(cmd)
132*9c5db199SXin Li
133*9c5db199SXin Li
134*9c5db199SXin Li@contextmanager
135*9c5db199SXin Lidef TempDir(*args, **kwargs):
136*9c5db199SXin Li    """Context manager for creating a temporary directory."""
137*9c5db199SXin Li    tmpdir = tempfile.mkdtemp(*args, **kwargs)
138*9c5db199SXin Li    try:
139*9c5db199SXin Li        yield tmpdir
140*9c5db199SXin Li    finally:
141*9c5db199SXin Li        shutil.rmtree(tmpdir)
142*9c5db199SXin Li
143*9c5db199SXin Li
144*9c5db199SXin Liclass BindMount(object):
145*9c5db199SXin Li    """Manages setup and cleanup of bind-mounts."""
146*9c5db199SXin Li    def __init__(self, spec):
147*9c5db199SXin Li        """Sets up a new bind mount.
148*9c5db199SXin Li
149*9c5db199SXin Li        Do not call this directly, use the create or from_existing class
150*9c5db199SXin Li        methods.
151*9c5db199SXin Li
152*9c5db199SXin Li        @param spec: A two-element tuple (dir, mountpoint) where dir is the
153*9c5db199SXin Li                     location of an existing directory, and mountpoint is the
154*9c5db199SXin Li                     path under that directory to the desired mount point.
155*9c5db199SXin Li        """
156*9c5db199SXin Li        self.spec = spec
157*9c5db199SXin Li
158*9c5db199SXin Li
159*9c5db199SXin Li    def __eq__(self, rhs):
160*9c5db199SXin Li        if isinstance(rhs, self.__class__):
161*9c5db199SXin Li            return self.spec == rhs.spec
162*9c5db199SXin Li        return NotImplemented
163*9c5db199SXin Li
164*9c5db199SXin Li
165*9c5db199SXin Li    def __ne__(self, rhs):
166*9c5db199SXin Li        return not (self == rhs)
167*9c5db199SXin Li
168*9c5db199SXin Li
169*9c5db199SXin Li    @classmethod
170*9c5db199SXin Li    def create(cls, src, dst, rename=None, readonly=False):
171*9c5db199SXin Li        """Creates a new bind mount.
172*9c5db199SXin Li
173*9c5db199SXin Li        @param src: The path of the source file/dir.
174*9c5db199SXin Li        @param dst: The destination directory.  The new mount point will be
175*9c5db199SXin Li                    ${dst}/${src} unless renamed.  If the mount point does not
176*9c5db199SXin Li                    already exist, it will be created.
177*9c5db199SXin Li        @param rename: An optional path to rename the mount.  If provided, the
178*9c5db199SXin Li                       mount point will be ${dst}/${rename} instead of
179*9c5db199SXin Li                       ${dst}/${src}.
180*9c5db199SXin Li        @param readonly: If True, the mount will be read-only.  False by
181*9c5db199SXin Li                         default.
182*9c5db199SXin Li
183*9c5db199SXin Li        @return An object representing the bind-mount, which can be used to
184*9c5db199SXin Li                clean it up later.
185*9c5db199SXin Li        """
186*9c5db199SXin Li        spec = (dst, (rename if rename else src).lstrip(os.path.sep))
187*9c5db199SXin Li        full_dst = os.path.join(*list(spec))
188*9c5db199SXin Li
189*9c5db199SXin Li        if not path_exists(full_dst):
190*9c5db199SXin Li            utils.run('sudo mkdir -p %s' % full_dst)
191*9c5db199SXin Li
192*9c5db199SXin Li        utils.run('sudo mount --bind %s %s' % (src, full_dst))
193*9c5db199SXin Li        if readonly:
194*9c5db199SXin Li            utils.run('sudo mount -o remount,ro,bind %s' % full_dst)
195*9c5db199SXin Li
196*9c5db199SXin Li        return cls(spec)
197*9c5db199SXin Li
198*9c5db199SXin Li
199*9c5db199SXin Li    @classmethod
200*9c5db199SXin Li    def from_existing(cls, host_dir, mount_point):
201*9c5db199SXin Li        """Creates a BindMount for an existing mount point.
202*9c5db199SXin Li
203*9c5db199SXin Li        @param host_dir: Path of the host dir hosting the bind-mount.
204*9c5db199SXin Li        @param mount_point: Full path to the mount point (including the host
205*9c5db199SXin Li                            dir).
206*9c5db199SXin Li
207*9c5db199SXin Li        @return An object representing the bind-mount, which can be used to
208*9c5db199SXin Li                clean it up later.
209*9c5db199SXin Li        """
210*9c5db199SXin Li        spec = (host_dir, os.path.relpath(mount_point, host_dir))
211*9c5db199SXin Li        return cls(spec)
212*9c5db199SXin Li
213*9c5db199SXin Li
214*9c5db199SXin Li    def cleanup(self):
215*9c5db199SXin Li        """Cleans up the bind-mount.
216*9c5db199SXin Li
217*9c5db199SXin Li        Unmounts the destination, and deletes it if possible. If it was mounted
218*9c5db199SXin Li        alongside important files, it will not be deleted.
219*9c5db199SXin Li        """
220*9c5db199SXin Li        full_dst = os.path.join(*list(self.spec))
221*9c5db199SXin Li        utils.run('sudo umount %s' % full_dst)
222*9c5db199SXin Li        # Ignore errors because bind mount locations are sometimes nested
223*9c5db199SXin Li        # alongside actual file content (e.g. SSPs install into
224*9c5db199SXin Li        # /usr/local/autotest so rmdir -p will fail for any mounts located in
225*9c5db199SXin Li        # /usr/local/autotest).
226*9c5db199SXin Li        utils.run('sudo bash -c "cd %s; rmdir -p --ignore-fail-on-non-empty %s"'
227*9c5db199SXin Li                  % self.spec)
228*9c5db199SXin Li
229*9c5db199SXin Li
230*9c5db199SXin Lidef is_subdir(parent, subdir):
231*9c5db199SXin Li    """Determines whether the given subdir exists under the given parent dir.
232*9c5db199SXin Li
233*9c5db199SXin Li    @param parent: The parent directory.
234*9c5db199SXin Li    @param subdir: The subdirectory.
235*9c5db199SXin Li
236*9c5db199SXin Li    @return True if the subdir exists under the parent dir, False otherwise.
237*9c5db199SXin Li    """
238*9c5db199SXin Li    # Append a trailing path separator because commonprefix basically just
239*9c5db199SXin Li    # performs a prefix string comparison.
240*9c5db199SXin Li    parent = os.path.join(parent, '')
241*9c5db199SXin Li    return os.path.commonprefix([parent, subdir]) == parent
242*9c5db199SXin Li
243*9c5db199SXin Li
244*9c5db199SXin Lidef sudo_commands(commands):
245*9c5db199SXin Li    """Takes a list of bash commands and executes them all with one invocation
246*9c5db199SXin Li    of sudo. Saves ~400 ms per command.
247*9c5db199SXin Li
248*9c5db199SXin Li    @param commands: The bash commands, as strings.
249*9c5db199SXin Li
250*9c5db199SXin Li    @return The return code of the sudo call.
251*9c5db199SXin Li    """
252*9c5db199SXin Li
253*9c5db199SXin Li    combine = global_config.global_config.get_config_value(
254*9c5db199SXin Li        'LXC_POOL','combine_sudos', type=bool, default=False)
255*9c5db199SXin Li
256*9c5db199SXin Li    if combine:
257*9c5db199SXin Li        with tempfile.NamedTemporaryFile() as temp:
258*9c5db199SXin Li            temp.write(b"set -e\n")
259*9c5db199SXin Li            temp.writelines([command+"\n" for command in commands])
260*9c5db199SXin Li            logging.info("Commands to run: %s", str(commands))
261*9c5db199SXin Li            return utils.run("sudo bash %s" % temp.name)
262*9c5db199SXin Li    else:
263*9c5db199SXin Li        for command in commands:
264*9c5db199SXin Li            result = utils.run("sudo %s" % command)
265*9c5db199SXin Li
266*9c5db199SXin Li
267*9c5db199SXin Lidef get_lxc_version():
268*9c5db199SXin Li    """Gets the current version of lxc if available."""
269*9c5db199SXin Li    cmd = 'sudo lxc-info --version'
270*9c5db199SXin Li    result = utils.run(cmd)
271*9c5db199SXin Li    if result and result.exit_status == 0:
272*9c5db199SXin Li        version = re.split("[.-]", result.stdout.strip())
273*9c5db199SXin Li        if len(version) < 3:
274*9c5db199SXin Li            logging.error("LXC version is not expected format %s.",
275*9c5db199SXin Li                          result.stdout.strip())
276*9c5db199SXin Li            return None
277*9c5db199SXin Li        return_value = []
278*9c5db199SXin Li        for a in version[:3]:
279*9c5db199SXin Li            try:
280*9c5db199SXin Li                return_value.append(int(a))
281*9c5db199SXin Li            except ValueError:
282*9c5db199SXin Li                logging.error(("LXC version contains non numerical version "
283*9c5db199SXin Li                               "number %s (%s)."), a, result.stdout.strip())
284*9c5db199SXin Li                return None
285*9c5db199SXin Li        return return_value
286*9c5db199SXin Li    else:
287*9c5db199SXin Li        logging.error("Unable to determine LXC version.")
288*9c5db199SXin Li        return None
289*9c5db199SXin Li
290*9c5db199SXin Liclass LXCTests(unittest.TestCase):
291*9c5db199SXin Li    """Thin wrapper to call correct setup for LXC tests."""
292*9c5db199SXin Li
293*9c5db199SXin Li    @classmethod
294*9c5db199SXin Li    def setUpClass(cls):
295*9c5db199SXin Li        unittest_setup.setup()
296