Merge "Add encryption support for volumes to libvirt"

This commit is contained in:
Jenkins 2013-09-11 19:19:58 +00:00 committed by Gerrit Code Review
commit 4d89bda3d7
30 changed files with 824 additions and 70 deletions

View File

@ -198,6 +198,8 @@ systool: CommandFilter, systool, root
# nova/virt/libvirt/volume.py:
sginfo: CommandFilter, sginfo, root
sg_scan: CommandFilter, sg_scan, root
cryptsetup: CommandFilter, cryptsetup, root
ln: RegExpFilter, ln, root, ln, --symbolic, --force, /dev/mapper/ip-.*-iscsi-iqn.2010-10.org.openstack:volume-.*, /dev/disk/by-path/ip-.*-iscsi-iqn.2010-10.org.openstack:volume-.*
# nova/virt/xenapi/vm_utils.py:
xenstore-read: CommandFilter, xenstore-read, root

View File

@ -83,6 +83,7 @@ from nova.virt import event as virtevent
from nova.virt import storage_users
from nova.virt import virtapi
from nova import volume
from nova.volume import encryptors
compute_opts = [
@ -1414,7 +1415,6 @@ class ComputeManager(manager.SchedulerDependentManager):
injected_files, admin_password,
network_info,
block_device_info)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_('Instance failed to spawn'), instance=instance)
@ -1654,8 +1654,8 @@ class ComputeManager(manager.SchedulerDependentManager):
# NOTE(melwitt): attempt driver destroy before releasing ip, may
# want to keep ip allocated for certain failures
try:
self.driver.destroy(instance, network_info,
block_device_info)
self.driver.destroy(instance, network_info, block_device_info,
context=context)
except exception.InstancePowerOffFailure:
# if the instance can't power off, don't release the ip
with excutils.save_and_reraise_exception():
@ -3598,10 +3598,14 @@ class ComputeManager(manager.SchedulerDependentManager):
if 'serial' not in connection_info:
connection_info['serial'] = volume_id
encryption = encryptors.get_encryption_metadata(context, volume_id,
connection_info)
try:
self.driver.attach_volume(connection_info,
self.driver.attach_volume(context,
connection_info,
instance,
mountpoint)
mountpoint,
encryption=encryption)
except Exception: # pylint: disable=W0702
with excutils.save_and_reraise_exception():
LOG.exception(_("Failed to attach volume %(volume_id)s "
@ -3651,9 +3655,13 @@ class ComputeManager(manager.SchedulerDependentManager):
if not self.driver.instance_exists(instance['name']):
LOG.warn(_('Detaching volume from unknown instance'),
context=context, instance=instance)
encryption = encryptors.get_encryption_metadata(context, volume_id,
connection_info)
self.driver.detach_volume(connection_info,
instance,
mp)
mp,
encryption=encryption)
except Exception: # pylint: disable=W0702
with excutils.save_and_reraise_exception():
LOG.exception(_('Failed to detach volume %(volume_id)s '

View File

@ -368,6 +368,10 @@ class ComputeVolumeTestCase(BaseTestCase):
store_cinfo)
def test_attach_volume_serial(self):
def fake_get_volume_encryption_metadata(self, context, volume_id):
return {}
self.stubs.Set(cinder.API, 'get_volume_encryption_metadata',
fake_get_volume_encryption_metadata)
instance = self._create_fake_instance()
self.compute.attach_volume(self.context, self.volume_id,
@ -574,6 +578,11 @@ class ComputeVolumeTestCase(BaseTestCase):
self.mox.ReplayAll()
def fake_get_volume_encryption_metadata(self, context, volume_id):
return {}
self.stubs.Set(cinder.API, 'get_volume_encryption_metadata',
fake_get_volume_encryption_metadata)
self.compute.attach_volume(self.context, 1, '/dev/vdb', instance)
# Poll volume usage & then detach the volume. This will update the
@ -2860,8 +2869,10 @@ class ComputeTestCase(BaseTestCase):
self.mox.StubOutWithMock(self.compute.driver, 'destroy')
self.mox.StubOutWithMock(self.compute, '_deallocate_network')
exp = exception.InstancePowerOffFailure(reason='')
self.compute.driver.destroy(mox.IgnoreArg(), mox.IgnoreArg(),
mox.IgnoreArg()).AndRaise(exp)
self.compute.driver.destroy(mox.IgnoreArg(),
mox.IgnoreArg(),
mox.IgnoreArg(),
context=mox.IgnoreArg()).AndRaise(exp)
# mox will detect if _deallocate_network gets called unexpectedly
self.mox.ReplayAll()
instance = self._create_fake_instance()
@ -2875,8 +2886,10 @@ class ComputeTestCase(BaseTestCase):
self.mox.StubOutWithMock(self.compute.driver, 'destroy')
self.mox.StubOutWithMock(self.compute, '_deallocate_network')
exp = test.TestingException()
self.compute.driver.destroy(mox.IgnoreArg(), mox.IgnoreArg(),
mox.IgnoreArg()).AndRaise(exp)
self.compute.driver.destroy(mox.IgnoreArg(),
mox.IgnoreArg(),
mox.IgnoreArg(),
context=mox.IgnoreArg()).AndRaise(exp)
self.compute._deallocate_network(mox.IgnoreArg(),
mox.IgnoreArg(),
mox.IgnoreArg())
@ -3388,6 +3401,11 @@ class ComputeTestCase(BaseTestCase):
return volume
self.stubs.Set(cinder.API, "get", fake_volume_get)
def fake_get_volume_encryption_metadata(self, context, volume_id):
return {}
self.stubs.Set(cinder.API, 'get_volume_encryption_metadata',
fake_get_volume_encryption_metadata)
orig_connection_data = {
'target_discovered': True,
'target_iqn': 'iqn.2010-10.org.openstack:%s.1' % volume_id,
@ -9115,7 +9133,7 @@ class ComputeInjectedFilesTestCase(BaseTestCase):
self.stubs.Set(self.compute.driver, 'spawn', self._spawn)
def _spawn(self, context, instance, image_meta, injected_files,
admin_password, nw_info, block_device_info):
admin_password, nw_info, block_device_info, db_api=None):
self.assertEqual(self.expected, injected_files)
def _test(self, injected_files, decoded_files):

View File

@ -20,7 +20,6 @@ Test cases for the single key manager.
import array
from nova import context
from nova import exception
from nova.keymgr import key
from nova.tests.keymgr import single_key_mgr
@ -35,8 +34,6 @@ class SingleKeyManagerTestCase(test_mock_key_mgr.MockKeyManagerTestCase):
def setUp(self):
super(SingleKeyManagerTestCase, self).setUp()
self.ctxt = context.RequestContext('fake', 'fake')
self.key_id = '00000000-0000-0000-0000-000000000000'
encoded = array.array('B', ('0' * 64).decode('hex')).tolist()
self.key = key.SymmetricKey('AES', encoded)

View File

@ -1214,7 +1214,8 @@ class HyperVAPITestCase(test.TestCase):
target_portal)
self._mox.ReplayAll()
self._conn.attach_volume(connection_info, instance_data, mount_point)
self._conn.attach_volume(None, connection_info, instance_data,
mount_point)
self._mox.VerifyAll()
self.assertEquals(len(self._instance_volume_disks), 1)

View File

@ -2293,7 +2293,7 @@ class LibvirtConnTestCase(test.TestCase):
self.mox.ReplayAll()
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
self.assertRaises(exception.VolumeDriverNotFound,
conn.attach_volume,
conn.attach_volume, None,
{"driver_volume_type": "badtype"},
{"name": "fake-instance"},
"/dev/sda")
@ -2305,7 +2305,7 @@ class LibvirtConnTestCase(test.TestCase):
self.mox.ReplayAll()
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
self.assertRaises(exception.InvalidHypervisorType,
conn.attach_volume,
conn.attach_volume, None,
{"driver_volume_type": "fake",
"data": {"logical_block_size": "4096",
"physical_block_size": "4096"}
@ -2323,7 +2323,7 @@ class LibvirtConnTestCase(test.TestCase):
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
self.stubs.Set(self.conn, "getLibVersion", get_lib_version_stub)
self.assertRaises(exception.Invalid,
conn.attach_volume,
conn.attach_volume, None,
{"driver_volume_type": "fake",
"data": {"logical_block_size": "4096",
"physical_block_size": "4096"}

View File

@ -278,8 +278,8 @@ class TestDriverBlockDevice(test.TestCase):
instance = {'id': 'fake_id', 'uuid': 'fake_uuid'}
volume = {'id': 'fake-volume-id-1'}
connector = {'ip': 'fake_ip', 'host': 'fake_host'}
connection_info = {'data': 'fake_data'}
expected_conn_info = {'data': 'fake_data',
connection_info = {'data': {}}
expected_conn_info = {'data': {},
'serial': 'fake-volume-id-1'}
self.volume_api.get(self.context,
@ -308,8 +308,8 @@ class TestDriverBlockDevice(test.TestCase):
instance = {'id': 'fake_id', 'uuid': 'fake_uuid'}
connector = {'ip': 'fake_ip', 'host': 'fake_host'}
connection_info = {'data': 'fake_data'}
expected_conn_info = {'data': 'fake_data',
connection_info = {'data': {}}
expected_conn_info = {'data': {},
'serial': 'fake-volume-id-2'}
self.virt_driver.get_volume_connector(instance).AndReturn(connector)

View File

@ -420,17 +420,19 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase):
@catch_notimplementederror
def test_attach_detach_volume(self):
instance_ref, network_info = self._get_running_instance()
self.connection.attach_volume({'driver_volume_type': 'fake'},
instance_ref,
connection_info = {
"driver_volume_type": "fake",
"serial": "fake_serial",
}
self.connection.attach_volume(None, connection_info, instance_ref,
'/dev/sda')
self.connection.detach_volume({'driver_volume_type': 'fake'},
instance_ref,
self.connection.detach_volume(connection_info, instance_ref,
'/dev/sda')
@catch_notimplementederror
def test_swap_volume(self):
instance_ref, network_info = self._get_running_instance()
self.connection.attach_volume({'driver_volume_type': 'fake'},
self.connection.attach_volume(None, {'driver_volume_type': 'fake'},
instance_ref,
'/dev/sda')
self.connection.swap_volume({'driver_volume_type': 'fake'},
@ -441,9 +443,12 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase):
@catch_notimplementederror
def test_attach_detach_different_power_states(self):
instance_ref, network_info = self._get_running_instance()
connection_info = {
"driver_volume_type": "fake",
"serial": "fake_serial",
}
self.connection.power_off(instance_ref)
self.connection.attach_volume({'driver_volume_type': 'fake'},
instance_ref,
self.connection.attach_volume(None, connection_info, instance_ref,
'/dev/sda')
bdm = {
@ -463,7 +468,7 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase):
}]
}
self.connection.power_on(self.ctxt, instance_ref, network_info, bdm)
self.connection.detach_volume({'driver_volume_type': 'fake'},
self.connection.detach_volume(connection_info,
instance_ref,
'/dev/sda')

View File

@ -687,7 +687,8 @@ class VMwareAPIVMTestCase(test.TestCase):
volumeops.VMwareVolumeOps._attach_volume_vmdk(connection_info,
self.instance, mount_point)
self.mox.ReplayAll()
self.conn.attach_volume(connection_info, self.instance, mount_point)
self.conn.attach_volume(None, connection_info, self.instance,
mount_point)
def test_volume_detach_vmdk(self):
self._create_vm()
@ -723,7 +724,8 @@ class VMwareAPIVMTestCase(test.TestCase):
controller_key=mox.IgnoreArg(),
unit_number=mox.IgnoreArg())
self.mox.ReplayAll()
self.conn.attach_volume(connection_info, self.instance, mount_point)
self.conn.attach_volume(None, connection_info, self.instance,
mount_point)
def test_detach_vmdk_disk_from_vm(self):
self._create_vm()
@ -756,7 +758,8 @@ class VMwareAPIVMTestCase(test.TestCase):
volumeops.VMwareVolumeOps._attach_volume_iscsi(connection_info,
self.instance, mount_point)
self.mox.ReplayAll()
self.conn.attach_volume(connection_info, self.instance, mount_point)
self.conn.attach_volume(None, connection_info, self.instance,
mount_point)
def test_volume_detach_iscsi(self):
self._create_vm()
@ -788,7 +791,8 @@ class VMwareAPIVMTestCase(test.TestCase):
unit_number=mox.IgnoreArg(),
device_name=mox.IgnoreArg())
self.mox.ReplayAll()
self.conn.attach_volume(connection_info, self.instance, mount_point)
self.conn.attach_volume(None, connection_info, self.instance,
mount_point)
def test_detach_iscsi_disk_from_vm(self):
self._create_vm()

View File

@ -286,7 +286,7 @@ class XenAPIVolumeTestCase(stubs.XenAPITestBase):
conn = xenapi_conn.XenAPIDriver(fake.FakeVirtAPI(), False)
instance = db.instance_create(self.context, self.instance_values)
vm = xenapi_fake.create_vm(instance['name'], 'Running')
result = conn.attach_volume(self._make_connection_info(),
result = conn.attach_volume(None, self._make_connection_info(),
instance, '/dev/sdc')
# check that the VM has a VBD attached to it
@ -305,9 +305,8 @@ class XenAPIVolumeTestCase(stubs.XenAPITestBase):
xenapi_fake.create_vm(instance['name'], 'Running')
self.assertRaises(exception.VolumeDriverNotFound,
conn.attach_volume,
{'driver_volume_type': 'nonexist'},
instance,
'/dev/sdc')
None, {'driver_volume_type': 'nonexist'},
instance, '/dev/sdc')
class XenAPIVMTestCase(stubs.XenAPITestBase):

View File

@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# 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.

View File

@ -0,0 +1,40 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# 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.
from nova import keymgr
from nova import test
from nova.tests.keymgr import fake
class VolumeEncryptorTestCase(test.TestCase):
def _create(self, device_path):
pass
def setUp(self):
super(VolumeEncryptorTestCase, self).setUp()
self.stubs.Set(keymgr, 'API', fake.fake_api)
self.connection_info = {
"data": {
"device_path": "/dev/disk/by-path/"
"ip-192.0.2.0:3260-iscsi-iqn.2010-10.org.openstack"
":volume-fake_uuid-lun-1",
},
}
self.encryptor = self._create(self.connection_info)

View File

@ -0,0 +1,85 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# 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 array
import os
from nova.keymgr import key
from nova.tests.volume.encryptors import test_base
from nova import utils
from nova.volume.encryptors import cryptsetup
def fake__get_key(context):
raw = array.array('B', ('0' * 64).decode('hex')).tolist()
symmetric_key = key.SymmetricKey('AES', raw)
return symmetric_key
class CryptsetupEncryptorTestCase(test_base.VolumeEncryptorTestCase):
def _create(self, connection_info):
return cryptsetup.CryptsetupEncryptor(connection_info)
def setUp(self):
super(CryptsetupEncryptorTestCase, self).setUp()
self.executes = []
def fake_execute(*cmd, **kwargs):
self.executes.append(cmd)
return None, None
self.stubs.Set(utils, 'execute', fake_execute)
self.stubs.Set(os.path, "realpath", lambda x: x)
self.dev_path = self.connection_info['data']['device_path']
self.dev_name = self.dev_path.split('/')[-1]
self.symlink_path = self.dev_path
def test__open_volume(self):
self.encryptor._open_volume("passphrase")
expected_commands = [('cryptsetup', 'create', '--key-file=-',
self.dev_name, self.dev_path)]
self.assertEqual(expected_commands, self.executes)
def test_attach_volume(self):
self.stubs.Set(self.encryptor, '_get_key', fake__get_key)
self.encryptor.attach_volume(None)
expected_commands = [('cryptsetup', 'create', '--key-file=-',
self.dev_name, self.dev_path),
('ln', '--symbolic', '--force',
'/dev/mapper/%s' % self.dev_name,
self.symlink_path)]
self.assertEqual(expected_commands, self.executes)
def test__close_volume(self):
self.encryptor.detach_volume()
expected_commands = [('cryptsetup', 'remove', self.dev_name)]
self.assertEqual(expected_commands, self.executes)
def test_detach_volume(self):
self.encryptor.detach_volume()
expected_commands = [('cryptsetup', 'remove', self.dev_name)]
self.assertEqual(expected_commands, self.executes)

View File

@ -0,0 +1,76 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# 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.
from nova.tests.volume.encryptors import test_cryptsetup
from nova.volume.encryptors import luks
"""
The utility of these test cases is limited given the simplicity of the
LuksEncryptor class. The attach_volume method has the only significant logic
to handle cases where the volume has not previously been formatted, but
exercising this logic requires "real" devices and actually executing the
various cryptsetup commands rather than simply logging them.
"""
class LuksEncryptorTestCase(test_cryptsetup.CryptsetupEncryptorTestCase):
def _create(self, connection_info):
return luks.LuksEncryptor(connection_info)
def setUp(self):
super(LuksEncryptorTestCase, self).setUp()
def test__format_volume(self):
self.encryptor._format_volume("passphrase")
expected_commands = [('cryptsetup', '--batch-mode', 'luksFormat',
'--key-file=-', self.dev_path)]
self.assertEqual(expected_commands, self.executes)
def test__open_volume(self):
self.encryptor._open_volume("passphrase")
expected_commands = [('cryptsetup', 'luksOpen', '--key-file=-',
self.dev_path, self.dev_name)]
self.assertEqual(expected_commands, self.executes)
def test_attach_volume(self):
self.stubs.Set(self.encryptor, '_get_key',
test_cryptsetup.fake__get_key)
self.encryptor.attach_volume(None)
expected_commands = [('cryptsetup', 'luksOpen', '--key-file=-',
self.dev_path, self.dev_name),
('ln', '--symbolic', '--force',
'/dev/mapper/%s' % self.dev_name,
self.symlink_path)]
self.assertEqual(expected_commands, self.executes)
def test__close_volume(self):
self.encryptor.detach_volume()
expected_commands = [('cryptsetup', 'luksClose', self.dev_name)]
self.assertEqual(expected_commands, self.executes)
def test_detach_volume(self):
self.encryptor.detach_volume()
expected_commands = [('cryptsetup', 'luksClose', self.dev_name)]
self.assertEqual(expected_commands, self.executes)

View File

@ -0,0 +1,33 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# 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.
from nova.tests.volume.encryptors import test_base
from nova.volume.encryptors import nop
class NoOpEncryptorTestCase(test_base.VolumeEncryptorTestCase):
def _create(self, connection_info):
return nop.NoOpEncryptor(connection_info)
def setUp(self):
super(NoOpEncryptorTestCase, self).setUp()
def test_attach_volume(self):
self.encryptor.attach_volume(None)
def test_detach_volume(self):
self.encryptor.detach_volume()

View File

@ -296,3 +296,15 @@ class CinderApiTestCase(test.TestCase):
'id1', {'status': 'error', 'progress': '90%'})
self.mox.ReplayAll()
self.api.update_snapshot_status(self.ctx, 'id1', 'error')
def test_get_volume_encryption_metadata(self):
cinder.cinderclient(self.ctx).AndReturn(self.cinderclient)
self.mox.StubOutWithMock(self.cinderclient.volumes,
'get_encryption_metadata')
self.cinderclient.volumes.\
get_encryption_metadata({'encryption_key_id': 'fake_key'})
self.mox.ReplayAll()
self.api.get_volume_encryption_metadata(self.ctx,
{'encryption_key_id':
'fake_key'})

View File

@ -196,7 +196,7 @@ class BareMetalDriver(driver.ComputeDriver):
for vol in block_device_mapping:
connection_info = vol['connection_info']
mountpoint = vol['mount_device']
self.attach_volume(
self.attach_volume(None,
connection_info, instance['name'], mountpoint)
def _detach_block_devices(self, instance, block_device_info):
@ -294,7 +294,8 @@ class BareMetalDriver(driver.ComputeDriver):
"for instance %r") % instance['uuid'])
_update_state(ctx, node, instance, state)
def destroy(self, instance, network_info, block_device_info=None):
def destroy(self, instance, network_info, block_device_info=None,
context=None):
context = nova_context.get_admin_context()
try:
@ -354,11 +355,13 @@ class BareMetalDriver(driver.ComputeDriver):
def get_volume_connector(self, instance):
return self.volume_driver.get_volume_connector(instance)
def attach_volume(self, connection_info, instance, mountpoint):
def attach_volume(self, context, connection_info, instance, mountpoint,
encryption=None):
return self.volume_driver.attach_volume(connection_info,
instance, mountpoint)
def detach_volume(self, connection_info, instance_name, mountpoint):
def detach_volume(self, connection_info, instance_name, mountpoint,
encryption=None):
return self.volume_driver.detach_volume(connection_info,
instance_name, mountpoint)

View File

@ -320,11 +320,13 @@ class ComputeDriver(object):
# TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def attach_volume(self, connection_info, instance, mountpoint):
def attach_volume(self, context, connection_info, instance, mountpoint,
encryption=None):
"""Attach the disk to the instance at mountpoint using info."""
raise NotImplementedError()
def detach_volume(self, connection_info, instance, mountpoint):
def detach_volume(self, connection_info, instance, mountpoint,
encryption=None):
"""Detach the disk attached to the instance."""
raise NotImplementedError()

View File

@ -207,7 +207,7 @@ class FakeDriver(driver.ComputeDriver):
pass
def destroy(self, instance, network_info, block_device_info=None,
destroy_disks=True):
destroy_disks=True, context=None):
key = instance['name']
if key in self.instances:
del self.instances[key]
@ -216,7 +216,8 @@ class FakeDriver(driver.ComputeDriver):
{'key': key,
'inst': self.instances}, instance=instance)
def attach_volume(self, connection_info, instance, mountpoint):
def attach_volume(self, context, connection_info, instance, mountpoint,
encryption=None):
"""Attach the disk to the instance at mountpoint using info."""
instance_name = instance['name']
if instance_name not in self._mounts:
@ -224,7 +225,8 @@ class FakeDriver(driver.ComputeDriver):
self._mounts[instance_name][mountpoint] = connection_info
return True
def detach_volume(self, connection_info, instance, mountpoint):
def detach_volume(self, connection_info, instance, mountpoint,
encryption=None):
"""Detach the disk attached to the instance."""
try:
del self._mounts[instance['name']][mountpoint]

View File

@ -59,18 +59,20 @@ class HyperVDriver(driver.ComputeDriver):
self._vmops.reboot(instance, network_info, reboot_type)
def destroy(self, instance, network_info, block_device_info=None,
destroy_disks=True):
destroy_disks=True, context=None):
self._vmops.destroy(instance, network_info, block_device_info,
destroy_disks)
def get_info(self, instance):
return self._vmops.get_info(instance)
def attach_volume(self, connection_info, instance, mountpoint):
def attach_volume(self, context, connection_info, instance, mountpoint,
encryption=None):
return self._volumeops.attach_volume(connection_info,
instance['name'])
def detach_volume(self, connection_info, instance, mountpoint):
def detach_volume(self, connection_info, instance, mountpoint,
encryption=None):
return self._volumeops.detach_volume(connection_info,
instance['name'])

View File

@ -100,6 +100,7 @@ from nova.virt.libvirt import imagecache
from nova.virt.libvirt import utils as libvirt_utils
from nova.virt import netutils
from nova import volume
from nova.volume import encryptors
native_threading = patcher.original("threading")
native_Queue = patcher.original("Queue")
@ -828,9 +829,10 @@ class LibvirtDriver(driver.ComputeDriver):
self._destroy(instance)
def destroy(self, instance, network_info, block_device_info=None,
destroy_disks=True):
destroy_disks=True, context=None):
self._destroy(instance)
self._cleanup(instance, network_info, block_device_info, destroy_disks)
self._cleanup(instance, network_info, block_device_info,
destroy_disks, context=context)
def _undefine_domain(self, instance):
try:
@ -864,7 +866,7 @@ class LibvirtDriver(driver.ComputeDriver):
{'errcode': errcode, 'e': e}, instance=instance)
def _cleanup(self, instance, network_info, block_device_info,
destroy_disks):
destroy_disks, context=None):
self._undefine_domain(instance)
self.unplug_vifs(instance, network_info)
retry = True
@ -908,6 +910,21 @@ class LibvirtDriver(driver.ComputeDriver):
for vol in block_device_mapping:
connection_info = vol['connection_info']
disk_dev = vol['mount_device'].rpartition("/")[2]
if ('data' in connection_info and
'volume_id' in connection_info['data']):
volume_id = connection_info['data']['volume_id']
encryption = \
encryptors.get_encryption_metadata(context, volume_id,
connection_info)
if encryption:
# The volume must be detached from the VM before
# disconnecting it from its encryptor. Otherwise, the
# encryptor may report that the volume is still in use.
encryptor = self._get_volume_encryptor(connection_info,
encryption)
encryptor.detach_volume(**encryption)
self.volume_driver_method('disconnect_volume',
connection_info,
disk_dev)
@ -1012,7 +1029,13 @@ class LibvirtDriver(driver.ComputeDriver):
method = getattr(driver, method_name)
return method(connection_info, *args, **kwargs)
def attach_volume(self, connection_info, instance, mountpoint):
def _get_volume_encryptor(self, connection_info, encryption):
encryptor = encryptors.get_volume_encryptor(connection_info,
**encryption)
return encryptor
def attach_volume(self, context, connection_info, instance, mountpoint,
encryption=None):
instance_name = instance['name']
virt_dom = self._lookup_by_name(instance_name)
disk_dev = mountpoint.rpartition("/")[2]
@ -1056,6 +1079,16 @@ class LibvirtDriver(driver.ComputeDriver):
state = LIBVIRT_POWER_STATE[virt_dom.info()[0]]
if state == power_state.RUNNING:
flags |= libvirt.VIR_DOMAIN_AFFECT_LIVE
# cache device_path in connection_info -- required by encryptors
if 'data' in connection_info:
connection_info['data']['device_path'] = conf.source_path
if encryption:
encryptor = self._get_volume_encryptor(connection_info,
encryption)
encryptor.attach_volume(context, **encryption)
virt_dom.attachDeviceFlags(conf.to_xml(), flags)
except Exception as ex:
if isinstance(ex, libvirt.libvirtError):
@ -1157,7 +1190,8 @@ class LibvirtDriver(driver.ComputeDriver):
block_device_info=block_device_info)
return xml
def detach_volume(self, connection_info, instance, mountpoint):
def detach_volume(self, connection_info, instance, mountpoint,
encryption=None):
instance_name = instance['name']
disk_dev = mountpoint.rpartition("/")[2]
try:
@ -1174,6 +1208,14 @@ class LibvirtDriver(driver.ComputeDriver):
if state == power_state.RUNNING:
flags |= libvirt.VIR_DOMAIN_AFFECT_LIVE
virt_dom.detachDeviceFlags(xml, flags)
if encryption:
# The volume must be detached from the VM before
# disconnecting it from its encryptor. Otherwise, the
# encryptor may report that the volume is still in use.
encryptor = self._get_volume_encryptor(connection_info,
encryption)
encryptor.detach_volume(**encryption)
except libvirt.libvirtError as ex:
# NOTE(vish): This is called to cleanup volumes after live
# migration, so we should still disconnect even if
@ -1864,7 +1906,8 @@ class LibvirtDriver(driver.ComputeDriver):
# Initialize all the necessary networking, block devices and
# start the instance.
self._create_domain_and_network(xml, instance, network_info,
block_device_info)
block_device_info, context=context,
reboot=True)
self._prepare_pci_devices_for_use(
pci_manager.get_instance_pci_devs(instance))
@ -2022,7 +2065,7 @@ class LibvirtDriver(driver.ComputeDriver):
write_to_disk=True)
self._create_domain_and_network(xml, instance, network_info,
block_device_info)
block_device_info, context=context)
LOG.debug(_("Instance is running"), instance=instance)
def _wait_for_boot():
@ -3108,7 +3151,8 @@ class LibvirtDriver(driver.ComputeDriver):
return domain
def _create_domain_and_network(self, xml, instance, network_info,
block_device_info=None, power_on=True):
block_device_info=None, power_on=True,
context=None, reboot=False):
"""Do required network setup and create domain."""
block_device_mapping = driver.block_device_info_get_mapping(
@ -3118,9 +3162,25 @@ class LibvirtDriver(driver.ComputeDriver):
connection_info = vol['connection_info']
disk_dev = vol['mount_device'].rpartition("/")[2]
disk_info = blockinfo.get_info_from_bdm(CONF.libvirt_type, vol)
self.volume_driver_method('connect_volume',
connection_info,
disk_info)
conf = self.volume_driver_method('connect_volume',
connection_info,
disk_info)
# cache device_path in connection_info -- required by encryptors
if (not reboot and 'data' in connection_info and
'volume_id' in connection_info['data']):
connection_info['data']['device_path'] = conf.source_path
self.virtapi.block_device_mapping_update(context, vol.id,
{'connection_info': jsonutils.dumps(connection_info)})
volume_id = connection_info['data']['volume_id']
encryption = \
encryptors.get_encryption_metadata(context, volume_id,
connection_info)
if encryption:
encryptor = self._get_volume_encryptor(connection_info,
encryption)
encryptor.attach_volume(context, **encryption)
self.plug_vifs(instance, network_info)
self.firewall_driver.setup_basic_filtering(instance, network_info)
@ -4454,7 +4514,8 @@ class LibvirtDriver(driver.ComputeDriver):
block_device_info=block_device_info,
write_to_disk=True)
self._create_domain_and_network(xml, instance, network_info,
block_device_info, power_on)
block_device_info, power_on,
context=context)
if power_on:
timer = loopingcall.FixedIntervalLoopingCall(
self._wait_for_running,

View File

@ -105,7 +105,7 @@ class PowerVMDriver(driver.ComputeDriver):
self._powervm.spawn(context, instance, image_meta['id'], network_info)
def destroy(self, instance, network_info, block_device_info=None,
destroy_disks=True):
destroy_disks=True, context=None):
"""Destroy (shutdown and delete) the specified instance."""
self._powervm.destroy(instance['name'], destroy_disks)

