Add encryption support for volumes to libvirt
Add support to encrypt Cinder volumes. Hooks within the libvirt driver encrypt volumes during the attach call. Created a VolumeEncryptor interface as well as several concrete subclasses (e.g., CryptsetupEncryptor and LuksEncryptor) to handle encryption. This feature depends upon related changes that have been accepted into Cinder (e.g., storing encryption key UUIDs for encrypted volumes). Implements: blueprint encrypt-cinder-volumes Change-Id: I358813b3ecde4f88de7202c1c07d9b1168c2c332 SecurityImpact
This commit is contained in:
parent
cf5645fdee
commit
8c0673d310
@ -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
|
||||
|
@ -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 '
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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"}
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
16
nova/tests/volume/encryptors/__init__.py
Normal file
16
nova/tests/volume/encryptors/__init__.py
Normal 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.
|
40
nova/tests/volume/encryptors/test_base.py
Normal file
40
nova/tests/volume/encryptors/test_base.py
Normal 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)
|
85
nova/tests/volume/encryptors/test_cryptsetup.py
Normal file
85
nova/tests/volume/encryptors/test_cryptsetup.py
Normal 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)
|
76
nova/tests/volume/encryptors/test_luks.py
Normal file
76
nova/tests/volume/encryptors/test_luks.py
Normal 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)
|
33
nova/tests/volume/encryptors/test_nop.py
Normal file
33
nova/tests/volume/encryptors/test_nop.py
Normal 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()
|
@ -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'})
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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'])
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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'],
|
||||
|
@ -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()
|
||||
|
68
nova/volume/encryptors/__init__.py
Normal file
68
nova/volume/encryptors/__init__.py
Normal 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
|
60
nova/volume/encryptors/base.py
Normal file
60
nova/volume/encryptors/base.py
Normal 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
|
103
nova/volume/encryptors/cryptsetup.py
Normal file
103
nova/volume/encryptors/cryptsetup.py
Normal 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)
|
108
nova/volume/encryptors/luks.py
Normal file
108
nova/volume/encryptors/luks.py
Normal 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)
|
41
nova/volume/encryptors/nop.py
Normal file
41
nova/volume/encryptors/nop.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user