 9094675555
			
		
	
	9094675555
	
	
	
		
			
			- Use future module to allow python2/3 compatibility. - New dependencies added future and configparser.
		
			
				
	
	
		
			409 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			409 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # coding=utf-8
 | |
| #
 | |
| # 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
 | |
| 
 | |
| """
 | |
| from __future__ import unicode_literals
 | |
| from __future__ import print_function
 | |
| from __future__ import division
 | |
| from __future__ import absolute_import
 | |
| from future import standard_library
 | |
| standard_library.install_aliases()
 | |
| from builtins import object
 | |
| 
 | |
| 
 | |
| 
 | |
| import json
 | |
| from urllib.parse import urlparse
 | |
| import requests
 | |
| from . import config
 | |
| from . import types
 | |
| from . import mapping
 | |
| from . import exception
 | |
| 
 | |
| """Function to wrap RedfishConnection"""
 | |
| 
 | |
| 
 | |
| def connect(
 | |
|         url,
 | |
|         user,
 | |
|         password,
 | |
|         simulator=False,
 | |
|         enforceSSL=True,
 | |
|         verify_cert=True
 | |
|         ):
 | |
| 
 | |
|     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."""
 | |
|         # Specify a name for the logger as recommended by the logging
 | |
|         # documentation. However for strange reason requests logs are not
 | |
|         # anymore capture in the log file.
 | |
|         # TODO : Check strange behavior about requests logs.
 | |
|         config.logger = config.initialize_logger(config.REDFISH_LOGFILE,
 | |
|                                                  config.CONSOLE_LOGGER_LEVEL,
 | |
|                                                  config.FILE_LOGGER_LEVEL,
 | |
|                                                  __name__)
 | |
| 
 | |
|         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()
 | |
|         mapping.redfish_root_name = self.Root.get_name()
 | |
| 
 | |
|         # Instantiate a global mapping object to handle Redfish version variation
 | |
|         mapping.redfish_mapper = mapping.RedfishVersionMapping(self.get_api_version(), self.Root.get_name())
 | |
| 
 | |
|         # 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.")
 | |
| 
 | |
| 
 | |
| 
 | |
|         # Structure 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
 | |
|                                                  )
 | |
| 
 | |
| #         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()
 | |
|                                     )
 | |
| 
 | |
|         # Handle login with redfish 1.00, url must be :
 | |
|         # /rest/v1/SessionService/Sessions as specified by the specification
 | |
|         if float(mapping.redfish_version) >= 1.00:
 | |
|             url += '/Sessions'
 | |
| 
 | |
|         # Craft request body and header
 | |
|         requestBody = {"UserName": self.connection_parameters.user_name  , "Password": self.connection_parameters.password}
 | |
|         config.logger.debug(requestBody)
 | |
|         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:
 | |
|             try:
 | |
|                 answer=auth.json()
 | |
|             except ValueError as e:
 | |
|                 answer = ""
 | |
|             raise exception.AuthenticationFailureException("Login request return an invalid status code ", code=auth.status_code, queryAnswer=answer)
 | |
| 
 | |
|         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
 |