encryptors: Switch to os-brick encryptor classes

This change drops the encryptor classes and supporting code from the
codebase in favor of the classes provided by os-brick. This is made
possible by the following os-brick change that introduced new encryption
provider constants during Ocata :

Ic155bd29d46059832cce970bf60375e7e472eca6

Thanks to the following bugfix also released as part of 1.11.0 for Ocata
the constants present in os-brick also support the use of the deprecated
legacy class paths from Nova, for example
nova.volume.encryptors.luks.LuksEncryptor, while using the os-brick
provided classes :

I3ec6e3fe919bc03d158da04a18fb8b651002ed52

Implements: blueprint switch-to-os-brick-encryptor-classes
Change-Id: I37ffc90c0bd57029fced251b5cfd7cd4318a0292
Depends-On: Iae12605dc7d0607e78020a24b5b8801606c2f169
This commit is contained in:
Lee Yarwood 2016-10-29 12:05:46 +01:00 committed by Matt Riedemann
parent 3d09b67205
commit 9c23cdc247
15 changed files with 14 additions and 1125 deletions

View File

@ -225,14 +225,6 @@ privsep-rootwrap: RegExpFilter, privsep-helper, root, privsep-helper, --config-f
# nova/storage/linuxscsi.py: sg_scan device
sg_scan: CommandFilter, sg_scan, root
# nova/volume/encryptors/cryptsetup.py:
# nova/volume/encryptors/luks.py:
ln: RegExpFilter, ln, root, ln, --symbolic, --force, /dev/mapper/crypt-.+, .+
# nova/volume/encryptors.py:
# nova/virt/libvirt/dmcrypt.py:
cryptsetup: CommandFilter, cryptsetup, root
# nova/virt/xenapi/vm_utils.py:
xenstore-read: CommandFilter, xenstore-read, root

View File

@ -35,6 +35,7 @@ import fixtures
from lxml import etree
import mock
from mox3 import mox
from os_brick import encryptors
from os_brick import exception as brick_exception
from os_brick.initiator import connector
import os_vif
@ -6423,7 +6424,7 @@ class LibvirtConnTestCase(test.NoDBTestCase):
mock_get_domain.assert_called_once_with(instance)
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._disconnect_volume')
@mock.patch('nova.volume.encryptors.get_volume_encryptor')
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_volume_encryptor')
@mock.patch('nova.virt.libvirt.host.Host.get_guest')
def test_detach_volume_order_with_encryptors(self, mock_get_guest,
mock_get_encryptor, mock_disconnect_volume):
@ -6432,7 +6433,7 @@ class LibvirtConnTestCase(test.NoDBTestCase):
mock_guest.get_power_state.return_value = power_state.RUNNING
mock_get_guest.return_value = mock_guest
mock_encryptor = mock.MagicMock(
spec=nova.volume.encryptors.nop.NoOpEncryptor)
spec=encryptors.nop.NoOpEncryptor)
mock_get_encryptor.return_value = mock_encryptor
mock_order = mock.Mock()
@ -14591,7 +14592,7 @@ class LibvirtConnTestCase(test.NoDBTestCase):
block_device_info=None, destroy_disks=True)
self.assertTrue(guest.poweroff.called)
@mock.patch('nova.volume.encryptors.get_encryption_metadata')
@mock.patch('os_brick.encryptors.get_encryption_metadata')
@mock.patch('nova.virt.libvirt.blockinfo.get_info_from_bdm')
def test_create_with_bdm(self, get_info_from_bdm, get_encryption_metadata):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)

View File

@ -13,6 +13,7 @@
# under the License.
import mock
from os_brick import encryptors
from oslo_serialization import jsonutils
from nova import block_device
@ -28,7 +29,6 @@ from nova.tests import uuidsentinel as uuids
from nova.virt import block_device as driver_block_device
from nova.virt import driver
from nova.volume import cinder
from nova.volume import encryptors
class TestDriverBlockDevice(test.NoDBTestCase):

View File

