Lin Yang b6360378d1 Allow user to only specify capacity parameter
Previously, the endpoint parameter is required for attaching storage.
It returns InvalidParameterValueError exception when user only
specify capacity parameter. Fixed it by checking endpoint only when
it is not None.

Change-Id: Ie3da659f5c8e4fe35e19dc3ba60db7c83b6cfae7
2017-09-08 10:59:42 -07:00

403 lines
15 KiB
Python

# Copyright 2017 Intel, Inc.
# 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 logging
from sushy import exceptions
from sushy.resources import base
from sushy.resources import common
from sushy.resources.system import system
from sushy import utils
from rsd_lib.resources.node import constants as node_cons
from rsd_lib.resources.node import mappings as node_maps
LOG = logging.getLogger(__name__)
class AssembleActionField(base.CompositeField):
target_uri = base.Field('target', required=True)
class AttachEndpointActionField(base.CompositeField):
allowed_values = base.Field('Resource@Redfish.AllowableValues',
default=[],
adapter=utils.get_members_identities)
target_uri = base.Field('target', required=True)
class DetachEndpointActionField(base.CompositeField):
allowed_values = base.Field('Resource@Redfish.AllowableValues',
default=[],
adapter=utils.get_members_identities)
target_uri = base.Field('target', required=True)
class ComposeNodeActionField(base.CompositeField):
target_uri = base.Field('target', required=True)
class NodeActionsField(base.CompositeField):
reset = common.ResetActionField('#ComposedNode.Reset')
assemble = AssembleActionField('#ComposedNode.Assemble')
attach_endpoint = AttachEndpointActionField('#ComposedNode.AttachEndpoint')
detach_endpoint = DetachEndpointActionField('#ComposedNode.DetachEndpoint')
class NodeCollectionActionsField(base.CompositeField):
compose = ComposeNodeActionField('#ComposedNodeCollection.Allocate')
class BootField(base.CompositeField):
allowed_values = base.Field(
'BootSourceOverrideTarget@Redfish.AllowableValues',
adapter=list)
enabled = base.MappedField('BootSourceOverrideEnabled',
node_maps.BOOT_SOURCE_ENABLED_MAP)
mode = base.MappedField('BootSourceOverrideMode',
node_maps.BOOT_SOURCE_MODE_MAP)
target = base.MappedField('BootSourceOverrideTarget',
node_maps.BOOT_SOURCE_TARGET_MAP)
class MemorySummaryField(base.CompositeField):
health = base.Field(['Status', 'Health'])
"""The overall health state of memory.
This signifies health state of memory along with its dependent resources.
"""
size_gib = base.Field('TotalSystemMemoryGiB', adapter=int)
"""The size of memory of the node in GiB.
This signifies the total installed, operating system-accessible memory
(RAM), measured in GiB.
"""
class ProcessorSummaryField(base.CompositeField):
health = base.Field(['Status', 'Health'])
"""The overall health state of the node processors."""
count = base.Field('Count', adapter=int)
"""The number of CPUs in the node."""
model = base.Field('Model')
"""Basic information about processor model."""
class Node(base.ResourceBase):
boot = BootField('Boot', required=True)
"""A dictionary containg the current boot device, frequency and mode"""
composed_node_state = base.MappedField('ComposedNodeState',
node_maps.COMPOSED_NODE_STATE_MAP)
"""Current state of assembly process for this node"""
description = base.Field('Description')
"""The node description"""
identity = base.Field('Id', required=True)
"""The node identity string"""
name = base.Field('Name')
"""The node name"""
power_state = base.MappedField('PowerState',
node_maps.NODE_POWER_STATE_MAP)
"""The node power state"""
uuid = base.Field('UUID')
"""The node UUID"""
memory_summary = MemorySummaryField('Memory')
"""The summary info of memory of the node in general detail"""
processor_summary = ProcessorSummaryField('Processors')
"""The summary info for the node processors in general detail"""
_system = None # ref to System instance
_actions = NodeActionsField('Actions', required=True)
def __init__(self, connector, identity, redfish_version=None):
"""A class representing a ComposedNode
:param connector: A Connector instance
:param identity: The identity of the Node resource
:param redfish_version: The version of RedFish. Used to construct
the object according to schema of the given version.
"""
super(Node, 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='#ComposedNode.Reset',
resource=self._path)
return reset_action
def _get_assemble_action_element(self):
assemble_action = self._actions.assemble
if not assemble_action:
raise exceptions.MissingActionError(
action='#ComposedNode.Assemble',
resource=self._path)
return assemble_action
def get_allowed_reset_node_values(self):
"""Get the allowed values for resetting the node.
:returns: A set with the allowed values.
"""
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 node action for Node %s', self.identity)
return set(node_maps.RESET_NODE_VALUE_MAP_REV)
return set([node_maps.RESET_NODE_VALUE_MAP[v] for v in
set(node_maps.RESET_NODE_VALUE_MAP).
intersection(reset_action.allowed_values)])
def reset_node(self, value):
"""Reset the node.
:param value: The target value.
:raises: InvalidParameterValueError, if the target value is not
allowed.
"""
valid_resets = self.get_allowed_reset_node_values()
if value not in valid_resets:
raise exceptions.InvalidParameterValueError(
parameter='value', value=value, valid_values=valid_resets)
value = node_maps.RESET_NODE_VALUE_MAP_REV[value]
target_uri = self._get_reset_action_element().target_uri
self._conn.post(target_uri, data={'ResetType': value})
def assemble_node(self):
"""Assemble the composed node."""
target_uri = self._get_assemble_action_element().target_uri
self._conn.post(target_uri)
def get_allowed_node_boot_source_values(self):
"""Get the allowed values for changing the boot source.
:returns: A set with the allowed values.
"""
if not self.boot.allowed_values:
LOG.warning('Could not figure out the allowed values for '
'configuring the boot source for Node %s',
self.identity)
return set(node_maps.BOOT_SOURCE_TARGET_MAP_REV)
return set([node_maps.BOOT_SOURCE_TARGET_MAP[v] for v in
set(node_maps.BOOT_SOURCE_TARGET_MAP).
intersection(self.boot.allowed_values)])
def set_node_boot_source(self, target,
enabled=node_cons.BOOT_SOURCE_ENABLED_ONCE,
mode=None):
"""Set the boot source.
Set the boot source to use on next reboot of the Node.
:param target: The target boot source.
:param enabled: The frequency, whether to set it for the next
reboot only (BOOT_SOURCE_ENABLED_ONCE) or persistent to all
future reboots (BOOT_SOURCE_ENABLED_CONTINUOUS) or disabled
(BOOT_SOURCE_ENABLED_DISABLED).
:param mode: The boot mode, UEFI (BOOT_SOURCE_MODE_UEFI) or
Legacy (BOOT_SOURCE_MODE_LEGACY).
:raises: InvalidParameterValueError, if any information passed is
invalid.
"""
valid_targets = self.get_allowed_node_boot_source_values()
if target not in valid_targets:
raise exceptions.InvalidParameterValueError(
parameter='target', value=target, valid_values=valid_targets)
if enabled not in node_maps.BOOT_SOURCE_ENABLED_MAP_REV:
raise exceptions.InvalidParameterValueError(
parameter='enabled', value=enabled,
valid_values=list(node_maps.BOOT_SOURCE_TARGET_MAP_REV))
data = {
'Boot': {
'BootSourceOverrideTarget':
node_maps.BOOT_SOURCE_TARGET_MAP_REV[target],
'BootSourceOverrideEnabled':
node_maps.BOOT_SOURCE_ENABLED_MAP_REV[enabled]
}
}
if mode is not None:
if mode not in node_maps.BOOT_SOURCE_MODE_MAP_REV:
raise exceptions.InvalidParameterValueError(
parameter='mode', value=mode,
valid_values=list(node_maps.BOOT_SOURCE_MODE_MAP_REV))
data['Boot']['BootSourceOverrideMode'] = (
node_maps.BOOT_SOURCE_MODE_MAP_REV[mode])
self._conn.patch(self.path, data=data)
def _get_system_path(self):
"""Helper function to find the System path"""
system_col = self.json.get('Links').get('ComputerSystem')
if not system_col:
raise exceptions.MissingAttributeError(attribute='System',
resource=self._path)
return system_col.get('@odata.id')
@property
def system(self):
"""Property to provide reference to `System` instance
It is calculated once the first time it is queried. On refresh,
this property is reset.
"""
if self._system is None:
self._system = system.System(self._conn, self._get_system_path(),
redfish_version=self.redfish_version)
return self._system
def _get_attach_endpoint_action_element(self):
attach_endpoint_action = self._actions.attach_endpoint
if not attach_endpoint_action:
raise exceptions.MissingActionError(
action='#ComposedNode.AttachEndpoint',
resource=self._path)
return attach_endpoint_action
def attach_endpoint(self, endpoint=None, capacity=None):
"""Attach endpoint from available pool to composed node
:param endpoint: Link to endpoint to attach.
:param capacity: Requested capacity of the drive in GiB.
:raises: InvalidParameterValueError
:raises: BadRequestError if at least one param isn't specified
"""
attach_action = self._get_attach_endpoint_action_element()
valid_endpoints = attach_action.allowed_values
target_uri = attach_action.target_uri
if endpoint and endpoint not in valid_endpoints:
raise exceptions.InvalidParameterValueError(
parameter='endpoint', value=endpoint,
valid_values=valid_endpoints)
data = {}
if endpoint is not None:
data['Resource'] = {'@odata.id': endpoint}
if capacity is not None:
data['CapacityGiB'] = capacity
self._conn.post(target_uri, data=data)
def _get_detach_endpoint_action_element(self):
detach_endpoint_action = self._actions.detach_endpoint
if not detach_endpoint_action:
raise exceptions.MissingActionError(
action='#ComposedNode.DetachEndpoint',
resource=self._path)
return detach_endpoint_action
def detach_endpoint(self, endpoint):
"""Detach already attached endpoint from composed node
:param endpoint: Link to endpoint to detach
:raises: InvalidParameterValueError
:raises: BadRequestError
"""
detach_action = self._get_detach_endpoint_action_element()
valid_endpoints = detach_action.allowed_values
target_uri = detach_action.target_uri
if endpoint not in valid_endpoints:
raise exceptions.InvalidParameterValueError(
parameter='endpoint', value=endpoint,
valid_values=valid_endpoints)
data = {'Resource': endpoint}
self._conn.post(target_uri, data=data)
def delete_node(self):
"""Delete (disassemble) the node.
When this action is called several tasks are performed. A graceful
shutdown is sent to the computer system, all VLANs except reserved ones
are removed from associated ethernet switch ports, the computer system
is deallocated and the remote target is deallocated.
"""
self._conn.delete(self.path)
def refresh(self):
super(Node, self).refresh()
self._system = None
class NodeCollection(base.ResourceCollectionBase):
_actions = NodeCollectionActionsField('Actions', required=True)
@property
def _resource_type(self):
return Node
def __init__(self, connector, path, redfish_version=None):
"""A class representing a ComposedNodeCollection
:param connector: A Connector instance
:param path: The canonical path to the Node collection resource
:param redfish_version: The version of RedFish. Used to construct
the object according to schema of the given version.
"""
super(NodeCollection, self).__init__(connector, path,
redfish_version)
def _get_compose_action_element(self):
compose_action = self._actions.compose
if not compose_action:
raise exceptions.MissingActionError(
action='#ComposedNodeCollection.Allocate',
resource=self._path)
return compose_action
def compose_node(self, properties={}):
"""Compose a node from RackScale hardware
:param properties: The properties requested for node composition
:returns: The location of the composed node
"""
target_uri = self._get_compose_action_element().target_uri
resp = self._conn.post(target_uri, data=properties)
LOG.info("Node created at %s", resp.headers['Location'])
node_url = resp.headers['Location']
return node_url[node_url.find(self._path):]