• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
8import collections
9import os
10import shutil
11import tempfile
12from contextlib import contextmanager
13
14import common
15from autotest_lib.client.bin import utils
16from autotest_lib.client.common_lib import error
17from autotest_lib.client.common_lib.cros.network import interface
18from autotest_lib.site_utils.lxc import constants
19
20
21def 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
39def 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
65def 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
92def 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
101class 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
187MountInfo = collections.namedtuple('MountInfo', ['root', 'mount_point', 'tags'])
188
189
190def 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
226def 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