models.py 19.6 KB
Newer Older
Lukas Burgey's avatar
Lukas Burgey committed
1
2
3
4
5
6
7
8
# django senders need their arguments
# pylint: disable=unused-argument

import json
import logging
import requests
from requests.auth import HTTPBasicAuth
import pika
Lukas Burgey's avatar
Lukas Burgey committed
9
from django.conf import settings
Lukas Burgey's avatar
Lukas Burgey committed
10
11
from django.contrib.auth.models import AbstractUser, Group
from django.db import models
12
from django.db.models.signals import post_save, pre_delete
Lukas Burgey's avatar
Lukas Burgey committed
13
from django.dispatch import receiver
Lukas Burgey's avatar
Lukas Burgey committed
14
from rest_framework.authtoken.models import Token
Lukas Burgey's avatar
Lukas Burgey committed
15
from .auth.v1.models import OIDCConfig
Lukas Burgey's avatar
Lukas Burgey committed
16

Lukas Burgey's avatar
Lukas Burgey committed
17
LOGGER = logging.getLogger(__name__)
18

Lukas Burgey's avatar
Lukas Burgey committed
19

Lukas Burgey's avatar
Lukas Burgey committed
20
21
22
23
# clients are registerred at rabbitmq, when they are assigned to a site
# (because we only then know what services they provide)
class RabbitMQInstance(models.Model):
    host = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
24
25
26
        max_length=150,
        default='localhost',
    )
Lukas Burgey's avatar
Lukas Burgey committed
27
28
29
30
    vhost = models.CharField(
        max_length=150,
        default='%2f',
    )
Lukas Burgey's avatar
Lukas Burgey committed
31
    exchange = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
32
33
34
        max_length=150,
        default='deployments',
    )
Lukas Burgey's avatar
Lukas Burgey committed
35
    port = models.IntegerField(
Lukas Burgey's avatar
Lukas Burgey committed
36
        default=15672,
Lukas Burgey's avatar
Lukas Burgey committed
37
    )
Lukas Burgey's avatar
Lukas Burgey committed
38
    path = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
39
40
41
        max_length=150,
        default='api',
    )
Lukas Burgey's avatar
Lukas Burgey committed
42
    username = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
43
44
45
        max_length=150,
        default='guest',
    )
Lukas Burgey's avatar
Lukas Burgey committed
46
    password = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
47
48
49
        max_length=150,
        default='guest',
    )
Lukas Burgey's avatar
Lukas Burgey committed
50
51
    is_active = models.BooleanField(
        default=True,
Lukas Burgey's avatar
Lukas Burgey committed
52
    )
Lukas Burgey's avatar
Lukas Burgey committed
53
54
55
56

    def __str__(self):
        return self.host

Lukas Burgey's avatar
Lukas Burgey committed
57
    def _msg(self, msg):
Lukas Burgey's avatar
Lukas Burgey committed
58
        return '[RabbitMQ:{}] {}'.format(self.host, msg)
Lukas Burgey's avatar
Lukas Burgey committed
59
60
61
62

    @property
    def auth(self):
        return HTTPBasicAuth(
Lukas Burgey's avatar
Lukas Burgey committed
63
            self.username,
Lukas Burgey's avatar
Lukas Burgey committed
64
            self.password,
Lukas Burgey's avatar
Lukas Burgey committed
65
        )
Lukas Burgey's avatar
Lukas Burgey committed
66
67

    @property
Lukas Burgey's avatar
Lukas Burgey committed
68
69
70
71
72
    def _connection_parameters(self):
        return pika.ConnectionParameters(
            host=self.host,
            ssl=True,
        )
73
74
75

    @property
    def connection(self):
Lukas Burgey's avatar
Lukas Burgey committed
76
77
78
79
        LOGGER.debug(self._msg('opened connection'))
        return pika.BlockingConnection(
            self._connection_parameters,
        )
80
81
82

    @property
    def channel(self):
