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>
This commit is contained in:
Chuck Short 2016-07-05 10:55:27 -04:00
parent a1d57ddcb5
commit b03c33ce64
6 changed files with 151 additions and 26 deletions

View File

@ -38,7 +38,7 @@ class MockConf(mock.Mock):
lxd_default = { lxd_default = {
'root_dir': '/fake/lxd/root', 'root_dir': '/fake/lxd/root',
'timeout': 20, 'timeout': 20,
'retry_interval': 2 'retry_interval': 2,
} }
lxd_default.update(lxd_kwargs) lxd_default.update(lxd_kwargs)
self.lxd = mock.Mock(lxd_args, **lxd_default) self.lxd = mock.Mock(lxd_args, **lxd_default)

View File

@ -70,8 +70,10 @@ class LXDTestContainerConfig(test.NoDBTestCase):
instance = stubs._fake_instance() instance = stubs._fake_instance()
network_info = fake_network.fake_get_instance_nw_info(self) network_info = fake_network.fake_get_instance_nw_info(self)
mock_config_drive.return_value = False mock_config_drive.return_value = False
block_device_info = mock.Mock()
container_profile = self.config.create_profile(instance, network_info) container_profile = self.config.create_profile(
instance, network_info, block_device_info)
self.assertEqual(container_profile['name'], instance.name) self.assertEqual(container_profile['name'], instance.name)
self.assertEqual(container_profile['config'], self.assertEqual(container_profile['config'],
{'boot.autostart': 'True', {'boot.autostart': 'True',
@ -96,8 +98,10 @@ class LXDTestContainerConfig(test.NoDBTestCase):
"""Verify the LXD profile without network configuration.""" """Verify the LXD profile without network configuration."""
instance = stubs._fake_instance() instance = stubs._fake_instance()
mock_config_drive.return_value = False mock_config_drive.return_value = False
block_device_info = mock.Mock()
container_profile = self.config.create_profile(instance, None) container_profile = self.config.create_profile(
instance, None, block_device_info)
self.assertEqual(len(container_profile['devices']), 1) self.assertEqual(len(container_profile['devices']), 1)
self.assertEqual(container_profile['devices']['root'], self.assertEqual(container_profile['devices']['root'],
{'path': '/', 'size': '10GB', 'type': 'disk'}) {'path': '/', 'size': '10GB', 'type': 'disk'})
@ -109,7 +113,9 @@ class LXDTestContainerConfig(test.NoDBTestCase):
"""Verify the LXD profile with both network and configdrive enabled.""" """Verify the LXD profile with both network and configdrive enabled."""
instance = stubs._fake_instance() instance = stubs._fake_instance()
network_info = fake_network.fake_get_instance_nw_info(self) network_info = fake_network.fake_get_instance_nw_info(self)
container_profile = self.config.create_profile(instance, network_info) block_device_info = mock.Mock()
container_profile = self.config.create_profile(
instance, network_info, block_device_info)
mock_configdrive.return_value = True mock_configdrive.return_value = True
self.assertEqual(len(container_profile['devices']), 3) self.assertEqual(len(container_profile['devices']), 3)
@ -134,7 +140,9 @@ class LXDTestContainerConfig(test.NoDBTestCase):
def test_create_contianer_profile_configdrive_network(self, mock_config): def test_create_contianer_profile_configdrive_network(self, mock_config):
"""Verify the LXD profile with no network and configdrive enabled.""" """Verify the LXD profile with no network and configdrive enabled."""
instance = stubs._fake_instance() instance = stubs._fake_instance()
container_profile = self.config.create_profile(instance, None) block_device_info = mock.Mock()
container_profile = self.config.create_profile(
instance, None, block_device_info)
mock_config.return_value = True mock_config.return_value = True
self.assertEqual(len(container_profile['devices']), 2) self.assertEqual(len(container_profile['devices']), 2)
@ -143,11 +151,90 @@ class LXDTestContainerConfig(test.NoDBTestCase):
'path': 'var/lib/cloud/data', 'path': 'var/lib/cloud/data',
'source': '/fake/instances/path/' 'source': '/fake/instances/path/'
'instance-00000001/configdrive', 'instance-00000001/configdrive',
'type': 'disk'}, 'type': 'disk'}
) )
self.assertEqual(container_profile['devices']['root'], self.assertEqual(container_profile['devices']['root'],
{'path': '/', 'size': '10GB', 'type': 'disk'}) {'path': '/', 'size': '10GB', 'type': 'disk'})
@mock.patch.object(session.LXDAPISession, 'get_host_config',
mock.Mock(return_value={'storage': 'zfs'}))
def test_create_container_profile_with_ephemeral_and_network(self):
"""Verify the LXD profile with ephemeral drive is enabled."""
instance = stubs._fake_instance()
instance.ephemeral_gb = 1
network_info = fake_network.fake_get_instance_nw_info(self)
block_device_info = {'ephemerals': [{'virtual_name': 'ephemeral0'}]}
container_profile = self.config.create_profile(
instance, network_info, block_device_info)
self.assertEqual(len(container_profile['devices']), 3)
self.assertEqual(container_profile['devices']['root'],
{'path': '/', 'size': '10GB', 'type': 'disk'})
self.assertEqual(container_profile['devices']['fake_br1'],
{'hwaddr': 'DE:AD:BE:EF:00:01',
'nictype': 'bridged',
'parent': 'fake_br1',
'type': 'nic'})
self.assertEqual(container_profile['devices']['ephemeral0'],
{'optional': 'True',
'path': '/mnt',
'source': '/fake/instances/path/'
'instance-00000001/storage/ephemeral0',
'type': 'disk'}
)
@mock.patch.object(session.LXDAPISession, 'get_host_config',
mock.Mock(return_value={'storage': 'zfs'}))
@mock.patch('nova.virt.configdrive.required_by')
def test_create_container_profile_with_ephemeral_and_configdrive(
self, mock_configdrive):
"""Verify container profile configuration with ephemeral and
configdrive.
"""
instance = stubs._fake_instance()
instance.ephemeral_gb = 1
network_info = fake_network.fake_get_instance_nw_info(self)
block_device_info = {'ephemerals': [{'virtual_name': 'ephemeral0'}]}
mock_configdrive.return_value = True
container_profile = self.config.create_profile(
instance, network_info, block_device_info)
self.assertEqual(len(container_profile['devices']), 4)
self.assertEqual(container_profile['devices']['root'],
{'path': '/', 'size': '10GB', 'type': 'disk'})
self.assertEqual(container_profile['devices']['fake_br1'],
{'hwaddr': 'DE:AD:BE:EF:00:01',
'nictype': 'bridged',
'parent': 'fake_br1',
'type': 'nic'})
self.assertEqual(container_profile['devices']['ephemeral0'],
{'optional': 'True',
'path': '/mnt',
'source': '/fake/instances/path/'
'instance-00000001/storage/ephemeral0',
'type': 'disk'}
)
@mock.patch.object(session.LXDAPISession, 'get_host_config',
mock.Mock(return_value={'storage': 'zfs'}))
def test_create_container_profile_with_ephemeral_and_no_network(self):
"""Verify container profile configuration with ephemeral and
without network.
"""
instance = stubs._fake_instance()
instance.ephemeral_gb = 1
block_device_info = {'ephemerals': [{'virtual_name': 'ephemeral0'}]}
container_profile = self.config.create_profile(
instance, None, block_device_info)
self.assertEqual(len(container_profile['devices']), 2)
self.assertEqual(container_profile['devices']['root'],
{'path': '/', 'size': '10GB', 'type': 'disk'})
self.assertEqual(container_profile['devices']['ephemeral0'],
{'optional': 'True',
'path': '/mnt',
'source': '/fake/instances/path/'
'instance-00000001/storage/ephemeral0',
'type': 'disk'}
)
def test_create_network(self): def test_create_network(self):
instance = stubs._fake_instance() instance = stubs._fake_instance()
instance_name = 'fake_instance' instance_name = 'fake_instance'

