summaryrefslogtreecommitdiff
path: root/ironic_python_agent
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2022-07-20 08:23:55 +0000
committerGerrit Code Review <review@openstack.org>2022-07-20 08:23:55 +0000
commit21b21a5f15bce4871f6af0dc06ebf50d0e9b0967 (patch)
treeb39fc6a45ecc4aeac290a91a71e2ff99534abe7a /ironic_python_agent
parent6a1334a0683121dc94390c8a57aec1c3af208398 (diff)
parentbeb7484858d56ef34699895412881945c5507c81 (diff)
downloadironic-python-agent-21b21a5f15bce4871f6af0dc06ebf50d0e9b0967.tar.gz
Merge "Guard shared device/cluster filesystems"
Diffstat (limited to 'ironic_python_agent')
-rw-r--r--ironic_python_agent/config.py11
-rw-r--r--ironic_python_agent/errors.py19
-rw-r--r--ironic_python_agent/hardware.py121
-rw-r--r--ironic_python_agent/tests/unit/test_hardware.py202
4 files changed, 347 insertions, 6 deletions
diff --git a/ironic_python_agent/config.py b/ironic_python_agent/config.py
index 8901c45a..9251a3e3 100644
--- a/ironic_python_agent/config.py
+++ b/ironic_python_agent/config.py
@@ -315,6 +315,17 @@ cli_opts = [
min=0, max=99, # 100 is when IPA is booted
help='Priority of the inject_files deploy step (disabled '
'by default), an integer between 1 and .'),
+ cfg.BoolOpt('guard-special-filesystems',
+ default=APARAMS.get('ipa-guard-special-filesystems', True),
+ help='Guard "special" shared device filesystems from '
+ 'cleaning by the stock hardware manager\'s cleaning '
+ 'methods. If one of these filesystems is detected '
+ 'during cleaning, the cleaning process will be aborted '
+ 'and infrastructure operator intervention may be '
+ 'required as this option is intended to prevent '
+ 'cleaning from inadvertently destroying a running '
+ 'cluster which may be visible over a storage fabric '
+ 'such as FibreChannel.'),
]
CONF.register_cli_opts(cli_opts)
diff --git a/ironic_python_agent/errors.py b/ironic_python_agent/errors.py
index 1f97c0e2..a0994207 100644
--- a/ironic_python_agent/errors.py
+++ b/ironic_python_agent/errors.py
@@ -348,3 +348,22 @@ class HeartbeatConnectionError(IronicAPIError):
def __init__(self, details):
super(HeartbeatConnectionError, self).__init__(details)
+
+
+class ProtectedDeviceError(CleaningError):
+ """Error raised when a cleaning is halted due to a protected device."""
+
+ message = 'Protected device located, cleaning aborted.'
+
+ def __init__(self, device, what):
+ details = ('Protected %(what)s located on device %(device)s. '
+ 'This volume or contents may be a shared block device. '
+ 'Please consult your storage administrator, and restart '
+ 'cleaning after either detaching the volumes, or '
+ 'instructing IPA to not protect contents. See Ironic '
+ 'Python Agent documentation for more information.' %
+ {'what': what,
+ 'device': device})
+
+ self.message = details
+ super(CleaningError, self).__init__(details)
diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py
index 0f7e4f82..dfcce6f8 100644
--- a/ironic_python_agent/hardware.py
+++ b/ironic_python_agent/hardware.py
@@ -924,6 +924,10 @@ class HardwareManager(object, metaclass=abc.ABCMeta):
:param node: Ironic node object
:param ports: list of Ironic port objects
+ :raises: ProtectedDeviceFound if a device has been identified which
+ may require manual intervention due to the contents and
+ operational risk which exists as it could also be a sign
+ of an environmental misconfiguration.
:return: a dictionary in the form {device.name: erasure output}
"""
erase_results = {}
@@ -937,6 +941,7 @@ class HardwareManager(object, metaclass=abc.ABCMeta):
thread_pool = ThreadPool(min(max_pool_size, len(block_devices)))
for block_device in block_devices:
params = {'node': node, 'block_device': block_device}
+ safety_check_block_device(node, block_device.name)
erase_results[block_device.name] = thread_pool.apply_async(
dispatch_to_managers, ('erase_block_device',), params)
thread_pool.close()
@@ -1541,6 +1546,10 @@ class GenericHardwareManager(HardwareManager):
:param ports: list of Ironic port objects
:raises BlockDeviceEraseError: when there's an error erasing the
block device
+ :raises: ProtectedDeviceFound if a device has been identified which
+ may require manual intervention due to the contents and
+ operational risk which exists as it could also be a sign
+ of an environmental misconfiguration.
"""
block_devices = self.list_block_devices(include_partitions=True)
# NOTE(coreywright): Reverse sort by device name so a partition (eg
@@ -1549,6 +1558,7 @@ class GenericHardwareManager(HardwareManager):
block_devices.sort(key=lambda dev: dev.name, reverse=True)
erase_errors = {}
for dev in self._list_erasable_devices():
+ safety_check_block_device(node, dev.name)
try:
disk_utils.destroy_disk_metadata(dev.name, node['uuid'])
except processutils.ProcessExecutionError as e:
@@ -1572,6 +1582,10 @@ class GenericHardwareManager(HardwareManager):
:param ports: list of Ironic port objects
:raises BlockDeviceEraseError: when there's an error erasing the
block device
+ :raises: ProtectedDeviceFound if a device has been identified which
+ may require manual intervention due to the contents and
+ operational risk which exists as it could also be a sign
+ of an environmental misconfiguration.
"""
erase_errors = {}
info = node.get('driver_internal_info', {})
@@ -1579,6 +1593,7 @@ class GenericHardwareManager(HardwareManager):
LOG.debug("No erasable devices have been found.")
return
for dev in self._list_erasable_devices():
+ safety_check_block_device(node, dev.name)
try:
if self._is_nvme(dev):
execute_nvme_erase = info.get(
@@ -2957,3 +2972,109 @@ def get_multipath_status():
# as if we directly try and work with the global var, we will be racing
# tests endlessly.
return MULTIPATH_ENABLED
+
+
+def safety_check_block_device(node, device):
+ """Performs safety checking of a block device before destroying.
+
+ In order to guard against distruction of file systems such as
+ shared-disk file systems
+ (https://en.wikipedia.org/wiki/Clustered_file_system#SHARED-DISK)
+ or similar filesystems where multiple distinct computers may have
+ unlocked concurrent IO access to the entire block device or
+ SAN Logical Unit Number, we need to evaluate, and block cleaning
+ from occuring on these filesystems *unless* we have been explicitly
+ configured to do so.
+
+ This is because cleaning is an intentionally distructive operation,
+ and once started against such a device, given the complexities of
+ shared disk clustered filesystems where concurrent access is a design
+ element, in all likelihood the entire cluster can be negatively
+ impacted, and an operator will be forced to recover from snapshot and
+ or backups of the volume's contents.
+
+ :param node: A node, or cached node object.
+ :param device: String representing the path to the block
+ device to be checked.
+ :raises: ProtectedDeviceError when a device is identified with
+ one of these known clustered filesystems, and the overall
+ settings have not indicated for the agent to skip such
+ safety checks.
+ """
+
+ # NOTE(TheJulia): While this seems super rare, I found out after this
+ # thread of discussion started that I have customers which have done
+ # this and wiped out SAN volumes and their contents unintentionally
+ # as a result of these filesystems not being guarded.
+ # For those not familiar with shared disk clustered filesystems, think
+ # of it as like your impacting a Ceph cluster, except your suddenly
+ # removing the underlying disks from the OSD, and the entire cluster
+ # goes down.
+
+ if not CONF.guard_special_filesystems:
+ return
+ di_info = node.get('driver_internal_info', {})
+ if not di_info.get('wipe_special_filesystems', True):
+ return
+ report, _e = il_utils.execute('lsblk', '-Pbia',
+ '-oFSTYPE,UUID,PTUUID,PARTTYPE,PARTUUID',
+ device)
+
+ lines = report.splitlines()
+
+ identified_fs_types = []
+ identified_ids = []
+ for line in lines:
+ device = {}
+ # Split into KEY=VAL pairs
+ vals = shlex.split(line)
+ if not vals:
+ continue
+ for key, val in (v.split('=', 1) for v in vals):
+ if key == 'FSTYPE':
+ identified_fs_types.append(val)
+ if key in ['UUID', 'PTUUID', 'PARTTYPE', 'PARTUUID']:
+ identified_ids.append(val)
+ # Ignore block types not specified
+
+ _check_for_special_partitions_filesystems(
+ device,
+ identified_ids,
+ identified_fs_types)
+
+
+def _check_for_special_partitions_filesystems(device, ids, fs_types):
+ """Compare supplied IDs, Types to known items, and raise if found.
+
+ :param device: The block device in use, specificially for logging.
+ :param ids: A list above IDs found to check.
+ :param fs_types: A list of FS types found to check.
+ :raises: ProtectedDeviceError should a partition label or metadata
+ be discovered which suggests a shared disk clustered filesystem
+ has been discovered.
+ """
+
+ guarded_ids = {
+ # Apparently GPFS can used shared volumes....
+ '37AFFC90-EF7D-4E96-91C3-2D7AE055B174': 'IBM GPFS Partition',
+ # Shared volume parallel filesystem
+ 'AA31E02A-400F-11DB-9590-000C2911D1B8': 'VMware VMFS Partition (GPT)',
+ '0xfb': 'VMware VMFS Partition (MBR)',
+ }
+ for key, value in guarded_ids.items():
+ for id_value in ids:
+ if key == id_value:
+ raise errors.ProtectedDeviceError(
+ device=device,
+ what=value)
+
+ guarded_fs_types = {
+ 'gfs2': 'Red Hat Global File System 2',
+ }
+
+ for key, value in guarded_fs_types.items():
+ for fs in fs_types:
+ if key == fs:
+ raise errors.ProtectedDeviceError(
+ device=device,
+ what=value)
diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py
index a610eb2f..21349050 100644
--- a/ironic_python_agent/tests/unit/test_hardware.py
+++ b/ironic_python_agent/tests/unit/test_hardware.py
@@ -1710,10 +1710,12 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_listdir.assert_has_calls(expected_calls)
mocked_mpath.assert_called_once_with()
+ @mock.patch.object(hardware, 'safety_check_block_device', autospec=True)
@mock.patch.object(hardware, 'ThreadPool', autospec=True)
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
def test_erase_devices_no_parallel_by_default(self, mocked_dispatch,
- mock_threadpool):
+ mock_threadpool,
+ mock_safety_check):
# NOTE(TheJulia): This test was previously more elaborate and
# had a high failure rate on py37 and py38. So instead, lets just
@@ -1732,10 +1734,43 @@ class TestGenericHardwareManager(base.IronicAgentTest):
calls = [mock.call(1)]
self.hardware.erase_devices({}, [])
mock_threadpool.assert_has_calls(calls)
+ mock_safety_check.assert_has_calls([
+ mock.call({}, '/dev/sdj'),
+ mock.call({}, '/dev/hdaa')
+ ])
+
+ @mock.patch.object(hardware, 'safety_check_block_device', autospec=True)
+ @mock.patch.object(hardware, 'ThreadPool', autospec=True)
+ @mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
+ def test_erase_devices_no_parallel_by_default_protected_device(
+ self, mocked_dispatch,
+ mock_threadpool,
+ mock_safety_check):
+ mock_safety_check.side_effect = errors.ProtectedDeviceError(
+ device='foo',
+ what='bar')
+
+ self.hardware.list_block_devices = mock.Mock()
+
+ self.hardware.list_block_devices.return_value = [
+ hardware.BlockDevice('/dev/sdj', 'big', 1073741824, True),
+ hardware.BlockDevice('/dev/hdaa', 'small', 65535, False),
+ ]
+
+ calls = [mock.call(1)]
+ self.assertRaises(errors.ProtectedDeviceError,
+ self.hardware.erase_devices, {}, [])
+ mock_threadpool.assert_has_calls(calls)
+ mock_safety_check.assert_has_calls([
+ mock.call({}, '/dev/sdj'),
+ ])
+ mock_threadpool.apply_async.assert_not_called()
+ @mock.patch.object(hardware, 'safety_check_block_device', autospec=True)
@mock.patch('multiprocessing.pool.ThreadPool.apply_async', autospec=True)
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
- def test_erase_devices_concurrency(self, mocked_dispatch, mocked_async):
+ def test_erase_devices_concurrency(self, mocked_dispatch, mocked_async,
+ mock_safety_check):
internal_info = self.node['driver_internal_info']
internal_info['disk_erasure_concurrency'] = 10
mocked_dispatch.return_value = 'erased device'
@@ -1760,9 +1795,15 @@ class TestGenericHardwareManager(base.IronicAgentTest):
for dev in (blkdev1, blkdev2)]
mocked_async.assert_has_calls(calls)
self.assertEqual(expected, result)
+ mock_safety_check.assert_has_calls([
+ mock.call(self.node, '/dev/sdj'),
+ mock.call(self.node, '/dev/hdaa'),
+ ])
+ @mock.patch.object(hardware, 'safety_check_block_device', autospec=True)
@mock.patch.object(hardware, 'ThreadPool', autospec=True)
- def test_erase_devices_concurrency_pool_size(self, mocked_pool):
+ def test_erase_devices_concurrency_pool_size(self, mocked_pool,
+ mock_safety_check):
self.hardware.list_block_devices = mock.Mock()
self.hardware.list_block_devices.return_value = [
hardware.BlockDevice('/dev/sdj', 'big', 1073741824, True),
@@ -1782,6 +1823,10 @@ class TestGenericHardwareManager(base.IronicAgentTest):
self.hardware.erase_devices(self.node, [])
mocked_pool.assert_called_with(1)
+ mock_safety_check.assert_has_calls([
+ mock.call(self.node, '/dev/sdj'),
+ mock.call(self.node, '/dev/hdaa'),
+ ])
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
def test_erase_devices_without_disk(self, mocked_dispatch):
@@ -2441,6 +2486,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mock.call('/sys/fs/pstore/' + arg) for arg in pstore_entries
])
+ @mock.patch.object(hardware, 'safety_check_block_device', autospec=True)
@mock.patch.object(il_utils, 'execute', autospec=True)
@mock.patch.object(disk_utils, 'destroy_disk_metadata', autospec=True)
@mock.patch.object(hardware.GenericHardwareManager,
@@ -2449,7 +2495,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
'_list_erasable_devices', autospec=True)
def test_erase_devices_express(
self, mock_list_erasable_devices, mock_nvme_erase,
- mock_destroy_disk_metadata, mock_execute):
+ mock_destroy_disk_metadata, mock_execute, mock_safety_check):
block_devices = [
hardware.BlockDevice('/dev/sda', 'sata', 65535, False),
hardware.BlockDevice('/dev/md0', 'raid-device', 32767, False),
@@ -2466,7 +2512,44 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mock.call('/dev/md0', self.node['uuid'])],
mock_destroy_disk_metadata.call_args_list)
mock_list_erasable_devices.assert_called_with(self.hardware)
+ mock_safety_check.assert_has_calls([
+ mock.call(self.node, '/dev/sda'),
+ mock.call(self.node, '/dev/md0'),
+ mock.call(self.node, '/dev/nvme0n1'),
+ mock.call(self.node, '/dev/nvme1n1')
+ ])
+
+ @mock.patch.object(hardware, 'safety_check_block_device', autospec=True)
+ @mock.patch.object(il_utils, 'execute', autospec=True)
+ @mock.patch.object(disk_utils, 'destroy_disk_metadata', autospec=True)
+ @mock.patch.object(hardware.GenericHardwareManager,
+ '_nvme_erase', autospec=True)
+ @mock.patch.object(hardware.GenericHardwareManager,
+ '_list_erasable_devices', autospec=True)
+ def test_erase_devices_express_stops_on_safety_failure(
+ self, mock_list_erasable_devices, mock_nvme_erase,
+ mock_destroy_disk_metadata, mock_execute, mock_safety_check):
+ mock_safety_check.side_effect = errors.ProtectedDeviceError(
+ device='foo',
+ what='bar')
+ block_devices = [
+ hardware.BlockDevice('/dev/sda', 'sata', 65535, False),
+ hardware.BlockDevice('/dev/md0', 'raid-device', 32767, False),
+ hardware.BlockDevice('/dev/nvme0n1', 'nvme', 32767, False),
+ hardware.BlockDevice('/dev/nvme1n1', 'nvme', 32767, False)
+ ]
+ mock_list_erasable_devices.return_value = list(block_devices)
+
+ self.assertRaises(errors.ProtectedDeviceError,
+ self.hardware.erase_devices_express, self.node, [])
+ mock_nvme_erase.assert_not_called()
+ mock_destroy_disk_metadata.assert_not_called()
+ mock_list_erasable_devices.assert_called_with(self.hardware)
+ mock_safety_check.assert_has_calls([
+ mock.call(self.node, '/dev/sda')
+ ])
+ @mock.patch.object(hardware, 'safety_check_block_device', autospec=True)
@mock.patch.object(il_utils, 'execute', autospec=True)
@mock.patch.object(hardware.GenericHardwareManager,
'_is_virtual_media_device', autospec=True)
@@ -2475,7 +2558,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(disk_utils, 'destroy_disk_metadata', autospec=True)
def test_erase_devices_metadata(
self, mock_metadata, mock_list_devs, mock__is_vmedia,
- mock_execute):
+ mock_execute, mock_safety_check):
block_devices = [
hardware.BlockDevice('/dev/sr0', 'vmedia', 12345, True),
hardware.BlockDevice('/dev/sdb2', 'raid-member', 32767, False),
@@ -2509,7 +2592,62 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mock.call(self.hardware, block_devices[2]),
mock.call(self.hardware, block_devices[5])],
mock__is_vmedia.call_args_list)
+ mock_safety_check.assert_has_calls([
+ mock.call(self.node, '/dev/sda1'),
+ mock.call(self.node, '/dev/sda'),
+ # This is kind of redundant code/pattern behavior wise
+ # but you never know what someone has done...
+ mock.call(self.node, '/dev/md0')
+ ])
+ @mock.patch.object(hardware, 'safety_check_block_device', autospec=True)
+ @mock.patch.object(il_utils, 'execute', autospec=True)
+ @mock.patch.object(hardware.GenericHardwareManager,
+ '_is_virtual_media_device', autospec=True)
+ @mock.patch.object(hardware.GenericHardwareManager,
+ 'list_block_devices', autospec=True)
+ @mock.patch.object(disk_utils, 'destroy_disk_metadata', autospec=True)
+ def test_erase_devices_metadata_safety_check(
+ self, mock_metadata, mock_list_devs, mock__is_vmedia,
+ mock_execute, mock_safety_check):
+ block_devices = [
+ hardware.BlockDevice('/dev/sr0', 'vmedia', 12345, True),
+ hardware.BlockDevice('/dev/sdb2', 'raid-member', 32767, False),
+ hardware.BlockDevice('/dev/sda', 'small', 65535, False),
+ hardware.BlockDevice('/dev/sda1', '', 32767, False),
+ hardware.BlockDevice('/dev/sda2', 'raid-member', 32767, False),
+ hardware.BlockDevice('/dev/md0', 'raid-device', 32767, False)
+ ]
+ # NOTE(coreywright): Don't return the list, but a copy of it, because
+ # we depend on its elements' order when referencing it later during
+ # verification, but the method under test sorts the list changing it.
+ mock_list_devs.return_value = list(block_devices)
+ mock__is_vmedia.side_effect = lambda _, dev: dev.name == '/dev/sr0'
+ mock_execute.side_effect = [
+ ('sdb2 linux_raid_member host:1 f9978968', ''),
+ ('sda2 linux_raid_member host:1 f9978969', ''),
+ ('sda1', ''), ('sda', ''), ('md0', '')]
+ mock_safety_check.side_effect = [
+ None,
+ errors.ProtectedDeviceError(
+ device='foo',
+ what='bar')
+ ]
+
+ self.assertRaises(errors.ProtectedDeviceError,
+ self.hardware.erase_devices_metadata,
+ self.node, [])
+
+ self.assertEqual([mock.call('/dev/sda1', self.node['uuid'])],
+ mock_metadata.call_args_list)
+ mock_list_devs.assert_called_with(self.hardware,
+ include_partitions=True)
+ mock_safety_check.assert_has_calls([
+ mock.call(self.node, '/dev/sda1'),
+ mock.call(self.node, '/dev/sda'),
+ ])
+
+ @mock.patch.object(hardware, 'safety_check_block_device', autospec=True)
@mock.patch.object(hardware.GenericHardwareManager,
'_is_linux_raid_member', autospec=True)
@mock.patch.object(hardware.GenericHardwareManager,
@@ -2519,7 +2657,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(disk_utils, 'destroy_disk_metadata', autospec=True)
def test_erase_devices_metadata_error(
self, mock_metadata, mock_list_devs, mock__is_vmedia,
- mock__is_raid_member):
+ mock__is_raid_member, mock_safety_check):
block_devices = [
hardware.BlockDevice('/dev/sda', 'small', 65535, False),
hardware.BlockDevice('/dev/sdb', 'big', 10737418240, True),
@@ -2553,6 +2691,10 @@ class TestGenericHardwareManager(base.IronicAgentTest):
self.assertEqual([mock.call(self.hardware, block_devices[1]),
mock.call(self.hardware, block_devices[0])],
mock__is_vmedia.call_args_list)
+ mock_safety_check.assert_has_calls([
+ mock.call(self.node, '/dev/sdb'),
+ mock.call(self.node, '/dev/sda')
+ ])
@mock.patch.object(il_utils, 'execute', autospec=True)
def test__is_linux_raid_member(self, mocked_execute):
@@ -4991,3 +5133,51 @@ class TestAPIClientSaveAndUse(base.IronicAgentTest):
calls = [mock.call('list_hardware_info'),
mock.call('wait_for_disks')]
mock_dispatch.assert_has_calls(calls)
+
+
+@mock.patch.object(il_utils, 'execute', autospec=True)
+class TestProtectedDiskSafetyChecks(base.IronicAgentTest):
+
+ def test_special_filesystem_guard_not_enabled(self, mock_execute):
+ CONF.set_override('guard_special_filesystems', False)
+ hardware.safety_check_block_device({}, '/dev/foo')
+ mock_execute.assert_not_called()
+
+ def test_special_filesystem_guard_node_indicates_skip(self, mock_execute):
+ node = {
+ 'driver_internal_info': {
+ 'wipe_special_filesystems': False
+ }
+ }
+ mock_execute.return_value = ('', '')
+ hardware.safety_check_block_device(node, '/dev/foo')
+ mock_execute.assert_not_called()
+
+ def test_special_filesystem_guard_enabled_no_results(self, mock_execute):
+ mock_execute.return_value = ('', '')
+ hardware.safety_check_block_device({}, '/dev/foo')
+
+ def test_special_filesystem_guard_raises(self, mock_execute):
+ GFS2 = 'FSTYPE="gfs2"\n'
+ GPFS1 = 'UUID="37AFFC90-EF7D-4E96-91C3-2D7AE055B174"\n'
+ GPFS2 = 'PTUUID="37AFFC90-EF7D-4E96-91C3-2D7AE055B174"\n'
+ GPFS3 = 'PARTTYPE="37AFFC90-EF7D-4E96-91C3-2D7AE055B174"\n'
+ GPFS4 = 'PARTUUID="37AFFC90-EF7D-4E96-91C3-2D7AE055B174"\n'
+ VMFS1 = 'UUID="AA31E02A-400F-11DB-9590-000C2911D1B8"\n'
+ VMFS2 = 'UUID="AA31E02A-400F-11DB-9590-000C2911D1B8"\n'
+ VMFS3 = 'UUID="AA31E02A-400F-11DB-9590-000C2911D1B8"\n'
+ VMFS4 = 'UUID="AA31E02A-400F-11DB-9590-000C2911D1B8"\n'
+ VMFS5 = 'UUID="0xfb"\n'
+ VMFS6 = 'PTUUID="0xfb"\n'
+ VMFS7 = 'PARTTYPE="0xfb"\n'
+ VMFS8 = 'PARTUUID="0xfb"\n'
+
+ expected_failures = [GFS2, GPFS1, GPFS2, GPFS3, GPFS4, VMFS1, VMFS2,
+ VMFS3, VMFS4, VMFS5, VMFS6, VMFS7, VMFS8]
+ for failure in expected_failures:
+ mock_execute.reset_mock()
+ mock_execute.return_value = (failure, '')
+ self.assertRaises(errors.ProtectedDeviceError,
+ hardware.safety_check_block_device,
+ {}, '/dev/foo')
+ self.assertEqual(1, mock_execute.call_count)