• 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 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 destroy(path, name,
85            force=True, snapshots=False, ignore_status=False, timeout=-1):
86  """
87  Destroy an LXC container.
88
89  @param force: Destroy even if running. Default true.
90  @param snapshots: Destroy all snapshots based on the container. Default false.
91  @param ignore_status: Ignore return code of command. Default false.
92  @param timeout: Seconds to wait for completion. No timeout imposed if the
93    value is negative. Default -1 (no timeout).
94
95  @returns: CmdResult object from the shell command
96  """
97  cmd = 'sudo lxc-destroy -P %s -n %s' % (path, name)
98  if force:
99    cmd += ' -f'
100  if snapshots:
101    cmd += ' -s'
102  if timeout >= 0:
103    return utils.run(cmd, ignore_status=ignore_status, timeout=timeout)
104  else:
105    return utils.run(cmd, ignore_status=ignore_status)
106
107def clone(lxc_path, src_name, new_path, dst_name, snapshot):
108    """Clones a container.
109
110    @param lxc_path: The LXC path of the source container.
111    @param src_name: The name of the source container.
112    @param new_path: The LXC path of the destination container.
113    @param dst_name: The name of the destination container.
114    @param snapshot: Whether or not to create a snapshot clone.
115    """
116    snapshot_arg = '-s' if snapshot and constants.SUPPORT_SNAPSHOT_CLONE else ''
117    # overlayfs is the default clone backend storage. However it is not
118    # supported in Ganeti yet. Use aufs as the alternative.
119    aufs_arg = '-B aufs' if is_vm() and snapshot else ''
120    cmd = (('sudo lxc-copy --lxcpath {lxcpath} --newpath {newpath} '
121                    '--name {name} --newname {newname} {snapshot} {backing}')
122           .format(
123               lxcpath = lxc_path,
124               newpath = new_path,
125               name = src_name,
126               newname = dst_name,
127               snapshot = snapshot_arg,
128               backing = aufs_arg
129           ))
130    utils.run(cmd)
131
132
133@contextmanager
134def TempDir(*args, **kwargs):
135    """Context manager for creating a temporary directory."""
136    tmpdir = tempfile.mkdtemp(*args, **kwargs)
137    try:
138        yield tmpdir
139    finally:
140        shutil.rmtree(tmpdir)
141
142
143class BindMount(object):
144    """Manages setup and cleanup of bind-mounts."""
145    def __init__(self, spec):
146        """Sets up a new bind mount.
147
148        Do not call this directly, use the create or from_existing class
149        methods.
150
151        @param spec: A two-element tuple (dir, mountpoint) where dir is the
152                     location of an existing directory, and mountpoint is the
153                     path under that directory to the desired mount point.
154        """
155        self.spec = spec
156
157
158    def __eq__(self, rhs):
159        if isinstance(rhs, self.__class__):
160            return self.spec == rhs.spec
161        return NotImplemented
162
163
164    def __ne__(self, rhs):
165        return not (self == rhs)
166
167
168    @classmethod
169    def create(cls, src, dst, rename=None, readonly=False):
170        """Creates a new bind mount.
171
172        @param src: The path of the source file/dir.
173        @param dst: The destination directory.  The new mount point will be
174                    ${dst}/${src} unless renamed.  If the mount point does not
175                    already exist, it will be created.
176        @param rename: An optional path to rename the mount.  If provided, the
177                       mount point will be ${dst}/${rename} instead of
178                       ${dst}/${src}.
179        @param readonly: If True, the mount will be read-only.  False by
180                         default.
181
182        @return An object representing the bind-mount, which can be used to
183                clean it up later.
184        """
185        spec = (dst, (rename if rename else src).lstrip(os.path.sep))
186        full_dst = os.path.join(*list(spec))
187
188        if not path_exists(full_dst):
189            utils.run('sudo mkdir -p %s' % full_dst)
190
191        utils.run('sudo mount --bind %s %s' % (src, full_dst))
192        if readonly:
193            utils.run('sudo mount -o remount,ro,bind %s' % full_dst)
194
195        return cls(spec)
196
197
198    @classmethod
199    def from_existing(cls, host_dir, mount_point):
200        """Creates a BindMount for an existing mount point.
201
202        @param host_dir: Path of the host dir hosting the bind-mount.
203        @param mount_point: Full path to the mount point (including the host
204                            dir).
205
206        @return An object representing the bind-mount, which can be used to
207                clean it up later.
208        """
209        spec = (host_dir, os.path.relpath(mount_point, host_dir))
210        return cls(spec)
211
212
213    def cleanup(self):
214        """Cleans up the bind-mount.
215
216        Unmounts the destination, and deletes it if possible. If it was mounted
217        alongside important files, it will not be deleted.
218        """
219        full_dst = os.path.join(*list(self.spec))
220        utils.run('sudo umount %s' % full_dst)
221        # Ignore errors because bind mount locations are sometimes nested
222        # alongside actual file content (e.g. SSPs install into
223        # /usr/local/autotest so rmdir -p will fail for any mounts located in
224        # /usr/local/autotest).
225        utils.run('sudo bash -c "cd %s; rmdir -p --ignore-fail-on-non-empty %s"'
226                  % self.spec)
227
228
229def is_subdir(parent, subdir):
230    """Determines whether the given subdir exists under the given parent dir.
231
232    @param parent: The parent directory.
233    @param subdir: The subdirectory.
234
235    @return True if the subdir exists under the parent dir, False otherwise.
236    """
237    # Append a trailing path separator because commonprefix basically just
238    # performs a prefix string comparison.
239    parent = os.path.join(parent, '')
240    return os.path.commonprefix([parent, subdir]) == parent
241
242
243def sudo_commands(commands):
244    """Takes a list of bash commands and executes them all with one invocation
245    of sudo. Saves ~400 ms per command.
246
247    @param commands: The bash commands, as strings.
248
249    @return The return code of the sudo call.
250    """
251
252    combine = global_config.global_config.get_config_value(
253        'LXC_POOL','combine_sudos', type=bool, default=False)
254
255    if combine:
256        with tempfile.NamedTemporaryFile() as temp:
257            temp.write("set -e\n")
258            temp.writelines([command+"\n" for command in commands])
259            logging.info("Commands to run: %s", str(commands))
260            return utils.run("sudo bash %s" % temp.name)
261    else:
262        for command in commands:
263            result = utils.run("sudo %s" % command)
264
265
266def get_lxc_version():
267    """Gets the current version of lxc if available."""
268    cmd = 'sudo lxc-info --version'
269    result = utils.run(cmd)
270    if result and result.exit_status == 0:
271        version = re.split("[.-]", result.stdout.strip())
272        if len(version) < 3:
273            logging.error("LXC version is not expected format %s.",
274                          result.stdout.strip())
275            return None
276        return_value = []
277        for a in version[:3]:
278            try:
279                return_value.append(int(a))
280            except ValueError:
281                logging.error(("LXC version contains non numerical version "
282                               "number %s (%s)."), a, result.stdout.strip())
283                return None
284        return return_value
285    else:
286        logging.error("Unable to determine LXC version.")
287        return None
288
289class LXCTests(unittest.TestCase):
290    """Thin wrapper to call correct setup for LXC tests."""
291
292    @classmethod
293    def setUpClass(cls):
294        unittest_setup.setup()
295