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
|
# PyBuilder
|
||||||
target/
|
target/
|
||||||
|
|
||||||
|
# Pydev
|
||||||
|
.project
|
||||||
|
.pydevproject
|
||||||
|
.settings/
|
||||||
|
.metadata
|
||||||
|
@ -23,6 +23,7 @@ for build and test automation::
|
|||||||
doc/ # documentation
|
doc/ # documentation
|
||||||
doc/source # the doc source files live here
|
doc/source # the doc source files live here
|
||||||
doc/build/html # output of building any docs will go 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
|
examples/ # any sample code using this library, eg. for education
|
||||||
# should be put here
|
# should be put here
|
||||||
redfish/ # the redfish library
|
redfish/ # the redfish library
|
||||||
|
@ -3,7 +3,6 @@ DMTF Redfish specification
|
|||||||
|
|
||||||
This directory contains the current references from the DMTF on the Redfish
|
This directory contains the current references from the DMTF on the Redfish
|
||||||
specification (1.0.0 at the time of the writing)
|
specification (1.0.0 at the time of the writing)
|
||||||
|
|
||||||
In order to ease test, the DMTF has published a mockup environment to simulate
|
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
|
a Redfish based system so it is possible to write programs without real Redfish
|
||||||
compliant hardware platform.
|
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 logging
|
||||||
import sys
|
import sys
|
||||||
from oslo_config import cfg
|
#from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
#from oslo_log import log as logging
|
||||||
|
|
||||||
import redfish
|
import redfish
|
||||||
|
|
||||||
@ -12,9 +12,9 @@ import redfish
|
|||||||
#log_root.addHandler(logging.StreamHandler(sys.stdout))
|
#log_root.addHandler(logging.StreamHandler(sys.stdout))
|
||||||
#log_root.setLevel(logging.DEBUG)
|
#log_root.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
CONF = cfg.CONF
|
#CONF = cfg.CONF
|
||||||
logging.set_defaults(['redfish=DEBUG'])
|
#logging.set_defaults(['redfish=DEBUG'])
|
||||||
logging.register_options(CONF)
|
#logging.register_options(CONF)
|
||||||
#logging.setup(CONF, "redfish")
|
#logging.setup(CONF, "redfish")
|
||||||
|
|
||||||
# Connect to a redfish API endpoint
|
# Connect to a redfish API endpoint
|
||||||
|
@ -28,6 +28,7 @@ system such as defined by http://www.redfishcertification.org
|
|||||||
%install
|
%install
|
||||||
%{__python} setup.py install -O1 --skip-build --root %{buildroot}
|
%{__python} setup.py install -O1 --skip-build --root %{buildroot}
|
||||||
|
|
||||||
|
# TODO: Add examples
|
||||||
%files
|
%files
|
||||||
%doc README.rst examples/*.py
|
%doc README.rst examples/*.py
|
||||||
%dir %{python_sitelib}/redfish
|
%dir %{python_sitelib}/redfish
|
||||||
|
@ -12,11 +12,11 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import pbr.version
|
#import pbr.version
|
||||||
|
|
||||||
import redfish.server
|
from redfish.main import *
|
||||||
import redfish.types
|
#import redfish.types
|
||||||
|
|
||||||
|
|
||||||
__version__ = pbr.version.VersionInfo(
|
#__version__ = pbr.version.VersionInfo(
|
||||||
'redfish').version_string()
|
# '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 -*-
|
# -*- 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):
|
class RedfishException(Exception):
|
||||||
"""Base class for redfish exceptions"""
|
"""Base class for redfish exceptions"""
|
||||||
def __init__(self, message=None, **kwargs):
|
def __init__(self, message=None, **kwargs):
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
self.message = message
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectLoadException(RedfishException):
|
|
||||||
|
class AuthenticationFailureException(RedfishException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class LogoutFailureException(RedfishException):
|
||||||
pass
|
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.
|
# 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 pprint
|
||||||
|
from urlparse import urljoin
|
||||||
|
import requests
|
||||||
|
import tortilla
|
||||||
|
import config
|
||||||
|
import mapping
|
||||||
|
|
||||||
"""
|
# Global variable
|
||||||
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):
|
class Base(object):
|
||||||
def __init__(self, obj, connection=None):
|
"""Abstract class to manage types (Chassis, Servers etc...)."""
|
||||||
self._conn = connection
|
|
||||||
"""handle to the redfish connection"""
|
|
||||||
|
|
||||||
self._attrs = []
|
def __init__(self, url, connection_parameters):
|
||||||
"""list of discovered attributes"""
|
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 = []
|
try:
|
||||||
"""list of linked resources"""
|
if connection_parameters.auth_token == None:
|
||||||
|
self.data = self.api_url.get(verify=connection_parameters.verify_cert)
|
||||||
# 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:
|
else:
|
||||||
# TODO: use a regex here to strip and store the version
|
self.data = self.api_url.get(verify=connection_parameters.verify_cert,
|
||||||
# instead of assuming it is 7 chars long
|
headers={'x-auth-token': connection_parameters.auth_token}
|
||||||
self.type = self.type[:-7]
|
)
|
||||||
|
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.
|
def get_link_url(self, link_type):
|
||||||
# Note that this may have different nested structure, depending on
|
"""Need to be explained.
|
||||||
# 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):
|
:param redfish_logfile: redfish log
|
||||||
"""Map linked resources to getter functions
|
:type str
|
||||||
|
:returns: True
|
||||||
|
|
||||||
The root resource returns a dict of links to top-level resources
|
|
||||||
"""
|
"""
|
||||||
def getter(connection, href):
|
self.links=[]
|
||||||
def _get():
|
|
||||||
return connection.rest_get(href, {})
|
|
||||||
return _get
|
|
||||||
|
|
||||||
for k in obj['links']:
|
# Manage standard < 1.0
|
||||||
ref = "get_" + k.lower()
|
if float(mapping.redfish_version) < 1.00:
|
||||||
self._links.append(ref)
|
links = getattr(self.data, mapping.redfish_mapper.map_links())
|
||||||
href = obj['links'][k]['href']
|
if link_type in links:
|
||||||
setattr(self, ref, getter(self._conn, href))
|
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):
|
@property
|
||||||
"""Return this object's _attrs as a dict"""
|
def url(self):
|
||||||
res = {}
|
return self.__url
|
||||||
for a in self._attrs:
|
|
||||||
res[a] = getattr(self, a)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def __str__(self):
|
@url.setter
|
||||||
"""Return the string representation of this object's _attrs"""
|
def url(self, url):
|
||||||
return json.dumps(self.__repr__())
|
self.__url = url
|
||||||
|
|
||||||
|
|
||||||
class BaseCollection(Base):
|
class BaseCollection(Base):
|
||||||
"""Base class for collection types"""
|
"""Abstract class to manage collection (Chassis, Servers etc...)."""
|
||||||
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):
|
def __init__(self, url, connection_parameters):
|
||||||
"""links are special on a chassis; dont parse them"""
|
super(BaseCollection, self).__init__(url, connection_parameters)
|
||||||
pass
|
|
||||||
|
|
||||||
def _parse_items(self, obj):
|
self.links=[]
|
||||||
"""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 = []
|
#linksmembers = self.data.Links.Members
|
||||||
self._item_getters = []
|
#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
|
print self.links
|
||||||
# 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):
|
class Root(Base):
|
||||||
"""Root '/' resource class"""
|
"""Class to manage redfish Root data."""
|
||||||
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
|
def get_api_version(self):
|
||||||
|
"""Return api version.
|
||||||
|
|
||||||
|
:returns: string -- version
|
||||||
|
:raises: AttributeError
|
||||||
|
|
||||||
"""
|
"""
|
||||||
mapping = {
|
try:
|
||||||
'Systems': Systems,
|
version = self.data.RedfishVersion
|
||||||
'Chassis': Chassis,
|
except AttributeError:
|
||||||
'Managers': Base,
|
version = self.data.ServiceVersion
|
||||||
'Schemas': Base,
|
|
||||||
'Registries': Base,
|
|
||||||
'Tasks': Base,
|
|
||||||
'AccountService': Base,
|
|
||||||
'Sessions': Base,
|
|
||||||
'EventService': Base,
|
|
||||||
}
|
|
||||||
|
|
||||||
def getter(connection, href, type):
|
version = version.replace('.', '')
|
||||||
def _get():
|
version = version[0] + '.' + version[1:]
|
||||||
return mapping[type](connection.rest_get(href, {}), self._conn)
|
return(version)
|
||||||
return _get
|
|
||||||
|
|
||||||
for k in obj['links']:
|
def get_api_UUID(self):
|
||||||
ref = "get_" + k.lower()
|
return self.data.UUID
|
||||||
self._links.append(ref)
|
|
||||||
href = obj['links'][k]['href']
|
|
||||||
setattr(self, ref, getter(self._conn, href, k))
|
|
||||||
|
|
||||||
|
|
||||||
class Chassis(BaseCollection):
|
def get_api_link_to_server(self):
|
||||||
"""Chassis resource class"""
|
"""Return api link to server.
|
||||||
def __len__(self):
|
|
||||||
return len(self.items)
|
: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):
|
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
|
pass
|
@ -3,5 +3,6 @@
|
|||||||
# process, which may cause wedges in the gate later.
|
# process, which may cause wedges in the gate later.
|
||||||
|
|
||||||
pbr>=0.6,!=0.7,<1.0
|
pbr>=0.6,!=0.7,<1.0
|
||||||
oslo.log>=1.0,<2.0
|
#oslo.log>=1.0,<2.0
|
||||||
Babel>=1.3
|
Babel>=1.3
|
||||||
|
tortilla>=0.4.1
|
||||||
|
@ -3,8 +3,8 @@ name = python-redfish
|
|||||||
summary = Reference implementation of Redfish standard client.
|
summary = Reference implementation of Redfish standard client.
|
||||||
description-file =
|
description-file =
|
||||||
README.rst
|
README.rst
|
||||||
author = OpenStack
|
author = Redfish dev team
|
||||||
author-email = openstack-dev@lists.openstack.org
|
author-email = python-redfish@lists.mondorescue.org
|
||||||
home-page = http://www.openstack.org/
|
home-page = http://www.openstack.org/
|
||||||
classifier =
|
classifier =
|
||||||
Environment :: OpenStack
|
Environment :: OpenStack
|
||||||
|
@ -8,8 +8,8 @@ coverage>=3.6
|
|||||||
discover
|
discover
|
||||||
python-subunit>=0.0.18
|
python-subunit>=0.0.18
|
||||||
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
|
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
|
||||||
oslosphinx>=2.2.0 # Apache-2.0
|
#oslosphinx>=2.2.0 # Apache-2.0
|
||||||
oslotest>=1.2.0 # Apache-2.0
|
#oslotest>=1.2.0 # Apache-2.0
|
||||||
testrepository>=0.0.18
|
testrepository>=0.0.18
|
||||||
testscenarios>=0.4
|
testscenarios>=0.4
|
||||||
testtools>=0.9.36,!=1.2.0
|
testtools>=0.9.36,!=1.2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user