Skip to content

Commit

Permalink
[RFR] Adds Redfish API client (#328)
Browse files Browse the repository at this point in the history
* add Redfish API client

The Redfish provider's implementation now uses the Redfish API
client [1].

[1] https://pypi.org/project/redfish-client/

* Add new entity representing servers

Server is a base entity that exists side-by-side with VM-like
entities. It represents persistent, possibly physical servers
(e.g., bare metal).

This patch provides a general implementation of the classes
and methods that describe servers and their states.

* Implement stats for Redfish physical servers

This adds the handling of the stat retrieval from the Redfish
API. We implemented the stats that are currently supported at
the provider end.

The implementation of the API stat retrieval is a specialisation
of the `Server` entity, providing an example for possible other
future (or past) (physical) servers.
  • Loading branch information
matejart authored and mshriver committed Oct 5, 2018
1 parent 3f39f6f commit 46647a0
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 10 deletions.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ python-novaclient==7.1.2
python-heatclient
pyvcloud==19.1.2
pywinrm
redfish-client==0.1.0
requests
six
tzlocal
Expand Down
3 changes: 2 additions & 1 deletion wrapanapi/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
from .vm import Vm, VmState, VmMixin
from .instance import Instance
from .stack import Stack, StackMixin
from .server import Server, ServerState

__all__ = [
'Template', 'TemplateMixin', 'Vm', 'VmState', 'VmMixin', 'Instance',
'Stack', 'StackMixin'
'Server', 'ServerState', 'Stack', 'StackMixin'
]
108 changes: 108 additions & 0 deletions wrapanapi/entities/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
wrapanapi.entities.server
Implements classes and methods related to actions performed on (physical) servers
"""
import six
from abc import ABCMeta, abstractmethod

from wrapanapi.entities.base import Entity


class ServerState(object):
"""
Represents a state for a server on the provider system.
Implementations of ``Server`` should map to these states
"""
ON = 'ServerState.On'
OFF = 'ServerState.Off'
POWERING_ON = 'ServerState.PoweringOn'
POWERING_OFF = 'ServerState.PoweringOff'
UNKNOWN = 'ServerState.Unknown'

@classmethod
def valid_states(cls):
return [
var_val for _, var_val in vars(cls).items()
if isinstance(var_val, six.string_types) and var_val.startswith('ServerState.')
]


class Server(Entity):
"""
Represents a single server on a management system.
"""
__metaclass__ = ABCMeta
# Implementations must define a dict which maps API states returned by the
# system to a ServerState. Example:
# {'On': ServerState.ON, 'Off': ServerState.OFF}
state_map = None

def __init__(self, *args, **kwargs):
"""
Verify the required class variables are implemented during init
Since abc has no 'abstract class property' concept, this is the approach taken.
"""
state_map = self.state_map
if (not state_map or not isinstance(state_map, dict) or
not all(value in ServerState.valid_states() for value in state_map.values())):
raise NotImplementedError(
"property '{}' not properly implemented in class '{}'"
.format('state_map', self.__class__.__name__)
)
super(Server, self).__init__(*args, **kwargs)

def _api_state_to_serverstate(self, api_state):
"""
Use the state_map for this instance to map a state string into a ServerState constant
"""
try:
return self.state_map[api_state]
except KeyError:
self.logger.warn(
"Unmapped Server state '%s' received from system, mapped to '%s'",
api_state, ServerState.UNKNOWN
)
return ServerState.UNKNOWN

@abstractmethod
def _get_state(self):
"""
Return ServerState object representing the server's current state.
Should call self.refresh() first to get the latest status from the API
"""

def delete(self):
"""Remove the entity on the provider. Not supported on servers."""
raise NotImplementedError("Deleting not supported for servers")

def cleanup(self):
"""
Remove the entity on the provider and any of its associated resources.
Not supported on servers.
"""
raise NotImplementedError("Cleanup not supported for servers")

@property
def is_on(self):
"""Return True if the server is powered on."""
return self._get_state() == ServerState.ON

@property
def is_off(self):
"""Return True if the server is powered off."""
return self._get_state() == ServerState.OFF

@property
def is_powering_on(self):
"""Return True if the server is in the process of being powered on."""
return self._get_state() == ServerState.POWERING_ON

@property
def is_powering_off(self):
"""Return True if the server is in the process of powered off."""
return self._get_state() == ServerState.POWERING_OFF
165 changes: 156 additions & 9 deletions wrapanapi/systems/redfish.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,124 @@
"""
from __future__ import absolute_import

import redfish_client

from wrapanapi.entities import Server, ServerState
from wrapanapi.systems.base import System


class RedfishServer(Server):
state_map = {
'On': ServerState.ON,
'Off': ServerState.OFF,
'PoweringOn': ServerState.POWERING_ON,
'PoweringOff': ServerState.POWERING_OFF,
}

def __init__(self, system, raw=None, **kwargs):
"""
Constructor for RedfishServer.
Args:
system: RedfishSystem instance
raw: the root resource in the Redfish API
odata_id: (optional) the @odata.id reference of this instance
"""
self._odata_id = raw['@odata.id'] if raw else kwargs.get('odata_id')
if not self._odata_id:
raise ValueError("missing required kwargs: 'odata_id'")

super(RedfishServer, self).__init__(system, raw, **kwargs)

@property
def server_cores(self):
"""Return the number of cores on this server."""
return sum([int(p.TotalCores) for p in self.raw.Processors.Members])

@property
def server_memory(self):
"""Return the amount of memory on the server, in MiB."""
return self.raw.MemorySummary.TotalSystemMemoryGiB * 1024

@property
def state(self):
"""Retrieve the current power status of the physical server."""
return self.raw.PowerState

def _get_state(self):
"""
Return ServerState object representing the server's current state.
The caller should call self.refresh() first to get the latest status
from the API.
"""
return self._api_state_to_serverstate(self.state)

def _identifying_attrs(self):
"""
Return the list of attributes that make this instance uniquely identifiable.
These attributes identify the instance without needing to query the API
for updated data.
"""
return {'ems_ref': self._ems_ref}

def refresh(self):
"""
Re-pull data for this entity using the system's API and update this instance's attributes.
This method should be called any time the most up-to-date info needs to be
returned
This method should re-set self.raw with fresh data for this entity
Returns:
New value of self.raw
"""
self.raw._cache = {}

@property
def name(self):
"""Return name from most recent raw data."""
return self._ems_ref

def uuid(self):
"""Return uuid from most recent raw data."""
return self._ems_ref


class RedfishSystem(System):
"""Client to Redfish API
"""Client to Redfish API.
Args:
hostname: The hostname of the system.
username: The username to connect with.
password: The password to connect with.
security_protocol: The security protocol to be used for connecting with
the API. Expected values: 'Non-SSL', 'SSL', 'SSL without validation'
"""

# statistics for the provider
_stats_available = {
'num_server': lambda self: 1,
'num_server': lambda self: self.num_servers,
}

# statistics for an individual server
_server_stats_available = {
'cores_capacity': lambda server: server.server_cores,
'memory_capacity': lambda server: server.server_memory,
}

def __init__(self, hostname, username, password, protocol="https", api_port=443, **kwargs):
super(RedfishSystem, self).__init__(kwargs)
self.api_port = api_port
self.auth = (username, password)
self.url = '{}://{}:{}/'.format(protocol, hostname, self.api_port)
_server_inventory_available = {
'power_state': lambda server: server.state.lower(),
}

def __init__(self, hostname, username, password, security_protocol, api_port=443, **kwargs):
super(RedfishSystem, self).__init__(**kwargs)
protocol = 'http' if security_protocol == 'Non-SSL' else 'https'
self.url = '{}://{}:{}/'.format(protocol, hostname, api_port)
self.kwargs = kwargs
self.api_client = redfish_client.connect(self.url, username, password)

@property
def _identifying_attrs(self):
Expand All @@ -33,5 +130,55 @@ def _identifying_attrs(self):
def info(self):
return 'RedfishSystem url={}'.format(self.url)

def __del__(self):
"""Disconnect from the API when the object is deleted"""
def server_stats(self, physical_server, requested_stats, **kwargs):
"""
Evaluate the requested server stats at the API server.
Returns a dictionary of stats and their respective evaluated values.
Args:
physical_server: representation for the class of this method's caller
requested_stats: the statistics to be obtained
"""
# Retrieve and return the stats
requested_stats = requested_stats or self._stats_available

# Get an instance of the requested Redfish server
redfish_server = self.get_server(physical_server.ems_ref)

return {stat: self._server_stats_available[stat](redfish_server)
for stat in requested_stats}

def server_inventory(self, physical_server, requested_items, **kwargs):
"""
Evaluate the requested inventory item statuses at the API server.
Returns a dictionary of items and their respective evaluated values.
Args:
physical_server: representation for the class of this method's caller
requested_items: the inventory items to be obtained for the server
"""
# Retrieve and return the inventory
requested_items = requested_items or self._server_inventory_available

# Get an instance of the requested Redfish server
redfish_server = self.get_server(physical_server.ems_ref)

return {item: self._server_inventory_available[item](redfish_server)
for item in requested_items}

def get_server(self, resource_id):
"""
Fetch a RedfishServer instance of the physical server representing resource_id.
Args:
resource_id: the Redfish @odata.id of the resource representing the
server to be retrieved
"""
return RedfishServer(self, raw=self.api_client.find(resource_id))

@property
def num_servers(self):
"""Return the number of servers discovered by the provider."""
return len(self.api_client.Systems.Members)

0 comments on commit 46647a0

Please sign in to comment.