Projet

Général

Profil

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

Valentin Deniaud, 29 juillet 2020 11:10

Télécharger (9,13 ko)

Voir les différences:

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

 .../ovh/migrations/0009_auto_20200729_1105.py | 35 +++++++++
 passerelle/apps/ovh/models.py                 | 77 ++++++++++++++++++-
 tests/test_sms.py                             | 44 +++++++++++
 tests/utils.py                                |  1 +
 4 files changed, 155 insertions(+), 2 deletions(-)
 create mode 100644 passerelle/apps/ovh/migrations/0009_auto_20200729_1105.py
passerelle/apps/ovh/migrations/0009_auto_20200729_1105.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-07-29 09:05
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, 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, 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, max_length=32, verbose_name='Consumer key'),
29
        ),
30
        migrations.AlterField(
31
            model_name='ovhsmsgateway',
32
            name='password',
33
            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)'),
34
        ),
35
    ]
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
    API_URL = 'https://eu.api.ovh.com/1.0/sms/%(serviceName)s/users/%(login)s/jobs/'
12 16
    URL = 'https://www.ovh.com/cgi-bin/sms/http2sms.cgi'
13 17
    MESSAGES_CLASSES = (
14 18
        (0, _('Message are directly shown to users on phone screen '
......
21 25
        (3, _('Messages are stored in external storage like a PDA or '
22 26
              'a PC.')),
23 27
    )
28
    NEW_MESSAGES_CLASSES = ['flash', 'phoneDisplay', 'sim', 'toolkit']
29

  
24 30
    account = models.CharField(verbose_name=_('Account'), max_length=64)
31
    application_key = models.CharField(verbose_name=_('Application key'), max_length=16, blank=True)
32
    application_secret = models.CharField(verbose_name=_('Application secret'), max_length=32, blank=True)
33
    consumer_key = models.CharField(verbose_name=_('Consumer key'), max_length=32, blank=True)
25 34
    username = models.CharField(verbose_name=_('Username'), max_length=64)
26
    password = models.CharField(verbose_name=_('Password'), max_length=64)
35
    password = models.CharField(verbose_name=_('Password (deprecated)'), max_length=64, blank=True,
36
                                help_text=_('Password for legacy API. This field is obsolete once keys '
37
                                            'and secret fields below are filled.'))
27 38
    msg_class = models.IntegerField(choices=MESSAGES_CLASSES, default=1,
28 39
                                    verbose_name=_('Message class'))
29 40
    credit_threshold_alert = models.PositiveIntegerField(verbose_name=_('Credit alert threshold'),
......
75 86
        verbose_name = 'OVH'
76 87
        db_table = 'sms_ovh'
77 88

  
89

  
78 90
    def send_msg(self, text, sender, destinations, **kwargs):
79
        """Send a SMS using the OVH provider"""
91
        if not (self.application_key and self.consumer_key and self.application_secret):
92
            return self.send_msg_legacy(text, sender, destinations, **kwargs)
93

  
94
        url = self.API_URL % {'serviceName': self.account, 'login': self.username}
95
        body = {
96
            'sender': sender,
97
            'receivers': destinations,
98
            'message': text,
99
            'class': self.NEW_MESSAGES_CLASSES[self.msg_class],
100
        }
101
        if not kwargs['stop']:
102
            body.update({'noStopClause': 1})
103

  
104
        # sign request
105
        now = str(int(time.time()))
106
        signature = hashlib.sha1()
107
        to_sign = "+".join((
108
            self.application_secret, self.consumer_key, 'POST', url, json.dumps(body), now
109
        ))
110
        signature.update(to_sign.encode())
111

  
112
        headers = {
113
            'X-Ovh-Application': self.application_key,
114
            'X-Ovh-Consumer': self.consumer_key,
115
            'X-Ovh-Timestamp': now,
116
            'X-Ovh-Signature': "$1$" + signature.hexdigest(),
117
        }
118

  
119
        try:
120
            response = self.requests.post(url, headers=headers, json=body)
121
        except requests.RequestException as e:
122
            raise APIError('OVH error: POST failed, %s' % e)
123
        else:
124
            try:
125
                result = response.json()
126
            except ValueError as e:
127
                raise APIError('OVH error: bad JSON response')
128
        try:
129
            response.raise_for_status()
130
        except requests.RequestException as e:
131
            raise APIError('OVH error: %s "%s"' % (e, result))
132

  
133
        ret = {}
134
        credits_removed = result['totalCreditsRemoved']
135
        # update credit left
136
        self.credit_left -= credits_removed
137
        if self.credit_left < 0:
138
            self.credit_left = 0
139
        self.save()
140
        if self.credit_left < self.credit_threshold_alert:
141
            ret['warning'] = ('credit level too low for %s: %s (threshold %s)' %
142
                              (self.slug, self.credit_left, self.credit_threshold_alert))
143
        ret['credit_left'] = self.credit_left
144
        ret['ovh_result'] = result
145
        ret['sms_ids'] = result.get('ids', [])
146

  
147
        return ret
148

  
149
    def send_msg_legacy(self, text, sender, destinations, **kwargs):
150
        """Send a SMS using the HTTP2 endpoint"""
151
        if not self.password:
152
            raise APIError('Improperly configured, empty keys or password fields.')
80 153

  
81 154
        text = force_text(text).encode('utf-8')
82 155
        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
-