Projet

Général

Profil

0001-api-add-an-API-to-request-jobs-status-43278.patch

Nicolas Roche, 18 septembre 2020 16:33

Télécharger (11,8 ko)

Voir les différences:

Subject: [PATCH] api: add an API to request jobs status (#43278)

 passerelle/api/__init__.py |   0
 passerelle/api/urls.py     |  23 ++++++++
 passerelle/api/utils.py    |  31 ++++++++++
 passerelle/api/views.py    |  56 ++++++++++++++++++
 passerelle/urls.py         |   2 +
 tests/test_api.py          | 118 +++++++++++++++++++++++++++++++++++++
 6 files changed, 230 insertions(+)
 create mode 100644 passerelle/api/__init__.py
 create mode 100644 passerelle/api/urls.py
 create mode 100644 passerelle/api/utils.py
 create mode 100644 passerelle/api/views.py
 create mode 100644 tests/test_api.py
passerelle/api/urls.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import url
18

  
19
from .views import JobDetailView
20

  
21
urlpatterns = [
22
    url(r'jobs/(?P<pk>[\w,-]+)/$', JobDetailView.as_view(), name='api-job'),
23
]
passerelle/api/utils.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.apps import apps
18
from django.conf import settings
19

  
20

  
21
def get_related_app(connector):
22
    for app in apps.get_app_configs():
23
        if not hasattr(app, 'get_connector_model'):
24
            continue
25
        if app.get_connector_model() == connector.__class__:
26
            return app
27
    return None
28

  
29

  
30
def app_enabled(app_label):
31
    return getattr(settings, 'PASSERELLE_APP_%s_ENABLED' % app_label.upper(), True)
passerelle/api/views.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.http import Http404
18
from django.http import JsonResponse
19
from django.views.generic import DetailView
20

  
21
from passerelle.api.utils import get_related_app, app_enabled
22
from passerelle.base.models import Job
23
from passerelle.utils import is_authorized
24

  
25

  
26
class JobDetailView(DetailView):
27
    model = Job
28

  
29
    def error(self, message):
30
        return JsonResponse({'err': 1, 'err_desc': message})
31

  
32
    def get(self, *args, **kwargs):
33
        try:
34
            job = self.get_object()
35
        except Http404 as exc:
36
            return self.error(str(exc))
37
        connector = job.resource
38

  
39
        app = get_related_app(connector)
40
        if not app:
41
            return self.error('Cannot retrieve job resource connector')
42
        if not app_enabled(app.label):
43
            return self.error('Please enable %s' % app.label)
44
        if not is_authorized(self.request, connector, 'can_access'):
45
            return self.error('Permission denied')
46

  
47
        data = {
48
            'id': job.id,
49
            'resource': job.resource.__class__.__name__,
50
            'parameters': job.parameters,
51
            'status': job.status,
52
            'status_details': job.status_details,
53
            'update_timestamp': job.update_timestamp,
54
            'done_timestamp': job.done_timestamp,
55
        }
56
        return JsonResponse({'err': 0, 'data': data})
passerelle/urls.py
2 2
from django.conf.urls import include, url
3 3

  
4 4
from django.contrib import admin
5 5

  
6 6
from django.contrib.auth.decorators import login_required
7 7
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
8 8
from django.views.static import serve as static_serve
9 9

  
10
from .api.urls import urlpatterns as api_urls
10 11
from .views import (
11 12
    HomePageView, ManageView, ManageAddView,
12 13
    GenericCreateConnectorView, GenericDeleteConnectorView,
13 14
    GenericEditConnectorView, GenericEndpointView, GenericConnectorView,
14 15
    GenericViewLogsConnectorView, GenericLogView, GenericExportConnectorView,
15 16
    login, logout, menu_json)
16 17
from .base.views import GenericViewJobsConnectorView, GenericJobView, GenericRestartJobView
17 18
from .urls_utils import decorated_includes, manager_required
......
32 33
        'document_root': settings.MEDIA_ROOT,
33 34
    }),
34 35
    url(r'^admin/', admin.site.urls),
35 36

  
36 37
    url(r'^manage/access/',
37 38
        decorated_includes(manager_required, include(access_urlpatterns))),
38 39
    url(r'^manage/',
39 40
        decorated_includes(manager_required, include(import_export_urlpatterns))),
41
    url('^api/', include(api_urls)),
