diff --git a/proliantutils/exception.py b/proliantutils/exception.py index dd8b36e..b272749 100644 --- a/proliantutils/exception.py +++ b/proliantutils/exception.py @@ -200,3 +200,20 @@ class HpsumOperationError(ProliantUtilsException): message = self.message % kwargs super(HpsumOperationError, self).__init__(message) + + +class RedfishError(ProliantUtilsException): + """Basic exception for errors raised by Redfish operations.""" + + message = None + + def __init__(self, **kwargs): + if self.message and kwargs: + self.message = self.message % kwargs + + super(RedfishError, self).__init__(self.message) + + +class MissingAttributeError(RedfishError): + message = ('The attribute %(attribute)s is missing from the ' + 'resource %(resource)s') diff --git a/proliantutils/redfish/__init__.py b/proliantutils/redfish/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/proliantutils/redfish/redfish.py b/proliantutils/redfish/redfish.py new file mode 100644 index 0000000..17077a8 --- /dev/null +++ b/proliantutils/redfish/redfish.py @@ -0,0 +1,124 @@ +# 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. + +__author__ = 'HPE' + +from six.moves.urllib import parse + +from proliantutils import exception +from proliantutils.ilo import operations +from proliantutils import log +from proliantutils import rest + + +""" +Class specific for Redfish APIs. +""" + +LOG = log.get_logger(__name__) + + +class RedfishOperations(operations.IloOperations): + """Operations supported on redfish based hardware. + + This is the list of APIs which are currently supported via Redfish mode + of operation. This is a growing list which needs to be updated as and when + the existing API/s (of its cousin RIS and RIBCL interfaces) are migrated. + + --- START --- + + * get_product_name(self) + * get_host_power_status(self) + + --- END --- + + """ + + def __init__(self, redfish_controller_ip, username, password, + bios_password=None, cacert=None, root_prefix='/redfish/v1/'): + """A class representing supported RedfishOperations + + :param redfish_controller_ip: The ip address of the Redfish controller. + :param username: User account with admin/server-profile access + privilege + :param password: User account password + :param bios_password: bios password + :param cacert: a path to a CA_BUNDLE file or directory with + certificates of trusted CAs. If set to None, the driver will + ignore verifying the SSL certificate; if it's a path the driver + will use the specified certificate or one of the certificates in + the directory. Defaults to None. + :param root_prefix: The default URL prefix. This part includes + the root service and version. Defaults to /redfish/v1 + """ + super(RedfishOperations, self).__init__() + self._conn = rest.RestConnectorBase(redfish_controller_ip, username, + password, bios_password, cacert) + self._root_prefix = root_prefix + # Fetch the ServiceRoot response + self._fetch_root_resources() + + def _fetch_root_resources(self): + """Fetches the service root resources + + Retrieves/fetches the resources at ServiceRoot + :raises: IloConnectionError + """ + status, headers, service_root_resp = ( + self._conn._rest_get(self._root_prefix)) + self._root_resp = service_root_resp + + def _get_system_collection_path(self): + """Helper function to find the SystemCollection path""" + systems_col = self._root_resp.get('Systems') + if not systems_col: + raise exception.MissingAttributeError(attribute='Systems', + resource=self._root_prefix) + return systems_col.get('@odata.id') + + def _get_system_details(self, system_id): + """Get the system details. + + :param system_id: The identity of the System resource + :raises: IloError + :raises: MissingAttributeError + """ + system_url = parse.urljoin(self._get_system_collection_path(), + system_id) + status, headers, system = self._conn._rest_get(system_url) + return system + + def get_product_name(self): + """Gets the product name of the server. + + :returns: server model name. + :raises: IloError, on an error from iLO. + :raises: MissingAttributeError + """ + # Assuming only one system present as part of collection, + # as we are dealing with iLO's here. + system = self._get_system_details('1') + return system['Model'] + + def get_host_power_status(self): + """Request the power state of the server. + + :returns: Power State of the server, 'ON' or 'OFF' + :raises: IloError, on an error from iLO. + :raises: MissingAttributeError + """ + # Assuming only one system present as part of collection, + # as we are dealing with iLO's here. + system = self._get_system_details('1') + return system['PowerState'].upper() diff --git a/proliantutils/tests/redfish/__init__.py b/proliantutils/tests/redfish/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/proliantutils/tests/redfish/json_samples/root.json b/proliantutils/tests/redfish/json_samples/root.json new file mode 100644 index 0000000..eeb1906 --- /dev/null +++ b/proliantutils/tests/redfish/json_samples/root.json @@ -0,0 +1,90 @@ +{ + "@odata.context": "/redfish/v1/$metadata#ServiceRoot", + "@odata.etag": "W/\"06F9101D\"", + "@odata.id": "/redfish/v1/", + "@odata.type": "#ServiceRoot.v1_1_0.ServiceRoot", + "AccountService": { + "@odata.id": "/redfish/v1/AccountService/" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis/" + }, + "EventService": { + "@odata.id": "/redfish/v1/EventService/" + }, + "Id": "v1", + "JsonSchemas": { + "@odata.id": "/redfish/v1/Schemas/" + }, + "Links": { + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions/" + } + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers/" + }, + "Name": "HPE RESTful Root Service", + "Oem": { + "Hpe": { + "@odata.type": "#HpeiLOServiceExt.v2_0_0.HpeiLOServiceExt", + "Links": { + "ResourceDirectory": { + "@odata.id": "/redfish/v1/ResourceDirectory/" + }, + "ViewClassService": { + "@odata.id": "/redfish/v1/Views/" + } + }, + "Manager": [ + { + "DefaultLanguage": "en", + "FQDN": "ILO.asiapacific.hpqcorp.net", + "HostName": "ILO", + "Languages": [ + { + "Language": "en", + "TranslationName": "English", + "Version": "1.10" + } + ], + "ManagerFirmwareVersion": "1.10", + "ManagerType": "iLO 5" + } + ], + "Sessions": { + "CertCommonName": "ILO.asiapacific.hpqcorp.net", + "CertificateLoginEnabled": false, + "KerberosEnabled": false, + "LDAPAuthLicenced": true, + "LDAPEnabled": false, + "LocalLoginEnabled": true, + "LoginFailureDelay": 0, + "LoginHint": { + "Hint": "POST to /Sessions to login using the following JSON object:", + "HintPOSTData": { + "Password": "password", + "UserName": "username" + } + }, + "SecurityOverride": false, + "ServerName": "" + }, + "Time": "2017-03-30T12:13:38Z" + } + }, + "RedfishVersion": "1.0.0", + "Registries": { + "@odata.id": "/redfish/v1/Registries/" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService/" + }, + "Systems": { + "@odata.id": "/redfish/v1/Systems/" + }, + "UUID": "7704b47b-2fbe-5920-99a5-b766dd84cc28", + "UpdateService": { + "@odata.id": "/redfish/v1/UpdateService/" + } +} diff --git a/proliantutils/tests/redfish/json_samples/system.json b/proliantutils/tests/redfish/json_samples/system.json new file mode 100644 index 0000000..d187aaf --- /dev/null +++ b/proliantutils/tests/redfish/json_samples/system.json @@ -0,0 +1,226 @@ +{ + "@odata.context": "/redfish/v1/$metadata#Systems/Members/$entity", + "@odata.etag": "W/\"0E79655D\"", + "@odata.id": "/redfish/v1/Systems/1/", + "@odata.type": "#ComputerSystem.v1_2_0.ComputerSystem", + "Actions": { + "#ComputerSystem.Reset": { + "ResetType@Redfish.AllowableValues": [ + "On", + "ForceOff", + "ForceRestart", + "Nmi", + "PushPowerButton" + ], + "target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset/" + } + }, + "AssetTag": "", + "Bios": { + "@odata.id": "/redfish/v1/systems/1/bios/" + }, + "BiosVersion": "U31 v1.00 (03/11/2017)", + "Boot": { + "BootSourceOverrideEnabled": "Disabled", + "BootSourceOverrideMode": "UEFI", + "BootSourceOverrideTarget": "None", + "BootSourceOverrideTarget@Redfish.AllowableValues": [ + "None", + "Cd", + "Hdd", + "Usb", + "SDCard", + "Utilities", + "Diags", + "BiosSetup", + "Pxe", + "UefiShell", + "UefiHttp", + "UefiTarget" + ], + "UefiTargetBootSourceOverride": "None", + "UefiTargetBootSourceOverride@Redfish.AllowableValues": [ + "HD(1,GPT,7F14DF43-6600-420A-9950-C028836F6A5D,0x800,0x64000)/\\EFI\\centos\\shim.efi", + "UsbClass(0xFFFF,0xFFFF,0xFF,0xFF,0xFF)", + "PciRoot(0x0)/Pci(0x17,0x0)/Sata(0x3,0x0,0x0)" + ] + }, + "HostName": "", + "Id": "1", + "IndicatorLED": "Off", + "Links": { + "Chassis": [ + { + "@odata.id": "/redfish/v1/Chassis/1/" + } + ], + "ManagedBy": [ + { + "@odata.id": "/redfish/v1/Managers/1/" + } + ] + }, + "LogServices": { + "@odata.id": "/redfish/v1/Systems/1/LogServices/" + }, + "Manufacturer": "HPE", + "Memory": { + "@odata.id": "/redfish/v1/Systems/1/Memory/" + }, + "MemorySummary": { + "Status": { + "HealthRollup": "OK" + }, + "TotalSystemMemoryGiB": 8 + }, + "Model": "ProLiant DL180 Gen10", + "Name": "Computer System", + "Oem": { + "Hpe": { + "@odata.type": "#HpeComputerSystemExt.v2_1_0.HpeComputerSystemExt", + "Actions": { + "#HpeComputerSystemExt.PowerButton": { + "PushType@Redfish.AllowableValues": [ + "Press", + "PressAndHold" + ], + "target": "/redfish/v1/Systems/1/Actions/Oem/Hpe/HpeComputerSystemExt.PowerButton/" + }, + "#HpeComputerSystemExt.SystemReset": { + "ResetType@Redfish.AllowableValues": [ + "ColdBoot" + ], + "target": "/redfish/v1/Systems/1/Actions/Oem/Hpe/HpeComputerSystemExt.SystemReset/" + } + }, + "AggregateHealthStatus": { + "AgentlessManagementService": "Unavailable", + "BiosOrHardwareHealth": { + "Status": { + "Health": "OK" + } + }, + "FanRedundancy": "Redundant", + "Fans": { + "Status": { + "Health": "OK" + } + }, + "Memory": { + "Status": { + "Health": "OK" + } + }, + "PowerSupplies": { + "PowerSuppliesMismatch": false, + "Status": { + "Health": "OK" + } + }, + "Processors": { + "Status": { + "Health": "OK" + } + }, + "Storage": { + "Status": { + "Health": "OK" + } + }, + "Temperatures": { + "Status": { + "Health": "OK" + } + } + }, + "Bios": { + "Backup": { + "Date": "03/11/2017", + "Family": "U31", + "VersionString": "U31 v1.00 (03/11/2017)" + }, + "Current": { + "Date": "03/11/2017", + "Family": "U31", + "VersionString": "U31 v1.00 (03/11/2017)" + }, + "UefiClass": 2 + }, + "DeviceDiscoveryComplete": { + "AMSDeviceDiscovery": "NoAMS", + "DeviceDiscovery": "DataIncomplete", + "SmartArrayDiscovery": "Complete" + }, + "EndOfPostDelaySeconds": null, + "Links": { + "NetworkAdapters": { + "@odata.id": "/redfish/v1/Systems/1/NetworkAdapters/" + }, + "PCIDevices": { + "@odata.id": "/redfish/v1/Systems/1/PCIDevices/" + }, + "PCISlots": { + "@odata.id": "/redfish/v1/Systems/1/PCISlots/" + }, + "SmartStorage": { + "@odata.id": "/redfish/v1/Systems/1/SmartStorage/" + }, + "USBPorts": { + "@odata.id": "/redfish/v1/Systems/1/USBPorts/" + } + }, + "PCAPartNumber": "", + "PCASerialNumber": "847012-001", + "PostDiscoveryCompleteTimeStamp": "2017-03-13T11:11:48Z", + "PostDiscoveryMode": null, + "PostMode": null, + "PostState": "FinishedPost", + "PowerAllocationLimit": 500, + "PowerAutoOn": "Restore", + "PowerOnDelay": "Minimum", + "PowerRegulatorMode": "Dynamic", + "PowerRegulatorModesSupported": [ + "OSControl", + "Dynamic", + "Max", + "Min" + ], + "SMBIOS": { + "extref": "/smbios" + }, + "VirtualProfile": "Inactive" + } + }, + "PowerState": "On", + "ProcessorSummary": { + "Count": 2, + "Model": "Intel(R) Genuine processor", + "Status": { + "HealthRollup": "OK" + } + }, + "Processors": { + "@odata.id": "/redfish/v1/Systems/1/Processors/" + }, + "SKU": " ", + "SecureBoot": { + "@odata.id": "/redfish/v1/Systems/1/SecureBoot/" + }, + "SerialNumber": " ", + "Status": { + "Health": "OK", + "State": "Enabled" + }, + "Storage": { + "@odata.id": "/redfish/v1/Systems/1/Storage/" + }, + "SystemType": "Physical", + "TrustedModules": [ + { + "Status": { + "State": "Absent" + } + } + ], + "UUID": "00000000-0000-0000-0000-000000000000" +} diff --git a/proliantutils/tests/redfish/test_redfish.py b/proliantutils/tests/redfish/test_redfish.py new file mode 100644 index 0000000..df26572 --- /dev/null +++ b/proliantutils/tests/redfish/test_redfish.py @@ -0,0 +1,76 @@ +# 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 mock +import testtools + +from proliantutils import exception +from proliantutils.redfish import redfish +from proliantutils import rest + + +class RedfishOperationsTestCase(testtools.TestCase): + + @mock.patch.object(rest, 'RestConnectorBase', autospec=True) + def setUp(self, rest_connector_mock): + super(RedfishOperationsTestCase, self).setUp() + self.conn = mock.MagicMock() + rest_connector_mock.return_value = self.conn + with open('proliantutils/tests/redfish/' + 'json_samples/root.json', 'r') as f: + self.conn._rest_get.return_value = 200, None, json.loads(f.read()) + + self.rf_client = redfish.RedfishOperations( + '1.2.3.4', username='foo', password='bar') + rest_connector_mock.assert_called_once_with( + '1.2.3.4', 'foo', 'bar', None, None) + + def test__fetch_root_resources(self): + rf_client = self.rf_client + rf_client._fetch_root_resources() + self.assertEqual('HPE RESTful Root Service', + rf_client._root_resp.get('Name')) + self.assertEqual('1.0.0', rf_client._root_resp.get('RedfishVersion')) + self.assertEqual('7704b47b-2fbe-5920-99a5-b766dd84cc28', + rf_client._root_resp.get('UUID')) + for resource in ['Systems', 'Managers', 'Chassis']: + self.assertTrue(resource in rf_client._root_resp) + + def test__get_system_collection_path(self): + self.assertEqual('/redfish/v1/Systems/', + self.rf_client._get_system_collection_path()) + + def test__get_system_collection_path_missing_systems_attr(self): + self.rf_client._root_resp.pop('Systems') + self.assertRaisesRegex( + exception.MissingAttributeError, + 'The attribute Systems is missing', + self.rf_client._get_system_collection_path) + + def test_get_product_name(self): + with open('proliantutils/tests/redfish/' + 'json_samples/system.json', 'r') as f: + self.conn._rest_get.return_value = 200, None, json.loads(f.read()) + product_name = self.rf_client.get_product_name() + self.assertEqual('ProLiant DL180 Gen10', product_name) + + def test_get_host_power_status(self): + with open('proliantutils/tests/redfish/' + 'json_samples/system.json', 'r') as f: + self.conn._rest_get.return_value = 200, None, json.loads(f.read()) + power_state = self.rf_client.get_host_power_status() + self.assertEqual('ON', power_state)