Usage¶
Incorporating oslo.versionedobjects into your project can be accomplished in the following steps:
Initial scaffolding¶
By convention, objects reside in the <project>/objects directory. This is the place from which all objects should be imported.
Start the implementation by creating objects/base.py with a subclass of the
oslo_versionedobjects.base.VersionedObject. This class will form the
base class for all objects in the project. You need to populate the
OBJ_PROJECT_NAMESPACE property.
Note
OBJ_SERIAL_NAMESPACE is used only for backward compatibility and
should not be set in new projects.
You may also wish to optionally include the following mixins:
oslo_versionedobjects.base.VersionedPersistentObjectA mixin class for persistent objects can be created, defining repeated fields like
created_at,updated_at. Fields are defined in the fields property (which is a dict).oslo_versionedobjects.base.VersionedObjectDictCompatIf objects were previously passed as dicts (a common situation), this class can be used as a mixin class to support dict operations.
A minimal objects/base.py looks like this:
from oslo_versionedobjects import base as ovo_base
from oslo_versionedobjects import fields as ovo_fields
class MyProjectObjectRegistry(ovo_base.VersionedObjectRegistry):
def registration_hook(self, cls, index):
# Make registered objects accessible as myproject.objects.Foo
from myproject import objects
setattr(objects, cls.obj_name(), cls)
class MyProjectObject(ovo_base.VersionedObject):
OBJ_PROJECT_NAMESPACE = 'myproject'
class MyProjectPersistentObject:
"""Mixin for objects that are stored in the database."""
fields = {
'created_at': ovo_fields.DateTimeField(nullable=True),
'updated_at': ovo_fields.DateTimeField(nullable=True),
'deleted_at': ovo_fields.DateTimeField(nullable=True),
'deleted': ovo_fields.BooleanField(default=False),
}
Once you have your base class defined, you can define your actual object classes. Objects classes should be created for all resources/objects passed via RPC as IDs or dicts in order to:
spare the database (or other resource) from extra calls
pass objects instead of dicts, which are tagged with their version
handle all object versions in one place (the
obj_make_compatiblemethod)
To make sure all objects are accessible at all times, you should import them in
__init__.py in the objects/ directory and expose them via a
register_all() function. This function is called at service startup to
ensure objects are registered before any RPC communication takes place.
A typical objects/__init__.py looks like this:
# NOTE: When objects are registered, an attribute is set on this module
# automatically, pointing to the newest/latest version of the object.
# This allows callers to use myproject.objects.Foo without importing
# the specific module.
def register_all():
# NOTE: You must make sure your object gets imported in this function
# in order for it to be registered by services that may need to receive
# it via RPC.
__import__('myproject.objects.thing')
Finally, you should create an object registry by subclassing
oslo_versionedobjects.base.VersionedObjectRegistry. The object
registry is the place where all objects are registered. All object classes
should be registered by the
oslo_versionedobjects.base.ObjectRegistry.register class decorator.
Defining objects¶
Each object class represents a versioned resource type. At minimum, an object
must declare its VERSION, a fields dictionary, and be decorated with
the registry’s @register decorator.
A concrete object with database interaction looks like this:
from oslo_versionedobjects import base as ovo_base
from myproject.db import api as dbapi
from myproject.objects import base
from myproject.objects import fields as object_fields
@base.MyProjectObjectRegistry.register
class Thing(base.MyProjectObject, base.MyProjectPersistentObject,
ovo_base.VersionedObjectDictCompat):
# Version 1.0: Initial version
# Version 1.1: Added 'description' field
VERSION = '1.1'
fields = {
'id': object_fields.IntegerField(),
'uuid': object_fields.UUIDField(nullable=True),
'name': object_fields.StringField(nullable=True),
'description': object_fields.StringField(nullable=True),
'extra': object_fields.FlexibleDictField(nullable=True),
}
@ovo_base.remotable_classmethod
def get_by_uuid(cls, context, uuid):
db_thing = dbapi.get_thing_by_uuid(uuid)
return cls._from_db_object(context, cls(), db_thing)
@ovo_base.remotable_classmethod
def list(cls, context, limit=None, marker=None):
db_things = dbapi.get_thing_list(limit=limit, marker=marker)
return cls._from_db_object_list(context, db_things)
@ovo_base.remotable
def create(self, context=None):
values = self.obj_get_changes()
db_thing = dbapi.create_thing(values)
self._from_db_object(self._context, self, db_thing)
@ovo_base.remotable
def save(self, context=None):
updates = self.obj_get_changes()
db_thing = dbapi.update_thing(self.uuid, updates)
self._from_db_object(self._context, self, db_thing)
@ovo_base.remotable
def destroy(self, context=None):
dbapi.destroy_thing(self.uuid)
self.obj_reset_changes()
def obj_make_compatible(self, primitive, target_version):
super().obj_make_compatible(primitive, target_version)
target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (1, 1) and 'description' in primitive:
del primitive['description']
The _from_db_object static method is responsible for mapping a database row
to the object’s fields and resetting the change-tracking state:
@staticmethod
def _from_db_object(context, obj, db_object, fields=None):
fields = fields or obj.fields
for field in fields:
setattr(obj, field, db_object[field])
obj._context = context
obj.obj_reset_changes()
return obj
The obj_make_compatible method is the key mechanism for rolling upgrades:
it is called when an object needs to be sent to a service running an older
version of the code. It must strip or transform any fields that did not exist
in the target version.
List objects¶
Resources that can be retrieved as a collection should have a corresponding
list object. A list object uses
oslo_versionedobjects.base.ObjectListBase and its fields
dictionary contains a single entry, objects, which is a
oslo_versionedobjects.fields.ListOfObjectsField:
@base.MyProjectObjectRegistry.register
class ThingList(base.ObjectListBase, base.MyProjectObject):
# Version 1.0: Initial version
# Thing <= version 1.1
VERSION = '1.0'
fields = {
'objects': object_fields.ListOfObjectsField('Thing'),
}
@ovo_base.remotable_classmethod
def get_all(cls, context):
db_things = dbapi.get_thing_list()
return ovo_base.obj_make_list(context, cls(context),
Thing, db_things)
The list object’s version should be bumped whenever the version of the contained object type is bumped.
Using custom field types¶
New field types can be implemented by inheriting from
oslo_versionedobjects.field.Field and overwriting the from_primitive
and to_primitive methods.
By subclassing oslo_versionedobjects.fields.AutoTypedField you can
stack multiple fields together, making sure even nested data structures are
being validated.
A common pattern is a FlexibleDictField that accepts both strings and dicts
(useful for data stored as JSON blobs in the database):
import ast
from oslo_versionedobjects import fields as ovo_fields
class FlexibleDict(ovo_fields.FieldType):
@staticmethod
def coerce(obj, attr, value):
if isinstance(value, str):
value = ast.literal_eval(value)
return dict(value)
class FlexibleDictField(ovo_fields.AutoTypedField):
AUTO_TYPE = FlexibleDict()
For enumerated types, inherit from oslo_versionedobjects.fields.Enum
and then wrap it in a oslo_versionedobjects.fields.BaseEnumField:
from oslo_versionedobjects import fields as ovo_fields
class ThingState(ovo_fields.Enum):
ACTIVE = 'active'
PENDING = 'pending'
ERROR = 'error'
ALL = (ACTIVE, PENDING, ERROR)
def __init__(self):
super().__init__(valid_values=ThingState.ALL)
class ThingStateField(ovo_fields.BaseEnumField):
AUTO_TYPE = ThingState()
Custom fields should be defined in a fields.py module within the
objects/ package, and re-exported from there. Projects typically also
re-export the standard oslo.versionedobjects field types from this module to
provide a single import point for all field types:
# objects/fields.py
from oslo_versionedobjects import fields as ovo_fields
# Re-export standard field types
IntegerField = ovo_fields.IntegerField
UUIDField = ovo_fields.UUIDField
StringField = ovo_fields.StringField
BooleanField = ovo_fields.BooleanField
DateTimeField = ovo_fields.DateTimeField
ListOfStringsField = ovo_fields.ListOfStringsField
ObjectField = ovo_fields.ObjectField
ListOfObjectsField = ovo_fields.ListOfObjectsField
# Project-specific field types
class FlexibleDictField(ovo_fields.AutoTypedField):
...
Configure serialization¶
To transfer objects by RPC, subclass the
oslo_versionedobjects.base.VersionedObjectSerializer setting the
OBJ_BASE_CLASS property to the previously defined Object class.
# objects/base.py (add to existing file)
from oslo_versionedobjects import base as ovo_base
class MyProjectObjectSerializer(ovo_base.VersionedObjectSerializer):
OBJ_BASE_CLASS = MyProjectObject
The serializer is then passed to oslo.messaging when creating RPC servers and clients. For example, to use a custom serializer with oslo_messaging:
# common/rpc_service.py
import oslo_messaging as messaging
from myproject.objects import base as objects_base
class RPCService:
def start(self):
serializer = objects_base.MyProjectObjectSerializer()
target = messaging.Target(topic=self.topic, server=self.host)
endpoints = [self.manager]
self.rpcserver = messaging.get_rpc_server(
transport, target, endpoints, serializer=serializer)
self.rpcserver.start()
Implement the indirection API¶
oslo.versionedobjects supports “remotable” method calls. These are calls of
the object methods and classmethods which can be executed locally or remotely
depending on the configuration. Setting the indirection_api as a property
of an object relays the calls to decorated methods through the defined RPC API.
The attachment of the indirection_api should be handled by configuration at
startup time.
The second function of the indirection API is backporting. When the object
serializer attempts to deserialize an object with a future version not
supported by the current instance, it calls the object_backport method in
an attempt to backport the object to a version which can then be handled as
normal.
The oslo_versionedobjects.base.VersionedObjectIndirectionAPI class
provides a base class for implementing your own indirection API.
A typical implementation delegates to the conductor service’s RPC API, so that
object methods decorated with @remotable or @remotable_classmethod are
executed on the conductor:
# objects/indirection.py
from oslo_versionedobjects import base as ovo_base
from myproject.conductor import rpcapi as conductor_api
class MyProjectObjectIndirectionAPI(ovo_base.VersionedObjectIndirectionAPI):
def __init__(self):
super().__init__()
self._conductor = conductor_api.ConductorAPI()
def object_action(self, context, objinst, objmethod, args, kwargs):
return self._conductor.object_action(
context, objinst, objmethod, args, kwargs)
def object_class_action_versions(self, context, objname, objmethod,
object_versions, args, kwargs):
return self._conductor.object_class_action_versions(
context, objname, objmethod, object_versions, args, kwargs)
def object_backport_versions(self, context, objinst, object_versions):
return self._conductor.object_backport_versions(
context, objinst, object_versions)
The indirection API is then attached to the base object class at service
startup. Services that act as clients (e.g. the API tier) attach the
indirection API so that @remotable calls are forwarded to the conductor.
Services that act as servers (e.g. the conductor itself) leave it unset or set
it to None so that @remotable calls are executed locally:
# command/api.py (API service entry point)
from myproject.objects import base as objects_base
from myproject.objects import indirection
def main():
# Attach the indirection API so that remotable object methods
# are executed on the conductor via RPC.
objects_base.MyProjectObject.indirection_api = (
indirection.MyProjectObjectIndirectionAPI()
)
# ... start the WSGI server
# command/conductor.py (conductor service entry point)
def main():
# The conductor executes remotable methods locally; no indirection API.
# objects_base.MyProjectObject.indirection_api = None (default)
# ... start the RPC server