Home | History | Annotate | Download | only in lxc
      1 # Copyright 2015 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 collections
      6 import json
      7 import logging
      8 import os
      9 import re
     10 import tempfile
     11 import time
     12 
     13 import common
     14 from autotest_lib.client.bin import utils
     15 from autotest_lib.client.common_lib import error
     16 from autotest_lib.site_utils.lxc import constants
     17 from autotest_lib.site_utils.lxc import lxc
     18 from autotest_lib.site_utils.lxc import utils as lxc_utils
     19 
     20 try:
     21     from chromite.lib import metrics
     22 except ImportError:
     23     metrics = utils.metrics_mock
     24 
     25 from chromite.lib import constants as chromite_constants
     26 
     27 # Naming convention of test container, e.g., test_300_1422862512_2424, where:
     28 # 300:        The test job ID.
     29 # 1422862512: The tick when container is created.
     30 # 2424:       The PID of autoserv that starts the container.
     31 _TEST_CONTAINER_NAME_FMT = 'test_%s_%d_%d'
     32 # Name of the container ID file.
     33 _CONTAINER_ID_FILENAME = 'container_id.json'
     34 
     35 
     36 class ContainerId(collections.namedtuple('ContainerId',
     37                                          ['job_id', 'creation_time', 'pid'])):
     38     """An identifier for containers."""
     39 
     40     # Optimization.  Avoids __dict__ creation.  Empty because this subclass has
     41     # no instance vars of its own.
     42     __slots__ = ()
     43 
     44 
     45     def __str__(self):
     46         return _TEST_CONTAINER_NAME_FMT % self
     47 
     48 
     49     def save(self, path):
     50         """Saves the ID to the given path.
     51 
     52         @param path: Path to a directory where the container ID will be
     53                      serialized.
     54         """
     55         dst = os.path.join(path, _CONTAINER_ID_FILENAME)
     56         with open(dst, 'w') as f:
     57             json.dump(self, f)
     58 
     59     @classmethod
     60     def load(cls, path):
     61         """Reads the ID from the given path.
     62 
     63         @param path: Path to check for a serialized container ID.
     64 
     65         @return: A container ID if one is found on the given path, or None
     66                  otherwise.
     67 
     68         @raise ValueError: If a JSON load error occurred.
     69         @raise TypeError: If the file was valid JSON but didn't contain a valid
     70                           ContainerId.
     71         """
     72         src = os.path.join(path, _CONTAINER_ID_FILENAME)
     73 
     74         try:
     75             with open(src, 'r') as f:
     76                 job_id, ctime, pid = json.load(f)
     77         except IOError:
     78             # File not found, or couldn't be opened for some other reason.
     79             # Treat all these cases as no ID.
     80             return None
     81         # TODO(pprabhu, crbug.com/842343) Remove this once all persistent
     82         # container ids have migrated to str.
     83         job_id = str(job_id)
     84         return cls(job_id, ctime, pid)
     85 
     86 
     87     @classmethod
     88     def create(cls, job_id, ctime=None, pid=None):
     89         """Creates a new container ID.
     90 
     91         @param job_id: The first field in the ID.
     92         @param ctime: The second field in the ID.  Optional. If not provided,
     93                       the current epoch timestamp is used.
     94         @param pid: The third field in the ID.  Optional.  If not provided, the
     95                     PID of the current process is used.
     96         """
     97         if ctime is None:
     98             ctime = int(time.time())
     99         if pid is None:
    100             pid = os.getpid()
    101         # TODO(pprabhu) Drop str() cast once
    102         # job_directories.get_job_id_or_task_id() starts returning str directly.
    103         return cls(str(job_id), ctime, pid)
    104 
    105 
    106 class Container(object):
    107     """A wrapper class of an LXC container.
    108 
    109     The wrapper class provides methods to interact with a container, e.g.,
    110     start, stop, destroy, run a command. It also has attributes of the
    111     container, including:
    112     name: Name of the container.
    113     state: State of the container, e.g., ABORTING, RUNNING, STARTING, STOPPED,
    114            or STOPPING.
    115 
    116     lxc-ls can also collect other attributes of a container including:
    117     ipv4: IP address for IPv4.
    118     ipv6: IP address for IPv6.
    119     autostart: If the container will autostart at system boot.
    120     pid: Process ID of the container.
    121     memory: Memory used by the container, as a string, e.g., "6.2MB"
    122     ram: Physical ram used by the container, as a string, e.g., "6.2MB"
    123     swap: swap used by the container, as a string, e.g., "1.0MB"
    124 
    125     For performance reason, such info is not collected for now.
    126 
    127     The attributes available are defined in ATTRIBUTES constant.
    128     """
    129 
    130     _LXC_VERSION = None
    131 
    132     def __init__(self, container_path, name, attribute_values, src=None,
    133                  snapshot=False):
    134         """Initialize an object of LXC container with given attribute values.
    135 
    136         @param container_path: Directory that stores the container.
    137         @param name: Name of the container.
    138         @param attribute_values: A dictionary of attribute values for the
    139                                  container.
    140         @param src: An optional source container.  If provided, the source
    141                     continer is cloned, and the new container will point to the
    142                     clone.
    143         @param snapshot: If a source container was specified, this argument
    144                          specifies whether or not to create a snapshot clone.
    145                          The default is to attempt to create a snapshot.
    146                          If a snapshot is requested and creating the snapshot
    147                          fails, a full clone will be attempted.
    148         """
    149         self.container_path = os.path.realpath(container_path)
    150         # Path to the rootfs of the container. This will be initialized when
    151         # property rootfs is retrieved.
    152         self._rootfs = None
    153         self.name = name
    154         for attribute, value in attribute_values.iteritems():
    155             setattr(self, attribute, value)
    156 
    157         # Clone the container
    158         if src is not None:
    159             # Clone the source container to initialize this one.
    160             lxc_utils.clone(src.container_path, src.name, self.container_path,
    161                             self.name, snapshot)
    162             # Newly cloned containers have no ID.
    163             self._id = None
    164         else:
    165             # This may be an existing container.  Try to read the ID.
    166             try:
    167                 self._id = ContainerId.load(
    168                         os.path.join(self.container_path, self.name))
    169             except (ValueError, TypeError):
    170                 # Ignore load errors.  ContainerBucket currently queries every
    171                 # container quite frequently, and emitting exceptions here would
    172                 # cause any invalid containers on a server to block all
    173                 # ContainerBucket.get_all calls (see crbug/783865).
    174                 # TODO(kenobi): Containers with invalid ID files are probably
    175                 # the result of an aborted or failed operation.  There is a
    176                 # non-zero chance that such containers would contain leftover
    177                 # state, or themselves be corrupted or invalid.  Should we
    178                 # provide APIs for checking if a container is in this state?
    179                 logging.exception('Error loading ID for container %s:',
    180                                   self.name)
    181                 self._id = None
    182 
    183         if not Container._LXC_VERSION:
    184           Container._LXC_VERSION = lxc_utils.get_lxc_version()
    185 
    186 
    187     @classmethod
    188     def create_from_existing_dir(cls, lxc_path, name, **kwargs):
    189         """Creates a new container instance for an lxc container that already
    190         exists on disk.
    191 
    192         @param lxc_path: The LXC path for the container.
    193         @param name: The container name.
    194 
    195         @raise error.ContainerError: If the container doesn't already exist.
    196 
    197         @return: The new container.
    198         """
    199         return cls(lxc_path, name, kwargs)
    200 
    201 
    202     # Containers have a name and an ID.  The name is simply the name of the LXC
    203     # container.  The ID is the actual key that is used to identify the
    204     # container to the autoserv system.  In the case of a JIT-created container,
    205     # we have the ID at the container's creation time so we use that to name the
    206     # container.  This may not be the case for other types of containers.
    207     @classmethod
    208     def clone(cls, src, new_name=None, new_path=None, snapshot=False,
    209               cleanup=False):
    210         """Creates a clone of this container.
    211 
    212         @param src: The original container.
    213         @param new_name: Name for the cloned container.  If this is not
    214                          provided, a random unique container name will be
    215                          generated.
    216         @param new_path: LXC path for the cloned container (optional; if not
    217                          specified, the new container is created in the same
    218                          directory as the source container).
    219         @param snapshot: Whether to snapshot, or create a full clone.  Note that
    220                          snapshot cloning is not supported on all platforms.  If
    221                          this code is running on a platform that does not
    222                          support snapshot clones, this flag is ignored.
    223         @param cleanup: If a container with the given name and path already
    224                         exist, clean it up first.
    225         """
    226         if new_path is None:
    227             new_path = src.container_path
    228 
    229         if new_name is None:
    230             _, new_name = os.path.split(
    231                 tempfile.mkdtemp(dir=new_path, prefix='container.'))
    232             logging.debug('Generating new name for container: %s', new_name)
    233         else:
    234             # If a container exists at this location, clean it up first
    235             container_folder = os.path.join(new_path, new_name)
    236             if lxc_utils.path_exists(container_folder):
    237                 if not cleanup:
    238                     raise error.ContainerError('Container %s already exists.' %
    239                                                new_name)
    240                 container = Container.create_from_existing_dir(new_path,
    241                                                                new_name)
    242                 try:
    243                     container.destroy()
    244                 except error.CmdError as e:
    245                     # The container could be created in a incompleted
    246                     # state. Delete the container folder instead.
    247                     logging.warn('Failed to destroy container %s, error: %s',
    248                                  new_name, e)
    249                     utils.run('sudo rm -rf "%s"' % container_folder)
    250             # Create the directory prior to creating the new container.  This
    251             # puts the ownership of the container under the current process's
    252             # user, rather than root.  This is necessary to enable the
    253             # ContainerId to serialize properly.
    254             os.mkdir(container_folder)
    255 
    256         # Create and return the new container.
    257         new_container = cls(new_path, new_name, {}, src, snapshot)
    258 
    259         return new_container
    260 
    261 
    262     def refresh_status(self):
    263         """Refresh the status information of the container.
    264         """
    265         containers = lxc.get_container_info(self.container_path, name=self.name)
    266         if not containers:
    267             raise error.ContainerError(
    268                     'No container found in directory %s with name of %s.' %
    269                     (self.container_path, self.name))
    270         attribute_values = containers[0]
    271         for attribute, value in attribute_values.iteritems():
    272             setattr(self, attribute, value)
    273 
    274 
    275     @property
    276     def rootfs(self):
    277         """Path to the rootfs of the container.
    278 
    279         This property returns the path to the rootfs of the container, that is,
    280         the folder where the container stores its local files. It reads the
    281         attribute lxc.rootfs from the config file of the container, e.g.,
    282             lxc.rootfs = /usr/local/autotest/containers/t4/rootfs
    283         If the container is created with snapshot, the rootfs is a chain of
    284         folders, separated by `:` and ordered by how the snapshot is created,
    285         e.g.,
    286             lxc.rootfs = overlayfs:/usr/local/autotest/containers/base/rootfs:
    287             /usr/local/autotest/containers/t4_s/delta0
    288         This function returns the last folder in the chain, in above example,
    289         that is `/usr/local/autotest/containers/t4_s/delta0`
    290 
    291         Files in the rootfs will be accessible directly within container. For
    292         example, a folder in host "[rootfs]/usr/local/file1", can be accessed
    293         inside container by path "/usr/local/file1". Note that symlink in the
    294         host can not across host/container boundary, instead, directory mount
    295         should be used, refer to function mount_dir.
    296 
    297         @return: Path to the rootfs of the container.
    298         """
    299         lxc_rootfs_config_name = 'lxc.rootfs'
    300         # Check to see if the major lxc version is 3 or greater
    301         if Container._LXC_VERSION:
    302             logging.info("Detected lxc version %s", Container._LXC_VERSION)
    303             if Container._LXC_VERSION[0] >= 3:
    304                 lxc_rootfs_config_name = 'lxc.rootfs.path'
    305         if not self._rootfs:
    306             lxc_rootfs = self._get_lxc_config(lxc_rootfs_config_name)[0]
    307             cloned_from_snapshot = ':' in lxc_rootfs
    308             if cloned_from_snapshot:
    309                 self._rootfs = lxc_rootfs.split(':')[-1]
    310             else:
    311                 self._rootfs = lxc_rootfs
    312         return self._rootfs
    313 
    314 
    315     def attach_run(self, command, bash=True):
    316         """Attach to a given container and run the given command.
    317 
    318         @param command: Command to run in the container.
    319         @param bash: Run the command through bash -c "command". This allows
    320                      pipes to be used in command. Default is set to True.
    321 
    322         @return: The output of the command.
    323 
    324         @raise error.CmdError: If container does not exist, or not running.
    325         """
    326         cmd = 'sudo lxc-attach -P %s -n %s' % (self.container_path, self.name)
    327         if bash and not command.startswith('bash -c'):
    328             command = 'bash -c "%s"' % utils.sh_escape(command)
    329         cmd += ' -- %s' % command
    330         # TODO(dshi): crbug.com/459344 Set sudo to default to False when test
    331         # container can be unprivileged container.
    332         return utils.run(cmd)
    333 
    334 
    335     def is_network_up(self):
    336         """Check if network is up in the container by curl base container url.
    337 
    338         @return: True if the network is up, otherwise False.
    339         """
    340         try:
    341             self.attach_run('curl --head %s' % constants.CONTAINER_BASE_URL)
    342             return True
    343         except error.CmdError as e:
    344             logging.debug(e)
    345             return False
    346 
    347 
    348     @metrics.SecondsTimerDecorator(
    349         '%s/container_start_duration' % constants.STATS_KEY)
    350     def start(self, wait_for_network=True):
    351         """Start the container.
    352 
    353         @param wait_for_network: True to wait for network to be up. Default is
    354                                  set to True.
    355 
    356         @raise ContainerError: If container does not exist, or fails to start.
    357         """
    358         cmd = 'sudo lxc-start -P %s -n %s -d' % (self.container_path, self.name)
    359         output = utils.run(cmd).stdout
    360         if not self.is_running():
    361             raise error.ContainerError(
    362                     'Container %s failed to start. lxc command output:\n%s' %
    363                     (os.path.join(self.container_path, self.name),
    364                      output))
    365 
    366         if wait_for_network:
    367             logging.debug('Wait for network to be up.')
    368             start_time = time.time()
    369             utils.poll_for_condition(
    370                 condition=self.is_network_up,
    371                 timeout=constants.NETWORK_INIT_TIMEOUT,
    372                 sleep_interval=constants.NETWORK_INIT_CHECK_INTERVAL)
    373             logging.debug('Network is up after %.2f seconds.',
    374                           time.time() - start_time)
    375 
    376 
    377     @metrics.SecondsTimerDecorator(
    378         '%s/container_stop_duration' % constants.STATS_KEY)
    379     def stop(self):
    380         """Stop the container.
    381 
    382         @raise ContainerError: If container does not exist, or fails to start.
    383         """
    384         cmd = 'sudo lxc-stop -P %s -n %s' % (self.container_path, self.name)
    385         output = utils.run(cmd).stdout
    386         self.refresh_status()
    387         if self.state != 'STOPPED':
    388             raise error.ContainerError(
    389                     'Container %s failed to be stopped. lxc command output:\n'
    390                     '%s' % (os.path.join(self.container_path, self.name),
    391                             output))
    392 
    393 
    394     @metrics.SecondsTimerDecorator(
    395         '%s/container_destroy_duration' % constants.STATS_KEY)
    396     def destroy(self, force=True):
    397         """Destroy the container.
    398 
    399         @param force: Set to True to force to destroy the container even if it's
    400                       running. This is faster than stop a container first then
    401                       try to destroy it. Default is set to True.
    402 
    403         @raise ContainerError: If container does not exist or failed to destroy
    404                                the container.
    405         """
    406         logging.debug('Destroying container %s/%s',
    407                       self.container_path,
    408                       self.name)
    409         cmd = 'sudo lxc-destroy -P %s -n %s' % (self.container_path,
    410                                                 self.name)
    411         if force:
    412             cmd += ' -f'
    413         utils.run(cmd)
    414 
    415 
    416     def mount_dir(self, source, destination, readonly=False):
    417         """Mount a directory in host to a directory in the container.
    418 
    419         @param source: Directory in host to be mounted.
    420         @param destination: Directory in container to mount the source directory
    421         @param readonly: Set to True to make a readonly mount, default is False.
    422         """
    423         # Destination path in container must be relative.
    424         destination = destination.lstrip('/')
    425         # Create directory in container for mount.  Changes to container rootfs
    426         # require sudo.
    427         utils.run('sudo mkdir -p %s' % os.path.join(self.rootfs, destination))
    428         mount = ('%s %s none bind%s 0 0' %
    429                  (source, destination, ',ro' if readonly else ''))
    430         self._set_lxc_config('lxc.mount.entry', mount)
    431 
    432     def verify_autotest_setup(self, job_folder):
    433         """Verify autotest code is set up properly in the container.
    434 
    435         @param job_folder: Name of the job result folder.
    436 
    437         @raise ContainerError: If autotest code is not set up properly.
    438         """
    439         # Test autotest code is setup by verifying a list of
    440         # (directory, minimum file count)
    441         directories_to_check = [
    442                 (constants.CONTAINER_AUTOTEST_DIR, 3),
    443                 (constants.RESULT_DIR_FMT % job_folder, 0),
    444                 (constants.CONTAINER_SITE_PACKAGES_PATH, 3)]
    445         for directory, count in directories_to_check:
    446             result = self.attach_run(command=(constants.COUNT_FILE_CMD %
    447                                               {'dir': directory})).stdout
    448             logging.debug('%s entries in %s.', int(result), directory)
    449             if int(result) < count:
    450                 raise error.ContainerError('%s is not properly set up.' %
    451                                            directory)
    452         # lxc-attach and run command does not run in shell, thus .bashrc is not
    453         # loaded. Following command creates a symlink in /usr/bin/ for gsutil
    454         # if it's installed.
    455         # TODO(dshi): Remove this code after lab container is updated with
    456         # gsutil installed in /usr/bin/
    457         self.attach_run('test -f /root/gsutil/gsutil && '
    458                         'ln -s /root/gsutil/gsutil /usr/bin/gsutil || true')
    459 
    460 
    461     def modify_import_order(self):
    462         """Swap the python import order of lib and local/lib.
    463 
    464         In Moblab, the host's python modules located in
    465         /usr/lib64/python2.7/site-packages is mounted to following folder inside
    466         container: /usr/local/lib/python2.7/dist-packages/. The modules include
    467         an old version of requests module, which is used in autotest
    468         site-packages. For test, the module is only used in
    469         dev_server/symbolicate_dump for requests.call and requests.codes.OK.
    470         When pip is installed inside the container, it installs requests module
    471         with version of 2.2.1 in /usr/lib/python2.7/dist-packages/. The version
    472         is newer than the one used in autotest site-packages, but not the latest
    473         either.
    474         According to /usr/lib/python2.7/site.py, modules in /usr/local/lib are
    475         imported before the ones in /usr/lib. That leads to pip to use the older
    476         version of requests (0.11.2), and it will fail. On the other hand,
    477         requests module 2.2.1 can't be installed in CrOS (refer to CL:265759),
    478         and higher version of requests module can't work with pip.
    479         The only fix to resolve this is to switch the import order, so modules
    480         in /usr/lib can be imported before /usr/local/lib.
    481         """
    482         site_module = '/usr/lib/python2.7/site.py'
    483         self.attach_run("sed -i ':a;N;$!ba;s/\"local\/lib\",\\n/"
    484                         "\"lib_placeholder\",\\n/g' %s" % site_module)
    485         self.attach_run("sed -i ':a;N;$!ba;s/\"lib\",\\n/"
    486                         "\"local\/lib\",\\n/g' %s" % site_module)
    487         self.attach_run('sed -i "s/lib_placeholder/lib/g" %s' %
    488                         site_module)
    489 
    490 
    491     def is_running(self):
    492         """Returns whether or not this container is currently running."""
    493         self.refresh_status()
    494         return self.state == 'RUNNING'
    495 
    496 
    497     def set_hostname(self, hostname):
    498         """Sets the hostname within the container.
    499 
    500         This method can only be called on a running container.
    501 
    502         @param hostname The new container hostname.
    503 
    504         @raise ContainerError: If the container is not running.
    505         """
    506         if not self.is_running():
    507             raise error.ContainerError(
    508                     'set_hostname can only be called on running containers.')
    509 
    510         self.attach_run('hostname %s' % (hostname))
    511         self.attach_run(constants.APPEND_CMD_FMT % {
    512                 'content': '127.0.0.1 %s' % (hostname),
    513                 'file': '/etc/hosts'})
    514 
    515 
    516     def install_ssp(self, ssp_url):
    517         """Downloads and installs the given server package.
    518 
    519         @param ssp_url: The URL of the ssp to download and install.
    520         """
    521         usr_local_path = os.path.join(self.rootfs, 'usr', 'local')
    522         autotest_pkg_path = os.path.join(usr_local_path,
    523                                          'autotest_server_package.tar.bz2')
    524         # Changes within the container rootfs require sudo.
    525         utils.run('sudo mkdir -p %s'% usr_local_path)
    526 
    527         lxc.download_extract(ssp_url, autotest_pkg_path, usr_local_path)
    528 
    529     def install_ssp_isolate(self, isolate_hash, dest_path=None):
    530         """Downloads and install the contents of the given isolate.
    531         This places the isolate contents under /usr/local or a provided path.
    532         Most commonly this is a copy of a specific autotest version, in which
    533         case:
    534           /usr/local/autotest contains the autotest code
    535           /usr/local/logs contains logs from the installation process.
    536 
    537         @param isolate_hash: The hash string which serves as a key to retrieve
    538                              the desired isolate
    539         @param dest_path: Path to the directory to place the isolate in.
    540                           Defaults to /usr/local/
    541 
    542         @return: Exit status of the installation command.
    543         """
    544         dest_path = dest_path or os.path.join(self.rootfs, 'usr', 'local')
    545         isolate_log_path = os.path.join(
    546             self.rootfs, 'usr', 'local', 'logs', 'isolate')
    547         log_file = os.path.join(isolate_log_path,
    548             'contents.' + time.strftime('%Y-%m-%d-%H.%M.%S'))
    549 
    550         utils.run('sudo mkdir -p %s' % isolate_log_path)
    551         _command = ("sudo isolated download -isolated {sha} -I {server}"
    552                     " -output-dir {dest_dir} -output-files {log_file}")
    553 
    554         return utils.run(_command.format(
    555             sha=isolate_hash, dest_dir=dest_path,
    556             log_file=log_file, server=chromite_constants.ISOLATESERVER))
    557 
    558 
    559     def install_control_file(self, control_file):
    560         """Installs the given control file.
    561 
    562         The given file will be copied into the container.
    563 
    564         @param control_file: Path to the control file to install.
    565         """
    566         dst = os.path.join(constants.CONTROL_TEMP_PATH,
    567                            os.path.basename(control_file))
    568         self.copy(control_file, dst)
    569 
    570 
    571     def copy(self, host_path, container_path):
    572         """Copies files into the container.
    573 
    574         @param host_path: Path to the source file/dir to be copied.
    575         @param container_path: Path to the destination dir (in the container).
    576         """
    577         dst_path = os.path.join(self.rootfs,
    578                                 container_path.lstrip(os.path.sep))
    579         self._do_copy(src=host_path, dst=dst_path)
    580 
    581 
    582     @property
    583     def id(self):
    584         """Returns the container ID."""
    585         return self._id
    586 
    587 
    588     @id.setter
    589     def id(self, new_id):
    590         """Sets the container ID."""
    591         self._id = new_id;
    592         # Persist the ID so other container objects can pick it up.
    593         self._id.save(os.path.join(self.container_path, self.name))
    594 
    595 
    596     def _do_copy(self, src, dst):
    597         """Copies files and directories on the host system.
    598 
    599         @param src: The source file or directory.
    600         @param dst: The destination file or directory.  If the path to the
    601                     destination does not exist, it will be created.
    602         """
    603         # Create the dst dir. mkdir -p will not fail if dst_dir exists.
    604         dst_dir = os.path.dirname(dst)
    605         # Make sure the source ends with `/.` if it's a directory. Otherwise
    606         # command cp will not work.
    607         if os.path.isdir(src) and os.path.split(src)[1] != '.':
    608             src = os.path.join(src, '.')
    609         utils.run("sudo sh -c 'mkdir -p \"%s\" && cp -RL \"%s\" \"%s\"'" %
    610                   (dst_dir, src, dst))
    611 
    612     def _set_lxc_config(self, key, value):
    613         """Sets an LXC config value for this container.
    614 
    615         Configuration changes made while a container is running don't take
    616         effect until the container is restarted.  Since this isn't a scenario
    617         that should ever come up in our use cases, calling this method on a
    618         running container will cause a ContainerError.
    619 
    620         @param key: The LXC config key to set.
    621         @param value: The value to use for the given key.
    622 
    623         @raise error.ContainerError: If the container is already started.
    624         """
    625         if self.is_running():
    626             raise error.ContainerError(
    627                 '_set_lxc_config(%s, %s) called on a running container.' %
    628                 (key, value))
    629         config_file = os.path.join(self.container_path, self.name, 'config')
    630         config = '%s = %s' % (key, value)
    631         utils.run(
    632             constants.APPEND_CMD_FMT % {'content': config, 'file': config_file})
    633 
    634 
    635     def _get_lxc_config(self, key):
    636         """Retrieves an LXC config value from the container.
    637 
    638         @param key The key of the config value to retrieve.
    639         """
    640         cmd = ('sudo lxc-info -P %s -n %s -c %s' %
    641                (self.container_path, self.name, key))
    642         config = utils.run(cmd).stdout.strip().splitlines()
    643 
    644         # Strip the decoration from line 1 of the output.
    645         match = re.match('%s = (.*)' % key, config[0])
    646         if not match:
    647             raise error.ContainerError(
    648                     'Config %s not found for container %s. (%s)' %
    649                     (key, self.name, ','.join(config)))
    650         config[0] = match.group(1)
    651         return config
    652