nova-lxd/nova/virt/lxd/config.py
Chuck Short b03c33ce64 Add support for ephemeral storage
Add previously missing ephemeral storage support for nova-lxd.
This patch provides the infrastructure  to translate
the configuration of the instance to something
that LXD can understand and act upon.

The way that the ephemeral storage works is that block_device_info
dict and instance object. If an empeheral storage device
is needed then the storage deivce is created on the compute host
and bind mounted into the container when the container is started.
This is due to the contanier is running unprivileged and restricted
mount(8)

To come is restricting the ephemeral storage based on the LXD storage
backend on the compute host.

Update exist unit tests and adds unit new tests.

Change-Id: I38d48708b3c6fb450258e03a6106f47be3aeb998
Signed-off-by: Chuck Short <chuck.short@canonical.com>
2016-07-05 14:23:46 -04:00

465 lines
18 KiB
Python

# Copyright 2011 Justin Santa Barbara
# Copyright 2015 Canonical Ltd
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import nova.conf
from nova import exception
from nova import i18n
from nova.virt import configdrive
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import units
from nova.virt.lxd import session
from nova.virt.lxd import utils as container_dir
from nova.virt.lxd import vif
_ = i18n._
_LE = i18n._LE
_LI = i18n._LI
CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
class LXDContainerConfig(object):
"""LXD configuration methods."""
def __init__(self):
self.session = session.LXDAPISession()
self.vif_driver = vif.LXDGenericDriver()
def create_container(self, instance):
"""Create a LXD container dictionary so that we can
use it to initialize a container
:param instance: nova instance object
"""
LOG.debug('create_container called for instance', instance=instance)
instance_name = instance.name
try:
# Fetch the container configuration from the current nova
# instance object
return {
'name': instance_name,
'profiles': [str(instance.name)],
'source': self.get_container_source(instance),
'devices': {}
}
except Exception as ex:
with excutils.save_and_reraise_exception():
LOG.error('Failed to get container configuration'
' %(instance)s: %(ex)s',
{'instance': instance_name, 'ex': ex},
instance=instance)
def create_profile(self, instance, network_info, block_device_info):
"""Create a LXD container profile configuration
:param instance: nova instance object
:param network_info: nova network configuration object
:return: LXD container profile dictionary
"""
LOG.debug('create_container_profile called for instance',
instance=instance)
instance_name = instance.name
try:
config = {}
config['name'] = str(instance_name)
config['config'] = self.create_config(instance_name, instance)
# Restrict the size of the "/" disk
config['devices'] = self.configure_container_root(instance)
if instance.get('ephemeral_gb', 0) != 0:
ephemerals = block_device_info.get('ephemerals', [])
if ephemerals == []:
ephemeral_src = container_dir.get_container_storage(
ephemerals['virtual_name'], instance.name)
config['devices'].update(
self.configure_disk_path(
ephemeral_src, '/mnt', ephemerals['virtual_name'],
instance))
else:
for idx, ephemerals in enumerate(ephemerals):
ephemeral_src = container_dir.get_container_storage(
ephemerals['virtual_name'], instance.name)
config['devices'].update(self.configure_disk_path(
ephemeral_src, '/mnt', ephemerals['virtual_name'],
instance))
# if a configdrive is required, setup the mount point for
# the container
if configdrive.required_by(instance):
configdrive_dir = \
container_dir.get_container_configdrive(
instance.name)
config_drive = self.configure_disk_path(
configdrive_dir, 'var/lib/cloud/data',
'configdrive', instance)
config['devices'].update(config_drive)
if network_info:
config['devices'].update(self.create_network(instance_name,
instance,
network_info))
return config
except Exception as ex:
with excutils.save_and_reraise_exception():
LOG.error(
_LE('Failed to create profile %(instance)s: %(ex)s'),
{'instance': instance_name, 'ex': ex}, instance=instance)
def create_config(self, instance_name, instance):
"""Create the LXD container resources
:param instance_name: instance name
:param instance: nova instance object
:return: LXD resources dictionary
"""
LOG.debug('create_config called for instance', instance=instance)
try:
config = {}
# Update container options
config.update(self.config_instance_options(config, instance))
# Set the instance memory limit
mem = instance.memory_mb
if mem >= 0:
config['limits.memory'] = '%sMB' % mem
# Set the instance vcpu limit
vcpus = instance.flavor.vcpus
if vcpus >= 0:
config['limits.cpu'] = str(vcpus)
# Configure the console for the instance
config['raw.lxc'] = 'lxc.console.logfile=%s\n' \
% container_dir.get_console_path(instance_name)
return config
except Exception as ex:
with excutils.save_and_reraise_exception():
LOG.error(
_LE('Failed to set container resources %(instance)s: '
'%(ex)s'), {'instance': instance_name, 'ex': ex},
instance=instance)
def config_instance_options(self, config, instance):
LOG.debug('config_instance_options called for instance',
instance=instance)
# Set the container to autostart when the host reboots
config['boot.autostart'] = 'True'
# Determine if we require a nested container
flavor = instance.flavor
lxd_nested_allowed = flavor.extra_specs.get(
'lxd:nested_allowed', False)
if lxd_nested_allowed:
config['security.nesting'] = 'True'
# Determine if we require a privileged container
lxd_privileged_allowed = flavor.extra_specs.get(
'lxd:privileged_allowed', False)
if lxd_privileged_allowed:
config['security.privileged'] = 'True'
return config
def configure_container_root(self, instance):
LOG.debug('configure_container_root called for instance',
instance=instance)
try:
config = {}
lxd_config = self.session.get_host_config(instance)
config.setdefault('root', {'type': 'disk', 'path': '/'})
if str(lxd_config['storage']) in ['btrfs', 'zfs']:
config['root'].update({'size': '%sGB' % str(instance.root_gb)})
# Set disk quotas
config['root'].update(self.create_disk_quota_config(instance))
return config
except Exception as ex:
with excutils.save_and_reraise_exception():
LOG.error(_LE('Failed to configure disk for '
'%(instance)s: %(ex)s'),
{'instance': instance.name, 'ex': ex},
instance=instance)
def create_disk_quota_config(self, instance):
md = instance.flavor.extra_specs
disk_config = {}
md_namespace = 'quota:'
params = ['disk_read_iops_sec', 'disk_read_bytes_sec',
'disk_write_iops_sec', 'disk_write_bytes_sec',
'disk_total_iops_sec', 'disk_total_bytes_sec']
# Get disk quotas from flavor metadata and cast the values to int
q = {}
for param in params:
try:
q[param] = int(md.get(md_namespace + param, 0))
except ValueError:
LOG.warning(_LE('Disk quota %(p)s must be an integer'),
{'p': param},
instance=instance)
# Bytes and IOps are not separate config options in a container
# profile - we let Bytes take priority over IOps if both are set.
# Align all limits to MiB/s, which should be a sensible middle road.
if q.get('disk_read_iops_sec'):
disk_config['limits.read'] = \
('%s' + 'iops') % q['disk_read_iops_sec']
if q.get('disk_read_bytes_sec'):
disk_config['limits.read'] = \
('%s' + 'MB') % (q['disk_read_bytes_sec'] / units.Mi)
if q.get('disk_write_iops_sec'):
disk_config['limits.write'] = \
('%s' + 'iops') % q['disk_write_iops_sec']
if q.get('disk_write_bytes_sec'):
disk_config['limits.write'] = \
('%s' + 'MB') % (q['disk_write_bytes_sec'] / units.Mi)
# If at least one of the above limits has been defined, do not set
# the "max" quota (which would apply to both read and write)
minor_quota_defined = any(
q.get(param) for param in
['disk_read_iops_sec', 'disk_write_iops_sec',
'disk_read_bytes_sec', 'disk_write_bytes_sec']
)
if q.get('disk_total_iops_sec') and not minor_quota_defined:
disk_config['limits.max'] = \
('%s' + 'iops') % q['disk_total_iops_sec']
if q.get('disk_total_bytes_sec') and not minor_quota_defined:
disk_config['limits.max'] = \
('%s' + 'MB') % (q['disk_total_bytes_sec'] / units.Mi)
return disk_config
def create_network(self, instance_name, instance, network_info):
"""Create the LXD container network on the host
:param instance_name: nova instance name
:param instance: nova instance object
:param network_info: instance network configuration object
:return:network configuration dictionary
"""
LOG.debug('create_network called for instance', instance=instance)
try:
network_devices = {}
if not network_info:
return
for vifaddr in network_info:
cfg = self.vif_driver.get_config(instance, vifaddr)
network_devices[str(cfg['bridge'])] = \
{'nictype': 'bridged',
'hwaddr': str(cfg['mac_address']),
'parent': str(cfg['bridge']),
'type': 'nic'}
# Set network device quotas
network_devices[str(cfg['bridge'])].update(
self.create_network_quota_config(instance)
)
return network_devices
except Exception as ex:
with excutils.save_and_reraise_exception():
LOG.error(
_LE('Fail to configure network for %(instance)s: %(ex)s'),
{'instance': instance_name, 'ex': ex}, instance=instance)
def create_network_quota_config(self, instance):
md = instance.flavor.extra_specs
network_config = {}
md_namespace = 'quota:'
params = ['vif_inbound_average', 'vif_inbound_peak',
'vif_outbound_average', 'vif_outbound_peak']
# Get network quotas from flavor metadata and cast the values to int
q = {}
for param in params:
try:
q[param] = int(md.get(md_namespace + param, 0))
except ValueError:
LOG.warning(_LE('Network quota %(p)s must be an integer'),
{'p': param},
instance=instance)
# Since LXD does not implement average NIC IO and number of burst
# bytes, we take the max(vif_*_average, vif_*_peak) to set the peak
# network IO and simply ignore the burst bytes.
# Align values to MBit/s (8 * powers of 1000 in this case), having
# in mind that the values are recieved in Kilobytes/s.
vif_inbound_limit = max(
q.get('vif_inbound_average'),
q.get('vif_inbound_peak')
)
if vif_inbound_limit:
network_config['limits.ingress'] = \
('%s' + 'Mbit') % (vif_inbound_limit * units.k * 8 / units.M)
vif_outbound_limit = max(
q.get('vif_outbound_average'),
q.get('vif_outbound_peak')
)
if vif_outbound_limit:
network_config['limits.egress'] = \
('%s' + 'Mbit') % (vif_outbound_limit * units.k * 8 / units.M)
return network_config
def get_container_source(self, instance):
"""Set the LXD container image for the instance.
:param instance: nova instance object
:return: the container source
"""
LOG.debug('get_container_source called for instance',
instance=instance)
try:
container_source = {'type': 'image',
'alias': str(instance.image_ref)}
if container_source is None:
msg = _('Failed to determine container source for %s') \
% instance.name
raise exception.NovaException(msg)
return container_source
except Exception as ex:
with excutils.save_and_reraise_exception():
LOG.error(
_LE('Failed to configure container source '
'%(instance)s: %(ex)s'),
{'instance': instance.name, 'ex': ex},
instance=instance)
def get_container_migrate(self, container_migrate, host, instance):
"""Create the image source for a migrating container
:container_migrate: the container websocket information
:host: the source host
:instance: nova instance object
return dictionary of the image source
"""
LOG.debug('get_container_migrate called for instance',
instance=instance)
try:
# Generate the container config
container_metadata = container_migrate['metadata']
container_url = 'https://%s:8443%s' \
% (host, container_migrate.get('operation'))
return {
'base_image': '',
'mode': 'pull',
'certificate': str(self.session.host_certificate(instance,
host)),
'operation': str(container_url),
'secrets': container_metadata,
'type': 'migration'
}
except Exception as ex:
with excutils.save_and_reraise_exception():
LOG.error(_LE('Failed to configure migation source '
'%(instance)s: %(ex)s'),
{'instance': instance.name, 'ex': ex},
instance=instance)
def configure_disk_path(self, src_path, dest_path, vfs_type, instance):
"""Configure the host mount point for the LXD container
:param src_path: source path on the house
:param dest_path: destination path on the LXD container
:param vfs_type: dictionary identifier
:param instance: nova instance object
:return: container disk paths
"""
LOG.debug('configure_disk_path called for instance',
instance=instance)
try:
config = {}
config[vfs_type] = {'path': dest_path,
'source': src_path,
'type': 'disk',
'optional': 'True'}
return config
except Exception as ex:
with excutils.save_and_reraise_exception():
LOG.error(_LE('Failed to configure disk for '
'%(instance)s: %(ex)s'),
{'instance': instance.name, 'ex': ex},
instance=instance)
def create_container_net_device(self, instance, vif):
"""Translate nova network object into a LXD interface
:param instance: nova instance object
:param vif: network instaance object
"""
LOG.debug('create_container_net_device called for instance',
insance=instance)
try:
network_config = self.vif_driver.get_config(instance, vif)
config = {}
config[self.get_network_device(instance)] = {
'nictype': 'bridged',
'hwaddr': str(vif['address']),
'parent': str(network_config['bridge']),
'type': 'nic'}
return config
except Exception as ex:
LOG.error(_LE('Failed to configure network for '
'%(instance)s: %(ex)s'),
{'instance': instance.name, 'ex': ex},
instance=instance)
def get_network_device(self, instance):
"""Try to detect which network interfaces are available in a contianer
:param instance: nova instance object
"""
LOG.debug('get_network_device called for instance', instance=instance)
data = self.session.container_info(instance)
lines = open('/proc/%s/net/dev' % data['init']).readlines()
interfaces = []
for line in lines[2:]:
if line.find(':') < 0:
continue
face, _ = line.split(':')
if 'eth' in face:
interfaces.append(face.strip())
if len(interfaces) == 1:
return 'eth1'
else:
return 'eth%s' % int(len(interfaces) - 1)