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