Projet

Général

Profil

0001-ovh-support-jobs-API-endpoint-44313.patch

Valentin Deniaud, 30 juillet 2020 10:51

Télécharger (10,5 ko)

Voir les différences:

Subject: [PATCH] ovh: support /jobs/ API endpoint (#44313)

 .../ovh/migrations/0009_auto_20200730_1047.py |  45 +++++++
 passerelle/apps/ovh/models.py                 | 110 +++++++++++++++++-
 tests/test_sms.py                             |  44 +++++++
 tests/utils.py                                |   1 +
 4 files changed, 196 insertions(+), 4 deletions(-)
 create mode 100644 passerelle/apps/ovh/migrations/0009_auto_20200730_1047.py
passerelle/apps/ovh/migrations/0009_auto_20200730_1047.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-07-30 08:47
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('ovh', '0008_ovhsmsgateway_max_message_length'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='ovhsmsgateway',
17
            name='application_key',
18
            field=models.CharField(blank=True, help_text='Random token obtained from OVH.', max_length=16, verbose_name='Application key'),
19
        ),
20
        migrations.AddField(
21
            model_name='ovhsmsgateway',
22
            name='application_secret',
23
            field=models.CharField(blank=True, help_text='Obtained at the same time as "Application key".', max_length=32, verbose_name='Application secret'),
24
        ),
25
        migrations.AddField(
26
            model_name='ovhsmsgateway',
27
            name='consumer_key',
28
            field=models.CharField(blank=True, help_text='Obtained at the same time as "Application key".', max_length=32, verbose_name='Consumer key'),
29
        ),
30
        migrations.AlterField(
31
            model_name='ovhsmsgateway',
32
            name='account',
33
            field=models.CharField(help_text='Account identifier, such as sms-XXXXXX-1.', max_length=64, verbose_name='Account'),
34
        ),
35
        migrations.AlterField(
36
            model_name='ovhsmsgateway',
37
            name='password',
38
            field=models.CharField(blank=True, help_text='Password for legacy API. This field is obsolete once keys and secret fields below are filled.', max_length=64, verbose_name='Password (deprecated)'),
39
        ),
40
        migrations.AlterField(
41
            model_name='ovhsmsgateway',
42
            name='username',
43
            field=models.CharField(help_text='API user created on the SMS account.', max_length=64, verbose_name='Username'),
44
        ),
45
    ]
passerelle/apps/ovh/models.py
1
import hashlib
2
import json
1 3
import requests
4
import time
2 5

  
3 6
from django.db import models
4 7
from django.utils.encoding import force_text
......
9 12

  
10 13

  
11 14
class OVHSMSGateway(SMSResource):
15
    documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
16
    API_URL = 'https://eu.api.ovh.com/1.0/sms/%(serviceName)s/users/%(login)s/jobs/'
12 17
    URL = 'https://www.ovh.com/cgi-bin/sms/http2sms.cgi'
