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