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 collections
      9 import os
     10 import shutil
     11 import tempfile
     12 from contextlib import contextmanager
     13 
     14 import common
     15 from autotest_lib.client.bin import utils
     16 from autotest_lib.client.common_lib import error
     17 from autotest_lib.client.common_lib.cros.network import interface
     18 from autotest_lib.site_utils.lxc import constants
     19 
     20 
     21 def path_exists(path):
     22     """Check if path exists.
     23 
     24     If the process is not running with root user, os.path.exists may fail to
     25     check if a path owned by root user exists. This function uses command
     26     `test -e` to check if path exists.
     27 
     28     @param path: Path to check if it exists.
     29 
     30     @return: True if path exists, otherwise False.
     31     """
     32     try:
     33         utils.run('sudo test -e "%s"' % path)
     34         return True
     35     except error.CmdError:
     36         return False
     37 
     38 
     39 def get_host_ip():
     40     """Get the IP address of the host running containers on lxcbr*.
     41 
     42     This function gets the IP address on network interface lxcbr*. The
     43     assumption is that lxc uses the network interface started with "lxcbr".
     44 
     45     @return: IP address of the host running containers.
     46     """
     47     # The kernel publishes symlinks to various network devices in /sys.
     48     result = utils.run('ls /sys/class/net', ignore_status=True)
     49     # filter out empty strings
     50     interface_names = [x for x in result.stdout.split() if x]
     51 
     52     lxc_network = None
     53     for name in interface_names:
     54         if name.startswith('lxcbr'):
     55             lxc_network = name
     56             break
     57     if not lxc_network:
     58         raise error.ContainerError('Failed to find network interface used by '
     59                                    'lxc. All existing interfaces are: %s' %
     60                                    interface_names)
     61     netif = interface.Interface(lxc_network)
     62     return netif.ipv4_address
     63 
     64 
     65 def clone(lxc_path, src_name, new_path, dst_name, snapshot):
     66     """Clones a container.
     67 
     68     @param lxc_path: The LXC path of the source container.
     69     @param src_name: The name of the source container.
     70     @param new_path: The LXC path of the destination container.
     71     @param dst_name: The name of the destination container.
     72     @param snapshot: Whether or not to create a snapshot clone.
     73     """
     74     snapshot_arg = '-s' if snapshot and constants.SUPPORT_SNAPSHOT_CLONE else ''
     75     # overlayfs is the default clone backend storage. However it is not
     76     # supported in Ganeti yet. Use aufs as the alternative.
     77     aufs_arg = '-B aufs' if utils.is_vm() and snapshot else ''
     78     cmd = (('sudo lxc-clone --lxcpath {lxcpath} --newpath {newpath} '
     79             '--orig {orig} --new {new} {snapshot} {backing}')
     80            .format(
     81                lxcpath = lxc_path,
     82                newpath = new_path,
     83                orig = src_name,
     84                new = dst_name,
     85                snapshot = snapshot_arg,
     86                backing = aufs_arg
     87            ))
     88     utils.run(cmd)
     89 
     90 
     91 @contextmanager
     92 def TempDir(*args, **kwargs):
     93     """Context manager for creating a temporary directory."""
     94     tmpdir = tempfile.mkdtemp(*args, **kwargs)
     95     try:
     96         yield tmpdir
     97     finally:
     98         shutil.rmtree(tmpdir)
     99 
    100 
    101 class BindMount(object):
    102     """Manages setup and cleanup of bind-mounts."""
    103     def __init__(self, spec):
    104         """Sets up a new bind mount.
    105 
    106         Do not call this directly, use the create or from_existing class
    107         methods.
    108 
    109         @param spec: A two-element tuple (dir, mountpoint) where dir is the
    110                      location of an existing directory, and mountpoint is the
    111                      path under that directory to the desired mount point.
    112         """
    113         self.spec = spec
    114 
    115 
    116     def __eq__(self, rhs):
    117         if isinstance(rhs, self.__class__):
    118             return self.spec == rhs.spec
    119         return NotImplemented
    120 
    121 
    122     def __ne__(self, rhs):
    123         return not (self == rhs)
    124 
    125 
    126     @classmethod
    127     def create(cls, src, dst, rename=None, readonly=False):
    128         """Creates a new bind mount.
    129 
    130         @param src: The path of the source file/dir.
    131         @param dst: The destination directory.  The new mount point will be
    132                     ${dst}/${src} unless renamed.  If the mount point does not
    133                     already exist, it will be created.
    134         @param rename: An optional path to rename the mount.  If provided, the
    135                        mount point will be ${dst}/${rename} instead of
    136                        ${dst}/${src}.
    137         @param readonly: If True, the mount will be read-only.  False by
    138                          default.
    139 
    140         @return An object representing the bind-mount, which can be used to
    141                 clean it up later.
    142         """
    143         spec = (dst, (rename if rename else src).lstrip(os.path.sep))
    144         full_dst = os.path.join(*list(spec))
    145 
    146         if not path_exists(full_dst):
    147             utils.run('sudo mkdir -p %s' % full_dst)
    148 
    149         utils.run('sudo mount --bind %s %s' % (src, full_dst))
    150         if readonly:
    151             utils.run('sudo mount -o remount,ro,bind %s' % full_dst)
    152 
    153         return cls(spec)
    154 
    155 
    156     @classmethod
    157     def from_existing(cls, host_dir, mount_point):
    158         """Creates a BindMount for an existing mount point.
    159 
    160         @param host_dir: Path of the host dir hosting the bind-mount.
    161         @param mount_point: Full path to the mount point (including the host
    162                             dir).
    163 
    164         @return An object representing the bind-mount, which can be used to
    165                 clean it up later.
    166         """
    167         spec = (host_dir, os.path.relpath(mount_point, host_dir))
    168         return cls(spec)
    169 
    170 
    171     def cleanup(self):
    172         """Cleans up the bind-mount.
    173 
    174         Unmounts the destination, and deletes it if possible. If it was mounted
    175         alongside important files, it will not be deleted.
    176         """
    177         full_dst = os.path.join(*list(self.spec))
    178         utils.run('sudo umount %s' % full_dst)
    179         # Ignore errors because bind mount locations are sometimes nested
    180         # alongside actual file content (e.g. SSPs install into
    181         # /usr/local/autotest so rmdir -p will fail for any mounts located in
    182         # /usr/local/autotest).
    183         utils.run('sudo bash -c "cd %s; rmdir -p --ignore-fail-on-non-empty %s"'
    184                   % self.spec)
    185 
    186 
    187 MountInfo = collections.namedtuple('MountInfo', ['root', 'mount_point', 'tags'])
    188 
    189 
    190 def get_mount_info(mount_point=None):
    191     """Retrieves information about currently mounted file systems.
    192 
    193     @param mount_point: (optional) The mount point (a path).  If this is
    194                         provided, only information about the given mount point
    195                         is returned.  If this is omitted, info about all mount
    196                         points is returned.
    197 
    198     @return A generator yielding one MountInfo object for each relevant mount
    199             found in /proc/self/mountinfo.
    200     """
    201     with open('/proc/self/mountinfo') as f:
    202         for line in f.readlines():
    203             # These lines are formatted according to the proc(5) manpage.
    204             # Sample line:
    205             # 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root \
    206             #     rw,errors=continue
    207             # Fields (descriptions omitted for fields we don't care about)
    208             # 3: the root of the mount.
    209             # 4: the mount point.
    210             # 5: mount options.
    211             # 6: tags.  There can be more than one of these.  This is where
    212             #    shared mounts are indicated.
    213             # 7: a dash separator marking the end of the tags.
    214             mountinfo = line.split()
    215             if mount_point is None or mountinfo[4] == mount_point:
    216                 tags = []
    217                 for field in mountinfo[6:]:
    218                     if field == '-':
    219                         break
    220                     tags.append(field.split(':')[0])
    221                 yield MountInfo(root = mountinfo[3],
    222                                 mount_point = mountinfo[4],
    223                                 tags = tags)
    224 
    225 
    226 def is_subdir(parent, subdir):
    227     """Determines whether the given subdir exists under the given parent dir.
    228 
    229     @param parent: The parent directory.
    230     @param subdir: The subdirectory.
    231 
    232     @return True if the subdir exists under the parent dir, False otherwise.
    233     """
    234     # Append a trailing path separator because commonprefix basically just
    235     # performs a prefix string comparison.
    236     parent = os.path.join(parent, '')
    237     return os.path.commonprefix([parent, subdir]) == parent
    238