Add a HardwareManager method to erase devices

Add erase_devices method to the HardwareManager class. By default this
method iterates block devices, and calls a new abstract
erase_block_device method for each device. This patch includes a
simple implementation of erase_block_device on the
GenericHardwareManager which attempts to issue an ATA secure erase on
supported devices.

Change-Id: I81da065395b8785f636f1b0a0d60c9f1c045441e
This commit is contained in:
Russell Haering 2014-06-04 10:44:25 -07:00
parent 8fd76c877b
commit dff46583d3
4 changed files with 312 additions and 1 deletions

@ -10,7 +10,7 @@ ADD . /tmp/ironic-python-agent
RUN apt-get update && \
apt-get -y upgrade && \
apt-get install -y --no-install-recommends python2.7 python2.7-dev \
python-pip qemu-utils parted util-linux genisoimage git gcc && \
python-pip qemu-utils parted hdparm util-linux genisoimage git gcc && \
apt-get -y autoremove && \
apt-get clean

@ -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

@ -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

@ -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)