This section walks you through the process of writing an XOS model and synchronizer. It does not provide exhaustive coverage, but it does guide you through the most common use cases, including:
xos-core
.sync_step
for the models.sync_step
(TODO).The following assumes you start with a minikube
deployment of XOS (although any Kubernetes cluster will do), so that running kubectl get pods
returns something similar to:
NAME READY STATUS RESTARTS AGE xos-chameleon-6fb76d5689-s7vxb 1/1 Running 0 21h xos-core-58bcf4f477-79hs7 1/1 Running 0 21h xos-db-566dd8c6f9-l24h5 1/1 Running 0 21h xos-gui-665c5f85bc-kdmbm 1/1 Running 0 21h xos-redis-5cf77fd49f-fcw5h 1/1 Running 0 21h xos-tosca-69588f677c-77lll 1/1 Running 0 20h xos-ws-748c7f9f75-cnjnh 1/1 Running 0 21h
The tutorial also assumes you have downloaded the XOS source code into directory $SRC_DIR
.
Note: Instructions on how to install XOS on a Kubernetes cluster and download the XOS source code is provided elsewhere in this Guide.
XOS services are located under $SRC_DIR/services
. The first step is to create a new directory to store our models and synchronizer code. We will use the name hello-world
for this example:
cd $SRC_DIR/services mkdir hello-world cd hello-world
Although empty when we start, we will end up with a directory structure that looks like the following. You can look at the corresponding directories of other services in $SRC_DIR/services
for examples.
hello-world ├── Dockerfile.synchronizer ├── VERSION ├── samples │ └── hello-world.yaml └── xos ├── synchronizer │ ├── config.yaml │ ├── hello-world-synchronizer.py │ ├── models │ │ └── hello-world.xproto │ ├── steps │ │ ├── sync_hello_world_service.py │ │ ├── sync_hello_world_service_instance.py │ │ ├── test_sync_hello_world_service.py │ │ └── test_sync_hello_world_service_instance.py │ └── test_config.yaml └── unittest.cfg
Walking through the structure, we see the following:
Dockerfile.synchronizer
specifies the Docker image we will build to run the synchronizer.VERSION
specifies the version of our code; it is reported to the core.xos/synchronizer
contains all the code that will be bundled in the Docker image.Looking at some of the files, we see:
xos/synchronizer/models/hello-world.xproto
contains the model definitions.samples/hello-world.yaml
is an example of a TOSCA recipe to instantiate those models.xos/synchronizer/hello-world-synchronizer.py
is the main synchronizer process.xos/synchronizer/steps/sync_hello_world_service.py
contains the operations needed to synchronize the backend component.The synchronizer entry point (main looping process) is responsible for:
Cut-and-paste the following into hello-world-synchronizer.py
:
import importlib import os import sys from xosconfig import Config config_file = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + '/config.yaml') Config.init(config_file, 'synchronizer-config-schema.yaml') observer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../../synchronizers/new_base") sys.path.append(observer_path) mod = importlib.import_module("xos-synchronizer") mod.main()
In xos/synchronizer/config.yaml
add this content:
name: hello-world accessor: username: admin@opencord.org password: letmein endpoint: xos-core:50051 models_dir: "/opt/xos/synchronizers/hello-world/models" steps_dir: "/opt/xos/synchronizers/hello-world/steps" required_models: - HelloWorldService - HelloWorldServiceInstance logging: version: 1 handlers: console: class: logging.StreamHandler loggers: 'multistructlog': handlers: - console level: DEBUG
This tells the synchronizer framework in a running container where to fine the configuration parameters specific to HelloWorld. By convention, we use /opt/xos
.
For our example, we are going to define the two most common models for an XOS-managed service: HelloWorldService
and HelloWorldServiceInstance
. These are HelloWorld-specific definitions of the two core models:
Service
: defines service-wide parameters/fields.ServiceInstance
: defines subscriber-specific parameters/fields.Open the hello-world.xproto
file and add the following content:
option name = "hello-world"; option app_label = "hello-world"; message HelloWorldService (Service){ required string hello_from = 1 [help_text = "The name of who is saying hello", null = False, db_index = False, blank = False]; } message HelloWorldServiceInstance (ServiceInstance){ option owner_class_name="HelloWorldService"; required string hello_to = 1 [help_text = "The name of who is being greeted", null = False, db_index = False, blank = False]; }
This specifies two models: HelloWorldService
extends the Service
model, and HelloWorldServiceInstance
extends the ServiceInstance
model. Both of these models inherit the attributes defined in the parent classes, which you can see in file $SRC_DIR/xos-core/core/models/core.xproto
.
Service models are pushed to the core through a mechanism referred to as dynamic onboarding
or dynamic loading
. In practice, when a synchronizer container runs, the first thing it does is to push its models into the core container.
But first we need to build and deploy our synchronizer container in the test environment.
We assume that you are familiar with the Docker concepts of container and image. If not, we encourage you to look here: Docker concepts
The first thing we need to do is to define a Dockerfile
. To do that, open Dockerfile.synchronizer
and add the following content:
FROM xosproject/xos-synchronizer-base:candidate COPY xos/synchronizer /opt/xos/synchronizers/hello-world COPY VERSION /opt/xos/synchronizers/hello-world/ ENTRYPOINT [] WORKDIR "/opt/xos/synchronizers/hello-world" CMD bash -c "python hello-world-synchronizer.py"
This file is used to build our synchronizer container image. As you might have noticed, the container image we're defining inherits FROM xosproject/xos-synchronizer-base:candidate
, so we'll need to obtain that image.
We can use the following commands to do this:
eval $(minikube docker-env) # this will point our shell on the minikube docker daemon docker pull xosproject/xos-synchronizer-base:master docker tag xosproject/xos-synchronizer-base:master xosproject/xos-synchronizer-base:candidate
Now we can build our synchronizer image by executing the following from the $SRC_DIR/service/hello-world
directory:
eval $(minikube docker-env) docker build -t xosproject/hello-world-synchronizer:candidate -f Dockerfile.synchronizer .
You can create a simple Kubernetes resource in a file called kb8s-hello-world.yaml
:
apiVersion: v1 kind: Pod metadata: name: hello-world-synchronizer spec: containers: - name: hello-world-synchronizer image: xosproject/hello-world-synchronizer:candidate volumeMounts: - name: certchain-volume mountPath: /usr/local/share/ca-certificates/local_certs.crt subPath: config/ca_cert_chain.pem volumes: - name: certchain-volume configMap: name: ca-certificates items: - key: chain path: config/ca_cert_chain.pem restartPolicy: Never
and then run it using kubectl create -f kb8s-hello-world.yaml
Check the logs of your synchronizer using:
kubetcl logs -f hello-world-synchronizer
You should see output similar to the following:
Service version is 1.0.0.dev required_models, found: models=HelloWorldService, HelloWorldServiceInstance Loading sync steps step_dir=/opt/xos/synchronizers/hello-world/steps synchronizer_name=hello-world Loaded sync steps steps=[] synchronizer_name=hello-world Skipping event engine due to no event_steps dir. synchronizer_name=hello-world Skipping model policies thread due to no model_policies dir. synchronizer_name=hello-world No sync steps, no policies, and no event steps. Synchronizer exiting. synchronizer_name=hello-world
Check that your models are on-boarded in the XOS GUI by opening the GUI:
minikube service xos-gui
Use the default credentials admin@opencord.org/letmein
to login.
The models you defined earlier in this tutorial are, more precisely, model schema. Once your model schema has been loaded into the XOS core, you can create one or more instances of those models. We typically use a TOSCA recipe to do this.
The XOS TOSCA engine automatically understands workflows for any models that have been loaded into the core. You can consult them at any time connecting to the TOSCA endpoint from a browser:
http://<minikube-ip>:30007
Note: You can find the minikube ip by executing this command on your system:
minikube ip
.
In this page you should find a list of all the available resources. Just search for helloworldservice
and visit the corresponding page at:
http://<minikube-ip>:30007/custom_type/helloworldservice
You will see the TOSCA definition for the HelloWorldService
model.
You can use that (and the HelloWorldServiceInstance
model definition too) to create an instance of both models. For your convenience, save the following content to a file called hello-world-tosca.yaml
tosca_definitions_version: tosca_simple_yaml_1_0 imports: - custom_types/helloworldservice.yaml - custom_types/helloworldserviceinstance.yaml - custom_types/servicegraphconstraint.yaml description: Create an instance of HelloWorldService and one of HelloWorldServiceInstance topology_template: node_templates: service: type: tosca.nodes.HelloWorldService properties: name: HelloWorld hello_from: Jhon Snow serviceinstance: type: tosca.nodes.HelloWorldServiceInstance properties: name: HelloWorld Service Instance hello_to: Daenerys Targaryen constraints: type: tosca.nodes.ServiceGraphConstraint properties: constraints: '["HelloWorld"]'
This TOSCA will create an instance of your service and an instance of your service instance.
You can then submit this TOSCA using this command:
curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X POST --data-binary @hello-world-tosca.yaml http://<minikube-ip>:30007/run Created models: ['service', 'serviceinstance', 'serviceinstance']
Once this command has been executed, connect to the GUI at:
http://<minikube-ip>:30001
and see that your models have been instantiated.
Note: In the home page press the
Service Instances
button to displayServiceInstance
models, and the navigate to theHello World
sub-menu to the left.
Everything up to this point (with the exception of defining the models themselves) is the boilerplate needed to run a synchronizer. It is the sync_step
that is actually responsible for mapping changes in the XOS data model into some action on the backend component XOS is managing.
To keep the tutorial simple we are not going to operate on a real component, but we can demonstrate the basic idea of how a sync_step
interacts with the models.
sync_step
Before continuing, let's remove the container we just deployed. Do this by running:
kubectl delete pod hello-world-synchronizer
To write the sync_step
we need to create two files in xos/synchronizer/sync_step
. The first one synchronizes the HelloWorldService
model and it is called sync_hello_world_service.py
.
Every sync_step
extends the SyncStep
base class, and overrides two methods:
sync_record
delete_record
See the synchronizer reference for more details.
Here is an example of sync_step
that simply logs changes on the HelloWorldService
model:
from synchronizers.new_base.SyncInstanceUsingAnsible import SyncStep from synchronizers.new_base.modelaccessor import HelloWorldService from xosconfig import Config from multistructlog import create_logger log = create_logger(Config().get('logging')) class SyncHelloWorldService(SyncStep): provides = [HelloWorldService] observes = HelloWorldService def sync_record(self, o): log.info("HelloWorldService has been updated!", object=str(o), hello_from=o.hello_from) def delete_record(self, o): log.info("HelloWorldService has been deleted!", object=str(o), hello_from=o.hello_from)
Let's deploy this first step and see what happens. The first thing you need to do is rebuild the synchronizer container:
eval $(minikube docker-env) docker build -t xosproject/hello-world-synchronizer:candidate -f Dockerfile.synchronizer .
Once done, restart it as follows:
kubectl create -f kb8s-hello-world.yaml
At his point, running kubectl logs -f hello-world-synchronizer
should show that your synchronizer is no longer exiting, but is now looping while waiting for changes in the models.
Every time you make a change to the model, you will see:
kubectl logs -f hello-world-synchronizer
)backend_code
and backend status of the model are updatedWhen you make changes to the models (you can do this via the GUI or by updating the TOSCA you created before), you will see a message similar to this one in the logs:
Syncing object model_name=HelloWorldService pk=1 synchronizer_name=hello-world thread_id=140152420452096 HelloWorldService has been updated! hello_from=u'Jhon Snow' object=HelloWorld Synced object model_name=HelloWorldService pk=1 synchronizer_name=hello-world thread_id=140152420452096
Note: The
sync_record
method is triggered also when a model is created, so as soon as you start the synchronizer you will see the above message.
If you delete the model, you'll see the delete_record
method being invoked.
sync_step
We are now going to trigger an error, to demonstrate how the synchronizer framework is going to help us in dealing with them.
Let's start creating the sync_step
for HelloWorldServiceInstance
in a file named sync_hello_world_service.py
.
from synchronizers.new_base.SyncInstanceUsingAnsible import SyncStep, DeferredException from synchronizers.new_base.modelaccessor import HelloWorldServiceInstance from xosconfig import Config from multistructlog import create_logger log = create_logger(Config().get('logging')) class SyncHelloWorldServiceInstance(SyncStep): provides = [HelloWorldServiceInstance] observes = HelloWorldServiceInstance def sync_record(self, o): log.debug("HelloWorldServiceInstance has been updated!", object=str(o), hello_to=o.hello_to) if o.hello_to == "Tyrion Lannister": raise DeferredException("Maybe later") if o.hello_to == "Joffrey Baratheon": raise Exception("Maybe not") log.info("%s is saying hello to %s" % (o.owner.leaf_model.hello_from, o.hello_to)) def delete_record(self, o): log.debug("HelloWorldServiceInstance has been deleted!", object=str(o), hello_to=o.hello_to)
To run this code you will need to:
In this case we are emulating an error in our sync_step
. In reality, this can be caused by a connection error, malformed data, or any of a number of reasons.
Go to the GUI and start playing a little bit with the models!
If you set the HelloWorldServiceInstance.hello_to
property to Tyrion Lannister
you will see this keep popping up:
HelloWorldServiceInstance has been updated! hello_to=u'Tyrion Lannister' object=HelloWorld Service Instance sync step failed! e=DeferredException('Maybe later',) model_name=HelloWorldServiceInstance pk=1 synchronizer_name=hello-world Traceback (most recent call last): File "/opt/xos/synchronizers/new_base/event_loop.py", line 357, in sync_cohort self.sync_record(o, log) File "/opt/xos/synchronizers/new_base/event_loop.py", line 227, in sync_record step.sync_record(o) File "/opt/xos/synchronizers/hello-world/steps/sync_hello_world_service_instance.py", line 18, in sync_record raise DeferredException("Maybe later") DeferredException: Maybe later
Here is what happens when an error occurs. The synchronizer framework will:
backend_code
of that instance to 2
Exception
error in the backend_status
Note: To see
backend_code
andbackend_status
in the GUI you can pressd
to open the debug tab while looking at a model detail view.