1*9c5db199SXin Li# Copyright 2015 The Chromium Authors. All rights reserved. 2*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 3*9c5db199SXin Li# found in the LICENSE file. 4*9c5db199SXin Li 5*9c5db199SXin Li"""This module provides some utilities used by LXC and its tools. 6*9c5db199SXin Li""" 7*9c5db199SXin Li 8*9c5db199SXin Liimport logging 9*9c5db199SXin Liimport os 10*9c5db199SXin Liimport re 11*9c5db199SXin Liimport shutil 12*9c5db199SXin Liimport tempfile 13*9c5db199SXin Liimport unittest 14*9c5db199SXin Lifrom contextlib import contextmanager 15*9c5db199SXin Li 16*9c5db199SXin Liimport common 17*9c5db199SXin Lifrom autotest_lib.client.bin import utils 18*9c5db199SXin Lifrom autotest_lib.client.common_lib import error 19*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros.network import interface 20*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config 21*9c5db199SXin Lifrom autotest_lib.site_utils.lxc import constants 22*9c5db199SXin Lifrom autotest_lib.site_utils.lxc import unittest_setup 23*9c5db199SXin Li 24*9c5db199SXin Li 25*9c5db199SXin Lidef path_exists(path): 26*9c5db199SXin Li """Check if path exists. 27*9c5db199SXin Li 28*9c5db199SXin Li If the process is not running with root user, os.path.exists may fail to 29*9c5db199SXin Li check if a path owned by root user exists. This function uses command 30*9c5db199SXin Li `test -e` to check if path exists. 31*9c5db199SXin Li 32*9c5db199SXin Li @param path: Path to check if it exists. 33*9c5db199SXin Li 34*9c5db199SXin Li @return: True if path exists, otherwise False. 35*9c5db199SXin Li """ 36*9c5db199SXin Li try: 37*9c5db199SXin Li utils.run('sudo test -e "%s"' % path) 38*9c5db199SXin Li return True 39*9c5db199SXin Li except error.CmdError: 40*9c5db199SXin Li return False 41*9c5db199SXin Li 42*9c5db199SXin Li 43*9c5db199SXin Lidef get_host_ip(): 44*9c5db199SXin Li """Get the IP address of the host running containers on lxcbr*. 45*9c5db199SXin Li 46*9c5db199SXin Li This function gets the IP address on network interface lxcbr*. The 47*9c5db199SXin Li assumption is that lxc uses the network interface started with "lxcbr". 48*9c5db199SXin Li 49*9c5db199SXin Li @return: IP address of the host running containers. 50*9c5db199SXin Li """ 51*9c5db199SXin Li # The kernel publishes symlinks to various network devices in /sys. 52*9c5db199SXin Li result = utils.run('ls /sys/class/net', ignore_status=True) 53*9c5db199SXin Li # filter out empty strings 54*9c5db199SXin Li interface_names = [x for x in result.stdout.split() if x] 55*9c5db199SXin Li 56*9c5db199SXin Li lxc_network = None 57*9c5db199SXin Li for name in interface_names: 58*9c5db199SXin Li if name.startswith('lxcbr'): 59*9c5db199SXin Li lxc_network = name 60*9c5db199SXin Li break 61*9c5db199SXin Li if not lxc_network: 62*9c5db199SXin Li raise error.ContainerError('Failed to find network interface used by ' 63*9c5db199SXin Li 'lxc. All existing interfaces are: %s' % 64*9c5db199SXin Li interface_names) 65*9c5db199SXin Li netif = interface.Interface(lxc_network) 66*9c5db199SXin Li return netif.ipv4_address 67*9c5db199SXin Li 68*9c5db199SXin Lidef is_vm(): 69*9c5db199SXin Li """Check if the process is running in a virtual machine. 70*9c5db199SXin Li 71*9c5db199SXin Li @return: True if the process is running in a virtual machine, otherwise 72*9c5db199SXin Li return False. 73*9c5db199SXin Li """ 74*9c5db199SXin Li try: 75*9c5db199SXin Li virt = utils.run('sudo -n virt-what').stdout.strip() 76*9c5db199SXin Li logging.debug('virt-what output: %s', virt) 77*9c5db199SXin Li return bool(virt) 78*9c5db199SXin Li except error.CmdError: 79*9c5db199SXin Li logging.warning('Package virt-what is not installed, default to assume ' 80*9c5db199SXin Li 'it is not a virtual machine.') 81*9c5db199SXin Li return False 82*9c5db199SXin Li 83*9c5db199SXin Li 84*9c5db199SXin Lidef destroy(path, name, 85*9c5db199SXin Li force=True, snapshots=False, ignore_status=False, timeout=-1): 86*9c5db199SXin Li """ 87*9c5db199SXin Li Destroy an LXC container. 88*9c5db199SXin Li 89*9c5db199SXin Li @param force: Destroy even if running. Default true. 90*9c5db199SXin Li @param snapshots: Destroy all snapshots based on the container. Default false. 91*9c5db199SXin Li @param ignore_status: Ignore return code of command. Default false. 92*9c5db199SXin Li @param timeout: Seconds to wait for completion. No timeout imposed if the 93*9c5db199SXin Li value is negative. Default -1 (no timeout). 94*9c5db199SXin Li 95*9c5db199SXin Li @returns: CmdResult object from the shell command 96*9c5db199SXin Li """ 97*9c5db199SXin Li cmd = 'sudo lxc-destroy -P %s -n %s' % (path, name) 98*9c5db199SXin Li if force: 99*9c5db199SXin Li cmd += ' -f' 100*9c5db199SXin Li if snapshots: 101*9c5db199SXin Li cmd += ' -s' 102*9c5db199SXin Li if timeout >= 0: 103*9c5db199SXin Li return utils.run(cmd, ignore_status=ignore_status, timeout=timeout) 104*9c5db199SXin Li else: 105*9c5db199SXin Li return utils.run(cmd, ignore_status=ignore_status) 106*9c5db199SXin Li 107*9c5db199SXin Li 108*9c5db199SXin Lidef clone(lxc_path, src_name, new_path, dst_name, snapshot): 109*9c5db199SXin Li """Clones a container. 110*9c5db199SXin Li 111*9c5db199SXin Li @param lxc_path: The LXC path of the source container. 112*9c5db199SXin Li @param src_name: The name of the source container. 113*9c5db199SXin Li @param new_path: The LXC path of the destination container. 114*9c5db199SXin Li @param dst_name: The name of the destination container. 115*9c5db199SXin Li @param snapshot: Whether or not to create a snapshot clone. 116*9c5db199SXin Li """ 117*9c5db199SXin Li snapshot_arg = '-s' if snapshot and constants.SUPPORT_SNAPSHOT_CLONE else '' 118*9c5db199SXin Li # overlayfs is the default clone backend storage. However it is not 119*9c5db199SXin Li # supported in Ganeti yet. Use aufs as the alternative. 120*9c5db199SXin Li aufs_arg = '-B aufs' if is_vm() and snapshot else '' 121*9c5db199SXin Li cmd = (('sudo lxc-copy --lxcpath {lxcpath} --newpath {newpath} ' 122*9c5db199SXin Li '--name {name} --newname {newname} {snapshot} {backing}') 123*9c5db199SXin Li .format( 124*9c5db199SXin Li lxcpath = lxc_path, 125*9c5db199SXin Li newpath = new_path, 126*9c5db199SXin Li name = src_name, 127*9c5db199SXin Li newname = dst_name, 128*9c5db199SXin Li snapshot = snapshot_arg, 129*9c5db199SXin Li backing = aufs_arg 130*9c5db199SXin Li )) 131*9c5db199SXin Li utils.run(cmd) 132*9c5db199SXin Li 133*9c5db199SXin Li 134*9c5db199SXin Li@contextmanager 135*9c5db199SXin Lidef TempDir(*args, **kwargs): 136*9c5db199SXin Li """Context manager for creating a temporary directory.""" 137*9c5db199SXin Li tmpdir = tempfile.mkdtemp(*args, **kwargs) 138*9c5db199SXin Li try: 139*9c5db199SXin Li yield tmpdir 140*9c5db199SXin Li finally: 141*9c5db199SXin Li shutil.rmtree(tmpdir) 142*9c5db199SXin Li 143*9c5db199SXin Li 144*9c5db199SXin Liclass BindMount(object): 145*9c5db199SXin Li """Manages setup and cleanup of bind-mounts.""" 146*9c5db199SXin Li def __init__(self, spec): 147*9c5db199SXin Li """Sets up a new bind mount. 148*9c5db199SXin Li 149*9c5db199SXin Li Do not call this directly, use the create or from_existing class 150*9c5db199SXin Li methods. 151*9c5db199SXin Li 152*9c5db199SXin Li @param spec: A two-element tuple (dir, mountpoint) where dir is the 153*9c5db199SXin Li location of an existing directory, and mountpoint is the 154*9c5db199SXin Li path under that directory to the desired mount point. 155*9c5db199SXin Li """ 156*9c5db199SXin Li self.spec = spec 157*9c5db199SXin Li 158*9c5db199SXin Li 159*9c5db199SXin Li def __eq__(self, rhs): 160*9c5db199SXin Li if isinstance(rhs, self.__class__): 161*9c5db199SXin Li return self.spec == rhs.spec 162*9c5db199SXin Li return NotImplemented 163*9c5db199SXin Li 164*9c5db199SXin Li 165*9c5db199SXin Li def __ne__(self, rhs): 166*9c5db199SXin Li return not (self == rhs) 167*9c5db199SXin Li 168*9c5db199SXin Li 169*9c5db199SXin Li @classmethod 170*9c5db199SXin Li def create(cls, src, dst, rename=None, readonly=False): 171*9c5db199SXin Li """Creates a new bind mount. 172*9c5db199SXin Li 173*9c5db199SXin Li @param src: The path of the source file/dir. 174*9c5db199SXin Li @param dst: The destination directory. The new mount point will be 175*9c5db199SXin Li ${dst}/${src} unless renamed. If the mount point does not 176*9c5db199SXin Li already exist, it will be created. 177*9c5db199SXin Li @param rename: An optional path to rename the mount. If provided, the 178*9c5db199SXin Li mount point will be ${dst}/${rename} instead of 179*9c5db199SXin Li ${dst}/${src}. 180*9c5db199SXin Li @param readonly: If True, the mount will be read-only. False by 181*9c5db199SXin Li default. 182*9c5db199SXin Li 183*9c5db199SXin Li @return An object representing the bind-mount, which can be used to 184*9c5db199SXin Li clean it up later. 185*9c5db199SXin Li """ 186*9c5db199SXin Li spec = (dst, (rename if rename else src).lstrip(os.path.sep)) 187*9c5db199SXin Li full_dst = os.path.join(*list(spec)) 188*9c5db199SXin Li 189*9c5db199SXin Li if not path_exists(full_dst): 190*9c5db199SXin Li utils.run('sudo mkdir -p %s' % full_dst) 191*9c5db199SXin Li 192*9c5db199SXin Li utils.run('sudo mount --bind %s %s' % (src, full_dst)) 193*9c5db199SXin Li if readonly: 194*9c5db199SXin Li utils.run('sudo mount -o remount,ro,bind %s' % full_dst) 195*9c5db199SXin Li 196*9c5db199SXin Li return cls(spec) 197*9c5db199SXin Li 198*9c5db199SXin Li 199*9c5db199SXin Li @classmethod 200*9c5db199SXin Li def from_existing(cls, host_dir, mount_point): 201*9c5db199SXin Li """Creates a BindMount for an existing mount point. 202*9c5db199SXin Li 203*9c5db199SXin Li @param host_dir: Path of the host dir hosting the bind-mount. 204*9c5db199SXin Li @param mount_point: Full path to the mount point (including the host 205*9c5db199SXin Li dir). 206*9c5db199SXin Li 207*9c5db199SXin Li @return An object representing the bind-mount, which can be used to 208*9c5db199SXin Li clean it up later. 209*9c5db199SXin Li """ 210*9c5db199SXin Li spec = (host_dir, os.path.relpath(mount_point, host_dir)) 211*9c5db199SXin Li return cls(spec) 212*9c5db199SXin Li 213*9c5db199SXin Li 214*9c5db199SXin Li def cleanup(self): 215*9c5db199SXin Li """Cleans up the bind-mount. 216*9c5db199SXin Li 217*9c5db199SXin Li Unmounts the destination, and deletes it if possible. If it was mounted 218*9c5db199SXin Li alongside important files, it will not be deleted. 219*9c5db199SXin Li """ 220*9c5db199SXin Li full_dst = os.path.join(*list(self.spec)) 221*9c5db199SXin Li utils.run('sudo umount %s' % full_dst) 222*9c5db199SXin Li # Ignore errors because bind mount locations are sometimes nested 223*9c5db199SXin Li # alongside actual file content (e.g. SSPs install into 224*9c5db199SXin Li # /usr/local/autotest so rmdir -p will fail for any mounts located in 225*9c5db199SXin Li # /usr/local/autotest). 226*9c5db199SXin Li utils.run('sudo bash -c "cd %s; rmdir -p --ignore-fail-on-non-empty %s"' 227*9c5db199SXin Li % self.spec) 228*9c5db199SXin Li 229*9c5db199SXin Li 230*9c5db199SXin Lidef is_subdir(parent, subdir): 231*9c5db199SXin Li """Determines whether the given subdir exists under the given parent dir. 232*9c5db199SXin Li 233*9c5db199SXin Li @param parent: The parent directory. 234*9c5db199SXin Li @param subdir: The subdirectory. 235*9c5db199SXin Li 236*9c5db199SXin Li @return True if the subdir exists under the parent dir, False otherwise. 237*9c5db199SXin Li """ 238*9c5db199SXin Li # Append a trailing path separator because commonprefix basically just 239*9c5db199SXin Li # performs a prefix string comparison. 240*9c5db199SXin Li parent = os.path.join(parent, '') 241*9c5db199SXin Li return os.path.commonprefix([parent, subdir]) == parent 242*9c5db199SXin Li 243*9c5db199SXin Li 244*9c5db199SXin Lidef sudo_commands(commands): 245*9c5db199SXin Li """Takes a list of bash commands and executes them all with one invocation 246*9c5db199SXin Li of sudo. Saves ~400 ms per command. 247*9c5db199SXin Li 248*9c5db199SXin Li @param commands: The bash commands, as strings. 249*9c5db199SXin Li 250*9c5db199SXin Li @return The return code of the sudo call. 251*9c5db199SXin Li """ 252*9c5db199SXin Li 253*9c5db199SXin Li combine = global_config.global_config.get_config_value( 254*9c5db199SXin Li 'LXC_POOL','combine_sudos', type=bool, default=False) 255*9c5db199SXin Li 256*9c5db199SXin Li if combine: 257*9c5db199SXin Li with tempfile.NamedTemporaryFile() as temp: 258*9c5db199SXin Li temp.write(b"set -e\n") 259*9c5db199SXin Li temp.writelines([command+"\n" for command in commands]) 260*9c5db199SXin Li logging.info("Commands to run: %s", str(commands)) 261*9c5db199SXin Li return utils.run("sudo bash %s" % temp.name) 262*9c5db199SXin Li else: 263*9c5db199SXin Li for command in commands: 264*9c5db199SXin Li result = utils.run("sudo %s" % command) 265*9c5db199SXin Li 266*9c5db199SXin Li 267*9c5db199SXin Lidef get_lxc_version(): 268*9c5db199SXin Li """Gets the current version of lxc if available.""" 269*9c5db199SXin Li cmd = 'sudo lxc-info --version' 270*9c5db199SXin Li result = utils.run(cmd) 271*9c5db199SXin Li if result and result.exit_status == 0: 272*9c5db199SXin Li version = re.split("[.-]", result.stdout.strip()) 273*9c5db199SXin Li if len(version) < 3: 274*9c5db199SXin Li logging.error("LXC version is not expected format %s.", 275*9c5db199SXin Li result.stdout.strip()) 276*9c5db199SXin Li return None 277*9c5db199SXin Li return_value = [] 278*9c5db199SXin Li for a in version[:3]: 279*9c5db199SXin Li try: 280*9c5db199SXin Li return_value.append(int(a)) 281*9c5db199SXin Li except ValueError: 282*9c5db199SXin Li logging.error(("LXC version contains non numerical version " 283*9c5db199SXin Li "number %s (%s)."), a, result.stdout.strip()) 284*9c5db199SXin Li return None 285*9c5db199SXin Li return return_value 286*9c5db199SXin Li else: 287*9c5db199SXin Li logging.error("Unable to determine LXC version.") 288*9c5db199SXin Li return None 289*9c5db199SXin Li 290*9c5db199SXin Liclass LXCTests(unittest.TestCase): 291*9c5db199SXin Li """Thin wrapper to call correct setup for LXC tests.""" 292*9c5db199SXin Li 293*9c5db199SXin Li @classmethod 294*9c5db199SXin Li def setUpClass(cls): 295*9c5db199SXin Li unittest_setup.setup() 296