From 8f48ebaf761dc6c5df59d6a48e78f706d408a513 Mon Sep 17 00:00:00 2001 From: Aparna Date: Wed, 26 Jul 2017 11:22:04 +0000 Subject: [PATCH] Redfish: Add 'iscsi_boot' capability This commit adds support to add the new server capability 'iscsi_boot' Change-Id: Ia6cc56586143d651ad672206b3d97f2292ed99aa --- proliantutils/redfish/redfish.py | 6 +- .../redfish/resources/system/bios.py | 20 +++ .../redfish/resources/system/iscsi.py | 36 +++++ proliantutils/redfish/utils.py | 27 ++++ .../tests/redfish/json_samples/iscsi.json | 152 ++++++++++++++++++ .../redfish/resources/system/test_bios.py | 42 +++++ .../redfish/resources/system/test_iscsi.py | 49 ++++++ proliantutils/tests/redfish/test_redfish.py | 11 +- proliantutils/tests/redfish/test_utils.py | 22 +++ 9 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 proliantutils/redfish/resources/system/iscsi.py create mode 100644 proliantutils/tests/redfish/json_samples/iscsi.json create mode 100644 proliantutils/tests/redfish/resources/system/test_iscsi.py diff --git a/proliantutils/redfish/redfish.py b/proliantutils/redfish/redfish.py index 55d650a1..a7653058 100644 --- a/proliantutils/redfish/redfish.py +++ b/proliantutils/redfish/redfish.py @@ -667,7 +667,11 @@ class RedfishOperations(operations.IloOperations): or tpm_state == sys_cons.TPM_PRESENT_DISABLED)), ('secure_boot', GET_SECUREBOOT_CURRENT_BOOT_MAP.get( - sushy_system.secure_boot.current_boot)),) if value}) + sushy_system.secure_boot.current_boot)), + ('iscsi_boot', + (sushy_system.bios_settings.iscsi_settings. + is_iscsi_boot_supported())), + ) if value}) except sushy.exceptions.SushyError as e: msg = (self._("The Redfish controller is unable to get " diff --git a/proliantutils/redfish/resources/system/bios.py b/proliantutils/redfish/resources/system/bios.py index 2233bb2f..96bfdb23 100644 --- a/proliantutils/redfish/resources/system/bios.py +++ b/proliantutils/redfish/resources/system/bios.py @@ -18,6 +18,7 @@ from sushy.resources import base from proliantutils import exception from proliantutils import log from proliantutils.redfish.resources.system import constants as sys_cons +from proliantutils.redfish.resources.system import iscsi from proliantutils.redfish.resources.system import mappings from proliantutils.redfish import utils @@ -44,6 +45,8 @@ class BIOSSettings(base.ResourceBase): cpu_vt = base.MappedField(["Attributes", "ProcVirtualization"], mappings.CPUVT_MAP) + _iscsi_settings = None + _pending_settings = None _boot_settings = None _base_configs = None @@ -80,6 +83,22 @@ class BIOSSettings(base.ResourceBase): return self._boot_settings + @property + def iscsi_settings(self): + """Property to provide reference to bios iscsi instance + + It is calculated once when the first time it is queried. On refresh, + this property gets reset. + """ + if self._iscsi_settings is None: + self._iscsi_settings = iscsi.ISCSISettings( + self._conn, + utils.get_subresource_path_by( + self, ["Oem", "Hpe", "Links", "iScsi"]), + redfish_version=self.redfish_version) + + return self._iscsi_settings + def _get_base_configs(self): """Method that returns object of bios base configs.""" if self._base_configs is None: @@ -100,6 +119,7 @@ class BIOSSettings(base.ResourceBase): self._pending_settings = None self._boot_settings = None self._base_configs = None + self._iscsi_settings = None class BIOSBaseConfigs(base.ResourceBase): diff --git a/proliantutils/redfish/resources/system/iscsi.py b/proliantutils/redfish/resources/system/iscsi.py new file mode 100644 index 00000000..140cf7f7 --- /dev/null +++ b/proliantutils/redfish/resources/system/iscsi.py @@ -0,0 +1,36 @@ +# Copyright 2017 Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sushy.resources import base + +from proliantutils.redfish import utils + + +class ISCSISettings(base.ResourceBase): + """Class that represents the iSCSI settings resource. + + This class extends the functionality of base resource class + from sushy. + """ + + def is_iscsi_boot_supported(self): + """Checks whether iscsi boot is supported or not. + + To find the iscsi boot support, check whether the PATCH + operation is allowed on the iscsi 'settings' uri. + :returns: True if it is supported else False + """ + return utils.is_operation_allowed( + 'PATCH', self, + ['@Redfish.Settings', 'SettingsObject']) diff --git a/proliantutils/redfish/utils.py b/proliantutils/redfish/utils.py index 9474c68d..9f2fbdb5 100644 --- a/proliantutils/redfish/utils.py +++ b/proliantutils/redfish/utils.py @@ -81,3 +81,30 @@ def get_supported_boot_mode(supported_boot_mode): return SupportedBootModes(boot_mode_bios=boot_mode_bios, boot_mode_uefi=boot_mode_uefi) + + +def get_allowed_operations(resource, subresouce_path): + """Helper function to get the HTTP allowed methods. + + :param resource: ResourceBase instance from which the path is loaded. + :param subresource_path: JSON field to fetch the value from. + Either a string, or a list of strings in case of a nested field. + :returns: A list of allowed HTTP methods. + """ + uri = get_subresource_path_by(resource, subresouce_path) + response = resource._conn.get(path=uri) + return response.headers['Allow'] + + +def is_operation_allowed(method, resource, subresouce_path): + """Checks whether the operation is allowed for the resource. + + This method checks whether a HTTP method is allowed to be + performed on the given sub resource path. + :param method: A HTTP method. example: GET, PATCH, POST + :param resource: ResourceBase instance from which the path is loaded. + :param subresource_path: JSON field to fetch the value from. + Either a string, or a list of strings in case of a nested field. + :returns: True if the operation is allowed else False + """ + return method in get_allowed_operations(resource, subresouce_path) diff --git a/proliantutils/tests/redfish/json_samples/iscsi.json b/proliantutils/tests/redfish/json_samples/iscsi.json new file mode 100644 index 00000000..5596b9a7 --- /dev/null +++ b/proliantutils/tests/redfish/json_samples/iscsi.json @@ -0,0 +1,152 @@ + + + { + "@Redfish.Settings": + { + "@odata.type": "#Settings.v1_0_0.Settings", + "ETag": "", + "Messages": + [ + { + "MessageId": "Base.1.0.Success" + } + ], + "SettingsObject": + { + "@odata.id": "/redfish/v1/systems/1/bios/iscsi/settings/" + }, + "Time": "" + }, + "@odata.context": "/redfish/v1/$metadata#HpeiSCSISoftwareInitiator.HpeiSCSISoftwareInitiator", + "@odata.etag": "W/\"A772A3CF45F53A3A3A285057B9B505F1\"", + "@odata.id": "/redfish/v1/systems/1/bios/iscsi/", + "@odata.type": "#HpeiSCSISoftwareInitiator.v2_0_0.HpeiSCSISoftwareInitiator", + "Id": "iscsi", + "Name": "iSCSI Software Initiator Current Settings", + "Oem": + { + "Hpe": + { + "@odata.type": "#HpeBiosExt.v2_0_0.HpeBiosExt", + "Links": + { + "BaseConfigs": + { + "@odata.id": "/redfish/v1/systems/1/bios/iscsi/baseconfigs/" + } + }, + "SettingsObject": + { + "UnmodifiedETag": "W/\"F92FCCF7BBE885858530538BD89804A4\"" + } + } + }, + "iSCSIInitiatorName": "iqn.2015-02.com.hpe:uefi-U31", + "iSCSISources": + [ + { + "StructuredBootString": null, + "UEFIDevicePath": null, + "iSCSIAttemptInstance": 0, + "iSCSIAttemptName": "", + "iSCSIAuthenticationMethod": "None", + "iSCSIChapSecret": null, + "iSCSIChapType": "OneWay", + "iSCSIChapUsername": null, + "iSCSIConnectRetry": 3, + "iSCSIConnectTimeoutMS": 20000, + "iSCSIConnection": "Disabled", + "iSCSIInitiatorGateway": "0.0.0.0", + "iSCSIInitiatorInfoViaDHCP": true, + "iSCSIInitiatorIpAddress": "0.0.0.0", + "iSCSIInitiatorNetmask": "0.0.0.0", + "iSCSIIpAddressType": "IPv4", + "iSCSILUN": "0", + "iSCSINicSource": null, + "iSCSIReverseChapSecret": null, + "iSCSIReverseChapUsername": null, + "iSCSITargetInfoViaDHCP": true, + "iSCSITargetIpAddress": "0.0.0.0", + "iSCSITargetName": "", + "iSCSITargetTcpPort": 3260 + }, + { + "StructuredBootString": null, + "UEFIDevicePath": null, + "iSCSIAttemptInstance": 0, + "iSCSIAttemptName": "", + "iSCSIAuthenticationMethod": "None", + "iSCSIChapSecret": null, + "iSCSIChapType": "OneWay", + "iSCSIChapUsername": null, + "iSCSIConnectRetry": 3, + "iSCSIConnectTimeoutMS": 20000, + "iSCSIConnection": "Disabled", + "iSCSIInitiatorGateway": "0.0.0.0", + "iSCSIInitiatorInfoViaDHCP": true, + "iSCSIInitiatorIpAddress": "0.0.0.0", + "iSCSIInitiatorNetmask": "0.0.0.0", + "iSCSIIpAddressType": "IPv4", + "iSCSILUN": "0", + "iSCSINicSource": null, + "iSCSIReverseChapSecret": null, + "iSCSIReverseChapUsername": null, + "iSCSITargetInfoViaDHCP": true, + "iSCSITargetIpAddress": "0.0.0.0", + "iSCSITargetName": "", + "iSCSITargetTcpPort": 3260 + }, + { + "StructuredBootString": null, + "UEFIDevicePath": null, + "iSCSIAttemptInstance": 0, + "iSCSIAttemptName": "", + "iSCSIAuthenticationMethod": "None", + "iSCSIChapSecret": null, + "iSCSIChapType": "OneWay", + "iSCSIChapUsername": null, + "iSCSIConnectRetry": 3, + "iSCSIConnectTimeoutMS": 20000, + "iSCSIConnection": "Disabled", + "iSCSIInitiatorGateway": "0.0.0.0", + "iSCSIInitiatorInfoViaDHCP": true, + "iSCSIInitiatorIpAddress": "0.0.0.0", + "iSCSIInitiatorNetmask": "0.0.0.0", + "iSCSIIpAddressType": "IPv4", + "iSCSILUN": "0", + "iSCSINicSource": null, + "iSCSIReverseChapSecret": null, + "iSCSIReverseChapUsername": null, + "iSCSITargetInfoViaDHCP": true, + "iSCSITargetIpAddress": "0.0.0.0", + "iSCSITargetName": "", + "iSCSITargetTcpPort": 3260 + }, + { + "StructuredBootString": null, + "UEFIDevicePath": null, + "iSCSIAttemptInstance": 0, + "iSCSIAttemptName": "", + "iSCSIAuthenticationMethod": "None", + "iSCSIChapSecret": null, + "iSCSIChapType": "OneWay", + "iSCSIChapUsername": null, + "iSCSIConnectRetry": 3, + "iSCSIConnectTimeoutMS": 20000, + "iSCSIConnection": "Disabled", + "iSCSIInitiatorGateway": "0.0.0.0", + "iSCSIInitiatorInfoViaDHCP": true, + "iSCSIInitiatorIpAddress": "0.0.0.0", + "iSCSIInitiatorNetmask": "0.0.0.0", + "iSCSIIpAddressType": "IPv4", + "iSCSILUN": "0", + "iSCSINicSource": null, + "iSCSIReverseChapSecret": null, + "iSCSIReverseChapUsername": null, + "iSCSITargetInfoViaDHCP": true, + "iSCSITargetIpAddress": "0.0.0.0", + "iSCSITargetName": "", + "iSCSITargetTcpPort": 3260 + } + ] + } diff --git a/proliantutils/tests/redfish/resources/system/test_bios.py b/proliantutils/tests/redfish/resources/system/test_bios.py index 9dfb360e..e8f31091 100644 --- a/proliantutils/tests/redfish/resources/system/test_bios.py +++ b/proliantutils/tests/redfish/resources/system/test_bios.py @@ -22,6 +22,7 @@ import testtools from proliantutils import exception from proliantutils.redfish.resources.system import bios from proliantutils.redfish.resources.system import constants as sys_cons +from proliantutils.redfish.resources.system import iscsi class BIOSSettingsTestCase(testtools.TestCase): @@ -82,6 +83,24 @@ class BIOSSettingsTestCase(testtools.TestCase): self.bios_inst.boot_settings) self.conn.get.return_value.json.assert_not_called() + def test_iscsi_settings(self): + self.assertIsNone(self.bios_inst._iscsi_settings) + + self.conn.get.return_value.json.reset_mock() + with open('proliantutils/tests/redfish/' + 'json_samples/bios_boot.json', 'r') as f: + self.conn.get.return_value.json.return_value = ( + json.loads(f.read())['Default']) + actual_settings = self.bios_inst.iscsi_settings + self.assertIsInstance(actual_settings, + iscsi.ISCSISettings) + self.conn.get.return_value.json.assert_called_once_with() + # reset mock + self.conn.get.return_value.json.reset_mock() + self.assertIs(actual_settings, + self.bios_inst.iscsi_settings) + self.conn.get.return_value.json.assert_not_called() + def test__get_base_configs(self): self.assertIsNone(self.bios_inst._base_configs) with open('proliantutils/tests/redfish/' @@ -140,6 +159,29 @@ class BIOSSettingsTestCase(testtools.TestCase): self.assertIsInstance(actual_settings, bios.BIOSBootSettings) + def test_iscsi_settings_on_refresh(self): + with open('proliantutils/tests/redfish/' + 'json_samples/bios_boot.json', 'r') as f: + self.conn.get.return_value.json.return_value = ( + json.loads(f.read())['Default']) + actual_settings = self.bios_inst.iscsi_settings + self.assertIsInstance(actual_settings, + iscsi.ISCSISettings) + + with open('proliantutils/tests/redfish/' + 'json_samples/bios.json', 'r') as f: + self.conn.get.return_value.json.return_value = ( + json.loads(f.read())['Default']) + self.bios_inst.refresh() + self.assertIsNone(self.bios_inst._iscsi_settings) + + with open('proliantutils/tests/redfish/' + 'json_samples/bios_boot.json', 'r') as f: + self.conn.get.return_value.json.return_value = ( + json.loads(f.read())['Default']) + self.assertIsInstance(actual_settings, + iscsi.ISCSISettings) + def test__get_base_configs_on_refresh(self): with open('proliantutils/tests/redfish/' 'json_samples/bios_base_configs.json', 'r') as f: diff --git a/proliantutils/tests/redfish/resources/system/test_iscsi.py b/proliantutils/tests/redfish/resources/system/test_iscsi.py new file mode 100644 index 00000000..9e9afabb --- /dev/null +++ b/proliantutils/tests/redfish/resources/system/test_iscsi.py @@ -0,0 +1,49 @@ +# Copyright 2017 Hewlett Packard Enterprise Development LP +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +import ddt +import mock +import testtools + +from proliantutils.redfish.resources.system import iscsi +from proliantutils.redfish import utils + + +@ddt.ddt +class ISCSISettingsTestCase(testtools.TestCase): + + def setUp(self): + super(ISCSISettingsTestCase, self).setUp() + self.conn = mock.MagicMock() + with open('proliantutils/tests/redfish/' + 'json_samples/iscsi.json', 'r') as f: + self.conn.get.return_value.json.return_value = ( + json.loads(f.read())) + + self.iscsi_inst = iscsi.ISCSISettings( + self.conn, '/redfish/v1/Systems/1/bios/iscsi', + redfish_version='1.0.2') + + @ddt.data((['GET', 'PATCH', 'POST', 'HEAD'], True), + (['GET', 'HEAD'], False)) + @ddt.unpack + @mock.patch.object(utils, 'get_allowed_operations') + def test_is_iscsi_boot_supported(self, allowed_method, + expected, get_method_mock): + get_method_mock.return_value = allowed_method + ret_val = self.iscsi_inst.is_iscsi_boot_supported() + self.assertEqual(ret_val, expected) diff --git a/proliantutils/tests/redfish/test_redfish.py b/proliantutils/tests/redfish/test_redfish.py index 6babe08d..37d071bf 100644 --- a/proliantutils/tests/redfish/test_redfish.py +++ b/proliantutils/tests/redfish/test_redfish.py @@ -30,6 +30,7 @@ from proliantutils.redfish.resources.manager import manager from proliantutils.redfish.resources.manager import virtual_media from proliantutils.redfish.resources.system import bios from proliantutils.redfish.resources.system import constants as sys_cons +from proliantutils.redfish.resources.system import iscsi from proliantutils.redfish.resources.system import pci_device from proliantutils.redfish.resources.system import system as pro_sys from sushy.resources.system import system @@ -690,6 +691,10 @@ class RedfishOperationsTestCase(testtools.TestCase): tpm_mock) type(get_system_mock.return_value).supported_boot_mode = ( sys_cons.SUPPORTED_LEGACY_BIOS_AND_UEFI) + iscsi_mock = mock.MagicMock(spec=iscsi.ISCSISettings) + iscsi_mock.is_iscsi_boot_supported = mock.MagicMock(return_value=True) + type(get_system_mock.return_value.bios_settings).iscsi_settings = ( + iscsi_mock) actual = self.rf_client.get_server_capabilities() expected = {'pci_gpu_devices': 1, 'sriov_enabled': 'true', 'secure_boot': 'true', 'cpu_vt': 'true', @@ -699,7 +704,7 @@ class RedfishOperationsTestCase(testtools.TestCase): 'trusted_boot': 'true', 'server_model': 'ProLiant DL180 Gen10', 'boot_mode_bios': 'true', - 'boot_mode_uefi': 'true'} + 'boot_mode_uefi': 'true', 'iscsi_boot': 'true'} self.assertEqual(expected, actual) @mock.patch.object(redfish.RedfishOperations, '_get_sushy_system') @@ -733,6 +738,10 @@ class RedfishOperationsTestCase(testtools.TestCase): tpm_mock) type(get_system_mock.return_value).supported_boot_mode = ( sys_cons.SUPPORTED_UEFI_ONLY) + iscsi_mock = mock.MagicMock(spec=iscsi.ISCSISettings) + iscsi_mock.is_iscsi_boot_supported = mock.MagicMock(return_value=False) + type(get_system_mock.return_value.bios_settings).iscsi_settings = ( + iscsi_mock) actual = self.rf_client.get_server_capabilities() expected = {'pci_gpu_devices': 1, 'rom_firmware_version': 'U31 v1.00 (03/11/2017)', diff --git a/proliantutils/tests/redfish/test_utils.py b/proliantutils/tests/redfish/test_utils.py index d9fb76d9..4da77928 100644 --- a/proliantutils/tests/redfish/test_utils.py +++ b/proliantutils/tests/redfish/test_utils.py @@ -82,3 +82,25 @@ class UtilsTestCase(testtools.TestCase): expected_boot_modes): actual_boot_modes = utils.get_supported_boot_mode(boot_mode) self.assertEqual(expected_boot_modes, actual_boot_modes) + + @ddt.data(('SecureBoot', ['HEAD', 'GET', 'PATCH', 'POST']), + ('Bios', ['GET', 'HEAD'])) + @ddt.unpack + def test_get_allowed_operations(self, subresource_path, expected): + response_mock = mock.MagicMock() + response_mock.headers = {'Allow': expected} + self.conn.get.return_value = response_mock + ret_val = utils.get_allowed_operations(self.sys_inst, subresource_path) + self.assertEqual(ret_val, expected) + + @ddt.data(('PATCH', 'SecureBoot', ['HEAD', 'GET', 'PATCH', 'POST'], True), + ('POST', 'Bios', ['GET', 'HEAD'], False)) + @ddt.unpack + @mock.patch.object(utils, 'get_allowed_operations') + def test_is_operation_allowed(self, method, subresource_path, + allowed_operations, expected, + get_method_mock): + get_method_mock.return_value = allowed_operations + ret_val = utils.is_operation_allowed(method, self.sys_inst, + subresource_path) + self.assertEqual(ret_val, expected)