XiVO dird developer’s guide

../../../_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.

  1. Back-End

  2. Service

  3. View

../../../_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:

 1#!/usr/bin/env python
 2# -*- coding: utf-8 -*-
 3
 4from setuptools import setup
 5from setuptools import find_packages
 6
 7
 8setup(
 9    name='XiVO dird plugin sample',
10    version='0.0.1',
11
12    description='An example program',
13
14    packages=find_packages(),
15
16    entry_points={
17        'xivo_dird.services': [
18            'my_service = dummy:DummyServicePlugin',
19        ],
20        'xivo_dird.backends': [
21            'my_backend = dummy:DummyBackend',
22        ],
23        'xivo_dird.views': [
24            'my_view = dummy:DummyView',
25        ],
26    }
27)

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 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 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 xivo-auth)

See 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:

 1# -*- coding: utf-8 -*-
 2
 3import logging
 4
 5logger = logging.getLogger(__name__)
 6
 7class DummyBackendPlugin(object):
 8
 9    def name(self):
10        return 'my_local_dummy'
11
12    def load(self, args):
13        logger.info('dummy backend loaded')
14
15    def unload(self):
16        logger.info('dummy backend unloaded')
17
18    def search(self, term, args):
19        nb_results = random.randint(1, 20)
20        return _random_list(nb_results)
21
22    def list(self, unique_ids):
23        return _random_list(len(unique_ids))
24
25    def _random_list(self, nb_results):
26        columns = ['Firstname', 'Lastname', 'Number']
27        return [_random_entry(columns) for _ in xrange(nb_results)]
28
29    def _random_entry(self, columns):
30        random_stuff = [_random_string() for _ in xrange(len(columns))]
31        return dict(zip(columns, random_stuff))
32
33    def _random_string(self):
34        return ''.join(random.choice(string.lowercase) for _ in xrange(5))

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:

 1# -*- coding: utf-8 -*-
 2
 3import logging
 4
 5from xivo_dird import BaseServicePlugin
 6
 7logger = logging.getLogger(__name__)
 8
 9class DummyServicePlugin(BaseServicePlugin):
10    """
11    This plugin is responsible fow instantiating and returning the
12    DummyService. It manages its life time and should take care of
13    its cleanup if necessary
14    """
15
16    def load(self, args):
17        """
18        Ignores all provided arguments and instantiate a DummyService that
19        is returned to the core
20        """
21        logger.info('dummy loaded')
22        self._service = DummyService()
23        return self._service
24
25    def unload(self):
26        logger.info('dummy unloaded')
27
28
29class DummyService(object):
30    """
31    A very dumb service that will return an empty list every time it is used
32    """
33
34    def list(self):
35        """
36        This function must be called explicitly from the view, `list` is not a
37        special method name for xivo-dird
38        """
39        return []

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

    • 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:

 1# -*- coding: utf-8 -*-
 2
 3import logging
 4
 5from flask_restful import Resource
 6
 7logger = logging.getLogger(__name__)
 8
 9
10class PingViewPlugin(object):
11
12    name = 'ping'
13
14    def __init__(self):
15        logger.debug('dummy view created')
16
17    def load(self, args):
18        logger.debug('dummy view args: %s', args)
19
20        args['rest_api'].add_resource(PingView, '/0.1/directories/ping')
21
22    def unload(self):
23        logger.debug('dummy view unloaded')
24
25
26class PingView(Resource):
27    """
28    Simple API using Flask-Restful: GET /0.1/directories/ping answers "pong"
29    """
30
31    def get(self):
32        return {'message': 'pong'}