• 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
5import logging
6import os
7import time
8
9import common
10from autotest_lib.client.bin import utils
11from autotest_lib.client.common_lib import error
12from autotest_lib.site_utils.lxc import Container
13from autotest_lib.site_utils.lxc import config as lxc_config
14from autotest_lib.site_utils.lxc import constants
15from autotest_lib.site_utils.lxc import lxc
16from autotest_lib.site_utils.lxc import utils as lxc_utils
17from autotest_lib.site_utils.lxc.cleanup_if_fail import cleanup_if_fail
18
19try:
20    from chromite.lib import metrics
21except ImportError:
22    metrics = utils.metrics_mock
23
24
25class ContainerBucket(object):
26    """A wrapper class to interact with containers in a specific container path.
27    """
28
29    def __init__(self,
30                 container_path=constants.DEFAULT_CONTAINER_PATH,
31                 shared_host_path = constants.DEFAULT_SHARED_HOST_PATH):
32        """Initialize a ContainerBucket.
33
34        @param container_path: Path to the directory used to store containers.
35                               Default is set to AUTOSERV/container_path in
36                               global config.
37        """
38        self.container_path = os.path.realpath(container_path)
39        self.shared_host_path = os.path.realpath(shared_host_path)
40        # Try to create the base container.
41        try:
42            base_container = Container.createFromExistingDir(
43                    container_path, constants.BASE);
44            base_container.refresh_status()
45            self.base_container = base_container
46        except error.ContainerError:
47            self.base_container = None
48
49
50    def get_all(self):
51        """Get details of all containers.
52
53        @return: A dictionary of all containers with detailed attributes,
54                 indexed by container name.
55        """
56        info_collection = lxc.get_container_info(self.container_path)
57        containers = {}
58        for info in info_collection:
59            container = Container.createFromExistingDir(self.container_path,
60                                                        **info)
61            containers[container.name] = container
62        return containers
63
64
65    def get(self, name):
66        """Get a container with matching name.
67
68        @param name: Name of the container.
69
70        @return: A container object with matching name. Returns None if no
71                 container matches the given name.
72        """
73        return self.get_all().get(name, None)
74
75
76    def exist(self, name):
77        """Check if a container exists with the given name.
78
79        @param name: Name of the container.
80
81        @return: True if the container with the given name exists, otherwise
82                 returns False.
83        """
84        return self.get(name) != None
85
86
87    def destroy_all(self):
88        """Destroy all containers, base must be destroyed at the last.
89        """
90        containers = self.get_all().values()
91        for container in sorted(
92            containers, key=lambda n: 1 if n.name == constants.BASE else 0):
93            logging.info('Destroy container %s.', container.name)
94            container.destroy()
95        self._cleanup_shared_host_path()
96
97
98    @metrics.SecondsTimerDecorator(
99        '%s/create_from_base_duration' % constants.STATS_KEY)
100    def create_from_base(self, name, disable_snapshot_clone=False,
101                         force_cleanup=False):
102        """Create a container from the base container.
103
104        @param name: Name of the container.
105        @param disable_snapshot_clone: Set to True to force to clone without
106                using snapshot clone even if the host supports that.
107        @param force_cleanup: Force to cleanup existing container.
108
109        @return: A Container object for the created container.
110
111        @raise ContainerError: If the container already exist.
112        @raise error.CmdError: If lxc-clone call failed for any reason.
113        """
114        if self.exist(name) and not force_cleanup:
115            raise error.ContainerError('Container %s already exists.' % name)
116
117        use_snapshot = (constants.SUPPORT_SNAPSHOT_CLONE and not
118                        disable_snapshot_clone)
119
120        try:
121            return Container.clone(src=self.base_container,
122                                   new_name=name,
123                                   new_path=self.container_path,
124                                   snapshot=use_snapshot,
125                                   cleanup=force_cleanup)
126        except error.CmdError:
127            logging.debug('Creating snapshot clone failed. Attempting without '
128                           'snapshot...')
129            if not use_snapshot:
130                raise
131            else:
132                # Snapshot clone failed, retry clone without snapshot.
133                container = Container.clone(src=self.base_container,
134                                            new_name=name,
135                                            new_path=self.container_path,
136                                            snapshot=False,
137                                            cleanup=force_cleanup)
138                return container
139
140
141    @cleanup_if_fail()
142    def setup_base(self, name=constants.BASE, force_delete=False):
143        """Setup base container.
144
145        @param name: Name of the base container, default to base.
146        @param force_delete: True to force to delete existing base container.
147                             This action will destroy all running test
148                             containers. Default is set to False.
149        """
150        if not self.container_path:
151            raise error.ContainerError(
152                    'You must set a valid directory to store containers in '
153                    'global config "AUTOSERV/ container_path".')
154
155        if not os.path.exists(self.container_path):
156            os.makedirs(self.container_path)
157
158        base_path = os.path.join(self.container_path, name)
159        if self.exist(name) and not force_delete:
160            logging.error(
161                    'Base container already exists. Set force_delete to True '
162                    'to force to re-stage base container. Note that this '
163                    'action will destroy all running test containers')
164            # Set proper file permission. base container in moblab may have
165            # owner of not being root. Force to update the folder's owner.
166            # TODO(dshi): Change root to current user when test container can be
167            # unprivileged container.
168            utils.run('sudo chown -R root "%s"' % base_path)
169            utils.run('sudo chgrp -R root "%s"' % base_path)
170            return
171
172        # Destroy existing base container if exists.
173        if self.exist(name):
174            # TODO: We may need to destroy all snapshots created from this base
175            # container, not all container.
176            self.destroy_all()
177
178        # Download and untar the base container.
179        tar_path = os.path.join(self.container_path, '%s.tar.xz' % name)
180        path_to_cleanup = [tar_path, base_path]
181        for path in path_to_cleanup:
182            if os.path.exists(path):
183                utils.run('sudo rm -rf "%s"' % path)
184        container_url = constants.CONTAINER_BASE_URL_FMT % name
185        lxc.download_extract(container_url, tar_path, self.container_path)
186        # Remove the downloaded container tar file.
187        utils.run('sudo rm "%s"' % tar_path)
188        # Set proper file permission.
189        # TODO(dshi): Change root to current user when test container can be
190        # unprivileged container.
191        utils.run('sudo chown -R root "%s"' % base_path)
192        utils.run('sudo chgrp -R root "%s"' % base_path)
193
194        # Update container config with container_path from global config.
195        config_path = os.path.join(base_path, 'config')
196        rootfs_path = os.path.join(base_path, 'rootfs')
197        utils.run(('sudo sed '
198                   '-i "s|\(lxc\.rootfs[[:space:]]*=\).*$|\\1 {rootfs}|" '
199                   '"{config}"').format(rootfs=rootfs_path,
200                                        config=config_path))
201
202        self.base_container = Container.createFromExistingDir(
203                self.container_path, name)
204
205        self._setup_shared_host_path()
206
207
208    def _setup_shared_host_path(self):
209        """Sets up the shared host directory."""
210        # First, clear out the old shared host dir if it exists.
211        if lxc_utils.path_exists(self.shared_host_path):
212            self._cleanup_shared_host_path()
213        # Create the dir and set it up as a shared mount point.
214        utils.run(('sudo mkdir "{path}" && '
215                   'sudo mount --bind "{path}" "{path}" && '
216                   'sudo mount --make-unbindable "{path}" && '
217                   'sudo mount --make-shared "{path}"')
218                  .format(path=self.shared_host_path))
219
220
221    def _cleanup_shared_host_path(self):
222        """Removes the shared host directory.
223
224        This should only be called after all containers have been destroyed
225        (i.e. all host mounts have been disconnected and removed, so the shared
226        host directory should be empty).
227        """
228        if not os.path.exists(self.shared_host_path):
229            return
230
231        if len(os.listdir(self.shared_host_path)) > 0:
232            raise RuntimeError('Attempting to clean up host dir before all '
233                               'hosts have been disconnected')
234        utils.run('sudo umount "{path}" && sudo rmdir "{path}"'
235                  .format(path=self.shared_host_path))
236
237
238    @metrics.SecondsTimerDecorator(
239        '%s/setup_test_duration' % constants.STATS_KEY)
240    @cleanup_if_fail()
241    def setup_test(self, name, job_id, server_package_url, result_path,
242                   control=None, skip_cleanup=False, job_folder=None,
243                   dut_name=None):
244        """Setup test container for the test job to run.
245
246        The setup includes:
247        1. Install autotest_server package from given url.
248        2. Copy over local shadow_config.ini.
249        3. Mount local site-packages.
250        4. Mount test result directory.
251
252        TODO(dshi): Setup also needs to include test control file for autoserv
253                    to run in container.
254
255        @param name: Name of the container.
256        @param job_id: Job id for the test job to run in the test container.
257        @param server_package_url: Url to download autotest_server package.
258        @param result_path: Directory to be mounted to container to store test
259                            results.
260        @param control: Path to the control file to run the test job. Default is
261                        set to None.
262        @param skip_cleanup: Set to True to skip cleanup, used to troubleshoot
263                             container failures.
264        @param job_folder: Folder name of the job, e.g., 123-debug_user.
265        @param dut_name: Name of the dut to run test, used as the hostname of
266                         the container. Default is None.
267        @return: A Container object for the test container.
268
269        @raise ContainerError: If container does not exist, or not running.
270        """
271        start_time = time.time()
272
273        if not os.path.exists(result_path):
274            raise error.ContainerError('Result directory does not exist: %s',
275                                       result_path)
276        result_path = os.path.abspath(result_path)
277
278        # Save control file to result_path temporarily. The reason is that the
279        # control file in drone_tmp folder can be deleted during scheduler
280        # restart. For test not using SSP, the window between test starts and
281        # control file being picked up by the test is very small (< 2 seconds).
282        # However, for tests using SSP, it takes around 1 minute before the
283        # container is setup. If scheduler is restarted during that period, the
284        # control file will be deleted, and the test will fail.
285        if control:
286            control_file_name = os.path.basename(control)
287            safe_control = os.path.join(result_path, control_file_name)
288            utils.run('cp %s %s' % (control, safe_control))
289
290        # Create test container from the base container.
291        container = self.create_from_base(name)
292
293        # Update the hostname of the test container to be `dut-name`.
294        # Some TradeFed tests use hostname in test results, which is used to
295        # group test results in dashboard. The default container name is set to
296        # be the name of the folder, which is unique (as it is composed of job
297        # id and timestamp. For better result view, the container's hostname is
298        # set to be a string containing the dut hostname.
299        if dut_name:
300            container.set_hostname(dut_name.replace('.', '-'))
301
302        # Deploy server side package
303        container.install_ssp(server_package_url)
304
305        deploy_config_manager = lxc_config.DeployConfigManager(container)
306        deploy_config_manager.deploy_pre_start()
307
308        # Copy over control file to run the test job.
309        if control:
310            container.install_control_file(safe_control)
311
312        mount_entries = [(constants.SITE_PACKAGES_PATH,
313                          constants.CONTAINER_SITE_PACKAGES_PATH,
314                          True),
315                         (os.path.join(common.autotest_dir, 'puppylab'),
316                          os.path.join(constants.CONTAINER_AUTOTEST_DIR,
317                                       'puppylab'),
318                          True),
319                         (result_path,
320                          os.path.join(constants.RESULT_DIR_FMT % job_folder),
321                          False),
322                        ]
323
324        # Update container config to mount directories.
325        for source, destination, readonly in mount_entries:
326            container.mount_dir(source, destination, readonly)
327
328        # Update file permissions.
329        # TODO(dshi): crbug.com/459344 Skip following action when test container
330        # can be unprivileged container.
331        autotest_path = os.path.join(
332                container.rootfs,
333                constants.CONTAINER_AUTOTEST_DIR.lstrip(os.path.sep))
334        utils.run('sudo chown -R root "%s"' % autotest_path)
335        utils.run('sudo chgrp -R root "%s"' % autotest_path)
336
337        container.start(name)
338        deploy_config_manager.deploy_post_start()
339
340        container.modify_import_order()
341
342        container.verify_autotest_setup(job_folder)
343
344        logging.debug('Test container %s is set up.', name)
345        return container
346