From c5d13e4385abed40c7bb41c84419c8d53e708449 Mon Sep 17 00:00:00 2001 From: Gabriela Soria Date: Fri, 5 Oct 2018 00:51:58 -0700 Subject: [PATCH] Add `ChassisCollection` and `Chassis` classes Add representation of Chassis and ChassisCollection resources. The Chassis is used to represent a chassis or other physical enclosure as a Redfish resource. Also adds the methods get_chassis_collection and get_chassis in the public API. Implements: ChassisCollection and Chassis classes Story: #2003853 Task: #26647 Change-Id: I59083562ff2ab3b18bfeebdabc0f4cfd663d01bb --- .../add-chassis-support-5b97daffe1c61a2b.yaml | 5 + sushy/__init__.py | 1 + sushy/main.py | 27 +++ sushy/resources/chassis/__init__.py | 0 sushy/resources/chassis/chassis.py | 213 ++++++++++++++++++ sushy/resources/chassis/constants.py | 162 +++++++++++++ sushy/resources/chassis/mappings.py | 48 ++++ sushy/resources/constants.py | 61 +++++ sushy/resources/manager/constants.py | 9 +- sushy/resources/mappings.py | 30 +++ sushy/resources/system/constants.py | 34 +-- sushy/resources/system/mappings.py | 16 -- sushy/resources/system/system.py | 5 +- sushy/tests/unit/json_samples/chassis.json | 98 ++++++++ .../unit/json_samples/chassis_collection.json | 25 ++ .../tests/unit/resources/chassis/__init__.py | 0 .../unit/resources/chassis/test_chassis.py | 157 +++++++++++++ sushy/tests/unit/test_main.py | 15 ++ 18 files changed, 870 insertions(+), 36 deletions(-) create mode 100644 releasenotes/notes/add-chassis-support-5b97daffe1c61a2b.yaml create mode 100644 sushy/resources/chassis/__init__.py create mode 100644 sushy/resources/chassis/chassis.py create mode 100644 sushy/resources/chassis/constants.py create mode 100644 sushy/resources/chassis/mappings.py create mode 100644 sushy/tests/unit/json_samples/chassis.json create mode 100644 sushy/tests/unit/json_samples/chassis_collection.json create mode 100644 sushy/tests/unit/resources/chassis/__init__.py create mode 100644 sushy/tests/unit/resources/chassis/test_chassis.py diff --git a/releasenotes/notes/add-chassis-support-5b97daffe1c61a2b.yaml b/releasenotes/notes/add-chassis-support-5b97daffe1c61a2b.yaml new file mode 100644 index 00000000..7852e7c1 --- /dev/null +++ b/releasenotes/notes/add-chassis-support-5b97daffe1c61a2b.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support for the Chassis resource to the library. + diff --git a/sushy/__init__.py b/sushy/__init__.py index 913dd057..92623cfe 100644 --- a/sushy/__init__.py +++ b/sushy/__init__.py @@ -20,6 +20,7 @@ from sushy.main import Sushy from sushy.resources.constants import * # noqa from sushy.resources.system.constants import * # noqa from sushy.resources.manager.constants import * # noqa +from sushy.resources.chassis.constants import * # noqa __all__ = ('Sushy',) __version__ = pbr.version.VersionInfo( diff --git a/sushy/main.py b/sushy/main.py index 5f9e9fa9..0b88bf02 100644 --- a/sushy/main.py +++ b/sushy/main.py @@ -18,6 +18,7 @@ from sushy import auth as sushy_auth from sushy import connector as sushy_connector from sushy import exceptions from sushy.resources import base +from sushy.resources.chassis import chassis from sushy.resources.manager import manager from sushy.resources.registry import message_registry_file from sushy.resources.sessionservice import session @@ -69,6 +70,9 @@ class Sushy(base.ResourceBase): _managers_path = base.Field(['Managers', '@odata.id']) """ManagerCollection path""" + _chassis_path = base.Field(['Chassis', '@odata.id']) + """ChassisCollection path""" + _session_service_path = base.Field(['SessionService', '@odata.id']) """SessionService path""" @@ -141,6 +145,29 @@ class Sushy(base.ResourceBase): return system.System(self._conn, identity, redfish_version=self.redfish_version) + def get_chassis_collection(self): + """Get the ChassisCollection object + + :raises: MissingAttributeError, if the collection attribute is + not found + :returns: a ChassisCollection object + """ + if not self._chassis_path: + raise exceptions.MissingAttributeError( + attribute='Chassis/@odata.id', resource=self._path) + + return chassis.ChassisCollection(self._conn, self._chassis_path, + redfish_version=self.redfish_version) + + def get_chassis(self, identity): + """Given the identity return a Chassis object + + :param identity: The identity of the Chassis resource + :returns: The Chassis object + """ + return chassis.Chassis(self._conn, identity, + redfish_version=self.redfish_version) + def get_manager_collection(self): """Get the ManagerCollection object diff --git a/sushy/resources/chassis/__init__.py b/sushy/resources/chassis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sushy/resources/chassis/chassis.py b/sushy/resources/chassis/chassis.py new file mode 100644 index 00000000..6eeed641 --- /dev/null +++ b/sushy/resources/chassis/chassis.py @@ -0,0 +1,213 @@ +# 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. + +# This is referred from Redfish standard schema. +# http://redfish.dmtf.org/schemas/v1/Chassis.v1_8_0.json + +from sushy import exceptions +from sushy.resources import base +from sushy.resources.chassis import mappings as cha_maps +from sushy.resources import common +from sushy.resources import mappings as res_maps + +import logging + +LOG = logging.getLogger(__name__) + + +class ActionsField(base.CompositeField): + reset = common.ResetActionField('#Chassis.Reset') + + +class PhysicalSecurity(base.CompositeField): + intrusion_sensor = base.MappedField('IntrusionSensor', + cha_maps.CHASSIS_INTRUSION_SENSOR_MAP) + """IntrusionSensor + This indicates the known state of the physical security sensor, such as if + it is hardware intrusion detected. + """ + + intrusion_sensor_number = base.Field('IntrusionSensorNumber') + """A numerical identifier to represent the physical security sensor""" + + intrusion_sensor_re_arm = ( + base.MappedField('IntrusionSensorReArm', + cha_maps.CHASSIS_INTRUSION_SENSOR_RE_ARM_MAP)) + """This indicates how the Normal state to be restored""" + + +class Chassis(base.ResourceBase): + """Chassis resource + + The Chassis represents the physical components of a system. This + resource represents the sheet-metal confined spaces and logical zones + such as racks, enclosures, chassis and all other containers. + """ + + chassis_type = base.MappedField('ChassisType', + cha_maps.CHASSIS_TYPE_VALUE_MAP, + required=True) + """The type of physical form factor of the chassis""" + + identity = base.Field('Id', required=True) + """Identifier for the chassis""" + + name = base.Field('Name', required=True) + """The chassis name""" + + asset_tag = base.Field('AssetTag') + """The user assigned asset tag of this chassis""" + + depth_mm = base.Field('DepthMm') + """Depth in millimeters + The depth of the chassis. The value of this property shall represent + the depth (length) of the chassis (in millimeters) as specified by the + manufacturer. + """ + + description = base.Field('Description') + """The chassis description""" + + height_mm = base.Field('HeightMm') + """Height in millimeters + The height of the chassis. The value of this property shall represent + the height of the chassis (in millimeters) as specified by the + manufacturer. + """ + + indicator_led = base.MappedField('IndicatorLED', + res_maps.INDICATOR_LED_VALUE_MAP) + """The state of the indicator LED, used to identify the chassis""" + + manufacturer = base.Field('Manufacturer') + """The manufacturer of this chassis""" + + model = base.Field('Model') + """The model number of the chassis""" + + part_number = base.Field('PartNumber') + """The part number of the chassis""" + + physical_security = PhysicalSecurity('PhysicalSecurity') + """PhysicalSecurity + This value of this property shall contain the sensor state of the physical + security. + """ + + power_state = base.MappedField('PowerState', + res_maps.POWER_STATE_VALUE_MAP) + """The current power state of the chassis""" + + serial_number = base.Field('SerialNumber') + """The serial number of the chassis""" + + sku = base.Field('SKU') + """Stock-keeping unit number (SKU) + The value of this property shall be the stock-keeping unit number for + this chassis. + """ + + status = common.StatusField('Status') + """Status and Health + This property describes the status and health of the chassis and its + children. + """ + + uuid = base.Field('UUID') + """The Universal Unique Identifier (UUID) for this Chassis.""" + + weight_kg = base.Field('WeightKg') + """Weight in kilograms + The value of this property shall represent the published mass (commonly + referred to as weight) of the chassis (in kilograms). + """ + + width_mm = base.Field('WidthMm') + """Width in millimeters + The value of this property shall represent the width of the chassis + (in millimeters) as specified by the manufacturer. + """ + + _actions = ActionsField('Actions') + + def __init__(self, connector, identity, redfish_version=None): + """A class representing a Chassis + + :param connector: A Connector instance + :param identity: The identity of the Chassis resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of the given version. + """ + super(Chassis, self).__init__(connector, identity, redfish_version) + + def _get_reset_action_element(self): + reset_action = self._actions.reset + + if not reset_action: + raise exceptions.MissingActionError(action='#Chassis.Reset', + resource=self._path) + return reset_action + + def get_allowed_reset_chassis_values(self): + """Get the allowed values for resetting the chassis. + + :returns: A set of allowed values. + :raises: MissingAttributeError, if Actions/#Chassis.Reset attribute + not present. + """ + reset_action = self._get_reset_action_element() + + if not reset_action.allowed_values: + LOG.warning('Could not figure out the allowed values for the ' + 'reset chassis action for Chassis %s', self.identity) + return set(res_maps.RESET_TYPE_VALUE_MAP_REV) + + return set([res_maps.RESET_TYPE_VALUE_MAP[v] for v in + set(res_maps.RESET_TYPE_VALUE_MAP). + intersection(reset_action.allowed_values)]) + + def reset_chassis(self, value): + """Reset the chassis. + + :param value: The target value. + :raises: InvalidParameterValueError, if the target value is not + allowed. + """ + valid_resets = self.get_allowed_reset_chassis_values() + if value not in valid_resets: + raise exceptions.InvalidParameterValueError( + parameter='value', value=value, valid_values=valid_resets) + + value = res_maps.RESET_TYPE_VALUE_MAP_REV[value] + target_uri = self._get_reset_action_element().target_uri + + LOG.debug('Resetting the Chassis %s ...', self.identity) + self._conn.post(target_uri, data={'ResetType': value}) + LOG.info('The Chassis %s is being reset', self.identity) + + +class ChassisCollection(base.ResourceCollectionBase): + + @property + def _resource_type(self): + return Chassis + + def __init__(self, connector, path, redfish_version=None): + """A class representing a ChassisCollection + + :param connector: A Connector instance + :param path: The canonical path to the Chassis collection resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of the given version. + """ + super(ChassisCollection, self).__init__(connector, path, + redfish_version) diff --git a/sushy/resources/chassis/constants.py b/sushy/resources/chassis/constants.py new file mode 100644 index 00000000..3235af4b --- /dev/null +++ b/sushy/resources/chassis/constants.py @@ -0,0 +1,162 @@ +# 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. + +# Values comes from the Redfish Chassis json-schema 1.8.0: +# http://redfish.dmtf.org/schemas/v1/Chassis.v1_8_0.json#/definitions/Chassis + +# Chassis Types constants + +CHASSIS_TYPE_RACK = 'rack chassis type' +"""An equipment rack, typically a 19-inch wide freestanding unit""" + +CHASSIS_TYPE_BLADE = 'blade chassis type' +"""Blade + +An enclosed or semi-enclosed, typically vertically-oriented, system +chassis which must be plugged into a multi-system chassis to function +normally. +""" + +CHASSIS_TYPE_ENCLOSURE = 'enclosure chassis type' +"""A generic term for a chassis that does not fit any other description""" + +CHASSIS_TYPE_STAND_ALONE = 'stand alone chassis type' +"""StandAlone + +A single, free-standing system, commonly called a tower or desktop +chassis. +""" + +CHASSIS_TYPE_RACK_MOUNT = 'rack mount chassis type' +"""RackMount + +A single system chassis designed specifically for mounting in an +equipment rack. +""" + +CHASSIS_TYPE_CARD = 'card chassis type' +"""Card + +A loose device or circuit board intended to be installed in a system or +other enclosure. +""" + +CHASSIS_TYPE_CARTRIDGE = 'cartridge chassis type' +"""Cartridge + +A small self-contained system intended to be plugged into a multi-system +chassis""" + +CHASSIS_TYPE_ROW = 'row chassis type' +"""A collection of equipment rack""" + +CHASSIS_TYPE_POD = 'pod chassis type' +"""Pod + +A collection of equipment racks in a large, likely transportable, +container""" + +CHASSIS_TYPE_EXPANSION = 'expansion chassis type' +"""A chassis which expands the capabilities or capacity of another chassis""" + +CHASSIS_TYPE_SIDECAR = 'sidecar chassis type' +"""Sidecar + +A chassis that mates mechanically with another chassis to expand its +capabilities or capacity. +""" + +CHASSIS_TYPE_ZONE = 'zone chassis type' +"""Zone + +A logical division or portion of a physical chassis that contains multiple +devices or systems that cannot be physically separated. +""" + +CHASSIS_TYPE_SLED = 'sled chassis type' +"""Sled + +An enclosed or semi-enclosed, system chassis which must be plugged into a +multi-system chassis to function normally similar to a blade type chassis. +""" + +CHASSIS_TYPE_SHELF = 'shelf chassis type' +"""Shelf + +An enclosed or semi-enclosed, typically horizontally-oriented, system chassis +which must be plugged into a multi-system chassis to function +normally. +""" + +CHASSIS_TYPE_DRAWER = 'drawer chassis type' +"""Drawer + +An enclosed or semi-enclosed, typically horizontally-oriented, system +chassis which may be slid into a multi-system chassis. +""" + +CHASSIS_TYPE_MODULE = 'module chassis type' +"""Module + +A small, typically removable, chassis or card which contains devices for +a particular subsystem or function. +""" + +CHASSIS_TYPE_COMPONENT = 'component chassis type' +"""Component + +A small chassis, card, or device which contains devices for a particular +subsystem or function. +""" + +CHASSIS_TYPE_IP_BASED_DRIVE = 'IP based drive chassis type' +"""A chassis in a drive form factor with IP-based network connections""" + +CHASSIS_TYPE_RACK_GROUP = 'rack group chassis type' +"""A group of racks which form a single entity or share infrastructure""" + +CHASSIS_TYPE_STORAGE_ENCLOSURE = 'storage enclosure chassis type' +"""A chassis which encloses storage""" + +CHASSIS_TYPE_OTHER = 'other chassis type' +"""A chassis that does not fit any of these definitions""" + +# Chassis IntrusionSensor constants + +CHASSIS_INTRUSION_SENSOR_NORMAL = 'normal chassis intrusion sensor' +"""No abnormal physical security conditions are detected at this time""" + +CHASSIS_INTRUSION_SENSOR_HARDWARE_INTRUSION = 'hardware intrusion chassis ' \ + 'intrusion sensor' +"""HardwareIntrusion + +A door, lock, or other mechanism protecting the internal system hardware from +being accessed is detected as being in an insecure state. +""" + +CHASSIS_INTRUSION_SENSOR_TAMPERING_DETECTED = 'tampering detected chassis ' \ + 'intrusion sensor' +"""Physical tampering of the monitored entity is detected""" + +# Chassis IntrusionSensorReArm constants + +CHASSIS_INTRUSION_SENSOR_RE_ARM_MANUAL = 'manual re arm chassis intrusion ' \ + 'sensor' +"""This sensor would be restored to the Normal state by a manual re-arm""" + +CHASSIS_INTRUSION_SENSOR_RE_ARM_AUTOMATIC = 'automatic re arm chassis ' \ + 'intrusion sensor' +"""Automatic + +This sensor would be restored to the Normal state automatically as no abnormal +physical security conditions are detected. +""" diff --git a/sushy/resources/chassis/mappings.py b/sushy/resources/chassis/mappings.py new file mode 100644 index 00000000..eaa8c16a --- /dev/null +++ b/sushy/resources/chassis/mappings.py @@ -0,0 +1,48 @@ +# 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.chassis import constants as cha_cons + +CHASSIS_TYPE_VALUE_MAP = { + 'Rack': cha_cons.CHASSIS_TYPE_RACK, + 'Blade': cha_cons.CHASSIS_TYPE_BLADE, + 'Enclosure': cha_cons.CHASSIS_TYPE_ENCLOSURE, + 'StandAlone': cha_cons.CHASSIS_TYPE_STAND_ALONE, + 'RackMount': cha_cons.CHASSIS_TYPE_RACK_MOUNT, + 'Card': cha_cons.CHASSIS_TYPE_CARD, + 'Cartridge': cha_cons.CHASSIS_TYPE_CARTRIDGE, + 'Row': cha_cons.CHASSIS_TYPE_ROW, + 'Pod': cha_cons.CHASSIS_TYPE_POD, + 'Expansion': cha_cons.CHASSIS_TYPE_EXPANSION, + 'Sidecar': cha_cons.CHASSIS_TYPE_SIDECAR, + 'Zone': cha_cons.CHASSIS_TYPE_ZONE, + 'Sled': cha_cons.CHASSIS_TYPE_SLED, + 'Shelf': cha_cons.CHASSIS_TYPE_SHELF, + 'Drawer': cha_cons.CHASSIS_TYPE_DRAWER, + 'Module': cha_cons.CHASSIS_TYPE_MODULE, + 'Component': cha_cons.CHASSIS_TYPE_COMPONENT, + 'IPBasedDrive': cha_cons.CHASSIS_TYPE_IP_BASED_DRIVE, + 'RackGroup': cha_cons.CHASSIS_TYPE_RACK_GROUP, + 'StorageEnclosure': cha_cons.CHASSIS_TYPE_STORAGE_ENCLOSURE, + 'Other': cha_cons.CHASSIS_TYPE_OTHER, +} + +CHASSIS_INTRUSION_SENSOR_MAP = { + 'Normal': cha_cons.CHASSIS_INTRUSION_SENSOR_NORMAL, + 'HardwareIntrusion': cha_cons.CHASSIS_INTRUSION_SENSOR_HARDWARE_INTRUSION, + 'TamperingDetected': cha_cons.CHASSIS_INTRUSION_SENSOR_TAMPERING_DETECTED, +} + +CHASSIS_INTRUSION_SENSOR_RE_ARM_MAP = { + 'Manual': cha_cons.CHASSIS_INTRUSION_SENSOR_RE_ARM_MANUAL, + 'Automatic': cha_cons.CHASSIS_INTRUSION_SENSOR_RE_ARM_AUTOMATIC, +} diff --git a/sushy/resources/constants.py b/sushy/resources/constants.py index ea8a279b..dc72f810 100644 --- a/sushy/resources/constants.py +++ b/sushy/resources/constants.py @@ -40,3 +40,64 @@ PARAMTYPE_NUMBER = 'number' SEVERITY_OK = 'ok' SEVERITY_WARNING = 'warning' SEVERITY_CRITICAL = 'critical' + +# Indicator LED Constants + +INDICATOR_LED_LIT = 'indicator led lit' +"""The Indicator LED is lit""" + +INDICATOR_LED_BLINKING = 'indicator led blinking' +"""The Indicator LED is blinking""" + +INDICATOR_LED_OFF = 'indicator led off' +"""The Indicator LED is off""" + +INDICATOR_LED_UNKNOWN = 'indicator led unknown' +"""The state of the Indicator LED cannot be determine""" + +# System' PowerState constants + +POWER_STATE_ON = 'on' +"""The resource is powered on""" + +POWER_STATE_OFF = 'off' +"""The resource is powered off, although some components may continue to + have AUX power such as management controller""" + +POWER_STATE_POWERING_ON = 'powering on' +"""A temporary state between Off and On. This temporary state can + be very short""" + +POWER_STATE_POWERING_OFF = 'powering off' +"""A temporary state between On and Off. The power off action can take + time while the OS is in the shutdown process""" + +# Reset action constants + +RESET_TYPE_ON = 'on' +"""Turn the unit on""" + +RESET_TYPE_FORCE_ON = 'force on' +"""Turn the unit on immediately""" + +RESET_TYPE_FORCE_OFF = 'force off' +"""Turn the unit off immediately (non-graceful shutdown)""" + +RESET_TYPE_GRACEFUL_SHUTDOWN = 'graceful shutdown' +"""Perform a graceful shutdown and power off""" + +RESET_TYPE_GRACEFUL_RESTART = 'graceful restart' +"""Perform a graceful shutdown followed by a restart of the system""" + +RESET_TYPE_FORCE_RESTART = 'force restart' +"""Perform an immediate (non-graceful) shutdown, followed by a restart""" + +RESET_TYPE_NMI = 'nmi' +"""Generate a Diagnostic Interrupt (usually an NMI on x86 systems) to cease +normal operations, perform diagnostic actions and typically halt the system""" + +RESET_TYPE_PUSH_POWER_BUTTON = 'push power button' +"""Simulate the pressing of the physical power button on this unit""" + +RESET_TYPE_POWER_CYCLE = 'power cycle' +"""Perform a power cycle of the unit""" diff --git a/sushy/resources/manager/constants.py b/sushy/resources/manager/constants.py index 9c1cf187..4459c65d 100644 --- a/sushy/resources/manager/constants.py +++ b/sushy/resources/manager/constants.py @@ -13,10 +13,15 @@ # Values comes from the Redfish System json-schema 1.0.0: # http://redfish.dmtf.org/schemas/v1/Manager.v1_0_0.json#/definitions/Manager # noqa +from sushy.resources import constants as res_cons + # Manager Reset action constants -RESET_MANAGER_GRACEFUL_RESTART = 'graceful restart' -RESET_MANAGER_FORCE_RESTART = 'force restart' +RESET_MANAGER_GRACEFUL_RESTART = res_cons.RESET_TYPE_GRACEFUL_RESTART +"""Perform a graceful shutdown followed by a restart of the system""" + +RESET_MANAGER_FORCE_RESTART = res_cons.RESET_TYPE_FORCE_RESTART +"""Perform an immediate (non-graceful) shutdown, followed by a restart""" # Manager Type constants diff --git a/sushy/resources/mappings.py b/sushy/resources/mappings.py index 8afad7e1..34167699 100644 --- a/sushy/resources/mappings.py +++ b/sushy/resources/mappings.py @@ -45,3 +45,33 @@ SEVERITY_VALUE_MAP = { 'Warning': res_cons.SEVERITY_WARNING, 'Critical': res_cons.SEVERITY_CRITICAL } + +INDICATOR_LED_VALUE_MAP = { + 'Lit': res_cons.INDICATOR_LED_LIT, + 'Blinking': res_cons.INDICATOR_LED_BLINKING, + 'Off': res_cons.INDICATOR_LED_OFF, + 'Unknown': res_cons.INDICATOR_LED_UNKNOWN, +} + +POWER_STATE_VALUE_MAP = { + 'On': res_cons.POWER_STATE_ON, + 'Off': res_cons.POWER_STATE_OFF, + 'PoweringOn': res_cons.POWER_STATE_POWERING_ON, + 'PoweringOff': res_cons.POWER_STATE_POWERING_OFF, +} + +POWER_STATE_MAP_REV = utils.revert_dictionary(POWER_STATE_VALUE_MAP) + +RESET_TYPE_VALUE_MAP = { + 'On': res_cons.RESET_TYPE_ON, + 'ForceOff': res_cons.RESET_TYPE_FORCE_OFF, + 'GracefulShutdown': res_cons.RESET_TYPE_GRACEFUL_SHUTDOWN, + 'GracefulRestart': res_cons.RESET_TYPE_GRACEFUL_RESTART, + 'ForceRestart': res_cons.RESET_TYPE_FORCE_RESTART, + 'Nmi': res_cons.RESET_TYPE_NMI, + 'ForceOn': res_cons.RESET_TYPE_FORCE_ON, + 'PushPowerButton': res_cons.RESET_TYPE_PUSH_POWER_BUTTON, + 'PowerCycle': res_cons.RESET_TYPE_POWER_CYCLE, +} + +RESET_TYPE_VALUE_MAP_REV = utils.revert_dictionary(RESET_TYPE_VALUE_MAP) diff --git a/sushy/resources/system/constants.py b/sushy/resources/system/constants.py index 06b07b09..37e0652f 100644 --- a/sushy/resources/system/constants.py +++ b/sushy/resources/system/constants.py @@ -16,46 +16,48 @@ # Values comes from the Redfish System json-schema 1.0.0: # http://redfish.dmtf.org/schemas/v1/ComputerSystem.v1_0_0.json#/definitions/ComputerSystem # noqa +from sushy.resources import constants as res_cons + # Reset action constants -RESET_ON = 'on' -RESET_FORCE_OFF = 'force off' -RESET_GRACEFUL_SHUTDOWN = 'graceful shutdown' -RESET_GRACEFUL_RESTART = 'graceful restart' -RESET_FORCE_RESTART = 'force restart' -RESET_NMI = 'nmi' -RESET_FORCE_ON = 'force on' -RESET_PUSH_POWER_BUTTON = 'push power button' +RESET_ON = res_cons.RESET_TYPE_ON +RESET_FORCE_OFF = res_cons.RESET_TYPE_FORCE_OFF +RESET_GRACEFUL_SHUTDOWN = res_cons.RESET_TYPE_GRACEFUL_SHUTDOWN +RESET_GRACEFUL_RESTART = res_cons.RESET_TYPE_GRACEFUL_RESTART +RESET_FORCE_RESTART = res_cons.RESET_TYPE_FORCE_RESTART +RESET_NMI = res_cons.RESET_TYPE_NMI +RESET_FORCE_ON = res_cons.RESET_TYPE_FORCE_ON +RESET_PUSH_POWER_BUTTON = res_cons.RESET_TYPE_PUSH_POWER_BUTTON # System' PowerState constants -SYSTEM_POWER_STATE_ON = 'on' +SYSTEM_POWER_STATE_ON = res_cons.POWER_STATE_ON """The system is powered on""" -SYSTEM_POWER_STATE_OFF = 'off' +SYSTEM_POWER_STATE_OFF = res_cons.POWER_STATE_OFF """The system is powered off, although some components may continue to have AUX power such as management controller""" -SYSTEM_POWER_STATE_POWERING_ON = 'powering on' +SYSTEM_POWER_STATE_POWERING_ON = res_cons.POWER_STATE_POWERING_ON """A temporary state between Off and On. This temporary state can be very short""" -SYSTEM_POWER_STATE_POWERING_OFF = 'powering off' +SYSTEM_POWER_STATE_POWERING_OFF = res_cons.POWER_STATE_POWERING_OFF """A temporary state between On and Off. The power off action can take time while the OS is in the shutdown process""" # Indicator LED Constants -SYSTEM_INDICATOR_LED_LIT = 'Lit' +SYSTEM_INDICATOR_LED_LIT = res_cons.INDICATOR_LED_LIT """The Indicator LED is lit""" -SYSTEM_INDICATOR_LED_BLINKING = 'Blinking' +SYSTEM_INDICATOR_LED_BLINKING = res_cons.INDICATOR_LED_BLINKING """The Indicator LED is blinking""" -SYSTEM_INDICATOR_LED_OFF = 'Off' +SYSTEM_INDICATOR_LED_OFF = res_cons.INDICATOR_LED_OFF """The Indicator LED is off""" -SYSTEM_INDICATOR_LED_UNKNOWN = 'Unknown' +SYSTEM_INDICATOR_LED_UNKNOWN = res_cons.INDICATOR_LED_UNKNOWN """The state of the Indicator LED cannot be determine""" # Boot source target constants diff --git a/sushy/resources/system/mappings.py b/sushy/resources/system/mappings.py index c9a3244c..3ff34f0f 100644 --- a/sushy/resources/system/mappings.py +++ b/sushy/resources/system/mappings.py @@ -30,22 +30,6 @@ RESET_SYSTEM_VALUE_MAP = { RESET_SYSTEM_VALUE_MAP_REV = utils.revert_dictionary(RESET_SYSTEM_VALUE_MAP) -SYSTEM_POWER_STATE_MAP = { - 'On': sys_cons.SYSTEM_POWER_STATE_ON, - 'Off': sys_cons.SYSTEM_POWER_STATE_OFF, - 'PoweringOn': sys_cons.SYSTEM_POWER_STATE_POWERING_ON, - 'PoweringOff': sys_cons.SYSTEM_POWER_STATE_POWERING_OFF, -} - -SYSTEM_POWER_STATE_MAP_REV = utils.revert_dictionary(SYSTEM_POWER_STATE_MAP) - -SYSTEM_INDICATOR_LED_MAP = { - 'Lit': sys_cons.SYSTEM_INDICATOR_LED_LIT, - 'Blinking': sys_cons.SYSTEM_INDICATOR_LED_BLINKING, - 'Off': sys_cons.SYSTEM_INDICATOR_LED_OFF, - 'Unknown': sys_cons.SYSTEM_INDICATOR_LED_UNKNOWN, -} - BOOT_SOURCE_TARGET_MAP = { 'None': sys_cons.BOOT_SOURCE_TARGET_NONE, 'Pxe': sys_cons.BOOT_SOURCE_TARGET_PXE, diff --git a/sushy/resources/system/system.py b/sushy/resources/system/system.py index 14064cdf..6859ee16 100644 --- a/sushy/resources/system/system.py +++ b/sushy/resources/system/system.py @@ -18,6 +18,7 @@ import logging from sushy import exceptions from sushy.resources import base from sushy.resources import common +from sushy.resources import mappings as res_maps from sushy.resources.system import bios from sushy.resources.system import constants as sys_cons from sushy.resources.system import ethernet_interface @@ -86,7 +87,7 @@ class System(base.ResourceBase): """The system identity string""" indicator_led = base.MappedField('IndicatorLED', - sys_maps.SYSTEM_INDICATOR_LED_MAP) + res_maps.INDICATOR_LED_VALUE_MAP) """Whether the indicator LED is lit or off""" manufacturer = base.Field('Manufacturer') @@ -99,7 +100,7 @@ class System(base.ResourceBase): """The system part number""" power_state = base.MappedField('PowerState', - sys_maps.SYSTEM_POWER_STATE_MAP) + res_maps.POWER_STATE_VALUE_MAP) """The system power state""" serial_number = base.Field('SerialNumber') diff --git a/sushy/tests/unit/json_samples/chassis.json b/sushy/tests/unit/json_samples/chassis.json new file mode 100644 index 00000000..45cd1e75 --- /dev/null +++ b/sushy/tests/unit/json_samples/chassis.json @@ -0,0 +1,98 @@ +{ + "@odata.type": "#Chassis.v1_8_0.Chassis", + "Id": "Blade1", + "Name": "Blade", + "Description": "Test description", + "ChassisType": "Blade", + "AssetTag": "45Z-2381", + "Manufacturer": "Contoso", + "Model": "SX1000", + "SKU": "6914260", + "SerialNumber": "529QB9450R6", + "PartNumber": "166480-S23", + "UUID": "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", + "PowerState": "On", + "IndicatorLED": "Off", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "HeightMm": 44.45, + "WidthMm": 431.8, + "DepthMm": 711, + "WeightKg": 15.31, + "Location": { + "PartLocation": { + "ServiceLabel": "Blade 1", + "LocationType": "Slot", + "LocationOrdinalValue": 0, + "Reference": "Front", + "Orientation": "LeftToRight" + } + }, + "PhysicalSecurity": { + "IntrusionSensor": "Normal", + "IntrusionSensorNumber": 123, + "IntrusionSensorReArm": "Manual" + }, + "Thermal": { + "@odata.id": "/redfish/v1/Chassis/Blade1/Thermal" + }, + "Links": { + "ComputerSystems": [ + { + "@odata.id": "/redfish/v1/Systems/529QB9450R6" + } + ], + "ManagedBy": [ + { + "@odata.id": "/redfish/v1/Managers/Blade1BMC" + } + ], + "ContainedBy": { + "@odata.id": "/redfish/v1/Chassis/MultiBladeEncl" + }, + "CooledBy": [ + { + "@odata.id": "/redfish/v1/Chassis/MultiBladeEncl/Thermal#/Fans/0" + }, + { + "@odata.id": "/redfish/v1/Chassis/MultiBladeEncl/Thermal#/Fans/1" + }, + { + "@odata.id": "/redfish/v1/Chassis/MultiBladeEncl/Thermal#/Fans/2" + }, + { + "@odata.id": "/redfish/v1/Chassis/MultiBladeEncl/Thermal#/Fans/3" + } + ], + "PoweredBy": [ + { + "@odata.id": "/redfish/v1/Chassis/MultiBladeEncl/Power#/PowerSupplies/0" + }, + { + "@odata.id": "/redfish/v1/Chassis/MultiBladeEncl/Power#/PowerSupplies/1" + } + ] + }, + "Actions": { + "#Chassis.Reset": { + "target": "/redfish/v1/Chassis/Blade1/Actions/Chassis.Reset", + "ResetType@Redfish.AllowableValues": [ + "ForceRestart", + "GracefulRestart", + "On", + "ForceOff", + "GracefulShutdown", + "Nmi", + "ForceOn", + "PushPowerButton", + "PowerCycle" + ] + }, + "Oem": {} + }, + "@odata.context": "/redfish/v1/$metadata#Chassis.Chassis", + "@odata.id": "/redfish/v1/Chassis/Blade1", + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/sushy/tests/unit/json_samples/chassis_collection.json b/sushy/tests/unit/json_samples/chassis_collection.json new file mode 100644 index 00000000..0af4cad3 --- /dev/null +++ b/sushy/tests/unit/json_samples/chassis_collection.json @@ -0,0 +1,25 @@ +{ + "@odata.type": "#ChassisCollection.ChassisCollection", + "Name": "Chassis Collection", + "Members@odata.count": 5, + "Members": [ + { + "@odata.id": "/redfish/v1/Chassis/MultiBladeEncl" + }, + { + "@odata.id": "/redfish/v1/Chassis/Blade1" + }, + { + "@odata.id": "/redfish/v1/Chassis/Blade2" + }, + { + "@odata.id": "/redfish/v1/Chassis/Blade3" + }, + { + "@odata.id": "/redfish/v1/Chassis/Blade4" + } + ], + "@odata.context": "/redfish/v1/$metadata#ChassisCollection.ChassisCollection", + "@odata.id": "/redfish/v1/Chassis", + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/sushy/tests/unit/resources/chassis/__init__.py b/sushy/tests/unit/resources/chassis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sushy/tests/unit/resources/chassis/test_chassis.py b/sushy/tests/unit/resources/chassis/test_chassis.py new file mode 100644 index 00000000..8fc87ff8 --- /dev/null +++ b/sushy/tests/unit/resources/chassis/test_chassis.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- + +# 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 sushy +from sushy import exceptions +from sushy.resources.chassis import chassis +from sushy.tests.unit import base + + +class ChassisTestCase(base.TestCase): + + def setUp(self): + super(ChassisTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/chassis.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + self.chassis = chassis.Chassis(self.conn, '/redfish/v1/Chassis/Blade1', + redfish_version='1.8.0') + + def test__parse_attributes(self): + # | WHEN | + self.chassis._parse_attributes() + # | THEN | + self.assertEqual('1.8.0', self.chassis.redfish_version) + self.assertEqual('Blade1', self.chassis.identity) + self.assertEqual('Blade', self.chassis.name) + self.assertEqual('Test description', self.chassis.description) + self.assertEqual('45Z-2381', self.chassis.asset_tag) + self.assertEqual(sushy.CHASSIS_TYPE_BLADE, + self.chassis.chassis_type) + self.assertEqual('Contoso', self.chassis.manufacturer) + self.assertEqual('SX1000', self.chassis.model) + self.assertEqual('529QB9450R6', self.chassis.serial_number) + self.assertEqual('6914260', self.chassis.sku) + self.assertEqual('166480-S23', self.chassis.part_number) + self.assertEqual('FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF', + self.chassis.uuid) + self.assertEqual(sushy.INDICATOR_LED_OFF, + self.chassis.indicator_led) + self.assertEqual(sushy.POWER_STATE_ON, + self.chassis.power_state) + self.assertEqual(sushy.STATE_ENABLED, self.chassis.status.state) + self.assertEqual(44.45, self.chassis.height_mm) + self.assertEqual(431.8, self.chassis.width_mm) + self.assertEqual(711, self.chassis.depth_mm) + self.assertEqual(15.31, self.chassis.weight_kg) + self.assertEqual(sushy.HEALTH_OK, self.chassis.status.health) + self.assertEqual(sushy.CHASSIS_INTRUSION_SENSOR_NORMAL, + self.chassis.physical_security.intrusion_sensor) + self.assertEqual(123, + self.chassis.physical_security.intrusion_sensor_number + ) + self.assertEqual(sushy.CHASSIS_INTRUSION_SENSOR_RE_ARM_MANUAL, + self.chassis.physical_security.intrusion_sensor_re_arm + ) + + def test_get_allowed_reset_chasis_values(self): + # | GIVEN | + expected = {sushy.RESET_TYPE_POWER_CYCLE, + sushy.RESET_TYPE_PUSH_POWER_BUTTON, + sushy.RESET_TYPE_FORCE_ON, sushy.RESET_TYPE_NMI, + sushy.RESET_TYPE_FORCE_RESTART, + sushy.RESET_TYPE_GRACEFUL_RESTART, sushy.RESET_TYPE_ON, + sushy.RESET_TYPE_FORCE_OFF, + sushy.RESET_TYPE_GRACEFUL_SHUTDOWN} + # | WHEN | + values = self.chassis.get_allowed_reset_chassis_values() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_allowed_reset_chassis_values_for_no_values_set(self): + # | GIVEN | + self.chassis._actions.reset.allowed_values = [] + expected = {sushy.RESET_TYPE_POWER_CYCLE, + sushy.RESET_TYPE_PUSH_POWER_BUTTON, + sushy.RESET_TYPE_FORCE_ON, sushy.RESET_TYPE_NMI, + sushy.RESET_TYPE_FORCE_RESTART, + sushy.RESET_TYPE_GRACEFUL_RESTART, sushy.RESET_TYPE_ON, + sushy.RESET_TYPE_FORCE_OFF, + sushy.RESET_TYPE_GRACEFUL_SHUTDOWN} + # | WHEN | + values = self.chassis.get_allowed_reset_chassis_values() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_allowed_reset_chassis_values_missing_action_reset_attr(self): + # | GIVEN | + self.chassis._actions.reset = None + # | WHEN & THEN | + self.assertRaisesRegex( + exceptions.MissingActionError, 'action #Chassis.Reset') + + def test_reset_chassis(self): + self.chassis.reset_chassis(sushy.RESET_TYPE_GRACEFUL_RESTART) + self.chassis._conn.post.assert_called_once_with( + '/redfish/v1/Chassis/Blade1/Actions/Chassis.Reset', + data={'ResetType': 'GracefulRestart'}) + + def test_reset_chassis_with_invalid_value(self): + self.assertRaises(exceptions.InvalidParameterValueError, + self.chassis.reset_chassis, 'invalid-value') + + +class ChassisCollectionTestCase(base.TestCase): + + def setUp(self): + super(ChassisCollectionTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/' + 'chassis_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + self.chassis = chassis.ChassisCollection( + self.conn, '/redfish/v1/Chassis', redfish_version='1.5.0') + + @mock.patch.object(chassis, 'Chassis', autospec=True) + def test_get_member(self, chassis_mock): + self.chassis.get_member('/redfish/v1/Chassis/MultiBladeEncl') + chassis_mock.assert_called_once_with( + self.chassis._conn, '/redfish/v1/Chassis/MultiBladeEncl', + redfish_version=self.chassis.redfish_version) + + @mock.patch.object(chassis, 'Chassis', autospec=True) + def test_get_members(self, chassis_mock): + members = self.chassis.get_members() + calls = [ + mock.call(self.chassis._conn, '/redfish/v1/Chassis/MultiBladeEncl', + redfish_version=self.chassis.redfish_version), + mock.call(self.chassis._conn, '/redfish/v1/Chassis/Blade1', + redfish_version=self.chassis.redfish_version), + mock.call(self.chassis._conn, '/redfish/v1/Chassis/Blade2', + redfish_version=self.chassis.redfish_version), + mock.call(self.chassis._conn, '/redfish/v1/Chassis/Blade3', + redfish_version=self.chassis.redfish_version), + mock.call(self.chassis._conn, '/redfish/v1/Chassis/Blade4', + redfish_version=self.chassis.redfish_version) + ] + chassis_mock.assert_has_calls(calls) + self.assertIsInstance(members, list) + self.assertEqual(5, len(members)) diff --git a/sushy/tests/unit/test_main.py b/sushy/tests/unit/test_main.py index 60df9ac8..486616fd 100644 --- a/sushy/tests/unit/test_main.py +++ b/sushy/tests/unit/test_main.py @@ -21,6 +21,7 @@ from sushy import auth from sushy import connector from sushy import exceptions from sushy import main +from sushy.resources.chassis import chassis from sushy.resources.manager import manager from sushy.resources.registry import message_registry_file from sushy.resources.sessionservice import session @@ -99,6 +100,20 @@ class MainTestCase(base.TestCase): self.root._conn, 'fake-system-id', redfish_version=self.root.redfish_version) + @mock.patch.object(chassis, 'Chassis', autospec=True) + def test_get_chassis(self, mock_chassis): + self.root.get_chassis('fake-chassis-id') + mock_chassis.assert_called_once_with( + self.root._conn, 'fake-chassis-id', + redfish_version=self.root.redfish_version) + + @mock.patch.object(chassis, 'ChassisCollection', autospec=True) + def test_get_chassis_collection(self, chassis_collection_mock): + self.root.get_chassis_collection() + chassis_collection_mock.assert_called_once_with( + self.root._conn, '/redfish/v1/Chassis', + redfish_version=self.root.redfish_version) + @mock.patch.object(manager, 'ManagerCollection', autospec=True) def test_get_manager_collection(self, ManagerCollection_mock): self.root.get_manager_collection()