xref: /aosp_15_r20/external/autotest/site_utils/lxc/base_image.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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