summaryrefslogtreecommitdiff
path: root/ironic_python_agent
diff options
context:
space:
mode:
Diffstat (limited to 'ironic_python_agent')
-rw-r--r--ironic_python_agent/errors.py10
-rw-r--r--ironic_python_agent/hardware.py96
-rw-r--r--ironic_python_agent/tests/hardware.py205
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)