View File

@ -230,7 +230,7 @@ class LXDTestDriver(test.NoDBTestCase):
context, instance, image_meta) context, instance, image_meta)
mock_plug_vif.assert_called_once_with(instance, network_info) mock_plug_vif.assert_called_once_with(instance, network_info)
mock_container_profile.assert_called_once_with( mock_container_profile.assert_called_once_with(
instance, network_info) instance, network_info, block_device_info)
mock_profile_create.assert_called_once_with({}, instance) mock_profile_create.assert_called_once_with({}, instance)
mock_container_init.assert_called_once_with(container_config, mock_container_init.assert_called_once_with(container_config,
instance) instance)
@ -302,7 +302,7 @@ class LXDTestDriver(test.NoDBTestCase):
context, instance, image_meta) context, instance, image_meta)
mock_plug_vif.assert_called_once_with(instance, network_info) mock_plug_vif.assert_called_once_with(instance, network_info)
mock_container_profile.assert_called_once_with( mock_container_profile.assert_called_once_with(
instance, network_info) instance, network_info, block_device_info)
mock_profile_create.assert_called_once_with({}, instance) mock_profile_create.assert_called_once_with({}, instance)
mock_configdrive.assert_called_once_with(instance) mock_configdrive.assert_called_once_with(instance)
mock_add_configdrive.assert_called_once_with( mock_add_configdrive.assert_called_once_with(

View File

@ -68,7 +68,7 @@ class LXDContainerConfig(object):
{'instance': instance_name, 'ex': ex}, {'instance': instance_name, 'ex': ex},
instance=instance) instance=instance)
def create_profile(self, instance, network_info): def create_profile(self, instance, network_info, block_device_info):
"""Create a LXD container profile configuration """Create a LXD container profile configuration
:param instance: nova instance object :param instance: nova instance object
@ -86,10 +86,23 @@ class LXDContainerConfig(object):
# Restrict the size of the "/" disk # Restrict the size of the "/" disk
config['devices'] = self.configure_container_root(instance) config['devices'] = self.configure_container_root(instance)
if network_info: if instance.get('ephemeral_gb', 0) != 0:
config['devices'].update(self.create_network(instance_name, ephemerals = block_device_info.get('ephemerals', [])
instance,
network_info)) 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 # if a configdrive is required, setup the mount point for
# the container # the container
@ -102,6 +115,11 @@ class LXDContainerConfig(object):
'configdrive', instance) 'configdrive', instance)
config['devices'].update(config_drive) config['devices'].update(config_drive)
if network_info:
config['devices'].update(self.create_network(instance_name,
instance,
network_info))
return config return config
except Exception as ex: except Exception as ex:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
@ -173,13 +191,9 @@ class LXDContainerConfig(object):
try: try:
config = {} config = {}
lxd_config = self.session.get_host_config(instance) lxd_config = self.session.get_host_config(instance)
config.setdefault('root', {'type': 'disk', 'path': '/'})
if str(lxd_config['storage']) in ['btrfs', 'zfs']: if str(lxd_config['storage']) in ['btrfs', 'zfs']:
config['root'] = {'path': '/', config['root'].update({'size': '%sGB' % str(instance.root_gb)})
'type': 'disk',
'size': '%sGB' % str(instance.root_gb)}
else:
config['root'] = {'path': '/',
'type': 'disk'}
# Set disk quotas # Set disk quotas
config['root'].update(self.create_disk_quota_config(instance)) config['root'].update(self.create_disk_quota_config(instance))

