diff --git a/os_brick/encryptors/luks.py b/os_brick/encryptors/luks.py index 346027503..2aea3392b 100644 --- a/os_brick/encryptors/luks.py +++ b/os_brick/encryptors/luks.py @@ -13,10 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_concurrency import processutils as putils +import binascii +import os + +from oslo_concurrency import processutils from oslo_log import log as logging -from os_brick.encryptors import cryptsetup +from os_brick.encryptors import base +from os_brick import exception from os_brick.privileged import rootwrap as priv_rootwrap from os_brick import utils @@ -38,14 +42,14 @@ def is_luks(root_helper, device, execute=None): run_as_root=True, root_helper=root_helper, check_exit_code=True) return True - except putils.ProcessExecutionError as e: + except processutils.ProcessExecutionError as e: LOG.warning("isLuks exited abnormally (status %(exit_code)s): " "%(stderr)s", {"exit_code": e.exit_code, "stderr": e.stderr}) return False -class LuksEncryptor(cryptsetup.CryptsetupEncryptor): +class LuksEncryptor(base.VolumeEncryptor): """A VolumeEncryptor based on LUKS. This VolumeEncryptor uses dm-crypt to encrypt the specified volume. @@ -62,6 +66,59 @@ class LuksEncryptor(cryptsetup.CryptsetupEncryptor): execute=execute, *args, **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' % os.path.basename(self.symlink_path) + + # NOTE(lixiaoy1): This is to import fix for 1439869 from Nova. + # 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 = os.path.basename(self.symlink_path) + 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/'. + 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: + self._execute('cryptsetup', 'status', dev_name, run_as_root=True) + except processutils.ProcessExecutionError as e: + # If /dev/mapper/ 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('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 _format_volume(self, passphrase, **kwargs): """Creates a LUKS v1 header on the volume. @@ -103,6 +160,10 @@ class LuksEncryptor(cryptsetup.CryptsetupEncryptor): root_helper=self._root_helper, attempts=3) + 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 passphrase. @@ -128,7 +189,7 @@ class LuksEncryptor(cryptsetup.CryptsetupEncryptor): try: self._open_volume(passphrase, **kwargs) - except putils.ProcessExecutionError as e: + except processutils.ProcessExecutionError as e: if e.exit_code == 1 and not is_luks(self._root_helper, self.dev_path, execute=self._execute): @@ -160,6 +221,10 @@ class LuksEncryptor(cryptsetup.CryptsetupEncryptor): root_helper=self._root_helper, attempts=3) + def detach_volume(self, **kwargs): + """Removes the dm-crypt mapping for the device.""" + self._close_volume(**kwargs) + def extend_volume(self, context, **kwargs): """Extend an encrypted volume and return the decrypted volume size.""" symlink = self.symlink_path diff --git a/os_brick/tests/encryptors/test_luks.py b/os_brick/tests/encryptors/test_luks.py index 9e8ad7551..13436eb79 100644 --- a/os_brick/tests/encryptors/test_luks.py +++ b/os_brick/tests/encryptors/test_luks.py @@ -13,16 +13,35 @@ # License for the specific language governing permissions and limitations # under the License. +import binascii +import copy from unittest import mock +from castellan.common.objects import symmetric_key as key +from castellan.tests.unit.key_manager import fake from oslo_concurrency import processutils as putils -from os_brick.encryptors import cryptsetup from os_brick.encryptors import luks -from os_brick.tests.encryptors import test_cryptsetup +from os_brick import exception +from os_brick.tests.encryptors import test_base -class LuksEncryptorTestCase(test_cryptsetup.CryptsetupEncryptorTestCase): +def fake__get_key(context, passphrase): + raw = bytes(binascii.unhexlify(passphrase)) + symmetric_key = key.SymmetricKey('AES', len(raw) * 8, raw) + return symmetric_key + + +class LuksEncryptorTestCase(test_base.VolumeEncryptorTestCase): + + def setUp(self): + super().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 + def _create(self): return luks.LuksEncryptor(root_helper=self.root_helper, connection_info=self.connection_info, @@ -82,8 +101,7 @@ class LuksEncryptorTestCase(test_cryptsetup.CryptsetupEncryptorTestCase): def test_attach_volume(self, mock_execute): fake_key = '0c84146034e747639b698368807286df' self.encryptor._get_key = mock.MagicMock() - self.encryptor._get_key.return_value = ( - test_cryptsetup.fake__get_key(None, fake_key)) + self.encryptor._get_key.return_value = fake__get_key(None, fake_key) self.encryptor.attach_volume(None) @@ -102,8 +120,7 @@ class LuksEncryptorTestCase(test_cryptsetup.CryptsetupEncryptorTestCase): def test_attach_volume_not_formatted(self, mock_execute): fake_key = 'bc37c5eccebe403f9cc2d0dd20dac2bc' self.encryptor._get_key = mock.MagicMock() - self.encryptor._get_key.return_value = ( - test_cryptsetup.fake__get_key(None, fake_key)) + self.encryptor._get_key.return_value = fake__get_key(None, fake_key) mock_execute.side_effect = [ putils.ProcessExecutionError(exit_code=1), # luksOpen @@ -142,8 +159,7 @@ class LuksEncryptorTestCase(test_cryptsetup.CryptsetupEncryptorTestCase): def test_attach_volume_fail(self, mock_execute): fake_key = 'ea6c2e1b8f7f4f84ae3560116d659ba2' self.encryptor._get_key = mock.MagicMock() - self.encryptor._get_key.return_value = ( - test_cryptsetup.fake__get_key(None, fake_key)) + self.encryptor._get_key.return_value = fake__get_key(None, fake_key) mock_execute.side_effect = [ putils.ProcessExecutionError(exit_code=1), # luksOpen @@ -183,10 +199,63 @@ class LuksEncryptorTestCase(test_cryptsetup.CryptsetupEncryptorTestCase): attempts=3, run_as_root=True, check_exit_code=[0, 4]), ]) + 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, + luks.LuksEncryptor, + root_helper=self.root_helper, + connection_info=connection_info, + keymgr=fake.fake_api()) + self.assertIn(type, str(exc)) + + @mock.patch('os_brick.executor.Executor._execute') + @mock.patch('os.path.exists', return_value=True) + def test_init_volume_encryption_with_old_name(self, mock_exists, + mock_execute): + # If an old name crypt device exists, dev_path should be the old name. + old_dev_name = self.dev_path.split('/')[-1] + encryptor = luks.LuksEncryptor( + root_helper=self.root_helper, + connection_info=self.connection_info, + keymgr=self.keymgr) + 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_brick.executor.Executor._execute') + @mock.patch('os.path.exists', side_effect=[False, True]) + def test_init_volume_encryption_with_wwn(self, mock_exists, mock_execute): + # 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 = luks.LuksEncryptor( + root_helper=self.root_helper, + connection_info=connection_info, + keymgr=fake.fake_api()) + 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('os_brick.utils.get_device_size') - @mock.patch.object(cryptsetup.CryptsetupEncryptor, '_execute') - @mock.patch.object(cryptsetup.CryptsetupEncryptor, '_get_passphrase') - @mock.patch.object(cryptsetup.CryptsetupEncryptor, '_get_key') + @mock.patch.object(luks.LuksEncryptor, '_execute') + @mock.patch.object(luks.LuksEncryptor, '_get_passphrase') + @mock.patch.object(luks.LuksEncryptor, '_get_key') def test_extend_volume(self, mock_key, mock_pass, mock_exec, mock_size): encryptor = self.encryptor res = encryptor.extend_volume(mock.sentinel.context) @@ -225,8 +294,7 @@ class Luks2EncryptorTestCase(LuksEncryptorTestCase): def test_attach_volume_not_formatted(self, mock_execute): fake_key = 'bc37c5eccebe403f9cc2d0dd20dac2bc' self.encryptor._get_key = mock.MagicMock() - self.encryptor._get_key.return_value = ( - test_cryptsetup.fake__get_key(None, fake_key)) + self.encryptor._get_key.return_value = fake__get_key(None, fake_key) mock_execute.side_effect = [ putils.ProcessExecutionError(exit_code=1), # luksOpen