Lukas Burgey's avatar
Lukas Burgey committed
83
84
85
86
87
88
89
90
91
92
93
94
95
96
        rabbitmq_channel = self.connection.channel()
        rabbitmq_channel.exchange_declare(
            exchange=self.exchange,
            durable=True,
            auto_delete=False,
            exchange_type='topic',
        )
        rabbitmq_channel.confirm_delivery()
        LOGGER.debug(self._msg('opened channel'))

        return rabbitmq_channel

    def _get_api_uri(self, path):
        return 'http://{}:{}/{}/{}'.format(
Lukas Burgey's avatar
Lukas Burgey committed
97
98
99
            self.host,
            self.port,
            self.path,
Lukas Burgey's avatar
Lukas Burgey committed
100
            path,
Lukas Burgey's avatar
Lukas Burgey committed
101
102
        )

Lukas Burgey's avatar
Lukas Burgey committed
103
    def _rest_get(self, api_path):
Lukas Burgey's avatar
Lukas Burgey committed
104
        req = requests.get(
Lukas Burgey's avatar
Lukas Burgey committed
105
            self._get_api_uri(api_path),
Lukas Burgey's avatar
Lukas Burgey committed
106
            auth=self.auth)
Lukas Burgey's avatar
Lukas Burgey committed
107
108
        req.raise_for_status()
        return req.json()
Lukas Burgey's avatar
Lukas Burgey committed
109
110
111

    # send a rest call with path and data to the rest interface of
    # the rabbitmq instance
Lukas Burgey's avatar
Lukas Burgey committed
112
    def _rest_put(self, api_path, data):
Lukas Burgey's avatar
Lukas Burgey committed
113
        req = requests.put(
Lukas Burgey's avatar
Lukas Burgey committed
114
            self._get_api_uri(api_path),
Lukas Burgey's avatar
Lukas Burgey committed
115
116
            json=data,
            auth=self.auth)
Lukas Burgey's avatar
Lukas Burgey committed
117
118
        req.raise_for_status()
        return req
Lukas Burgey's avatar
Lukas Burgey committed
119

Lukas Burgey's avatar
Lukas Burgey committed
120
    def _rest_del(self, api_path):
Lukas Burgey's avatar
Lukas Burgey committed
121
        req = requests.delete(
Lukas Burgey's avatar
Lukas Burgey committed
122
            self._get_api_uri(api_path),
Lukas Burgey's avatar
Lukas Burgey committed
123
            auth=self.auth)
Lukas Burgey's avatar
Lukas Burgey committed
124
125
        req.raise_for_status()
        return req
Lukas Burgey's avatar
Lukas Burgey committed
126

