1# Copyright 2017 The Chromium OS 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 5from __future__ import absolute_import 6from __future__ import division 7from __future__ import print_function 8 9import logging 10import os 11import sys 12 13import common 14from autotest_lib.client.bin import utils 15from autotest_lib.client.common_lib import error 16from autotest_lib.site_utils.lxc import constants 17from autotest_lib.site_utils.lxc import lxc 18from autotest_lib.site_utils.lxc import utils as lxc_utils 19from autotest_lib.site_utils.lxc.container import Container 20import six 21from six.moves import range 22 23 24class BaseImage(object): 25 """A class that manages a base container. 26 27 Instantiating this class will cause it to search for a base container under 28 the given path and name. If one is found, the class adopts it. If not, the 29 setup() method needs to be called, to download and install a new base 30 container. 31 32 The actual base container can be obtained by calling the get() method. 33 34 Calling cleanup() will delete the base container along with all of its 35 associated snapshot clones. 36 """ 37 38 def __init__(self, container_path, base_name): 39 """Creates a new BaseImage. 40 41 If a valid base container already exists on this machine, the BaseImage 42 adopts it. Otherwise, setup needs to be called to download a base and 43 install a base container. 44 45 @param container_path: The LXC path for the base container. 46 @param base_name: The base container name. 47 """ 48 self.container_path = container_path 49 self.base_name = base_name 50 try: 51 base_container = Container.create_from_existing_dir( 52 container_path, base_name) 53 base_container.refresh_status() 54 self.base_container = base_container 55 except error.ContainerError as e: 56 self.base_container = None 57 self.base_container_error = e 58 59 def setup(self, name=None, force_delete=False): 60 """Download and setup the base container. 61 62 @param name: Name of the base container, defaults to the name passed to 63 the constructor. If a different name is provided, that 64 name overrides the name originally passed to the 65 constructor. 66 @param force_delete: True to force to delete existing base container. 67 This action will destroy all running test 68 containers. Default is set to False. 69 """ 70 if name is not None: 71 self.base_name = name 72 73 if not self.container_path: 74 raise error.ContainerError( 75 'You must set a valid directory to store containers in ' 76 'global config "AUTOSERV/ container_path".') 77 78 if not os.path.exists(self.container_path): 79 os.makedirs(self.container_path) 80 81 if self.base_container and not force_delete: 82 logging.error( 83 'Base container already exists. Set force_delete to True ' 84 'to force to re-stage base container. Note that this ' 85 'action will destroy all running test containers') 86 # Set proper file permission. base container in moblab may have 87 # owner of not being root. Force to update the folder's owner. 88 self._set_root_owner() 89 return 90 91 # Destroy existing base container if exists. 92 if self.base_container: 93 self.cleanup() 94 95 try: 96 self._download_and_install_base_container() 97 self._set_root_owner() 98 except: 99 # Clean up if something went wrong. 100 base_path = os.path.join(self.container_path, self.base_name) 101 if lxc_utils.path_exists(base_path): 102 exc_info = sys.exc_info() 103 container = Container.create_from_existing_dir( 104 self.container_path, self.base_name) 105 # Attempt destroy. Log but otherwise ignore errors. 106 try: 107 container.destroy() 108 except error.CmdError as e: 109 logging.error(e) 110 # Raise the cached exception with original backtrace. 111 six.reraise(exc_info[0], exc_info[1], exc_info[2]) 112 else: 113 raise 114 else: 115 self.base_container = Container.create_from_existing_dir( 116 self.container_path, self.base_name) 117 118 def cleanup(self): 119 """Destroys the base container. 120 121 This operation will also destroy all snapshot clones of the base 122 container. 123 """ 124 # Find and delete clones first. 125 for clone in self._find_clones(): 126 clone.destroy() 127 base = Container.create_from_existing_dir(self.container_path, 128 self.base_name) 129 base.destroy() 130 131 def get(self): 132 """Returns the base container. 133 134 @raise ContainerError: If the base image is invalid or missing. 135 """ 136 if self.base_container is None: 137 raise self.base_container_error 138 else: 139 return self.base_container 140 141 def _download_and_install_base_container(self): 142 """Downloads the base image, untars and configures it.""" 143 base_path = os.path.join(self.container_path, self.base_name) 144 tar_path = os.path.join(self.container_path, 145 '%s.tar.xz' % self.base_name) 146 147 # Force cleanup of any previously downloaded/installed base containers. 148 # This ensures a clean setup of the new base container. 149 # 150 # TODO(kenobi): Add a check to ensure that the base container doesn't 151 # get deleted while snapshot clones exist (otherwise running tests might 152 # get disrupted). 153 path_to_cleanup = [tar_path, base_path] 154 for path in path_to_cleanup: 155 if os.path.exists(path): 156 utils.run('sudo rm -rf "%s"' % path) 157 container_url = constants.CONTAINER_BASE_URL_FMT % self.base_name 158 lxc.download_extract(container_url, tar_path, self.container_path) 159 # Remove the downloaded container tar file. 160 utils.run('sudo rm "%s"' % tar_path) 161 162 # Update container config with container_path from global config. 163 config_path = os.path.join(base_path, 'config') 164 rootfs_path = os.path.join(base_path, 'rootfs') 165 utils.run(('sudo sed ' 166 '-i "s|\(lxc\.rootfs[[:space:]]*=\).*$|\\1 {rootfs}|" ' 167 '"{config}"').format(rootfs=rootfs_path, 168 config=config_path)) 169 170 def _set_root_owner(self): 171 """Changes the container group and owner to root. 172 173 This is necessary because we currently run privileged containers. 174 """ 175 # TODO(dshi): Change root to current user when test container can be 176 # unprivileged container. 177 base_path = os.path.join(self.container_path, self.base_name) 178 utils.run('sudo chown -R root "%s"' % base_path) 179 utils.run('sudo chgrp -R root "%s"' % base_path) 180 181 def _find_clones(self): 182 """Finds snapshot clones of the current base container.""" 183 snapshot_file = os.path.join(self.container_path, 184 self.base_name, 185 'lxc_snapshots') 186 if not lxc_utils.path_exists(snapshot_file): 187 return 188 cmd = 'sudo cat %s' % snapshot_file 189 clone_info = [line.strip() 190 for line in utils.run(cmd).stdout.splitlines()] 191 # lxc_snapshots contains pairs of lines (lxc_path, container_name). 192 for i in range(0, len(clone_info), 2): 193 lxc_path = clone_info[i] 194 name = clone_info[i+1] 195 yield Container.create_from_existing_dir(lxc_path, name) 196