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