blob: 86257772092f59c02f0079abd70e71a7ab7727b1 [file] [log] [blame]
#
#
# Copyright (C) 2010, 2013, 2014, 2015 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""LXC hypervisor
"""
import errno
import os
import os.path
import logging
import sys
import re
from ganeti import constants
from ganeti import errors # pylint: disable=W0611
from ganeti import utils
from ganeti import objects
from ganeti import pathutils
from ganeti import serializer
from ganeti.hypervisor import hv_base
from ganeti.errors import HypervisorError
def _CreateBlankFile(path, mode):
"""Create blank file.
Create a blank file for the path with specified mode.
An existing file will be overwritten.
"""
try:
utils.WriteFile(path, data="", mode=mode)
except EnvironmentError, err:
raise HypervisorError("Failed to create file %s: %s" % (path, err))
class LXCVersion(tuple): # pylint: disable=R0924
"""LXC version class.
"""
# Let beta version following micro version, but don't care about it
_VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)")
@classmethod
def _Parse(cls, version_string):
"""Parse a passed string as an LXC version string.
@param version_string: a valid LXC version string
@type version_string: string
@raise ValueError: if version_string is an invalid LXC version string
@rtype tuple(int, int, int)
@return (major_num, minor_num, micro_num)
"""
match = cls._VERSION_RE.match(version_string)
if match:
return tuple(map(int, match.groups()))
else:
raise ValueError("'%s' is not a valid LXC version string" %
version_string)
def __new__(cls, version_string):
version = super(LXCVersion, cls).__new__(cls, cls._Parse(version_string))
version.original_string = version_string
return version
def __str__(self):
return self.original_string
class LXCHypervisor(hv_base.BaseHypervisor):
"""LXC-based virtualization.
"""
_ROOT_DIR = pathutils.RUN_DIR + "/lxc"
_LOG_DIR = pathutils.LOG_DIR + "/lxc"
# The instance directory has to be structured in a way that would allow it to
# be passed as an argument of the --lxcpath option in lxc- commands.
# This means that:
# Each LXC instance should have a directory carrying their name under this
# directory.
# Each instance directory should contain the "config" file that contains the
# LXC container configuration of an instance.
#
# Therefore the structure of the directory tree should be:
#
# _INSTANCE_DIR
# \_ instance1
# \_ config
# \_ instance2
# \_ config
#
# Other instance specific files can also be placed under an instance
# directory.
_INSTANCE_DIR = _ROOT_DIR + "/instance"
_CGROUP_ROOT_DIR = _ROOT_DIR + "/cgroup"
_PROC_CGROUPS_FILE = "/proc/cgroups"
_PROC_SELF_CGROUP_FILE = "/proc/self/cgroup"
_LXC_MIN_VERSION_REQUIRED = LXCVersion("1.0.0")
_LXC_COMMANDS_REQUIRED = [
"lxc-console",
"lxc-ls",
"lxc-start",
"lxc-stop",
"lxc-wait",
]
_DIR_MODE = 0755
_STASH_KEY_ALLOCATED_LOOP_DEV = "allocated_loopdev"
_MEMORY_PARAMETER = "memory.limit_in_bytes"
_MEMORY_SWAP_PARAMETER = "memory.memsw.limit_in_bytes"
PARAMETERS = {
constants.HV_CPU_MASK: hv_base.OPT_CPU_MASK_CHECK,
constants.HV_LXC_DEVICES: hv_base.NO_CHECK,
constants.HV_LXC_DROP_CAPABILITIES: hv_base.NO_CHECK,
constants.HV_LXC_EXTRA_CGROUPS: hv_base.NO_CHECK,
constants.HV_LXC_EXTRA_CONFIG: hv_base.NO_CHECK,
constants.HV_LXC_NUM_TTYS: hv_base.REQ_NONNEGATIVE_INT_CHECK,
constants.HV_LXC_STARTUP_TIMEOUT: hv_base.OPT_NONNEGATIVE_INT_CHECK,
}
_REBOOT_TIMEOUT = 120 # secs
_REQUIRED_CGROUP_SUBSYSTEMS = [
"cpuset",
"memory",
"devices",
"cpuacct",
]
def __init__(self):
hv_base.BaseHypervisor.__init__(self)
self._EnsureDirectoryExistence()
@classmethod
def _InstanceDir(cls, instance_name):
"""Return the root directory for an instance.
"""
return utils.PathJoin(cls._INSTANCE_DIR, instance_name)
@classmethod
def _InstanceConfFilePath(cls, instance_name):
"""Return the configuration file for an instance.
"""
return utils.PathJoin(cls._InstanceDir(instance_name), "config")
@classmethod
def _InstanceLogFilePath(cls, instance):
"""Return the log file for an instance.
@type instance: L{objects.Instance}
"""
filename = "%s.%s.log" % (instance.name, instance.uuid)
return utils.PathJoin(cls._LOG_DIR, filename)
@classmethod
def _InstanceConsoleLogFilePath(cls, instance_name):
"""Return the console log file path for an instance.
"""
return utils.PathJoin(cls._InstanceDir(instance_name), "console.log")
@classmethod
def _InstanceStashFilePath(cls, instance_name):
"""Return the stash file path for an instance.
The stash file is used to keep information needed to clean up after the
destruction of the instance.
"""
return utils.PathJoin(cls._InstanceDir(instance_name), "stash")
def _EnsureDirectoryExistence(self):
"""Ensures all the directories needed for LXC use exist.
"""
utils.EnsureDirs([
(self._ROOT_DIR, self._DIR_MODE),
(self._LOG_DIR, 0750),
(self._INSTANCE_DIR, 0750),
])
def _SaveInstanceStash(self, instance_name, data):
"""Save data to the instance stash file in serialized format.
"""
stash_file = self._InstanceStashFilePath(instance_name)
serialized = serializer.Dump(data)
try:
utils.WriteFile(stash_file, data=serialized,
mode=constants.SECURE_FILE_MODE)
except EnvironmentError, err:
raise HypervisorError("Failed to save instance stash file %s : %s" %
(stash_file, err))
def _LoadInstanceStash(self, instance_name):
"""Load information stashed in file which was created by
L{_SaveInstanceStash}.
"""
stash_file = self._InstanceStashFilePath(instance_name)
try:
return serializer.Load(utils.ReadFile(stash_file))
except (EnvironmentError, ValueError), err:
raise HypervisorError("Failed to load instance stash file %s : %s" %
(stash_file, err))
@classmethod
def _MountCgroupSubsystem(cls, subsystem):
"""Mount the cgroup subsystem fs under the cgroup root dir.
@type subsystem: string
@param subsystem: cgroup subsystem name to mount
@rtype string
@return path of subsystem mount point
"""
subsys_dir = utils.PathJoin(cls._GetCgroupMountPoint(), subsystem)
if not os.path.isdir(subsys_dir):
try:
os.makedirs(subsys_dir)
except EnvironmentError, err:
raise HypervisorError("Failed to create directory %s: %s" %
(subsys_dir, err))
mount_cmd = ["mount", "-t", "cgroup", "-o", subsystem, subsystem,
subsys_dir]
result = utils.RunCmd(mount_cmd)
if result.failed:
raise HypervisorError("Failed to mount cgroup subsystem '%s': %s" %
(subsystem, result.output))
return subsys_dir
def _CleanupInstance(self, instance_name, stash):
"""Actual implementation of the instance cleanup procedure.
@type instance_name: string
@param instance_name: instance name
@type stash: dict(string:any)
@param stash: dict that contains desired information for instance cleanup
"""
try:
if self._STASH_KEY_ALLOCATED_LOOP_DEV in stash:
loop_dev_path = stash[self._STASH_KEY_ALLOCATED_LOOP_DEV]
utils.ReleaseBdevPartitionMapping(loop_dev_path)
except errors.CommandError, err:
raise HypervisorError("Failed to cleanup partition mapping : %s" % err)
utils.RemoveFile(self._InstanceStashFilePath(instance_name))
def CleanupInstance(self, instance_name):
"""Cleanup after a stopped instance.
"""
stash = self._LoadInstanceStash(instance_name)
self._CleanupInstance(instance_name, stash)
@classmethod
def _GetCgroupMountPoint(cls):
"""Return the directory that should be the base of cgroup fs.
"""
return cls._CGROUP_ROOT_DIR
@classmethod
def _GetOrPrepareCgroupSubsysMountPoint(cls, subsystem):
"""Prepare cgroup subsystem mount point.
@type subsystem: string
@param subsystem: cgroup subsystem name to mount
@rtype string
@return path of subsystem mount point
"""
for _, mpoint, fstype, options in utils.GetMounts():
if fstype == "cgroup" and subsystem in options.split(","):
return mpoint
return cls._MountCgroupSubsystem(subsystem)
@classmethod
def _GetCurrentCgroupSubsysGroups(cls):
"""Return the dict of cgroup subsystem hierarchies this process belongs to.
The dictionary has the cgroup subsystem as a key and its hierarchy as a
value.
Information is read from /proc/self/cgroup.
"""
try:
cgroup_list = utils.ReadFile(cls._PROC_SELF_CGROUP_FILE)
except EnvironmentError, err:
raise HypervisorError("Failed to read %s : %s" %
(cls._PROC_SELF_CGROUP_FILE, err))
cgroups = {}
for line in filter(None, cgroup_list.split("\n")):
_, subsystems, hierarchy = line.split(":")
for subsys in subsystems.split(","):
cgroups[subsys] = hierarchy[1:] # discard first '/'
return cgroups
@classmethod
def _GetCgroupSubsysDir(cls, subsystem):
"""Return the directory of the cgroup subsystem we use.
@type subsystem: string
@param subsystem: cgroup subsystem name
@rtype: string
@return: path of the hierarchy directory for the subsystem
"""
subsys_dir = cls._GetOrPrepareCgroupSubsysMountPoint(subsystem)
base_group = cls._GetCurrentCgroupSubsysGroups().get(subsystem, "")
return utils.PathJoin(subsys_dir, base_group, "lxc")
@classmethod
def _GetCgroupParamPath(cls, param_name, instance_name=None):
"""Return the path of the specified cgroup parameter file.
@type param_name: string
@param param_name: cgroup subsystem parameter name
@rtype: string
@return: path of the cgroup subsystem parameter file
"""
subsystem = param_name.split(".", 1)[0]
subsys_dir = cls._GetCgroupSubsysDir(subsystem)
if instance_name is not None:
return utils.PathJoin(subsys_dir, instance_name, param_name)
else:
return utils.PathJoin(subsys_dir, param_name)
@classmethod
def _GetCgroupInstanceValue(cls, instance_name, param_name):
"""Return the value of the specified cgroup parameter.
@type instance_name: string
@param instance_name: instance name
@type param_name: string
@param param_name: cgroup subsystem parameter name
@rtype string
@return value read from cgroup subsystem fs
"""
param_path = cls._GetCgroupParamPath(param_name,
instance_name=instance_name)
return utils.ReadFile(param_path).rstrip("\n")
@classmethod
def _SetCgroupInstanceValue(cls, instance_name, param_name, param_value):
"""Set the value to the specified instance cgroup parameter.
@type instance_name: string
@param instance_name: instance name
@type param_name: string
@param param_name: cgroup subsystem parameter name
@type param_value: string
@param param_value: cgroup subsystem parameter value to be set
"""
param_path = cls._GetCgroupParamPath(param_name,
instance_name=instance_name)
# When interacting with cgroup fs, errno is quite important information
# to see what happened when setting a cgroup parameter, so just throw
# an error to the upper level.
# e.g., we could know that the container can't reclaim its memory by
# checking if the errno is EBUSY when setting the
# memory.memsw.limit_in_bytes.
fd = -1
try:
fd = os.open(param_path, os.O_WRONLY)
os.write(fd, param_value)
finally:
if fd != -1:
os.close(fd)
@classmethod
def _IsCgroupParameterPresent(cls, parameter, hvparams=None):
"""Return whether a cgroup parameter can be used.
This is checked by seeing whether there is a file representation of the
parameter in the location where the cgroup is mounted.
@type parameter: string
@param parameter: The name of the parameter.
@param hvparams: dict
@param hvparams: The hypervisor parameters, optional.
@rtype: boolean
"""
cls._EnsureCgroupMounts(hvparams)
param_path = cls._GetCgroupParamPath(parameter)
return os.path.exists(param_path)
@classmethod
def _GetCgroupCpuList(cls, instance_name):
"""Return the list of CPU ids for an instance.
"""
try:
cpumask = cls._GetCgroupInstanceValue(instance_name, "cpuset.cpus")
except EnvironmentError, err:
raise errors.HypervisorError("Getting CPU list for instance"
" %s failed: %s" % (instance_name, err))
return utils.ParseCpuMask(cpumask)
@classmethod
def _GetCgroupCpuUsage(cls, instance_name):
"""Return the CPU usage of an instance.
"""
try:
cputime_ns = cls._GetCgroupInstanceValue(instance_name, "cpuacct.usage")
except EnvironmentError, err:
raise HypervisorError("Failed to get the cpu usage of %s: %s" %
(instance_name, err))
return float(cputime_ns) / 10 ** 9 # nano secs to float secs
@classmethod
def _GetCgroupMemoryLimit(cls, instance_name):
"""Return the memory limit for an instance
"""
try:
mem_limit = cls._GetCgroupInstanceValue(instance_name,
"memory.limit_in_bytes")
return int(mem_limit)
except EnvironmentError, err:
raise HypervisorError("Can't get instance memory limit of %s: %s" %
(instance_name, err))
def ListInstances(self, hvparams=None):
"""Get the list of running instances.
"""
return self._ListAliveInstances()
@classmethod
def _IsInstanceAlive(cls, instance_name):
"""Return True if instance is alive.
"""
result = utils.RunCmd(["lxc-ls", "--running", re.escape(instance_name)])
if result.failed:
raise HypervisorError("Failed to get running LXC containers list: %s" %
result.output)
return instance_name in result.stdout.split()
@classmethod
def _ListAliveInstances(cls):
"""Return list of alive instances.
"""
result = utils.RunCmd(["lxc-ls", "--running"])
if result.failed:
raise HypervisorError("Failed to get running LXC containers list: %s" %
result.output)
return result.stdout.split()
def GetInstanceInfo(self, instance_name, hvparams=None):
"""Get instance properties.
@type instance_name: string
@param instance_name: the instance name
@type hvparams: dict of strings
@param hvparams: hvparams to be used with this instance
@rtype: tuple of strings
@return: (name, id, memory, vcpus, stat, times)
"""
if not self._IsInstanceAlive(instance_name):
return None
return self._GetInstanceInfoInner(instance_name)
def _GetInstanceInfoInner(self, instance_name):
"""Get instance properties.
@type instance_name: string
@param instance_name: the instance name
@rtype: tuple of strings
@return: (name, id, memory, vcpus, stat, times)
"""
cpu_list = self._GetCgroupCpuList(instance_name)
memory = self._GetCgroupMemoryLimit(instance_name) / (1024 ** 2)
cputime = self._GetCgroupCpuUsage(instance_name)
return (instance_name, 0, memory, len(cpu_list),
hv_base.HvInstanceState.RUNNING, cputime)
def GetAllInstancesInfo(self, hvparams=None):
"""Get properties of all instances.
@type hvparams: dict of strings
@param hvparams: hypervisor parameter
@return: [(name, id, memory, vcpus, stat, times),...]
"""
data = []
running_instances = self._ListAliveInstances()
filter_fn = lambda x: os.path.isdir(utils.PathJoin(self._INSTANCE_DIR, x))
for dirname in filter(filter_fn, os.listdir(self._INSTANCE_DIR)):
if dirname not in running_instances:
continue
try:
info = self._GetInstanceInfoInner(dirname)
except errors.HypervisorError:
continue
if info:
data.append(info)
return data
@classmethod
def _GetInstanceDropCapabilities(cls, hvparams):
"""Get and parse the drop capabilities list from the instance hvparams.
@type hvparams: dict of strings
@param hvparams: instance hvparams
@rtype list(string)
@return list of drop capabilities
"""
drop_caps = hvparams[constants.HV_LXC_DROP_CAPABILITIES]
return drop_caps.split(",")
def _CreateConfigFile(self, instance, sda_dev_path):
"""Create an lxc.conf file for an instance.
"""
out = []
# hostname
out.append("lxc.utsname = %s" % instance.name)
# separate pseudo-TTY instances
out.append("lxc.pts = 255")
# standard TTYs
num_ttys = instance.hvparams[constants.HV_LXC_NUM_TTYS]
if num_ttys: # if it is the number greater than 0
out.append("lxc.tty = %s" % num_ttys)
# console log file
# After the following patch was applied, we lost the console log file output
# until the lxc.console.logfile parameter was introduced in 1.0.6.
# https://
# lists.linuxcontainers.org/pipermail/lxc-devel/2014-March/008470.html
lxc_version = self._GetLXCVersionFromCmd("lxc-start")
if lxc_version >= LXCVersion("1.0.6"):
console_log_path = self._InstanceConsoleLogFilePath(instance.name)
_CreateBlankFile(console_log_path, constants.SECURE_FILE_MODE)
out.append("lxc.console.logfile = %s" % console_log_path)
else:
logging.warn("Console log file is not supported in LXC version %s,"
" disabling.", lxc_version)
# root FS
out.append("lxc.rootfs = %s" % sda_dev_path)
# Necessary file systems
out.append("lxc.mount.entry = proc proc proc nodev,noexec,nosuid 0 0")
out.append("lxc.mount.entry = sysfs sys sysfs defaults 0 0")
# CPUs
if instance.hvparams[constants.HV_CPU_MASK]:
cpu_list = utils.ParseCpuMask(instance.hvparams[constants.HV_CPU_MASK])
cpus_in_mask = len(cpu_list)
if cpus_in_mask != instance.beparams["vcpus"]:
raise errors.HypervisorError("Number of VCPUs (%d) doesn't match"
" the number of CPUs in the"
" cpu_mask (%d)" %
(instance.beparams["vcpus"],
cpus_in_mask))
out.append("lxc.cgroup.cpuset.cpus = %s" %
instance.hvparams[constants.HV_CPU_MASK])
# Memory
out.append("lxc.cgroup.memory.limit_in_bytes = %dM" %
instance.beparams[constants.BE_MAXMEM])
if LXCHypervisor._IsCgroupParameterPresent(self._MEMORY_SWAP_PARAMETER,
instance.hvparams):
out.append("lxc.cgroup.memory.memsw.limit_in_bytes = %dM" %
instance.beparams[constants.BE_MAXMEM])
# Device control
# deny direct device access
out.append("lxc.cgroup.devices.deny = a")
dev_specs = instance.hvparams[constants.HV_LXC_DEVICES]
for dev_spec in dev_specs.split(","):
out.append("lxc.cgroup.devices.allow = %s" % dev_spec)
# Networking
for idx, nic in enumerate(instance.nics):
out.append("# NIC %d" % idx)
mode = nic.nicparams[constants.NIC_MODE]
link = nic.nicparams[constants.NIC_LINK]
if mode == constants.NIC_MODE_BRIDGED:
out.append("lxc.network.type = veth")
out.append("lxc.network.link = %s" % link)
else:
raise errors.HypervisorError("LXC hypervisor only supports"
" bridged mode (NIC %d has mode %s)" %
(idx, mode))
out.append("lxc.network.hwaddr = %s" % nic.mac)
out.append("lxc.network.flags = up")
# Capabilities
for cap in self._GetInstanceDropCapabilities(instance.hvparams):
out.append("lxc.cap.drop = %s" % cap)
# Extra config
# TODO: Currently a configuration parameter that includes comma
# in its value can't be added via this parameter.
# Make this parameter able to read from a file once the
# "parameter from a file" feature added.
extra_configs = instance.hvparams[constants.HV_LXC_EXTRA_CONFIG]
if extra_configs:
out.append("# User defined configs")
out.extend(extra_configs.split(","))
return "\n".join(out) + "\n"
@classmethod
def _GetCgroupEnabledKernelSubsystems(cls):
"""Return cgroup subsystems list that are enabled in current kernel.
"""
try:
subsys_table = utils.ReadFile(cls._PROC_CGROUPS_FILE)
except EnvironmentError, err:
raise HypervisorError("Failed to read cgroup info from %s: %s"
% (cls._PROC_CGROUPS_FILE, err))
return [x.split(None, 1)[0] for x in subsys_table.split("\n")
if x and not x.startswith("#")]
@classmethod
def _EnsureCgroupMounts(cls, hvparams=None):
"""Ensures all cgroup subsystems required to run LXC container are mounted.
"""
# Check cgroup subsystems required by the Ganeti LXC hypervisor
for subsystem in cls._REQUIRED_CGROUP_SUBSYSTEMS:
cls._GetOrPrepareCgroupSubsysMountPoint(subsystem)
# Check cgroup subsystems required by the LXC
if hvparams is None or not hvparams[constants.HV_LXC_EXTRA_CGROUPS]:
enable_subsystems = cls._GetCgroupEnabledKernelSubsystems()
else:
enable_subsystems = hvparams[constants.HV_LXC_EXTRA_CGROUPS].split(",")
for subsystem in enable_subsystems:
cls._GetOrPrepareCgroupSubsysMountPoint(subsystem)
@classmethod
def _PrepareInstanceRootFsBdev(cls, storage_path, stash):
"""Return mountable path for storage_path.
This function creates a partition mapping for storage_path and returns the
first partition device path as a rootfs partition, and stashes the loopback
device path.
If storage_path is not a multi-partition block device, just return
storage_path.
"""
try:
ret = utils.CreateBdevPartitionMapping(storage_path)
except errors.CommandError, err:
raise HypervisorError("Failed to create partition mapping for %s"
": %s" % (storage_path, err))
if ret is None:
return storage_path
else:
loop_dev_path, dm_dev_paths = ret
stash[cls._STASH_KEY_ALLOCATED_LOOP_DEV] = loop_dev_path
return dm_dev_paths[0]
@classmethod
def _WaitForInstanceState(cls, instance_name, state, timeout):
"""Wait for an instance state transition within timeout
Return True if an instance state changed to the desired state within
timeout secs.
"""
result = utils.RunCmd(["lxc-wait", "-n", instance_name, "-s", state],
timeout=timeout)
if result.failed_by_timeout:
return False
elif result.failed:
raise HypervisorError("Failure while waiting for instance state"
" transition: %s" % result.output)
else:
return True
def _SpawnLXC(self, instance, log_file, conf_file):
"""Execute lxc-start and wait until container health is confirmed.
"""
lxc_start_cmd = [
"lxc-start",
"-n", instance.name,
"-o", log_file,
"-l", "DEBUG",
"-f", conf_file,
"-d"
]
result = utils.RunCmd(lxc_start_cmd)
if result.failed:
raise HypervisorError("Failed to start instance %s : %s" %
(instance.name, result.output))
lxc_startup_timeout = instance.hvparams[constants.HV_LXC_STARTUP_TIMEOUT]
if not self._WaitForInstanceState(instance.name,
constants.LXC_STATE_RUNNING,
lxc_startup_timeout):
raise HypervisorError("Instance %s state didn't change to RUNNING within"
" %s secs" % (instance.name, lxc_startup_timeout))
# Ensure that the instance is running correctly after being daemonized
if not self._IsInstanceAlive(instance.name):
raise HypervisorError("Failed to start instance %s :"
" lxc process exited after being daemonized" %
instance.name)
@classmethod
def _VerifyDiskRequirements(cls, block_devices):
"""Insures that the disks provided work with the current implementation.
"""
if len(block_devices) == 0:
raise HypervisorError("LXC cannot have diskless instances.")
if len(block_devices) > 1:
raise HypervisorError("At the moment, LXC cannot support more than one"
" disk attached to it. Please create this"
" instance anew with fewer disks.")
def StartInstance(self, instance, block_devices, startup_paused):
"""Start an instance.
For LXC, we try to mount the block device and execute 'lxc-start'.
We use volatile containers.
"""
LXCHypervisor._VerifyDiskRequirements(block_devices)
stash = {}
# Since LXC version >= 1.0.0, the LXC strictly requires all cgroup
# subsystems mounted before starting a container.
# Try to mount all cgroup subsystems needed to start a LXC container.
self._EnsureCgroupMounts(instance.hvparams)
root_dir = self._InstanceDir(instance.name)
try:
utils.EnsureDirs([(root_dir, self._DIR_MODE)])
except errors.GenericError, err:
raise HypervisorError("Creating instance directory failed: %s", str(err))
log_file = self._InstanceLogFilePath(instance)
if not os.path.exists(log_file):
_CreateBlankFile(log_file, constants.SECURE_FILE_MODE)
try:
sda_dev_path = block_devices[0][1]
# LXC needs to use partition mapping devices to access each partition
# of the storage
sda_dev_path = self._PrepareInstanceRootFsBdev(sda_dev_path, stash)
conf_file = self._InstanceConfFilePath(instance.name)
conf = self._CreateConfigFile(instance, sda_dev_path)
utils.WriteFile(conf_file, data=conf)
logging.info("Starting LXC container")
try:
self._SpawnLXC(instance, log_file, conf_file)
except:
logging.error("Failed to start instance %s. Please take a look at %s to"
" see LXC errors.", instance.name, log_file)
raise
except:
# Save the original error
exc_info = sys.exc_info()
try:
self._CleanupInstance(instance.name, stash)
except HypervisorError, err:
logging.warn("Cleanup for instance %s incomplete: %s",
instance.name, err)
raise exc_info[0], exc_info[1], exc_info[2]
self._SaveInstanceStash(instance.name, stash)
def StopInstance(self, instance, force=False, retry=False, name=None,
timeout=None):
"""Stop an instance.
"""
assert(timeout is None or force is not None)
if name is None:
name = instance.name
if self._IsInstanceAlive(instance.name):
lxc_stop_cmd = ["lxc-stop", "-n", name]
if force:
lxc_stop_cmd.append("--kill")
result = utils.RunCmd(lxc_stop_cmd, timeout=timeout)
if result.failed:
raise HypervisorError("Failed to kill instance %s: %s" %
(name, result.output))
else:
# The --timeout=-1 option is needed to prevent lxc-stop performs
# hard-stop(kill) for the container after the default timing out.
lxc_stop_cmd.extend(["--nokill", "--timeout", "-1"])
result = utils.RunCmd(lxc_stop_cmd, timeout=timeout)
if result.failed:
logging.error("Failed to stop instance %s: %s", name, result.output)
def RebootInstance(self, instance):
"""Reboot an instance.
"""
if "sys_boot" in self._GetInstanceDropCapabilities(instance.hvparams):
raise HypervisorError("The LXC container can't perform a reboot with the"
" SYS_BOOT capability dropped.")
# We can't use the --timeout=-1 approach as same as the StopInstance due to
# the following patch was applied in lxc-1.0.5 and we are supporting
# LXC >= 1.0.0.
# http://lists.linuxcontainers.org/pipermail/lxc-devel/2014-July/009742.html
result = utils.RunCmd(["lxc-stop", "-n", instance.name, "--reboot",
"--timeout", str(self._REBOOT_TIMEOUT)])
if result.failed:
raise HypervisorError("Failed to reboot instance %s: %s" %
(instance.name, result.output))
def BalloonInstanceMemory(self, instance, mem):
"""Balloon an instance memory to a certain value.
@type instance: L{objects.Instance}
@param instance: instance to be accepted
@type mem: int
@param mem: actual memory size to use for instance runtime
"""
mem_in_bytes = mem * 1024 ** 2
current_mem_usage = self._GetCgroupMemoryLimit(instance.name)
shrinking = mem_in_bytes <= current_mem_usage
# The memsw.limit_in_bytes parameter might be present depending on kernel
# parameters.
# If present, it has to be modified at the same time as limit_in_bytes.
if LXCHypervisor._IsCgroupParameterPresent(self._MEMORY_SWAP_PARAMETER,
instance.hvparams):
# memory.memsw.limit_in_bytes is the superlimit of memory.limit_in_bytes
# so the order of setting these parameters is quite important.
cgparams = [self._MEMORY_SWAP_PARAMETER, self._MEMORY_PARAMETER]
else:
cgparams = [self._MEMORY_PARAMETER]
if shrinking:
cgparams.reverse()
for i, cgparam in enumerate(cgparams):
try:
self._SetCgroupInstanceValue(instance.name, cgparam, str(mem_in_bytes))
except EnvironmentError, err:
if shrinking and err.errno == errno.EBUSY:
logging.warn("Unable to reclaim memory or swap usage from instance"
" %s", instance.name)
# Restore changed parameters for an atomicity
for restore_param in cgparams[0:i]:
try:
self._SetCgroupInstanceValue(instance.name, restore_param,
str(current_mem_usage))
except EnvironmentError, restore_err:
logging.warn("Can't restore the cgroup parameter %s of %s: %s",
restore_param, instance.name, restore_err)
raise HypervisorError("Failed to balloon the memory of %s, can't set"
" cgroup parameter %s: %s" %
(instance.name, cgparam, err))
def GetNodeInfo(self, hvparams=None):
"""Return information about the node.
See L{BaseHypervisor.GetLinuxNodeInfo}.
"""
return self.GetLinuxNodeInfo()
@classmethod
def GetInstanceConsole(cls, instance, primary_node, node_group,
hvparams, beparams):
"""Return a command for connecting to the console of an instance.
"""
ndparams = node_group.FillND(primary_node)
return objects.InstanceConsole(instance=instance.name,
kind=constants.CONS_SSH,
host=primary_node.name,
port=ndparams.get(constants.ND_SSH_PORT),
user=constants.SSH_CONSOLE_USER,
command=["lxc-console", "-n", instance.name])
@classmethod
def _GetLXCVersionFromCmd(cls, from_cmd):
"""Return the LXC version currently used in the system.
Version information will be retrieved by command specified by from_cmd.
@param from_cmd: the lxc command used to retrieve version information
@type from_cmd: string
@rtype: L{LXCVersion}
@return: a version object which represents the version retrieved from the
command
"""
result = utils.RunCmd([from_cmd, "--version"])
if result.failed:
raise HypervisorError("Failed to get version info from command %s: %s" %
(from_cmd, result.output))
try:
return LXCVersion(result.stdout.strip())
except ValueError, err:
raise HypervisorError("Can't parse LXC version from %s: %s" %
(from_cmd, err))
@classmethod
def _VerifyLXCCommands(cls):
"""Verify the validity of lxc command line tools.
@rtype: list(str)
@return: list of problem descriptions. the blank list will be returned if
there is no problem.
"""
msgs = []
for cmd in cls._LXC_COMMANDS_REQUIRED:
try:
# lxc-ls needs special checking procedure.
# there are two different version of lxc-ls, one is written in python
# and the other is written in shell script.
# we have to ensure the python version of lxc-ls is installed.
if cmd == "lxc-ls":
help_string = utils.RunCmd(["lxc-ls", "--help"]).output
if "--running" not in help_string:
# shell script version has no --running switch
msgs.append("The python version of 'lxc-ls' is required."
" Maybe lxc was installed without --enable-python")
else:
try:
version = cls._GetLXCVersionFromCmd(cmd)
except HypervisorError, err:
msgs.append(str(err))
continue
if version < cls._LXC_MIN_VERSION_REQUIRED:
msgs.append("LXC version >= %s is required but command %s has"
" version %s" %
(cls._LXC_MIN_VERSION_REQUIRED, cmd, version))
except errors.OpExecError:
msgs.append("Required command %s not found" % cmd)
return msgs
def Verify(self, hvparams=None):
"""Verify the hypervisor.
For the LXC manager, it just checks the existence of the base dir.
@type hvparams: dict of strings
@param hvparams: hypervisor parameters to be verified against; not used here
@return: Problem description if something is wrong, C{None} otherwise
"""
msgs = []
if not os.path.exists(self._ROOT_DIR):
msgs.append("The required directory '%s' does not exist" %
self._ROOT_DIR)
try:
self._EnsureCgroupMounts(hvparams)
except errors.HypervisorError, err:
msgs.append(str(err))
msgs.extend(self._VerifyLXCCommands())
return self._FormatVerifyResults(msgs)
@classmethod
def PowercycleNode(cls, hvparams=None):
"""LXC powercycle, just a wrapper over Linux powercycle.
@type hvparams: dict of strings
@param hvparams: hypervisor params to be used on this node
"""
cls.LinuxPowercycle()
def MigrateInstance(self, cluster_name, instance, target, live):
"""Migrate an instance.
@type cluster_name: string
@param cluster_name: name of the cluster
@type instance: L{objects.Instance}
@param instance: the instance to be migrated
@type target: string
@param target: hostname (usually ip) of the target node
@type live: boolean
@param live: whether to do a live or non-live migration
"""
raise HypervisorError("Migration is not supported by the LXC hypervisor")
def GetMigrationStatus(self, instance):
"""Get the migration status
@type instance: L{objects.Instance}
@param instance: the instance that is being migrated
@rtype: L{objects.MigrationStatus}
@return: the status of the current migration (one of
L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
progress info that can be retrieved from the hypervisor
"""
raise HypervisorError("Migration is not supported by the LXC hypervisor")