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 5import logging 6import os 7import re 8import time 9 10import common 11from autotest_lib.client.bin import utils 12from autotest_lib.client.common_lib import error 13from autotest_lib.site_utils.lxc import constants 14from autotest_lib.site_utils.lxc import lxc 15from autotest_lib.site_utils.lxc import utils as lxc_utils 16 17try: 18 from chromite.lib import metrics 19except ImportError: 20 metrics = utils.metrics_mock 21 22 23class Container(object): 24 """A wrapper class of an LXC container. 25 26 The wrapper class provides methods to interact with a container, e.g., 27 start, stop, destroy, run a command. It also has attributes of the 28 container, including: 29 name: Name of the container. 30 state: State of the container, e.g., ABORTING, RUNNING, STARTING, STOPPED, 31 or STOPPING. 32 33 lxc-ls can also collect other attributes of a container including: 34 ipv4: IP address for IPv4. 35 ipv6: IP address for IPv6. 36 autostart: If the container will autostart at system boot. 37 pid: Process ID of the container. 38 memory: Memory used by the container, as a string, e.g., "6.2MB" 39 ram: Physical ram used by the container, as a string, e.g., "6.2MB" 40 swap: swap used by the container, as a string, e.g., "1.0MB" 41 42 For performance reason, such info is not collected for now. 43 44 The attributes available are defined in ATTRIBUTES constant. 45 """ 46 47 def __init__(self, container_path, name, attribute_values, src=None, 48 snapshot=False): 49 """Initialize an object of LXC container with given attribute values. 50 51 @param container_path: Directory that stores the container. 52 @param name: Name of the container. 53 @param attribute_values: A dictionary of attribute values for the 54 container. 55 @param src: An optional source container. If provided, the source 56 continer is cloned, and the new container will point to the 57 clone. 58 @param snapshot: If a source container was specified, this argument 59 specifies whether or not to create a snapshot clone. 60 The default is to attempt to create a snapshot. 61 If a snapshot is requested and creating the snapshot 62 fails, a full clone will be attempted. 63 """ 64 self.container_path = os.path.realpath(container_path) 65 # Path to the rootfs of the container. This will be initialized when 66 # property rootfs is retrieved. 67 self._rootfs = None 68 self.name = name 69 for attribute, value in attribute_values.iteritems(): 70 setattr(self, attribute, value) 71 72 # Clone the container 73 if src is not None: 74 # Clone the source container to initialize this one. 75 lxc_utils.clone(src.container_path, src.name, self.container_path, 76 self.name, snapshot) 77 78 79 @classmethod 80 def createFromExistingDir(cls, lxc_path, name, **kwargs): 81 """Creates a new container instance for an lxc container that already 82 exists on disk. 83 84 @param lxc_path: The LXC path for the container. 85 @param name: The container name. 86 87 @raise error.ContainerError: If the container doesn't already exist. 88 89 @return: The new container. 90 """ 91 return cls(lxc_path, name, kwargs) 92 93 94 @classmethod 95 def clone(cls, src, new_name, new_path=None, snapshot=False, cleanup=False): 96 """Creates a clone of this container. 97 98 @param src: The original container. 99 @param new_name: Name for the cloned container. 100 @param new_path: LXC path for the cloned container (optional; if not 101 specified, the new container is created in the same directory as 102 the source container). 103 @param snapshot: Whether to snapshot, or create a full clone. 104 @param cleanup: If a container with the given name and path already 105 exist, clean it up first. 106 """ 107 if new_path is None: 108 new_path = src.container_path 109 110 # If a container exists at this location, clean it up first 111 container_folder = os.path.join(new_path, new_name) 112 if lxc_utils.path_exists(container_folder): 113 if not cleanup: 114 raise error.ContainerError('Container %s already exists.' % 115 new_name) 116 container = Container.createFromExistingDir(new_path, new_name) 117 try: 118 container.destroy() 119 except error.CmdError as e: 120 # The container could be created in a incompleted state. Delete 121 # the container folder instead. 122 logging.warn('Failed to destroy container %s, error: %s', 123 new_name, e) 124 utils.run('sudo rm -rf "%s"' % container_folder) 125 126 # Create and return the new container. 127 return cls(new_path, new_name, {}, src, snapshot) 128 129 130 def refresh_status(self): 131 """Refresh the status information of the container. 132 """ 133 containers = lxc.get_container_info(self.container_path, name=self.name) 134 if not containers: 135 raise error.ContainerError( 136 'No container found in directory %s with name of %s.' % 137 (self.container_path, self.name)) 138 attribute_values = containers[0] 139 for attribute, value in attribute_values.iteritems(): 140 setattr(self, attribute, value) 141 142 143 @property 144 def rootfs(self): 145 """Path to the rootfs of the container. 146 147 This property returns the path to the rootfs of the container, that is, 148 the folder where the container stores its local files. It reads the 149 attribute lxc.rootfs from the config file of the container, e.g., 150 lxc.rootfs = /usr/local/autotest/containers/t4/rootfs 151 If the container is created with snapshot, the rootfs is a chain of 152 folders, separated by `:` and ordered by how the snapshot is created, 153 e.g., 154 lxc.rootfs = overlayfs:/usr/local/autotest/containers/base/rootfs: 155 /usr/local/autotest/containers/t4_s/delta0 156 This function returns the last folder in the chain, in above example, 157 that is `/usr/local/autotest/containers/t4_s/delta0` 158 159 Files in the rootfs will be accessible directly within container. For 160 example, a folder in host "[rootfs]/usr/local/file1", can be accessed 161 inside container by path "/usr/local/file1". Note that symlink in the 162 host can not across host/container boundary, instead, directory mount 163 should be used, refer to function mount_dir. 164 165 @return: Path to the rootfs of the container. 166 """ 167 if not self._rootfs: 168 cmd = ('sudo lxc-info -P %s -n %s -c lxc.rootfs' % 169 (self.container_path, self.name)) 170 lxc_rootfs_config = utils.run(cmd).stdout.strip() 171 match = re.match('lxc.rootfs = (.*)', lxc_rootfs_config) 172 if not match: 173 raise error.ContainerError( 174 'Failed to locate rootfs for container %s. lxc.rootfs ' 175 'in the container config file is %s' % 176 (self.name, lxc_rootfs_config)) 177 lxc_rootfs = match.group(1) 178 cloned_from_snapshot = ':' in lxc_rootfs 179 if cloned_from_snapshot: 180 self._rootfs = lxc_rootfs.split(':')[-1] 181 else: 182 self._rootfs = lxc_rootfs 183 return self._rootfs 184 185 186 def attach_run(self, command, bash=True): 187 """Attach to a given container and run the given command. 188 189 @param command: Command to run in the container. 190 @param bash: Run the command through bash -c "command". This allows 191 pipes to be used in command. Default is set to True. 192 193 @return: The output of the command. 194 195 @raise error.CmdError: If container does not exist, or not running. 196 """ 197 cmd = 'sudo lxc-attach -P %s -n %s' % (self.container_path, self.name) 198 if bash and not command.startswith('bash -c'): 199 command = 'bash -c "%s"' % utils.sh_escape(command) 200 cmd += ' -- %s' % command 201 # TODO(dshi): crbug.com/459344 Set sudo to default to False when test 202 # container can be unprivileged container. 203 return utils.run(cmd) 204 205 206 def is_network_up(self): 207 """Check if network is up in the container by curl base container url. 208 209 @return: True if the network is up, otherwise False. 210 """ 211 try: 212 self.attach_run('curl --head %s' % constants.CONTAINER_BASE_URL) 213 return True 214 except error.CmdError as e: 215 logging.debug(e) 216 return False 217 218 219 @metrics.SecondsTimerDecorator( 220 '%s/container_start_duration' % constants.STATS_KEY) 221 def start(self, wait_for_network=True): 222 """Start the container. 223 224 @param wait_for_network: True to wait for network to be up. Default is 225 set to True. 226 227 @raise ContainerError: If container does not exist, or fails to start. 228 """ 229 cmd = 'sudo lxc-start -P %s -n %s -d' % (self.container_path, self.name) 230 output = utils.run(cmd).stdout 231 if not self.is_running(): 232 raise error.ContainerError( 233 'Container %s failed to start. lxc command output:\n%s' % 234 (os.path.join(self.container_path, self.name), 235 output)) 236 237 if wait_for_network: 238 logging.debug('Wait for network to be up.') 239 start_time = time.time() 240 utils.poll_for_condition( 241 condition=self.is_network_up, 242 timeout=constants.NETWORK_INIT_TIMEOUT, 243 sleep_interval=constants.NETWORK_INIT_CHECK_INTERVAL) 244 logging.debug('Network is up after %.2f seconds.', 245 time.time() - start_time) 246 247 248 @metrics.SecondsTimerDecorator( 249 '%s/container_stop_duration' % constants.STATS_KEY) 250 def stop(self): 251 """Stop the container. 252 253 @raise ContainerError: If container does not exist, or fails to start. 254 """ 255 cmd = 'sudo lxc-stop -P %s -n %s' % (self.container_path, self.name) 256 output = utils.run(cmd).stdout 257 self.refresh_status() 258 if self.state != 'STOPPED': 259 raise error.ContainerError( 260 'Container %s failed to be stopped. lxc command output:\n' 261 '%s' % (os.path.join(self.container_path, self.name), 262 output)) 263 264 265 @metrics.SecondsTimerDecorator( 266 '%s/container_destroy_duration' % constants.STATS_KEY) 267 def destroy(self, force=True): 268 """Destroy the container. 269 270 @param force: Set to True to force to destroy the container even if it's 271 running. This is faster than stop a container first then 272 try to destroy it. Default is set to True. 273 274 @raise ContainerError: If container does not exist or failed to destroy 275 the container. 276 """ 277 cmd = 'sudo lxc-destroy -P %s -n %s' % (self.container_path, 278 self.name) 279 if force: 280 cmd += ' -f' 281 utils.run(cmd) 282 283 284 def mount_dir(self, source, destination, readonly=False): 285 """Mount a directory in host to a directory in the container. 286 287 @param source: Directory in host to be mounted. 288 @param destination: Directory in container to mount the source directory 289 @param readonly: Set to True to make a readonly mount, default is False. 290 """ 291 # Destination path in container must be relative. 292 destination = destination.lstrip('/') 293 # Create directory in container for mount. 294 utils.run('sudo mkdir -p %s' % os.path.join(self.rootfs, destination)) 295 config_file = os.path.join(self.container_path, self.name, 'config') 296 mount = constants.MOUNT_FMT % {'source': source, 297 'destination': destination, 298 'readonly': ',ro' if readonly else ''} 299 utils.run( 300 constants.APPEND_CMD_FMT % {'content': mount, 'file': config_file}) 301 302 303 def verify_autotest_setup(self, job_folder): 304 """Verify autotest code is set up properly in the container. 305 306 @param job_folder: Name of the job result folder. 307 308 @raise ContainerError: If autotest code is not set up properly. 309 """ 310 # Test autotest code is setup by verifying a list of 311 # (directory, minimum file count) 312 directories_to_check = [ 313 (constants.CONTAINER_AUTOTEST_DIR, 3), 314 (constants.RESULT_DIR_FMT % job_folder, 0), 315 (constants.CONTAINER_SITE_PACKAGES_PATH, 3)] 316 for directory, count in directories_to_check: 317 result = self.attach_run(command=(constants.COUNT_FILE_CMD % 318 {'dir': directory})).stdout 319 logging.debug('%s entries in %s.', int(result), directory) 320 if int(result) < count: 321 raise error.ContainerError('%s is not properly set up.' % 322 directory) 323 # lxc-attach and run command does not run in shell, thus .bashrc is not 324 # loaded. Following command creates a symlink in /usr/bin/ for gsutil 325 # if it's installed. 326 # TODO(dshi): Remove this code after lab container is updated with 327 # gsutil installed in /usr/bin/ 328 self.attach_run('test -f /root/gsutil/gsutil && ' 329 'ln -s /root/gsutil/gsutil /usr/bin/gsutil || true') 330 331 332 def modify_import_order(self): 333 """Swap the python import order of lib and local/lib. 334 335 In Moblab, the host's python modules located in 336 /usr/lib64/python2.7/site-packages is mounted to following folder inside 337 container: /usr/local/lib/python2.7/dist-packages/. The modules include 338 an old version of requests module, which is used in autotest 339 site-packages. For test, the module is only used in 340 dev_server/symbolicate_dump for requests.call and requests.codes.OK. 341 When pip is installed inside the container, it installs requests module 342 with version of 2.2.1 in /usr/lib/python2.7/dist-packages/. The version 343 is newer than the one used in autotest site-packages, but not the latest 344 either. 345 According to /usr/lib/python2.7/site.py, modules in /usr/local/lib are 346 imported before the ones in /usr/lib. That leads to pip to use the older 347 version of requests (0.11.2), and it will fail. On the other hand, 348 requests module 2.2.1 can't be installed in CrOS (refer to CL:265759), 349 and higher version of requests module can't work with pip. 350 The only fix to resolve this is to switch the import order, so modules 351 in /usr/lib can be imported before /usr/local/lib. 352 """ 353 site_module = '/usr/lib/python2.7/site.py' 354 self.attach_run("sed -i ':a;N;$!ba;s/\"local\/lib\",\\n/" 355 "\"lib_placeholder\",\\n/g' %s" % site_module) 356 self.attach_run("sed -i ':a;N;$!ba;s/\"lib\",\\n/" 357 "\"local\/lib\",\\n/g' %s" % site_module) 358 self.attach_run('sed -i "s/lib_placeholder/lib/g" %s' % 359 site_module) 360 361 362 def is_running(self): 363 """Returns whether or not this container is currently running.""" 364 self.refresh_status() 365 return self.state == 'RUNNING' 366 367 368 def set_hostname(self, hostname): 369 """Sets the hostname within the container. This needs to be called 370 prior to starting the container. 371 """ 372 config_file = os.path.join(self.container_path, self.name, 'config') 373 lxc_utsname_setting = ( 374 'lxc.utsname = ' + 375 constants.CONTAINER_UTSNAME_FORMAT % hostname) 376 utils.run( 377 constants.APPEND_CMD_FMT % {'content': lxc_utsname_setting, 378 'file': config_file}) 379 380 381 def install_ssp(self, ssp_url): 382 """Downloads and installs the given server package. 383 384 @param ssp_url: The URL of the ssp to download and install. 385 """ 386 usr_local_path = os.path.join(self.rootfs, 'usr', 'local') 387 autotest_pkg_path = os.path.join(usr_local_path, 388 'autotest_server_package.tar.bz2') 389 # sudo is required so os.makedirs may not work. 390 utils.run('sudo mkdir -p %s'% usr_local_path) 391 392 lxc.download_extract(ssp_url, autotest_pkg_path, usr_local_path) 393 394 395 def install_control_file(self, control_file): 396 """Installs the given control file. 397 The given file will be moved into the container. 398 399 @param control_file: Path to the control file to install. 400 """ 401 dst_path = os.path.join(self.rootfs, 402 constants.CONTROL_TEMP_PATH.lstrip(os.path.sep)) 403 utils.run('sudo mkdir -p %s' % dst_path) 404 utils.run('sudo mv %s %s' % (control_file, dst_path)) 405