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