From f844afc49bf4e45f7d51f2b2eb5c266656cc6eee Mon Sep 17 00:00:00 2001 From: Bruno Cornec Date: Wed, 9 Sep 2015 21:43:13 +0200 Subject: [PATCH] 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). --- .gitignore | 8 +- README.rst | 3 +- dmtf/README.rst | 1 - examples/docker/Dockerfile | 13 ++ examples/redfish.conf | 9 + examples/simple-proliant.py | 45 ++++ examples/simple-simulator.py | 38 +++ examples/simple.py | 6 - examples/walk-chassis.py | 10 +- python-redfish.spec | 1 + redfish/__init__.py | 10 +- redfish/config.py | 37 +++ redfish/exception.py | 27 +-- redfish/main.py | 399 +++++++++++++++++++++++++++++++ redfish/mapping.py | 31 +++ redfish/{ => old}/functions.py | 0 redfish/old/types.py | 197 ++++++++++++++++ redfish/server.py | 412 --------------------------------- redfish/types.py | 346 ++++++++++++++------------- requirements.txt | 3 +- setup.cfg | 4 +- test-requirements.txt | 4 +- 22 files changed, 983 insertions(+), 621 deletions(-) create mode 100644 examples/docker/Dockerfile create mode 100644 examples/redfish.conf create mode 100644 examples/simple-proliant.py create mode 100644 examples/simple-simulator.py delete mode 100644 examples/simple.py create mode 100644 redfish/config.py create mode 100644 redfish/main.py create mode 100644 redfish/mapping.py rename redfish/{ => old}/functions.py (100%) create mode 100644 redfish/old/types.py delete mode 100644 redfish/server.py diff --git a/.gitignore b/.gitignore index 85c24ea..1d9192b 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,10 @@ coverage.xml docs/_build/ # PyBuilder -target/ \ No newline at end of file +target/ + +# Pydev +.project +.pydevproject +.settings/ +.metadata diff --git a/README.rst b/README.rst index 86a0128..266abc2 100644 --- a/README.rst +++ b/README.rst @@ -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 @@ -33,7 +34,7 @@ Requirements To use the enclosed examples, you will need Python 2.7 (https://www.python.org/downloads/). Note that Python 2.7.9 enforces greater -SSL verification requiring server certificates be installed. Parameters to +SSL verification requiring server certificates be installed. Parameters to relax the requirements are available in the library, but these configurations are discouraged due to security. diff --git a/dmtf/README.rst b/dmtf/README.rst index 9bf58c9..f113fb3 100644 --- a/dmtf/README.rst +++ b/dmtf/README.rst @@ -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. diff --git a/examples/docker/Dockerfile b/examples/docker/Dockerfile new file mode 100644 index 0000000..041ad08 --- /dev/null +++ b/examples/docker/Dockerfile @@ -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 diff --git a/examples/redfish.conf b/examples/redfish.conf new file mode 100644 index 0000000..80347c8 --- /dev/null +++ b/examples/redfish.conf @@ -0,0 +1,9 @@ +{ + "Nodes": { + "default": { + "url": "", + "login": "", + "password": "" + } + } +} diff --git a/examples/simple-proliant.py b/examples/simple-proliant.py new file mode 100644 index 0000000..9645b34 --- /dev/null +++ b/examples/simple-proliant.py @@ -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() diff --git a/examples/simple-simulator.py b/examples/simple-simulator.py new file mode 100644 index 0000000..c31a0f7 --- /dev/null +++ b/examples/simple-simulator.py @@ -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() diff --git a/examples/simple.py b/examples/simple.py deleted file mode 100644 index 91d34bd..0000000 --- a/examples/simple.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/examples/walk-chassis.py b/examples/walk-chassis.py index 900cfe6..22781ba 100644 --- a/examples/walk-chassis.py +++ b/examples/walk-chassis.py @@ -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 diff --git a/python-redfish.spec b/python-redfish.spec index e60f04d..d6c3171 100644 --- a/python-redfish.spec +++ b/python-redfish.spec @@ -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 diff --git a/redfish/__init__.py b/redfish/__init__.py index 625d02f..b1e8444 100644 --- a/redfish/__init__.py +++ b/redfish/__init__.py @@ -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() diff --git a/redfish/config.py b/redfish/config.py new file mode 100644 index 0000000..6bd9341 --- /dev/null +++ b/redfish/config.py @@ -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 \ No newline at end of file diff --git a/redfish/exception.py b/redfish/exception.py index b8a00f5..f6f759a 100644 --- a/redfish/exception.py +++ b/redfish/exception.py @@ -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 \ No newline at end of file diff --git a/redfish/main.py b/redfish/main.py new file mode 100644 index 0000000..5736732 --- /dev/null +++ b/redfish/main.py @@ -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 diff --git a/redfish/mapping.py b/redfish/mapping.py new file mode 100644 index 0000000..309e23a --- /dev/null +++ b/redfish/mapping.py @@ -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") \ No newline at end of file diff --git a/redfish/functions.py b/redfish/old/functions.py similarity index 100% rename from redfish/functions.py rename to redfish/old/functions.py diff --git a/redfish/old/types.py b/redfish/old/types.py new file mode 100644 index 0000000..ede311f --- /dev/null +++ b/redfish/old/types.py @@ -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 diff --git a/redfish/server.py b/redfish/server.py deleted file mode 100644 index c86b4a9..0000000 --- a/redfish/server.py +++ /dev/null @@ -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 diff --git a/redfish/types.py b/redfish/types.py index a31b8bc..d462731 100644 --- a/redfish/types.py +++ b/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""" + try: + if connection_parameters.auth_token == None: + self.data = self.api_url.get(verify=connection_parameters.verify_cert) + else: + 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 - # 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) + def get_link_url(self, link_type): + """Need to be explained. - # 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.") + :param redfish_logfile: redfish log + :type str + :returns: True - 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 + self.links=[] + + # 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) + + @property + def url(self): + return self.__url - 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__()) + @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 + #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()))) - 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() + 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 + + version = version.replace('.', '') + version = version[0] + '.' + version[1:] + return(version) - 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)) + 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): - pass + # 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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1cbb598..8a4779b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.cfg b/setup.cfg index 2fa161e..d2858c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/test-requirements.txt b/test-requirements.txt index 8592bde..1da9f7e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -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