• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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