Add Sanitize disk erase support using SSA

This commit adds the implementation to perfrom sanitize
disk erase on the disks attached to the  Smart storage
controller using ssacli.

Closes-bug: #1648807
Change-Id: I2a015345a31113f9f9a95b7dd722eb9b3a6bda43
This commit is contained in:
Aparna
2016-11-14 09:31:05 +00:00
parent ce7862dbad
commit 909428eab4
8 changed files with 378 additions and 25 deletions

View File

@@ -22,6 +22,7 @@ from proliantutils import exception
from proliantutils.hpssa import constants from proliantutils.hpssa import constants
from proliantutils.hpssa import disk_allocator from proliantutils.hpssa import disk_allocator
from proliantutils.hpssa import objects from proliantutils.hpssa import objects
from proliantutils.ilo import common
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
RAID_CONFIG_SCHEMA = os.path.join(CURRENT_DIR, "raid_config_schema.json") RAID_CONFIG_SCHEMA = os.path.join(CURRENT_DIR, "raid_config_schema.json")
@@ -74,27 +75,32 @@ def validate(raid_config):
raise exception.InvalidInputError(msg) raise exception.InvalidInputError(msg)
def _filter_raid_mode_controllers(server): def _select_controllers_by(server, select_condition, msg):
"""Filters out the hpssa controllers in raid mode. """Filters out the hpssa controllers based on the condition.
This method updates the server with only the controller which is in raid This method updates the server with only the controller which satisfies
mode. The controller which are in HBA mode are removed from the list. the condition. The controllers which doesn't satisfies the selection
condition will be removed from the list.
:param server: The object containing all the supported hpssa controllers :param server: The object containing all the supported hpssa controllers
details. details.
:param select_condition: A lambda function to select the controllers based
on requirement.
:param msg: A String which describes the controller selection.
:raises exception.HPSSAOperationError, if all the controller are in HBA :raises exception.HPSSAOperationError, if all the controller are in HBA
mode. mode.
""" """
all_controllers = server.controllers all_controllers = server.controllers
non_hba_controllers = [c for c in all_controllers supported_controllers = [c for c in all_controllers if select_condition(c)]
if not c.properties.get('HBA Mode Enabled', False)]
if not non_hba_controllers: if not supported_controllers:
reason = ("None of the available HPSSA controllers %s have RAID " reason = ("None of the available SSA controllers %(controllers)s "
"enabled" % ', '.join([c.id for c in all_controllers])) "have %(msg)s"
% {'controllers': ', '.join([c.id for c in all_controllers]),
'msg': msg})
raise exception.HPSSAOperationError(reason=reason) raise exception.HPSSAOperationError(reason=reason)
server.controllers = non_hba_controllers server.controllers = supported_controllers
def create_configuration(raid_config): def create_configuration(raid_config):
@@ -117,7 +123,9 @@ def create_configuration(raid_config):
""" """
server = objects.Server() server = objects.Server()
_filter_raid_mode_controllers(server) select_controllers = lambda x: not x.properties.get('HBA Mode Enabled',
False)
_select_controllers_by(server, select_controllers, 'RAID enabled')
validate(raid_config) validate(raid_config)
@@ -297,7 +305,9 @@ def delete_configuration():
""" """
server = objects.Server() server = objects.Server()
_filter_raid_mode_controllers(server) select_controllers = lambda x: not x.properties.get('HBA Mode Enabled',
False)
_select_controllers_by(server, select_controllers, 'RAID enabled')
for controller in server.controllers: for controller in server.controllers:
# Trigger delete only if there is some RAID array, otherwise # Trigger delete only if there is some RAID array, otherwise
@@ -337,3 +347,56 @@ def get_configuration():
_update_physical_disk_details(raid_config, server) _update_physical_disk_details(raid_config, server)
return raid_config return raid_config
def has_erase_completed():
server = objects.Server()
drives = server.get_physical_drives()
if any((drive.erase_status == 'Erase In Progress')
for drive in drives):
return False
else:
return True
def erase_devices():
"""Erase all the drives on this server.
This method performs sanitize erase on all the supported physical drives
in this server. This erase cannot be performed on logical drives.
:returns: a dictionary of controllers with drives and the erase status.
:raises exception.HPSSAException, if none of the drives support
sanitize erase.
"""
server = objects.Server()
select_controllers = lambda x: (x.properties.get(
'Sanitize Erase Supported',
False) == 'True')
_select_controllers_by(server, select_controllers,
'Sanitize Erase Supported')
for controller in server.controllers:
drives = [x for x in controller.unassigned_physical_drives
if (x.get_physical_drive_dict().get('erase_status', '')
== 'OK')]
if drives:
drives = ','.join(x.id for x in drives)
controller.erase_devices(drives)
common.wait_for_operation_to_complete(
has_erase_completed,
delay_bw_retries=300,
failover_msg='Disk erase failed.'
)
server.refresh()
status = {}
for controller in server.controllers:
drive_status = {x.id: x.erase_status
for x in controller.unassigned_physical_drives}
status[controller.id] = drive_status
return status