View File

@ -206,7 +206,8 @@ class LXDDriver(driver.ComputeDriver):
# Create the container profile # Create the container profile
container_profile = self.config.create_profile(instance, container_profile = self.config.create_profile(instance,
network_info) network_info,
block_device_info)
self.session.profile_create(container_profile, instance) self.session.profile_create(container_profile, instance)
# Create the container # Create the container
@ -222,6 +223,8 @@ class LXDDriver(driver.ComputeDriver):
if configdrive.required_by(instance): if configdrive.required_by(instance):
self._add_configdrive(instance, injected_files) self._add_configdrive(instance, injected_files)
self._add_ephemeral(block_device_info, instance)
# Start the container # Start the container
self.session.container_start(instance_name, instance) self.session.container_start(instance_name, instance)
@ -232,6 +235,27 @@ class LXDDriver(driver.ComputeDriver):
{'instance': instance.name, 'ex': ex}, {'instance': instance.name, 'ex': ex},
instance=instance) instance=instance)
def _add_ephemeral(self, block_device_info, instance):
if instance.get('ephemeral_gb', 0) != 0:
ephemerals = block_device_info.get('ephemerals', [])
root_dir = container_utils.get_container_rootfs(instance.name)
if ephemerals == []:
ephemeral_src = container_utils.get_container_storage(
ephemerals['virtual_name'], instance.name)
fileutils.ensure_tree(ephemeral_src)
utils.execute('chown',
os.stat(root_dir).st_uid,
ephemeral_src, run_as_root=True)
else:
for id, ephx in enumerate(ephemerals):
ephemeral_src = container_utils.get_container_storage(
ephx['virtual_name'], instance.name)
fileutils.ensure_tree(ephemeral_src)
utils.execute('chown',
os.stat(root_dir).st_uid,
ephemeral_src, run_as_root=True)
def _add_configdrive(self, instance, injected_files): def _add_configdrive(self, instance, injected_files):
"""Configure the config drive for the container """Configure the config drive for the container
@ -309,15 +333,11 @@ class LXDDriver(driver.ComputeDriver):
self.unplug_vifs(instance, network_info) self.unplug_vifs(instance, network_info)
name = pwd.getpwuid(os.getuid()).pw_name name = pwd.getpwuid(os.getuid()).pw_name
configdrive_dir = \
container_utils.get_container_configdrive(instance.name)
if os.path.exists(configdrive_dir):
utils.execute('chown', '-R', '%s:%s' % (name, name),
configdrive_dir, run_as_root=True)
shutil.rmtree(configdrive_dir)
container_dir = container_utils.get_instance_dir(instance.name) container_dir = container_utils.get_instance_dir(instance.name)
if os.path.exists(container_dir): if os.path.exists(container_dir):
utils.execute('chown', '-R', '%s:%s' % (name, name),
container_dir, run_as_root=True)
shutil.rmtree(container_dir) shutil.rmtree(container_dir)
except Exception as ex: except Exception as ex:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():

View File

@ -34,6 +34,10 @@ def get_container_manifest_image(image_meta):
return os.path.join(BASE_DIR, '%s-manifest.tar' % image_meta.id) return os.path.join(BASE_DIR, '%s-manifest.tar' % image_meta.id)
def get_container_storage(ephemeral, instance):
return os.path.join(CONF.instances_path, instance, 'storage', ephemeral)
def get_container_configdrive(instance): def get_container_configdrive(instance):
return os.path.join(CONF.instances_path, instance, 'configdrive') return os.path.join(CONF.instances_path, instance, 'configdrive')