40 42
]
41 43

  
42 44
# add patterns from apps
43 45
urlpatterns = register_apps_urls(urlpatterns)
44 46

  
45 47
# add authentication patterns
46 48
urlpatterns += [
47 49
    url(r'^logout/$', logout, name='logout'),
tests/test_api.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import mock
18
import pytest
19

  
20
from django.core.urlresolvers import reverse
21
from django.contrib.contenttypes.models import ContentType
22
from django.test import override_settings
23

  
24
from passerelle.apps.ovh.models import OVHSMSGateway
25
from passerelle.base.models import ApiUser, AccessRight, Job
26

  
27
from test_manager import login, simple_user, admin_user
28

  
29
pytestmark = pytest.mark.django_db
30

  
31
API_KEY = '1234'
32

  
33

  
34
@pytest.fixture
35
def connector(db):
36
    connector = OVHSMSGateway.objects.create(
37
        slug='my_connector',
38
    )
39
    apiuser = ApiUser.objects.create(username='me', keytype='API', key=API_KEY)
40
    obj_type = ContentType.objects.get_for_model(OVHSMSGateway)
41
    AccessRight.objects.create(codename='can_access', apiuser=apiuser,
42
                               resource_type=obj_type, resource_pk=connector.pk)
43
    return connector
44

  
45

  
46
@mock.patch('passerelle.sms.models.SMSResource.send_job')
47
def test_api_jobs(mocked_send_job, app, connector, simple_user, admin_user):
48
    assert Job.objects.count() == 0
49
    url = reverse('api-job', kwargs={'pk': 22})
50

  
51
    # no job
52
    resp = app.get(url, status=200)
53
    assert resp.json['err'] == 1
54
    assert resp.json['err_desc'] == 'No job found matching the query'
55

  
56
    # no job resource
57
    job = connector.add_job('send_job', my_parameter='my_value')
58
    assert Job.objects.count() == 1
59
    url = reverse('api-job', kwargs={'pk': job.id})
60
    with mock.patch('passerelle.api.views.get_related_app', return_value=None):
61
        resp = app.get(url, status=200)
62
    assert resp.json['err'] == 1
63
    assert resp.json['err_desc'] == 'Cannot retrieve job resource connector'
64

  
65
    # app disabled
66
    with override_settings(PASSERELLE_APP_OVH_ENABLED=False):
67
        resp = app.get(url, status=200)
68
    assert resp.json['err'] == 1
69
    assert resp.json['err_desc'] == 'Please enable ovh'
70

  
71
    # access without permission
72
    resp = app.get(url, status=200)
73
    assert resp.json['err'] == 1
74
    assert resp.json['err_desc'] == 'Permission denied'
75

  
76
    # apiuser access
77
    resp = app.get(url, params={'apikey': API_KEY}, status=200)
78
    assert not resp.json['err']
79

  
80
    # logged user access
81
    app = login(app, simple_user.username, password='user')
82
    resp = app.get(url, status=200)
83
    assert resp.json['err'] == 1
84
    assert resp.json['err_desc'] == 'Permission denied'
85
    app = login(app, admin_user.username, password='admin')
86
    resp = app.get(url, status=200)
87
    assert not resp.json['err']
88

  
89
    # registered job
90
    assert resp.json['data']['id'] == job.id
91
    assert resp.json['data']['resource'] == 'OVHSMSGateway'
92
    assert resp.json['data']['parameters'] == {'my_parameter': 'my_value'}
93
    assert resp.json['data']['status'] == 'registered'
94
    assert resp.json['data']['done_timestamp'] is None
95
    update_timestamp1 = resp.json['data']['update_timestamp']
96

  
97
    # completed job
98
    connector.jobs()
99
    assert mocked_send_job.call_args == mock.call(my_parameter='my_value')
100
    resp = app.get(url, status=200)
101
    assert not resp.json['err']
102
    assert resp.json['data']['id'] == job.id
103
    assert resp.json['data']['status'] == 'completed'
104
    assert resp.json['data']['done_timestamp'] is not None
105
    resp.json['data']['update_timestamp'] < update_timestamp1
106

  
107
    # failed job
108
    job = connector.add_job('send_job')
109
    assert Job.objects.count() == 2
110
    mocked_send_job.side_effect = Exception('my error message')
111
    connector.jobs()
112
    url = reverse('api-job', kwargs={'pk': job.id})
113
    resp = app.get(url, status=200)
114
    assert not resp.json['err']
115
    assert resp.json['data']['id'] == job.id
116
    assert resp.json['data']['status'] == 'failed'
117
    assert resp.json['data']['status_details'] == {'error_summary': 'Exception: my error message'}
118
    assert resp.json['data']['done_timestamp'] is not None
0
-