| # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import logging |
| import os |
| import sys |
| |
| import common |
| from autotest_lib.client.bin import utils |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.site_utils.lxc import constants |
| from autotest_lib.site_utils.lxc import lxc |
| from autotest_lib.site_utils.lxc import utils as lxc_utils |
| from autotest_lib.site_utils.lxc.container import Container |
| |
| |
| class BaseImage(object): |
| """A class that manages a base container. |
| |
| Instantiating this class will cause it to search for a base container under |
| the given path and name. If one is found, the class adopts it. If not, the |
| setup() method needs to be called, to download and install a new base |
| container. |
| |
| The actual base container can be obtained by calling the get() method. |
| |
| Calling cleanup() will delete the base container along with all of its |
| associated snapshot clones. |
| """ |
| |
| def __init__(self, |
| container_path=constants.DEFAULT_CONTAINER_PATH, |
| base_name=constants.BASE): |
| """Creates a new BaseImage. |
| |
| If a valid base container already exists on this machine, the BaseImage |
| adopts it. Otherwise, setup needs to be called to download a base and |
| install a base container. |
| |
| @param container_path: The LXC path for the base container. |
| @param base_name: The base container name. |
| """ |
| self.container_path = container_path |
| self.base_name = base_name |
| try: |
| base_container = Container.create_from_existing_dir( |
| container_path, base_name); |
| base_container.refresh_status() |
| self.base_container = base_container |
| except error.ContainerError as e: |
| self.base_container = None |
| self.base_container_error = e |
| |
| |
| def setup(self, name=None, force_delete=False): |
| """Download and setup the base container. |
| |
| @param name: Name of the base container, defaults to the name passed to |
| the constructor. If a different name is provided, that |
| name overrides the name originally passed to the |
| constructor. |
| @param force_delete: True to force to delete existing base container. |
| This action will destroy all running test |
| containers. Default is set to False. |
| """ |
| if name is not None: |
| self.base_name = name |
| |
| if not self.container_path: |
| raise error.ContainerError( |
| 'You must set a valid directory to store containers in ' |
| 'global config "AUTOSERV/ container_path".') |
| |
| if not os.path.exists(self.container_path): |
| os.makedirs(self.container_path) |
| |
| if self.base_container and not force_delete: |
| logging.error( |
| 'Base container already exists. Set force_delete to True ' |
| 'to force to re-stage base container. Note that this ' |
| 'action will destroy all running test containers') |
| # Set proper file permission. base container in moblab may have |
| # owner of not being root. Force to update the folder's owner. |
| self._set_root_owner() |
| return |
| |
| # Destroy existing base container if exists. |
| if self.base_container: |
| self.cleanup() |
| |
| try: |
| self._download_and_install_base_container() |
| self._set_root_owner() |
| except: |
| # Clean up if something went wrong. |
| base_path = os.path.join(self.container_path, self.base_name) |
| if lxc_utils.path_exists(base_path): |
| exc_info = sys.exc_info() |
| container = Container.create_from_existing_dir( |
| self.container_path, self.base_name) |
| # Attempt destroy. Log but otherwise ignore errors. |
| try: |
| container.destroy() |
| except error.CmdError as e: |
| logging.error(e) |
| # Raise the cached exception with original backtrace. |
| raise exc_info[0], exc_info[1], exc_info[2] |
| else: |
| raise |
| else: |
| self.base_container = Container.create_from_existing_dir( |
| self.container_path, self.base_name) |
| |
| |
| def cleanup(self): |
| """Destroys the base container. |
| |
| This operation will also destroy all snapshot clones of the base |
| container. |
| """ |
| # Find and delete clones first. |
| for clone in self._find_clones(): |
| clone.destroy() |
| base = Container.create_from_existing_dir(self.container_path, |
| self.base_name) |
| base.destroy() |
| |
| |
| def get(self): |
| """Returns the base container. |
| |
| @raise ContainerError: If the base image is invalid or missing. |
| """ |
| if self.base_container is None: |
| raise self.base_container_error |
| else: |
| return self.base_container |
| |
| |
| def _download_and_install_base_container(self): |
| """Downloads the base image, untars and configures it.""" |
| base_path = os.path.join(self.container_path, self.base_name) |
| tar_path = os.path.join(self.container_path, |
| '%s.tar.xz' % self.base_name) |
| |
| # Force cleanup of any previously downloaded/installed base containers. |
| # This ensures a clean setup of the new base container. |
| # |
| # TODO(kenobi): Add a check to ensure that the base container doesn't |
| # get deleted while snapshot clones exist (otherwise running tests might |
| # get disrupted). |
| path_to_cleanup = [tar_path, base_path] |
| for path in path_to_cleanup: |
| if os.path.exists(path): |
| utils.run('sudo rm -rf "%s"' % path) |
| container_url = constants.CONTAINER_BASE_URL_FMT % self.base_name |
| lxc.download_extract(container_url, tar_path, self.container_path) |
| # Remove the downloaded container tar file. |
| utils.run('sudo rm "%s"' % tar_path) |
| |
| # Update container config with container_path from global config. |
| config_path = os.path.join(base_path, 'config') |
| rootfs_path = os.path.join(base_path, 'rootfs') |
| utils.run(('sudo sed ' |
| '-i "s|\(lxc\.rootfs[[:space:]]*=\).*$|\\1 {rootfs}|" ' |
| '"{config}"').format(rootfs=rootfs_path, |
| config=config_path)) |
| |
| def _set_root_owner(self): |
| """Changes the container group and owner to root. |
| |
| This is necessary because we currently run privileged containers. |
| """ |
| # TODO(dshi): Change root to current user when test container can be |
| # unprivileged container. |
| base_path = os.path.join(self.container_path, self.base_name) |
| utils.run('sudo chown -R root "%s"' % base_path) |
| utils.run('sudo chgrp -R root "%s"' % base_path) |
| |
| |
| def _find_clones(self): |
| """Finds snapshot clones of the current base container.""" |
| snapshot_file = os.path.join(self.container_path, |
| self.base_name, |
| 'lxc_snapshots') |
| if not lxc_utils.path_exists(snapshot_file): |
| return |
| cmd = 'sudo cat %s' % snapshot_file |
| clone_info = [line.strip() |
| for line in utils.run(cmd).stdout.splitlines()] |
| # lxc_snapshots contains pairs of lines (lxc_path, container_name). |
| for i in range(0, len(clone_info), 2): |
| lxc_path = clone_info[i] |
| name = clone_info[i+1] |
| yield Container.create_from_existing_dir(lxc_path, name) |