@ -1,128 +0,0 @@
# 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 mock
from nova import test
from nova.tests.unit.keymgr import fake
from nova.volume import encryptors
from nova.volume.encryptors import cryptsetup
from nova.volume.encryptors import luks
from nova.volume.encryptors import nop
class VolumeEncryptorTestCase(test.NoDBTestCase):
def _create(self, device_path):
pass
def setUp(self):
super(VolumeEncryptorTestCase, self).setUp()
self.stub_out('nova.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)
class VolumeEncryptorInitTestCase(VolumeEncryptorTestCase):
def setUp(self):
super(VolumeEncryptorInitTestCase, self).setUp()
def _test_get_encryptor(self, provider, expected_provider_class):
encryption = {'control_location': 'front-end',
'provider': provider}
encryptor = encryptors.get_volume_encryptor(self.connection_info,
**encryption)
self.assertIsInstance(encryptor, expected_provider_class)
def test_get_encryptors(self):
self._test_get_encryptor('luks',
luks.LuksEncryptor)
# TODO(lyarwood): Remove the following in 16.0.0 Pike
self._test_get_encryptor('LuksEncryptor',
luks.LuksEncryptor)
self._test_get_encryptor('nova.volume.encryptors.luks.LuksEncryptor',
luks.LuksEncryptor)
self._test_get_encryptor('plain',
cryptsetup.CryptsetupEncryptor)
# TODO(lyarwood): Remove the following in 16.0.0 Pike
self._test_get_encryptor('CryptsetupEncryptor',
cryptsetup.CryptsetupEncryptor)
self._test_get_encryptor(
'nova.volume.encryptors.cryptsetup.CryptsetupEncryptor',
cryptsetup.CryptsetupEncryptor)
self._test_get_encryptor(None,
nop.NoOpEncryptor)
# TODO(lyarwood): Remove the following in 16.0.0 Pike
self._test_get_encryptor('NoOpEncryptor',
nop.NoOpEncryptor)
self._test_get_encryptor('nova.volume.encryptors.nop.NoOpEncryptor',
nop.NoOpEncryptor)
def test_get_missing_encryptor_error(self):
encryption = {'control_location': 'front-end',
'provider': 'ErrorEncryptor'}
self.assertRaises(ValueError, encryptors.get_volume_encryptor,
self.connection_info, **encryption)
@mock.patch('nova.volume.encryptors.LOG')
def test_get_missing_out_of_tree_encryptor_log(self, log):
provider = 'TestEncryptor'
encryption = {'control_location': 'front-end',
'provider': provider}
try:
encryptors.get_volume_encryptor(self.connection_info, **encryption)
except Exception as e:
log.error.assert_called_once_with("Error instantiating "
"%(provider)s: "
"%(exception)s",
{'provider': provider,
'exception': e})
log.warning.assert_called_once_with("Use of the out of tree "
"encryptor class %(provider)s "
"will be blocked with the "
"16.0.0 Pike release of Nova.",
{'provider': provider})
@mock.patch('nova.volume.encryptors.LOG')
def test_get_direct_encryptor_log(self, log):
encryption = {'control_location': 'front-end',
'provider': 'LuksEncryptor'}
encryptors.get_volume_encryptor(self.connection_info, **encryption)
encryption = {'control_location': 'front-end',
'provider': 'nova.volume.encryptors.luks.LuksEncryptor'}
encryptors.get_volume_encryptor(self.connection_info, **encryption)
log.warning.assert_has_calls([
mock.call("Use of the in tree encryptor class %(provider)s by "
"directly referencing the implementation class will be "
"blocked in the 16.0.0 Pike release of Nova.",
{'provider': 'LuksEncryptor'}),
mock.call("Use of the in tree encryptor class %(provider)s by "
"directly referencing the implementation class will be "
"blocked in the 16.0.0 Pike release of Nova.",
{'provider':
'nova.volume.encryptors.luks.LuksEncryptor'})])

View File

@ -1,170 +0,0 @@
# 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 binascii
import copy
import uuid
from castellan.common.objects import symmetric_key as key
import mock
from oslo_concurrency import processutils
import six
from nova import exception
from nova.tests.unit.volume.encryptors import test_base
from nova.volume.encryptors import cryptsetup
def fake__get_key(context, passphrase):
raw = bytes(binascii.unhexlify(passphrase))
symmetric_key = key.SymmetricKey('AES', len(raw) * 8, raw)
return symmetric_key
class CryptsetupEncryptorTestCase(test_base.VolumeEncryptorTestCase):
@mock.patch('os.path.exists', return_value=False)
def _create(self, connection_info, mock_exists):
return cryptsetup.CryptsetupEncryptor(connection_info)
def setUp(self):
super(CryptsetupEncryptorTestCase, self).setUp()
self.dev_path = self.connection_info['data']['device_path']
self.dev_name = 'crypt-%s' % self.dev_path.split('/')[-1]
self.symlink_path = self.dev_path
@mock.patch('nova.utils.execute')
def test__open_volume(self, mock_execute):
self.encryptor._open_volume("passphrase")
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'create', '--key-file=-', self.dev_name,
self.dev_path, process_input='passphrase',
run_as_root=True, check_exit_code=True),
])
self.assertEqual(1, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test_attach_volume(self, mock_execute):
fake_key = uuid.uuid4().hex
self.encryptor._get_key = mock.MagicMock()
self.encryptor._get_key.return_value = fake__get_key(None, fake_key)
self.encryptor.attach_volume(None)
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'create', '--key-file=-', self.dev_name,
self.dev_path, process_input=fake_key,
run_as_root=True, check_exit_code=True),
mock.call('ln', '--symbolic', '--force',
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
run_as_root=True, check_exit_code=True),
])
self.assertEqual(2, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test__close_volume(self, mock_execute):
self.encryptor.detach_volume()
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'remove', self.dev_name,
run_as_root=True, check_exit_code=[0, 4]),
])
self.assertEqual(1, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test_detach_volume(self, mock_execute):
self.encryptor.detach_volume()
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'remove', self.dev_name,
run_as_root=True, check_exit_code=[0, 4]),
])
self.assertEqual(1, mock_execute.call_count)
def test_init_volume_encryption_not_supported(self):
# Tests that creating a CryptsetupEncryptor fails if there is no
# device_path key.
type = 'unencryptable'
data = dict(volume_id='a194699b-aa07-4433-a945-a5d23802043e')
connection_info = dict(driver_volume_type=type, data=data)
exc = self.assertRaises(exception.VolumeEncryptionNotSupported,
cryptsetup.CryptsetupEncryptor,
connection_info)
self.assertIn(type, six.text_type(exc))
@mock.patch('os.path.exists', return_value=True)
@mock.patch('nova.utils.execute')
def test_init_volume_encryption_with_old_name(self, mock_execute,
mock_exists):
# If an old name crypt device exists, dev_path should be the old name.
old_dev_name = self.dev_path.split('/')[-1]
encryptor = cryptsetup.CryptsetupEncryptor(self.connection_info)
self.assertFalse(encryptor.dev_name.startswith('crypt-'))
self.assertEqual(old_dev_name, encryptor.dev_name)
self.assertEqual(self.dev_path, encryptor.dev_path)
self.assertEqual(self.symlink_path, encryptor.symlink_path)
mock_exists.assert_called_once_with('/dev/mapper/%s' % old_dev_name)
mock_execute.assert_called_once_with(
'cryptsetup', 'status', old_dev_name, run_as_root=True)
@mock.patch('os.path.exists', side_effect=[False, True])
@mock.patch('nova.utils.execute')
def test_init_volume_encryption_with_wwn(self, mock_execute, mock_exists):
# If an wwn name crypt device exists, dev_path should be based on wwn.
old_dev_name = self.dev_path.split('/')[-1]
wwn = 'fake_wwn'
connection_info = copy.deepcopy(self.connection_info)
connection_info['data']['multipath_id'] = wwn
encryptor = cryptsetup.CryptsetupEncryptor(connection_info)
self.assertFalse(encryptor.dev_name.startswith('crypt-'))
self.assertEqual(wwn, encryptor.dev_name)
self.assertEqual(self.dev_path, encryptor.dev_path)
self.assertEqual(self.symlink_path, encryptor.symlink_path)
mock_exists.assert_has_calls([
mock.call('/dev/mapper/%s' % old_dev_name),
mock.call('/dev/mapper/%s' % wwn)])
mock_execute.assert_called_once_with(
'cryptsetup', 'status', wwn, run_as_root=True)
@mock.patch('nova.utils.execute')
def test_attach_volume_unmangle_passphrase(self, mock_execute):
fake_key = '0725230b'
fake_key_mangled = '72523b'
self.encryptor._get_key = mock.MagicMock()
self.encryptor._get_key.return_value = fake__get_key(None, fake_key)
mock_execute.side_effect = [
processutils.ProcessExecutionError(exit_code=2), # luksOpen
mock.DEFAULT,
mock.DEFAULT,
]
self.encryptor.attach_volume(None)
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'create', '--key-file=-', self.dev_name,
self.dev_path, process_input=fake_key,
run_as_root=True, check_exit_code=True),
mock.call('cryptsetup', 'create', '--key-file=-', self.dev_name,
self.dev_path, process_input=fake_key_mangled,
run_as_root=True, check_exit_code=True),
mock.call('ln', '--symbolic', '--force',
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
run_as_root=True, check_exit_code=True),
])
self.assertEqual(3, mock_execute.call_count)

