diff --git a/proliantutils/hpssa/manager.py b/proliantutils/hpssa/manager.py index a575ad3..17fb242 100644 --- a/proliantutils/hpssa/manager.py +++ b/proliantutils/hpssa/manager.py @@ -22,6 +22,7 @@ from proliantutils import exception from proliantutils.hpssa import constants from proliantutils.hpssa import disk_allocator from proliantutils.hpssa import objects +from proliantutils.ilo import common CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) RAID_CONFIG_SCHEMA = os.path.join(CURRENT_DIR, "raid_config_schema.json") @@ -74,27 +75,32 @@ def validate(raid_config): raise exception.InvalidInputError(msg) -def _filter_raid_mode_controllers(server): - """Filters out the hpssa controllers in raid mode. +def _select_controllers_by(server, select_condition, msg): + """Filters out the hpssa controllers based on the condition. - This method updates the server with only the controller which is in raid - mode. The controller which are in HBA mode are removed from the list. + This method updates the server with only the controller which satisfies + 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 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 mode. """ all_controllers = server.controllers - non_hba_controllers = [c for c in all_controllers - if not c.properties.get('HBA Mode Enabled', False)] + supported_controllers = [c for c in all_controllers if select_condition(c)] - if not non_hba_controllers: - reason = ("None of the available HPSSA controllers %s have RAID " - "enabled" % ', '.join([c.id for c in all_controllers])) + if not supported_controllers: + reason = ("None of the available SSA controllers %(controllers)s " + "have %(msg)s" + % {'controllers': ', '.join([c.id for c in all_controllers]), + 'msg': msg}) raise exception.HPSSAOperationError(reason=reason) - server.controllers = non_hba_controllers + server.controllers = supported_controllers def create_configuration(raid_config): @@ -117,7 +123,9 @@ def create_configuration(raid_config): """ 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) @@ -297,7 +305,9 @@ def delete_configuration(): """ 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: # Trigger delete only if there is some RAID array, otherwise @@ -337,3 +347,56 @@ def get_configuration(): _update_physical_disk_details(raid_config, server) 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 diff --git a/proliantutils/hpssa/objects.py b/proliantutils/hpssa/objects.py index 0df299d..c51215a 100644 --- a/proliantutils/hpssa/objects.py +++ b/proliantutils/hpssa/objects.py @@ -317,7 +317,12 @@ class Controller(object): self.unassigned_physical_drives = [] 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(): self.unassigned_physical_drives.append(PhysicalDrive(key, value, @@ -407,6 +412,15 @@ class Controller(object): """ 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 for a RAID Array. @@ -580,6 +594,7 @@ class PhysicalDrive: self.disk_type = constants.get_disk_type(ssa_interface) self.model = self.properties.get('Model') self.firmware = self.properties.get('Firmware Revision') + self.erase_status = self.properties.get('Status') def get_physical_drive_dict(self): """Returns a dictionary of with the details of the physical drive.""" @@ -598,4 +613,5 @@ class PhysicalDrive: 'interface_type': self.interface_type, 'model': self.model, 'firmware': self.firmware, - 'status': status} + 'status': status, + 'erase_status': self.erase_status} diff --git a/proliantutils/ipa_hw_manager/hardware_manager.py b/proliantutils/ipa_hw_manager/hardware_manager.py index f033859..71dd598 100644 --- a/proliantutils/ipa_hw_manager/hardware_manager.py +++ b/proliantutils/ipa_hw_manager/hardware_manager.py @@ -14,6 +14,7 @@ from ironic_python_agent import hardware +from proliantutils import exception from proliantutils.hpssa import manager as hpssa_manager @@ -80,3 +81,21 @@ class ProliantHardwareManager(hardware.GenericHardwareManager): for the node """ 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 diff --git a/proliantutils/tests/hpssa/raid_constants.py b/proliantutils/tests/hpssa/raid_constants.py index 7b079aa..ebd22fa 100644 --- a/proliantutils/tests/hpssa/raid_constants.py +++ b/proliantutils/tests/hpssa/raid_constants.py @@ -2330,3 +2330,156 @@ Smart Array P822 in Slot 3 Mount Points: None 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 + +''' diff --git a/proliantutils/tests/hpssa/test_manager.py b/proliantutils/tests/hpssa/test_manager.py index fca94a5..7cd8075 100644 --- a/proliantutils/tests/hpssa/test_manager.py +++ b/proliantutils/tests/hpssa/test_manager.py @@ -191,7 +191,7 @@ class ManagerTestCases(testtools.TestCase): raid_info = {'logical_disks': 'foo'} 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") ex = self.assertRaises(exception.HPSSAOperationError, manager.create_configuration, @@ -373,7 +373,6 @@ class ManagerTestCases(testtools.TestCase): def test_delete_configuration(self, controller_exec_cmd_mock, get_configuration_mock, get_all_details_mock): - get_all_details_mock.return_value = raid_constants.HPSSA_ONE_DRIVE get_configuration_mock.return_value = 'foo' @@ -406,7 +405,7 @@ class ManagerTestCases(testtools.TestCase): get_all_details_mock.return_value = drives 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") ex = self.assertRaises(exception.HPSSAOperationError, manager.delete_configuration) @@ -454,28 +453,82 @@ class ManagerTestCases(testtools.TestCase): self.assertEqual(sorted(pds_active_expected), sorted(pds_active)) 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 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" - " of the available HPSSA controllers Smart Array P822 in " - "Slot 3 have RAID enabled") + msg = ("An error was encountered while doing hpssa configuration: " + "None of the available SSA controllers Smart Array P822 in " + "Slot 3 have Raid enabled.") ex = self.assertRaises(exception.HPSSAOperationError, - manager._filter_raid_mode_controllers, - server) + manager._select_controllers_by, + server, select_controllers, 'Raid enabled') 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 server = objects.Server() + select_controllers = lambda x: not x.properties.get('HBA Mode Enabled', + False) 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) + @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): diff --git a/proliantutils/tests/hpssa/test_objects.py b/proliantutils/tests/hpssa/test_objects.py index fbfffc8..e98ec9c 100644 --- a/proliantutils/tests/hpssa/test_objects.py +++ b/proliantutils/tests/hpssa/test_objects.py @@ -337,6 +337,18 @@ class ControllerTest(testtools.TestCase): 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') class LogicalDriveTest(testtools.TestCase): @@ -516,6 +528,8 @@ class PhysicalDriveTest(testtools.TestCase): self.assertEqual('ssd', ret_sata['disk_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): 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('HPD6', ret['firmware']) self.assertEqual('active', ret['status']) + self.assertEqual('OK', ret['erase_status']) 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('HPD6', ret['firmware']) self.assertEqual('ready', ret['status']) + self.assertEqual('OK', ret['erase_status']) class PrivateMethodsTestCase(testtools.TestCase): diff --git a/proliantutils/tests/ipa_hw_manager/__init__.py b/proliantutils/tests/ipa_hw_manager/__init__.py index b1d7e23..e4cfb09 100644 --- a/proliantutils/tests/ipa_hw_manager/__init__.py +++ b/proliantutils/tests/ipa_hw_manager/__init__.py @@ -24,5 +24,6 @@ if not ironic_python_agent: sys.modules['ironic_python_agent.errors'] = ipa_mock.errors sys.modules['ironic_python_agent.hardware'] = ipa_mock.hardware ipa_mock.hardware.GenericHardwareManager = mock.MagicMock + mock.MagicMock.erase_devices = mock.MagicMock(name='erase_devices') if 'proliantutils.ipa_hw_manager' in sys.modules: reload(sys.modules['proliantutils.ipa_hw_manager']) diff --git a/proliantutils/tests/ipa_hw_manager/test_hardware_manager.py b/proliantutils/tests/ipa_hw_manager/test_hardware_manager.py index 7cffa52..65ed705 100644 --- a/proliantutils/tests/ipa_hw_manager/test_hardware_manager.py +++ b/proliantutils/tests/ipa_hw_manager/test_hardware_manager.py @@ -13,11 +13,15 @@ # under the License. import mock +from oslo_utils import importutils import testtools +from proliantutils import exception from proliantutils.hpssa import manager as hpssa_manager from proliantutils.ipa_hw_manager import hardware_manager +ironic_python_agent = importutils.try_import('ironic_python_agent') + class ProliantHardwareManagerTestCase(testtools.TestCase): @@ -50,3 +54,31 @@ class ProliantHardwareManagerTestCase(testtools.TestCase): ret = self.hardware_manager.delete_configuration("", "") delete_mock.assert_called_once_with() 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)