First working 0.1 version
- Uses tortilla lib to wrap the REST API (dep) - Uses python requests to manage login/logout (dep) - Provides 2 functional working examples with Redfish simulator and ProLiant server or Moonshot Server - Remove OpenStack deps as this code has to be usable outside of OpenStack - Provides a configuration file to handle credentials and connection URL - Provides a mapping class to handle multiple versions of Redfish (in this version, 0.95.0 for ProLiant and 1.0.0 for mockup) - Provides a first action reset_server to ... reset system The action is commented into simple-proliant.py to not do unexpected reset. - Provides a first retrieving function get_bios_version to get the BIOS version of a system. - Add basic logging capability - Clean up to meet pep8 and doc strings (in progress).
This commit is contained in:
parent
359b5db8db
commit
f844afc49b
6
.gitignore
vendored
6
.gitignore
vendored
@ -54,3 +54,9 @@ docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Pydev
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
.metadata
|
||||
|
@ -23,6 +23,7 @@ for build and test automation::
|
||||
doc/ # documentation
|
||||
doc/source # the doc source files live here
|
||||
doc/build/html # output of building any docs will go here
|
||||
dmtf # Reference documents and mockup provided by the DMTF
|
||||
examples/ # any sample code using this library, eg. for education
|
||||
# should be put here
|
||||
redfish/ # the redfish library
|
||||
|
@ -3,7 +3,6 @@ DMTF Redfish specification
|
||||
|
||||
This directory contains the current references from the DMTF on the Redfish
|
||||
specification (1.0.0 at the time of the writing)
|
||||
|
||||
In order to ease test, the DMTF has published a mockup environment to simulate
|
||||
a Redfish based system so it is possible to write programs without real Redfish
|
||||
compliant hardware platform.
|
||||
|
13
examples/docker/Dockerfile
Normal file
13
examples/docker/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM ubuntu:15.04
|
||||
MAINTAINER bruno.cornec@hp.com
|
||||
ENV DEBIAN_FRONTEND noninterative
|
||||
ENV http_proxy http://web-proxy.fra.hp.com:8080
|
||||
ENV https_proxy http://web-proxy.fra.hp.com:8080
|
||||
# Install deps for Redfish mockup
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install python-mock python-pip git openssh-client libpython2.7-dev python-oslotest
|
||||
RUN apt-get -y ansible
|
||||
RUN useradd -m bruno
|
||||
RUN chown -R bruno /usr/local
|
||||
RUN su - bruno -c "git clone https://github.com/bcornec/python-redfish.git ; pip install -r python-redfish/requirements.txt ; cd python-redfish ; python setup.py install -O1"
|
||||
CMD /bin/bash
|
9
examples/redfish.conf
Normal file
9
examples/redfish.conf
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Nodes": {
|
||||
"default": {
|
||||
"url": "",
|
||||
"login": "",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
}
|
45
examples/simple-proliant.py
Normal file
45
examples/simple-proliant.py
Normal file
@ -0,0 +1,45 @@
|
||||
# coding=utf-8
|
||||
|
||||
""" Simple example to use python-redfish on HP Proliant servers """
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import redfish
|
||||
|
||||
# Get $HOME environment.
|
||||
HOME = os.getenv('HOME')
|
||||
|
||||
if HOME == '':
|
||||
print("$HOME environment variable not set, please check your system")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(HOME + "/.redfish.conf") as json_data:
|
||||
config = json.load(json_data)
|
||||
json_data.close()
|
||||
except IOError as e:
|
||||
print("Please create a json configuration file")
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
|
||||
URL = config["Nodes"]["default"]["url"]
|
||||
USER_NAME = config["Nodes"]["default"]["login"]
|
||||
PASSWORD = config["Nodes"]["default"]["password"]
|
||||
|
||||
''' remote_mgmt is a redfish.RedfishConnection object '''
|
||||
remote_mgmt = redfish.connect(URL, USER_NAME, PASSWORD, verify_cert=False)
|
||||
|
||||
print ("Redfish API version : %s \n" % remote_mgmt.get_api_version())
|
||||
|
||||
# Uncomment following line to reset the blade !!!
|
||||
#remote_mgmt.Systems.systems_list[0].reset_system()
|
||||
|
||||
# TODO : create an attribute to link the managed system directly
|
||||
# and avoid systems_list[0]
|
||||
# --> will be something like :
|
||||
# remote_mgmt.Systems.systems_list[0] = remote_mgmt.Systems.managed_system
|
||||
|
||||
print("Bios version : {}\n".format(remote_mgmt.Systems.systems_list[0].get_bios_version()))
|
||||
|
||||
remote_mgmt.logout()
|
38
examples/simple-simulator.py
Normal file
38
examples/simple-simulator.py
Normal file
@ -0,0 +1,38 @@
|
||||
# coding=utf-8
|
||||
|
||||
""" Simple example to use python-redfish with DMTF simulator """
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import redfish
|
||||
|
||||
# Get $HOME environment.
|
||||
HOME = os.getenv('HOME')
|
||||
|
||||
if HOME == '':
|
||||
print("$HOME environment variable not set, please check your system")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(HOME + "/.redfish.conf") as json_data:
|
||||
config = json.load(json_data)
|
||||
json_data.close()
|
||||
except IOError as e:
|
||||
print("Please create a json configuration file")
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
|
||||
URL = config["Nodes"]["default"]["url"]
|
||||
USER_NAME = config["Nodes"]["default"]["login"]
|
||||
PASSWORD = config["Nodes"]["default"]["password"]
|
||||
|
||||
''' remoteMgmt is a redfish.RedfishConnection object '''
|
||||
remote_mgmt = redfish.connect(URL, USER_NAME, PASSWORD,
|
||||
simulator=True, enforceSSL=False)
|
||||
|
||||
print ("Redfish API version : {} \n".format(remote_mgmt.get_api_version()))
|
||||
print ("UUID : {} \n".format(remote_mgmt.Root.get_api_UUID()))
|
||||
print ("Bios version : {}\n".format(remote_mgmt.Systems.systems_list[0].get_bios_version()))
|
||||
|
||||
#print remoteMgmt.get_api_link_to_server()
|
@ -1,6 +0,0 @@
|
||||
from redfish import connection
|
||||
|
||||
host = '127.0.0.1'
|
||||
user_name = 'Admin'
|
||||
password = 'password'
|
||||
server = connection.RedfishConnection(host, user_name, password)
|
@ -2,8 +2,8 @@
|
||||
|
||||
#import logging
|
||||
import sys
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
#from oslo_config import cfg
|
||||
#from oslo_log import log as logging
|
||||
|
||||
import redfish
|
||||
|
||||
@ -12,9 +12,9 @@ import redfish
|
||||
#log_root.addHandler(logging.StreamHandler(sys.stdout))
|
||||
#log_root.setLevel(logging.DEBUG)
|
||||
|
||||
CONF = cfg.CONF
|
||||
logging.set_defaults(['redfish=DEBUG'])
|
||||
logging.register_options(CONF)
|
||||
#CONF = cfg.CONF
|
||||
#logging.set_defaults(['redfish=DEBUG'])
|
||||
#logging.register_options(CONF)
|
||||
#logging.setup(CONF, "redfish")
|
||||
|
||||
# Connect to a redfish API endpoint
|
||||
|
@ -28,6 +28,7 @@ system such as defined by http://www.redfishcertification.org
|
||||
%install
|
||||
%{__python} setup.py install -O1 --skip-build --root %{buildroot}
|
||||
|
||||
# TODO: Add examples
|
||||
%files
|
||||
%doc README.rst examples/*.py
|
||||
%dir %{python_sitelib}/redfish
|
||||
|
@ -12,11 +12,11 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import pbr.version
|
||||
#import pbr.version
|
||||
|
||||
import redfish.server
|
||||
import redfish.types
|
||||
from redfish.main import *
|
||||
#import redfish.types
|
||||
|
||||
|
||||
__version__ = pbr.version.VersionInfo(
|
||||
'redfish').version_string()
|
||||
#__version__ = pbr.version.VersionInfo(
|
||||
# 'redfish').version_string()
|
||||
|
37
redfish/config.py
Normal file
37
redfish/config.py
Normal file
@ -0,0 +1,37 @@
|
||||
# coding=utf-8
|
||||
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
# Global variable definition
|
||||
TORTILLADEBUG = True
|
||||
logger = None
|
||||
|
||||
|
||||
def initialize_logger(redfish_logfile):
|
||||
"""Return api version.
|
||||
|
||||
:param redfish_logfile: redfish log
|
||||
:type str
|
||||
:returns: True
|
||||
|
||||
"""
|
||||
global logger
|
||||
logger = logging.getLogger()
|
||||
|
||||
logger.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s :: %(levelname)s :: %(message)s'
|
||||
)
|
||||
file_handler = RotatingFileHandler(redfish_logfile, 'a', 1000000, 1)
|
||||
|
||||
# First logger to file
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# Second logger to console
|
||||
steam_handler = logging.StreamHandler()
|
||||
steam_handler.setLevel(logging.DEBUG)
|
||||
logger.addHandler(steam_handler)
|
||||
return True
|
@ -1,31 +1,16 @@
|
||||
# -*- 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.
|
||||
|
||||
|
||||
class RedfishException(Exception):
|
||||
"""Base class for redfish exceptions"""
|
||||
def __init__(self, message=None, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
if not message:
|
||||
try:
|
||||
message = self.message % kwargs
|
||||
except Excetion as e:
|
||||
LOG.exception('Error in string format operation')
|
||||
message = self.message
|
||||
super(RedfishException, self).__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
class ObjectLoadException(RedfishException):
|
||||
|
||||
class AuthenticationFailureException(RedfishException):
|
||||
pass
|
||||
|
||||
class LogoutFailureException(RedfishException):
|
||||
pass
|
399
redfish/main.py
Normal file
399
redfish/main.py
Normal file
@ -0,0 +1,399 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
STARTING ASSUMPTIONS
|
||||
|
||||
On URIs:
|
||||
|
||||
The Redfish RESTful API is a "hypermedia API" by design. This is to avoid
|
||||
building in restrictive assumptions to the data model that will make it
|
||||
difficult to adapt to future hardware implementations. A hypermedia API avoids
|
||||
these assumptions by making the data model discoverable via links between
|
||||
resources.
|
||||
|
||||
A URI should be treated by the client as opaque, and thus should not be
|
||||
attempted to be understood or deconstructed by the client. Only specific top
|
||||
level URIs (any URI in this sample code) may be assumed, and even these may be
|
||||
absent based upon the implementation (e.g. there might be no /redfish/v1/Systems
|
||||
collection on something that doesn't have compute nodes.)
|
||||
|
||||
The other URIs must be discovered dynamically by following href links. This is
|
||||
because the API will eventually be implemented on a system that breaks any
|
||||
existing data model "shape" assumptions we may make now. In particular,
|
||||
clients should not make assumptions about the URIs for the resource members of
|
||||
a collection. For instance, the URI of a collection member will NOT always be
|
||||
/redfish/v1/.../collection/1, or 2. On systems with multiple compute nodes per
|
||||
manager, a System collection member might be /redfish/v1/Systems/C1N1.
|
||||
|
||||
This sounds very complicated, but in reality (as these examples demonstrate),
|
||||
if you are looking for specific items, the traversal logic isn't too
|
||||
complicated.
|
||||
|
||||
On Resource Model Traversal:
|
||||
|
||||
Although the resources in the data model are linked together, because of cross
|
||||
link references between resources, a client may not assume the resource model
|
||||
is a tree. It is a graph instead, so any crawl of the data model should keep
|
||||
track of visited resources to avoid an infinite traversal loop.
|
||||
|
||||
A reference to another resource is any property called "href" no matter where
|
||||
it occurs in a resource.
|
||||
|
||||
An external reference to a resource outside the data model is referred to by a
|
||||
property called "extref". Any resource referred to by extref should not be
|
||||
assumed to follow the conventions of the API.
|
||||
|
||||
On Resource Versions:
|
||||
|
||||
Each resource has a "Type" property with a value of the format Tyepname.x.y.z
|
||||
where
|
||||
* x = major version - incrementing this is a breaking change to the schema y =
|
||||
* minor version - incrementing this is a non-breaking additive change to the
|
||||
* schema z = errata - non-breaking change
|
||||
|
||||
Because all resources are versioned and schema also have a version, it is
|
||||
possible to design rules for "nearest" match (e.g. if you are interacting with
|
||||
multiple services using a common batch of schema files). The mechanism is not
|
||||
prescribed, but a client should be prepared to encounter both older and newer
|
||||
versions of resource types.
|
||||
|
||||
On HTTP POST to create:
|
||||
|
||||
WHen POSTing to create a resource (e.g. create an account or session) the
|
||||
guarantee is that a successful response includes a "Location" HTTP header
|
||||
indicating the resource URI of the newly created resource. The POST may also
|
||||
include a representation of the newly created object in a JSON response body
|
||||
but may not. Do not assume the response body, but test it. It may also be an
|
||||
ExtendedError object.
|
||||
|
||||
HTTP REDIRECT:
|
||||
|
||||
All clients must correctly handle HTTP redirect. We (or Redfish) may
|
||||
eventually need to use redirection as a way to alias portions of the data
|
||||
model.
|
||||
|
||||
FUTURE: Asynchronous tasks
|
||||
|
||||
In the future some operations may start asynchonous tasks. In this case, the
|
||||
client should recognized and handle HTTP 202 if needed and the 'Location'
|
||||
header will point to a resource with task information and status.
|
||||
|
||||
JSON-SCHEMA:
|
||||
|
||||
The json-schema available at /redfish/v1/Schemas governs the content of the
|
||||
resources, but keep in mind:
|
||||
* not every property in the schema is implemented in every implementation.
|
||||
* some properties are schemed to allow both null and anotehr type like string
|
||||
* or integer.
|
||||
|
||||
Robust client code should check both the existence and type of interesting
|
||||
properties and fail gracefully if expectations are not met.
|
||||
|
||||
GENERAL ADVICE:
|
||||
|
||||
Clients should always be prepared for:
|
||||
* unimplemented properties (e.g. a property doesn't apply in a particular case)
|
||||
* null values in some cases if the value of a property is not currently known
|
||||
* due to system conditions HTTP status codes other than 200 OK. Can your code
|
||||
* handle an HTTP 500 Internal Server Error with no other info? URIs are case
|
||||
* insensitive HTTP header names are case insensitive JSON Properties and Enum
|
||||
* values are case sensitive A client should be tolerant of any set of HTTP
|
||||
* headers the service returns
|
||||
|
||||
"""
|
||||
|
||||
# coding=utf-8
|
||||
|
||||
import sys
|
||||
import json
|
||||
from urlparse import urlparse
|
||||
import requests
|
||||
import config
|
||||
import types
|
||||
import mapping
|
||||
import exception
|
||||
|
||||
# Global variable definition
|
||||
redfish_logfile = "/var/log/python-redfish/python-redfish.log"
|
||||
|
||||
# ===============================================================================
|
||||
# TODO : create method to set logging level and TORTILLADEBUG.
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
def set_log_file(logfile):
|
||||
global redfish_logfile
|
||||
redfish_logfile = logfile
|
||||
return True
|
||||
|
||||
|
||||
""" Function to wrap RedfishConnection """
|
||||
|
||||
|
||||
def connect(
|
||||
url,
|
||||
user,
|
||||
password,
|
||||
simulator=False,
|
||||
enforceSSL=True,
|
||||
verify_cert=True
|
||||
):
|
||||
global redfish_logfile
|
||||
config.initialize_logger(redfish_logfile)
|
||||
return RedfishConnection(
|
||||
url,
|
||||
user,
|
||||
password,
|
||||
simulator=simulator,
|
||||
enforceSSL=enforceSSL,
|
||||
verify_cert=verify_cert
|
||||
)
|
||||
|
||||
|
||||
class RedfishConnection(object):
|
||||
"""Implements basic connection handling for Redfish APIs."""
|
||||
|
||||
def __init__(self,
|
||||
url,
|
||||
user,
|
||||
password,
|
||||
simulator=False,
|
||||
enforceSSL=True,
|
||||
verify_cert=True
|
||||
):
|
||||
"""Initialize a connection to a Redfish service."""
|
||||
super(RedfishConnection, self).__init__()
|
||||
|
||||
config.logger.info("Initialize python-redfish")
|
||||
|
||||
self.connection_parameters = ConnectionParameters()
|
||||
self.connection_parameters.rooturl = url
|
||||
self.connection_parameters.user_name = user
|
||||
self.connection_parameters.password = password
|
||||
self.connection_parameters.enforceSSL = enforceSSL
|
||||
self.connection_parameters.verify_cert = verify_cert
|
||||
|
||||
# Use DMTF mockup or not
|
||||
self.__simulator = simulator
|
||||
|
||||
# Session attributes
|
||||
self.connection_parameters.auth_token = None
|
||||
self.connection_parameters.user_uri = None
|
||||
|
||||
rooturl = urlparse(self.connection_parameters.rooturl)
|
||||
|
||||
# Enforce ssl
|
||||
if self.connection_parameters.enforceSSL is True:
|
||||
config.logger.debug("Enforcing SSL")
|
||||
rooturl = rooturl._replace(scheme="https")
|
||||
self.connection_parameters.rooturl = rooturl.geturl()
|
||||
|
||||
# Verify cert
|
||||
if self.connection_parameters.verify_cert is False:
|
||||
config.logger.info("Certificat is not checked, " +
|
||||
"this is insecure and can allow" +
|
||||
" a man in the middle attack")
|
||||
|
||||
config.logger.debug("Root url : %s", self.connection_parameters.rooturl)
|
||||
self.Root = types.Root(self.connection_parameters.rooturl,
|
||||
self.connection_parameters
|
||||
)
|
||||
#self.api_url = tortilla.wrap(self.connection_parameters.rooturl,
|
||||
# debug=TORTILLADEBUG)
|
||||
#self.root = self.api_url.get(verify=self.connection_parameters.verify_cert)
|
||||
|
||||
config.logger.info("API Version : %s", self.get_api_version())
|
||||
mapping.redfish_version = self.get_api_version()
|
||||
|
||||
# Instanciate a global mapping object to handle Redfish version variation
|
||||
mapping.redfish_mapper = mapping.RedfishVersionMapping(self.get_api_version())
|
||||
|
||||
# Now we need to login otherwise we are not allowed to extract data
|
||||
if self.__simulator is False:
|
||||
try:
|
||||
config.logger.info("Login to %s", rooturl.netloc)
|
||||
self.login()
|
||||
config.logger.info("Login successful")
|
||||
except "Error getting token":
|
||||
config.logger.error("Login fail, fail to get auth token")
|
||||
raise exception.AuthenticationFailureException("Fail to get an auth token.")
|
||||
|
||||
|
||||
|
||||
# Struture change with mockup 1.0.0, there is no links
|
||||
# section anymore.
|
||||
# ===================================================================
|
||||
# TODO : Add a switch to allow the both structure
|
||||
# ===================================================================
|
||||
|
||||
# Types
|
||||
self.SessionService = types.SessionService(
|
||||
self.Root.get_link_url(
|
||||
mapping.redfish_mapper.map_sessionservice()),
|
||||
self.connection_parameters
|
||||
)
|
||||
|
||||
self.Managers = types.ManagersCollection(self.Root.get_link_url("Managers"),
|
||||
self.connection_parameters
|
||||
)
|
||||
|
||||
self.Systems = types.SystemsCollection(self.Root.get_link_url("Systems"),
|
||||
self.connection_parameters
|
||||
)
|
||||
|
||||
#for system in self.Systems.systems_list:
|
||||
#config.logger.debug(system.data.links.ManagedBy)
|
||||
# self.Chassis
|
||||
|
||||
# self.EventService
|
||||
# self.AccountService
|
||||
# self.Tasks
|
||||
|
||||
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# systemCollectionLink = getattr(self.root.Links.Systems,"@odata.id")
|
||||
# self.systemCollection = self.apiUrl.redfish.v1.Systems.get()
|
||||
#
|
||||
# print self.systemCollection.Name
|
||||
#
|
||||
# ========================================================================
|
||||
def get_api_version(self):
|
||||
"""Return api version.
|
||||
|
||||
:returns: string -- version
|
||||
:raises: AttributeError
|
||||
|
||||
"""
|
||||
return (self.Root.get_api_version())
|
||||
|
||||
def login(self):
|
||||
# Craft full url
|
||||
url = self.Root.get_link_url(
|
||||
mapping.redfish_mapper.map_sessionservice()
|
||||
)
|
||||
|
||||
# Craft request body and header
|
||||
requestBody = {"UserName": self.connection_parameters.user_name , "Password": self.connection_parameters.password}
|
||||
header = {'Content-type': 'application/json'}
|
||||
# =======================================================================
|
||||
# Tortilla seems not able to provide the header of a post request answer.
|
||||
# However this is required by redfish standard to get X-Auth-Token.
|
||||
# So jump to "requests" library to get the required token.
|
||||
# TODO : Patch tortilla to handle this case.
|
||||
# =======================================================================
|
||||
# sessionsUrl = tortilla.wrap("https://10.3.222.104/rest/v1/Sessions", debug=TORTILLADEBUG)
|
||||
# sessions = sessionsUrl.post(verify=self.verify_cert, data=requestBody)
|
||||
auth = requests.post(url,
|
||||
data=json.dumps(requestBody),
|
||||
headers=header,
|
||||
verify=self.connection_parameters.verify_cert
|
||||
)
|
||||
|
||||
# =======================================================================
|
||||
# TODO : Manage exception with a class.
|
||||
# =======================================================================
|
||||
if auth.status_code != 201:
|
||||
pass
|
||||
#sysraise "Error getting token", auth.status_code
|
||||
|
||||
self.connection_parameters.auth_token = auth.headers.get("x-auth-token")
|
||||
self.connection_parameters.user_uri = auth.headers.get("location")
|
||||
config.logger.debug("x-auth-token : %s", self.connection_parameters.auth_token)
|
||||
config.logger.debug("user session : %s", self.connection_parameters.user_uri)
|
||||
return True
|
||||
|
||||
def logout(self):
|
||||
# Craft full url
|
||||
url = self.connection_parameters.user_uri
|
||||
|
||||
# Craft request header
|
||||
header = {"Content-type": "application/json",
|
||||
"x-auth-token": self.connection_parameters.auth_token
|
||||
}
|
||||
|
||||
logout = requests.delete(url, headers=header,
|
||||
verify=self.connection_parameters.verify_cert
|
||||
)
|
||||
|
||||
if logout.status_code == 200:
|
||||
config.logger.info("Logout successful")
|
||||
else:
|
||||
config.logger.error("Logout failed")
|
||||
raise exception.LogoutFailureException("Fail to logout properly.")
|
||||
|
||||
|
||||
class ConnectionParameters(object):
|
||||
"""Store connection parameters."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def rooturl(self):
|
||||
return self.__rooturl
|
||||
|
||||
@rooturl.setter
|
||||
def rooturl(self, rooturl):
|
||||
self.__rooturl = rooturl
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
return self.__user_name
|
||||
|
||||
@user_name.setter
|
||||
def user_name(self, user_name):
|
||||
self.__user_name = user_name
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
return self.__password
|
||||
|
||||
@password.setter
|
||||
def password(self, password):
|
||||
self.__password = password
|
||||
|
||||
@property
|
||||
def enforceSSL(self):
|
||||
return self.__enforceSSL
|
||||
|
||||
@enforceSSL.setter
|
||||
def enforceSSL(self, enforceSSL):
|
||||
self.__enforceSSL = enforceSSL
|
||||
|
||||
@property
|
||||
def verify_cert(self):
|
||||
return self.__verify_cert
|
||||
|
||||
@verify_cert.setter
|
||||
def verify_cert(self, verify_cert):
|
||||
self.__verify_cert = verify_cert
|
||||
|
||||
@property
|
||||
def auth_token(self):
|
||||
return self.__auth_token
|
||||
|
||||
@auth_token.setter
|
||||
def auth_token(self, auth_token):
|
||||
self.__auth_token = auth_token
|
||||
|
||||
@property
|
||||
def user_uri(self):
|
||||
return self.__user_uri
|
||||
|
||||
@user_uri.setter
|
||||
def user_uri(self, user_uri):
|
||||
self.__user_uri = user_uri
|
31
redfish/mapping.py
Normal file
31
redfish/mapping.py
Normal file
@ -0,0 +1,31 @@
|
||||
# coding=utf-8
|
||||
|
||||
redfish_mapper = None
|
||||
redfish_version = None
|
||||
|
||||
class RedfishVersionMapping(object):
|
||||
"""Implements basic url path mapping beetween Redfish versions."""
|
||||
|
||||
def __init__(self, version):
|
||||
self.__version = version
|
||||
|
||||
def map_sessionservice(self):
|
||||
if self.__version == "0.95":
|
||||
return "Sessions"
|
||||
return("SessionService")
|
||||
|
||||
|
||||
def map_links(self):
|
||||
if self.__version == "0.95":
|
||||
return "links"
|
||||
return("Links")
|
||||
|
||||
def map_links_ref(self):
|
||||
if self.__version == "0.95":
|
||||
return "href"
|
||||
return("@odata.id")
|
||||
|
||||
def map_members(self):
|
||||
if self.__version == "0.95":
|
||||
return "Member"
|
||||
return("Members")
|
197
redfish/old/types.py
Normal file
197
redfish/old/types.py
Normal file
@ -0,0 +1,197 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
"""
|
||||
Redfish Resource Types
|
||||
"""
|
||||
|
||||
import base64
|
||||
import gzip
|
||||
import hashlib
|
||||
import httplib
|
||||
import json
|
||||
import ssl
|
||||
import StringIO
|
||||
import sys
|
||||
import urllib2
|
||||
from urlparse import urlparse
|
||||
|
||||
#from oslo_log import log as logging
|
||||
from redfish import exception
|
||||
|
||||
#LOG = logging.getLogger('redfish')
|
||||
|
||||
|
||||
class Base(object):
|
||||
def __init__(self, obj, connection=None):
|
||||
self._conn = connection
|
||||
"""handle to the redfish connection"""
|
||||
|
||||
self._attrs = []
|
||||
"""list of discovered attributes"""
|
||||
|
||||
self._links = []
|
||||
"""list of linked resources"""
|
||||
|
||||
# parse the individual resources, appending them to
|
||||
# the list of object attributes
|
||||
for k in obj.keys():
|
||||
ref = k.lower()
|
||||
if ref in ["links", "oem", "items"]:
|
||||
continue
|
||||
setattr(self, ref, obj[k])
|
||||
self._attrs.append(ref)
|
||||
|
||||
# make sure the required attributes are present
|
||||
if not getattr(self, 'name', False):
|
||||
raise ObjectLoadException(
|
||||
"Failed to load object. Reason: could not determine name.")
|
||||
if not getattr(self, 'type', False):
|
||||
raise ObjectLoadException(
|
||||
"Failed to load object. Reason: could not determine type.")
|
||||
|
||||
if getattr(self, 'serviceversion', False):
|
||||
self.type = self.type.replace('.' + self.serviceversion, '')
|
||||
else:
|
||||
# TODO: use a regex here to strip and store the version
|
||||
# instead of assuming it is 7 chars long
|
||||
self.type = self.type[:-7]
|
||||
|
||||
# Lastly, parse the 'links' resource.
|
||||
# Note that this may have different nested structure, depending on
|
||||
# what type of resource this is, or what vendor it is.
|
||||
# subclasses may follow this by parsing other resources / collections
|
||||
self._parse_links(obj)
|
||||
|
||||
def _parse_links(self, obj):
|
||||
"""Map linked resources to getter functions
|
||||
|
||||
The root resource returns a dict of links to top-level resources
|
||||
"""
|
||||
def getter(connection, href):
|
||||
def _get():
|
||||
return connection.rest_get(href, {})
|
||||
return _get
|
||||
|
||||
for k in obj['links']:
|
||||
ref = "get_" + k.lower()
|
||||
self._links.append(ref)
|
||||
href = obj['links'][k]['href']
|
||||
setattr(self, ref, getter(self._conn, href))
|
||||
|
||||
def __repr__(self):
|
||||
"""Return this object's _attrs as a dict"""
|
||||
res = {}
|
||||
for a in self._attrs:
|
||||
res[a] = getattr(self, a)
|
||||
return res
|
||||
|
||||
def __str__(self):
|
||||
"""Return the string representation of this object's _attrs"""
|
||||
return json.dumps(self.__repr__())
|
||||
|
||||
|
||||
class BaseCollection(Base):
|
||||
"""Base class for collection types"""
|
||||
def __init__(self, obj, connection=None):
|
||||
super(BaseCollection, self).__init__(obj, connection=connection)
|
||||
self._parse_items(obj)
|
||||
self._attrs.append('items')
|
||||
|
||||
def _parse_links(self, obj):
|
||||
"""links are special on a chassis; dont parse them"""
|
||||
pass
|
||||
|
||||
def _parse_items(self, obj):
|
||||
"""Map linked items to getter methods
|
||||
|
||||
The chassis resource returns a list of items and corresponding
|
||||
link data in a separate entity.
|
||||
"""
|
||||
def getter(connection, href):
|
||||
def _get():
|
||||
return connection.rest_get(href, {})
|
||||
return _get
|
||||
|
||||
self.items = []
|
||||
self._item_getters = []
|
||||
|
||||
if 'links' in obj and 'Member' in obj['links']:
|
||||
# NOTE: this assumes the lists are ordered the same
|
||||
counter = 0
|
||||
for item in obj['links']['Member']:
|
||||
self.items.append(obj['Items'][counter])
|
||||
self._item_getters.append(
|
||||
getter(self._conn, item['href']))
|
||||
counter+=1
|
||||
elif 'Items' in obj:
|
||||
# TODO: find an example of this format and make sure it works
|
||||
for item in obj['Items']:
|
||||
if 'links' in item and 'self' in item['links']:
|
||||
href = item['links']['self']['href']
|
||||
self.items.append(item)
|
||||
|
||||
# TODO: implement paging support
|
||||
# if 'links' in obj and 'NextPage' in obj['links']:
|
||||
# next_page = THIS_URI + '?page=' + str(obj['links']['NextPage']['page'])
|
||||
# do something with next_page URI
|
||||
|
||||
def __iter__(self):
|
||||
for getter in self._item_getters:
|
||||
yield getter()
|
||||
|
||||
|
||||
class Root(Base):
|
||||
"""Root '/' resource class"""
|
||||
def _parse_links(self, obj):
|
||||
"""Map linked resources to getter functions
|
||||
|
||||
The root resource returns a dict of links to top-level resources
|
||||
|
||||
TODO: continue implementing customizations for top-level resources
|
||||
|
||||
"""
|
||||
mapping = {
|
||||
'Systems': Systems,
|
||||
'Chassis': Chassis,
|
||||
'Managers': Base,
|
||||
'Schemas': Base,
|
||||
'Registries': Base,
|
||||
'Tasks': Base,
|
||||
'AccountService': Base,
|
||||
'Sessions': Base,
|
||||
'EventService': Base,
|
||||
}
|
||||
|
||||
def getter(connection, href, type):
|
||||
def _get():
|
||||
return mapping[type](connection.rest_get(href, {}), self._conn)
|
||||
return _get
|
||||
|
||||
for k in obj['links']:
|
||||
ref = "get_" + k.lower()
|
||||
self._links.append(ref)
|
||||
href = obj['links'][k]['href']
|
||||
setattr(self, ref, getter(self._conn, href, k))
|
||||
|
||||
|
||||
class Chassis(BaseCollection):
|
||||
"""Chassis resource class"""
|
||||
def __len__(self):
|
||||
return len(self.items)
|
||||
|
||||
|
||||
class Systems(Base):
|
||||
pass
|
@ -1,412 +0,0 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
"""
|
||||
STARTING ASSUMPTIONS
|
||||
|
||||
On URIs:
|
||||
|
||||
The Redfish RESTful API is a "hypermedia API" by design. This is to avoid
|
||||
building in restrictive assumptions to the data model that will make it
|
||||
difficult to adapt to future hardware implementations. A hypermedia API avoids
|
||||
these assumptions by making the data model discoverable via links between
|
||||
resources.
|
||||
|
||||
A URI should be treated by the client as opaque, and thus should not be
|
||||
attempted to be understood or deconstructed by the client. Only specific top
|
||||
level URIs (any URI in this sample code) may be assumed, and even these may be
|
||||
absent based upon the implementation (e.g. there might be no /redfish/v1/Systems
|
||||
collection on something that doesn't have compute nodes.)
|
||||
|
||||
The other URIs must be discovered dynamically by following href links. This is
|
||||
because the API will eventually be implemented on a system that breaks any
|
||||
existing data model "shape" assumptions we may make now. In particular,
|
||||
clients should not make assumptions about the URIs for the resource members of
|
||||
a collection. For instance, the URI of a collection member will NOT always be
|
||||
/redfish/v1/.../collection/1, or 2. On systems with multiple compute nodes per
|
||||
manager, a System collection member might be /redfish/v1/Systems/C1N1.
|
||||
|
||||
This sounds very complicated, but in reality (as these examples demonstrate),
|
||||
if you are looking for specific items, the traversal logic isn't too
|
||||
complicated.
|
||||
|
||||
On Resource Model Traversal:
|
||||
|
||||
Although the resources in the data model are linked together, because of cross
|
||||
link references between resources, a client may not assume the resource model
|
||||
is a tree. It is a graph instead, so any crawl of the data model should keep
|
||||
track of visited resources to avoid an infinite traversal loop.
|
||||
|
||||
A reference to another resource is any property called "href" no matter where
|
||||
it occurs in a resource.
|
||||
|
||||
An external reference to a resource outside the data model is referred to by a
|
||||
property called "extref". Any resource referred to by extref should not be
|
||||
assumed to follow the conventions of the API.
|
||||
|
||||
On Resource Versions:
|
||||
|
||||
Each resource has a "Type" property with a value of the format Tyepname.x.y.z
|
||||
where
|
||||
* x = major version - incrementing this is a breaking change to the schema y =
|
||||
* minor version - incrementing this is a non-breaking additive change to the
|
||||
* schema z = errata - non-breaking change
|
||||
|
||||
Because all resources are versioned and schema also have a version, it is
|
||||
possible to design rules for "nearest" match (e.g. if you are interacting with
|
||||
multiple services using a common batch of schema files). The mechanism is not
|
||||
prescribed, but a client should be prepared to encounter both older and newer
|
||||
versions of resource types.
|
||||
|
||||
On HTTP POST to create:
|
||||
|
||||
WHen POSTing to create a resource (e.g. create an account or session) the
|
||||
guarantee is that a successful response includes a "Location" HTTP header
|
||||
indicating the resource URI of the newly created resource. The POST may also
|
||||
include a representation of the newly created object in a JSON response body
|
||||
but may not. Do not assume the response body, but test it. It may also be an
|
||||
ExtendedError object.
|
||||
|
||||
HTTP REDIRECT:
|
||||
|
||||
All clients must correctly handle HTTP redirect. We (or Redfish) may
|
||||
eventually need to use redirection as a way to alias portions of the data
|
||||
model.
|
||||
|
||||
FUTURE: Asynchronous tasks
|
||||
|
||||
In the future some operations may start asynchonous tasks. In this case, the
|
||||
client should recognized and handle HTTP 202 if needed and the 'Location'
|
||||
header will point to a resource with task information and status.
|
||||
|
||||
JSON-SCHEMA:
|
||||
|
||||
The json-schema available at /redfish/v1/Schemas governs the content of the
|
||||
resources, but keep in mind:
|
||||
* not every property in the schema is implemented in every implementation.
|
||||
* some properties are schemed to allow both null and anotehr type like string
|
||||
* or integer.
|
||||
|
||||
Robust client code should check both the existence and type of interesting
|
||||
properties and fail gracefully if expectations are not met.
|
||||
|
||||
GENERAL ADVICE:
|
||||
|
||||
Clients should always be prepared for:
|
||||
* unimplemented properties (e.g. a property doesn't apply in a particular case)
|
||||
* null values in some cases if the value of a property is not currently known
|
||||
* due to system conditions HTTP status codes other than 200 OK. Can your code
|
||||
* handle an HTTP 500 Internal Server Error with no other info? URIs are case
|
||||
* insensitive HTTP header names are case insensitive JSON Properties and Enum
|
||||
* values are case sensitive A client should be tolerant of any set of HTTP
|
||||
* headers the service returns
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import gzip
|
||||
import hashlib
|
||||
import httplib
|
||||
import json
|
||||
import ssl
|
||||
import StringIO
|
||||
import sys
|
||||
import urllib2
|
||||
from urlparse import urlparse
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from redfish import exception
|
||||
from redfish import types
|
||||
|
||||
|
||||
LOG = logging.getLogger('redfish')
|
||||
|
||||
|
||||
def connect(host, user, password):
|
||||
return RedfishConnection(host, user, password)
|
||||
|
||||
|
||||
class RedfishConnection(object):
|
||||
"""Implements basic connection handling for Redfish APIs."""
|
||||
|
||||
def __init__(self, host, user_name, password,
|
||||
auth_token=None, enforce_SSL=True):
|
||||
"""Initialize a connection to a Redfish service."""
|
||||
super(RedfishConnection, self).__init__()
|
||||
|
||||
self.user_name = user_name
|
||||
self.password = password
|
||||
self.auth_token = auth_token
|
||||
self.enforce_SSL = enforce_SSL
|
||||
|
||||
# context for the last status and header returned from a call
|
||||
self.status = None
|
||||
self.headers = None
|
||||
|
||||
# If the http schema wasn't specified, default to HTTPS
|
||||
if host[0:4] != 'http':
|
||||
host = 'https://' + host
|
||||
self.host = host
|
||||
|
||||
self._connect()
|
||||
|
||||
if not self.auth_token:
|
||||
# TODO: if a token is returned by this call, cache it. However,
|
||||
# the sample HTML does not include any token data, so it's unclear
|
||||
# what we should do here.
|
||||
LOG.debug('Initiating session with host %s', self.host)
|
||||
auth_dict = {'Password': self.password, 'UserName': self.user_name}
|
||||
response = self.rest_post(
|
||||
'/redfish/v1/Sessions', None, json.dumps(auth_dict))
|
||||
|
||||
# TODO: do some schema discovery here and cache the result
|
||||
# self.schema = ...
|
||||
LOG.info('Connection established to host %s', self.host)
|
||||
|
||||
def _connect(self):
|
||||
LOG.debug("Establishing connection to host %s", self.host)
|
||||
url = urlparse(self.host)
|
||||
if url.scheme == 'https':
|
||||
# New in Python 2.7.9, SSL enforcement is defaulted on.
|
||||
# It can be opted-out of, which might be useful for debugging
|
||||
# some things. The below case is the Opt-Out condition and
|
||||
# should be used with GREAT caution.
|
||||
if (sys.version_info.major == 2
|
||||
and sys.version_info.minor == 7
|
||||
and sys.version_info.micro >= 9
|
||||
and self.enforce_SSL == False):
|
||||
cont = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
|
||||
cont.verify_mode = ssl.CERT_NONE
|
||||
self.connection = httplib.HTTPSConnection(
|
||||
host=url.netloc, strict=True, context=cont)
|
||||
else:
|
||||
self.connection = httplib.HTTPSConnection(
|
||||
host=url.netloc, strict=True)
|
||||
elif url.scheme == 'http':
|
||||
self.connection = httplib.HTTPConnection(
|
||||
host=url.netloc, strict=True)
|
||||
else:
|
||||
raise exception.RedfishException(
|
||||
message='Unknown connection schema')
|
||||
|
||||
def _op(self, operation, suburi, request_headers=None, request_body=None):
|
||||
"""
|
||||
REST operation generic handler
|
||||
|
||||
:param operation: GET, POST, etc
|
||||
:param suburi: the URI path to the resource
|
||||
:param request_headers: optional dict of headers
|
||||
:param request_body: optional JSON body
|
||||
"""
|
||||
# ensure trailing slash
|
||||
if suburi[-1:] != '/':
|
||||
suburi = suburi + '/'
|
||||
url = urlparse(self.host + suburi)
|
||||
|
||||
if not isinstance(request_headers, dict):
|
||||
request_headers = dict()
|
||||
request_headers['Content-Type'] = 'application/json'
|
||||
|
||||
# if X-Auth-Token specified, supply it instead of basic auth
|
||||
if self.auth_token is not None:
|
||||
request_headers['X-Auth-Token'] = self.auth_token
|
||||
# else use user_name/password and Basic Auth
|
||||
elif self.user_name is not None and self.password is not None:
|
||||
request_headers['Authorization'] = ("BASIC " + base64.b64encode(
|
||||
self.user_name + ":" + self.password))
|
||||
# TODO: add support for other types of auth
|
||||
|
||||
redir_count = 4
|
||||
while redir_count:
|
||||
# NOTE: Do not assume every HTTP operation will return a JSON body.
|
||||
# For example, ExtendedError structures are only required for
|
||||
# HTTP 400 errors and are optional elsewhere as they are mostly
|
||||
# redundant for many of the other HTTP status code. In particular,
|
||||
# 200 OK responses should not have to return any body.
|
||||
self.connection.request(operation, url.path,
|
||||
headers=request_headers, body=json.dumps(request_body))
|
||||
resp = self.connection.getresponse()
|
||||
body = resp.read()
|
||||
# NOTE: this makes sure the headers names are all lower case
|
||||
# because HTTP says they are case insensitive
|
||||
headers = dict((x.lower(), y) for x, y in resp.getheaders())
|
||||
|
||||
# Follow HTTP redirect
|
||||
if resp.status == 301 and 'location' in headers:
|
||||
url = urlparse(headers['location'])
|
||||
# TODO: cache these redirects
|
||||
LOG.debug("Following redirect to %s", headers['location'])
|
||||
redir_count -= 1
|
||||
else:
|
||||
break
|
||||
|
||||
response = dict()
|
||||
try:
|
||||
response = json.loads(body.decode('utf-8'))
|
||||
except ValueError: # if it doesn't decode as json
|
||||
# NOTE: resources may return gzipped content, so try to decode
|
||||
# as gzip (we should check the headers for Content-Encoding=gzip)
|
||||
try:
|
||||
gzipper = gzip.GzipFile(fileobj=StringIO.StringIO(body))
|
||||
uncompressed_string = gzipper.read().decode('UTF-8')
|
||||
response = json.loads(uncompressed_string)
|
||||
except:
|
||||
raise exception.RedfishException(message=
|
||||
'Failed to parse response as a JSON document, '
|
||||
'received "%s".' % body)
|
||||
|
||||
self.status = resp.status
|
||||
self.headers = headers
|
||||
return response
|
||||
|
||||
def rest_get(self, suburi, request_headers):
|
||||
"""REST GET
|
||||
|
||||
:param: suburi
|
||||
:param: request_headers
|
||||
"""
|
||||
# NOTE: be prepared for various HTTP responses including 500, 404, etc
|
||||
return self._op('GET', suburi, request_headers, None)
|
||||
|
||||
def rest_patch(self, suburi, request_headers, request_body):
|
||||
"""REST PATCH
|
||||
|
||||
:param: suburi
|
||||
:param: request_headers
|
||||
:param: request_body
|
||||
NOTE: this body is a dict, not a JSONPATCH document.
|
||||
redfish does not follow IETF JSONPATCH standard
|
||||
https://tools.ietf.org/html/rfc6902
|
||||
"""
|
||||
# NOTE: be prepared for various HTTP responses including 500, 404, 202
|
||||
return self._op('PATCH', suburi, request_headers, request_body)
|
||||
|
||||
def rest_put(self, suburi, request_headers, request_body):
|
||||
"""REST PUT
|
||||
|
||||
:param: suburi
|
||||
:param: request_headers
|
||||
:param: request_body
|
||||
"""
|
||||
# NOTE: be prepared for various HTTP responses including 500, 404, 202
|
||||
return self._op('PUT', suburi, request_headers, request_body)
|
||||
|
||||
def rest_post(self, suburi, request_headers, request_body):
|
||||
"""REST POST
|
||||
|
||||
:param: suburi
|
||||
:param: request_headers
|
||||
:param: request_body
|
||||
"""
|
||||
# NOTE: don't assume any newly created resource is included in the
|
||||
# response. Only the Location header matters.
|
||||
# the response body may be the new resource, it may be an
|
||||
# ExtendedError, or it may be empty.
|
||||
return self._op('POST', suburi, request_headers, request_body)
|
||||
|
||||
def rest_delete(self, suburi, request_headers):
|
||||
"""REST DELETE
|
||||
|
||||
:param: suburi
|
||||
:param: request_headers
|
||||
"""
|
||||
# NOTE: be prepared for various HTTP responses including 500, 404
|
||||
# NOTE: response may be an ExtendedError or may be empty
|
||||
return self._op('DELETE', suburi, request_headers, None)
|
||||
|
||||
def get_root(self):
|
||||
return types.Root(self.rest_get('/redfish/v1', {}), connection=self)
|
||||
|
||||
|
||||
class Version(object):
|
||||
def __init__(self, string):
|
||||
try:
|
||||
buf = string.split('.')
|
||||
if len(buf) < 2:
|
||||
raise AttributeError
|
||||
except AttributeError:
|
||||
raise RedfishException(message="Failed to parse version string")
|
||||
self.major = int(buf[0])
|
||||
self.minor = int(buf[1])
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.major) + '.' + str(self.minor)
|
||||
|
||||
|
||||
# return the type of an object (down to the major version, skipping minor, and errata)
|
||||
def get_type(obj):
|
||||
typever = obj['Type']
|
||||
typesplit = typever.split('.')
|
||||
return typesplit[0] + '.' + typesplit[1]
|
||||
|
||||
|
||||
# checks HTTP response headers for specified operation (e.g. 'GET' or 'PATCH')
|
||||
def operation_allowed(headers_dict, operation):
|
||||
if 'allow' in headers_dict:
|
||||
if headers_dict['allow'].find(operation) != -1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# Message registry support
|
||||
# XXX not supported yet
|
||||
message_registries = {}
|
||||
|
||||
|
||||
# Build a list of decoded messages from the extended_error using the message
|
||||
# registries An ExtendedError JSON object is a response from the with its own
|
||||
# schema. This function knows how to parse the ExtendedError object and, using
|
||||
# any loaded message registries, render an array of plain language strings that
|
||||
# represent the response.
|
||||
def render_extended_error_message_list(extended_error):
|
||||
messages = []
|
||||
if isinstance(extended_error, dict):
|
||||
if 'Type' in extended_error and extended_error['Type'].startswith('ExtendedError.'):
|
||||
for msg in extended_error['Messages']:
|
||||
MessageID = msg['MessageID']
|
||||
x = MessageID.split('.')
|
||||
registry = x[0]
|
||||
msgkey = x[len(x) - 1]
|
||||
|
||||
# if the correct message registry is loaded, do string resolution
|
||||
if registry in message_registries:
|
||||
if registry in message_registries and msgkey in message_registries[registry]['Messages']:
|
||||
msg_dict = message_registries[registry]['Messages'][msgkey]
|
||||
msg_str = MessageID + ': ' + msg_dict['Message']
|
||||
|
||||
for argn in range(0, msg_dict['NumberOfArgs']):
|
||||
subst = '%' + str(argn+1)
|
||||
msg_str = msg_str.replace(subst, str(msg['MessageArgs'][argn]))
|
||||
|
||||
if 'Resolution' in msg_dict and msg_dict['Resolution'] != 'None':
|
||||
msg_str += ' ' + msg_dict['Resolution']
|
||||
|
||||
messages.append(msg_str)
|
||||
else: # no message registry, simply return the msg object in string form
|
||||
messages.append('No Message Registry Info: '+ str(msg))
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
# Print a list of decoded messages from the extended_error using the message registries
|
||||
def print_extended_error(extended_error):
|
||||
messages = render_extended_error_message_list(extended_error)
|
||||
msgcnt = 0
|
||||
for msg in messages:
|
||||
print('\t' + msg)
|
||||
msgcnt += 1
|
||||
if msgcnt == 0: # add a spacer
|
||||
print
|
334
redfish/types.py
334
redfish/types.py
@ -1,197 +1,215 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
# coding=utf-8
|
||||
|
||||
import pprint
|
||||
from urlparse import urljoin
|
||||
import requests
|
||||
import tortilla
|
||||
import config
|
||||
import mapping
|
||||
|
||||
"""
|
||||
Redfish Resource Types
|
||||
"""
|
||||
|
||||
import base64
|
||||
import gzip
|
||||
import hashlib
|
||||
import httplib
|
||||
import json
|
||||
import ssl
|
||||
import StringIO
|
||||
import sys
|
||||
import urllib2
|
||||
from urlparse import urlparse
|
||||
|
||||
from oslo_log import log as logging
|
||||
from redfish import exception
|
||||
|
||||
LOG = logging.getLogger('redfish')
|
||||
# Global variable
|
||||
|
||||
|
||||
class Base(object):
|
||||
def __init__(self, obj, connection=None):
|
||||
self._conn = connection
|
||||
"""handle to the redfish connection"""
|
||||
"""Abstract class to manage types (Chassis, Servers etc...)."""
|
||||
|
||||
self._attrs = []
|
||||
"""list of discovered attributes"""
|
||||
def __init__(self, url, connection_parameters):
|
||||
global TORTILLADEBUG
|
||||
self.connection_parameters = connection_parameters # Uggly hack to check
|
||||
self.url = url
|
||||
self.api_url = tortilla.wrap(url, debug=config.TORTILLADEBUG)
|
||||
|
||||
self._links = []
|
||||
"""list of linked resources"""
|
||||
|
||||
# parse the individual resources, appending them to
|
||||
# the list of object attributes
|
||||
for k in obj.keys():
|
||||
ref = k.lower()
|
||||
if ref in ["links", "oem", "items"]:
|
||||
continue
|
||||
setattr(self, ref, obj[k])
|
||||
self._attrs.append(ref)
|
||||
|
||||
# make sure the required attributes are present
|
||||
if not getattr(self, 'name', False):
|
||||
raise ObjectLoadException(
|
||||
"Failed to load object. Reason: could not determine name.")
|
||||
if not getattr(self, 'type', False):
|
||||
raise ObjectLoadException(
|
||||
"Failed to load object. Reason: could not determine type.")
|
||||
|
||||
if getattr(self, 'serviceversion', False):
|
||||
self.type = self.type.replace('.' + self.serviceversion, '')
|
||||
try:
|
||||
if connection_parameters.auth_token == None:
|
||||
self.data = self.api_url.get(verify=connection_parameters.verify_cert)
|
||||
else:
|
||||
# TODO: use a regex here to strip and store the version
|
||||
# instead of assuming it is 7 chars long
|
||||
self.type = self.type[:-7]
|
||||
self.data = self.api_url.get(verify=connection_parameters.verify_cert,
|
||||
headers={'x-auth-token': connection_parameters.auth_token}
|
||||
)
|
||||
except requests.ConnectionError as e:
|
||||
print e
|
||||
# Log and transmit the exception.
|
||||
config.logger.error("Connection error : %s", e)
|
||||
raise e
|
||||
print self.data
|
||||
|
||||
# Lastly, parse the 'links' resource.
|
||||
# Note that this may have different nested structure, depending on
|
||||
# what type of resource this is, or what vendor it is.
|
||||
# subclasses may follow this by parsing other resources / collections
|
||||
self._parse_links(obj)
|
||||
def get_link_url(self, link_type):
|
||||
"""Need to be explained.
|
||||
|
||||
def _parse_links(self, obj):
|
||||
"""Map linked resources to getter functions
|
||||
:param redfish_logfile: redfish log
|
||||
:type str
|
||||
:returns: True
|
||||
|
||||
The root resource returns a dict of links to top-level resources
|
||||
"""
|
||||
def getter(connection, href):
|
||||
def _get():
|
||||
return connection.rest_get(href, {})
|
||||
return _get
|
||||
self.links=[]
|
||||
|
||||
for k in obj['links']:
|
||||
ref = "get_" + k.lower()
|
||||
self._links.append(ref)
|
||||
href = obj['links'][k]['href']
|
||||
setattr(self, ref, getter(self._conn, href))
|
||||
# Manage standard < 1.0
|
||||
if float(mapping.redfish_version) < 1.00:
|
||||
links = getattr(self.data, mapping.redfish_mapper.map_links())
|
||||
if link_type in links:
|
||||
return urljoin(self.url, links[link_type][mapping.redfish_mapper.map_links_ref()])
|
||||
else:
|
||||
links = getattr(self.data, link_type)
|
||||
link = getattr(links, mapping.redfish_mapper.map_links_ref())
|
||||
return urljoin(self.url, link)
|
||||
|
||||
def __repr__(self):
|
||||
"""Return this object's _attrs as a dict"""
|
||||
res = {}
|
||||
for a in self._attrs:
|
||||
res[a] = getattr(self, a)
|
||||
return res
|
||||
@property
|
||||
def url(self):
|
||||
return self.__url
|
||||
|
||||
def __str__(self):
|
||||
"""Return the string representation of this object's _attrs"""
|
||||
return json.dumps(self.__repr__())
|
||||
@url.setter
|
||||
def url(self, url):
|
||||
self.__url = url
|
||||
|
||||
|
||||
class BaseCollection(Base):
|
||||
"""Base class for collection types"""
|
||||
def __init__(self, obj, connection=None):
|
||||
super(BaseCollection, self).__init__(obj, connection=connection)
|
||||
self._parse_items(obj)
|
||||
self._attrs.append('items')
|
||||
"""Abstract class to manage collection (Chassis, Servers etc...)."""
|
||||
|
||||
def _parse_links(self, obj):
|
||||
"""links are special on a chassis; dont parse them"""
|
||||
pass
|
||||
def __init__(self, url, connection_parameters):
|
||||
super(BaseCollection, self).__init__(url, connection_parameters)
|
||||
|
||||
def _parse_items(self, obj):
|
||||
"""Map linked items to getter methods
|
||||
self.links=[]
|
||||
|
||||
The chassis resource returns a list of items and corresponding
|
||||
link data in a separate entity.
|
||||
"""
|
||||
def getter(connection, href):
|
||||
def _get():
|
||||
return connection.rest_get(href, {})
|
||||
return _get
|
||||
|
||||
self.items = []
|
||||
self._item_getters = []
|
||||
#linksmembers = self.data.Links.Members
|
||||
#linksmembers = self.data.links.Member
|
||||
if float(mapping.redfish_version) < 1.00:
|
||||
linksmembers = getattr(self.data, mapping.redfish_mapper.map_links())
|
||||
linksmembers = getattr(linksmembers, mapping.redfish_mapper.map_members())
|
||||
else:
|
||||
linksmembers = getattr(self.data, mapping.redfish_mapper.map_members())
|
||||
for link in linksmembers:
|
||||
#self.links.append(getattr(link,"@odata.id"))
|
||||
#self.links.append(getattr(link,"href"))
|
||||
self.links.append(urljoin(self.url, getattr(link, mapping.redfish_mapper.map_links_ref())))
|
||||
|
||||
if 'links' in obj and 'Member' in obj['links']:
|
||||
# NOTE: this assumes the lists are ordered the same
|
||||
counter = 0
|
||||
for item in obj['links']['Member']:
|
||||
self.items.append(obj['Items'][counter])
|
||||
self._item_getters.append(
|
||||
getter(self._conn, item['href']))
|
||||
counter+=1
|
||||
elif 'Items' in obj:
|
||||
# TODO: find an example of this format and make sure it works
|
||||
for item in obj['Items']:
|
||||
if 'links' in item and 'self' in item['links']:
|
||||
href = item['links']['self']['href']
|
||||
self.items.append(item)
|
||||
|
||||
# TODO: implement paging support
|
||||
# if 'links' in obj and 'NextPage' in obj['links']:
|
||||
# next_page = THIS_URI + '?page=' + str(obj['links']['NextPage']['page'])
|
||||
# do something with next_page URI
|
||||
|
||||
def __iter__(self):
|
||||
for getter in self._item_getters:
|
||||
yield getter()
|
||||
print self.links
|
||||
|
||||
|
||||
class Root(Base):
|
||||
"""Root '/' resource class"""
|
||||
def _parse_links(self, obj):
|
||||
"""Map linked resources to getter functions
|
||||
"""Class to manage redfish Root data."""
|
||||
|
||||
The root resource returns a dict of links to top-level resources
|
||||
|
||||
TODO: continue implementing customizations for top-level resources
|
||||
def get_api_version(self):
|
||||
"""Return api version.
|
||||
|
||||
:returns: string -- version
|
||||
:raises: AttributeError
|
||||
|
||||
"""
|
||||
mapping = {
|
||||
'Systems': Systems,
|
||||
'Chassis': Chassis,
|
||||
'Managers': Base,
|
||||
'Schemas': Base,
|
||||
'Registries': Base,
|
||||
'Tasks': Base,
|
||||
'AccountService': Base,
|
||||
'Sessions': Base,
|
||||
'EventService': Base,
|
||||
}
|
||||
try:
|
||||
version = self.data.RedfishVersion
|
||||
except AttributeError:
|
||||
version = self.data.ServiceVersion
|
||||
|
||||
def getter(connection, href, type):
|
||||
def _get():
|
||||
return mapping[type](connection.rest_get(href, {}), self._conn)
|
||||
return _get
|
||||
version = version.replace('.', '')
|
||||
version = version[0] + '.' + version[1:]
|
||||
return(version)
|
||||
|
||||
for k in obj['links']:
|
||||
ref = "get_" + k.lower()
|
||||
self._links.append(ref)
|
||||
href = obj['links'][k]['href']
|
||||
setattr(self, ref, getter(self._conn, href, k))
|
||||
def get_api_UUID(self):
|
||||
return self.data.UUID
|
||||
|
||||
|
||||
class Chassis(BaseCollection):
|
||||
"""Chassis resource class"""
|
||||
def __len__(self):
|
||||
return len(self.items)
|
||||
def get_api_link_to_server(self):
|
||||
"""Return api link to server.
|
||||
|
||||
:returns: string -- path
|
||||
|
||||
"""
|
||||
return getattr(self.root.Links.Systems, "@odata.id")
|
||||
|
||||
|
||||
class SessionService(Base):
|
||||
"""Class to manage redfish SessionService data."""
|
||||
pass
|
||||
|
||||
|
||||
class Managers(Base):
|
||||
def __init__(self, url, connection_parameters):
|
||||
super(Managers, self).__init__(url, connection_parameters)
|
||||
|
||||
try:
|
||||
|
||||
# self.ethernet_interfaces_collection = EthernetInterfacesCollection(
|
||||
# self.get_link_url("EthernetInterfaces"),
|
||||
# connection_parameters
|
||||
# )
|
||||
|
||||
# Works on proliant, need to treat 095 vs 0.96 differences
|
||||
self.ethernet_interfaces_collection = EthernetInterfacesCollection(
|
||||
self.get_link_url("EthernetNICs"),
|
||||
connection_parameters
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class ManagersCollection(BaseCollection):
|
||||
"""Class to manage redfish ManagersCollection data."""
|
||||
def __init__(self, url, connection_parameters):
|
||||
super(ManagersCollection, self).__init__(url, connection_parameters)
|
||||
|
||||
self.managers_list = []
|
||||
|
||||
for link in self.links:
|
||||
self.managers_list.append(Managers(link, connection_parameters))
|
||||
|
||||
|
||||
|
||||
class Systems(Base):
|
||||
# TODO : Need to discuss with Bruno the required method.
|
||||
# Also to check with the ironic driver requirement.
|
||||
def __init__(self, url, connection_parameters):
|
||||
super(Systems, self).__init__(url, connection_parameters)
|
||||
|
||||
def reset_system(self):
|
||||
# Craft the request
|
||||
action = dict()
|
||||
action['Action'] = 'Reset'
|
||||
action['ResetType'] = 'ForceRestart'
|
||||
|
||||
# perform the POST action
|
||||
print self.api_url
|
||||
response = self.api_url.post(verify=self.connection_parameters.verify_cert,
|
||||
headers={'x-auth-token': self.connection_parameters.auth_token},
|
||||
data=action
|
||||
)
|
||||
#TODO : treat response.
|
||||
|
||||
def get_bios_version(self):
|
||||
try:
|
||||
# Returned by proliant
|
||||
return self.data.Bios.Current.VersionString
|
||||
except:
|
||||
# Returned by mockup.
|
||||
# Hopefully this kind of discrepencies will be fixed with Redfish 1.0 (August)
|
||||
return self.data.BiosVersion
|
||||
|
||||
|
||||
class SystemsCollection(BaseCollection):
|
||||
"""Class to manage redfish ManagersCollection data."""
|
||||
def __init__(self, url, connection_parameters):
|
||||
super(SystemsCollection, self).__init__(url, connection_parameters)
|
||||
|
||||
self.systems_list = []
|
||||
|
||||
for link in self.links:
|
||||
self.systems_list.append(Systems(link, connection_parameters))
|
||||
|
||||
|
||||
class EthernetInterfacesCollection(BaseCollection):
|
||||
def __init__(self, url, connection_parameters):
|
||||
super(EthernetInterfacesCollection, self).__init__(url, connection_parameters)
|
||||
|
||||
self.ethernet_interfaces_list = []
|
||||
|
||||
# Url returned by the mock up is wrong /redfish/v1/Managers/EthernetInterfaces/1 returns a 404.
|
||||
# The correct one should be /redfish/v1/Managers/1/EthernetInterfaces/1
|
||||
# Check more than 1 hour for this bug.... grrr....
|
||||
for link in self.links:
|
||||
self.ethernet_interfaces_list.append(EthernetInterfaces(link, connection_parameters))
|
||||
|
||||
|
||||
class EthernetInterfaces(Base):
|
||||
pass
|
@ -3,5 +3,6 @@
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
pbr>=0.6,!=0.7,<1.0
|
||||
oslo.log>=1.0,<2.0
|
||||
#oslo.log>=1.0,<2.0
|
||||
Babel>=1.3
|
||||
tortilla>=0.4.1
|
||||
|
@ -3,8 +3,8 @@ name = python-redfish
|
||||
summary = Reference implementation of Redfish standard client.
|
||||
description-file =
|
||||
README.rst
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
author = Redfish dev team
|
||||
author-email = python-redfish@lists.mondorescue.org
|
||||
home-page = http://www.openstack.org/
|
||||
classifier =
|
||||
Environment :: OpenStack
|
||||
|
@ -8,8 +8,8 @@ coverage>=3.6
|
||||
discover
|
||||
python-subunit>=0.0.18
|
||||
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
|
||||
oslosphinx>=2.2.0 # Apache-2.0
|
||||
oslotest>=1.2.0 # Apache-2.0
|
||||
#oslosphinx>=2.2.0 # Apache-2.0
|
||||
#oslotest>=1.2.0 # Apache-2.0
|
||||
testrepository>=0.0.18
|
||||
testscenarios>=0.4
|
||||
testtools>=0.9.36,!=1.2.0
|
||||
|
Loading…
Reference in New Issue
Block a user