.. _xivo-dird-developer:
===========================
xivo-dird developer's guide
===========================
.. figure:: images/startup.png
xivo-dird startup flow
The xivo-dird architecture uses plugins as extension points for most of its
job. It uses `stevedore `_ to do the plugin
instantiation and discovery and `ABC `_
classes to define the required interface.
Plugins in xivo-dird use setuptools' entry points. That means that installing a
new plugin to xivo-dird requires an entry point in the plugin's setup.py. Each
entry point's `namespace` is documented in the appropriate documentation
section. These entry points allow xivo-dird to be able to discover and load
extensions packaged with xivo-dird or installed separately.
Each kind of plugin does a specific job. There are three kinds of plugins in
dird.
#. :ref:`dird-back-end`
#. :ref:`dird-service`
#. :ref:`dird-view`
.. figure:: images/query.png
xivo-dird HTTP query
All plugins are instantiated by the core. The core then keeps a catalogue of
loaded extensions that can be supplied to other extensions.
The following setup.py shows an example of a python library that add a plugin
of each kind to xivo-dird:
.. code-block:: python
:linenos:
:emphasize-lines: 15-26
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import setup
from setuptools import find_packages
setup(
name='Wazo dird plugin sample',
version='0.0.1',
description='An example program',
packages=find_packages(),
entry_points={
'xivo_dird.services': [
'my_service = dummy:DummyServicePlugin',
],
'xivo_dird.backends': [
'my_backend = dummy:DummyBackend',
],
'xivo_dird.views': [
'my_view = dummy:DummyView',
],
}
)
.. _dird-back-end:
Back-End
========
Back-ends are used to query directories. Each back-end implements a way to query
a given directory. Each instance of a given back-end is called a source. Sources
are used by the services to get results from each configured directory.
Given one LDAP back-end, I can configure a source from the LDAP at alpha.example.com and another
source from the other LDAP at beta.example.com. Both of these sources use the LDAP back-end.
Implementation details
----------------------
* Namespace: ``xivo_dird.backends``
* Abstract source plugin: `BaseSourcePlugin `_
* Methods:
* ``name``: the name of the source, typically retrieved from the configuration injected to
``load()``
* ``load(args)``: set up resources used by the plugin, depending on the config.
``args`` is a dictionary containing:
* key ``config``: the source configuration for this instance of the back-end
* key ``main_config``: the whole configuration of xivo-dird
* ``unload()``: free resources used by the plugin.
* ``search(term, args)``: The search method returns a list of dictionary.
* Empty values should be ``None``, instead of empty string.
* ``args`` is a dictionary containing:
* key ``token_infos``: data associated to the authentication token (see :ref:`xivo-auth`)
* ``first_match(term, args)``: The first_match method returns a dictionary.
* Empty values should be ``None``, instead of empty string.
* ``args`` is a dictionary containing:
* key ``token_infos``: data associated to the authentication token (see :ref:`xivo-auth`)
* ``list(uids, args)``: The list method returns a list of dictionary from a list of uids. Each uid
is a string identifying a contact within the source.
* ``args`` is a dictionary containing:
* key ``token_infos``: data associated to the authentication token (see :ref:`xivo-auth`)
See :ref:`dird-sources_configuration`. The implementation of the back-end should take these values into
account and return results accordingly.
Example
-------
The following example add a backend that will return random names and number.
``dummy.py``:
.. code-block:: python
:linenos:
:emphasize-lines: 18-20, 22-23
# -*- coding: utf-8 -*-
import logging
logger = logging.getLogger(__name__)
class DummyBackendPlugin(object):
def name(self):
return 'my_local_dummy'
def load(self, args):
logger.info('dummy backend loaded')
def unload(self):
logger.info('dummy backend unloaded')
def search(self, term, args):
nb_results = random.randint(1, 20)
return _random_list(nb_results)
def list(self, unique_ids):
return _random_list(len(unique_ids))
def _random_list(self, nb_results):
columns = ['Firstname', 'Lastname', 'Number']
return [_random_entry(columns) for _ in xrange(nb_results)]
def _random_entry(self, columns):
random_stuff = [_random_string() for _ in xrange(len(columns))]
return dict(zip(columns, random_stuff))
def _random_string(self):
return ''.join(random.choice(string.lowercase) for _ in xrange(5))
.. _dird-service:
Service
=======
Service plugins add new functionality to the dird server. These functionalities
are available to views. When loaded, a service plugin receives its configuration
and a dictionary of available sources.
Some service examples that come to mind include:
* A lookup service to search through all configured sources.
* A reverse lookup service to search through all configured sources and return a
specific field of the first matching result.
Implementation details
----------------------
* Namespace: ``xivo_dird.services``
* Abstract service plugin: `BaseServicePlugin `_
* Methods:
* ``load(args)``: set up resources used by the plugin, depending on the config.
``args`` is a dictionary containing:
* key ``config``: the whole configuration file in dict form
* key ``sources``: a dictionary of source names to sources
``load`` must return the service object, which is any kind of python object.
* ``unload()``: free resources used by the plugin.
Example
-------
The following example adds a service that will return an empty list when used.
``dummy.py``:
.. code-block:: python
:linenos:
:emphasize-lines: 17, 23-25, 30, 35-36
# -*- coding: utf-8 -*-
import logging
from xivo_dird import BaseServicePlugin
logger = logging.getLogger(__name__)
class DummyServicePlugin(BaseServicePlugin):
"""
This plugin is responsible fow instantiating and returning the
DummyService. It manages its life time and should take care of
its cleanup if necessary
"""
def load(self, args):
"""
Ignores all provided arguments and instantiate a DummyService that
is returned to the core
"""
logger.info('dummy loaded')
self._service = DummyService()
return self._service
def unload(self):
logger.info('dummy unloaded')
class DummyService(object):
"""
A very dumb service that will return an empty list every time it is used
"""
def list(self):
"""
This function must be called explicitly from the view, `list` is not a
special method name for xivo-dird
"""
return []
.. _dird-view:
View
====
View plugins add new routes to the HTTP application in xivo-dird, in particular the REST API of
xivo-dird: they define the URLs to which xivo-dird will respond and the formatting of data received
and sent through those URLs.
For example, we can define a REST API formatted in JSON with one view and the same API formatted in
XML with another view. Supporting the directory function of a phone is generally a matter of
adding a new view for the format that the phone consumes.
Implementation details
----------------------
* Namespace: ``xivo_dird.views``
* Abstract view plugin: `BaseViewPlugin `_
* Methods:
* ``load(args)``: set up resources used by the plugin, depending on the config. Typically,
register routes on Flask. Those routes would typically call a service.
``args`` is a dictionary containing:
* key ``config``: the section of the configuration file for all views in dict form
* key ``services``: a dictionary of services, indexed by name, which may be called from a route
* key ``http_app``: the `Flask application`_ instance
* key ``rest_api``: a `Flask-RestFul Api`_ instance
.. _Flask application: http://flask.pocoo.org/
.. _Flask-RestFul Api: http://flask-restful.readthedocs.org/en/latest/quickstart.html#a-minimal-api
* ``unload()``: free resources used by the plugin.
Example
-------
The following example adds a simple view: ``GET /0.1/directories/ping`` answers ``{"message": "pong"}``.
``dummy.py``:
.. code-block:: python
:linenos:
:emphasize-lines: 20, 26-32
# -*- coding: utf-8 -*-
import logging
from flask_restful import Resource
logger = logging.getLogger(__name__)
class PingViewPlugin(object):
name = 'ping'
def __init__(self):
logger.debug('dummy view created')
def load(self, args):
logger.debug('dummy view args: %s', args)
args['rest_api'].add_resource(PingView, '/0.1/directories/ping')
def unload(self):
logger.debug('dummy view unloaded')
class PingView(Resource):
"""
Simple API using Flask-Restful: GET /0.1/directories/ping answers "pong"
"""
def get(self):
return {'message': 'pong'}