In order to support LIKE statement filter in OVO objects, StringMatchingFilterObj class is introduced. However, this class does not provide initial attributes of "starts", "contains" and "ends". In real usage, if these attributes are not initialized, we will hit failures in method apply_filters() in db/_model_query.py. This patch adds the initialization back. Secondly, a typo of "obj_utils.StringMatchingContains" is revised in this patch too. Change-Id: If828068d8d08ff09dff6c63d53320bc397d32448 Closes-Bug: #1724446
28 KiB
Objects in neutron
Object versioning is a key concept in achieving rolling upgrades. Since its initial implementation by the nova community, a versioned object model has been pushed to an oslo library so that its benefits can be shared across projects.
Oslo VersionedObjects (aka OVO) is a database facade, where you define the middle layer between software and the database schema. In this layer, a versioned object per database resource is created with a strict data definition and version number. With OVO, when you change the database schema, the version of the object also changes and a backward compatible translation is provided. This allows different versions of software to communicate with one another (via RPC).
OVO is also commonly used for RPC payload versioning. OVO creates versioned dictionary messages by defining a strict structure and keeping strong typing. Because of it, you can be sure of what is sent and how to use the data on the receiving end.
Usage of objects
CRUD operations
Objects support CRUD operations: create()
,
get_object()
and get_objects()
(equivalent of
read
), update()
, delete()
,
update_objects()
, and delete_objects()
. The
nature of OVO is, when any change is applied, OVO tracks it. After
calling create()
or update()
, OVO detects this
and changed fields are saved in the database. Please take a look at
simple object usage scenarios using example of DNSNameServer:
# to create an object, you can pass the attributes in constructor:
= DNSNameServer(context, address='asd', subnet_id='xxx', order=1)
dns
dns.create()
# or you can create a dict and pass it as kwargs:
= {'address': 'asd', 'subnet_id': 'xxx', 'order': 1}
dns_data = DNSNameServer(context, **dns_data)
dns
dns.create()
# for fetching multiple objects:
= DNSNameServer.get_objects(context)
dnses # will return list of all dns name servers from DB
# for fetching objects with substrings in a string field:
from neutron.objects import utils as obj_utils
= DNSNameServer.get_objects(context, address=obj_utils.StringContains('10.0.0'))
dnses # will return list of all dns name servers from DB that has '10.0.0' in their addresses
# to update fields:
= DNSNameServer.get_object(context, address='asd', subnet_id='xxx')
dns = 2
dns.order
dns.update()
# if you don't care about keeping the object, you can execute the update
# without fetch of the object state from the underlying persistent layer
= DNSNameServer.update_objects(
count 'order': 3}, address='asd', subnet_id='xxx')
context, {
# to remove object with filter arguments:
= {'address': 'asd', 'subnet_id': 'xxx'}
filters **filters) DNSNameServer.delete_objects(context,
Filter, sort and paginate
The NeutronDbObject
class has strict validation on which
field sorting and filtering can happen. When calling
get_objects()
, count()
,
update_objects()
, delete_objects()
and
objects_exist()
, validate_filters()
is
invoked, to see if it's a supported filter criterion (which is by
default non-synthetic fields only). Additional filters can be defined
using register_filter_hook_on_model()
. This will add the
requested string to valid filter names in object implementation. It is
optional.
In order to disable filter validation,
validate_filters=False
needs to be passed as an argument in
aforementioned methods. It was added because the default behaviour of
the neutron API is to accept everything at API level and filter it out
at DB layer. This can be used by out of tree extensions.
register_filter_hook_on_model()
is a complementary
implementation in the NeutronDbObject
layer to DB layer's
register_model_query_hook()
, which adds support for extra
filtering during construction of SQL query. When extension defines extra
query hook, it needs to be registered using the objects
register_filter_hook_on_model()
, if it is not already
included in the objects fields
.
To limit or paginate results, Pager
object can be used.
It accepts sorts
(list of (key, direction)
tuples), limit
, page_reverse
and
marker
keywords.
# filtering
# to get an object based on primary key filter
= DNSNameServer.get_object(context, address='asd', subnet_id='xxx')
dns
# to get multiple objects
= DNSNameServer.get_objects(context, subnet_id='xxx')
dnses
= {'subnet_id': ['xxx', 'yyy']}
filters = DNSNameServer.get_objects(context, **filters)
dnses
# do not validate filters
= DNSNameServer.get_objects(context, validate_filters=False,
dnses ='xxx')
fake_filter
# count the dns servers for given subnet
= DNSNameServer.count(context, subnet_id='xxx')
dns_count
# sorting
# direction True == ASC, False == DESC
= False
direction = Pager(sorts=[('order', direction)])
pager = DNSNameServer.get_objects(context, _pager=pager, subnet_id='xxx') dnses
Defining your own object
In order to add a new object in neutron, you have to:
- Create an object derived from
NeutronDbObject
(aka base object) - Add/reuse data model
- Define fields
It is mandatory to define data model using db_model
attribute from NeutronDbObject
.
Fields should be defined using
oslo_versionobjects.fields
exposed types. If there is a
special need to create a new type of field, you can use
common_types.py
in the neutron.objects
directory. Example:
fields = {
'id': common_types.UUIDField(),
'name': obj_fields.StringField(),
'subnetpool_id': common_types.UUIDField(nullable=True),
'ip_version': common_types.IPVersionEnumField()
}
VERSION
is mandatory and defines the version of the
object. Initially, set the VERSION
field to 1.0. Change
VERSION
if fields or their types are modified. When you
change the version of objects being exposed via RPC, add method
obj_make_compatible(self, primitive, target_version)
.
Note
Standard Attributes are automatically added to OVO fields in base
class. Attributes1 like description
,
created_at
, updated_at
and
revision_number
are added in2.
primary_keys
is used to define the list of fields that
uniquely identify the object. In case of database backed objects, it's
usually mapped onto SQL primary keys. For immutable object fields that
cannot be changed, there is a fields_no_update
list, that
contains primary_keys
by default.
If there is a situation where a field needs to be named differently
in an object than in the database schema, you can use
fields_need_translation
. This dictionary contains the name
of the field in the object definition (the key) and the name of the
field in the database (the value). This allows to have a different
object layer representation for database persisted data. For example in
IP allocation pools:
fields_need_translation = {
'start': 'first_ip', # field_ovo: field_db
'end': 'last_ip'
}
The above dictionary is used in modify_fields_from_db()
and in modify_fields_to_db()
methods which are implemented
in base class and will translate the software layer to database schema
naming, and vice versa. It can also be used to rename
orm.relationship
backed object-type fields.
Most object fields are usually directly mapped to database model
attributes. Sometimes it's useful to expose attributes that are not
defined in the model table itself, like relationships and such. In this
case, synthetic_fields
may become handy. This object
property can define a list of object fields that don't belong to the
object database model and that are hence instead to be implemented in
some custom way. Some of those fields map to
orm.relationships
defined on models, while others are
completely untangled from the database layer.
When exposing existing orm.relationships
as an
ObjectField-typed field, you can use the foreign_keys
object property that defines a link between two object types. When used,
it allows objects framework to automatically instantiate child objects,
and fill the relevant parent fields, based on
orm.relationships
defined on parent models. In order to
automatically populate the synthetic_fields
, the
foreign_keys
property is introduced.
load_synthetic_db_fields()
3
method from NeutronDbObject uses foreign_keys
to match the
foreign key in related object and local field that the foreign key is
referring to. See simplified examples:
class DNSNameServerSqlModel(model_base.BASEV2):
= sa.Column(sa.String(128), nullable=False, primary_key=True)
address = sa.Column(sa.String(36),
subnet_id 'subnets.id', ondelete="CASCADE"),
sa.ForeignKey(=True)
primary_key
class SubnetSqlModel(model_base.BASEV2, HasId, HasProject):
= sa.Column(sa.String(attr.NAME_MAX_LEN))
name = orm.relationship(IPAllocationPoolSqlModel)
allocation_pools = orm.relationship(DNSNameServerSqlModel,
dns_nameservers ='subnet',
backref='all, delete, delete-orphan',
cascade='subquery')
lazy
class IPAllocationPoolSqlModel(model_base.BASEV2, HasId):
= sa.Column(sa.String(36), sa.ForeignKey('subnets.id'))
subnet_id
@obj_base.VersionedObjectRegistry.register
class DNSNameServerOVO(base.NeutronDbObject):
= '1.0'
VERSION = DNSNameServerSqlModel
db_model
# Created based on primary_key=True in model definition.
# The object is uniquely identified by the pair of address and
# subnet_id fields. Override the default 'id' 1-tuple.
= ['address', 'subnet_id']
primary_keys
# Allow to link DNSNameServerOVO child objects into SubnetOVO parent
# object fields via subnet_id child database model attribute.
# Used during loading synthetic fields in SubnetOVO get_objects.
= {'SubnetOVO': {'subnet_id': 'id'}}
foreign_keys
= {
fields 'address': obj_fields.StringField(),
'subnet_id': common_types.UUIDField(),
}
@obj_base.VersionedObjectRegistry.register
class SubnetOVO(base.NeutronDbObject):
= '1.0'
VERSION = SubnetSqlModel
db_model
= {
fields 'id': common_types.UUIDField(), # HasId from model class
'project_id': obj_fields.StringField(nullable=True), # HasProject from model class
'subnet_name': obj_fields.StringField(nullable=True),
'dns_nameservers': obj_fields.ListOfObjectsField('DNSNameServer',
=True),
nullable'allocation_pools': obj_fields.ListOfObjectsField('IPAllocationPoolOVO',
=True)
nullable
}
# Claim dns_nameservers field as not directly mapped into the object
# database model table.
= ['allocation_pools', 'dns_nameservers']
synthetic_fields
# Rename in-database subnet_name attribute into name object field
= {
fields_need_translation 'name': 'subnet_name'
}
@obj_base.VersionedObjectRegistry.register
class IPAllocationPoolOVO(base.NeutronDbObject):
= '1.0'
VERSION = IPAllocationPoolSqlModel
db_model
= {
fields 'subnet_id': common_types.UUIDField()
}
= {'SubnetOVO': {'subnet_id': 'id'}} foreign_keys
The foreign_keys
is used in SubnetOVO
to
populate the allocation_pools
4
synthetic field using the IPAllocationPoolOVO
class. Single
object type may be linked to multiple parent object types, hence
foreign_keys
property may have multiple keys in the
dictionary.
Note
foreign_keys
is declared in related object
IPAllocationPoolOVO
, the same way as it's done in the SQL
model IPAllocationPoolSqlModel
:
sa.ForeignKey('subnets.id')
Note
Only single foreign key is allowed (usually parent ID), you cannot link through multiple model attributes.
It is important to remember about the nullable parameter. In the
SQLAlchemy model, the nullable parameter is by default
True
, while for OVO fields, the nullable is set to
False
. Make sure you correctly map database column
nullability properties to relevant object fields.
Synthetic fields
synthetic_fields
is a list of fields, that are not
directly backed by corresponding object SQL table attributes. Synthetic
fields are not limited in types that can be used to implement them.
= {
fields 'dhcp_agents': obj_fields.ObjectField('NetworkDhcpAgentBinding',
=True), # field that contains another single NeutronDbObject of NetworkDhcpAgentBinding type
nullable'shared': obj_fields.BooleanField(default=False),
'subnets': obj_fields.ListOfObjectsField('Subnet', nullable=True)
}
# All three fields do not belong to corresponding SQL table, and will be
# implemented in some object-specific way.
= ['dhcp_agents', 'shared', 'subnets'] synthetic_fields
ObjectField
and ListOfObjectsField
take the
name of object class as an argument.
Implementing custom synthetic fields
Sometimes you may want to expose a field on an object that is not
mapped into a corresponding database model attribute, or its
orm.relationship
; or may want to expose a
orm.relationship
data in a format that is not directly
mapped onto a child object type. In this case, here is what you need to
do to implement custom getters and setters for the custom field. The
custom method to load the synthetic fields can be helpful if the field
is not directly defined in the database, OVO class is not suitable to
load the data or the related object contains only the ID and property of
the parent object, for example subnet_id
and property of
it: is_external
.
In order to implement the custom method to load the synthetic field,
you need to provide loading method in the OVO class and override the
base class method from_db_object()
and
obj_load_attr()
. The first one is responsible for loading
the fields to object attributes when calling get_object()
and get_objects()
, create()
and
update()
. The second is responsible for loading attribute
when it is not set in object. Also, when you need to create related
object with attributes passed in constructor, create()
and
update()
methods need to be overwritten. Additionally
is_external
attribute can be exposed as a boolean, instead
of as an object-typed field. When field is changed, but it doesn't need
to be saved into database, obj_reset_changes()
can be
called, to tell OVO library to ignore that. Let's see an example:
@obj_base.VersionedObjectRegistry.register
class ExternalSubnet(base.NeutronDbObject):
= '1.0'
VERSION = {'subnet_id': common_types.UUIDField(),
fields 'is_external': obj_fields.BooleanField()}
= ['subnet_id']
primary_keys = {'Subnet': {'subnet_id': 'id'}}
foreign_keys
@obj_base.VersionedObjectRegistry.register
class Subnet(base.NeutronDbObject):
= '1.0'
VERSION = {'external': obj_fields.BooleanField(nullable=True),}
fields = ['external']
synthetic_fields
# support new custom 'external=' filter for get_objects family of
# objects API
def __init__(self, context=None, **kwargs):
super(Subnet, self).__init__(context, **kwargs)
self.add_extra_filter_name('external')
def create(self):
= self.get_changes()
fields with db_api.context_manager.writer.using(context):
if 'external' in fields:
=self.id,
ExternalSubnet(context, subnet_id=fields['external']).create()
is_external# Call to super() to create the SQL record for the object, and
# reload its fields from the database, if needed.
super(Subnet, self).create()
def update(self):
= self.get_changes()
fields with db_api.context_manager.writer.using(context):
if 'external' in fields:
# delete the old ExternalSubnet record, if present
obj_db_api.delete_objects(self.obj_context, ExternalSubnet.db_model,
=self.id)
subnet_id# create the new intended ExternalSubnet object
=self.id,
ExternalSubnet(context, subnet_id=fields['external']).create()
is_external# calling super().update() will reload the synthetic fields
# and also will update any changed non-synthetic fields, if any
super(Subnet, self).update()
# this method is called when user of an object accesses the attribute
# and requested attribute is not set.
def obj_load_attr(self, attrname):
if attrname == 'external':
return self._load_external()
# it is important to call super if attrname does not match
# because the base implementation is handling the nullable case
super(Subnet, self).obj_load_attr(attrname)
def _load_external(self, db_obj=None):
# do the loading here
if db_obj:
# use DB model to fetch the data that may be side-loaded
= db_obj.external.is_external if db_obj.external else None
external else:
# perform extra operation to fetch the data from DB
= ExternalSubnet.get_object(context,
external_obj =self.id)
subnet_id= external_obj.is_external if external_obj else None
external
# it is important to set the attribute and call obj_reset_changes
setattr(self, 'external', external)
self.obj_reset_changes(['external'])
# this is defined in NeutronDbObject and is invoked during get_object(s)
# and create/update.
def from_db_object(self, obj):
super(Subnet, self).from_db_object(obj)
self._load_external(obj)
In the above example, the get_object(s)
methods do not
have to be overwritten, because from_db_object()
takes care
of loading the synthetic fields in custom way.
Standard attributes
The standard attributes are added automatically in metaclass
DeclarativeObject
. If adding standard attribute, it has to
be added in
neutron/objects/extensions/standardattributes.py
. It will
be added to all relevant objects that use the
standardattributes
model. Be careful when adding something
to the above, because it could trigger a change in the object's
VERSION
. For more on how standard attributes work, check5.
RBAC handling in objects
The RBAC is implemented currently for resources like: Subnet(*), Network and QosPolicy. Subnet is a special case, because access control of Subnet depends on Network RBAC entries.
The RBAC support for objects is defined in
neutron/objects/rbac_db.py
. It defines new base class
NeutronRbacObject
. The new class wraps standard
NeutronDbObject
methods like create()
,
update()
and to_dict()
. It checks if the
shared
attribute is defined in the fields
dictionary and adds it to synthetic_fields
. Also,
rbac_db_model
is required to be defined in Network and
QosPolicy classes.
NeutronRbacObject
is a common place to handle all
operations on the RBAC entries, like getting the info if resource is
shared or not, creation and updates of them. By wrapping the
NeutronDbObject
methods, it is manipulating the 'shared'
attribute while create()
and update()
methods
are called.
The example of defining the Network OVO:
class Network(standard_attr.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasProject):"""Represents a v2 neutron network."""
= sa.Column(sa.String(attr.NAME_MAX_LEN))
name = orm.relationship(rbac_db_models.NetworkRBAC,
rbac_entries ='network', lazy='joined',
backref='all, delete, delete-orphan')
cascade
# Note the base class for Network OVO:
@obj_base.VersionedObjectRegistry.register
class Network(rbac_db.NeutronRbacObject):
# Version 1.0: Initial version
= '1.0'
VERSION
# rbac_db_model is required to be added here
= rbac_db_models.NetworkRBAC
rbac_db_model = models_v2.Network
db_model
= {
fields 'id': common_types.UUIDField(),
'project_id': obj_fields.StringField(nullable=True),
'name': obj_fields.StringField(nullable=True),
# share is required to be added to fields
'shared': obj_fields.BooleanField(default=False),
}
Note
The shared
field is not added to the
synthetic_fields
, because NeutronRbacObject
requires to add it by itself, otherwise ObjectActionError
is raised.6
Extensions to neutron resources
One of the methods to extend neutron resources is to add an arbitrary
value to dictionary representing the data by providing
extend_(subnet|port|network)_dict()
function and defining
loading method.
From DB perspective, all the data will be loaded, including all
declared fields from DB relationships. Current implementation for core
resources (Port, Subnet, Network etc.) is that DB result is parsed by
make_<resource>_dict()
and
extend_<resource>_dict()
. When extension is enabled,
extend_<resource>_dict()
takes the DB results and
declares new fields in resulting dict. When extension is not enabled,
data will be fetched, but will not be populated into resulting dict,
because extend_<resource>_dict()
will not be
called.
Plugins can still use objects for some work, but then convert them to dicts and work as they please, extending the dict as they wish.
For example:
class TestSubnetExtension(model_base.BASEV2):
= sa.Column(sa.String(36),
subnet_id 'subnets.id', ondelete="CASCADE"),
sa.ForeignKey(=True)
primary_key= sa.Column(sa.String(64))
value = orm.relationship(
subnet
models_v2.Subnet,# here is the definition of loading the extension with Subnet model:
=orm.backref('extension', cascade='delete', uselist=False))
backref
@oslo_obj_base.VersionedObjectRegistry.register_if(False)
class TestSubnetExtensionObject(obj_base.NeutronDbObject):
# Version 1.0: Initial version
= '1.0'
VERSION
= TestSubnetExtension
db_model
= {
fields 'subnet_id': common_types.UUIDField(),
'value': obj_fields.StringField(nullable=True)
}
= ['subnet_id']
primary_keys = {'Subnet': {'subnet_id': 'id'}}
foreign_keys
@obj_base.VersionedObjectRegistry.register
class Subnet(base.NeutronDbObject):
# Version 1.0: Initial version
= '1.0'
VERSION
= {
fields 'id': common_types.UUIDField(),
'extension': obj_fields.ObjectField(TestSubnetExtensionObject.__name__,
=True),
nullable
}
= ['extension']
synthetic_fields
# when defining the extend_subnet_dict function:
def extend_subnet_dict(self, session, subnet_ovo, result):
= subnet_ovo.extension.value if subnet_ovo.extension else ''
value 'subnet_extension'] = value result[
The above example is the ideal situation, where all extensions have objects adopted and enabled in core neutron resources.
By introducing the OVO work in tree, interface between base plugin
code and registered extension functions hasn't been changed. Those still
receive a SQLAlchemy model, not an object. This is achieved by capturing
the corresponding database model on get_***/create/update
,
and exposing it via <object>.db_obj
Backward compatibility for tenant_id
All objects can support tenant_id
and
project_id
filters and fields at the same time; it is
automatically enabled for all objects that have a
project_id
field. The base NeutronDbObject
class has support for exposing tenant_id
in dictionary
access to the object fields (subnet['tenant_id']
) and in
to_dict()
method. There is a tenant_id
read-only property for every object that has project_id
in
fields
. It is not exposed in
obj_to_primitive()
method, so it means that
tenant_id
will not be sent over RPC callback wire. When
talking about filtering/sorting by tenant_id
, the filters
should be converted to expose project_id
field. This means
that for the long run, the API layer should translate it, but as
temporary workaround it can be done at DB layer before passing filters
to objects get_objects()
method, for example:
def convert_filters(result):
if 'tenant_id' in result:
'project_id'] = result.pop('tenant_id')
result[return result
def get_subnets(context, filters):
= convert_filters(**filters)
filters return subnet_obj.Subnet.get_objects(context, **filters)
The convert_filters
method is available in
neutron.objects.utils
7.
References
https://git.openstack.org/cgit/openstack/neutron/tree/neutron/objects/base.py?h=stable/ocata#n258↩︎
https://git.openstack.org/cgit/openstack/neutron/tree/neutron/db/standard_attr.py?h=stable/ocata↩︎
https://git.openstack.org/cgit/openstack/neutron/tree/neutron/objects/base.py?h=stable/ocata#n516↩︎
https://git.openstack.org/cgit/openstack/neutron/tree/neutron/objects/base.py?h=stable/ocata#n542↩︎
https://docs.openstack.org/neutron/latest/contributor/internals/db_layer.html#the-standard-attribute-table↩︎
https://git.openstack.org/cgit/openstack/neutron/tree/neutron/objects/rbac_db.py?h=stable/ocata#n291↩︎
https://git.openstack.org/cgit/openstack/neutron/tree/neutron/objects/utils.py?h=stable/ocata↩︎