View File

@@ -317,7 +317,12 @@ class Controller(object):
self.unassigned_physical_drives = [] self.unassigned_physical_drives = []
self.raid_arrays = [] self.raid_arrays = []
unassigned_drives = properties.get('unassigned', {}) # This step is needed because of the mismatch in the data returned by
# hpssacli and ssacli.
attr = ''.join(x for x in properties
if x == 'Unassigned' or x == 'unassigned')
unassigned_drives = properties.get(attr, {})
for key, value in unassigned_drives.items(): for key, value in unassigned_drives.items():
self.unassigned_physical_drives.append(PhysicalDrive(key, self.unassigned_physical_drives.append(PhysicalDrive(key,
value, value,
@@ -407,6 +412,15 @@ class Controller(object):
""" """
self.execute_cmd("logicaldrive", "all", "delete", "forced") self.execute_cmd("logicaldrive", "all", "delete", "forced")
def erase_devices(self, drive):
cmd_args = []
cmd_args.append("pd %s" % drive)
cmd_args.extend(['modify', 'erase',
'erasepattern=overwrite',
'unrestricted=off',
'forced'])
self.execute_cmd(*cmd_args)
class RaidArray(object): class RaidArray(object):
"""Class for a RAID Array. """Class for a RAID Array.
@@ -580,6 +594,7 @@ class PhysicalDrive:
self.disk_type = constants.get_disk_type(ssa_interface) self.disk_type = constants.get_disk_type(ssa_interface)
self.model = self.properties.get('Model') self.model = self.properties.get('Model')
self.firmware = self.properties.get('Firmware Revision') self.firmware = self.properties.get('Firmware Revision')
self.erase_status = self.properties.get('Status')
def get_physical_drive_dict(self): def get_physical_drive_dict(self):
"""Returns a dictionary of with the details of the physical drive.""" """Returns a dictionary of with the details of the physical drive."""
@@ -598,4 +613,5 @@ class PhysicalDrive:
'interface_type': self.interface_type, 'interface_type': self.interface_type,
'model': self.model, 'model': self.model,
'firmware': self.firmware, 'firmware': self.firmware,
'status': status} 'status': status,
'erase_status': self.erase_status}

View File

@@ -14,6 +14,7 @@
from ironic_python_agent import hardware from ironic_python_agent import hardware
from proliantutils import exception
from proliantutils.hpssa import manager as hpssa_manager from proliantutils.hpssa import manager as hpssa_manager
@@ -80,3 +81,21 @@ class ProliantHardwareManager(hardware.GenericHardwareManager):
for the node for the node
""" """
return hpssa_manager.delete_configuration() return hpssa_manager.delete_configuration()
def erase_devices(self, node, port):
"""Erase the drives on the bare metal.
This method erase all the drives which supports sanitize on
bare metal. If fails, it falls back to the generic erase method.
:returns: The dictionary of controllers with the drives and erase
status for each drive.
"""
try:
result = {}
result['Sanitize Erase'] = hpssa_manager.erase_devices()
except exception.HPSSAOperationError:
result.update(super(ProliantHardwareManager,
self).erase_devices(node, port))
return result

View File

@@ -2330,3 +2330,156 @@ Smart Array P822 in Slot 3
Mount Points: None Mount Points: None
Sanitize Erase Supported: False Sanitize Erase Supported: False
''' '''
SSA_ERASE_DRIVE = '''
Smart Array P440 in Slot 2
Bus Interface: PCI
Slot: 2
Serial Number: PDNMF0ARH8Y342
RAID 6 (ADG) Status: Enabled
Controller Status: OK
Firmware Version: 4.52-0
Spare Activation Mode: Activate on physical drive failure (default)
Encryption: Disabled
Driver Name: hpsa
Driver Version: 3.4.16
Controller Mode: RAID
Pending Controller Mode: RAID
Controller Mode Reboot: Not Required
Host Serial Number: SGH537Y7AY
Sanitize Erase Supported: True
Primary Boot Volume: None
Secondary Boot Volume: None
Port Name: 1I
Port ID: 0
Port Connection Number: 0
SAS Address: 5001438035544EC0
Port Location: Internal
Managed Cable Connected: False
Internal Drive Cage at Port 1I, Box 0, OK
Power Supply Status: Not Redundant
Drive Bays: 4
Port: 1I
Box: 0
Location: Internal
Physical Drives
None attached
Internal Drive Cage at Port 1I, Box 2, OK
Power Supply Status: Not Redundant
Drive Bays: 4
Port: 1I
Box: 2
Location: Internal
Physical Drives
physicaldrive 1I:2:1 (port 1I:box 2:bay 1, SAS HDD, 300 GB, OK)
unassigned
physicaldrive 1I:2:1
Port: 1I
Box: 2
Bay: 1
Status: OK
Drive Type: Unassigned Drive
Interface Type: SAS
Size: 300 GB
Drive exposed to OS: False
Logical/Physical Block Size: 512/512
Rotational Speed: 15100
Firmware Revision: HPD4
Serial Number: S7K0C3FJ0000K601EZLM
WWID: 5000C5008E183B1D
Model: HP EH0300JEDHC
Current Temperature (C): 42
Maximum Temperature (C): 52
PHY Count: 2
PHY Transfer Rate: 12.0Gbps, Unknown
Drive Authentication Status: OK
Carrier Application Version: 11
Carrier Bootloader Version: 6
Sanitize Erase Supported: True
Sanitize Estimated Max Erase Time: 0 hour(s)36 minute(s)
Unrestricted Sanitize Supported: False
Shingled Magnetic Recording Support: None
'''
SSA_ERASE_IN_PROGRESS = '''
Smart Array P440 in Slot 2
Controller Mode: RAID
Pending Controller Mode: RAID
Sanitize Erase Supported: True
Primary Boot Volume: None
Secondary Boot Volume: None
Physical Drives
physicaldrive 1I:2:1 (port 1I:box 2:bay 1, SAS HDD, 300 GB, OK)
unassigned
physicaldrive 1I:2:1
Drive Type: Unassigned Drive
Interface Type: SAS
Size: 300 GB
Status: Erase In Progress
Drive Type: Unassigned Drive
Sanitize Erase Supported: True
Sanitize Estimated Max Erase Time: 0 hour(s)36 minute(s)
Unrestricted Sanitize Supported: False
'''
SSA_ERASE_COMPLETE = '''
Smart Array P440 in Slot 2
Controller Mode: RAID
Pending Controller Mode: RAID
Sanitize Erase Supported: True
Primary Boot Volume: None
Secondary Boot Volume: None
Physical Drives
physicaldrive 1I:2:1 (port 1I:box 2:bay 1, SAS HDD, 300 GB, OK)
unassigned
physicaldrive 1I:2:1
Drive Type: Unassigned Drive
Interface Type: SAS
Size: 300 GB
Status: Erase Complete. Reenable Before Using.
Drive Type: Unassigned Drive
Sanitize Erase Supported: True
Sanitize Estimated Max Erase Time: 0 hour(s)36 minute(s)
Unrestricted Sanitize Supported: False
'''
SSA_ERASE_NOT_SUPPORTED = '''
Smart Array P440 in Slot 2
Controller Status: OK
Firmware Version: 4.52-0
Spare Activation Mode: Activate on physical drive failure (default)
Controller Mode: RAID
Pending Controller Mode: RAID
Controller Mode Reboot: Not Required
Sanitize Erase Supported: False
Primary Boot Volume: None
Secondary Boot Volume: None
'''

View File

@@ -191,7 +191,7 @@ class ManagerTestCases(testtools.TestCase):
raid_info = {'logical_disks': 'foo'} raid_info = {'logical_disks': 'foo'}
msg = ("An error was encountered while doing hpssa configuration: None" msg = ("An error was encountered while doing hpssa configuration: None"
" of the available HPSSA controllers Smart Array P822 in " " of the available SSA controllers Smart Array P822 in "
"Slot 3 have RAID enabled") "Slot 3 have RAID enabled")
ex = self.assertRaises(exception.HPSSAOperationError, ex = self.assertRaises(exception.HPSSAOperationError,
manager.create_configuration, manager.create_configuration,
@@ -373,7 +373,6 @@ class ManagerTestCases(testtools.TestCase):
def test_delete_configuration(self, controller_exec_cmd_mock, def test_delete_configuration(self, controller_exec_cmd_mock,
get_configuration_mock, get_configuration_mock,
get_all_details_mock): get_all_details_mock):
get_all_details_mock.return_value = raid_constants.HPSSA_ONE_DRIVE get_all_details_mock.return_value = raid_constants.HPSSA_ONE_DRIVE
get_configuration_mock.return_value = 'foo' get_configuration_mock.return_value = 'foo'
@@ -406,7 +405,7 @@ class ManagerTestCases(testtools.TestCase):
get_all_details_mock.return_value = drives get_all_details_mock.return_value = drives
msg = ("An error was encountered while doing hpssa configuration: None" msg = ("An error was encountered while doing hpssa configuration: None"
" of the available HPSSA controllers Smart Array P822 in " " of the available SSA controllers Smart Array P822 in "
"Slot 3 have RAID enabled") "Slot 3 have RAID enabled")
ex = self.assertRaises(exception.HPSSAOperationError, ex = self.assertRaises(exception.HPSSAOperationError,
manager.delete_configuration) manager.delete_configuration)
@@ -454,28 +453,82 @@ class ManagerTestCases(testtools.TestCase):
self.assertEqual(sorted(pds_active_expected), sorted(pds_active)) self.assertEqual(sorted(pds_active_expected), sorted(pds_active))
self.assertEqual(sorted(pds_ready_expected), sorted(pds_ready)) self.assertEqual(sorted(pds_ready_expected), sorted(pds_ready))
def test__filter_raid_mode_controllers_hba(self, get_all_details_mock): def test__select_controllers_by_hba(self, get_all_details_mock):
get_all_details_mock.return_value = raid_constants.HPSSA_HBA_MODE get_all_details_mock.return_value = raid_constants.HPSSA_HBA_MODE
server = objects.Server() server = objects.Server()
select_controllers = lambda x: not x.properties.get('HBA Mode Enabled',
False)
msg = ("An error was encountered while doing hpssa configuration: None" msg = ("An error was encountered while doing hpssa configuration: "
" of the available HPSSA controllers Smart Array P822 in " "None of the available SSA controllers Smart Array P822 in "
"Slot 3 have RAID enabled") "Slot 3 have Raid enabled.")
ex = self.assertRaises(exception.HPSSAOperationError, ex = self.assertRaises(exception.HPSSAOperationError,
manager._filter_raid_mode_controllers, manager._select_controllers_by,
server) server, select_controllers, 'Raid enabled')
self.assertIn(msg, str(ex)) self.assertIn(msg, str(ex))
def test__filter_raid_mode_controllers(self, get_all_details_mock): def test__select_controllers_by(self, get_all_details_mock):
get_all_details_mock.return_value = raid_constants.HPSSA_NO_DRIVES get_all_details_mock.return_value = raid_constants.HPSSA_NO_DRIVES
server = objects.Server() server = objects.Server()
select_controllers = lambda x: not x.properties.get('HBA Mode Enabled',
False)
ctrl_expected = server.controllers ctrl_expected = server.controllers
manager._filter_raid_mode_controllers(server) manager._select_controllers_by(server, select_controllers,
'Raid enabled')
self.assertEqual(ctrl_expected, server.controllers) self.assertEqual(ctrl_expected, server.controllers)
@mock.patch.object(objects.Controller, 'execute_cmd')
def test_erase_devices(self, controller_exec_cmd_mock,
get_all_details_mock):
erase_drive = raid_constants.SSA_ERASE_DRIVE
erase_complete = raid_constants.SSA_ERASE_COMPLETE
cmd_args = []
cmd_args.append("pd 1I:2:1")
cmd_args.extend(['modify', 'erase',
'erasepattern=overwrite',
'unrestricted=off',
'forced'])
expt_ret = {
'Smart Array P440 in Slot 2': {
'1I:2:1': 'Erase Complete. Reenable Before Using.'}}
get_all_details_mock.side_effect = [erase_drive, erase_complete,
erase_complete]
ret = manager.erase_devices()
self.assertTrue(controller_exec_cmd_mock.called)
controller_exec_cmd_mock.assert_any_call(*cmd_args)
self.assertEqual(expt_ret, ret)
@mock.patch.object(objects.Controller, 'execute_cmd')
def test_erase_devices_no_drives(self, controller_exec_cmd_mock,
get_all_details_mock):
erase_no_drives = raid_constants.SSA_ERASE_NOT_SUPPORTED
get_all_details_mock.side_effect = [erase_no_drives]
ex = self.assertRaises(exception.HPSSAOperationError,
manager.erase_devices)
expt_ret = ("None of the available SSA controllers Smart "
"Array P440 in Slot 2 have Sanitize Erase Supported.")
self.assertIn(expt_ret, str(ex))
@mock.patch.object(objects.Controller, 'execute_cmd')
def test_erase_devices_in_progress(self, controller_exec_cmd_mock,
get_all_details_mock):
erase_progress = raid_constants.SSA_ERASE_IN_PROGRESS
erase_complete = raid_constants.SSA_ERASE_COMPLETE
expt_ret = {
'Smart Array P440 in Slot 2': {
'1I:2:1': 'Erase Complete. Reenable Before Using.'}}
get_all_details_mock.side_effect = [erase_progress, erase_complete,
erase_complete]
ret = manager.erase_devices()
self.assertFalse(controller_exec_cmd_mock.called)
self.assertEqual(expt_ret, ret)
class RaidConfigValidationTestCases(testtools.TestCase): class RaidConfigValidationTestCases(testtools.TestCase):

View File

@@ -337,6 +337,18 @@ class ControllerTest(testtools.TestCase):
self.assertIsNone(controller.get_physical_drive_by_id('foo')) self.assertIsNone(controller.get_physical_drive_by_id('foo'))
@mock.patch.object(objects.Controller, 'execute_cmd')
def test_erase_devices(self, execute_mock,
get_all_details_mock):
get_all_details_mock.return_value = raid_constants.SSA_ERASE_DRIVE
server = objects.Server()
controller = server.controllers[0]
controller.erase_devices('1I:2:1')
execute_mock.assert_called_once_with('pd 1I:2:1', 'modify', 'erase',
'erasepattern=overwrite',
'unrestricted=off',
'forced')
@mock.patch.object(objects.Server, '_get_all_details') @mock.patch.object(objects.Server, '_get_all_details')
class LogicalDriveTest(testtools.TestCase): class LogicalDriveTest(testtools.TestCase):
@@ -516,6 +528,8 @@ class PhysicalDriveTest(testtools.TestCase):
self.assertEqual('ssd', ret_sata['disk_type']) self.assertEqual('ssd', ret_sata['disk_type'])
self.assertEqual('sata', ret_sata['interface_type']) self.assertEqual('sata', ret_sata['interface_type'])
self.assertEqual('OK', ret_sata['erase_status'])
def test_get_physical_drive_dict_part_of_array(self, get_all_details_mock): def test_get_physical_drive_dict_part_of_array(self, get_all_details_mock):
get_all_details_mock.return_value = raid_constants.HPSSA_ONE_DRIVE get_all_details_mock.return_value = raid_constants.HPSSA_ONE_DRIVE
@@ -532,6 +546,7 @@ class PhysicalDriveTest(testtools.TestCase):
self.assertEqual('HP EF0600FARNA', ret['model']) self.assertEqual('HP EF0600FARNA', ret['model'])
self.assertEqual('HPD6', ret['firmware']) self.assertEqual('HPD6', ret['firmware'])
self.assertEqual('active', ret['status']) self.assertEqual('active', ret['status'])
self.assertEqual('OK', ret['erase_status'])
def test_get_physical_drive_dict_unassigned(self, get_all_details_mock): def test_get_physical_drive_dict_unassigned(self, get_all_details_mock):
@@ -549,6 +564,7 @@ class PhysicalDriveTest(testtools.TestCase):
self.assertEqual('HP EF0600FARNA', ret['model']) self.assertEqual('HP EF0600FARNA', ret['model'])
self.assertEqual('HPD6', ret['firmware']) self.assertEqual('HPD6', ret['firmware'])
self.assertEqual('ready', ret['status']) self.assertEqual('ready', ret['status'])
self.assertEqual('OK', ret['erase_status'])
class PrivateMethodsTestCase(testtools.TestCase): class PrivateMethodsTestCase(testtools.TestCase):

View File

@@ -24,5 +24,6 @@ if not ironic_python_agent:
sys.modules['ironic_python_agent.errors'] = ipa_mock.errors sys.modules['ironic_python_agent.errors'] = ipa_mock.errors
sys.modules['ironic_python_agent.hardware'] = ipa_mock.hardware sys.modules['ironic_python_agent.hardware'] = ipa_mock.hardware
ipa_mock.hardware.GenericHardwareManager = mock.MagicMock ipa_mock.hardware.GenericHardwareManager = mock.MagicMock
mock.MagicMock.erase_devices = mock.MagicMock(name='erase_devices')
if 'proliantutils.ipa_hw_manager' in sys.modules: if 'proliantutils.ipa_hw_manager' in sys.modules:
reload(sys.modules['proliantutils.ipa_hw_manager']) reload(sys.modules['proliantutils.ipa_hw_manager'])

View File

@@ -13,11 +13,15 @@
# under the License. # under the License.
import mock import mock
from oslo_utils import importutils
import testtools import testtools
from proliantutils import exception
from proliantutils.hpssa import manager as hpssa_manager from proliantutils.hpssa import manager as hpssa_manager
from proliantutils.ipa_hw_manager import hardware_manager from proliantutils.ipa_hw_manager import hardware_manager
ironic_python_agent = importutils.try_import('ironic_python_agent')
class ProliantHardwareManagerTestCase(testtools.TestCase): class ProliantHardwareManagerTestCase(testtools.TestCase):
@@ -50,3 +54,31 @@ class ProliantHardwareManagerTestCase(testtools.TestCase):
ret = self.hardware_manager.delete_configuration("", "") ret = self.hardware_manager.delete_configuration("", "")
delete_mock.assert_called_once_with() delete_mock.assert_called_once_with()
self.assertEqual('current-config', ret) self.assertEqual('current-config', ret)
@mock.patch.object(hpssa_manager, 'erase_devices')
def test_erase_devices(self, erase_mock):
node = {}
port = {}
erase_mock.return_value = 'erase_status'
ret = self.hardware_manager.erase_devices(node, port)
erase_mock.assert_called_once_with()
self.assertEqual({'Sanitize Erase': 'erase_status'}, ret)
@mock.patch.object(ironic_python_agent.hardware.GenericHardwareManager,
'erase_devices')
@mock.patch.object(hpssa_manager, 'erase_devices')
def test_erase_devices_not_supported(self, erase_mock, generic_erase_mock):
node = {}
port = {}
value = ("Sanitize erase not supported in the "
"available controllers")
e = exception.HPSSAOperationError(value)
erase_mock.side_effect = e
generic_erase_mock.return_value = {'foo': 'bar'}
expt_return = {
'Sanitize Erase': erase_mock.side_effect}
expt_return.update(generic_erase_mock.return_value)
self.hardware_manager.erase_devices(node, port)
generic_erase_mock.assert_called_once_with(node, port)