Refactoring and adding Types
Some big changes here: Rename connection.py to server.py Refactor about half of server.py into a new types.py module which builds classes for each resource type, and auto-builds links to fetch sub-resources from each type. Add examples/walk-chassis.py to demonstrate how to use the Root and Chassis classes to walk all the objects returned from /rest/v1/chassis/ Import oslo_log and start using it (more to do here, it's not working quite yet).
This commit is contained in:
parent
c7cd80458c
commit
2a70e6e765
@ -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()
|
||||
|
@ -25,3 +25,7 @@ class RedfishException(Exception):
|
||||
LOG.exception('Error in string format operation')
|
||||
message = self.message
|
||||
super(RedfishException, self).__init__(message)
|
||||
|
||||
|
||||
class ObjectLoadException(RedfishException):
|
||||
pass
|
||||
|
@ -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):
|
||||
@ -146,10 +152,15 @@ class RedfishConnection(object):
|
||||
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:
|
||||
@ -158,7 +169,7 @@ class RedfishConnection(object):
|
||||
# what we should do here.
|
||||
LOG.debug('Initiating session with host %s', self.host)
|
||||
auth_dict = {'Password': self.password, 'UserName': self.user_name}
|
||||
(status, headers, response) = self.rest_post(
|
||||
response = self.rest_post(
|
||||
'/rest/v1/Sessions', None, json.dumps(auth_dict))
|
||||
|
||||
# TODO: do some schema discovery here and cache the result
|
||||
@ -200,10 +211,14 @@ class RedfishConnection(object):
|
||||
: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()
|
||||
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:
|
||||
@ -253,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
|
||||
@ -261,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)
|
||||
|
||||
@ -276,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)
|
||||
|
||||
@ -289,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)
|
||||
|
||||
@ -302,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
|
||||
@ -317,80 +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('/rest/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)
|
@ -26,7 +26,8 @@ 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):
|
||||
@ -69,20 +70,20 @@ 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://)
|
||||
@ -96,14 +97,14 @@ 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())
|
||||
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)
|
||||
'GET', '/v1/test/', body='null', headers=mock.ANY)
|
||||
|
||||
# TODO: add test for redirects
|
||||
|
||||
@ -114,8 +115,8 @@ class TestRedfishConnection(base.TestCase):
|
||||
def test_post_ok(self):
|
||||
body = '{"fake": "body"}'
|
||||
json_body = json.dumps(body)
|
||||
con = connection.RedfishConnection(*get_fake_params())
|
||||
res = con.rest_post('/v1/test', '', body)
|
||||
self.assertEqual(200, res[0])
|
||||
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)
|
||||
'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…
Reference in New Issue
Block a user