diff options
Diffstat (limited to 'ironic_python_agent')
| -rw-r--r-- | ironic_python_agent/errors.py | 10 | ||||
| -rw-r--r-- | ironic_python_agent/hardware.py | 96 | ||||
| -rw-r--r-- | ironic_python_agent/tests/hardware.py | 205 |
3 files changed, 311 insertions, 0 deletions
diff --git a/ironic_python_agent/errors.py b/ironic_python_agent/errors.py index 0d86ebac..61d17b3b 100644 --- a/ironic_python_agent/errors.py +++ b/ironic_python_agent/errors.py @@ -184,5 +184,15 @@ class SystemRebootError(RESTError): self.details = self.details.format(exit_code) +class BlockDeviceEraseError(RESTError): + """Error raised when an error occurs erasing a block device.""" + + message = 'Error erasing block device' + + def __init__(self, details): + super(BlockDeviceEraseError, self).__init__(details) + self.details = details + + class ExtensionError(Exception): pass diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py index 2f0a188a..697b1854 100644 --- a/ironic_python_agent/hardware.py +++ b/ironic_python_agent/hardware.py @@ -21,6 +21,7 @@ import six import stevedore from ironic_python_agent import encoding +from ironic_python_agent import errors from ironic_python_agent.openstack.common import log from ironic_python_agent import utils @@ -106,6 +107,42 @@ class HardwareManager(object): def get_os_install_device(self): pass + @abc.abstractmethod + def erase_block_device(self, block_device): + """Attempt to erase a block device. + + Implementations should detect the type of device and erase it in the + most appropriate way possible. Generic implementations should support + common erase mechanisms such as ATA secure erase, or multi-pass random + writes. Operators with more specific needs should override this method + in order to detect and handle "interesting" cases, or delegate to the + parent class to handle generic cases. + + For example: operators running ACME MagicStore (TM) cards alongside + standard SSDs might check whether the device is a MagicStore and use a + proprietary tool to erase that, otherwise call this method on their + parent class. Upstream submissions of common functionality are + encouraged. + + :param block_device: a BlockDevice indicating a device to be erased. + :raises: BlockDeviceEraseError when an error occurs erasing a block + device, or if the block device is not supported. + + """ + pass + + def erase_devices(self): + """Erase any device that holds user data. + + By default this will attempt to erase block devices. This method can be + overridden in an implementation-specific hardware manager in order to + erase additional hardware, although backwards-compatible upstream + submissions are encouraged. + """ + block_devices = self.list_block_devices() + for block_device in block_devices: + self.erase_block_device(block_device) + def list_hardware_info(self): hardware_info = {} hardware_info['interfaces'] = self.list_network_interfaces() @@ -187,6 +224,65 @@ class GenericHardwareManager(HardwareManager): if device.size >= (4 * pow(1024, 3)): return device.name + def erase_block_device(self, block_device): + if self._ata_erase(block_device): + return + + # NOTE(russell_h): Support for additional generic erase methods should + # be added above this raise, in order of precedence. + raise errors.BlockDeviceEraseError(('Unable to erase block device ' + '{0}: device is unsupported.').format(block_device.name)) + + def _get_ata_security_lines(self, block_device): + output = utils.execute('hdparm', '-I', block_device.name)[0] + + if '\nSecurity: ' not in output: + return [] + + # Get all lines after the 'Security: ' line + security_and_beyond = output.split('\nSecurity: \n')[1] + security_and_beyond_lines = security_and_beyond.split('\n') + + security_lines = [] + for line in security_and_beyond_lines: + if line.startswith('\t'): + security_lines.append(line.strip().replace('\t', ' ')) + else: + break + + return security_lines + + def _ata_erase(self, block_device): + security_lines = self._get_ata_security_lines(block_device) + + # If secure erase isn't supported return False so erase_block_device + # can try another mechanism. Below here, if secure erase is supported + # but fails in some way, error out (operators of hardware that supports + # secure erase presumably expect this to work). + if 'supported' not in security_lines: + return False + + if 'enabled' in security_lines: + raise errors.BlockDeviceEraseError(('Block device {0} already has ' + 'a security password set').format(block_device.name)) + + if 'not frozen' not in security_lines: + raise errors.BlockDeviceEraseError(('Block device {0} is frozen ' + 'and cannot be erased').format(block_device.name)) + + utils.execute('hdparm', '--user-master', 'u', '--security-set-pass', + 'NULL', block_device.name) + utils.execute('hdparm', '--user-master', 'u', '--security-erase', + 'NULL', block_device.name) + + # Verify that security is now 'not enabled' + security_lines = self._get_ata_security_lines(block_device) + if 'not enabled' not in security_lines: + raise errors.BlockDeviceEraseError(('An unknown error occurred ' + 'erasing block device {0}').format(block_device.name)) + + return True + def _compare_extensions(ext1, ext2): mgr1 = ext1.obj diff --git a/ironic_python_agent/tests/hardware.py b/ironic_python_agent/tests/hardware.py index 3c0e4b1b..57be8b12 100644 --- a/ironic_python_agent/tests/hardware.py +++ b/ironic_python_agent/tests/hardware.py @@ -16,6 +16,7 @@ import mock from oslotest import base as test_base import six +from ironic_python_agent import errors from ironic_python_agent import hardware from ironic_python_agent import utils @@ -25,6 +26,93 @@ else: OPEN_FUNCTION_NAME = 'builtins.open' +HDPARM_INFO_TEMPLATE = ( + '/dev/sda:\n' + '\n' + 'ATA device, with non-removable media\n' + '\tModel Number: 7 PIN SATA FDM\n' + '\tSerial Number: 20131210000000000023\n' + '\tFirmware Revision: SVN406\n' + '\tTransport: Serial, ATA8-AST, SATA 1.0a, SATA II Extensions, ' + 'SATA Rev 2.5, SATA Rev 2.6, SATA Rev 3.0\n' + 'Standards: \n' + '\tSupported: 9 8 7 6 5\n' + '\tLikely used: 9\n' + 'Configuration: \n' + '\tLogical\t\tmax\tcurrent\n' + '\tcylinders\t16383\t16383\n' + '\theads\t\t16\t16\n' + '\tsectors/track\t63\t63\n' + '\t--\n' + '\tCHS current addressable sectors: 16514064\n' + '\tLBA user addressable sectors: 60579792\n' + '\tLBA48 user addressable sectors: 60579792\n' + '\tLogical Sector size: 512 bytes\n' + '\tPhysical Sector size: 512 bytes\n' + '\tLogical Sector-0 offset: 0 bytes\n' + '\tdevice size with M = 1024*1024: 29579 MBytes\n' + '\tdevice size with M = 1000*1000: 31016 MBytes (31 GB)\n' + '\tcache/buffer size = unknown\n' + '\tForm Factor: 2.5 inch\n' + '\tNominal Media Rotation Rate: Solid State Device\n' + 'Capabilities: \n' + '\tLBA, IORDY(can be disabled)\n' + '\tQueue depth: 32\n' + '\tStandby timer values: spec\'d by Standard, no device specific ' + 'minimum\n' + '\tR/W multiple sector transfer: Max = 1\tCurrent = 1\n' + '\tDMA: mdma0 mdma1 mdma2 udma0 udma1 udma2 udma3 udma4 *udma5\n' + '\t Cycle time: min=120ns recommended=120ns\n' + '\tPIO: pio0 pio1 pio2 pio3 pio4\n' + '\t Cycle time: no flow control=120ns IORDY flow ' + 'control=120ns\n' + 'Commands/features: \n' + '\tEnabled\tSupported:\n' + '\t *\tSMART feature set\n' + '\t \tSecurity Mode feature set\n' + '\t *\tPower Management feature set\n' + '\t *\tWrite cache\n' + '\t *\tLook-ahead\n' + '\t *\tHost Protected Area feature set\n' + '\t *\tWRITE_BUFFER command\n' + '\t *\tREAD_BUFFER command\n' + '\t *\tNOP cmd\n' + '\t \tSET_MAX security extension\n' + '\t *\t48-bit Address feature set\n' + '\t *\tDevice Configuration Overlay feature set\n' + '\t *\tMandatory FLUSH_CACHE\n' + '\t *\tFLUSH_CACHE_EXT\n' + '\t *\tWRITE_{DMA|MULTIPLE}_FUA_EXT\n' + '\t *\tWRITE_UNCORRECTABLE_EXT command\n' + '\t *\tGen1 signaling speed (1.5Gb/s)\n' + '\t *\tGen2 signaling speed (3.0Gb/s)\n' + '\t *\tGen3 signaling speed (6.0Gb/s)\n' + '\t *\tNative Command Queueing (NCQ)\n' + '\t *\tHost-initiated interface power management\n' + '\t *\tPhy event counters\n' + '\t *\tDMA Setup Auto-Activate optimization\n' + '\t \tDevice-initiated interface power management\n' + '\t *\tSoftware settings preservation\n' + '\t \tunknown 78[8]\n' + '\t *\tSMART Command Transport (SCT) feature set\n' + '\t *\tSCT Error Recovery Control (AC3)\n' + '\t *\tSCT Features Control (AC4)\n' + '\t *\tSCT Data Tables (AC5)\n' + '\t *\tData Set Management TRIM supported (limit 2 blocks)\n' + 'Security: \n' + '\tMaster password revision code = 65534\n' + '\t%(supported)s\n' + '\t%(enabled)s\n' + '\tnot\tlocked\n' + '\t%(frozen)s\n' + '\tnot\texpired: security count\n' + '\t\tsupported: enhanced erase\n' + '\t24min for SECURITY ERASE UNIT. 24min for ENHANCED SECURITY ' + 'ERASE UNIT.\n' + 'Checksum: correct\n' +) + + class TestGenericHardwareManager(test_base.BaseTestCase): def setUp(self): super(TestGenericHardwareManager, self).setUp() @@ -160,3 +248,120 @@ class TestGenericHardwareManager(test_base.BaseTestCase): self.hardware.list_block_devices()) self.assertEqual(hardware_info['interfaces'], self.hardware.list_network_interfaces()) + + @mock.patch.object(utils, 'execute') + def test_erase_block_device_ata_success(self, mocked_execute): + hdparm_info_fields = { + 'supported': '\tsupported', + 'enabled': 'not\tenabled', + 'frozen': 'not\tfrozen', + } + mocked_execute.side_effect = [ + (HDPARM_INFO_TEMPLATE % hdparm_info_fields, ''), + ('', ''), + ('', ''), + (HDPARM_INFO_TEMPLATE % hdparm_info_fields, ''), + ] + + block_device = hardware.BlockDevice('/dev/sda', 1073741824) + self.hardware.erase_block_device(block_device) + mocked_execute.assert_has_calls([ + mock.call('hdparm', '-I', '/dev/sda'), + mock.call('hdparm', '--user-master', 'u', '--security-set-pass', + 'NULL', '/dev/sda'), + mock.call('hdparm', '--user-master', 'u', '--security-erase', + 'NULL', '/dev/sda'), + mock.call('hdparm', '-I', '/dev/sda'), + ]) + + @mock.patch.object(utils, 'execute') + def test_erase_block_device_ata_nosecurtiy(self, mocked_execute): + hdparm_output = HDPARM_INFO_TEMPLATE.split('\nSecurity:')[0] + + mocked_execute.side_effect = [ + (hdparm_output, '') + ] + + block_device = hardware.BlockDevice('/dev/sda', 1073741824) + self.assertRaises(errors.BlockDeviceEraseError, + self.hardware.erase_block_device, + block_device) + + @mock.patch.object(utils, 'execute') + def test_erase_block_device_ata_not_supported(self, mocked_execute): + hdparm_output = HDPARM_INFO_TEMPLATE % { + 'supported': 'not\tsupported', + 'enabled': 'not\tenabled', + 'frozen': 'not\tfrozen', + } + + mocked_execute.side_effect = [ + (hdparm_output, '') + ] + + block_device = hardware.BlockDevice('/dev/sda', 1073741824) + self.assertRaises(errors.BlockDeviceEraseError, + self.hardware.erase_block_device, + block_device) + + @mock.patch.object(utils, 'execute') + def test_erase_block_device_ata_security_enabled(self, mocked_execute): + hdparm_output = HDPARM_INFO_TEMPLATE % { + 'supported': '\tsupported', + 'enabled': '\tenabled', + 'frozen': 'not\tfrozen', + } + + mocked_execute.side_effect = [ + (hdparm_output, '') + ] + + block_device = hardware.BlockDevice('/dev/sda', 1073741824) + self.assertRaises(errors.BlockDeviceEraseError, + self.hardware.erase_block_device, + block_device) + + @mock.patch.object(utils, 'execute') + def test_erase_block_device_ata_frozen(self, mocked_execute): + hdparm_output = HDPARM_INFO_TEMPLATE % { + 'supported': '\tsupported', + 'enabled': 'not\tenabled', + 'frozen': '\tfrozen', + } + + mocked_execute.side_effect = [ + (hdparm_output, '') + ] + + block_device = hardware.BlockDevice('/dev/sda', 1073741824) + self.assertRaises(errors.BlockDeviceEraseError, + self.hardware.erase_block_device, + block_device) + + @mock.patch.object(utils, 'execute') + def test_erase_block_device_ata_failed(self, mocked_execute): + hdparm_output_before = HDPARM_INFO_TEMPLATE % { + 'supported': '\tsupported', + 'enabled': 'not\tenabled', + 'frozen': 'not\tfrozen', + } + + # If security mode remains enabled after the erase, it is indiciative + # of a failed erase. + hdparm_output_after = HDPARM_INFO_TEMPLATE % { + 'supported': '\tsupported', + 'enabled': '\tenabled', + 'frozen': 'not\tfrozen', + } + + mocked_execute.side_effect = [ + (hdparm_output_before, ''), + ('', ''), + ('', ''), + (hdparm_output_after, ''), + ] + + block_device = hardware.BlockDevice('/dev/sda', 1073741824) + self.assertRaises(errors.BlockDeviceEraseError, + self.hardware.erase_block_device, + block_device) |
