Heat allows service providers to extend the capabilities of the orchestration service by writing their own resource plug-ins. These plug-ins are written in Python and included in a directory configured by the service provider. This guide describes a resource plug-in structure and life cycle in order to assist developers in writing their own resource plug-ins.
A resource plug-in is relatively simple in that it needs to extend a base Resource class and implement some relevant life cycle handler methods. The basic life cycle methods of a resource are:
The base class Resource implements each of these life cycle methods and defines one or more handler methods that plug-ins should implement in order to manifest and manage the actual physical resource abstracted by the plug-in. These handler methods will be described in detail in the following sections.
Plug-ins must extend the class heat.engine.resource.Resource.
This class is responsible for managing the overall life cycle of the plug-in. It defines methods corresponding to the life cycle as well as the basic hooks for plug-ins to handle the work of communicating with specific down-stream services. For example, when the engine determines it is time to create a resource, it calls the create method of the applicable plug-in. This method is implemented in the Resource base class and handles most of the bookkeeping and interaction with the engine. This method then calls a handle_create method defined in the plug-in class (if implemented) which is responsible for using specific service calls or other methods needed to instantiate the desired physical resource (server, network, volume, etc).
The base class handles reporting state of the resource back to the engine. A resource’s state is the combination of the life cycle action and the status of that action. For example, if a resource is created successfully, the status of that resource will be CREATE COMPLETE. Alternatively, if the plug-in encounters an error when attempting to create the physical resource, the status would be CREATE FAILED. The base class handles the reporting and persisting of resource state, so a plug-in’s handler methods only need to return data or raise exceptions as appropriate.
A resource’s properties define the settings the template author can manipulate when including that resource in a template. Some examples would be:
Attributes describe runtime state data of the physical resource that the plug-in can expose to other resources in a Stack. Generally, these aren’t available until the physical resource has been created and is in a usable state. Some examples would be:
Each property that a resource supports must be defined in a schema that informs the engine and validation logic what the properties are, what type each is, and validation constraints. The schema is a dictionary whose keys define property names and whose values describe the constraints on that property. This dictionary must be assigned to the properties_schema attribute of the plug-in.
from heat.engine import constraints
from heat.engine import properties
from heat.openstack.common.gettextutils import _
nested_schema = {
"foo": properties.Schema(
properties.Schema.STRING,
_('description of foo field'),
constraints=[
constraints.AllowedPattern('(Ba[rc]?)+'),
constraints.Length(max=10,
description="don't go crazy")
]
)
}
properties_schema = {
"property_name": properties.Schema(
properties.Schema.MAP,
_('Internationalized description of property'),
required=True,
default={"Foo": "Bar"},
schema": nested_schema
)
}
As shown above, some properties may themselves be complex and reference nested schema definitions. Following are the parameters to to the Schema constructor; all but the first have defaults.
data_type:
Defines the type of the property’s value. The valid types are the members of the list properties.Schema.TYPES, currently INTEGER, STRING, NUMBER, BOOLEAN, MAP, and LIST; please use those symbolic names rather than the literals to which they are equated. For LIST and MAP type properties, the schema referenced constrains the format of complex items in the list or map.
Accessing property values of the plug-in at runtime is then a simple call to:
self.properties['PropertyName']
Based on the property type, properties without a set value will return the default “empty” value for that type:
Type | Empty Value |
---|---|
String | ‘’ |
Number | 0 |
Integer | 0 |
List | [] |
Map | {} |
Following are the available kinds of constraints. The description is optional and, if given, states the constraint in plain language for the end user.
Attributes communicate runtime state of the physical resource. Note that some plug-ins do not define any attributes and doing so is optional. If the plug-in needs to expose attributes, it will define an attributes_schema similar to the properties schema described above. This schema, however, is much simpler to define as each item in the dictionary only defines the attribute name and a description of the attribute.
attributes_schema = {
"foo": _("The foo attribute"),
"bar": _("The bar attribute"),
"baz": _("The baz attribute")
}
If attributes are defined, their values must also be resolved by the plug-in. The simplest way to do this is to override the _resolve_attribute method from the Resource class:
def _resolve_attribute(self, name):
# _example_get_physical_resource is just an example and is not defined
# in the Resource class
phys_resource = self._example_get_physical_resource()
if phys_resource:
if not hasattr(phys_resource, name):
# this is usually not needed, but this is a simple example
raise exception.InvalidTemplateAttribute(name)
return getattr(phys_resource, name)
return None
If the plug-in needs to be more sophisticated in its attribute resolution, the plug-in may instead choose to override FnGetAttr. If this method is chosen, however, responsibility for validating the attribute and its accessibility is the responsibility of the plug-in.
Assume the following simple property and attribute definition:
properties_schema = {
'foo': properties.Schema(
properties.Schema.STRING,
_('foo prop description'),
default='foo',
required=True
),
'bar': properies.Schema(
properties.Schema.INTEGER,
_('bar prop description'),
required=True,
constraints=[
constraints.Range(5, 10)
]
)
}
attributes_schema = {
'Attr_1': 'The first attribute',
'Attr_2': 'The second attribute'
}
Also assume the plug-in defining the above has been registered under the template reference name ‘Resource::Foo’ (see Registering Resource Plug-ins). A template author could then use this plug-in in a stack by simply making following declarations in a template:
# ... other sections omitted for brevity ...
resources:
resource-1:
type: Resource::Foo
properties:
foo: Value of the foo property
bar: 7
outputs:
foo-attrib-1:
value: { get_attr: [resource-1, Attr_1] }
description: The first attribute of the foo resource
foo-attrib-2:
value: { get_attr: [resource-1, Attr_2] }
description: The second attribute of the foo resource
To do the work of managing the physical resource the plug-in supports, the following life cycle handler methods should be implemented. Note that the plug-in need not implement all of these methods; optional handlers will be documented as such.
Generally, the handler methods follow a basic pattern. The basic handler method for any life cycle step follows the format handle_<life cycle step>. So for the create step, the handler method would be handle_create. Once a handler is called, an optional check_<life cycle step>_complete may also be implemented so that the plug-in may return immediately from the basic handler and then take advantage of cooperative multi-threading built in to the base class and periodically poll a down-stream service for completion; the check method is polled until it returns True. Again, for the create step, this method would be check_create_complete.
Create a new physical resource. This function should make the required calls to create the physical resource and return as soon as there is enough information to identify the resource. The function should return this identifying information and implement check_create_complete which will take this information in as a parameter and then periodically be polled. This allows for cooperative multi-threading between multiple resources that have had their dependencies satisfied.
Note once the native identifier of the physical resource is known, this function should call self.resource_id_set passing the native identifier of the physical resource. This will persist the identifier and make it available to the plug-in by accessing self.resource_id.
Returns: | A representation of the created physical resource |
---|---|
Raise : | any Exception if the create failed |
If defined, will be called with the return value of handle_create
Parameters: | token – the return value of handle_create; used to poll the physical resource’s status. |
---|---|
Returns: | True if the physical resource is active and ready for use; False otherwise. |
Raise : | any Exception if the create failed. |
Note that there is a default implementation of handle_update in heat.engine.resource.Resource that simply raises an exception indicating that updates require the engine to delete and re-create the resource (this is the default behavior) so implementing this is optional.
Update the physical resources using updated information.
Parameters: |
|
---|
If defined, will be called with the return value of handle_update
Parameters: | token – the return value of handle_update; used to poll the physical resource’s status. |
---|---|
Returns: | True if the update has finished; False otherwise. |
Raise : | any Exception if the update failed. |
These handler functions are optional and only need to be implemented if the physical resource supports suspending
If the physical resource supports it, this function should call the native API and suspend the resource’s operation. This function should return information sufficient for check_suspend_complete to poll the native API to verify the operation’s status.
Returns: | a token containing enough information for check_suspend_complete to verify operation status. |
---|---|
Raise : | any Exception if the suspend operation fails. |
Verify the suspend operation completed successfully.
Parameters: | token – the return value of handle_suspend |
---|---|
Returns: | True if the suspend operation completed and the physical resource is now suspended; False otherwise. |
Raise : | any Exception if the suspend operation failed. |
These handler functions are optional and only need to be implemented if the physical resource supports resuming from a suspended state
If the physical resource supports it, this function should call the native API and resume a suspended resource’s operation. This function should return information sufficient for check_resume_complete to poll the native API to verify the operation’s status.
Returns: | a token containing enough information for check_resume_complete to verify operation status. |
---|---|
Raise : | any Exception if the resume operation fails. |
Verify the resume operation completed successfully.
Parameters: | token – the return value of handle_resume |
---|---|
Returns: | True if the resume operation completed and the physical resource is now active; False otherwise. |
Raise : | any Exception if the resume operation failed. |
Delete the physical resource.
Returns: | a token containing sufficient data to verify the operations status |
---|---|
Raise : | any Exception if the delete operation failed |
Called instead of handle_delete when the deletion policy is SNAPSHOT.
Parameters: | initial_state – the (action, status) tuple of the resource as of the start of this method |
---|---|
Returns: | a token containing sufficient data to verify the operations status |
Raise : | any Exception if the delete operation failed |
Verify the delete operation completed successfully.
Parameters: | token – the return value of handle_delete used to verify the status of the operation |
---|---|
Returns: | True if the delete operation completed and the physical resource is deleted; False otherwise. |
Raise : | any Exception if the delete operation failed. |
To make your plug-in available for use in stack templates, the plug-in must register a reference name with the engine. This is done by defining a resource_mapping function in your plug-in module that returns a map of template resource type names and their corresponding implementation classes:
def resource_mapping():
return { 'My::Custom::Plugin': MyResourceClass }
This would allow a template author to define a resource as:
resources:
my_resource:
type: My::Custom::Plugin
properties:
# ... your plug-in's properties ...
Note that you can define multiple plug-ins per module by simply returning a map containing a unique template type name for each. You may also use this to register a single resource plug-in under multiple template type names (which you would only want to do when constrained by backwards compatibility).
In order to use your plug-in, Heat must be configured to read your resources from a particular directory. The plugin_dirs configuration option lists the directories on the local file system where the engine will search for plug-ins. Simply place the file containing your resource in one of these directories and the engine will make them available next time the service starts.
See one of the Installation Guides at http://docs.OpenStack.org/ for more information on configuring the orchestration service.
You can find the plugin classes in heat/engine/resources. An exceptionally simple one to start with is random_string.py; it is unusual in that it does not manipulate anything in the cloud!