diff --git a/etc/nova/rootwrap.d/compute.filters b/etc/nova/rootwrap.d/compute.filters index fec68ff70d..e98c3f265d 100644 --- a/etc/nova/rootwrap.d/compute.filters +++ b/etc/nova/rootwrap.d/compute.filters @@ -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 diff --git a/nova/compute/manager.py b/nova/compute/manager.py index b80e2d163e..c44ef6f2b9 100755 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -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 ' diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py index 026f1133f4..c9f35b5a51 100644 --- a/nova/tests/compute/test_compute.py +++ b/nova/tests/compute/test_compute.py @@ -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): diff --git a/nova/tests/keymgr/test_single_key_mgr.py b/nova/tests/keymgr/test_single_key_mgr.py index a44a06d039..85eb74e503 100644 --- a/nova/tests/keymgr/test_single_key_mgr.py +++ b/nova/tests/keymgr/test_single_key_mgr.py @@ -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) diff --git a/nova/tests/virt/hyperv/test_hypervapi.py b/nova/tests/virt/hyperv/test_hypervapi.py index ab38af0fbf..13816ac48c 100644 --- a/nova/tests/virt/hyperv/test_hypervapi.py +++ b/nova/tests/virt/hyperv/test_hypervapi.py @@ -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) diff --git a/nova/tests/virt/libvirt/test_libvirt.py b/nova/tests/virt/libvirt/test_libvirt.py index 78aeb8235b..1ab70454cd 100644 --- a/nova/tests/virt/libvirt/test_libvirt.py +++ b/nova/tests/virt/libvirt/test_libvirt.py @@ -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"} diff --git a/nova/tests/virt/test_block_device.py b/nova/tests/virt/test_block_device.py index 5928c1f601..591efe8ed4 100644 --- a/nova/tests/virt/test_block_device.py +++ b/nova/tests/virt/test_block_device.py @@ -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) diff --git a/nova/tests/virt/test_virt_drivers.py b/nova/tests/virt/test_virt_drivers.py index 5c28cd701f..88d04c4f7c 100644 --- a/nova/tests/virt/test_virt_drivers.py +++ b/nova/tests/virt/test_virt_drivers.py @@ -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') diff --git a/nova/tests/virt/vmwareapi/test_vmwareapi.py b/nova/tests/virt/vmwareapi/test_vmwareapi.py index 4c7acc424c..85a0d7e8b3 100644 --- a/nova/tests/virt/vmwareapi/test_vmwareapi.py +++ b/nova/tests/virt/vmwareapi/test_vmwareapi.py @@ -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() diff --git a/nova/tests/virt/xenapi/test_xenapi.py b/nova/tests/virt/xenapi/test_xenapi.py index 358257f030..d4d8d0fc5d 100644 --- a/nova/tests/virt/xenapi/test_xenapi.py +++ b/nova/tests/virt/xenapi/test_xenapi.py @@ -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): diff --git a/nova/tests/volume/encryptors/__init__.py b/nova/tests/volume/encryptors/__init__.py new file mode 100644 index 0000000000..0ee3757019 --- /dev/null +++ b/nova/tests/volume/encryptors/__init__.py @@ -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. diff --git a/nova/tests/volume/encryptors/test_base.py b/nova/tests/volume/encryptors/test_base.py new file mode 100644 index 0000000000..aad1f11381 --- /dev/null +++ b/nova/tests/volume/encryptors/test_base.py @@ -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) diff --git a/nova/tests/volume/encryptors/test_cryptsetup.py b/nova/tests/volume/encryptors/test_cryptsetup.py new file mode 100644 index 0000000000..eaca299813 --- /dev/null +++ b/nova/tests/volume/encryptors/test_cryptsetup.py @@ -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) diff --git a/nova/tests/volume/encryptors/test_luks.py b/nova/tests/volume/encryptors/test_luks.py new file mode 100644 index 0000000000..2db7e77637 --- /dev/null +++ b/nova/tests/volume/encryptors/test_luks.py @@ -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) diff --git a/nova/tests/volume/encryptors/test_nop.py b/nova/tests/volume/encryptors/test_nop.py new file mode 100644 index 0000000000..ff813d473e --- /dev/null +++ b/nova/tests/volume/encryptors/test_nop.py @@ -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() diff --git a/nova/tests/volume/test_cinder.py b/nova/tests/volume/test_cinder.py index fe0e7c7dde..93fc775787 100644 --- a/nova/tests/volume/test_cinder.py +++ b/nova/tests/volume/test_cinder.py @@ -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'}) diff --git a/nova/virt/baremetal/driver.py b/nova/virt/baremetal/driver.py index 2cb1d6c760..e3b25f73d9 100755 --- a/nova/virt/baremetal/driver.py +++ b/nova/virt/baremetal/driver.py @@ -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) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 5c356065d3..d18f04a7fb 100755 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -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() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index ae977762e8..ed3f12e72d 100755 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -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] diff --git a/nova/virt/hyperv/driver.py b/nova/virt/hyperv/driver.py index e36c1ce5be..5966913adc 100755 --- a/nova/virt/hyperv/driver.py +++ b/nova/virt/hyperv/driver.py @@ -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']) diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 65c2432db9..a8e7d4f7dd 100755 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -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, diff --git a/nova/virt/powervm/driver.py b/nova/virt/powervm/driver.py index 2556f378c0..f746cc1474 100755 --- a/nova/virt/powervm/driver.py +++ b/nova/virt/powervm/driver.py @@ -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) diff --git a/nova/virt/vmwareapi/driver.py b/nova/virt/vmwareapi/driver.py index 459322ee7e..44b9b9ec20 100644 --- a/nova/virt/vmwareapi/driver.py +++ b/nova/virt/vmwareapi/driver.py @@ -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, diff --git a/nova/virt/xenapi/driver.py b/nova/virt/xenapi/driver.py index 45454a6e22..883bdd9da9 100755 --- a/nova/virt/xenapi/driver.py +++ b/nova/virt/xenapi/driver.py @@ -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'], diff --git a/nova/volume/cinder.py b/nova/volume/cinder.py index 8704f9e176..8ff5687136 100644 --- a/nova/volume/cinder.py +++ b/nova/volume/cinder.py @@ -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() diff --git a/nova/volume/encryptors/__init__.py b/nova/volume/encryptors/__init__.py new file mode 100644 index 0000000000..b492eab928 --- /dev/null +++ b/nova/volume/encryptors/__init__.py @@ -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 diff --git a/nova/volume/encryptors/base.py b/nova/volume/encryptors/base.py new file mode 100644 index 0000000000..9d5a9365cf --- /dev/null +++ b/nova/volume/encryptors/base.py @@ -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 diff --git a/nova/volume/encryptors/cryptsetup.py b/nova/volume/encryptors/cryptsetup.py new file mode 100644 index 0000000000..5d9096fb18 --- /dev/null +++ b/nova/volume/encryptors/cryptsetup.py @@ -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) diff --git a/nova/volume/encryptors/luks.py b/nova/volume/encryptors/luks.py new file mode 100644 index 0000000000..1ffbb1f1e1 --- /dev/null +++ b/nova/volume/encryptors/luks.py @@ -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) diff --git a/nova/volume/encryptors/nop.py b/nova/volume/encryptors/nop.py new file mode 100644 index 0000000000..6772585947 --- /dev/null +++ b/nova/volume/encryptors/nop.py @@ -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