Home | History | Annotate | Download | only in lxc
      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 
      5 import logging
      6 import os
      7 
      8 import common
      9 from autotest_lib.client.bin import utils
     10 from autotest_lib.client.common_lib import error
     11 from autotest_lib.site_utils.lxc import Container
     12 from autotest_lib.site_utils.lxc import constants
     13 from autotest_lib.site_utils.lxc import lxc
     14 from autotest_lib.site_utils.lxc import utils as lxc_utils
     15 
     16 
     17 class 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