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