1# Copyright 2017 The Chromium 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
5import logging
6import os
7
8import common
9from autotest_lib.client.bin import utils
10from autotest_lib.client.common_lib import error
11from autotest_lib.site_utils.lxc import Container
12from autotest_lib.site_utils.lxc import constants
13from autotest_lib.site_utils.lxc import lxc
14from autotest_lib.site_utils.lxc import utils as lxc_utils
15
16
17class Zygote(Container):
18    """A Container that implements post-bringup configuration.
19    """
20
21    def __init__(self, container_path, name, attribute_values, src=None,
22                 snapshot=False, host_path=None):
23        """Initialize an object of LXC container with given attribute values.
24
25        @param container_path: Directory that stores the container.
26        @param name: Name of the container.
27        @param attribute_values: A dictionary of attribute values for the
28                                 container.
29        @param src: An optional source container.  If provided, the source
30                    continer is cloned, and the new container will point to the
31                    clone.
32        @param snapshot: Whether or not to create a snapshot clone.  By default,
33                         this is false.  If a snapshot is requested and creating
34                         a snapshot clone fails, a full clone will be attempted.
35        @param host_path: If set to None (the default), a host path will be
36                          generated based on constants.DEFAULT_SHARED_HOST_PATH.
37                          Otherwise, this can be used to override the host path
38                          of the new container, for testing purposes.
39        """
40        # Check if this is a pre-existing LXC container.  Do this before calling
41        # the super ctor, because that triggers container creation.
42        exists = lxc.get_container_info(container_path, name=name)
43
44        super(Zygote, self).__init__(container_path, name, attribute_values,
45                                     src, snapshot)
46
47        logging.debug(
48                'Creating Zygote (lxcpath:%s name:%s)', container_path, name)
49
50        # host_path is a directory within a shared bind-mount, which enables
51        # bind-mounts from the host system to be shared with the LXC container.
52        if host_path is not None:
53            # Allow the host_path to be injected, for testing.
54            self.host_path = host_path
55        else:
56            if exists:
57                # Pre-existing Zygotes must have a host path.
58                self.host_path = self._find_existing_host_dir()
59                if self.host_path is None:
60                    raise error.ContainerError(
61                            'Container %s has no host path.' %
62                            os.path.join(container_path, name))
63            else:
64                # New Zygotes use a predefined template to generate a host path.
65                self.host_path = os.path.join(
66                        os.path.realpath(constants.DEFAULT_SHARED_HOST_PATH),
67                        self.name)
68
69        # host_path_ro is a directory for holding intermediate mount points,
70        # which are necessary when creating read-only bind mounts.  See the
71        # mount_dir method for more details.
72        #
73        # Generate a host_path_ro based on host_path.
74        ro_dir, ro_name = os.path.split(self.host_path.rstrip(os.path.sep))
75        self.host_path_ro = os.path.join(ro_dir, '%s.ro' % ro_name)
76
77        # Remember mounts so they can be cleaned up in destroy.
78        self.mounts = []
79
80        if exists:
81            self._find_existing_bind_mounts()
82        else:
83            # Creating a new Zygote - initialize the host dirs.  Don't use sudo,
84            # so that the resulting directories can be accessed by autoserv (for
85            # SSP installation, etc).
86            if not lxc_utils.path_exists(self.host_path):
87                os.makedirs(self.host_path)
88            if not lxc_utils.path_exists(self.host_path_ro):
89                os.makedirs(self.host_path_ro)
90
91            # Create the mount point within the container's rootfs.
92            # Changes within container's rootfs require sudo.
93            utils.run('sudo mkdir %s' %
94                      os.path.join(self.rootfs,
95                                   constants.CONTAINER_HOST_DIR.lstrip(
96                                           os.path.sep)))
97            self.mount_dir(self.host_path, constants.CONTAINER_HOST_DIR)
98
99
100    def destroy(self, force=True):
101        """Destroy the Zygote.
102
103        This destroys the underlying container (see Container.destroy) and also
104        cleans up any host mounts associated with it.
105
106        @param force: Force container destruction even if it's running.  See
107                      Container.destroy.
108        """
109        logging.debug('Destroying Zygote %s', self.name)
110        super(Zygote, self).destroy(force)
111        self._cleanup_host_mount()
112
113
114    def install_ssp(self, ssp_url):
115        """Downloads and installs the given server package.
116
117        @param ssp_url: The URL of the ssp to download and install.
118        """
119        # The host dir is mounted directly on /usr/local/autotest within the
120        # container.  The SSP structure assumes it gets untarred into the
121        # /usr/local directory of the container's rootfs.  In order to unpack
122        # with the correct directory structure, create a tmpdir, mount the
123        # container's host dir as ./autotest, and unpack the SSP.
124        if not self.is_running():
125            super(Zygote, self).install_ssp(ssp_url)
126            return
127
128        usr_local_path = os.path.join(self.host_path, 'usr', 'local')
129        os.makedirs(usr_local_path)
130
131        with lxc_utils.TempDir(dir=usr_local_path) as tmpdir:
132            download_tmp = os.path.join(tmpdir,
133                                        'autotest_server_package.tar.bz2')
134            lxc.download_extract(ssp_url, download_tmp, usr_local_path)
135
136        container_ssp_path = os.path.join(
137                constants.CONTAINER_HOST_DIR,
138                constants.CONTAINER_AUTOTEST_DIR.lstrip(os.path.sep))
139        self.attach_run('mkdir -p %s && mount --bind %s %s' %
140                        (constants.CONTAINER_AUTOTEST_DIR,
141                         container_ssp_path,
142                         constants.CONTAINER_AUTOTEST_DIR))
143
144
145    def copy(self, host_path, container_path):
146        """Copies files into the Zygote.
147
148        @param host_path: Path to the source file/dir to be copied.
149        @param container_path: Path to the destination dir (in the container).
150        """
151        if not self.is_running():
152            return super(Zygote, self).copy(host_path, container_path)
153
154        logging.debug('copy %s to %s', host_path, container_path)
155
156        # First copy the files into the host mount, then move them from within
157        # the container.
158        self._do_copy(src=host_path,
159                      dst=os.path.join(self.host_path,
160                                       container_path.lstrip(os.path.sep)))
161
162        src = os.path.join(constants.CONTAINER_HOST_DIR,
163                           container_path.lstrip(os.path.sep))
164        dst = container_path
165
166        # In the container, bind-mount from host path to destination.
167        # The mount destination must have the correct type (file vs dir).
168        if os.path.isdir(host_path):
169            self.attach_run('mkdir -p %s' % dst)
170        else:
171            self.attach_run(
172                'mkdir -p %s && touch %s' % (os.path.dirname(dst), dst))
173        self.attach_run('mount --bind %s %s' % (src, dst))
174
175
176    def mount_dir(self, source, destination, readonly=False):
177        """Mount a directory in host to a directory in the container.
178
179        @param source: Directory in host to be mounted.
180        @param destination: Directory in container to mount the source directory
181        @param readonly: Set to True to make a readonly mount, default is False.
182        """
183        if not self.is_running():
184            return super(Zygote, self).mount_dir(source, destination, readonly)
185
186        # Destination path in container must be absolute.
187        if not os.path.isabs(destination):
188            destination = os.path.join('/', destination)
189
190        # Create directory in container for mount.
191        self.attach_run('mkdir -p %s' % destination)
192
193        # Creating read-only shared bind mounts is a two-stage process.  First,
194        # the original file/directory is bind-mounted (with the ro option) to an
195        # intermediate location in self.host_path_ro.  Then, the intermediate
196        # location is bind-mounted into the shared host dir.
197        # Replace the original source with this intermediate read-only mount,
198        # then continue.
199        if readonly:
200            source_ro = os.path.join(self.host_path_ro,
201                                     source.lstrip(os.path.sep))
202            self.mounts.append(lxc_utils.BindMount.create(
203                    source, self.host_path_ro, readonly=True))
204            source = source_ro
205
206        # Mount the directory into the host dir, then from the host dir into the
207        # destination.
208        self.mounts.append(
209                lxc_utils.BindMount.create(source, self.host_path, destination))
210
211        container_host_path = os.path.join(constants.CONTAINER_HOST_DIR,
212                                           destination.lstrip(os.path.sep))
213        self.attach_run('mount --bind %s %s' %
214                        (container_host_path, destination))
215
216
217    def _cleanup_host_mount(self):
218        """Unmounts and removes the host dirs for this container."""
219        # Clean up all intermediate bind mounts into host_path and host_path_ro.
220        for mount in self.mounts:
221            mount.cleanup()
222        # The SSP and other "real" content gets copied into the host dir.  Use
223        # rm -r to clear it out.
224        if lxc_utils.path_exists(self.host_path):
225            utils.run('sudo rm -r "%s"' % self.host_path)
226        # The host_path_ro directory only contains intermediate bind points,
227        # which should all have been cleared out.  Use rmdir.
228        if lxc_utils.path_exists(self.host_path_ro):
229            utils.run('sudo rmdir "%s"' % self.host_path_ro)
230
231
232    def _find_existing_host_dir(self):
233        """Finds the host mounts for a pre-existing Zygote.
234
235        The host directory is passed into the Zygote constructor when creating a
236        new Zygote.  However, when a Zygote is instantiated on top of an already
237        existing LXC container, it has to reconnect to the existing host
238        directory.
239
240        @return: The host-side path to the host dir.
241        """
242        # Look for the mount that targets the "/host" dir within the container.
243        for mount in self._get_lxc_config('lxc.mount.entry'):
244            mount_cfg = mount.split(' ')
245            if mount_cfg[1] == 'host':
246                return mount_cfg[0]
247        return None
248
249
250    def _find_existing_bind_mounts(self):
251        """Locates bind mounts associated with an existing container.
252
253        When a Zygote object is instantiated on top of an existing LXC
254        container, this method needs to be called so that all the bind-mounts
255        associated with the container can be reconstructed.  This enables proper
256        cleanup later.
257        """
258        for info in utils.get_mount_info():
259            # Check for bind mounts in the host and host_ro directories, and
260            # re-add them to self.mounts.
261            if lxc_utils.is_subdir(self.host_path, info.mount_point):
262                logging.debug('mount: %s', info.mount_point)
263                self.mounts.append(lxc_utils.BindMount.from_existing(
264                        self.host_path, info.mount_point))
265            elif lxc_utils.is_subdir(self.host_path_ro, info.mount_point):
266                logging.debug('mount_ro: %s', info.mount_point)
267                self.mounts.append(lxc_utils.BindMount.from_existing(
268                        self.host_path_ro, info.mount_point))
269