Merge branch 'master' of github.com:bcornec/python-redfish into HEAD
This commit is contained in:
commit
7fd7894c0c
17
README.rst
17
README.rst
@ -5,12 +5,15 @@ This repository will be used to house the Redfish python library, a reference
|
||||
implementation to enable Python developers to communicate with the Redfish API
|
||||
(http://www.dmtf.org/standards/redfish).
|
||||
|
||||
.. sidebar:: NOTE - DRAFT - WORK IN PROGRESS
|
||||
NOTE::
|
||||
|
||||
DRAFT - WORK IN PROGRESS
|
||||
|
||||
The current Redfish specification revision is 0.96.0a - anything and everything
|
||||
in this library is subject to change until the DMTF ratifies the Redfish API
|
||||
standard v1.0.
|
||||
|
||||
|
||||
Project Structure
|
||||
-------------------
|
||||
|
||||
@ -32,11 +35,17 @@ 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
|
||||
relax the requirements are available in the library, but these configurations
|
||||
are discouraged due to security concerns.
|
||||
are discouraged due to sec
|
||||
|
||||
Project python dependencies are listed in "requirements.txt".
|
||||
Python requirements are listed in requirements.txt; additional requirements for
|
||||
running the unit test suite are listed in test-requirements.txt.
|
||||
|
||||
Developer setup
|
||||
---------------
|
||||
|
||||
To initialize a local development environment (eg, so you can run unit tests)
|
||||
you should run the following commands::
|
||||
|
||||
Any test-specific requirements are listed in "test-requirements.txt".
|
||||
|
||||
Further References
|
||||
------------------
|
||||
|
@ -1,3 +1,6 @@
|
||||
from redfish import connection
|
||||
|
||||
server = connection
|
||||
host = '127.0.0.1'
|
||||
user_name = 'Admin'
|
||||
password = 'password'
|
||||
server = connection.RedfishConnection(host, user_name, password)
|
58
examples/walk-chassis.py
Normal file
58
examples/walk-chassis.py
Normal file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#import logging
|
||||
import sys
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
import redfish
|
||||
|
||||
# Sets up basic logging for this module
|
||||
#log_root = logging.getLogger('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)
|
||||
#logging.setup(CONF, "redfish")
|
||||
|
||||
# Connect to a redfish API endpoint
|
||||
host = 'http://localhost'
|
||||
user_name = ''
|
||||
password = ''
|
||||
|
||||
# This returns a RedfishConnection object, which implements
|
||||
# the low-level HTTP methods like GET, PUT, etc
|
||||
connection = redfish.server.connect(host, user_name, password)
|
||||
|
||||
# From this connection, we can get the Root resource.
|
||||
# Note that the root resource is somewhat special - you create it from
|
||||
# the connection, but you create other resources from the root resource.
|
||||
# (You don't strictly have to do this, but it's simpler.)
|
||||
root = connection.get_root()
|
||||
|
||||
print("\n")
|
||||
print("ROOT CONTROLLER")
|
||||
print("===============")
|
||||
print(root)
|
||||
|
||||
|
||||
# The Root class has well-defined top-level resources, such as
|
||||
# chassis, systems, managers, sessions, etc...
|
||||
chassis = root.get_chassis()
|
||||
|
||||
print("\n")
|
||||
print("CHASSIS DATA")
|
||||
print("============")
|
||||
print(chassis)
|
||||
print("\n")
|
||||
print("WALKING CHASSIS")
|
||||
print("\n")
|
||||
print("CHASSIS contains %d items" % len(chassis))
|
||||
print("\n")
|
||||
for item in chassis:
|
||||
print("SYSTEM")
|
||||
print("======")
|
||||
print(item)
|
||||
print("\n")
|
@ -14,6 +14,9 @@
|
||||
|
||||
import pbr.version
|
||||
|
||||
import redfish.server
|
||||
import redfish.types
|
||||
|
||||
|
||||
__version__ = pbr.version.VersionInfo(
|
||||
'redfish').version_string()
|
||||
|
@ -24,4 +24,8 @@ class RedfishException(Exception):
|
||||
except Excetion as e:
|
||||
LOG.exception('Error in string format operation')
|
||||
message = self.message
|
||||
super(RedfishException, self).__init__(message)
|
||||
super(RedfishException, self).__init__(message)
|
||||
|
||||
|
||||
class ObjectLoadException(RedfishException):
|
||||
pass
|
||||
|
@ -28,7 +28,7 @@ from redfish import connection
|
||||
class RedfishOperation(connection.RedfishConnection):
|
||||
|
||||
def reset_server(self):
|
||||
(status, headers, system) = self.rest_get('/rest/v1/Systems', None)
|
||||
(status, headers, system) = self.rest_get('/redfish/v1/Systems', None)
|
||||
|
||||
memberuri = system['links']['Member'][0]['href']
|
||||
# verify expected type
|
||||
|
@ -27,7 +27,7 @@ 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 /rest/v1/Systems
|
||||
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
|
||||
@ -35,8 +35,8 @@ 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
|
||||
/rest/v1/.../collection/1, or 2. On systems with multiple compute nodes per
|
||||
manager, a System collection member might be /rest/v1/Systems/C1N1.
|
||||
/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
|
||||
@ -93,7 +93,7 @@ header will point to a resource with task information and status.
|
||||
|
||||
JSON-SCHEMA:
|
||||
|
||||
The json-schema available at /rest/v1/Schemas governs the content of the
|
||||
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
|
||||
@ -120,17 +120,23 @@ import gzip
|
||||
import hashlib
|
||||
import httplib
|
||||
import json
|
||||
import logging
|
||||
import ssl
|
||||
import StringIO
|
||||
import sys
|
||||
import urllib2
|
||||
from urlparse import urlparse
|
||||
|
||||
from redfish import exception
|
||||
from oslo_log import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
LOG.setLevel(logging.DEBUG)
|
||||
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):
|
||||
@ -138,19 +144,63 @@ class RedfishConnection(object):
|
||||
|
||||
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.host = host
|
||||
|
||||
self.user_name = user_name
|
||||
self.password = password
|
||||
self.auth_token = auth_token
|
||||
self.enforce_SSL = enforce_SSL
|
||||
|
||||
# TODO: cache the token returned by this call
|
||||
auth_dict = {'Password': self.password, 'UserName': self.user_name}
|
||||
self.rest_post('/rest/v1/sessions', None, json.dumps(auth_dict))
|
||||
# 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
|
||||
LOG.debug('Connection established to host %s.', self.host)
|
||||
# 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):
|
||||
"""
|
||||
@ -161,13 +211,14 @@ class RedfishConnection(object):
|
||||
:param request_headers: optional dict of headers
|
||||
:param request_body: optional JSON body
|
||||
"""
|
||||
|
||||
# If the http schema wasn't specified, default to HTTPS
|
||||
if self.host[0:4] != 'http':
|
||||
self.host = 'https://' + self.host
|
||||
# ensure trailing slash
|
||||
if suburi[-1:] != '/':
|
||||
suburi = suburi + '/'
|
||||
url = urlparse(self.host + suburi)
|
||||
|
||||
if not isinstance(request_headers, dict): request_headers = dict()
|
||||
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:
|
||||
@ -178,57 +229,33 @@ class RedfishConnection(object):
|
||||
self.user_name + ":" + self.password))
|
||||
# TODO: add support for other types of auth
|
||||
|
||||
# TODO: think about redirects....
|
||||
redir_count = 4
|
||||
while redir_count:
|
||||
conn = None
|
||||
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
|
||||
conn = httplib.HTTPSConnection(
|
||||
host=url.netloc, strict=True, context=cont)
|
||||
else:
|
||||
conn = httplib.HTTPSConnection(host=url.netloc,
|
||||
strict=True)
|
||||
elif url.scheme == 'http':
|
||||
conn = httplib.HTTPConnection(host=url.netloc, strict=True)
|
||||
else:
|
||||
raise exception.RedfishException(
|
||||
message='Unknown connection schema')
|
||||
|
||||
# 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.
|
||||
conn.request(operation, url.path, headers=request_headers,
|
||||
body=json.dumps(request_body))
|
||||
resp = conn.getresponse()
|
||||
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:
|
||||
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:
|
||||
LOG.debug("BODY: %s." % body.decode('utf-8'))
|
||||
response = json.loads(body.decode('utf-8'))
|
||||
LOG.debug("Loaded json: %s" % response)
|
||||
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)
|
||||
@ -241,7 +268,9 @@ class RedfishConnection(object):
|
||||
'Failed to parse response as a JSON document, '
|
||||
'received "%s".' % body)
|
||||
|
||||
return resp.status, headers, response
|
||||
self.status = resp.status
|
||||
self.headers = headers
|
||||
return response
|
||||
|
||||
def rest_get(self, suburi, request_headers):
|
||||
"""REST GET
|
||||
@ -249,8 +278,6 @@ class RedfishConnection(object):
|
||||
:param: suburi
|
||||
:param: request_headers
|
||||
"""
|
||||
if not isinstance(request_headers, dict):
|
||||
request_headers = dict()
|
||||
# NOTE: be prepared for various HTTP responses including 500, 404, etc
|
||||
return self._op('GET', suburi, request_headers, None)
|
||||
|
||||
@ -264,9 +291,6 @@ class RedfishConnection(object):
|
||||
redfish does not follow IETF JSONPATCH standard
|
||||
https://tools.ietf.org/html/rfc6902
|
||||
"""
|
||||
if not isinstance(request_headers, dict):
|
||||
request_headers = dict()
|
||||
request_headers['Content-Type'] = 'application/json'
|
||||
# NOTE: be prepared for various HTTP responses including 500, 404, 202
|
||||
return self._op('PATCH', suburi, request_headers, request_body)
|
||||
|
||||
@ -277,9 +301,6 @@ class RedfishConnection(object):
|
||||
:param: request_headers
|
||||
:param: request_body
|
||||
"""
|
||||
if not isinstance(request_headers, dict):
|
||||
request_headers = dict()
|
||||
request_headers['Content-Type'] = 'application/json'
|
||||
# NOTE: be prepared for various HTTP responses including 500, 404, 202
|
||||
return self._op('PUT', suburi, request_headers, request_body)
|
||||
|
||||
@ -290,9 +311,6 @@ class RedfishConnection(object):
|
||||
:param: request_headers
|
||||
:param: request_body
|
||||
"""
|
||||
if not isinstance(request_headers, dict):
|
||||
request_headers = dict()
|
||||
request_headers['Content-Type'] = 'application/json'
|
||||
# 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
|
||||
@ -305,75 +323,27 @@ class RedfishConnection(object):
|
||||
:param: suburi
|
||||
:param: request_headers
|
||||
"""
|
||||
if not isinstance(request_headers, dict):
|
||||
request_headers = dict()
|
||||
# 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)
|
||||
|
||||
# this is a generator that returns collection members
|
||||
def collection(self, collection_uri, request_headers):
|
||||
"""
|
||||
collections are of two tupes:
|
||||
- array of things that are fully expanded (details)
|
||||
- array of URLs (links)
|
||||
"""
|
||||
# get the collection
|
||||
status, headers, thecollection = self.rest_get(
|
||||
collection_uri, request_headers)
|
||||
def get_root(self):
|
||||
return types.Root(self.rest_get('/redfish/v1', {}), connection=self)
|
||||
|
||||
# TODO: commment this
|
||||
while status < 300:
|
||||
# verify expected type
|
||||
|
||||
# NOTE: Because of the Redfish standards effort, we have versioned many things at 0 in anticipation of
|
||||
# them being ratified for version 1 at some point. So this code makes the (unguarranteed) assumption
|
||||
# throughout that version 0 and 1 are both legitimate at this point. Don't write code requiring version 0 as
|
||||
# we will bump to version 1 at some point.
|
||||
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])
|
||||
|
||||
# hint: don't limit to version 0 here as we will rev to 1.0 at some point hopefully with minimal changes
|
||||
assert(get_type(thecollection) == 'Collection.0' or get_type(thecollection) == 'Collection.1')
|
||||
|
||||
# if this collection has inline items, return those
|
||||
|
||||
# NOTE: Collections are very flexible in how the represent members. They can be inline in the collection
|
||||
# as members of the 'Items' array, or they may be href links in the links/Members array. The could actually
|
||||
# be both. We have
|
||||
# to render it with the href links when an array contains PATCHable items because its complex to PATCH
|
||||
# inline collection members.
|
||||
# A client may wish to pass in a boolean flag favoring the href links vs. the Items in case a collection
|
||||
# contains both.
|
||||
|
||||
if 'Items' in thecollection:
|
||||
# iterate items
|
||||
for item in thecollection['Items']:
|
||||
# if the item has a self uri pointer, supply that for convenience
|
||||
memberuri = None
|
||||
if 'links' in item and 'self' in item['links']:
|
||||
memberuri = item['links']['self']['href']
|
||||
|
||||
# Read up on Python generator functions to understand what this does.
|
||||
yield 200, None, item, memberuri
|
||||
|
||||
# else walk the member links
|
||||
elif 'links' in thecollection and 'Member' in thecollection['links']:
|
||||
# iterate members
|
||||
for memberuri in thecollection['links']['Member']:
|
||||
# for each member return the resource indicated by the member link
|
||||
status, headers, member = rest_get(
|
||||
host, memberuri['href'], request_headers, user_name, password)
|
||||
|
||||
# Read up on Python generator functions to understand what this does.
|
||||
yield status, headers, member, memberuri['href']
|
||||
|
||||
# page forward if there are more pages in the collection
|
||||
if 'links' in thecollection and 'NextPage' in thecollection['links']:
|
||||
next_link_uri = collection_uri + '?page=' + str(thecollection['links']['NextPage']['page'])
|
||||
status, headers, thecollection = rest_get(host, next_link_uri, request_headers, user_name, password)
|
||||
|
||||
# else we are finished iterating the collection
|
||||
else:
|
||||
break
|
||||
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)
|
||||
@ -396,10 +366,11 @@ def operation_allowed(headers_dict, operation):
|
||||
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.
|
||||
# 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):
|
@ -21,11 +21,13 @@ Tests for `redfish` module.
|
||||
|
||||
import fixtures
|
||||
import httplib
|
||||
import json
|
||||
import mock
|
||||
import ssl
|
||||
|
||||
from redfish.tests import base
|
||||
from redfish import connection
|
||||
from redfish import server
|
||||
from redfish import types
|
||||
|
||||
|
||||
def get_fake_params(host=None, user=None, pword=None):
|
||||
@ -68,22 +70,24 @@ class TestRedfishConnection(base.TestCase):
|
||||
self.addCleanup(self.https_mock.stop)
|
||||
|
||||
def test_create_ok(self):
|
||||
con = connection.RedfishConnection(*get_fake_params())
|
||||
con = server.RedfishConnection(*get_fake_params())
|
||||
self.assertEqual(1, self.https_mock.call_count)
|
||||
self.assertEqual(0, self.http_mock.call_count)
|
||||
|
||||
def test_create_calls_https_connect(self):
|
||||
self.https_mock.side_effect = TestException()
|
||||
self.assertRaises(TestException,
|
||||
connection.RedfishConnection,
|
||||
server.RedfishConnection,
|
||||
*get_fake_params(host='https://fake'))
|
||||
|
||||
def test_create_calls_http_connect(self):
|
||||
self.http_mock.side_effect = TestException()
|
||||
self.assertRaises(TestException,
|
||||
connection.RedfishConnection,
|
||||
server.RedfishConnection,
|
||||
*get_fake_params(host='http://fake'))
|
||||
|
||||
# TODO: add test for unknown connection schema (eg, ftp://)
|
||||
|
||||
# FIXME: ssl module has no attribute 'SSLContext'
|
||||
# NOTE: skip this test if sys.version_info (major, minor) != (2, 7) and micro < 9
|
||||
# @mock.patch.object(ssl, 'SSLContext')
|
||||
@ -93,10 +97,26 @@ class TestRedfishConnection(base.TestCase):
|
||||
# ssl_mock.assert_called_once_with(ssl.PROTOCOL_TLSv1)
|
||||
|
||||
def test_get_ok(self):
|
||||
con = connection.RedfishConnection(*get_fake_params())
|
||||
res = con.rest_get('/v1/test', '')
|
||||
self.assertEqual(200, res[0])
|
||||
con = server.RedfishConnection(*get_fake_params())
|
||||
res = con.rest_get('/v1/test/', '')
|
||||
self.assertEqual(200, con.status)
|
||||
# Headers ae lower cased when returned
|
||||
self.assertIn('fake-header', res[1].keys())
|
||||
print(res)
|
||||
self.assertIn('foo', res[2].keys())
|
||||
self.assertIn('fake-header', con.headers.keys())
|
||||
self.assertIn('foo', res.keys())
|
||||
self.con_mock.request.assert_called_with(
|
||||
'GET', '/v1/test/', body='null', headers=mock.ANY)
|
||||
|
||||
# TODO: add test for redirects
|
||||
|
||||
# TODO: add test for collections
|
||||
|
||||
# TODO: add test for gzip'd body
|
||||
|
||||
def test_post_ok(self):
|
||||
body = '{"fake": "body"}'
|
||||
json_body = json.dumps(body)
|
||||
con = server.RedfishConnection(*get_fake_params())
|
||||
res = con.rest_post('/v1/test/', '', body)
|
||||
self.assertEqual(200, con.status)
|
||||
self.con_mock.request.assert_called_with(
|
||||
'POST', '/v1/test/', body=json_body, headers=mock.ANY)
|
||||
|
197
redfish/types.py
Normal file
197
redfish/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
|
@ -3,4 +3,5 @@
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
pbr>=0.6,!=0.7,<1.0
|
||||
oslo.log>=1.0,<2.0
|
||||
Babel>=1.3
|
||||
|
Loading…
x
Reference in New Issue
Block a user