Writing Synchronizers

Synchronizers are the components of CORD that map the abstract declarative state about how the system is suppose to behave (as defined by the XOS data model) into the concrete operational state of the backend components that implement the system (e.g., VNFs, micro-services, SDN control applications).

Writing a Synchronizer is half of the work required to on-board a service into CORD. First a model for the service is written as an xproto specification, and then you implement a synchronizer that translates that model onto some backend component.

To implement a Synchronizer, it is important to first understand the role they play in CORD and the assumptions made about their behavior. The next section describes a set of design guidelines and the following one presents the implementation details.

Overview of the synchronizer framework APIs

This section is intended to be a reference for the commonly used APIs exposed by the synchronizer framework.

Convenience Methods

As part of the model definition is possible to extend the autogenerated gRPC APIs with custom methods, for example to facilitate access to some kind of data from the synchronizers.

convenience_methods can be defined in a folder (convenience) that needs to be a child of the models_dir as per your configuration.

For example if your configuration contains:

models_dir: "/opt/xos/synchronizers/<synchronizer_name>/models"

then you convenience methods needs to be located in /opt/xos/synchronizers/<synchronizer_name>/models/convenience

Assuming our model definition looks like:

message MyModel (XOSBase){
    required string first_name = 1 [null = False, blank = False];
    required string last_name = 2 [null = False, blank = False];
}

here is an example of a basic convenience methods that will expose a full_name property over the APIs used by the synchronizers:

from xosapi.orm import ORMWrapper, register_convenience_wrapper

from xosconfig import Config
from multistructlog import create_logger

log = create_logger(Config().get('logging'))

class ORMWrapperMyModel(ORMWrapper):
    
    @property
    def full_name(self):
        return "%s %s" % (self.first_name, self.last_name)

register_convenience_wrapper("MyModel", ORMWrapperMyModel)

NOTE: The convenience methods will be loaded in all the synchronizer containers so that they can be used in multiple places.

Model Policies

Model Policies can be seen as post-save hooks and they are generally defined in the xos/synchronizer/model_policies folder of your service.

Model policies are generally used to dynamically create a service chain (when a ServiceInstance is created it will create a ServiceInstance of its east side Service)

NOTE: You'll need to add this folder in your synchronizer configuration file as:

model_policies_dir: "/opt/xos/synchronizers/<synchronizer_name>/model_policies"

A model policy is a class that inherits from Policy:

from synchronizers.new_base.modelaccessor import MyServiceInstance, ServiceInstanceLink, model_accessor
from synchronizers.new_base.policy import Policy

class MyServiceInstancePolicy(Policy):
    model_name = "MyServiceInstance"

and overrides one or more of the following methods:

def handle_create(self, model):
def handle_update(self, model):
def handle_delete(self, model):

Where model is the instance of the model that has been created.

Sync Steps

Sync Steps are the actual piece of code that provide the mapping between your models and your backend. You will need to define a sync step for each model.

NOTE: You'll need to add this folder in your synchronizer configuration file as:

steps_dir: "/opt/xos/synchronizers/<synchronizer_name>/steps"

A Sync Step is a class that inherits from SyncStep:


from synchronizers.new_base.SyncInstanceUsingAnsible import SyncStep from synchronizers.new_base.modelaccessor import MyModel from xosconfig import Config from multistructlog import create_logger log = create_logger(Config().get('logging')) class SyncMyModel(SyncStep): provides = [MyModel] observes = MyModel

and provides these methods:

def sync_record(self, o):
    log.info("sync'ing object", object=str(o), **o.tologdict())
def delete_record(self, o):
    log.info("deleting object", object=str(o), **o.tologdict())

This methods will be invoked anytime there is change in the model passing as argument the changed models. After performing the required operations to sync the model state with the backend state the synchronizer framework will update the models with the operational informations needed.

Pull Steps

Pull Steps can be used to observe the surrounding environment and update the data-model accordingly.

NOTE: You'll need to add this folder in your synchronizer configuration file as:

pull_steps_dir: "/opt/xos/synchronizers/<synchronizer_name>/pull_steps"

A Sync Step is a class that inherits from PullStep

from synchronizers.new_base.pullstep import PullStep
from synchronizers.new_base.modelaccessor import OLTDevice

from xosconfig import Config
from multistructlog import create_logger

log = create_logger(Config().get('logging'))

from synchronizers.new_base.modelaccessor import MyModel

class MyModelPullStep(PullStep):
    def __init__(self):
        super(MyModelPullStep, self).__init__(observed_model=OLTDevice)

and override the following method:

def pull_records(self):
    log.info("pulling MyModels")
    # create an empty model
    o = MyModel()
    # code to fetch information
    # populate the model
    o.first_name = "John"
    o.last_name = "Doe"
    o.no_sync = True # this is required to prevent the synchronizer to be invoked and start a loop
    o.save()