View File

@ -191,7 +191,7 @@ class VMwareESXDriver(driver.ComputeDriver):
self._vmops.reboot(instance, network_info)
def destroy(self, instance, network_info, block_device_info=None,
destroy_disks=True):
destroy_disks=True, context=None):
"""Destroy VM instance."""
self._vmops.destroy(instance, network_info, destroy_disks)
@ -283,13 +283,15 @@ class VMwareESXDriver(driver.ComputeDriver):
"""Retrieves the IP address of the ESX host."""
return self._host_ip
def attach_volume(self, connection_info, instance, mountpoint):
def attach_volume(self, context, connection_info, instance, mountpoint,
encryption=None):
"""Attach volume storage to VM instance."""
return self._volumeops.attach_volume(connection_info,
instance,
mountpoint)
def detach_volume(self, connection_info, instance, mountpoint):
def detach_volume(self, connection_info, instance, mountpoint,
encryption=None):
"""Detach volume storage to VM instance."""
return self._volumeops.detach_volume(connection_info,
instance,
@ -603,7 +605,8 @@ class VMwareVCDriver(VMwareESXDriver):
_vmops.spawn(context, instance, image_meta, injected_files,
admin_password, network_info, block_device_info)
def attach_volume(self, connection_info, instance, mountpoint):
def attach_volume(self, context, connection_info, instance, mountpoint,
encryption=None):
"""Attach volume storage to VM instance."""
_volumeops = self._get_volumeops_for_compute_node(instance['node'])
return _volumeops.attach_volume(connection_info,

View File

@ -237,7 +237,7 @@ class XenAPIDriver(driver.ComputeDriver):
self._vmops.change_instance_metadata(instance, diff)
def destroy(self, instance, network_info, block_device_info=None,
destroy_disks=True):
destroy_disks=True, context=None):
"""Destroy VM instance."""
self._vmops.destroy(instance, network_info, block_device_info,
destroy_disks)
@ -376,13 +376,15 @@ class XenAPIDriver(driver.ComputeDriver):
xs_url = urlparse.urlparse(CONF.xenapi_connection_url)
return xs_url.netloc
def attach_volume(self, connection_info, instance, mountpoint):
def attach_volume(self, context, connection_info, instance, mountpoint,
encryption=None):
"""Attach volume storage to VM instance."""
return self._volumeops.attach_volume(connection_info,
instance['name'],
mountpoint)
def detach_volume(self, connection_info, instance, mountpoint):
def detach_volume(self, connection_info, instance, mountpoint,
encryption=None):
"""Detach volume storage from VM instance."""
return self._volumeops.detach_volume(connection_info,
instance['name'],

View File

@ -351,6 +351,9 @@ class API(base.Base):
def delete_snapshot(self, context, snapshot_id):
cinderclient(context).volume_snapshots.delete(snapshot_id)
def get_volume_encryption_metadata(self, context, volume_id):
return cinderclient(context).volumes.get_encryption_metadata(volume_id)
@translate_volume_exception
def get_volume_metadata(self, context, volume_id):
raise NotImplementedError()

View File

@ -0,0 +1,68 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# 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.
from nova.openstack.common.gettextutils import _
from nova.openstack.common import importutils
from nova.openstack.common import log as logging
from nova import volume
from nova.volume.encryptors import nop
LOG = logging.getLogger(__name__)
def get_volume_encryptor(connection_info, **kwargs):
"""Creates a VolumeEncryptor used to encrypt the specified volume.
:param: the connection information used to attach the volume
:returns VolumeEncryptor: the VolumeEncryptor for the volume
"""
encryptor = nop.NoOpEncryptor(connection_info, **kwargs)
location = kwargs.get('control_location', None)
if location and location.lower() == 'front-end': # case insensitive
provider = kwargs.get('provider')
try:
encryptor = importutils.import_object(provider, connection_info,
**kwargs)
except Exception as e:
LOG.error(_("Error instantiating %(provider)s: %(exception)s"),
provider=provider, exception=e)
raise
return encryptor
_volume_api = volume.API()
def get_encryption_metadata(context, volume_id, connection_info):
metadata = {}
if ('data' in connection_info and
connection_info['data'].get('encrypted', False)):
try:
metadata = _volume_api.get_volume_encryption_metadata(context,
volume_id)
except Exception as e:
LOG.error(_("Failed to retrieve encryption metadata for "
"volume %(volume_id)s: %(exception)s"),
{'volume_id': volume_id, 'exception': e})
raise
return metadata

View File

@ -0,0 +1,60 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# 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 abc
from nova import keymgr
from nova.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class VolumeEncryptor(object):
"""Base class to support encrypted volumes.
A VolumeEncryptor provides hooks for attaching and detaching volumes, which
are called immediately prior to attaching the volume to an instance and
immediately following detaching the volume from an instance. This class
performs no actions for either hook.
"""
__metaclass__ = abc.ABCMeta
def __init__(self, connection_info, **kwargs):
self._key_manager = keymgr.API()
self.encryption_key_id = kwargs.get('encryption_key_id')
def _get_key(self, context):
"""Retrieves the encryption key for the specified volume.
:param: the connection information used to attach the volume
"""
return self._key_manager.get_key(context, self.encryption_key_id)
@abc.abstractmethod
def attach_volume(self, context, **kwargs):
"""Hook called immediately prior to attaching a volume to an instance.
"""
pass
@abc.abstractmethod
def detach_volume(self, **kwargs):
"""Hook called immediately after detaching a volume from an instance.
"""
pass

View File

@ -0,0 +1,103 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# 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 os
from nova.openstack.common.gettextutils import _
from nova.openstack.common import log as logging
from nova import utils
from nova.volume.encryptors import base
LOG = logging.getLogger(__name__)
class CryptsetupEncryptor(base.VolumeEncryptor):
"""A VolumeEncryptor based on dm-crypt.
This VolumeEncryptor uses dm-crypt to encrypt the specified volume.
"""
def __init__(self, connection_info, **kwargs):
super(CryptsetupEncryptor, self).__init__(connection_info, **kwargs)
# the device's path as given to libvirt -- e.g., /dev/disk/by-path/...
self.symlink_path = connection_info['data']['device_path']
# a unique name for the volume -- e.g., the iSCSI participant name
self.dev_name = self.symlink_path.split('/')[-1]
# the device's actual path on the compute host -- e.g., /dev/sd_
self.dev_path = os.path.realpath(self.symlink_path)
def _get_passphrase(self, key):
return ''.join(hex(x).replace('0x', '') for x in key)
def _open_volume(self, passphrase, **kwargs):
"""Opens the LUKS partition on the volume using the specified
passphrase.
:param passphrase: the passphrase used to access the volume
"""
LOG.debug(_("opening encrypted volume %s"), self.dev_path)
# NOTE(joel-coffman): cryptsetup will strip trailing newlines from
# input specified on stdin unless --key-file=- is specified.
cmd = ["cryptsetup", "create", "--key-file=-"]
cipher = kwargs.get("cipher", None)
if cipher is not None:
cmd.extend(["--cipher", cipher])
key_size = kwargs.get("key_size", None)
if key_size is not None:
cmd.extend(["--key-size", key_size])
cmd.extend([self.dev_name, self.dev_path])
utils.execute(*cmd, process_input=passphrase,
check_exit_code=True, run_as_root=True)
def attach_volume(self, context, **kwargs):
"""Shadows the device and passes an unencrypted version to the
instance.
Transparent disk encryption is achieved by mounting the volume via
dm-crypt and passing the resulting device to the instance. The
instance is unaware of the underlying encryption due to modifying the
original symbolic link to refer to the device mounted by dm-crypt.
"""
key = self._get_key(context).get_encoded()
passphrase = self._get_passphrase(key)
self._open_volume(passphrase, **kwargs)
# modify the original symbolic link to refer to the decrypted device
utils.execute('ln', '--symbolic', '--force',
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
run_as_root=True, check_exit_code=True)
def _close_volume(self, **kwargs):
"""Closes the device (effectively removes the dm-crypt mapping)."""
LOG.debug(_("closing encrypted volume %s"), self.dev_path)
utils.execute('cryptsetup', 'remove', self.dev_name,
run_as_root=True, check_exit_code=True)
def detach_volume(self, **kwargs):
"""Removes the dm-crypt mapping for the device."""
self._close_volume(**kwargs)

View File

@ -0,0 +1,108 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# 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 re
from nova.openstack.common.gettextutils import _
from nova.openstack.common import log as logging
from nova.openstack.common import processutils
from nova import utils
from nova.volume.encryptors import cryptsetup
LOG = logging.getLogger(__name__)
class LuksEncryptor(cryptsetup.CryptsetupEncryptor):
"""A VolumeEncryptor based on LUKS.
This VolumeEncryptor uses dm-crypt to encrypt the specified volume.
"""
def __init__(self, connection_info, **kwargs):
super(LuksEncryptor, self).__init__(connection_info, **kwargs)
def _format_volume(self, passphrase, **kwargs):
"""Creates a LUKS header on the volume.
:param passphrase: the passphrase used to access the volume
"""
LOG.debug(_("formatting encrypted volume %s"), self.dev_path)
# NOTE(joel-coffman): cryptsetup will strip trailing newlines from
# input specified on stdin unless --key-file=- is specified.
cmd = ["cryptsetup", "--batch-mode", "luksFormat", "--key-file=-"]
cipher = kwargs.get("cipher", None)
if cipher is not None:
cmd.extend(["--cipher", cipher])
key_size = kwargs.get("key_size", None)
if key_size is not None:
cmd.extend(["--key-size", key_size])
cmd.extend([self.dev_path])
utils.execute(*cmd, process_input=passphrase,
check_exit_code=True, run_as_root=True)
def _open_volume(self, passphrase, **kwargs):
"""Opens the LUKS partition on the volume using the specified
passphrase.
:param passphrase: the passphrase used to access the volume
"""
LOG.debug(_("opening encrypted volume %s"), self.dev_path)
utils.execute('cryptsetup', 'luksOpen', '--key-file=-',
self.dev_path, self.dev_name, process_input=passphrase,
run_as_root=True, check_exit_code=True)
def attach_volume(self, context, **kwargs):
"""Shadows the device and passes an unencrypted version to the
instance.
Transparent disk encryption is achieved by mounting the volume via
dm-crypt and passing the resulting device to the instance. The
instance is unaware of the underlying encryption due to modifying the
original symbolic link to refer to the device mounted by dm-crypt.
"""
key = self._get_key(context).get_encoded()
# LUKS uses a passphrase rather than a raw key -- convert to string
passphrase = ''.join(hex(x).replace('0x', '') for x in key)
try:
self._open_volume(passphrase, **kwargs)
except processutils.ProcessExecutionError as e:
pattern = re.compile('Device \S+ is not a valid LUKS device.')
if e.exit_code == 1 and pattern.search(e.stderr):
# the device has never been formatted; format it and try again
self._format_volume(passphrase, **kwargs)
self._open_volume(passphrase, **kwargs)
else:
raise
# modify the original symbolic link to refer to the decrypted device
utils.execute('ln', '--symbolic', '--force',
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
run_as_root=True, check_exit_code=True)
def _close_volume(self, **kwargs):
"""Closes the device (effectively removes the dm-crypt mapping)."""
LOG.debug(_("closing encrypted volume %s"), self.dev_path)
utils.execute('cryptsetup', 'luksClose', self.dev_name,
run_as_root=True, check_exit_code=True)

View File

@ -0,0 +1,41 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# 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.
from nova.openstack.common import log as logging
from nova.volume.encryptors import base
LOG = logging.getLogger(__name__)
class NoOpEncryptor(base.VolumeEncryptor):
"""A VolumeEncryptor that does nothing.
This class exists solely to wrap regular (i.e., unencrypted) volumes so
that they do not require special handling with respect to an encrypted
volume. This implementation performs no action when a volume is attached
or detached.
"""
def __init__(self, connection_info, **kwargs):
super(NoOpEncryptor, self).__init__(connection_info, **kwargs)
def attach_volume(self, context):
pass
def detach_volume(self):
pass