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
|
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 pbr.version
|
||||||
|
|
||||||
|
import redfish.server
|
||||||
|
import redfish.types
|
||||||
|
|
||||||
|
|
||||||
__version__ = pbr.version.VersionInfo(
|
__version__ = pbr.version.VersionInfo(
|
||||||
'redfish').version_string()
|
'redfish').version_string()
|
||||||
|
@ -25,3 +25,7 @@ class RedfishException(Exception):
|
|||||||
LOG.exception('Error in string format operation')
|
LOG.exception('Error in string format operation')
|
||||||
message = self.message
|
message = self.message
|
||||||
super(RedfishException, self).__init__(message)
|
super(RedfishException, self).__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectLoadException(RedfishException):
|
||||||
|
pass
|
||||||
|
@ -120,17 +120,23 @@ import gzip
|
|||||||
import hashlib
|
import hashlib
|
||||||
import httplib
|
import httplib
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
import ssl
|
import ssl
|
||||||
import StringIO
|
import StringIO
|
||||||
import sys
|
import sys
|
||||||
import urllib2
|
import urllib2
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
from redfish import exception
|
from oslo_log import log as logging
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
from redfish import exception
|
||||||
LOG.setLevel(logging.DEBUG)
|
from redfish import types
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger('redfish')
|
||||||
|
|
||||||
|
|
||||||
|
def connect(host, user, password):
|
||||||
|
return RedfishConnection(host, user, password)
|
||||||
|
|
||||||
|
|
||||||
class RedfishConnection(object):
|
class RedfishConnection(object):
|
||||||
@ -146,10 +152,15 @@ class RedfishConnection(object):
|
|||||||
self.auth_token = auth_token
|
self.auth_token = auth_token
|
||||||
self.enforce_SSL = enforce_SSL
|
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 the http schema wasn't specified, default to HTTPS
|
||||||
if host[0:4] != 'http':
|
if host[0:4] != 'http':
|
||||||
host = 'https://' + host
|
host = 'https://' + host
|
||||||
self.host = host
|
self.host = host
|
||||||
|
|
||||||
self._connect()
|
self._connect()
|
||||||
|
|
||||||
if not self.auth_token:
|
if not self.auth_token:
|
||||||
@ -158,7 +169,7 @@ class RedfishConnection(object):
|
|||||||
# what we should do here.
|
# what we should do here.
|
||||||
LOG.debug('Initiating session with host %s', self.host)
|
LOG.debug('Initiating session with host %s', self.host)
|
||||||
auth_dict = {'Password': self.password, 'UserName': self.user_name}
|
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))
|
'/rest/v1/Sessions', None, json.dumps(auth_dict))
|
||||||
|
|
||||||
# TODO: do some schema discovery here and cache the result
|
# 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_headers: optional dict of headers
|
||||||
:param request_body: optional JSON body
|
:param request_body: optional JSON body
|
||||||
"""
|
"""
|
||||||
|
# ensure trailing slash
|
||||||
|
if suburi[-1:] != '/':
|
||||||
|
suburi = suburi + '/'
|
||||||
url = urlparse(self.host + 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 X-Auth-Token specified, supply it instead of basic auth
|
||||||
if self.auth_token is not None:
|
if self.auth_token is not None:
|
||||||
@ -253,7 +268,9 @@ class RedfishConnection(object):
|
|||||||
'Failed to parse response as a JSON document, '
|
'Failed to parse response as a JSON document, '
|
||||||
'received "%s".' % body)
|
'received "%s".' % body)
|
||||||
|
|
||||||
return resp.status, headers, response
|
self.status = resp.status
|
||||||
|
self.headers = headers
|
||||||
|
return response
|
||||||
|
|
||||||
def rest_get(self, suburi, request_headers):
|
def rest_get(self, suburi, request_headers):
|
||||||
"""REST GET
|
"""REST GET
|
||||||
@ -261,8 +278,6 @@ class RedfishConnection(object):
|
|||||||
:param: suburi
|
:param: suburi
|
||||||
:param: request_headers
|
:param: request_headers
|
||||||
"""
|
"""
|
||||||
if not isinstance(request_headers, dict):
|
|
||||||
request_headers = dict()
|
|
||||||
# NOTE: be prepared for various HTTP responses including 500, 404, etc
|
# NOTE: be prepared for various HTTP responses including 500, 404, etc
|
||||||
return self._op('GET', suburi, request_headers, None)
|
return self._op('GET', suburi, request_headers, None)
|
||||||
|
|
||||||
@ -276,9 +291,6 @@ class RedfishConnection(object):
|
|||||||
redfish does not follow IETF JSONPATCH standard
|
redfish does not follow IETF JSONPATCH standard
|
||||||
https://tools.ietf.org/html/rfc6902
|
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
|
# NOTE: be prepared for various HTTP responses including 500, 404, 202
|
||||||
return self._op('PATCH', suburi, request_headers, request_body)
|
return self._op('PATCH', suburi, request_headers, request_body)
|
||||||
|
|
||||||
@ -289,9 +301,6 @@ class RedfishConnection(object):
|
|||||||
:param: request_headers
|
:param: request_headers
|
||||||
:param: request_body
|
: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
|
# NOTE: be prepared for various HTTP responses including 500, 404, 202
|
||||||
return self._op('PUT', suburi, request_headers, request_body)
|
return self._op('PUT', suburi, request_headers, request_body)
|
||||||
|
|
||||||
@ -302,9 +311,6 @@ class RedfishConnection(object):
|
|||||||
:param: request_headers
|
:param: request_headers
|
||||||
:param: request_body
|
: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
|
# NOTE: don't assume any newly created resource is included in the
|
||||||
# # response. Only the Location header matters.
|
# # response. Only the Location header matters.
|
||||||
# the response body may be the new resource, it may be an
|
# the response body may be the new resource, it may be an
|
||||||
@ -317,80 +323,27 @@ class RedfishConnection(object):
|
|||||||
:param: suburi
|
:param: suburi
|
||||||
:param: request_headers
|
:param: request_headers
|
||||||
"""
|
"""
|
||||||
if not isinstance(request_headers, dict):
|
|
||||||
request_headers = dict()
|
|
||||||
# NOTE: be prepared for various HTTP responses including 500, 404
|
# NOTE: be prepared for various HTTP responses including 500, 404
|
||||||
# NOTE: response may be an ExtendedError or may be empty
|
# NOTE: response may be an ExtendedError or may be empty
|
||||||
return self._op('DELETE', suburi, request_headers, None)
|
return self._op('DELETE', suburi, request_headers, None)
|
||||||
|
|
||||||
# this is a generator that returns collection members
|
def get_root(self):
|
||||||
def collection(self, collection_uri, request_headers):
|
return types.Root(self.rest_get('/rest/v1', {}), connection=self)
|
||||||
"""
|
|
||||||
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)
|
|
||||||
|
|
||||||
# TODO: commment this
|
|
||||||
while status < 300:
|
|
||||||
# verify expected type
|
|
||||||
|
|
||||||
# NOTE: Because of the Redfish standards effort, we have versioned
|
class Version(object):
|
||||||
# many things at 0 in anticipation of them being ratified for
|
def __init__(self, string):
|
||||||
# version 1 at some point. So this code makes the (unguarranteed)
|
try:
|
||||||
# assumption throughout that version 0 and 1 are both legitimate at
|
buf = string.split('.')
|
||||||
# this point. Don't write code requiring version 0 as we will bump
|
if len(buf) < 2:
|
||||||
# to version 1 at some point.
|
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
|
def __repr__(self):
|
||||||
# some point hopefully with minimal changes
|
return str(self.major) + '.' + str(self.minor)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# return the type of an object (down to the major version, skipping minor, and errata)
|
# return the type of an object (down to the major version, skipping minor, and errata)
|
@ -26,7 +26,8 @@ import mock
|
|||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
from redfish.tests import base
|
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):
|
def get_fake_params(host=None, user=None, pword=None):
|
||||||
@ -69,20 +70,20 @@ class TestRedfishConnection(base.TestCase):
|
|||||||
self.addCleanup(self.https_mock.stop)
|
self.addCleanup(self.https_mock.stop)
|
||||||
|
|
||||||
def test_create_ok(self):
|
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(1, self.https_mock.call_count)
|
||||||
self.assertEqual(0, self.http_mock.call_count)
|
self.assertEqual(0, self.http_mock.call_count)
|
||||||
|
|
||||||
def test_create_calls_https_connect(self):
|
def test_create_calls_https_connect(self):
|
||||||
self.https_mock.side_effect = TestException()
|
self.https_mock.side_effect = TestException()
|
||||||
self.assertRaises(TestException,
|
self.assertRaises(TestException,
|
||||||
connection.RedfishConnection,
|
server.RedfishConnection,
|
||||||
*get_fake_params(host='https://fake'))
|
*get_fake_params(host='https://fake'))
|
||||||
|
|
||||||
def test_create_calls_http_connect(self):
|
def test_create_calls_http_connect(self):
|
||||||
self.http_mock.side_effect = TestException()
|
self.http_mock.side_effect = TestException()
|
||||||
self.assertRaises(TestException,
|
self.assertRaises(TestException,
|
||||||
connection.RedfishConnection,
|
server.RedfishConnection,
|
||||||
*get_fake_params(host='http://fake'))
|
*get_fake_params(host='http://fake'))
|
||||||
|
|
||||||
# TODO: add test for unknown connection schema (eg, ftp://)
|
# 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)
|
# ssl_mock.assert_called_once_with(ssl.PROTOCOL_TLSv1)
|
||||||
|
|
||||||
def test_get_ok(self):
|
def test_get_ok(self):
|
||||||
con = connection.RedfishConnection(*get_fake_params())
|
con = server.RedfishConnection(*get_fake_params())
|
||||||
res = con.rest_get('/v1/test', '')
|
res = con.rest_get('/v1/test/', '')
|
||||||
self.assertEqual(200, res[0])
|
self.assertEqual(200, con.status)
|
||||||
# Headers ae lower cased when returned
|
# Headers ae lower cased when returned
|
||||||
self.assertIn('fake-header', res[1].keys())
|
self.assertIn('fake-header', con.headers.keys())
|
||||||
self.assertIn('foo', res[2].keys())
|
self.assertIn('foo', res.keys())
|
||||||
self.con_mock.request.assert_called_with(
|
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
|
# TODO: add test for redirects
|
||||||
|
|
||||||
@ -114,8 +115,8 @@ class TestRedfishConnection(base.TestCase):
|
|||||||
def test_post_ok(self):
|
def test_post_ok(self):
|
||||||
body = '{"fake": "body"}'
|
body = '{"fake": "body"}'
|
||||||
json_body = json.dumps(body)
|
json_body = json.dumps(body)
|
||||||
con = connection.RedfishConnection(*get_fake_params())
|
con = server.RedfishConnection(*get_fake_params())
|
||||||
res = con.rest_post('/v1/test', '', body)
|
res = con.rest_post('/v1/test/', '', body)
|
||||||
self.assertEqual(200, res[0])
|
self.assertEqual(200, con.status)
|
||||||
self.con_mock.request.assert_called_with(
|
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.
|
# process, which may cause wedges in the gate later.
|
||||||
|
|
||||||
pbr>=0.6,!=0.7,<1.0
|
pbr>=0.6,!=0.7,<1.0
|
||||||
|
oslo.log>=1.0,<2.0
|
||||||
Babel>=1.3
|
Babel>=1.3
|
||||||
|
Loading…
Reference in New Issue
Block a user