Jump to: navigation, search

Difference between revisions of "WritingRequestExtensions"

(Start by describing extensions)
 
(Taking a break and saving what I've written so far...)
Line 6: Line 6:
 
== Preliminaries ==
 
== Preliminaries ==
  
First, we'll discuss the creation of an extension in general.  Extensions to be distributed with nova should live in the nova/api/openstack/contrib directory, and the class describing the extension (which we'll get to shortly) should have a name identical to that of the module, with the first letter capitalized; that is, if your module is "admin_actions.py", then your class should be named "Admin_actions" (yes, with the underscore; this is documentation of existing practice, not an approval of it).  This naming convention is only necessary for extensions that should always be active; optional extensions should not live in this directory and can have any desired naming scheme.  To load these extensions, specify the dotted path to the module as an argument to the `--osapi_extension` flag.  (This flag may be given multiple times, to specify additional extensions.)
+
First, we'll discuss the creation of an extension in general.  Extensions to be distributed with nova should live in the `nova/api/openstack/contrib` directory, and the class describing the extension (which we'll get to shortly) should have a name identical to that of the module, with the first letter capitalized; that is, if your module is "admin_actions.py", then your class should be named "Admin_actions" (yes, with the underscore; this is documentation of existing practice, not an approval of it).  This naming convention is only necessary for extensions that should always be active; optional extensions should not live in this directory and can have any desired naming scheme.  To load these extensions, specify the dotted path to the module as an argument to the `--osapi_extension` flag.  (This flag may be given multiple times, to specify additional extensions.)
  
 
Now, on to the structure of an extension.  An extension is simply a class; it is recommended that it extend `nova.api.openstack.extensions.[[ExtensionDescriptor]]`, but this is not required.  What is required is that the class have a doc string, which will be used as the description of the extension sent to the user upon request.  Additionally, the following four class attributes must be present:
 
Now, on to the structure of an extension.  An extension is simply a class; it is recommended that it extend `nova.api.openstack.extensions.[[ExtensionDescriptor]]`, but this is not required.  What is required is that the class have a doc string, which will be used as the description of the extension sent to the user upon request.  Additionally, the following four class attributes must be present:
Line 15: Line 15:
 
;  updated : A complete ISO 8601-formatted date and time, with timezone, indicating the last update time of the extension.  This is used for versioning.  An example value would be "2011-10-27T15:00:00-0500", corresponding to 3 PM in US Central Daylight Time on October 27, 2011.
 
;  updated : A complete ISO 8601-formatted date and time, with timezone, indicating the last update time of the extension.  This is used for versioning.  An example value would be "2011-10-27T15:00:00-0500", corresponding to 3 PM in US Central Daylight Time on October 27, 2011.
  
The extension must have an `<u>init</u>()` method taking a single argument; it should call the `register()` method of that argument, passing it `self`.  This is provided by `nova.api.openstack.extensions.[[ExtensionDescriptor]]`:
+
The extension must have an `<u>init</u>()` method taking a single argument; it should call the `register()` method of that argument, passing it `self`.  This is provided by `[[ExtensionDescriptor]]`:
  
  
Line 30: Line 30:
 
;  get_request_extensions() : Returns a list of `nova.api.openstack.extensions.[[RequestExtension]]` objects describing extensions to existing resources.
 
;  get_request_extensions() : Returns a list of `nova.api.openstack.extensions.[[RequestExtension]]` objects describing extensions to existing resources.
  
Methods not needed to implement the extension are not required to be present.  The `nova.api.openstack.extensions.[[ExtensionDescriptor]]` class provides default implementations of these methods which simply return empty lists, but it is legal to omit them if you are not extending that class.
+
Methods not needed to implement the extension are not required to be present.  The `[[ExtensionDescriptor]]` class provides default implementations of these methods which simply return empty lists, but it is legal to omit them if you are not extending that class.
 +
 
 +
== Request Extensions ==
 +
 
 +
As mentioned above, `get_request_extensions()` returns a list of `[[RequestExtension]]` instances.  Creating a request extension is as simple as creating a function or other callable taking three arguments, and passing it—along with the HTTP method and the URL to extend—to the `[[RequestExtension]]` constructor, like so:
 +
 
 +
 
 +
