From d05c829736abddfabe2c95375812a892d1305703 Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Tue, 31 Mar 2015 16:22:23 -0700 Subject: [PATCH 1/7] make the NOTE in readme not hidden --- README.rst | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 37668c8..318363b 100644 --- a/README.rst +++ b/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.redfishspecification.org/). -.. sidebar:: NOTE - DRAFT - WORK IN PROGRESS +NOTE:: + + DRAFT - WORK IN PROGRESS The current Redfish specification revsion is 0.91 - 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 ------------------ @@ -45,4 +54,4 @@ The data model documentation can be found here: http://www.redfishspecification.org/redfish-data-model-and-schema/ The overall protocol documentation can be found here: - http://www.redfishspecification.org/ \ No newline at end of file + http://www.redfishspecification.org/ From 8d36961e557a5a37e8d20d6b48fbe90bb1ae2f0c Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Tue, 31 Mar 2015 16:23:23 -0700 Subject: [PATCH 2/7] make Sessions uppercase in connect, and log redirects --- redfish/connection.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/redfish/connection.py b/redfish/connection.py index 41f2626..14d09cd 100644 --- a/redfish/connection.py +++ b/redfish/connection.py @@ -147,7 +147,7 @@ class RedfishConnection(object): # 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)) + self.rest_post('/rest/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) @@ -218,8 +218,10 @@ class RedfishConnection(object): 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 @@ -438,4 +440,4 @@ def print_extended_error(extended_error): print('\t' + msg) msgcnt += 1 if msgcnt == 0: # add a spacer - print \ No newline at end of file + print From 695181c8b29fc6372c3f208798e000f97b195545 Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Tue, 31 Mar 2015 16:23:59 -0700 Subject: [PATCH 3/7] add test_post_ok, and some notes for future testing --- redfish/tests/test_redfish.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/redfish/tests/test_redfish.py b/redfish/tests/test_redfish.py index dfaadc7..3cfff3d 100644 --- a/redfish/tests/test_redfish.py +++ b/redfish/tests/test_redfish.py @@ -21,6 +21,7 @@ Tests for `redfish` module. import fixtures import httplib +import json import mock import ssl @@ -84,6 +85,8 @@ class TestRedfishConnection(base.TestCase): connection.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') @@ -98,5 +101,18 @@ class TestRedfishConnection(base.TestCase): self.assertEqual(200, res[0]) # Headers ae lower cased when returned self.assertIn('fake-header', res[1].keys()) - print(res) - self.assertIn('foo', res[2].keys()) \ No newline at end of file + self.assertIn('foo', res[2].keys()) + + # 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 = connection.RedfishConnection(*get_fake_params()) + res = con.rest_put('/v1/test', '', body) + self.con_mock.assert_called_with('GET', '/v1/test', {}, json_body) + self.assertEqual(200, res[0]) From c8df358c281876e059362fc94e0a1af961833bba Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Wed, 1 Apr 2015 03:52:45 -0700 Subject: [PATCH 4/7] Refactor _connect to be called only once Rather than establishing a new HTTP(S) connection for every operation this refactors HTTP[S]Connect into RedfishConnection._connect, and calls it only once, during __init__. --- redfish/connection.py | 78 ++++++++++++++++++++--------------- redfish/tests/test_redfish.py | 7 +++- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/redfish/connection.py b/redfish/connection.py index 14d09cd..19cb212 100644 --- a/redfish/connection.py +++ b/redfish/connection.py @@ -138,19 +138,55 @@ 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)) + # 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: cache the token returned by this call + LOG.debug('Initiating session with host %s', self.host) + auth_dict = {'Password': self.password, 'UserName': self.user_name} + self.rest_post('/rest/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): """ @@ -178,40 +214,16 @@ 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 @@ -228,9 +240,7 @@ class RedfishConnection(object): 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) diff --git a/redfish/tests/test_redfish.py b/redfish/tests/test_redfish.py index 3cfff3d..6371f19 100644 --- a/redfish/tests/test_redfish.py +++ b/redfish/tests/test_redfish.py @@ -102,6 +102,8 @@ class TestRedfishConnection(base.TestCase): # Headers ae lower cased when returned self.assertIn('fake-header', res[1].keys()) self.assertIn('foo', res[2].keys()) + self.con_mock.request.assert_called_with( + 'GET', '/v1/test', body='null', headers=mock.ANY) # TODO: add test for redirects @@ -113,6 +115,7 @@ class TestRedfishConnection(base.TestCase): body = '{"fake": "body"}' json_body = json.dumps(body) con = connection.RedfishConnection(*get_fake_params()) - res = con.rest_put('/v1/test', '', body) - self.con_mock.assert_called_with('GET', '/v1/test', {}, json_body) + res = con.rest_post('/v1/test', '', body) self.assertEqual(200, res[0]) + self.con_mock.request.assert_called_with( + 'POST', '/v1/test', body=json_body, headers=mock.ANY) From c7cd80458cb82afd223425731f23619174673b17 Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Wed, 1 Apr 2015 04:33:57 -0700 Subject: [PATCH 5/7] a little more string cleanup --- redfish/connection.py | 50 ++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/redfish/connection.py b/redfish/connection.py index 19cb212..3fe0b88 100644 --- a/redfish/connection.py +++ b/redfish/connection.py @@ -153,10 +153,13 @@ class RedfishConnection(object): self._connect() if not self.auth_token: - # TODO: cache the token returned by this call + # 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} - self.rest_post('/rest/v1/Sessions', None, json.dumps(auth_dict)) + (status, headers, response) = self.rest_post( + '/rest/v1/Sessions', None, json.dumps(auth_dict)) # TODO: do some schema discovery here and cache the result # self.schema = ... @@ -198,9 +201,6 @@ class RedfishConnection(object): :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 url = urlparse(self.host + suburi) if not isinstance(request_headers, dict): request_headers = dict() @@ -338,23 +338,28 @@ class RedfishConnection(object): 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. + # 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. - # 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') + # 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. + # 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 @@ -408,10 +413,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): From 2a70e6e765a35097b4b3c111d387acb170103d53 Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Sat, 4 Apr 2015 08:44:22 -0700 Subject: [PATCH 6/7] 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). --- examples/simple.py | 5 +- examples/walk-chassis.py | 58 ++++++++ redfish/__init__.py | 3 + redfish/exception.py | 6 +- redfish/{connection.py => server.py} | 125 ++++++----------- redfish/tests/test_redfish.py | 29 ++-- redfish/types.py | 197 +++++++++++++++++++++++++++ requirements.txt | 1 + 8 files changed, 322 insertions(+), 102 deletions(-) create mode 100644 examples/walk-chassis.py rename redfish/{connection.py => server.py} (78%) create mode 100644 redfish/types.py diff --git a/examples/simple.py b/examples/simple.py index 3d1efa3..91d34bd 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -1,3 +1,6 @@ from redfish import connection -server = connection \ No newline at end of file +host = '127.0.0.1' +user_name = 'Admin' +password = 'password' +server = connection.RedfishConnection(host, user_name, password) \ No newline at end of file diff --git a/examples/walk-chassis.py b/examples/walk-chassis.py new file mode 100644 index 0000000..900cfe6 --- /dev/null +++ b/examples/walk-chassis.py @@ -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") diff --git a/redfish/__init__.py b/redfish/__init__.py index 7290f2b..625d02f 100644 --- a/redfish/__init__.py +++ b/redfish/__init__.py @@ -14,6 +14,9 @@ import pbr.version +import redfish.server +import redfish.types + __version__ = pbr.version.VersionInfo( 'redfish').version_string() diff --git a/redfish/exception.py b/redfish/exception.py index 99644c7..b8a00f5 100644 --- a/redfish/exception.py +++ b/redfish/exception.py @@ -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) \ No newline at end of file + super(RedfishException, self).__init__(message) + + +class ObjectLoadException(RedfishException): + pass diff --git a/redfish/connection.py b/redfish/server.py similarity index 78% rename from redfish/connection.py rename to redfish/server.py index 3fe0b88..62f33f0 100644 --- a/redfish/connection.py +++ b/redfish/server.py @@ -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) diff --git a/redfish/tests/test_redfish.py b/redfish/tests/test_redfish.py index 6371f19..b70b43b 100644 --- a/redfish/tests/test_redfish.py +++ b/redfish/tests/test_redfish.py @@ -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) diff --git a/redfish/types.py b/redfish/types.py new file mode 100644 index 0000000..a31b8bc --- /dev/null +++ b/redfish/types.py @@ -0,0 +1,197 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +""" +Redfish Resource Types +""" + +import base64 +import gzip +import hashlib +import httplib +import json +import ssl +import StringIO +import sys +import urllib2 +from urlparse import urlparse + +from oslo_log import log as logging +from redfish import exception + +LOG = logging.getLogger('redfish') + + +class Base(object): + def __init__(self, obj, connection=None): + self._conn = connection + """handle to the redfish connection""" + + self._attrs = [] + """list of discovered attributes""" + + self._links = [] + """list of linked resources""" + + # parse the individual resources, appending them to + # the list of object attributes + for k in obj.keys(): + ref = k.lower() + if ref in ["links", "oem", "items"]: + continue + setattr(self, ref, obj[k]) + self._attrs.append(ref) + + # make sure the required attributes are present + if not getattr(self, 'name', False): + raise ObjectLoadException( + "Failed to load object. Reason: could not determine name.") + if not getattr(self, 'type', False): + raise ObjectLoadException( + "Failed to load object. Reason: could not determine type.") + + if getattr(self, 'serviceversion', False): + self.type = self.type.replace('.' + self.serviceversion, '') + else: + # TODO: use a regex here to strip and store the version + # instead of assuming it is 7 chars long + self.type = self.type[:-7] + + # Lastly, parse the 'links' resource. + # Note that this may have different nested structure, depending on + # what type of resource this is, or what vendor it is. + # subclasses may follow this by parsing other resources / collections + self._parse_links(obj) + + def _parse_links(self, obj): + """Map linked resources to getter functions + + The root resource returns a dict of links to top-level resources + """ + def getter(connection, href): + def _get(): + return connection.rest_get(href, {}) + return _get + + for k in obj['links']: + ref = "get_" + k.lower() + self._links.append(ref) + href = obj['links'][k]['href'] + setattr(self, ref, getter(self._conn, href)) + + def __repr__(self): + """Return this object's _attrs as a dict""" + res = {} + for a in self._attrs: + res[a] = getattr(self, a) + return res + + def __str__(self): + """Return the string representation of this object's _attrs""" + return json.dumps(self.__repr__()) + + +class BaseCollection(Base): + """Base class for collection types""" + def __init__(self, obj, connection=None): + super(BaseCollection, self).__init__(obj, connection=connection) + self._parse_items(obj) + self._attrs.append('items') + + def _parse_links(self, obj): + """links are special on a chassis; dont parse them""" + pass + + def _parse_items(self, obj): + """Map linked items to getter methods + + The chassis resource returns a list of items and corresponding + link data in a separate entity. + """ + def getter(connection, href): + def _get(): + return connection.rest_get(href, {}) + return _get + + self.items = [] + self._item_getters = [] + + if 'links' in obj and 'Member' in obj['links']: + # NOTE: this assumes the lists are ordered the same + counter = 0 + for item in obj['links']['Member']: + self.items.append(obj['Items'][counter]) + self._item_getters.append( + getter(self._conn, item['href'])) + counter+=1 + elif 'Items' in obj: + # TODO: find an example of this format and make sure it works + for item in obj['Items']: + if 'links' in item and 'self' in item['links']: + href = item['links']['self']['href'] + self.items.append(item) + + # TODO: implement paging support + # if 'links' in obj and 'NextPage' in obj['links']: + # next_page = THIS_URI + '?page=' + str(obj['links']['NextPage']['page']) + # do something with next_page URI + + def __iter__(self): + for getter in self._item_getters: + yield getter() + + +class Root(Base): + """Root '/' resource class""" + def _parse_links(self, obj): + """Map linked resources to getter functions + + The root resource returns a dict of links to top-level resources + + TODO: continue implementing customizations for top-level resources + + """ + mapping = { + 'Systems': Systems, + 'Chassis': Chassis, + 'Managers': Base, + 'Schemas': Base, + 'Registries': Base, + 'Tasks': Base, + 'AccountService': Base, + 'Sessions': Base, + 'EventService': Base, + } + + def getter(connection, href, type): + def _get(): + return mapping[type](connection.rest_get(href, {}), self._conn) + return _get + + for k in obj['links']: + ref = "get_" + k.lower() + self._links.append(ref) + href = obj['links'][k]['href'] + setattr(self, ref, getter(self._conn, href, k)) + + +class Chassis(BaseCollection): + """Chassis resource class""" + def __len__(self): + return len(self.items) + + +class Systems(Base): + pass diff --git a/requirements.txt b/requirements.txt index 95137a6..1cbb598 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From ea053e6d0504eb5b2031f8a89bd178d797664ab6 Mon Sep 17 00:00:00 2001 From: Bruno Cornec Date: Fri, 10 Apr 2015 14:42:16 +0200 Subject: [PATCH 7/7] - Use the new Redfish specification 0.95.0a (code was previously based on 0.94) --- redfish/functions.py | 2 +- redfish/server.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/redfish/functions.py b/redfish/functions.py index 843d9d8..9c1b0e0 100644 --- a/redfish/functions.py +++ b/redfish/functions.py @@ -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 diff --git a/redfish/server.py b/redfish/server.py index 3b3d88d..c86b4a9 100644 --- a/redfish/server.py +++ b/redfish/server.py @@ -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 @@ -170,7 +170,7 @@ class RedfishConnection(object): LOG.debug('Initiating session with host %s', self.host) auth_dict = {'Password': self.password, 'UserName': self.user_name} response = self.rest_post( - '/rest/v1/Sessions', None, json.dumps(auth_dict)) + '/redfish/v1/Sessions', None, json.dumps(auth_dict)) # TODO: do some schema discovery here and cache the result # self.schema = ... @@ -328,7 +328,7 @@ class RedfishConnection(object): return self._op('DELETE', suburi, request_headers, None) def get_root(self): - return types.Root(self.rest_get('/rest/v1', {}), connection=self) + return types.Root(self.rest_get('/redfish/v1', {}), connection=self) class Version(object):