13 18
    MESSAGES_CLASSES = (
14 19
        (0, _('Message are directly shown to users on phone screen '
......
21 26
        (3, _('Messages are stored in external storage like a PDA or '
22 27
              'a PC.')),
23 28
    )
24
    account = models.CharField(verbose_name=_('Account'), max_length=64)
25
    username = models.CharField(verbose_name=_('Username'), max_length=64)
26
    password = models.CharField(verbose_name=_('Password'), max_length=64)
29
    NEW_MESSAGES_CLASSES = ['flash', 'phoneDisplay', 'sim', 'toolkit']
30

  
31
    account = models.CharField(
32
        verbose_name=_('Account'), max_length=64, help_text=_('Account identifier, such as sms-XXXXXX-1.')
33
    )
34

  
35
    application_key = models.CharField(
36
        verbose_name=_('Application key'),
37
        max_length=16,
38
        blank=True,
39
        help_text=_('Random token obtained from OVH.'),
40
    )
41
    application_secret = models.CharField(
42
        verbose_name=_('Application secret'),
43
        max_length=32,
44
        blank=True,
45
        help_text=_('Obtained at the same time as "Application key".'),
46
    )
47
    consumer_key = models.CharField(
48
        verbose_name=_('Consumer key'),
49
        max_length=32,
50
        blank=True,
51
        help_text=_('Obtained at the same time as "Application key".'),
52
    )
53

  
54
    username = models.CharField(
55
        verbose_name=_('Username'),
56
        max_length=64,
57
        help_text=_('API user created on the SMS account.'),
58
    )
59
    password = models.CharField(
60
        verbose_name=_('Password (deprecated)'),
61
        max_length=64,
62
        blank=True,
63
        help_text=_(
64
            'Password for legacy API. This field is obsolete once keys and secret fields below are filled.'
65
        ),
66
    )
27 67
    msg_class = models.IntegerField(choices=MESSAGES_CLASSES, default=1,
28 68
                                    verbose_name=_('Message class'))
29 69
    credit_threshold_alert = models.PositiveIntegerField(verbose_name=_('Credit alert threshold'),
......
76 116
        db_table = 'sms_ovh'
77 117

  
78 118
    def send_msg(self, text, sender, destinations, **kwargs):
79
        """Send a SMS using the OVH provider"""
119
        if not (self.application_key and self.consumer_key and self.application_secret):
120
            return self.send_msg_legacy(text, sender, destinations, **kwargs)
121

  
122
        url = self.API_URL % {'serviceName': self.account, 'login': self.username}
123
        body = {
124
            'sender': sender,
125
            'receivers': destinations,
126
            'message': text,
127
            'class': self.NEW_MESSAGES_CLASSES[self.msg_class],
128
        }
129
        if not kwargs['stop']:
130
            body.update({'noStopClause': 1})
131

  
132
        # sign request
133
        now = str(int(time.time()))
134
        signature = hashlib.sha1()
135
        to_sign = "+".join((self.application_secret, self.consumer_key, 'POST', url, json.dumps(body), now))
136
        signature.update(to_sign.encode())
137

  
138
        headers = {
139
            'X-Ovh-Application': self.application_key,
140
            'X-Ovh-Consumer': self.consumer_key,
141
            'X-Ovh-Timestamp': now,
142
            'X-Ovh-Signature': "$1$" + signature.hexdigest(),
143
        }
144

  
145
        try:
146
            response = self.requests.post(url, headers=headers, json=body)
147
        except requests.RequestException as e:
148
            raise APIError('OVH error: POST failed, %s' % e)
149
        else:
150
            try:
151
                result = response.json()
152
            except ValueError as e:
153
                raise APIError('OVH error: bad JSON response')
154
        try:
155
            response.raise_for_status()
156
        except requests.RequestException as e:
157
            raise APIError('OVH error: %s "%s"' % (e, result))
158

  
159
        ret = {}
160
        credits_removed = result['totalCreditsRemoved']
161
        # update credit left
162
        self.credit_left -= credits_removed
163
        if self.credit_left < 0:
164
            self.credit_left = 0
165
        self.save()
166
        if self.credit_left < self.credit_threshold_alert:
167
            ret['warning'] = 'credit level too low for %s: %s (threshold %s)' % (
168
                self.slug,
169
                self.credit_left,
170
                self.credit_threshold_alert,
171
            )
172
        ret['credit_left'] = self.credit_left
173
        ret['ovh_result'] = result
174
        ret['sms_ids'] = result.get('ids', [])
175

  
176
        return ret
177

  
178
    def send_msg_legacy(self, text, sender, destinations, **kwargs):
179
        """Send a SMS using the HTTP2 endpoint"""
180
        if not self.password:
181
            raise APIError('Improperly configured, empty keys or password fields.')
80 182

  
81 183
        text = force_text(text).encode('utf-8')
82 184
        to = ','.join(destinations)
tests/test_sms.py
138 138
        result = app.post_json(path, params=payload)
139 139
        connector.jobs()
140 140
        assert SMSLog.objects.filter(appname=connector.get_connector_slug(), slug=connector.slug).exists()
141

  
142

  
143
def test_ovh_new_api(app, freezer):
144
    connector = OVHSMSGateway.objects.create(
145
        slug='ovh', account='sms-test42', username='john',
146
        application_key='RHrTdU2oTsrVC0pu',
147
        application_secret='CLjtS69tTcPgCKxedeoZlgMSoQGSiXMa',
148
        consumer_key='iF0zi0MJrbjNcI3hvuvwkhNk8skrigxz'
149
    )
150
    api = ApiUser.objects.create(username='apiuser')
151
    obj_type = ContentType.objects.get_for_model(connector)
152
    # no access check
153
    AccessRight.objects.create(codename='can_send_messages', apiuser=api, resource_type=obj_type,
154
                               resource_pk=connector.pk)
155

  
156
    payload = {
157
        'message': 'hello',
158
        'from': '+33699999999',
159
        'to': ['+33688888888', '+33677777777'],
160
    }
161

  
162
    # register job
163
    freezer.move_to('2019-01-01 00:00:00')
164
    path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug)
165
    result = app.post_json(path, params=payload)
166
    assert result.json['err'] == 0
167
    job_id = Job.objects.get(status='registered').id
168

  
169
    # perform job
170
    freezer.move_to('2019-01-01 01:00:03')
171
    resp = {
172
        'validReceivers': ['+33688888888', '+33677777777'],
173
        'totalCreditsRemoved': 1,
174
        'ids': [241615100],
175
        'invalidReceivers': []
176
    }
177
    url = connector.API_URL % {'serviceName': 'sms-test42', 'login': 'john'}
178
    with utils.mock_url(url, resp, 200) as mocked:
179
        connector.jobs()
180
    job = Job.objects.get(id=job_id)
181
    assert job.status == 'completed'
182

  
183
    request = mocked.handlers[0].call['requests'][0]
184
    assert 'X-Ovh-Signature' in request.headers
tests/utils.py
44 44
    if not isinstance(response, str):
45 45
        response = json.dumps(response)
46 46

  
47
    @httmock.remember_called
47 48
    @httmock.urlmatch(**urlmatch_kwargs)
48 49
    def mocked(url, request):
49 50
        if exception:
50
-