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