View File

@ -1,242 +0,0 @@
# 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 binascii
import uuid
from castellan.common.objects import symmetric_key as key
import mock
from oslo_concurrency import processutils
from nova.tests.unit.volume.encryptors import test_cryptsetup
from nova.volume.encryptors import luks
class LuksEncryptorTestCase(test_cryptsetup.CryptsetupEncryptorTestCase):
def _create(self, connection_info):
return luks.LuksEncryptor(connection_info)
@mock.patch('nova.utils.execute')
def test_is_luks(self, mock_execute):
luks.is_luks(self.dev_path)
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'isLuks', '--verbose', self.dev_path,
run_as_root=True, check_exit_code=True),
], any_order=False)
self.assertEqual(1, mock_execute.call_count)
@mock.patch('nova.volume.encryptors.luks.LOG')
@mock.patch('nova.utils.execute')
def test_is_luks_with_error(self, mock_execute, mock_log):
error_msg = "Device %s is not a valid LUKS device." % self.dev_path
mock_execute.side_effect = \
processutils.ProcessExecutionError(exit_code=1,
stderr=error_msg)
luks.is_luks(self.dev_path)
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'isLuks', '--verbose', self.dev_path,
run_as_root=True, check_exit_code=True),
])
self.assertEqual(1, mock_execute.call_count)
self.assertEqual(1, mock_log.warning.call_count) # warning logged
@mock.patch('nova.utils.execute')
def test__format_volume(self, mock_execute):
self.encryptor._format_volume("passphrase")
mock_execute.assert_has_calls([
mock.call('cryptsetup', '--batch-mode', 'luksFormat',
'--key-file=-', self.dev_path,
process_input='passphrase',
run_as_root=True, check_exit_code=True, attempts=3),
])
self.assertEqual(1, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test__open_volume(self, mock_execute):
self.encryptor._open_volume("passphrase")
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
self.dev_name, process_input='passphrase',
run_as_root=True, check_exit_code=True),
])
self.assertEqual(1, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test_attach_volume(self, mock_execute):
fake_key = uuid.uuid4().hex
self.encryptor._get_key = mock.MagicMock()
self.encryptor._get_key.return_value = test_cryptsetup.fake__get_key(
None, fake_key)
self.encryptor.attach_volume(None)
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
self.dev_name, process_input=fake_key,
run_as_root=True, check_exit_code=True),
mock.call('ln', '--symbolic', '--force',
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
run_as_root=True, check_exit_code=True),
])
self.assertEqual(2, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test_attach_volume_not_formatted(self, mock_execute):
fake_key = uuid.uuid4().hex
self.encryptor._get_key = mock.MagicMock()
self.encryptor._get_key.return_value = test_cryptsetup.fake__get_key(
None, fake_key)
mock_execute.side_effect = [
processutils.ProcessExecutionError(exit_code=1), # luksOpen
processutils.ProcessExecutionError(exit_code=1), # isLuks
mock.DEFAULT, # luksFormat
mock.DEFAULT, # luksOpen
mock.DEFAULT, # ln
]
self.encryptor.attach_volume(None)
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
self.dev_name, process_input=fake_key,
run_as_root=True, check_exit_code=True),
mock.call('cryptsetup', 'isLuks', '--verbose', self.dev_path,
run_as_root=True, check_exit_code=True),
mock.call('cryptsetup', '--batch-mode', 'luksFormat',
'--key-file=-', self.dev_path, process_input=fake_key,
run_as_root=True, check_exit_code=True, attempts=3),
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
self.dev_name, process_input=fake_key,
run_as_root=True, check_exit_code=True),
mock.call('ln', '--symbolic', '--force',
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
run_as_root=True, check_exit_code=True),
], any_order=False)
self.assertEqual(5, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test_attach_volume_fail(self, mock_execute):
fake_key = uuid.uuid4().hex
self.encryptor._get_key = mock.MagicMock()
self.encryptor._get_key.return_value = test_cryptsetup.fake__get_key(
None, fake_key)
mock_execute.side_effect = [
processutils.ProcessExecutionError(exit_code=1), # luksOpen
mock.DEFAULT, # isLuks
]
self.assertRaises(processutils.ProcessExecutionError,
self.encryptor.attach_volume, None)
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
self.dev_name, process_input=fake_key,
run_as_root=True, check_exit_code=True),
mock.call('cryptsetup', 'isLuks', '--verbose', self.dev_path,
run_as_root=True, check_exit_code=True),
], any_order=False)
self.assertEqual(2, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test__close_volume(self, mock_execute):
self.encryptor.detach_volume()
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'luksClose', self.dev_name,
attempts=3, run_as_root=True, check_exit_code=True),
])
self.assertEqual(1, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test_detach_volume(self, mock_execute):
self.encryptor.detach_volume()
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'luksClose', self.dev_name,
attempts=3, run_as_root=True, check_exit_code=True),
])
self.assertEqual(1, mock_execute.call_count)
def test_get_mangled_passphrase(self):
# Confirm that a mangled passphrase is provided as per bug#1633518
unmangled_raw_key = bytes(binascii.unhexlify('0725230b'))
symmetric_key = key.SymmetricKey('AES', len(unmangled_raw_key) * 8,
unmangled_raw_key)
unmangled_encoded_key = symmetric_key.get_encoded()
encryptor = luks.LuksEncryptor(self.connection_info)
self.assertEqual(encryptor._get_mangled_passphrase(
unmangled_encoded_key), '72523b')
@mock.patch('nova.utils.execute')
def test_attach_volume_unmangle_passphrase(self, mock_execute):
fake_key = '0725230b'
fake_key_mangled = '72523b'
self.encryptor._get_key = mock.MagicMock(name='mock_execute')
self.encryptor._get_key.return_value = \
test_cryptsetup.fake__get_key(None, fake_key)
mock_execute.side_effect = [
processutils.ProcessExecutionError(exit_code=2), # luksOpen
mock.DEFAULT, # luksOpen
mock.DEFAULT, # luksClose
mock.DEFAULT, # luksAddKey
mock.DEFAULT, # luksOpen
mock.DEFAULT, # luksClose
mock.DEFAULT, # luksRemoveKey
mock.DEFAULT, # luksOpen
mock.DEFAULT, # ln
]
self.encryptor.attach_volume(None)
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
self.dev_name, process_input=fake_key,
run_as_root=True, check_exit_code=True),
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
self.dev_name, process_input=fake_key_mangled,
run_as_root=True, check_exit_code=True),
mock.call('cryptsetup', 'luksClose', self.dev_name,
run_as_root=True, check_exit_code=True, attempts=3),
mock.call('cryptsetup', 'luksAddKey', self.dev_path,
process_input=''.join([fake_key_mangled,
'\n', fake_key,
'\n', fake_key]),
run_as_root=True, check_exit_code=True),
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
self.dev_name, process_input=fake_key,
run_as_root=True, check_exit_code=True),
mock.call('cryptsetup', 'luksClose', self.dev_name,
run_as_root=True, check_exit_code=True, attempts=3),
mock.call('cryptsetup', 'luksRemoveKey', self.dev_path,
process_input=fake_key_mangled, run_as_root=True,
check_exit_code=True),
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
self.dev_name, process_input=fake_key,
run_as_root=True, check_exit_code=True),
mock.call('ln', '--symbolic', '--force',
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
run_as_root=True, check_exit_code=True),
], any_order=False)
self.assertEqual(9, mock_execute.call_count)

