1# Copyright 2017 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 5import logging 6import os 7 8import common 9from autotest_lib.client.bin import utils 10from autotest_lib.client.common_lib import error 11from autotest_lib.site_utils.lxc import Container 12from autotest_lib.site_utils.lxc import constants 13from autotest_lib.site_utils.lxc import lxc 14from autotest_lib.site_utils.lxc import utils as lxc_utils 15 16 17class Zygote(Container): 18 """A Container that implements post-bringup configuration. 19 """ 20 21 def __init__(self, container_path, name, attribute_values, src=None, 22 snapshot=False, host_path=None): 23 """Initialize an object of LXC container with given attribute values. 24 25 @param container_path: Directory that stores the container. 26 @param name: Name of the container. 27 @param attribute_values: A dictionary of attribute values for the 28 container. 29 @param src: An optional source container. If provided, the source 30 continer is cloned, and the new container will point to the 31 clone. 32 @param snapshot: Whether or not to create a snapshot clone. By default, 33 this is false. If a snapshot is requested and creating 34 a snapshot clone fails, a full clone will be attempted. 35 @param host_path: If set to None (the default), a host path will be 36 generated based on constants.DEFAULT_SHARED_HOST_PATH. 37 Otherwise, this can be used to override the host path 38 of the new container, for testing purposes. 39 """ 40 # Check if this is a pre-existing LXC container. Do this before calling 41 # the super ctor, because that triggers container creation. 42 exists = lxc.get_container_info(container_path, name=name) 43 44 super(Zygote, self).__init__(container_path, name, attribute_values, 45 src, snapshot) 46 47 logging.debug( 48 'Creating Zygote (lxcpath:%s name:%s)', container_path, name) 49 50 # host_path is a directory within a shared bind-mount, which enables 51 # bind-mounts from the host system to be shared with the LXC container. 52 if host_path is not None: 53 # Allow the host_path to be injected, for testing. 54 self.host_path = host_path 55 else: 56 if exists: 57 # Pre-existing Zygotes must have a host path. 58 self.host_path = self._find_existing_host_dir() 59 if self.host_path is None: 60 raise error.ContainerError( 61 'Container %s has no host path.' % 62 os.path.join(container_path, name)) 63 else: 64 # New Zygotes use a predefined template to generate a host path. 65 self.host_path = os.path.join( 66 os.path.realpath(constants.DEFAULT_SHARED_HOST_PATH), 67 self.name) 68 69 # host_path_ro is a directory for holding intermediate mount points, 70 # which are necessary when creating read-only bind mounts. See the 71 # mount_dir method for more details. 72 # 73 # Generate a host_path_ro based on host_path. 74 ro_dir, ro_name = os.path.split(self.host_path.rstrip(os.path.sep)) 75 self.host_path_ro = os.path.join(ro_dir, '%s.ro' % ro_name) 76 77 # Remember mounts so they can be cleaned up in destroy. 78 self.mounts = [] 79 80 if exists: 81 self._find_existing_bind_mounts() 82 else: 83 # Creating a new Zygote - initialize the host dirs. Don't use sudo, 84 # so that the resulting directories can be accessed by autoserv (for 85 # SSP installation, etc). 86 if not lxc_utils.path_exists(self.host_path): 87 os.makedirs(self.host_path) 88 if not lxc_utils.path_exists(self.host_path_ro): 89 os.makedirs(self.host_path_ro) 90 91 # Create the mount point within the container's rootfs. 92 # Changes within container's rootfs require sudo. 93 utils.run('sudo mkdir %s' % 94 os.path.join(self.rootfs, 95 constants.CONTAINER_HOST_DIR.lstrip( 96 os.path.sep))) 97 self.mount_dir(self.host_path, constants.CONTAINER_HOST_DIR) 98 99 100 def destroy(self, force=True): 101 """Destroy the Zygote. 102 103 This destroys the underlying container (see Container.destroy) and also 104 cleans up any host mounts associated with it. 105 106 @param force: Force container destruction even if it's running. See 107 Container.destroy. 108 """ 109 logging.debug('Destroying Zygote %s', self.name) 110 super(Zygote, self).destroy(force) 111 self._cleanup_host_mount() 112 113 114 def install_ssp(self, ssp_url): 115 """Downloads and installs the given server package. 116 117 @param ssp_url: The URL of the ssp to download and install. 118 """ 119 # The host dir is mounted directly on /usr/local/autotest within the 120 # container. The SSP structure assumes it gets untarred into the 121 # /usr/local directory of the container's rootfs. In order to unpack 122 # with the correct directory structure, create a tmpdir, mount the 123 # container's host dir as ./autotest, and unpack the SSP. 124 if not self.is_running(): 125 super(Zygote, self).install_ssp(ssp_url) 126 return 127 128 usr_local_path = os.path.join(self.host_path, 'usr', 'local') 129 os.makedirs(usr_local_path) 130 131 with lxc_utils.TempDir(dir=usr_local_path) as tmpdir: 132 download_tmp = os.path.join(tmpdir, 133 'autotest_server_package.tar.bz2') 134 lxc.download_extract(ssp_url, download_tmp, usr_local_path) 135 136 container_ssp_path = os.path.join( 137 constants.CONTAINER_HOST_DIR, 138 constants.CONTAINER_AUTOTEST_DIR.lstrip(os.path.sep)) 139 self.attach_run('mkdir -p %s && mount --bind %s %s' % 140 (constants.CONTAINER_AUTOTEST_DIR, 141 container_ssp_path, 142 constants.CONTAINER_AUTOTEST_DIR)) 143 144 145 def copy(self, host_path, container_path): 146 """Copies files into the Zygote. 147 148 @param host_path: Path to the source file/dir to be copied. 149 @param container_path: Path to the destination dir (in the container). 150 """ 151 if not self.is_running(): 152 return super(Zygote, self).copy(host_path, container_path) 153 154 logging.debug('copy %s to %s', host_path, container_path) 155 156 # First copy the files into the host mount, then move them from within 157 # the container. 158 self._do_copy(src=host_path, 159 dst=os.path.join(self.host_path, 160 container_path.lstrip(os.path.sep))) 161 162 src = os.path.join(constants.CONTAINER_HOST_DIR, 163 container_path.lstrip(os.path.sep)) 164 dst = container_path 165 166 # In the container, bind-mount from host path to destination. 167 # The mount destination must have the correct type (file vs dir). 168 if os.path.isdir(host_path): 169 self.attach_run('mkdir -p %s' % dst) 170 else: 171 self.attach_run( 172 'mkdir -p %s && touch %s' % (os.path.dirname(dst), dst)) 173 self.attach_run('mount --bind %s %s' % (src, dst)) 174 175 176 def mount_dir(self, source, destination, readonly=False): 177 """Mount a directory in host to a directory in the container. 178 179 @param source: Directory in host to be mounted. 180 @param destination: Directory in container to mount the source directory 181 @param readonly: Set to True to make a readonly mount, default is False. 182 """ 183 if not self.is_running(): 184 return super(Zygote, self).mount_dir(source, destination, readonly) 185 186 # Destination path in container must be absolute. 187 if not os.path.isabs(destination): 188 destination = os.path.join('/', destination) 189 190 # Create directory in container for mount. 191 self.attach_run('mkdir -p %s' % destination) 192 193 # Creating read-only shared bind mounts is a two-stage process. First, 194 # the original file/directory is bind-mounted (with the ro option) to an 195 # intermediate location in self.host_path_ro. Then, the intermediate 196 # location is bind-mounted into the shared host dir. 197 # Replace the original source with this intermediate read-only mount, 198 # then continue. 199 if readonly: 200 source_ro = os.path.join(self.host_path_ro, 201 source.lstrip(os.path.sep)) 202 self.mounts.append(lxc_utils.BindMount.create( 203 source, self.host_path_ro, readonly=True)) 204 source = source_ro 205 206 # Mount the directory into the host dir, then from the host dir into the 207 # destination. 208 self.mounts.append( 209 lxc_utils.BindMount.create(source, self.host_path, destination)) 210 211 container_host_path = os.path.join(constants.CONTAINER_HOST_DIR, 212 destination.lstrip(os.path.sep)) 213 self.attach_run('mount --bind %s %s' % 214 (container_host_path, destination)) 215 216 217 def _cleanup_host_mount(self): 218 """Unmounts and removes the host dirs for this container.""" 219 # Clean up all intermediate bind mounts into host_path and host_path_ro. 220 for mount in self.mounts: 221 mount.cleanup() 222 # The SSP and other "real" content gets copied into the host dir. Use 223 # rm -r to clear it out. 224 if lxc_utils.path_exists(self.host_path): 225 utils.run('sudo rm -r "%s"' % self.host_path) 226 # The host_path_ro directory only contains intermediate bind points, 227 # which should all have been cleared out. Use rmdir. 228 if lxc_utils.path_exists(self.host_path_ro): 229 utils.run('sudo rmdir "%s"' % self.host_path_ro) 230 231 232 def _find_existing_host_dir(self): 233 """Finds the host mounts for a pre-existing Zygote. 234 235 The host directory is passed into the Zygote constructor when creating a 236 new Zygote. However, when a Zygote is instantiated on top of an already 237 existing LXC container, it has to reconnect to the existing host 238 directory. 239 240 @return: The host-side path to the host dir. 241 """ 242 # Look for the mount that targets the "/host" dir within the container. 243 for mount in self._get_lxc_config('lxc.mount.entry'): 244 mount_cfg = mount.split(' ') 245 if mount_cfg[1] == 'host': 246 return mount_cfg[0] 247 return None 248 249 250 def _find_existing_bind_mounts(self): 251 """Locates bind mounts associated with an existing container. 252 253 When a Zygote object is instantiated on top of an existing LXC 254 container, this method needs to be called so that all the bind-mounts 255 associated with the container can be reconstructed. This enables proper 256 cleanup later. 257 """ 258 for info in utils.get_mount_info(): 259 # Check for bind mounts in the host and host_ro directories, and 260 # re-add them to self.mounts. 261 if lxc_utils.is_subdir(self.host_path, info.mount_point): 262 logging.debug('mount: %s', info.mount_point) 263 self.mounts.append(lxc_utils.BindMount.from_existing( 264 self.host_path, info.mount_point)) 265 elif lxc_utils.is_subdir(self.host_path_ro, info.mount_point): 266 logging.debug('mount_ro: %s', info.mount_point) 267 self.mounts.append(lxc_utils.BindMount.from_existing( 268 self.host_path_ro, info.mount_point)) 269