Documentation en ligne

Développement d'un connecteur

Cette documentation s'appuie sur une installation locale de Passerelle, réalisée au moyen des paquets Debian. Assurez-vous également que votre utilisateur soit ajouté au groupe "passerelle".

Démarrage

Il faut d'abord créer un répertoire qui contiendra le connecteur, appelons ce répertoire "passerelle-test" (mkdir passerellet-test). Dans ce répertoire (cd passerelle-test), il faut créer le module Python qui correspondra au connecteur, ici les tirets ne sont pas autorisés dans les noms, appelons-le donc "passerelle_test", un module Python est un simple répertoire (mkdir passerelle_test) contenant un fichier __init__.py, qui peut être vide (touch passerelle_test/__init__.py).

Code du connecteur

Le code du connecteur se crée dans le fichier passerelle-test/models.py, il correspond à un modèle Django, créé pour hériter du modèle "BaseResource" :

from passerelle.base.models import BaseResource

class TestConnector(BaseResource):
    category = 'Divers'

    class Meta:
        verbose_name = 'Connecteur de test'

La catégorie où se range le connecteur se définit dans l'attribut category et le nom du connecteur va lui dans les métadonnées du modèle.

Très clairement ce connecteur ne fait encore rien mais il y a désormais assez de code pour l'instancier ; mais avant cela, il faut qu'il puisse être installé sur le système.

Installation du connecteur

Pour que le connecteur puisse être installé, il faut créer un fichier setup.py à la racine du projet (i.e. directement dans le répertoire passerelle_test/); son contenu peut être minimal :

#! /usr/bin/env python

from setuptools import setup, find_packages

setup(
    name='passerelle-test',
    author='John Doe',
    author_email='john.doe@example.net',
    url='http://example.net/',
    packages=find_packages(),
)

Le code d'installation exige la présence d'un fichier de documentation README, il doit donc être créé pour l'occasion (il peut très bien être vide, touch README).

À ce stade, l'arborescence du projet est donc celle-ci :

passerelle-test/
passerelle-test/README
passerelle-test/setup.py
passerelle-test/passerelle_test/
passerelle-test/passerelle_test/__init__.py
passerelle-test/passerelle_test/models.py

Il est maintenant possible de lancer l'installation, en mode développement pour permettre à nos modifications ultérieures d'être prises en compte sans demander à chaque fois une réinstallation (sudo python setup.py develop).

$ sudo python setup.py develop
running develop
running egg_info
writing passerelle_test.egg-info/PKG-INFO
writing top-level names to passerelle_test.egg-info/top_level.txt
writing dependency_links to passerelle_test.egg-info/dependency_links.txt
reading manifest file 'passerelle_test.egg-info/SOURCES.txt'
writing manifest file 'passerelle_test.egg-info/SOURCES.txt'
running build_ext
Creating /usr/local/lib/python2.7/dist-packages/passerelle-test.egg-link (link to .)
Adding passerelle-test 0.0.0 to easy-install.pth file

Installed /home/test/passerelle-test
Processing dependencies for passerelle-test==0.0.0
Finished processing dependencies for passerelle-test==0.0.0

Activation du connecteur

Le connecteur a désormais tout le nécessaire pour être activé, il reste juste à le déclarer dans la configuration, pour cela il faut créer un fichier /etc/passerelle/settings.d/connecteur.py (ou tout autre nom en .py), avec ces deux lignes :

INSTALLED_APPS += ('passerelle_test',)
TENANT_APPS += ('passerelle_test',)

Migration initiale

Django dispose d'un système automatique de migration des données des différents modèles, qui facilite grandement les mises à jour. Dans la majorité des cas ce système est automatique, il est temps de l'initialiser en créant une première migration :

$ passerelle-manage --forceuser makemigrations passerelle_test
Migrations for 'passerelle_test':
  0001_initial.py:
    - Create model TestConnector

Si cette commande échoue sur un problème d'accès, vérifiez que votre utilisateur est bien dans le groupe "passerelle".

Instanciation du connecteur

Passerelle peut désormais être rédémarré, sudo service passerelle restart. Et voilà, il est maintenant possible de naviguer, le nouveau connecteur apparaitra dans la liste des connecteurs disponibles. Créons-le et donnons-lui le nom de "démo", sa première instance devient disponible : https://passerelle.exemple.net/passerelle-test/demo/

Endpoints

C'est ici que tout commence car bien sûr un connecteur vide n'a aucun intérêt, il faut désormais lui ajouter le code qui le rendra utile, sous forme de "endpoints", les URL qui seront utilisées pour exposer les webservices.