<pre><nowiki>#!highlight python
 +
def handle_request(req, res, body):
 +
    pass
 +
...
 +
    def get_request_extensions(self):
 +
        return [RequestExtension('GET', '/foo', handle_request)]
 +
</nowiki></pre>
 +
 
 +
 
 +
(For an example of this in practice, see `nova/tests/api/openstack/extensions/foxinsocks.py`.)
 +
 
 +
The handler is passed a `Request` object, the `Response` object generated by the nova API, and `body`, which is the actual, unserialized object being returned to the caller.  (This object is deserialized from the response's `body` attribute.)  The handler should not touch the `body` attribute of the response; it should, instead, manipulate the object passed as the `body` argument.  It is perfectly reasonable for a request extension to manipulate other parts of the response, for instance, setting a header.
 +
 
 +
Any elements that a request extension adds to the `body` object it is passed should be prefixed by the extension's alias value.  For example, an extension with alias "EXA-EXT", adding the 'example' key to the `body`, would do something like the following:
 +
 
 +
 
 +
<pre><nowiki>#!highlight python
 +
    body['EXA-EXT:example'] = "Example value"
 +
</nowiki></pre>
 +
 
 +
 
 +
== XML Responses ==
 +
 
 +
The procedure indicated above is relatively straightforward, when the response body is requested in JSON format.  However, nova also supports XML serialization, and by default, extra attributes such as the above will not be serialized.  Serialization was recently rewritten to enable this capability through the use of XML templates.  The XML templates support was written to look similar to the [http://effbot.org/zone/element-index.htm ElementTree] interface, so the reader may wish to familiarize themselves with that system before proceeding.
 +
 
 +
An XML template is constructed using `nova.api.openstack.xmlutil.[[TemplateElement]]` instances, arranged into a tree (for which, the `nova.api.openstack.xmlutil.[[SubTemplateElement]]()` helper function may be useful).  Each such template element corresponds to an XML element, and has attributes (settable using the `set()` method, or using keyword arguments to the constructor) and text (settable using the `text` property of the template element).  Once a tree of elements has been constructed, it is used to construct a `nova.api.openstack.xmlutil.Template` (of which there are two usable subclasses, `nova.api.openstack.xmlutil.[[MasterTemplate]]` and `nova.api.openstack.xmlutil.[[SlaveTemplate]]`; more about these in a moment).  The critical component is data selectors, which specify the source of the data to include in the text or attribute.  A selector is simply a callable taking two arguments; the first is the `body` object (or some already-selected subcomponent of the `body` object), and the second is simply a `do_raise` boolean indicating whether the selector should return `None` if the data is not found (a `False` value of `do_raise`) or raise a `[[KeyError]]` (a `True` value of `do_raise`).  There exists `nova.api.openstack.xmlutil.Selector` and `nova.api.openstack.xmlutil.[[ConstantSelector]]` classes for building these selectors, but the templates system normally constructs these automatically.

Revision as of 21:05, 27 October 2011

Writing Request Extensions

This page is, at present, sketchy notes on how to create a new "request" extension (an extension applying to existing nova API operations, such as showing information about a server). The primary emphasis is using XML templates, which were created as a means of enabling the request extensions to actually exist in the first place.

Preliminaries

First, we'll discuss the creation of an extension in general. Extensions to be distributed with nova should live in the `nova/api/openstack/contrib` directory, and the class describing the extension (which we'll get to shortly) should have a name identical to that of the module, with the first letter capitalized; that is, if your module is "admin_actions.py", then your class should be named "Admin_actions" (yes, with the underscore; this is documentation of existing practice, not an approval of it). This naming convention is only necessary for extensions that should always be active; optional extensions should not live in this directory and can have any desired naming scheme. To load these extensions, specify the dotted path to the module as an argument to the `--osapi_extension` flag. (This flag may be given multiple times, to specify additional extensions.)

Now, on to the structure of an extension. An extension is simply a class; it is recommended that it extend `nova.api.openstack.extensions.ExtensionDescriptor`, but this is not required. What is required is that the class have a doc string, which will be used as the description of the extension sent to the user upon request. Additionally, the following four class attributes must be present:

name 
The name of the extension. This need not match the class name.
alias 
An alias for the extension. This is used as the namespace prefix on XML elements.
namespace 
An XML namespace declaration, typically a URL to a document describing the extension.
updated 
A complete ISO 8601-formatted date and time, with timezone, indicating the last update time of the extension. This is used for versioning. An example value would be "2011-10-27T15:00:00-0500", corresponding to 3 PM in US Central Daylight Time on October 27, 2011.

The extension must have an `init()` method taking a single argument; it should call the `register()` method of that argument, passing it `self`. This is provided by `ExtensionDescriptor`:


#!highlight python
def __init__(self, ext_mgr):
    ext_mgr.register(self)


The only other thing the extension requires is at least one of the following three methods:

get_resources() 
Returns a list of `nova.api.openstack.extensions.ResourceExtension` objects describing new resources. A resource extension introduces a new resource (i.e., a new URL component). This document does not describe these further.
get_actions() 
Returns a list of `nova.api.openstack.extensions.ActionExtension` objects describing new actions. An action extension introduces a new action defined on the `action` endpoint of an existing resource. This document does not describe these further.
get_request_extensions() 
Returns a list of `nova.api.openstack.extensions.RequestExtension` objects describing extensions to existing resources.

Methods not needed to implement the extension are not required to be present. The `ExtensionDescriptor` class provides default implementations of these methods which simply return empty lists, but it is legal to omit them if you are not extending that class.

Request Extensions

As mentioned above, `get_request_extensions()` returns a list of `RequestExtension` instances. Creating a request extension is as simple as creating a function or other callable taking three arguments, and passing it—along with the HTTP method and the URL to extend—to the `RequestExtension` constructor, like so:


#!highlight python
def handle_request(req, res, body):
    pass
...
    def get_request_extensions(self):
        return [RequestExtension('GET', '/foo', handle_request)]


(For an example of this in practice, see `nova/tests/api/openstack/extensions/foxinsocks.py`.)

The handler is passed a `Request` object, the `Response` object generated by the nova API, and `body`, which is the actual, unserialized object being returned to the caller. (This object is deserialized from the response's `body` attribute.) The handler should not touch the `body` attribute of the response; it should, instead, manipulate the object passed as the `body` argument. It is perfectly reasonable for a request extension to manipulate other parts of the response, for instance, setting a header.

Any elements that a request extension adds to the `body` object it is passed should be prefixed by the extension's alias value. For example, an extension with alias "EXA-EXT", adding the 'example' key to the `body`, would do something like the following:


#!highlight python
    body['EXA-EXT:example'] = "Example value"


XML Responses

The procedure indicated above is relatively straightforward, when the response body is requested in JSON format. However, nova also supports XML serialization, and by default, extra attributes such as the above will not be serialized. Serialization was recently rewritten to enable this capability through the use of XML templates. The XML templates support was written to look similar to the ElementTree interface, so the reader may wish to familiarize themselves with that system before proceeding.

An XML template is constructed using `nova.api.openstack.xmlutil.TemplateElement` instances, arranged into a tree (for which, the `nova.api.openstack.xmlutil.SubTemplateElement()` helper function may be useful). Each such template element corresponds to an XML element, and has attributes (settable using the `set()` method, or using keyword arguments to the constructor) and text (settable using the `text` property of the template element). Once a tree of elements has been constructed, it is used to construct a `nova.api.openstack.xmlutil.Template` (of which there are two usable subclasses, `nova.api.openstack.xmlutil.MasterTemplate` and `nova.api.openstack.xmlutil.SlaveTemplate`; more about these in a moment). The critical component is data selectors, which specify the source of the data to include in the text or attribute. A selector is simply a callable taking two arguments; the first is the `body` object (or some already-selected subcomponent of the `body` object), and the second is simply a `do_raise` boolean indicating whether the selector should return `None` if the data is not found (a `False` value of `do_raise`) or raise a `KeyError` (a `True` value of `do_raise`). There exists `nova.api.openstack.xmlutil.Selector` and `nova.api.openstack.xmlutil.ConstantSelector` classes for building these selectors, but the templates system normally constructs these automatically.