Some more cleanup

This commit is contained in:
Devananda van der Veen 2015-03-25 12:55:27 -07:00
parent 0c3d4768be
commit 8c84b5585b

View File

@ -14,100 +14,104 @@
""" """
Provides examples of using the HP RESTful API on iLO for common use cases. This is for tutorial/example purposes only.
---------------------------------------------------------------------------------------------------------------------
IMPORTANT!!!
---------------------------------------------------------------------------------------------------------------------
When developing a client for the HP RESTful API, be sure to not code based upon assumptions that are not guaranteed.
Search for, and note any 'NOTE' comments in this code to read about ways to avoid incorrect assumptions.
The reason avoiding these assumptions is so important is that implementations may vary across systems and firmware
versions, and we want your code to work consistently.
---------------------------------------------------------------------------------------------------------------------
STARTING ASSUMPTIONS STARTING ASSUMPTIONS
---------------------------------------------------------------------------------------------------------------------
On URIs: On URIs:
The HP RESTful API is a "hypermedia API" by design. This is to avoid building in restrictive assumptions to the The RedFish RESTful API is a "hypermedia API" by design. This is to avoid
data model that will make it difficult to adapt to future hardware implementations. A hypermedia API avoids these building in restrictive assumptions to the data model that will make it
assumptions by making the data model discoverable via links between resources. difficult to adapt to future hardware implementations. A hypermedia API avoids
these assumptions by making the data model discoverable via links between
resources.
A URI should be treated by the client as opaque, and thus should not be attempted to be understood or deconstructed A URI should be treated by the client as opaque, and thus should not be
by the client. Only specific top level URIs (any URI in this sample code) may be assumed, and even these may be attempted to be understood or deconstructed by the client. Only specific top
absent based upon the implementation (e.g. there might be no /rest/v1/Systems collection on something that doesn't level URIs (any URI in this sample code) may be assumed, and even these may be
have compute nodes.) absent based upon the implementation (e.g. there might be no /rest/v1/Systems
collection on something that doesn't have compute nodes.)
The other URIs must be discovered dynamically by following href links. This is because the API will eventually be The other URIs must be discovered dynamically by following href links. This is
implemented on a system that breaks any existing data model "shape" assumptions we may make now. In particular, because the API will eventually be implemented on a system that breaks any
clients should not make assumptions about the URIs for the resource members of a collection. For instance, the URI of existing data model "shape" assumptions we may make now. In particular,
a collection member will NOT always be /rest/v1/.../collection/1, or 2. On Moonshot a System collection member might be clients should not make assumptions about the URIs for the resource members of
/rest/v1/Systems/C1N1. 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.
This sounds very complicated, but in reality (as these examples demonstrate), if you are looking for specific items, This sounds very complicated, but in reality (as these examples demonstrate),
the traversal logic isn't too complicated. if you are looking for specific items, the traversal logic isn't too
complicated.
On Resource Model Traversal: On Resource Model Traversal:
Although the resources in the data model are linked together, because of cross link references between resources, Although the resources in the data model are linked together, because of cross
a client may not assume the resource model is a tree. It is a graph instead, so any crawl of the data model should link references between resources, a client may not assume the resource model
keep track of visited resources to avoid an infinite traversal loop. is a tree. It is a graph instead, so any crawl of the data model should keep
track of visited resources to avoid an infinite traversal loop.
A reference to another resource is any property called "href" no matter where it occurs in a resource. A reference to another resource is any property called "href" no matter where
it occurs in a resource.
An external reference to a resource outside the data model is referred to by a property called "extref". Any An external reference to a resource outside the data model is referred to by a
resource referred to by extref should not be assumed to follow the conventions of the API. property called "extref". Any resource referred to by extref should not be
assumed to follow the conventions of the API.
On Resource Versions: On Resource Versions:
Each resource has a "Type" property with a value of the format Tyepname.x.y.z where Each resource has a "Type" property with a value of the format Tyepname.x.y.z
* x = major version - incrementing this is a breaking change to the schema where
* y = minor version - incrementing this is a non-breaking additive change to the schema * x = major version - incrementing this is a breaking change to the schema y =
* z = errata - non-breaking change * minor version - incrementing this is a non-breaking additive change to the
* schema z = errata - non-breaking change
Because all resources are versioned and schema also have a version, it is possible to design rules for "nearest" Because all resources are versioned and schema also have a version, it is
match (e.g. if you are interacting with multiple services using a common batch of schema files). The mechanism possible to design rules for "nearest" match (e.g. if you are interacting with
is not prescribed, but a client should be prepared to encounter both older and newer versions of resource types. multiple services using a common batch of schema files). The mechanism is not
prescribed, but a client should be prepared to encounter both older and newer
versions of resource types.
On HTTP POST to create: On HTTP POST to create:
WHen POSTing to create a resource (e.g. create an account or session) the guarantee is that a successful response WHen POSTing to create a resource (e.g. create an account or session) the
includes a "Location" HTTP header indicating the resource URI of the newly created resource. The POST may also guarantee is that a successful response includes a "Location" HTTP header
include a representation of the newly created object in a JSON response body but may not. Do not assume the response indicating the resource URI of the newly created resource. The POST may also
body, but test it. It may also be an ExtendedError object. include a representation of the newly created object in a JSON response body
but may not. Do not assume the response body, but test it. It may also be an
ExtendedError object.
HTTP REDIRECT: HTTP REDIRECT:
All clients must correctly handle HTTP redirect. We (or Redfish) may eventually need to use redirection as a way All clients must correctly handle HTTP redirect. We (or Redfish) may
to alias portions of the data model. eventually need to use redirection as a way to alias portions of the data
model.
FUTURE: Asynchronous tasks FUTURE: Asynchronous tasks
In the future some operations may start asynchonous tasks. In this case, the client should recognized and handle In the future some operations may start asynchonous tasks. In this case, the
HTTP 202 if needed and the 'Location' header will point to a resource with task information and status. client should recognized and handle HTTP 202 if needed and the 'Location'
header will point to a resource with task information and status.
JSON-SCHEMA: JSON-SCHEMA:
The json-schema available at /rest/v1/Schemas governs the content of the resources, but keep in mind: The json-schema available at /rest/v1/Schemas governs the content of the
resources, but keep in mind:
* not every property in the schema is implemented in every implementation. * not every property in the schema is implemented in every implementation.
* some properties are schemed to allow both null and anotehr type like string or integer. * some properties are schemed to allow both null and anotehr type like string
* or integer.
Robust client code should check both the existence and type of interesting properties and fail gracefully if Robust client code should check both the existence and type of interesting
expectations are not met. properties and fail gracefully if expectations are not met.
GENERAL ADVICE: GENERAL ADVICE:
Clients should always be prepared for: Clients should always be prepared for:
* unimplemented properties (e.g. a property doesn't apply in a particular case) * unimplemented properties (e.g. a property doesn't apply in a particular case)
* null values in some cases if the value of a property is not currently known due to system conditions * null values in some cases if the value of a property is not currently known
* HTTP status codes other than 200 OK. Can your code handle an HTTP 500 Internal Server Error with no other info? * due to system conditions HTTP status codes other than 200 OK. Can your code
* URIs are case insensitive * handle an HTTP 500 Internal Server Error with no other info? URIs are case
* HTTP header names are case insensitive * insensitive HTTP header names are case insensitive JSON Properties and Enum
* JSON Properties and Enum values are case sensitive * values are case sensitive A client should be tolerant of any set of HTTP
* A client should be tolerant of any set of HTTP headers the service returns * headers the service returns
""" """
@ -125,11 +129,12 @@ import gzip
import StringIO import StringIO
import sys import sys
# REST operation generic handler
def rest_op(operation, host, suburi, request_headers, request_body, iLO_loginname, iLO_password, x_auth_token=None, enforce_SSL=True): def rest_op(operation, host, suburi, request_headers, request_body, user_name,
password, x_auth_token=None, enforce_SSL=True):
"""REST operation generic handler"""
url = urlparse('https://' + host + suburi) url = urlparse('https://' + host + suburi)
#url = urlparse('http://' + host + suburi)
if request_headers is None: if request_headers is None:
request_headers = dict() request_headers = dict()
@ -137,16 +142,19 @@ def rest_op(operation, host, suburi, request_headers, request_body, iLO_loginnam
# if X-Auth-Token specified, supply it instead of basic auth # if X-Auth-Token specified, supply it instead of basic auth
if x_auth_token is not None: if x_auth_token is not None:
request_headers['X-Auth-Token'] = x_auth_token request_headers['X-Auth-Token'] = x_auth_token
# else use iLO_loginname/iLO_password and Basic Auth # else use user_name/password and Basic Auth
elif iLO_loginname is not None and iLO_password is not None: elif user_name is not None and password is not None:
request_headers['Authorization'] = "BASIC " + base64.b64encode(iLO_loginname + ":" + iLO_password) request_headers['Authorization'] = "BASIC " + base64.b64encode(user_name + ":" + password)
# TODO: add support for other types of auth
# TODO: think about redirects....
redir_count = 4 redir_count = 4
while redir_count: while redir_count:
conn = None conn = None
if url.scheme == 'https': if url.scheme == 'https':
# New in Python 2.7.9, SSL enforcement is defaulted on, but can be opted-out of. # New in Python 2.7.9, SSL enforcement is defaulted on, but can be opted-out of.
# The below case is the Opt-Out condition and should be used with GREAT caution. # The below case is the Opt-Out condition and should be used with GREAT caution.
# But could be useful for debugging some things, so we're leaving it in.
if( sys.version_info.major == 2 and if( sys.version_info.major == 2 and
sys.version_info.minor == 7 and sys.version_info.minor == 7 and
sys.version_info.micro >= 9 and sys.version_info.micro >= 9 and
@ -164,11 +172,14 @@ def rest_op(operation, host, suburi, request_headers, request_body, iLO_loginnam
resp = conn.getresponse() resp = conn.getresponse()
body = resp.read() body = resp.read()
# NOTE: Do not assume every HTTP operation will return a JSON body. For example, ExtendedError structures # NOTE: Do not assume every HTTP operation will return a JSON body.
# are only required for HTTP 400 errors and are optional elsewhere as they are mostly redundant for many of the # For example, ExtendedError structures are only required for HTTP 400
# other HTTP status code. In particular, 200 OK responses should not have to return any body. # 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.
# NOTE: this makes sure the headers names are all lower cases because HTTP says they are case insensitive # NOTE: this makes sure the headers names are all lower cases because
# HTTP says they are case insensitive
headers = dict((x.lower(), y) for x, y in resp.getheaders()) headers = dict((x.lower(), y) for x, y in resp.getheaders())
# Follow HTTP redirect # Follow HTTP redirect
@ -196,47 +207,56 @@ def rest_op(operation, host, suburi, request_headers, request_body, iLO_loginnam
return resp.status, headers, response return resp.status, headers, response
# REST GET
def rest_get(host, suburi, request_headers, iLO_loginname, iLO_password):
return rest_op('GET', host, suburi, request_headers, None, iLO_loginname, iLO_password)
# NOTE: be prepared for various HTTP responses including 500, 404, etc.
# REST PATCH def rest_get(host, suburi, request_headers, user_name, password):
def rest_patch(server, suburi, request_headers, request_body, iLO_loginname, iLO_password): """Generic REST GET handler"""
# NOTE: be prepared for various HTTP responses including 500, 404, etc.
return rest_op('GET', host, suburi, request_headers, None, user_name, password)
def rest_patch(server, suburi, request_headers, request_body, user_name, password):
"""REST PATCH"""
if not isinstance(request_headers, dict): request_headers = dict() if not isinstance(request_headers, dict): request_headers = dict()
request_headers['Content-Type'] = 'application/json' request_headers['Content-Type'] = 'application/json'
return rest_op('PATCH', server, suburi, request_headers, request_body, iLO_loginname, iLO_password) return rest_op('PATCH', server, suburi, request_headers, request_body, user_name, password)
# NOTE: be prepared for various HTTP responses including 500, 404, 202 etc. # NOTE: be prepared for various HTTP responses including 500, 404, 202 etc.
# REST PUT
def rest_put(host, suburi, request_headers, request_body, iLO_loginname, iLO_password): def rest_put(host, suburi, request_headers, request_body, user_name, password):
"""REST PUT"""
if not isinstance(request_headers, dict): request_headers = dict() if not isinstance(request_headers, dict): request_headers = dict()
request_headers['Content-Type'] = 'application/json' request_headers['Content-Type'] = 'application/json'
return rest_op('PUT', host, suburi, request_headers, request_body, iLO_loginname, iLO_password) return rest_op('PUT', host, suburi, request_headers, request_body, user_name, password)
# NOTE: be prepared for various HTTP responses including 500, 404, 202 etc. # NOTE: be prepared for various HTTP responses including 500, 404, 202 etc.
# REST POST # REST POST
def rest_post(host, suburi, request_headers, request_body, iLO_loginname, iLO_password): def rest_post(host, suburi, request_headers, request_body, user_name, password):
if not isinstance(request_headers, dict): request_headers = dict() if not isinstance(request_headers, dict): request_headers = dict()
request_headers['Content-Type'] = 'application/json' request_headers['Content-Type'] = 'application/json'
return rest_op('POST', host, suburi, request_headers, request_body, iLO_loginname, iLO_password) return rest_op('POST', host, suburi, request_headers, request_body, user_name, password)
# NOTE: don't assume any newly created resource is included in the response. Only the Location header matters. # 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 ExtendedError, or it may be empty. # the response body may be the new resource, it may be an ExtendedError, or it may be empty.
# REST DELETE # REST DELETE
def rest_delete(host, suburi, request_headers, iLO_loginname, iLO_password): def rest_delete(host, suburi, request_headers, user_name, password):
return rest_op('DELETE', host, suburi, request_headers, None, iLO_loginname, iLO_password) return rest_op('DELETE', host, suburi, request_headers, None, user_name, password)
# NOTE: be prepared for various HTTP responses including 500, 404, etc. # NOTE: be prepared for various HTTP responses including 500, 404, etc.
# NOTE: response may be an ExtendedError or may be empty # NOTE: response may be an ExtendedError or may be empty
# this is a generator that returns collection members # this is a generator that returns collection members
def collection(host, collection_uri, request_headers, iLO_loginname, iLO_password): def collection(host, collection_uri, request_headers, user_name, password):
"""
collections are of two tupes:
- array of things that are fully expanded (details)
- array of URLs (links)
"""
# get the collection # get the collection
status, headers, thecollection = rest_get(host, collection_uri, request_headers, iLO_loginname, iLO_password) status, headers, thecollection = rest_get(
host, collection_uri, request_headers, user_name, password)
# TODO: commment this
while status < 300: while status < 300:
# verify expected type # verify expected type
# NOTE: Because of the Redfish standards effort, we have versioned many things at 0 in anticipation of # NOTE: Because of the Redfish standards effort, we have versioned many things at 0 in anticipation of
@ -251,7 +271,7 @@ def collection(host, collection_uri, request_headers, iLO_loginname, iLO_passwor
# NOTE: Collections are very flexible in how the represent members. They can be inline in the collection # 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 # as members of the 'Items' array, or they may be href links in the links/Members array. The could actually
# be both. Typically, iLO implements the inline (Items) for only when the collection is read only. We have # be both. We have
# to render it with the href links when an array contains PATCHable items because its complex to PATCH # to render it with the href links when an array contains PATCHable items because its complex to PATCH
# inline collection members. # inline collection members.
# A client may wish to pass in a boolean flag favoring the href links vs. the Items in case a collection # A client may wish to pass in a boolean flag favoring the href links vs. the Items in case a collection
@ -275,7 +295,7 @@ def collection(host, collection_uri, request_headers, iLO_loginname, iLO_passwor
# iterate members # iterate members
for memberuri in thecollection['links']['Member']: for memberuri in thecollection['links']['Member']:
# for each member return the resource indicated by the member link # for each member return the resource indicated by the member link
status, headers, member = rest_get(host, memberuri['href'], request_headers, iLO_loginname, iLO_password) status, headers, member = rest_get(host, memberuri['href'], request_headers, user_name, password)
# Read up on Python generator functions to understand what this does. # Read up on Python generator functions to understand what this does.
yield status, headers, member, memberuri['href'] yield status, headers, member, memberuri['href']
@ -283,12 +303,13 @@ def collection(host, collection_uri, request_headers, iLO_loginname, iLO_passwor
# page forward if there are more pages in the collection # page forward if there are more pages in the collection
if 'links' in thecollection and 'NextPage' in thecollection['links']: if 'links' in thecollection and 'NextPage' in thecollection['links']:
next_link_uri = collection_uri + '?page=' + str(thecollection['links']['NextPage']['page']) next_link_uri = collection_uri + '?page=' + str(thecollection['links']['NextPage']['page'])
status, headers, thecollection = rest_get(host, next_link_uri, request_headers, iLO_loginname, iLO_password) status, headers, thecollection = rest_get(host, next_link_uri, request_headers, user_name, password)
# else we are finished iterating the collection # else we are finished iterating the collection
else: else:
break 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)
def get_type(obj): def get_type(obj):
typever = obj['Type'] typever = obj['Type']
@ -302,9 +323,12 @@ def operation_allowed(headers_dict, operation):
return True return True
return False return False
# Message registry support # Message registry support
# XXX not supported yet
message_registries = {} message_registries = {}
# Build a list of decoded messages from the extended_error using the 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 # 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 # how to parse the ExtendedError object and, using any loaded message registries, render an array of
@ -338,6 +362,7 @@ def render_extended_error_message_list(extended_error):
return messages return messages
# Print a list of decoded messages from the extended_error using the message registries # Print a list of decoded messages from the extended_error using the message registries
def print_extended_error(extended_error): def print_extended_error(extended_error):
messages = render_extended_error_message_list(extended_error) messages = render_extended_error_message_list(extended_error)