Il faut importer le code permettant de déclarer les endpoints from passerelle.utils.api import endpoint en haut du fichier models.py, et ensuite, dans le modèle TestConnector créé plus haut, nous pouvons alors déclarer un premier endpoint,

class TestConnector(BaseResource):
    []

    @endpoint()
    def info(self, request):
        return {'hello': 'world'}

Redémarrons Passerelle pour que le code soit pris en compte (sudo service passerelle reload) et voilà, accéder à l'URL du endpoint, https://passerelle.exemple.net/passerelle-test/demo/info retourne la donnée au format JSON :

{
  "hello": "world" 
}

Il est aussi possible pour un endpoint d'attendre des paramètres, déclarons donc un deuxième endpoint, faisant l'addition de nombres entiers et retournant le résultat :

class TestConnector(BaseResource):
    []

    @endpoint()
    def addition(self, request, a, b):
        return {'total': int(a) + int(b)}

Visiter https://passerelle.exemple.net/passerelle-test/demo/addition?a=2&b=3 retournera au format JSON :

{
  "total": 5
}

Journalisation

Passerelle suit la configuration de la journalisation établie au niveau de Django (cf Journalisation dans la documentation officielle de Django) et ajoute à celle-ci une journalisation interne, consultable depuis la page de visualisation du connecteur.

L'objet logger d'un connecteur dispose des méthodes standards de log (debug, info, warning, critical, error, fatal); ainsi un connecteur opérant une division pourrait logguer au niveau "debug" toutes les demandes et logguer au niveau "error" les divisions par zéro :

class TestConnector(BaseResource):
    []

    @endpoint()
    def division(self, request, a, b):
        self.logger.debug('division de %s par %s', a, b)
        try:
            return {'result': int(a) / int(b)}
        except ZeroDivisionError:
            self.logger.error('division par zéro')
            raise

Autorisations d'accès

Par défaut les endpoints ajoutés sont accessibles publiquement, pour limiter l'accès, il faut ajouter un paramètre à @endpoint(), pour lui dire que la permission can_access est nécessaire :

class TestConnector(BaseResource):
    []

    @endpoint(perm='can_access')
    def info(self, request):
        return {'hello': 'world'}

Un tableau de gestion des autorisations d'accès sera alors automatiquement ajouté à la page de gestion du connecteur.

can_access est le nom communément utilisé pour limiter l'accès mais il est possible d'augmenter la granularité des autorisations, on pourrait ainsi avoir un autre endpoint dont l'accès serait différencié :

class TestConnector(BaseResource):
    []

    _can_compute_description = 'Accès aux fonctions de calcul'

    @endpoint(perm='can_compute')
    def addition(self, request, a, b):
        return {'total': int(a) + int(b)}

À noter donc l'ajout dans la classe d'un attribut _can_compute_description, qui servira au niveau de l'interface à préciser le sens de cette autorisation d'accès.

Documentation

Pour renforcer l'utilité de la page d'information d'un connecteur il est important de documenter le connecteur et ses différents endpoints. Le connecteur en lui-même peut être décrit en ajoutant un attribut api_description. Pour les endpoints, cela passe par l'ajout de paramètres à la déclaration @endpoint :

class TestConnector(BaseResource):
    []
    api_description = "Ce connecteur propose quelques opération arithmétiques élémentaires." 

    @endpoint(description='Addition de deux nombres entiers',
              parameters={
                'a': {'description': 'Un premier nombre', 'example_value': '7'},
                'b': {'description': 'Un second nombre', 'example_value': '2'}
              }):
    def addition(self, request, a, b):
        return {'total': int(a) + int(b)}

Dans description se place donc une description général de l'endpoint et dans parameters se place les informations sur les différents paramètres autorisés par l'appel, pour chacun d'eux une description et une valeur qui sera reprise en exemple peuvent être fournies.

Paramétrages supplémentaires

À l'instanciation d'un connecteur quelques paramètres sont demandés, un titre et une description, ainsi que le niveau de journalisation souhaité. Il est possible d'ajouter d'autres paramètres, il faut pour cela ajouter de nouveaux attributs à la classe du connecteur. Comme noté en début de document cette classe correspond à un modèle Django, il s'agit ainsi d'y ajouter des champs.

Comme exemple, créons un connecteur dont le rôle sera de nous renseigner sur le bon état de fonctionnement, ou pas, d'une adresse, en faisant en sorte que cette adresse soit un paramètre supplémentaire du connecteur.

Il faut donc ajouter un attribut au modèle, url = models.URLField(...) et cette information sera par la suite disponible dans self.url :

import requests
from passerelle.base.models import BaseResource

