Merge "Add encryption support for volumes to libvirt"
This commit is contained in:
commit
4d89bda3d7
@ -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…
Reference in New Issue
Block a user