• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5import logging
6import os
7import re
8import time
9
10import common
11from autotest_lib.client.bin import utils
12from autotest_lib.client.common_lib import error
13from autotest_lib.site_utils.lxc import constants
14from autotest_lib.site_utils.lxc import lxc
15from autotest_lib.site_utils.lxc import utils as lxc_utils
16
17try:
18    from chromite.lib import metrics
19except ImportError:
20    metrics = utils.metrics_mock
21
22
23class Container(object):
24    """A wrapper class of an LXC container.
25
26    The wrapper class provides methods to interact with a container, e.g.,
27    start, stop, destroy, run a command. It also has attributes of the
28    container, including:
29    name: Name of the container.
30    state: State of the container, e.g., ABORTING, RUNNING, STARTING, STOPPED,
31           or STOPPING.
32
33    lxc-ls can also collect other attributes of a container including:
34    ipv4: IP address for IPv4.
35    ipv6: IP address for IPv6.
36    autostart: If the container will autostart at system boot.
37    pid: Process ID of the container.
38    memory: Memory used by the container, as a string, e.g., "6.2MB"
39    ram: Physical ram used by the container, as a string, e.g., "6.2MB"
40    swap: swap used by the container, as a string, e.g., "1.0MB"
41
42    For performance reason, such info is not collected for now.
43
44    The attributes available are defined in ATTRIBUTES constant.
45    """
46
47    def __init__(self, container_path, name, attribute_values, src=None,
48                 snapshot=False):
49        """Initialize an object of LXC container with given attribute values.
50
51        @param container_path: Directory that stores the container.
52        @param name: Name of the container.
53        @param attribute_values: A dictionary of attribute values for the
54                                 container.
55        @param src: An optional source container.  If provided, the source
56                    continer is cloned, and the new container will point to the
57                    clone.
58        @param snapshot: If a source container was specified, this argument
59                         specifies whether or not to create a snapshot clone.
60                         The default is to attempt to create a snapshot.
61                         If a snapshot is requested and creating the snapshot
62                         fails, a full clone will be attempted.
63        """
64        self.container_path = os.path.realpath(container_path)
65        # Path to the rootfs of the container. This will be initialized when
66        # property rootfs is retrieved.
67        self._rootfs = None
68        self.name = name
69        for attribute, value in attribute_values.iteritems():
70            setattr(self, attribute, value)
71
72        # Clone the container
73        if src is not None:
74            # Clone the source container to initialize this one.
75            lxc_utils.clone(src.container_path, src.name, self.container_path,
76                            self.name, snapshot)
77
78
79    @classmethod
80    def createFromExistingDir(cls, lxc_path, name, **kwargs):
81        """Creates a new container instance for an lxc container that already
82        exists on disk.
83
84        @param lxc_path: The LXC path for the container.
85        @param name: The container name.
86
87        @raise error.ContainerError: If the container doesn't already exist.
88
89        @return: The new container.
90        """
91        return cls(lxc_path, name, kwargs)
92
93
94    @classmethod
95    def clone(cls, src, new_name, new_path=None, snapshot=False, cleanup=False):
96        """Creates a clone of this container.
97
98        @param src: The original container.
99        @param new_name: Name for the cloned container.
100        @param new_path: LXC path for the cloned container (optional; if not
101                specified, the new container is created in the same directory as
102                the source container).
103        @param snapshot: Whether to snapshot, or create a full clone.
104        @param cleanup: If a container with the given name and path already
105                exist, clean it up first.
106        """
107        if new_path is None:
108            new_path = src.container_path
109
110        # If a container exists at this location, clean it up first
111        container_folder = os.path.join(new_path, new_name)
112        if lxc_utils.path_exists(container_folder):
113            if not cleanup:
114                raise error.ContainerError('Container %s already exists.' %
115                                           new_name)
116            container = Container.createFromExistingDir(new_path, new_name)
117            try:
118                container.destroy()
119            except error.CmdError as e:
120                # The container could be created in a incompleted state. Delete
121                # the container folder instead.
122                logging.warn('Failed to destroy container %s, error: %s',
123                             new_name, e)
124                utils.run('sudo rm -rf "%s"' % container_folder)
125
126        # Create and return the new container.
127        return cls(new_path, new_name, {}, src, snapshot)
128
129
130    def refresh_status(self):
131        """Refresh the status information of the container.
132        """
133        containers = lxc.get_container_info(self.container_path, name=self.name)
134        if not containers:
135            raise error.ContainerError(
136                    'No container found in directory %s with name of %s.' %
137                    (self.container_path, self.name))
138        attribute_values = containers[0]
139        for attribute, value in attribute_values.iteritems():
140            setattr(self, attribute, value)
141
142
143    @property
144    def rootfs(self):
145        """Path to the rootfs of the container.
146
147        This property returns the path to the rootfs of the container, that is,
148        the folder where the container stores its local files. It reads the
149        attribute lxc.rootfs from the config file of the container, e.g.,
150            lxc.rootfs = /usr/local/autotest/containers/t4/rootfs
151        If the container is created with snapshot, the rootfs is a chain of
152        folders, separated by `:` and ordered by how the snapshot is created,
153        e.g.,
154            lxc.rootfs = overlayfs:/usr/local/autotest/containers/base/rootfs:
155            /usr/local/autotest/containers/t4_s/delta0
156        This function returns the last folder in the chain, in above example,
157        that is `/usr/local/autotest/containers/t4_s/delta0`
158
159        Files in the rootfs will be accessible directly within container. For
160        example, a folder in host "[rootfs]/usr/local/file1", can be accessed
161        inside container by path "/usr/local/file1". Note that symlink in the
162        host can not across host/container boundary, instead, directory mount
163        should be used, refer to function mount_dir.
164
165        @return: Path to the rootfs of the container.
166        """
167        if not self._rootfs:
168            cmd = ('sudo lxc-info -P %s -n %s -c lxc.rootfs' %
169                   (self.container_path, self.name))
170            lxc_rootfs_config = utils.run(cmd).stdout.strip()
171            match = re.match('lxc.rootfs = (.*)', lxc_rootfs_config)
172            if not match:
173                raise error.ContainerError(
174                        'Failed to locate rootfs for container %s. lxc.rootfs '
175                        'in the container config file is %s' %
176                        (self.name, lxc_rootfs_config))
177            lxc_rootfs = match.group(1)
178            cloned_from_snapshot = ':' in lxc_rootfs
179            if cloned_from_snapshot:
180                self._rootfs = lxc_rootfs.split(':')[-1]
181            else:
182                self._rootfs = lxc_rootfs
183        return self._rootfs
184
185
186    def attach_run(self, command, bash=True):
187        """Attach to a given container and run the given command.
188
189        @param command: Command to run in the container.
190        @param bash: Run the command through bash -c "command". This allows
191                     pipes to be used in command. Default is set to True.
192
193        @return: The output of the command.
194
195        @raise error.CmdError: If container does not exist, or not running.
196        """
197        cmd = 'sudo lxc-attach -P %s -n %s' % (self.container_path, self.name)
198        if bash and not command.startswith('bash -c'):
199            command = 'bash -c "%s"' % utils.sh_escape(command)
200        cmd += ' -- %s' % command
201        # TODO(dshi): crbug.com/459344 Set sudo to default to False when test
202        # container can be unprivileged container.
203        return utils.run(cmd)
204
205
206    def is_network_up(self):
207        """Check if network is up in the container by curl base container url.
208
209        @return: True if the network is up, otherwise False.
210        """
211        try:
212            self.attach_run('curl --head %s' % constants.CONTAINER_BASE_URL)
213            return True
214        except error.CmdError as e:
215            logging.debug(e)
216            return False
217
218
219    @metrics.SecondsTimerDecorator(
220        '%s/container_start_duration' % constants.STATS_KEY)
221    def start(self, wait_for_network=True):
222        """Start the container.
223
224        @param wait_for_network: True to wait for network to be up. Default is
225                                 set to True.
226
227        @raise ContainerError: If container does not exist, or fails to start.
228        """
229        cmd = 'sudo lxc-start -P %s -n %s -d' % (self.container_path, self.name)
230        output = utils.run(cmd).stdout
231        if not self.is_running():
232            raise error.ContainerError(
233                    'Container %s failed to start. lxc command output:\n%s' %
234                    (os.path.join(self.container_path, self.name),
235                     output))
236
237        if wait_for_network:
238            logging.debug('Wait for network to be up.')
239            start_time = time.time()
240            utils.poll_for_condition(
241                condition=self.is_network_up,
242                timeout=constants.NETWORK_INIT_TIMEOUT,
243                sleep_interval=constants.NETWORK_INIT_CHECK_INTERVAL)
244            logging.debug('Network is up after %.2f seconds.',
245                          time.time() - start_time)
246
247
248    @metrics.SecondsTimerDecorator(
249        '%s/container_stop_duration' % constants.STATS_KEY)
250    def stop(self):
251        """Stop the container.
252
253        @raise ContainerError: If container does not exist, or fails to start.
254        """
255        cmd = 'sudo lxc-stop -P %s -n %s' % (self.container_path, self.name)
256        output = utils.run(cmd).stdout
257        self.refresh_status()
258        if self.state != 'STOPPED':
259            raise error.ContainerError(
260                    'Container %s failed to be stopped. lxc command output:\n'
261                    '%s' % (os.path.join(self.container_path, self.name),
262                            output))
263
264
265    @metrics.SecondsTimerDecorator(
266        '%s/container_destroy_duration' % constants.STATS_KEY)
267    def destroy(self, force=True):
268        """Destroy the container.
269
270        @param force: Set to True to force to destroy the container even if it's
271                      running. This is faster than stop a container first then
272                      try to destroy it. Default is set to True.
273
274        @raise ContainerError: If container does not exist or failed to destroy
275                               the container.
276        """
277        cmd = 'sudo lxc-destroy -P %s -n %s' % (self.container_path,
278                                                self.name)
279        if force:
280            cmd += ' -f'
281        utils.run(cmd)
282
283
284    def mount_dir(self, source, destination, readonly=False):
285        """Mount a directory in host to a directory in the container.
286
287        @param source: Directory in host to be mounted.
288        @param destination: Directory in container to mount the source directory
289        @param readonly: Set to True to make a readonly mount, default is False.
290        """
291        # Destination path in container must be relative.
292        destination = destination.lstrip('/')
293        # Create directory in container for mount.
294        utils.run('sudo mkdir -p %s' % os.path.join(self.rootfs, destination))
295        config_file = os.path.join(self.container_path, self.name, 'config')
296        mount = constants.MOUNT_FMT % {'source': source,
297                                       'destination': destination,
298                                       'readonly': ',ro' if readonly else ''}
299        utils.run(
300            constants.APPEND_CMD_FMT % {'content': mount, 'file': config_file})
301
302
303    def verify_autotest_setup(self, job_folder):
304        """Verify autotest code is set up properly in the container.
305
306        @param job_folder: Name of the job result folder.
307
308        @raise ContainerError: If autotest code is not set up properly.
309        """
310        # Test autotest code is setup by verifying a list of
311        # (directory, minimum file count)
312        directories_to_check = [
313                (constants.CONTAINER_AUTOTEST_DIR, 3),
314                (constants.RESULT_DIR_FMT % job_folder, 0),
315                (constants.CONTAINER_SITE_PACKAGES_PATH, 3)]
316        for directory, count in directories_to_check:
317            result = self.attach_run(command=(constants.COUNT_FILE_CMD %
318                                              {'dir': directory})).stdout
319            logging.debug('%s entries in %s.', int(result), directory)
320            if int(result) < count:
321                raise error.ContainerError('%s is not properly set up.' %
322                                           directory)
323        # lxc-attach and run command does not run in shell, thus .bashrc is not
324        # loaded. Following command creates a symlink in /usr/bin/ for gsutil
325        # if it's installed.
326        # TODO(dshi): Remove this code after lab container is updated with
327        # gsutil installed in /usr/bin/
328        self.attach_run('test -f /root/gsutil/gsutil && '
329                        'ln -s /root/gsutil/gsutil /usr/bin/gsutil || true')
330
331
332    def modify_import_order(self):
333        """Swap the python import order of lib and local/lib.
334
335        In Moblab, the host's python modules located in
336        /usr/lib64/python2.7/site-packages is mounted to following folder inside
337        container: /usr/local/lib/python2.7/dist-packages/. The modules include
338        an old version of requests module, which is used in autotest
339        site-packages. For test, the module is only used in
340        dev_server/symbolicate_dump for requests.call and requests.codes.OK.
341        When pip is installed inside the container, it installs requests module
342        with version of 2.2.1 in /usr/lib/python2.7/dist-packages/. The version
343        is newer than the one used in autotest site-packages, but not the latest
344        either.
345        According to /usr/lib/python2.7/site.py, modules in /usr/local/lib are
346        imported before the ones in /usr/lib. That leads to pip to use the older
347        version of requests (0.11.2), and it will fail. On the other hand,
348        requests module 2.2.1 can't be installed in CrOS (refer to CL:265759),
349        and higher version of requests module can't work with pip.
350        The only fix to resolve this is to switch the import order, so modules
351        in /usr/lib can be imported before /usr/local/lib.
352        """
353        site_module = '/usr/lib/python2.7/site.py'
354        self.attach_run("sed -i ':a;N;$!ba;s/\"local\/lib\",\\n/"
355                        "\"lib_placeholder\",\\n/g' %s" % site_module)
356        self.attach_run("sed -i ':a;N;$!ba;s/\"lib\",\\n/"
357                        "\"local\/lib\",\\n/g' %s" % site_module)
358        self.attach_run('sed -i "s/lib_placeholder/lib/g" %s' %
359                        site_module)
360
361
362    def is_running(self):
363        """Returns whether or not this container is currently running."""
364        self.refresh_status()
365        return self.state == 'RUNNING'
366
367
368    def set_hostname(self, hostname):
369        """Sets the hostname within the container.  This needs to be called
370        prior to starting the container.
371        """
372        config_file = os.path.join(self.container_path, self.name, 'config')
373        lxc_utsname_setting = (
374                'lxc.utsname = ' +
375                constants.CONTAINER_UTSNAME_FORMAT % hostname)
376        utils.run(
377            constants.APPEND_CMD_FMT % {'content': lxc_utsname_setting,
378                                        'file': config_file})
379
380
381    def install_ssp(self, ssp_url):
382        """Downloads and installs the given server package.
383
384        @param ssp_url: The URL of the ssp to download and install.
385        """
386        usr_local_path = os.path.join(self.rootfs, 'usr', 'local')
387        autotest_pkg_path = os.path.join(usr_local_path,
388                                         'autotest_server_package.tar.bz2')
389        # sudo is required so os.makedirs may not work.
390        utils.run('sudo mkdir -p %s'% usr_local_path)
391
392        lxc.download_extract(ssp_url, autotest_pkg_path, usr_local_path)
393
394
395    def install_control_file(self, control_file):
396        """Installs the given control file.
397        The given file will be moved into the container.
398
399        @param control_file: Path to the control file to install.
400        """
401        dst_path = os.path.join(self.rootfs,
402                                constants.CONTROL_TEMP_PATH.lstrip(os.path.sep))
403        utils.run('sudo mkdir -p %s' % dst_path)
404        utils.run('sudo mv %s %s' % (control_file, dst_path))
405