class TestConnector(BaseResource):
    category = 'Divers'
    url = models.URLField('URL', default='http://www.example.net')

    class Meta:
        verbose_name = 'Connecteur de test'

    @endpoint(description='Teste une adresse', perm='can_access')
    def up(self, request):
        try:
            response = self.requests.get(self.url)
            response.raise_for_status()
        except requests.RequestException:
            return {'result': '%s est en panne' % self.url}
        return {'result': '%s est ok' % self.url}

Comme le modèle se trouve modifié, il est nécessaire de prévoir pour la base de données une migration qui ajoutera une colonne pour ce champ url, passerelle-manage --forceuser makemigrations passerelle_test. Cette migration sera exécutée automatiquement au redémarrage de Passerelle, qui est donc nécessaire à cette étape.

Note : self.requests utilisé ici est un wrapper léger au-dessus du module Requests, qui ajoute une journalisation et une expiration automatique des appels.

Exécution de tâches planifiées (cron)

Un connecteur peut souhaiter exécuter des tâches de manière régulière, comme par exemple lancer une synchronisation avec un référentiel externe. Passerelle prend en charge quatre fréquences, une tâche peut être planifiée pour s'exécuter toutes les heures, tous les jours, toutes les semaines ou tous les mois. Pour ce faire il faut définir dans le connecteur une fonction avec la tâche à exécuter et la nommer selon la fréquence (hourly / daily / weekly / monthly).

from passerelle.base.models import BaseResource

class TestConnector(BaseResource):
    []

    def hourly(self):
        pass  # code exécuté toutes les heures

Suivi de la disponibilité

Dans le prolongement des tâches planifiées se trouve le suivi de la disponibilité. Un connecteur peut régulièrement (toutes les cinq minutes) interroger le service distant pour s'assurer de sa disponibilité et marquer celui-ci si jamais ce n'était pas le cas.

Il suffit pour assurer ça définir une méthode check_status, le service sera considéré indisponible quand la méthode lèvera une exception.

from passerelle.base.models import BaseResource

class TestConnector(BaseResource):
    []

    def check_status(self):
        response = self.requests.get('http://example.net')
        response.raise_for_status()

Tests unitaires

Ils sont indispensables pour dormir tranquille, ils se créent dans un nouveau répertoire, tests et utilisent les modules pytest, pytest-django et django-webtest (ces trois modules doivent donc être installés).

Il y a d'abord à définir un fichier tests/settings.py particulier, qui assurera la présence du connecteur :

INSTALLED_APPS += ('passerelle_test',)

Puis ensuite créer un fichier pour les tests en eux-mêmes, ça peut être tests/test_connecteur.py, il commencera par les imports des modules souhaités puis la définition des objets qui seront ensuite utiles.

# -*- coding: utf-8 -*-

import pytest
import django_webtest

from django.contrib.contenttypes.models import ContentType
from passerelle_test.models import TestConnector
from passerelle.base.models import ApiUser, AccessRight

@pytest.fixture
def app(request):
    # création de l'application Django
    wtm = django_webtest.WebTestMixin()
    wtm._patch_settings()
    request.addfinalizer(wtm._unpatch_settings)
    return django_webtest.DjangoTestApp()

@pytest.fixture
def connector(db):
    # création du connecteur et ouverture de la permission "can_access" sans authentification.
    connector = TestConnector.objects.create(slug='test')
    api = ApiUser.objects.create(username='all', keytype='', key='')
    obj_type = ContentType.objects.get_for_model(connector)
    AccessRight.objects.create(
            codename='can_access', apiuser=api,
            resource_type=obj_type, resource_pk=connector.pk)
    return connector

Vient alors enfin le temps de tester le connecteur, pour un test sur la fonction d'addition :

def test_addition(app, connector):
    resp = app.get('/passerelle-test/test/addition?a=5&b=3')
    assert resp.json.get('total') == 8

Les tests s'exécutent ensuite via la commande py.test, en pointant le fichier settings.py créé spécialement :

 $ DJANGO_SETTINGS_MODULE=passerelle.settings PASSERELLE_SETTINGS_FILE=tests/settings.py py.test 
======================================= test session starts =======================================
platform linux2 -- Python 2.7.14+, pytest-3.3.1, ...
cachedir: .cache
Django settings: passerelle.settings (from environment variable)
rootdir: ..., inifile:
plugins: ...
collected 1 item

tests/test_connecteur.py::test_addition PASSED                                              [100%]

==================================== 1 passed in 0.48 seconds =====================================

Au-delà

  • gestion des POST.
  • pattern sur les endpoints.
  • json/jsonp.
  • Squelette HTML de description du connecteur.

Dernière mise à jour le 17 août 2018 17:24