View File

@ -1,28 +0,0 @@
# 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.unit.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 test_attach_volume(self):
self.encryptor.attach_volume(None)
def test_detach_volume(self):
self.encryptor.detach_volume()

View File

@ -15,6 +15,7 @@
import functools
import itertools
from os_brick import encryptors
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import excutils
@ -26,7 +27,6 @@ from nova import exception
from nova.i18n import _LE
from nova.i18n import _LI
from nova.i18n import _LW
from nova.volume import encryptors
CONF = nova.conf.CONF

View File

@ -44,6 +44,7 @@ import eventlet
from eventlet import greenthread
from eventlet import tpool
from lxml import etree
from os_brick import encryptors
from os_brick import exception as brick_exception
from os_brick.initiator import connector
from oslo_concurrency import processutils
@ -74,6 +75,7 @@ from nova.i18n import _LE
from nova.i18n import _LI
from nova.i18n import _LW
from nova import image
from nova import keymgr
from nova.network import model as network_model
from nova import objects
from nova.objects import fields
@ -109,7 +111,6 @@ from nova.virt.libvirt import vif as libvirt_vif
from nova.virt.libvirt.volume import remotefs
from nova.virt import netutils
from nova.volume import cinder
from nova.volume import encryptors
libvirt = None
@ -1164,9 +1165,12 @@ class LibvirtDriver(driver.ComputeDriver):
return vol_driver.get_config(connection_info, disk_info)
def _get_volume_encryptor(self, connection_info, encryption):
encryptor = encryptors.get_volume_encryptor(connection_info,
**encryption)
return encryptor
root_helper = utils.get_root_helper()
key_manager = keymgr.API(CONF)
return encryptors.get_volume_encryptor(root_helper=root_helper,
keymgr=key_manager,
connection_info=connection_info,
**encryption)
def _check_discard_for_attach_volume(self, conf, instance):
"""Perform some checks for volumes configured for discard support.

View File

@ -1,111 +0,0 @@
# 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 oslo_log import log as logging
from oslo_utils import importutils
from oslo_utils import strutils
from nova.i18n import _LE, _LW
LOG = logging.getLogger(__name__)
LUKS = "luks"
PLAIN = "plain"
FORMAT_TO_FRONTEND_ENCRYPTOR_MAP = {
LUKS: 'nova.volume.encryptors.luks.LuksEncryptor',
PLAIN: 'nova.volume.encryptors.cryptsetup.CryptsetupEncryptor'
}
LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP = {
"nova.volume.encryptors.luks.LuksEncryptor": LUKS,
"nova.volume.encryptors.cryptsetup.CryptsetupEncryptor": PLAIN,
"nova.volume.encryptors.nop.NoopEncryptor": None,
"LuksEncryptor": LUKS,
"CryptsetupEncryptor": PLAIN,
"NoOpEncryptor": None,
}
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
"""
location = kwargs.get('control_location', None)
if location and location.lower() == 'front-end': # case insensitive
provider = kwargs.get('provider')
# TODO(lyarwood): Remove the following in 16.0.0 Pike and raise an
# ERROR if provider is not a key in SUPPORTED_ENCRYPTION_PROVIDERS.
# Until then continue to allow both the class name and path to be used.
if provider in LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP:
LOG.warning(_LW("Use of the in tree encryptor class %(provider)s"
" by directly referencing the implementation class"
" will be blocked in the 16.0.0 Pike release of "
"Nova."), {'provider': provider})
provider = LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP[provider]
if provider in FORMAT_TO_FRONTEND_ENCRYPTOR_MAP:
provider = FORMAT_TO_FRONTEND_ENCRYPTOR_MAP[provider]
elif provider is None:
provider = "nova.volume.encryptors.nop.NoOpEncryptor"
else:
LOG.warning(_LW("Use of the out of tree encryptor class "
"%(provider)s will be blocked with the 16.0.0 "
"Pike release of Nova."), {'provider': provider})
try:
encryptor = importutils.import_object(provider, connection_info,
**kwargs)
except Exception as e:
LOG.error(_LE("Error instantiating %(provider)s: %(exception)s"),
{'provider': provider, 'exception': e})
raise
msg = ("Using volume encryptor '%(encryptor)s' for connection: "
"%(connection_info)s" %
{'encryptor': encryptor, 'connection_info': connection_info})
LOG.debug(strutils.mask_password(msg))
return encryptor
def get_encryption_metadata(context, volume_api, 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)
if not metadata:
LOG.warning(_LW(
'Volume %s should be encrypted but there is no '
'encryption metadata.'), volume_id)
except Exception as e:
LOG.error(_LE("Failed to retrieve encryption metadata for "
"volume %(volume_id)s: %(exception)s"),
{'volume_id': volume_id, 'exception': e})
raise
if metadata:
msg = ("Using volume encryption metadata '%(metadata)s' for "
"connection: %(connection_info)s" %
{'metadata': metadata, 'connection_info': connection_info})
LOG.debug(strutils.mask_password(msg))
return metadata

