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 logging
9import os
10import re
11import shutil
12import tempfile
13import unittest
14from contextlib import contextmanager
15
16import common
17from autotest_lib.client.bin import utils
18from autotest_lib.client.common_lib import error
19from autotest_lib.client.common_lib.cros.network import interface
20from autotest_lib.client.common_lib import global_config
21from autotest_lib.site_utils.lxc import constants
22from autotest_lib.site_utils.lxc import unittest_setup
23
24
25def 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
43def 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
68def 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
84def 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
111def 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
120class 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
206def 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
220def 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
243def 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
267class 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