Lukas Burgey's avatar
Lukas Burgey committed
127
    def _set_topic_permissions(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
128
129
        username = site.client.username
        path = 'topic-permissions/{}/{}/'.format(
Lukas Burgey's avatar
Lukas Burgey committed
130
131
132
            self.vhost,
            username,
        )
Lukas Burgey's avatar
Lukas Burgey committed
133
134
135
136

        # set permissions for the correct topics
        # we construct a regex to match the services of the site
        services = ''
Lukas Burgey's avatar
Lukas Burgey committed
137
        omit_bar = True
Lukas Burgey's avatar
Lukas Burgey committed
138
139
        for service in site.services.all():
            prefix = '|'
Lukas Burgey's avatar
Lukas Burgey committed
140
            if omit_bar:
Lukas Burgey's avatar
Lukas Burgey committed
141
                prefix = ''
Lukas Burgey's avatar
Lukas Burgey committed
142
                omit_bar = False
Lukas Burgey's avatar
Lukas Burgey committed
143
144
145
146

            services = services + prefix + service.name

        set_topic_permission_data = {
Lukas Burgey's avatar
Lukas Burgey committed
147
148
            'exchange': self.exchange,
            'write': '^$',
Lukas Burgey's avatar
Lukas Burgey committed
149
            'read': r'^service\.({})$'.format(services),
Lukas Burgey's avatar
Lukas Burgey committed
150
        }
Lukas Burgey's avatar
Lukas Burgey committed
151

Lukas Burgey's avatar
Lukas Burgey committed
152
        return self._rest_put(path, set_topic_permission_data)
Lukas Burgey's avatar
Lukas Burgey committed
153
154

    # set permissions for the user
Lukas Burgey's avatar
Lukas Burgey committed
155
    def _set_permissions(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
156
157
        username = site.client.username
        path = 'permissions/{}/{}/'.format(
Lukas Burgey's avatar
Lukas Burgey committed
158
159
160
            self.vhost,
            username,
        )
Lukas Burgey's avatar
Lukas Burgey committed
161
        permission = r'^(amq\.gen.*|{})'.format(self.exchange)
Lukas Burgey's avatar
Lukas Burgey committed
162
        set_permission_data = {
Lukas Burgey's avatar
Lukas Burgey committed
163
164
165
166
            'configure': permission,
            'write': permission,
            'read': permission,
        }
Lukas Burgey's avatar
Lukas Burgey committed
167

Lukas Burgey's avatar
Lukas Burgey committed
168
        return self._rest_put(path, set_permission_data)
Lukas Burgey's avatar
Lukas Burgey committed
169
170

    # create user at the rabbitmq instance
Lukas Burgey's avatar
Lukas Burgey committed
171
    def _create_user(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
172
173
174
175
        username = site.client.username
        path = 'users/{}/'.format(username)

        user_creation_data = {
Lukas Burgey's avatar
Lukas Burgey committed
176
177
178
            'password': str(site.client.auth_token.key),
            'tags': '',
        }
Lukas Burgey's avatar
Lukas Burgey committed
179

Lukas Burgey's avatar
Lukas Burgey committed
180
        return self._rest_put(path, user_creation_data)
Lukas Burgey's avatar
Lukas Burgey committed
181
182

    # delete user at the rabbitmq instance
Lukas Burgey's avatar
Lukas Burgey committed
183
    def _delete_user(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
184
185
186
        username = site.client.username
        path = 'users/{}/'.format(username)

Lukas Burgey's avatar
Lukas Burgey committed
187
188
189
190
191
        return self._rest_del(path)

    def _disconnect(self):
        LOGGER.debug(self._msg('closing connection'))
        self.connection.close()
Lukas Burgey's avatar
Lukas Burgey committed
192
193
194
195

    # PUBLIC API

    def register_site(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
196
197
198
199
        self._create_user(site)
        self._set_permissions(site)
        self._set_topic_permissions(site)
        LOGGER.info(self._msg('registered {}'.format(site.client)))
Lukas Burgey's avatar
Lukas Burgey committed
200
201

    def update_site(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
202
203
        self._set_topic_permissions(site)
        LOGGER.info(self._msg('updated permissions for {}'.format(site.client)))
Lukas Burgey's avatar
Lukas Burgey committed
204
205

    def deregister_site(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
206
207
        # TODO implement
        LOGGER.info(self._msg('deregistered {}'.format(site.client)))
Lukas Burgey's avatar
Lukas Burgey committed
208
209

    def is_client_connected(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
210
        connections = self._rest_get("connections/")
Lukas Burgey's avatar
Lukas Burgey committed
211
212
213
214
215
216
217
218
219
220
        clients_for_site = [c
                            for c in connections
                            if c['user'] == site.client.username]
        return len(clients_for_site) > 0

    def online_clients(self, service):
        return [site
                for site in service.site.all()
                if self.is_client_connected(site)]

221
222
    def publish_by_service(self, service, msg):
        return self.channel.basic_publish(
Lukas Burgey's avatar
Lukas Burgey committed
223
            exchange=self.exchange,
224
            routing_key=service.routing_key,
Lukas Burgey's avatar
Lukas Burgey committed
225
226
227
228
229
            body=msg,
            properties=pika.BasicProperties(
                delivery_mode=1,
            ),
        )
Lukas Burgey's avatar
Lukas Burgey committed
230
231


Lukas Burgey's avatar
Lukas Burgey committed
232
233
def default_rabbitmq_instance():
    return RabbitMQInstance.objects.filter(is_active=True).first()
Lukas Burgey's avatar
Lukas Burgey committed
234
235


Lukas Burgey's avatar
Lukas Burgey committed
236
class User(AbstractUser):
237
    TYPE_CHOICES = (
Lukas Burgey's avatar
Lukas Burgey committed
238
239
240
241
        ('apiclient', 'API-Client'),
        ('oidcuser', 'OIDC User'),
        ('admin', 'Admin'),
    )
242
    user_type = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
243
244
245
246
        max_length=20,
        choices=TYPE_CHOICES,
        default='oidcuser',
    )
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
    sub = models.CharField(
        max_length=150,
        blank=True,
        null=True,
    )
    password = models.CharField(
        max_length=150,
        blank=True,
        null=True,
    )
    # the real state of the user
    # (self.is_active is the supposed state of the user)
    _is_active = models.BooleanField(
        default=True,
        editable=False,
    )
Lukas Burgey's avatar
Lukas Burgey committed
263
264
265
266
267
268
    # the idp which authenticated the user
    idp = models.ForeignKey(
        OIDCConfig,
        related_name='users',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
269

Lukas Burgey's avatar
Lukas Burgey committed
270
    # we hide deleted keys here
271
    # the full list of ssh keys is self._ssh_keys
Lukas Burgey's avatar
Lukas Burgey committed
272
273
274
275
    @property
    def ssh_keys(self):
        return self._ssh_keys.filter(deleted=False)

276
277
278
279
    @property
    def is_active_at_clients(self):
        return self._is_active

280
281
282
    def __str__(self):
        if self.user_type == 'admin':
            return 'ADMIN {}'.format(self.username)
Lukas Burgey's avatar
Lukas Burgey committed
283
        elif self.user_type == 'oidcuser':
284
285
286
            if not self.is_active:
                return 'DEACTIVATED USER {}'.format(self.username)
            return 'USER {}'.format(self.username)
Lukas Burgey's avatar
Lukas Burgey committed
287
        elif self.user_type == 'apiclient':
288
            return 'APICLIENT {}@{}'.format(self.username, self.site)
Lukas Burgey's avatar
Lukas Burgey committed
289
290
        else:
            raise Exception()
Lukas Burgey's avatar
Lukas Burgey committed
291

Lukas Burgey's avatar
Lukas Burgey committed
292
    def _msg(self, msg):
293
294
295
296
297
298
        return '[{}] {}'.format(self, msg)

    # oidcuser: withdraw and delete all credentials and delete the user
    def remove(self):
        if self.user_type == 'oidcuser':
            self.deactivate()
Lukas Burgey's avatar
Lukas Burgey committed
299
            LOGGER.info(self._msg('Deleting'))
300
301
302
303
304
305
306

            # TODO: deleting the user brings problems:
            # the deletion cascades down to DeploymentTask and DeploymentTaskItem
            # but these need to be conserved so all clients withdrawals can be tracked
            self.delete()

    def activate(self):
307
        if self._is_active:
Lukas Burgey's avatar
Lukas Burgey committed
308
            LOGGER.error(self._msg('already activated'))
309
310
311
312
            return

        if self.user_type == 'oidcuser':
            self.is_active = True
313
            self._is_active = True
314
315
316
317
318
            self.save()

            for dep in self.deployments.all():
                dep.activate()

Lukas Burgey's avatar
Lukas Burgey committed
319
            LOGGER.info(self._msg('activated'))
320
321
322

    # oidcuser: withdraw all credentials
    def deactivate(self):
323
        if not self._is_active:
Lukas Burgey's avatar
Lukas Burgey committed
324
            LOGGER.error(self._msg('already deactivated'))
325
326
327
328
            return

        if self.user_type == 'oidcuser':
            self.is_active = False
329
            self._is_active = False
330
331
332
333
334
            self.save()

            for dep in self.deployments.all():
                dep.deactivate()

Lukas Burgey's avatar
Lukas Burgey committed
335
            LOGGER.info(self._msg('deactivated'))
336
337


Lukas Burgey's avatar
Lukas Burgey committed
338
339
def construct_user(user_info):
    return User(
Lukas Burgey's avatar
Lukas Burgey committed
340
341
342
343
344
345
346
        sub=user_info['sub'],
        name=user_info['name'],
        first_name=user_info['given_name'],
        last_name=user_info['family_name'],
        email=user_info['email'],
        username=user_info['email'],
    )
Lukas Burgey's avatar
Lukas Burgey committed
347
348
349


class Site(models.Model):
350
    client = models.OneToOneField(
Lukas Burgey's avatar
Lukas Burgey committed
351
352
353
        User,
        related_name='site',
    )
Lukas Burgey's avatar
Lukas Burgey committed
354
355
356
357
358
359
    name = models.CharField(max_length=150, unique=True)
    description = models.TextField(max_length=300, blank=True)

    def __str__(self):
        return self.name

360
361
362
363
364
365
    # tasks which are still to be executed on this site
    @property
    def tasks(self):
        return [item.task
                for item
                in self.task_items.all()]
Lukas Burgey's avatar
Lukas Burgey committed
366

Lukas Burgey's avatar
Lukas Burgey committed
367
368
369
370

class Service(models.Model):
    name = models.CharField(max_length=150, unique=True)
    description = models.TextField(max_length=300, blank=True)
371
    site = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
372
373
        Site,
        related_name='services')
Lukas Burgey's avatar
Lukas Burgey committed
374
    groups = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
375
376
377
        Group,
        related_name='services',
        blank=True)
Lukas Burgey's avatar
Lukas Burgey committed
378

379
380
381
382
    @property
    def routing_key(self):
        return 'service.{}'.format(self.name)

Lukas Burgey's avatar
Lukas Burgey committed
383
    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
384
        return self.name
Lukas Burgey's avatar
Lukas Burgey committed
385
386
387


class SSHPublicKey(models.Model):
Lukas Burgey's avatar
Lukas Burgey committed
388
389
390
391
392
393
394
    name = models.CharField(
        max_length=150,
        unique=True,
    )
    key = models.TextField(
        max_length=1000
    )
Lukas Burgey's avatar
Lukas Burgey committed
395
    # hidden field at the user
Lukas Burgey's avatar
Lukas Burgey committed
396
    user = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
397
398
399
        User,
        related_name='_ssh_keys',
    )
Lukas Burgey's avatar
Lukas Burgey committed
400

Lukas Burgey's avatar
Lukas Burgey committed
401
402
    # has the user triggered the deletion of this key
    deleted = models.BooleanField(
Lukas Burgey's avatar
Lukas Burgey committed
403
404
405
        default=False,
        editable=False,
    )
406

Lukas Burgey's avatar
Lukas Burgey committed
407
    def _msg(self, msg):
Lukas Burgey's avatar
Lukas Burgey committed
408
        return '[SSHPublicKey:{}] {}'.format(self, msg)
409

410
411
412
    # does not directly delete the key if the key is deployed or withdrawn
    # somewhere
    # the receiver 'delete_withdrawn_ssh_key' does the actual deletion
413
    def delete_key(self):
Lukas Burgey's avatar
Lukas Burgey committed
414
        if (not self.tasks.exists() and not self.deployments.exists()):
Lukas Burgey's avatar
Lukas Burgey committed
415
            LOGGER.info(self._msg('Direct deletion of key'))
416
417
418
            self.delete()
            return

Lukas Burgey's avatar
Lukas Burgey committed
419
        LOGGER.info(self._msg('Deletion of key started'))
420
421
422
        self.deleted = True
        self.save()

Lukas Burgey's avatar
Lukas Burgey committed
423
        # delete implies withdrawing the key from all clients
424
425
426
        for deployment in self.deployments.all():
            deployment.withdraw_key(self)

Lukas Burgey's avatar
Lukas Burgey committed
427
428
    # when a key is withdrawn by a client we try to finally delete it
    def try_final_deletion(self):
Lukas Burgey's avatar
Lukas Burgey committed
429
        if (self.deleted and not self.tasks.exists()):
Lukas Burgey's avatar
Lukas Burgey committed
430
            LOGGER.info(self._msg(
Lukas Burgey's avatar
Lukas Burgey committed
431
                'All clients have withdrawn this key. Final deletion'))
Lukas Burgey's avatar
Lukas Burgey committed
432
433
434
            self.delete()
            return

Lukas Burgey's avatar
Lukas Burgey committed
435
    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
436
437
        if self.deleted:
            return "DELETED: {}".format(self.name)
Lukas Burgey's avatar
Lukas Burgey committed
438
439
440
        return self.name


441
# Deployment describes the credential state per user as it is supposed to be
442
443
444
445
#
# (exception: if is_active=False the ssh_keys contain the keys to be deployed
# if the deployment is reactivated)
#
446
447
# DeploymentTask is what is sent to the clients via rabbitmq
# The DeploymentTaskItem track the acknowledgements from the clients
Lukas Burgey's avatar
Lukas Burgey committed
448
449
class Deployment(models.Model):
    user = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
450
451
452
453
        User,
        related_name='deployments',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
454
    service = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
455
456
457
458
        Service,
        related_name='deployments',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
459
    ssh_keys = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
460
461
462
463
        SSHPublicKey,
        related_name='deployments',
        blank=True,
    )
464
    ssh_keys_to_withdraw = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
465
466
467
468
        SSHPublicKey,
        related_name='withdrawn_deployments',
        blank=True,
    )
469
    is_active = models.BooleanField(
Lukas Burgey's avatar
Lukas Burgey committed
470
471
        default=True,
    )
472

473
474
475
    @property
    def withdrawals(self):
        return self.tasks.filter(action='withdraw')
Lukas Burgey's avatar
Lukas Burgey committed
476

477
478
479
    @property
    def deploys(self):
        return self.tasks.filter(action='deploy')
Lukas Burgey's avatar
Lukas Burgey committed
480

481
482
    def __str__(self):
        return '{}:{}'.format(self.service, self.user)
483

Lukas Burgey's avatar
Lukas Burgey committed
484
    def _msg(self, msg):
485
        return '[Deployment:{}] {}'.format(self, msg)
486

487
488
489
    # deploy credentials which were deployed prior to deactivation
    def activate(self):
        if self.is_active:
Lukas Burgey's avatar
Lukas Burgey committed
490
            LOGGER.error(self._msg('already active'))
491
492
            return

Lukas Burgey's avatar
Lukas Burgey committed
493
        LOGGER.debug(self._msg(str(self.ssh_keys.all())))
494
495
496
497
498
        for key in self.ssh_keys.all():
            self._deploy_key(key)

        self.is_active = True
        self.save()
Lukas Burgey's avatar
Lukas Burgey committed
499
        LOGGER.info(self._msg('activated'))
500
501
502
503

    # withdraw all credentials
    def deactivate(self):
        if not self.is_active:
Lukas Burgey's avatar
Lukas Burgey committed
504
            LOGGER.error(self._msg('already deactivated'))
505
506
507
            return

        self.is_active = False
508
        self.save()
509

510
511
512
        for key in self.ssh_keys.all():
            self._withdraw_key(key)

Lukas Burgey's avatar
Lukas Burgey committed
513
        LOGGER.info(self._msg('deactivated'))
514
515
516
517

    # only deploy the key
    def _deploy_key(self, key):
        # delete outstanding tasks which are made obsolete by this task
518
        for withdrawal in self.withdrawals.filter(key=key):
Lukas Burgey's avatar
Lukas Burgey committed
519
            LOGGER.debug(withdrawal._msg('now obsolete'))
Lukas Burgey's avatar
Lukas Burgey committed
520
            withdrawal.delete()
521
522
523

        # generate task
        task = DeploymentTask(
Lukas Burgey's avatar
Lukas Burgey committed
524
525
526
527
            action='deploy',
            deployment=self,
            key=key,
        )
528
        task.save()
Lukas Burgey's avatar
Lukas Burgey committed
529
        LOGGER.debug(task._msg('generated'))
530
531
532
533

        # generate task items
        for site in self.service.site.all():
            deploy = DeploymentTaskItem(
Lukas Burgey's avatar
Lukas Burgey committed
534
535
536
                task=task,
                site=site,
            )
537
            deploy.save()
Lukas Burgey's avatar
Lukas Burgey committed
538
            LOGGER.debug(deploy._msg('generated'))
539
540
541

        # publish the task
        task.publish()
542

543
544
    def _withdraw_key(self, key):
        # delete outstanding tasks which are made obsolete by this task
545
        for deploy in self.deploys.filter(key=key):
Lukas Burgey's avatar
Lukas Burgey committed
546
            LOGGER.debug(deploy._msg("now obsolete"))
Lukas Burgey's avatar
Lukas Burgey committed
547
            deploy.delete()
Lukas Burgey's avatar
Lukas Burgey committed
548

549
550
        # generate task
        task = DeploymentTask(
Lukas Burgey's avatar
Lukas Burgey committed
551
552
553
554
            action='withdraw',
            deployment=self,
            key=key,
        )
555
        task.save()
Lukas Burgey's avatar
Lukas Burgey committed
556
        LOGGER.debug(task._msg('generated'))
Lukas Burgey's avatar
Lukas Burgey committed
557

558
559
560
        # generate task items
        for site in self.service.site.all():
            withdrawal = DeploymentTaskItem(
Lukas Burgey's avatar
Lukas Burgey committed
561
562
563
                task=task,
                site=site,
            )
564
            withdrawal.save()
Lukas Burgey's avatar
Lukas Burgey committed
565
            LOGGER.debug(withdrawal._msg('generated'))
566

567
568
        # publish the task
        task.publish()
Lukas Burgey's avatar
Lukas Burgey committed
569

570
571
572
    # deploy key and track changes in the key lists
    def deploy_key(self, key):
        if not self.is_active:
Lukas Burgey's avatar
Lukas Burgey committed
573
            LOGGER.error(self._msg('cannot deploy while deactivated'))
574
575
576
577
578
579
580
581
582
583
584
585
586
            raise Exception('deployment deactivated')

        self.ssh_keys.add(key)

        if key in self.ssh_keys_to_withdraw.all():
            self.ssh_keys_to_withdraw.remove(key)
        self.save()

        self._deploy_key(key)

    # withdraw key and track changes in the key lists
    def withdraw_key(self, key):
        if not self.is_active:
Lukas Burgey's avatar
Lukas Burgey committed
587
            LOGGER.error(self._msg('cannot withdraw while deactivated'))
588
589
590
591
592
593
594
595
596
597
            raise Exception('deployment deactivated')

        self.ssh_keys.remove(key)

        # keys which are to be withdrawn by the clients
        self.ssh_keys_to_withdraw.add(key)
        self.save()

        self._withdraw_key(key)

Lukas Burgey's avatar
Lukas Burgey committed
598

599
600
class DeploymentTask(models.Model):
    ACTION_CHOICES = (
Lukas Burgey's avatar
Lukas Burgey committed
601
602
603
        ('deploy', 'deploy'),
        ('withdraw', 'withdraw'),
    )
604
    action = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
605
606
607
        max_length=10,
        choices=ACTION_CHOICES,
    )
608
    key = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
609
610
611
612
        SSHPublicKey,
        related_name='tasks',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
613
    deployment = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
614
615
616
617
        Deployment,
        related_name='tasks',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
618
619
620
621
622
623
624
625
626

    @property
    def user(self):
        return self.deployment.user

    @property
    def service(self):
        return self.deployment.service

627
    def __str__(self):
628
        return "{}:{}:{} - {}".format(
Lukas Burgey's avatar
Lukas Burgey committed
629
630
631
632
633
            self.deployment.service,
            self.deployment.user,
            self.key,
            self.action,
        )
634

Lukas Burgey's avatar
Lukas Burgey committed
635
    def _msg(self, msg):
Lukas Burgey's avatar
Lukas Burgey committed
636
        return '[DeploymentTask:{}] {}'.format(self, msg)
637
638

    def publish(self):
639
        # FIXME mitigating circular dependencies here
640
641
642
        from .clientapi.serializers import DeploymentTaskSerializer
        msg = json.dumps(DeploymentTaskSerializer(self).data)

Lukas Burgey's avatar
Lukas Burgey committed
643
644
        # FIXME select the rabbitmq instance more meaningful
        default_rabbitmq_instance().publish_by_service(
Lukas Burgey's avatar
Lukas Burgey committed
645
646
647
            self.service,
            msg,
        )
648
649
650

    # the client acked the receipt and execution of the task for his site
    def item_finished(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
651
        item = self.task_items.get(site=site)
Lukas Burgey's avatar
Lukas Burgey committed
652
        LOGGER.debug(item._msg('done'))
Lukas Burgey's avatar
Lukas Burgey committed
653
        item.delete()
654
655
656
657
658
659

        if not self.task_items.exists():
            self.finished()

    # maintenance after all task items are done
    def finished(self):
Lukas Burgey's avatar
Lukas Burgey committed
660
        LOGGER.info(self._msg('done'))
Lukas Burgey's avatar
Lukas Burgey committed
661
        self.delete()
662
663
664
665
666
667
668
669

        # check if this was the final withdraw in a key deletion
        if self.action == 'withdraw':
            self.key.try_final_deletion()


class DeploymentTaskItem(models.Model):
    task = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
670
671
672
673
        DeploymentTask,
        related_name='task_items',
        on_delete=models.CASCADE,
    )
674
    site = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
675
676
677
678
        Site,
        related_name='task_items',
        on_delete=models.CASCADE,
    )
679

Lukas Burgey's avatar
Lukas Burgey committed
680
    def __str__(self):
681
        return "{}@{}".format(
Lukas Burgey's avatar
Lukas Burgey committed
682
683
684
            self.task,
            self.site,
        )
685

Lukas Burgey's avatar
Lukas Burgey committed
686
    def _msg(self, msg):
Lukas Burgey's avatar
Lukas Burgey committed
687
        return '[DeploymentTaskItem:{}] {}'.format(self, msg)
688

Lukas Burgey's avatar
Lukas Burgey committed
689

690
691
692
#
# RECEIVERS
#
Lukas Burgey's avatar
Lukas Burgey committed
693
694
695
696
697
698
699
700

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if instance.user_type == 'apiclient' and created:
        Token.objects.create(user=instance)


@receiver(post_save, sender=Site)
Lukas Burgey's avatar
Lukas Burgey committed
701
def register_at_rabbitmq(sender, instance=None, created=False, **kwargs):
Lukas Burgey's avatar
Lukas Burgey committed
702
703
704
705
706
707
708
    if not created:
        return

    RabbitMQInstance().register_site(instance)


@receiver(pre_delete, sender=Site)
Lukas Burgey's avatar
Lukas Burgey committed
709
def deregister_at_rabbitmq(sender, instance=None, **kwargs):
Lukas Burgey's avatar
Lukas Burgey committed
710
    RabbitMQInstance().deregister_site(instance)
711
712
713
714
715
716
717


@receiver(post_save, sender=User)
def deactivate_user(sender, instance=None, created=False, **kwargs):
    if created:
        return

718
    if not instance.is_active and instance.is_active_at_clients:
719
720
721
722
723
724
725
726
        instance.deactivate()


@receiver(post_save, sender=User)
def activate_user(sender, instance=None, created=False, **kwargs):
    if created:
        return

727
    if instance.is_active and not instance.is_active_at_clients:
728
        instance.activate()