View File

@ -1,56 +0,0 @@
# 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
import six
from nova import keymgr
@six.add_metaclass(abc.ABCMeta)
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.
"""
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(context, self.encryption_key_id)
@abc.abstractmethod
def attach_volume(self, context, **kwargs):
"""Hook called immediately prior to attaching a volume to an instance.
"""
pass
@abc.abstractmethod
def detach_volume(self, **kwargs):
"""Hook called immediately after detaching a volume from an instance.
"""
pass

View File

@ -1,174 +0,0 @@
# 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 binascii
import os
from oslo_concurrency import processutils
from oslo_log import log as logging
from nova import exception
from nova.i18n import _LW, _LI
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)
# Fail if no device_path was set when connecting the volume, e.g. in
# the case of libvirt network volume drivers.
data = connection_info['data']
if not data.get('device_path'):
volume_id = data.get('volume_id') or connection_info.get('serial')
raise exception.VolumeEncryptionNotSupported(
volume_id=volume_id,
volume_type=connection_info['driver_volume_type'])
# 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 = 'crypt-%s' % self.symlink_path.split('/')[-1]
# NOTE(tsekiyama): In older version of nova, dev_name was the same
# as the symlink name. Now it has 'crypt-' prefix to avoid conflict
# with multipath device symlink. To enable rolling update, we use the
# old name when the encrypted volume already exists.
old_dev_name = self.symlink_path.split('/')[-1]
wwn = data.get('multipath_id')
if self._is_crypt_device_available(old_dev_name):
self.dev_name = old_dev_name
LOG.debug("Using old encrypted volume name: %s", self.dev_name)
elif wwn and wwn != old_dev_name:
# FibreChannel device could be named '/dev/mapper/<WWN>'.
if self._is_crypt_device_available(wwn):
self.dev_name = wwn
LOG.debug("Using encrypted volume name from wwn: %s",
self.dev_name)
# the device's actual path on the compute host -- e.g., /dev/sd_
self.dev_path = os.path.realpath(self.symlink_path)
def _is_crypt_device_available(self, dev_name):
if not os.path.exists('/dev/mapper/%s' % dev_name):
return False
try:
utils.execute('cryptsetup', 'status', dev_name, run_as_root=True)
except processutils.ProcessExecutionError as e:
# If /dev/mapper/<dev_name> is a non-crypt block device (such as a
# normal disk or multipath device), exit_code will be 1. In the
# case, we will omit the warning message.
if e.exit_code != 1:
LOG.warning(_LW('cryptsetup status %(dev_name)s exited '
'abnormally (status %(exit_code)s): %(err)s'),
{"dev_name": dev_name, "exit_code": e.exit_code,
"err": e.stderr})
return False
return True
def _get_passphrase(self, key):
"""Convert raw key to string."""
return binascii.hexlify(key).decode('utf-8')
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 _get_mangled_passphrase(self, key):
"""Convert the raw key into a list of unsigned int's and then a string
"""
# NOTE(lyarwood): This replicates the methods used prior to Newton to
# first encode the passphrase as a list of unsigned int's before
# decoding back into a string. This method strips any leading 0's
# of the resulting hex digit pairs, resulting in a different
# passphrase being returned.
encoded_key = array.array('B', key).tolist()
return ''.join(hex(x).replace('0x', '') for x in encoded_key)
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)
try:
self._open_volume(passphrase, **kwargs)
except processutils.ProcessExecutionError as e:
if e.exit_code == 2:
# NOTE(lyarwood): Workaround bug#1633518 by attempting to use
# a mangled passphrase to open the device..
LOG.info(_LI("Unable to open %s with the current passphrase, "
"attempting to use a mangled passphrase to open "
"the volume."), self.dev_path)
self._open_volume(self._get_mangled_passphrase(key), **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)
# cryptsetup returns 4 when attempting to destroy a non-active
# dm-crypt device. We are going to ignore this error code to make
# nova deleting that instance successfully.
utils.execute('cryptsetup', 'remove', self.dev_name,
run_as_root=True, check_exit_code=[0, 4])
def detach_volume(self, **kwargs):
"""Removes the dm-crypt mapping for the device."""
self._close_volume(**kwargs)

View File

@ -1,168 +0,0 @@
# 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 oslo_concurrency import processutils
from oslo_log import log as logging
from nova.i18n import _LI
from nova.i18n import _LW
from nova import utils
from nova.volume.encryptors import cryptsetup
LOG = logging.getLogger(__name__)
def is_luks(device):
"""Checks if the specified device uses LUKS for encryption.
:param device: the device to check
:returns: true if the specified device uses LUKS; false otherwise
"""
try:
# check to see if the device uses LUKS: exit status is 0
# if the device is a LUKS partition and non-zero if not
utils.execute('cryptsetup', 'isLuks', '--verbose', device,
run_as_root=True, check_exit_code=True)
return True
except processutils.ProcessExecutionError as e:
LOG.warning(_LW("isLuks exited abnormally (status %(exit_code)s): "
"%(stderr)s"),
{"exit_code": e.exit_code, "stderr": e.stderr})
return False
class LuksEncryptor(cryptsetup.CryptsetupEncryptor):
"""A VolumeEncryptor based on LUKS.
This VolumeEncryptor uses dm-crypt to encrypt the specified volume.
"""
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, attempts=3)
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 _unmangle_volume(self, key, passphrase, **kwargs):
"""Workaround bug#1633518 by first identifying if a mangled passphrase
is used before replacing it with the correct passphrase.
"""
mangled_passphrase = self._get_mangled_passphrase(key)
self._open_volume(mangled_passphrase, **kwargs)
self._close_volume(**kwargs)
LOG.debug("%s correctly opened with a mangled passphrase, replacing"
"this with the original passphrase", self.dev_path)
# NOTE(lyarwood): Now that we are sure that the mangled passphrase is
# used attempt to add the correct passphrase before removing the
# mangled version from the volume.
# luksAddKey currently prompts for the following input :
# Enter any existing passphrase:
# Enter new passphrase for key slot:
# Verify passphrase:
utils.execute('cryptsetup', 'luksAddKey', self.dev_path,
process_input=''.join([mangled_passphrase, '\n',
passphrase, '\n', passphrase]),
run_as_root=True, check_exit_code=True)
# Verify that we can open the volume with the current passphrase
# before removing the mangled passphrase.
self._open_volume(passphrase, **kwargs)
self._close_volume(**kwargs)
# luksRemoveKey only prompts for the key to remove.
utils.execute('cryptsetup', 'luksRemoveKey', self.dev_path,
process_input=mangled_passphrase,
run_as_root=True, check_exit_code=True)
LOG.debug("%s mangled passphrase successfully replaced", self.dev_path)
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)
try:
self._open_volume(passphrase, **kwargs)
except processutils.ProcessExecutionError as e:
if e.exit_code == 1 and not is_luks(self.dev_path):
# the device has never been formatted; format it and try again
LOG.info(_LI("%s is not a valid LUKS device;"
" formatting device for first use"),
self.dev_path)
self._format_volume(passphrase, **kwargs)
self._open_volume(passphrase, **kwargs)
elif e.exit_code == 2:
# NOTE(lyarwood): Workaround bug#1633518 by replacing any
# mangled passphrases that are found on the volume.
# TODO(lyarwood): Remove workaround during R.
LOG.warning(_LW("%s is not usable with the current "
"passphrase, attempting to use a mangled "
"passphrase to open the volume."),
self.dev_path)
self._unmangle_volume(key, 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,
attempts=3)

View File

@ -1,31 +0,0 @@
# 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.volume.encryptors import base
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 attach_volume(self, context):
pass
def detach_volume(self):
pass