XOS Modeling Framework
XOS defines a modeling framework: a language for specifying data models (xproto) and a tool chain for generating code based on the set of models (xosgenx).
The xproto language is based on Google’s protocol buffers (protobufs), borrowing their syntax, but extending their semantics to express additional behavior. Although these extensions can be written in syntactically valid protobufs (using the protobuf option feature), the resulting model definitions are cumbersome and the semantics are under-specified.
Whereas protobufs primarily facilitate one operation on models, namely, data serialization, xproto goes beyond protobufs to provide a framework for implementing custom operators.
Users are free to define models using standard protobufs instead of the xproto syntax, but doing so obscures the fact that packing new behavior into the options field renders protobuf’s semantics under-specified. Full details are given below, but as two examples: (1) xproto supports relationships (foreign keys) among objects defined by the models, and (2) xproto supports boolean predicates (policies) that can be applied to objects defined by the models.
The xosgenx tool chain generates code based on a set of models loaded into the XOS Core. This tool chain can be used to produce multiple targets, including:
- Object Relation Mapping (ORM) – maps the data model onto a persistent database.
- gRPC Interface – how all the other containers communicate with XOS Core.
- TOSCA API – one of the UI/Views used to access CORD.
- Security Policies – governs which principals can read/write which objects.
- Synchronizer Framework – execution environment in which Ansible playbooks run.
- Unit Tests – auto-generate API unit tests.
The rest of this section describes xproto: first the models and then policies that can be applied to the models.
Models
The xproto syntax for models is based on Google Protobufs. This means that any protobuf file also qualifies as xproto. We currently use the Protobuf v2 syntax. For example, the file below specifies a model that describes container images:
message Image {
required string name = 1 [db_index = False, max_length = 256, null = False, content_type = "stripped", blank = False];
required string kind = 2 [default = "vm", choices = "(('vm', 'Virtual Machine'), ('container', 'Container'))", max_length = 30, blank = False, null = False, db_index = False];
required string disk_format = 3 [db_index = False, max_length = 256, null = False, content_type = "stripped", blank = False];
required string container_format = 4 [db_index = False, max_length = 256, null = False, content_type = "stripped", blank = False];
optional string path = 5 [max_length = 256, content_type = "stripped", blank = True, help_text = "Path to image on local disk", null = True, db_index = False];
optional string tag = 6 [max_length = 256, content_type = "stripped", blank = True, help_text = "For Docker Images, tag of image", null = True, db_index = False];
}
We use standard protobuf scalar types, for example: int32
, uint32
,
string
, bool
, and float
.
xproto contains several extensions, encoded as Protobuf options, which the xosgenx toolchain recognizes at the top level. The xproto extensions to Google Protobufs are as follows.
Inheritance
Inheritance instructs the xproto processor that a model inherits the fields of a set of base models. These base model fields are not copied into the derived model automatically. However, the fields can be accessed in an xproto target.
xproto
message EC2Instance (Instance, EC2Object) { // EC2Instance inherits the fields of Instance }
protobuf
message EC2Instance { option bases = "Instance,EC2Object" }
Links
Links are references to one model from another. A link specifies the type of the reference (manytoone, manytomany, onetomany, or onetoone), name of the field that contains the reference (slice in the following example), its type (e.g., Slice), the name of the field in the peer model that points back to the current model, and a “through” field, specifying a model declared separately as an xproto message, that stores properties of the link.
Links may be considered bidirectional, a link from Model A to Model B creates an implicit link from B back to A. Due to this bidirectionality, links typically have two protobuf field numbers associated with them. The first number is the field number in the model that is declaring the link. The second number is the field number in the model that is implicitly declaring the reverse link. Reverse link numbers are by convention allocated by the developer starting at 1001. The developer is responsible for ensuring reverse link numbers do not collide with respect to the destination model.
xproto
message Instance { required manytoone slice:Slice->instances = 1:1001; }
protobuf
message Instance { required int32 slice = 1 [model="Slice", link="manytoone", src_port="slice", dst_port="instances"]; } message Slice { ... other fields declared in the slice model ... repeated int32 instances_ids = 1001 [(reverseForeignKey).modelName = "Instance"]; }
The example shown below illustrates a manytomany link from Image to Deployment,
which goes through the model ImageDeployments
:
xproto
message Image { required manytomany deployments->Deployment/ImageDeployments:images = 7:1003 [help_text = "Select which images should be instantiated on this deployment", null = False, db_index = False, blank = True]; }
protobuf
message Image { required int32 deployments = 7 [help_text = "Select which images should be instantiated on this deployment", null = False, db_index = False, blank = True, model="Deployment", through="ImageDeployments", dst_port="images", link="manytomany"]; } message Deployment { ... other fields declared in the Deployment model ... repeated int32 images_ids = 1003 [(reverseForeignKey).modelName = "Image")]; }
Access Policies
Associates a policy (a boolean expression) with a model to control access to
instances of that model. How policies (e.g., slicle_policy
) are specified is
described below.
message Slice::slice_policy (XOSBase) {
…
}
Model Options
Model Options declare information about models. They can be declared for individual models, or at the top level in the xproto definition, in which case they are inherited by all of the models in the file, unless they are overridden by a particular model.
Currently supported model options include: name
, app_label
, verbose_name
,
custom_python
, tosca_description
, validators
, plural
, singular
, sync_implemented
, policy_implemented
and
gui_hidden
.
The name option is a short name used to refer to your service. For example, in
the Virtual Subscriber Gateway service, the name option is set to vSG
.
option name = "vSG"
The app_label option is a short programmatic name that does not need to be easily understood by humans. It should not include whitespaces, and should preferrably be all lowercase. If app_label is not specified, then its value defaults to the name option described above.
option app_label = "vsg"
The verbose_name option contains a short description of the service.
option verbose_name = "Virtual Subscriber Gateway Service";
The custom_python
option allows custom code to be attached to model.
option custom_python = "True"
The custom_python
option is for services that require custom Python code in their
generated models. This option may either be specified at the file level and apply
to all models within a file, or individually at the model level within a file.
When custom python models are in use, the following changes occur to the generative
toolchain,
- Models are created in a file called "models_decl.py" (or modelname_decl.py depending on whether you're working on a service or in the core).
- Autogenerated models have the suffix
_decl
attached to them, for exampleVSGService_decl
. - If there's a mix of custom_python and non-custom_python models in the same file, then
the non-custom_python models will automatically receive stubs that convert them
from the
_decl
model to the non-_decl
model.
Once the developer has designated some models as having custom python, it is then
up to the service developer to provide the final
models. The code below gives an example of custom models that inherit from such
intermediate decl
models:
class VSGService(VSGService__decl):
def __xos_base_save(self, *args, **kwargs):
self.prop1 = self.prop2 + self.prop3
pass
You can use the xproto service_extender
target to generate a stub for your
final model definitions.
NOTE: As custom_python code ends up extending the XOS core with service-specific code, it is recommended to avoid using custom_python if an alternative exists to achieve the same functionality. For example,
model_policies
are another way to attach service-specific features to models, and run in the synchronizer rather than in the core.NOTE: The
custom_python
option was previously calledlegacy
, and you may still encounter some services that use the keywordlegacy
instead of custom_python.
The plural and singular options provide the grammatically correct plural and singular forms of your model name to ensure that autogenerated API endpoints are valid.
option singular = "slice" # Singular of slice is not slouse, as computed by Python's pattern.en library
option plural = "ports" # Plural of ports is not portss
The tosca_description option is a description for the service entry in the autogenerated TOSCA schema.
The validators
option contains a set of declarative object validators applied
to every object of the present model when it is saved. Validators are a comma
separated list of tuples, where the two elements of each tuple are separated by
a ':'. The first element of the tuple is a reference to an XOS policy
(described in another section of this document). The second element is an error
message that is returned to an API client that attempts an operation that does
not pass validation.
option validators = "instance_creator:Instance has no creator, instance_isolation: Container instance {obj.name} must use container image, instance_isolation_container_vm_parent:Container-vm instance {obj.name} must have a parent";
How validators are used is described in the Policies
section later in this document.
The gui_hidden option is a directive to the XOS GUI to exclude the present model from the default view provided to users.
option gui_hidden = "True";
Then sync_implemented
and policy_implemented
are used to identify a model that implements respectively a
sync_step
or a model_policy
.
option sync_implemented = "True";
option policy_implemented = "True";
Field Options
Options are also supported on a per-field basis. The following lists the currently available field options.
auto_now_add
fields: string with content_type=date
The auto_now_add
option will set the field to the current datetime when the object is created.
Note that this option is mutually exclusive with the default
option. This option is only
usable on strings whose content_type
is set to date
.
option auto_now_add = True;
blank
fields: string, int32, float, manytoone, manytomany
Whether a field can be empty:
option blank = False;
The blank
option is generally implied by whether a field has a required
or optional
modifier and it is not necessary to explicitly specify the blank
option. Note that the permitted fields specifically excludes booleans. Boolean fields must either be true
or false
and cannot be blank.
bookkeeping_state
fields: all
Designates a field as bookkeeping_state
, by default hiding it from GUI or CLI tools.
option bookkeeping_state = True;
choices
fields: string
The set of valid values for a field, where each inner-tuple specifies equivalence classes (e.g., vm is equivalent to Virtual Machine):
option choices = "(('vm', 'Virtual Machine'), ('container', 'Container'))";
content_type
fields: string
How to interpret/parse string fields:
option content_type = “stripped”;
option content_type = “date”;
option content_type = “url”;
option content_type = “ip”;
The content type controls how the field is encoded in Django and what database schema is created by Django. The currently supported content types are implemented as follows:
- stripped. Implemented as a StrippedCharField, which is a derivative of CharField. Leading and trailing spaces are removed.
- date. Implemented as a DateTimeField.
- url. Implemented as a URLField, which is a derivative of CharField. Enforces URL syntax validation.
- ip. Implemented as a GenericIPAddressField. Capable of storing IPv4 or IPv6 addresses.
Content type may imply validation semantics. For example, since a GenericIPAddressField
is only capable of storing an IPv4 or IPv6 addresses, attempting to store something else will fail. If no content type is specified for a string field, then CharField
or TextField
will be used depending on whether or not the field has a max_length
or text=True
option specified.
db_index
fields: all
Whether the field is an index field by the XOS core. Index fields permit more efficient searching.
option db_index = True;
default
fields: all
The default value of the field:
option default = “Default value of field”;
The default option is required on boolean fields.
feedback_state
fields: all
Designates a field as feedback_state
, by default preventing it from being updated using GUI
or CLI tools.
option feedback_state = True;
gui_hidden
fields: all
Do not display this field in the GUI (also available at the model level):
option gui_hidden = True;
help_text
fields: all
Help text describes a field. This descriptive text is often displayed in GUI tools when editing existing models or adding new models. For example:
option help_text = “Descriptive text goes here”;
max_length
fields: string
The maximum length of a field whose type is string:
option max_length = 128;
A string field must either specify a max_length
or use text=True
to indicate the string is of variable length.
The maximum length must be greater than zero.
If a maximum length is near but not equal to 256
or 1024
(for example, 254
or 1024
) then it is recommended to use the convention of 256
or 1024
.
max_value / min_value
fields: int32
A min/max value for the field
option min_value = 10;
option max_value = 100;
null
fields: all
The null option specifies whether a field allows a null
value to be stored in the database. Note that null
is a different value than the empty string or the number 0
.
option null = True
The null
option is generally implied by whether a field has a required
or optional
modifier and it is not necessary to explicitly specify the null
option. The only exception to this rule are the manytomany
and bool
fields. For manytomany
fields, it is up to the developer to choose a setting for null
.
For boolean fields null=False
is always the case.
There are limitations on the XOS API on when null
values can be saved to a field. The API permits them to be saved to link fields such as manytoone
or manytomany
fields but the API does not permit saving null
to a string, int32, or other type of non-link field. In these cases although the database permits storing the null
value, there is no practical way to set the null
value over the API.
text
fields: string
The text option is set to true
to convey that a string has no maximum length. This option is mutually exclusive with max_length
.
option text = True
tosca_key, tosca_key_one_of
fields: all
Identify a field that is used as key by the TOSCA engine. A model can have multiple keys in case we need a composite key:
option tosca_key = True;
Identify a field that is used as key by the TOSCA engine. This needs to be used in case a composite key can be composed by different combination of fields:
tosca_key_one_of = "<field_name>"
For example, in the ServiceInstanceLink
model:
message ServiceInstanceLink (XOSBase) {
required manytoone provider_service_instance->ServiceInstance:provided_links = 1 [db_index = True, null = False, blank = False, tosca_key=True];
optional manytoone provider_service_interface->ServiceInterface:provided_links = 2 [db_index = True, null = True, blank = True];
optional manytoone subscriber_service_instance->ServiceInstance:subscribed_links = 3 [db_index = True, null = True, blank = True];
optional manytoone subscriber_service->Service:subscribed_links = 4 [db_index = True, null = True, blank = True, tosca_key_one_of=subscriber_service_instance];
optional manytoone subscriber_network->Network:subscribed_links = 5 [db_index = True, null = True, blank = True, tosca_key_one_of=subscriber_service_instance];
}
the key is composed by provider_service_instance
and one of
subscriber_service_instance
, subscriber_service
, subscriber_network
unique
fields: all
Signifies that this field is unique. Two different objects with the same field value should not be permitted.
unique = True
unique_with
fields: all
Signifies that this field, in combination with other fields, forms a composite key that is unique.
Two different objects with the same set of field values for the unique_together
keys should not be permitted.
unique_with = "service_instance"
In the following example, the network
and service_instance
fields form a unique composite key.
message Port::port_policy (XOSBase) {
required manytoone network->Network:links = 1:1003 [db_index = True, blank = False, unique_with = "service_instance", help_text = "Network bound to this port"];
optional manytoone service_instance->ServiceInstance:ports = 7:1001 [db_index = True, blank = True, help_text = "ServiceInstance bound to this port"];
}
verbose_name
fields: all
A label to be used by the GUI display for this field:
option verbose_name = “Verbose name goes here”;
Naming Conventions
Model names should use CamelCase without underscore. Model names should
always be singular, never plural. For example: Slice
, Network
, Site
.
Sometimes a model is used to relate two other models, and should be named after
the two models that it relates. For example, a model that relates the
Controller
and User
models should be called ControllerUser
.
Field names use lower-case with underscores separating names. Examples of valid
field names are: name, disk_format
, controller_format
.
Declarative vs Feedback vs Bookkeeping State
By convention, the fields that make up a model are classified as holding one of three kinds of state: declarative, feedback, or bookkeeping.
Fields set by the operator to specify (declare) the expected state of CORD's underlying components are said to hold declarative state. In contrast, fields that record operational data reported from CORD's underlying (backend) components are said to hold feedback state.
bookkeeping state are fields that are typically used by the synchronizer framework or by a synchronizer to hold information about the model that is useful to the synchronizer, but not to the operator. For example, timestamps that are used to track when models are dirty, error retry information, etc.
For more information about declarative and feedback state, and the role they play in synchornizing the data model with the backend components, read about the Synchronizer Architecture.
Policies
Policies are boolean expressions that can be associated with models. Consider
two examples. In the first, grant_policy
is a predicate applied to instances
of the Privilege
model. It is used to generate and inject security checks
into the API.
policy grant_policy < ctx.user.is_admin
| exists Privilege:Privilege.object_type = obj.object_type
& Privilege.object_id = obj.object_id
& Privilege.accessor_type = "User"
& Privilege.accessor_id = ctx.user.id
& Privilege.permission = "role:admin" >
message Privilege::grant_policy (XOSBase) {
required int32 accessor_id = 1 [null = False];
required string accessor_type = 2 [null = False, max_length=1024];
required int32 controller_id = 3 [null = True];
required int32 object_id = 4 [null = False];
required string object_type = 5 [null = False, max_length=1024];
required string permission = 6 [null = False, default = "all", max_length=1024];
required string granted = 7 [content_type = "date", auto_now_add = True, max_length=1024];
required string expires = 8 [content_type = "date", null = True, max_length=1024];
}
The policy is executed relative to three implied inputs:
- The object on which the policy is invoked (e.g.,
obj.object_type
). - The context in which the policy is invoked (e.g.,
cxt.user
). - The data model as a whole (e.g.,
exists Privilege:Privilege.accessor_id = ctx.user.id
).
Available context information includes the principal that invoked the operation
(ctx.user
) and the type of access that principal is requesting
(ctx.write_access
and ctx.read_access
).
A second example involves the Port
model and two related policies,
port_validator
and port_policy
.
policy port_validator < (obj.instance.slice in obj.network.permitted_slices.all()) | (obj.instance.slice = obj.network.owner) | obj.network.permit_all_slices >
policy port_policy < *instance_policy(instance) & *network_policy(network) >
message Port::port_policy (XOSBase) {
option validators = "port_validator:Slice is not allowed to connect to network";
required manytoone network->Network:links = 1 [db_index = True, null = False, blank = False, unique_with = "instance"];
optional manytoone instance->Instance:ports = 2 [db_index = True, null = True, blank = True];
optional string ip = 3 [max_length = 39, content_type = "ip", blank = True, help_text = "Instance ip address", null = True, db_index = False];
optional string port_id = 4 [help_text = "Neutron port id", max_length = 256, null = True, db_index = False, blank = True];
optional string mac = 5 [help_text = "MAC address associated with this port", max_length = 256, null = True, db_index = False, blank = True];
required bool xos_created = 6 [default = False, null = False, db_index = False, blank = True];
}
Similar to the previous example, port_policy
is associated with the Port
model, but unlike grant_policy
shown above (which is an expression over a set
of objects in the data model), port_policy
is defined by reference to two
other policies: instance_policy
and network_policy
(not shown).
This example also shows the use of validators, which enforce invariants on
how objects of a given model are used. In this case, policy port_validator
checks to make sure the slice associated with a given port is included in the
set of permitted networks.
Policy expressions may include the following operators:
- conjunction (
&
), - disjunction (
|
), - equality (
=
), - negation (
not
), - set membership (
in
), - implication (
->
), - qualifiers (
exists
,forall
), - sub-policy reference (
* <policy name>
), - python escapes (
{{ python expression }}
).