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:
@@ -10,7 +10,7 @@ ADD . /tmp/ironic-python-agent
|
|||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get -y upgrade && \
|
apt-get -y upgrade && \
|
||||||
apt-get install -y --no-install-recommends python2.7 python2.7-dev \
|
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 -y autoremove && \
|
||||||
apt-get clean
|
apt-get clean
|
||||||
|
|
||||||
|
@@ -184,5 +184,15 @@ class SystemRebootError(RESTError):
|
|||||||
self.details = self.details.format(exit_code)
|
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):
|
class ExtensionError(Exception):
|
||||||
pass
|
pass
|
||||||
|
@@ -21,6 +21,7 @@ import six
|
|||||||
import stevedore
|
import stevedore
|
||||||
|
|
||||||
from ironic_python_agent import encoding
|
from ironic_python_agent import encoding
|
||||||
|
from ironic_python_agent import errors
|
||||||
from ironic_python_agent.openstack.common import log
|
from ironic_python_agent.openstack.common import log
|
||||||
from ironic_python_agent import utils
|
from ironic_python_agent import utils
|
||||||
|
|
||||||
@@ -106,6 +107,42 @@ class HardwareManager(object):
|
|||||||
def get_os_install_device(self):
|
def get_os_install_device(self):
|
||||||
pass
|
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):
|
def list_hardware_info(self):
|
||||||
hardware_info = {}
|
hardware_info = {}
|
||||||
hardware_info['interfaces'] = self.list_network_interfaces()
|
hardware_info['interfaces'] = self.list_network_interfaces()
|
||||||
@@ -187,6 +224,65 @@ class GenericHardwareManager(HardwareManager):
|
|||||||
if device.size >= (4 * pow(1024, 3)):
|
if device.size >= (4 * pow(1024, 3)):
|
||||||
return device.name
|
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):
|
def _compare_extensions(ext1, ext2):
|
||||||
mgr1 = ext1.obj
|
mgr1 = ext1.obj
|
||||||
|
@@ -16,6 +16,7 @@ import mock
|
|||||||
from oslotest import base as test_base
|
from oslotest import base as test_base
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from ironic_python_agent import errors
|
||||||
from ironic_python_agent import hardware
|
from ironic_python_agent import hardware
|
||||||
from ironic_python_agent import utils
|
from ironic_python_agent import utils
|
||||||
|
|
||||||
@@ -25,6 +26,93 @@ else:
|
|||||||
OPEN_FUNCTION_NAME = 'builtins.open'
|
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):
|
class TestGenericHardwareManager(test_base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestGenericHardwareManager, self).setUp()
|
super(TestGenericHardwareManager, self).setUp()
|
||||||
@@ -160,3 +248,120 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
|
|||||||
self.hardware.list_block_devices())
|
self.hardware.list_block_devices())
|
||||||
self.assertEqual(hardware_info['interfaces'],
|
self.assertEqual(hardware_info['interfaces'],
|
||||||
self.hardware.list_network_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)
|
||||||
|
Reference in New Issue
Block a user