Some more cleanup
This commit is contained in:
parent
0c3d4768be
commit
8c84b5585b
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user