models.py 19.9 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
    exchange = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
28
29
30
        max_length=150,
        default='deployments',
    )
Lukas Burgey's avatar
Lukas Burgey committed
31
    port = models.IntegerField(
Lukas Burgey's avatar
Lukas Burgey committed
32
33
        default=15672
    )
Lukas Burgey's avatar
Lukas Burgey committed
34
    path = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
35
36
37
        max_length=150,
        default='api',
    )
Lukas Burgey's avatar
Lukas Burgey committed
38
    username = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
39
40
41
        max_length=150,
        default='guest',
    )
Lukas Burgey's avatar
Lukas Burgey committed
42
    password = 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
47
    is_active = models.BooleanField(
        default=True,
Lukas Burgey's avatar
Lukas Burgey committed
48
    )
Lukas Burgey's avatar
Lukas Burgey committed
49
50
51
52

    def __str__(self):
        return self.host

Lukas Burgey's avatar
Lukas Burgey committed
53
54
    def msg(self, msg):
        return '[RabbitMQ:{}] {}'.format(self.host, msg)
Lukas Burgey's avatar
Lukas Burgey committed
55
56
57
58

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

    @property
    def vhost(self):
        return '%2f'

67
68
69
70
71
72
    # singletons
    rabbitmq_connection = None
    rabbitmq_channel = None

    @property
    def connection(self):
73
74
75
76
77
        if (
                self.rabbitmq_connection is None
                or self.rabbitmq_connection.is_closed
                or self.rabbitmq_connection.is_closing
        ):
78
            rabbitmqconnection_properties = pika.ConnectionParameters(
Lukas Burgey's avatar
Lukas Burgey committed
79
80
81
                host=self.host,
                ssl=True,
            )
82
            self.rabbitmq_connection = pika.BlockingConnection(
Lukas Burgey's avatar
Lukas Burgey committed
83
84
                rabbitmqconnection_properties
            )
Lukas Burgey's avatar
Lukas Burgey committed
85
            LOGGER.debug(self.msg('opened connection'))
86
87
88
89
90

        return self.rabbitmq_connection

    @property
    def channel(self):
91
92
93
94
95
        if (
                self.rabbitmq_channel is None
                or self.rabbitmq_channel.is_closed
                or self.rabbitmq_channel.is_closing
        ):
96
97
            self.rabbitmq_channel = self.connection.channel()
            self.rabbitmq_channel.exchange_declare(
Lukas Burgey's avatar
Lukas Burgey committed
98
99
100
                exchange=self.exchange,
                durable=True,
                exchange_type='topic')
101
            self.rabbitmq_channel.confirm_delivery()
Lukas Burgey's avatar
Lukas Burgey committed
102
            LOGGER.debug(self.msg('opened channel'))
Lukas Burgey's avatar
Lukas Burgey committed
103

104
105
        return self.rabbitmq_channel

Lukas Burgey's avatar
Lukas Burgey committed
106
    def get_uri(self, path):
Lukas Burgey's avatar
Lukas Burgey committed
107
108
109
110
111
112
113
        api = 'http://{}:{}/{}'.format(
            self.host,
            self.port,
            self.path,
        )

        return '{}/{}'.format(api, path)
Lukas Burgey's avatar
Lukas Burgey committed
114
115

    def rest_get(self, api_path):
Lukas Burgey's avatar
Lukas Burgey committed
116
        req = requests.get(
Lukas Burgey's avatar
Lukas Burgey committed
117
118
            self.get_uri(api_path),
            auth=self.auth)
Lukas Burgey's avatar
Lukas Burgey committed
119
120
        req.raise_for_status()
        return req.json()
Lukas Burgey's avatar
Lukas Burgey committed
121
122
123
124

    # send a rest call with path and data to the rest interface of
    # the rabbitmq instance
    def rest_put(self, api_path, data):
Lukas Burgey's avatar
Lukas Burgey committed
125
        req = requests.put(
Lukas Burgey's avatar
Lukas Burgey committed
126
127
128
            self.get_uri(api_path),
            json=data,
            auth=self.auth)
Lukas Burgey's avatar
Lukas Burgey committed
129
130
        req.raise_for_status()
        return req
Lukas Burgey's avatar
Lukas Burgey committed
131
132

    def rest_del(self, api_path):
Lukas Burgey's avatar
Lukas Burgey committed
133
        req = requests.delete(
Lukas Burgey's avatar
Lukas Burgey committed
134
135
            self.get_uri(api_path),
            auth=self.auth)
Lukas Burgey's avatar
Lukas Burgey committed
136
137
        req.raise_for_status()
        return req
Lukas Burgey's avatar
Lukas Burgey committed
138
139
140
141

    def set_topic_permissions(self, site):
        username = site.client.username
        path = 'topic-permissions/{}/{}/'.format(
Lukas Burgey's avatar
Lukas Burgey committed
142
143
144
            self.vhost,
            username,
        )
Lukas Burgey's avatar
Lukas Burgey committed
145
146
147
148

        # 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
149
        omit_bar = True
Lukas Burgey's avatar
Lukas Burgey committed
150
151
        for service in site.services.all():
            prefix = '|'
Lukas Burgey's avatar
Lukas Burgey committed
152
            if omit_bar:
Lukas Burgey's avatar
Lukas Burgey committed
153
                prefix = ''
Lukas Burgey's avatar
Lukas Burgey committed
154
                omit_bar = False
Lukas Burgey's avatar
Lukas Burgey committed
155
156
157
158

            services = services + prefix + service.name

        set_topic_permission_data = {
Lukas Burgey's avatar
Lukas Burgey committed
159
160
            'exchange': self.exchange,
            'write': '^$',
Lukas Burgey's avatar
Lukas Burgey committed
161
            'read': r'^service\.({})$'.format(services),
Lukas Burgey's avatar
Lukas Burgey committed
162
        }
Lukas Burgey's avatar
Lukas Burgey committed
163
164
165
166
167
168
169

        return self.rest_put(path, set_topic_permission_data)

    # set permissions for the user
    def set_permissions(self, site):
        username = site.client.username
        path = 'permissions/{}/{}/'.format(
Lukas Burgey's avatar
Lukas Burgey committed
170
171
172
            self.vhost,
            username,
        )
Lukas Burgey's avatar
Lukas Burgey committed
173
        permission = r'^(amq\.gen.*|{})'.format(self.exchange)
Lukas Burgey's avatar
Lukas Burgey committed
174
        set_permission_data = {
Lukas Burgey's avatar
Lukas Burgey committed
175
176
177
178
            'configure': permission,
            'write': permission,
            'read': permission,
        }
Lukas Burgey's avatar
Lukas Burgey committed
179
180
181
182
183
184
185
186
187

        return self.rest_put(path, set_permission_data)

    # create user at the rabbitmq instance
    def create_user(self, site):
        username = site.client.username
        path = 'users/{}/'.format(username)

        user_creation_data = {
Lukas Burgey's avatar
Lukas Burgey committed
188
189
190
            'password': str(site.client.auth_token.key),
            'tags': '',
        }
Lukas Burgey's avatar
Lukas Burgey committed
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206

        return self.rest_put(path, user_creation_data)

    # delete user at the rabbitmq instance
    def delete_user(self, site):
        username = site.client.username
        path = 'users/{}/'.format(username)

        return self.rest_del(path)

    # PUBLIC API

    def register_site(self, site):
        self.create_user(site)
        self.set_permissions(site)
        self.set_topic_permissions(site)
Lukas Burgey's avatar
Lukas Burgey committed
207
        LOGGER.info(self.msg('registered {}'.format(site.client)))
Lukas Burgey's avatar
Lukas Burgey committed
208
209
210

    def update_site(self, site):
        self.set_topic_permissions(site)
Lukas Burgey's avatar
Lukas Burgey committed
211
        LOGGER.info(self.msg('updated permissions for {}'.format(site.client)))
Lukas Burgey's avatar
Lukas Burgey committed
212
213

    def deregister_site(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
214
        LOGGER.info(self.msg('deregistered {}'.format(site.client)))
Lukas Burgey's avatar
Lukas Burgey committed
215
216
217
218
219
220
221
222
223

    def is_client_connected(self, site):
        connections = self.rest_get("connections/")
        clients_for_site = [c
                            for c in connections
                            if c['user'] == site.client.username]
        return len(clients_for_site) > 0

    def disconnect(self):
Lukas Burgey's avatar
Lukas Burgey committed
224
        LOGGER.debug(self.msg('closing connection'))
Lukas Burgey's avatar
Lukas Burgey committed
225
226
227
228
229
230
231
        self.connection.close()

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

232
233
    def publish_by_service(self, service, msg):
        return self.channel.basic_publish(
Lukas Burgey's avatar
Lukas Burgey committed
234
            exchange=self.exchange,
235
            routing_key=service.routing_key,
Lukas Burgey's avatar
Lukas Burgey committed
236
237
238
239
240
            body=msg,
            properties=pika.BasicProperties(
                delivery_mode=1,
            ),
        )
Lukas Burgey's avatar
Lukas Burgey committed
241
242


Lukas Burgey's avatar
Lukas Burgey committed
243
RABBITMQ_INSTANCE = RabbitMQInstance.objects.filter(is_active=True).first()
Lukas Burgey's avatar
Lukas Burgey committed
244
245


Lukas Burgey's avatar
Lukas Burgey committed
246
class User(AbstractUser):
247
    TYPE_CHOICES = (
Lukas Burgey's avatar
Lukas Burgey committed
248
249
250
251
        ('apiclient', 'API-Client'),
        ('oidcuser', 'OIDC User'),
        ('admin', 'Admin'),
    )
252
    user_type = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
253
254
255
256
        max_length=20,
        choices=TYPE_CHOICES,
        default='oidcuser',
    )
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
    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
273
274
275
276
277
278
    # the idp which authenticated the user
    idp = models.ForeignKey(
        OIDCConfig,
        related_name='users',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
279

Lukas Burgey's avatar
Lukas Burgey committed
280
    # we hide deleted keys here
281
    # the full list of ssh keys is self._ssh_keys
Lukas Burgey's avatar
Lukas Burgey committed
282
283
284
285
    @property
    def ssh_keys(self):
        return self._ssh_keys.filter(deleted=False)

286
287
288
289
    @property
    def is_active_at_clients(self):
        return self._is_active

290
291
292
    def __str__(self):
        if self.user_type == 'admin':
            return 'ADMIN {}'.format(self.username)
Lukas Burgey's avatar
Lukas Burgey committed
293
        elif self.user_type == 'oidcuser':
294
295
296
            if not self.is_active:
                return 'DEACTIVATED USER {}'.format(self.username)
            return 'USER {}'.format(self.username)
Lukas Burgey's avatar
Lukas Burgey committed
297
        elif self.user_type == 'apiclient':
298
            return 'APICLIENT {}@{}'.format(self.username, self.site)
Lukas Burgey's avatar
Lukas Burgey committed
299
300
        else:
            raise Exception()
Lukas Burgey's avatar
Lukas Burgey committed
301

302
303
304
305
306
307
308
    def msg(self, msg):
        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
309
            LOGGER.info(self.msg('Deleting'))
310
311
312
313
314
315
316

            # 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):
317
        if self._is_active:
Lukas Burgey's avatar
Lukas Burgey committed
318
            LOGGER.error(self.msg('already activated'))
319
320
321
322
            return

        if self.user_type == 'oidcuser':
            self.is_active = True
323
            self._is_active = True
324
325
326
327
328
            self.save()

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

Lukas Burgey's avatar
Lukas Burgey committed
329
            LOGGER.info(self.msg('activated'))
330
331
332

    # oidcuser: withdraw all credentials
    def deactivate(self):
333
        if not self._is_active:
Lukas Burgey's avatar
Lukas Burgey committed
334
            LOGGER.error(self.msg('already deactivated'))
335
336
337
338
            return

        if self.user_type == 'oidcuser':
            self.is_active = False
339
            self._is_active = False
340
341
342
343
344
            self.save()

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

Lukas Burgey's avatar
Lukas Burgey committed
345
            LOGGER.info(self.msg('deactivated'))
346
347


Lukas Burgey's avatar
Lukas Burgey committed
348
349
def construct_user(user_info):
    return User(
Lukas Burgey's avatar
Lukas Burgey committed
350
351
352
353
354
355
356
        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
357
358
359


class Site(models.Model):
360
    client = models.OneToOneField(
Lukas Burgey's avatar
Lukas Burgey committed
361
362
363
        User,
        related_name='site',
    )
Lukas Burgey's avatar
Lukas Burgey committed
364
365
366
367
368
369
    name = models.CharField(max_length=150, unique=True)
    description = models.TextField(max_length=300, blank=True)

    def __str__(self):
        return self.name

370
371
372
373
374
375
    # 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
376

Lukas Burgey's avatar
Lukas Burgey committed
377
378
379
380

class Service(models.Model):
    name = models.CharField(max_length=150, unique=True)
    description = models.TextField(max_length=300, blank=True)
381
    site = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
382
383
        Site,
        related_name='services')
Lukas Burgey's avatar
Lukas Burgey committed
384
    groups = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
385
386
387
        Group,
        related_name='services',
        blank=True)
Lukas Burgey's avatar
Lukas Burgey committed
388

389
390
391
392
    @property
    def routing_key(self):
        return 'service.{}'.format(self.name)

Lukas Burgey's avatar
Lukas Burgey committed
393
    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
394
        return self.name
Lukas Burgey's avatar
Lukas Burgey committed
395
396
397


class SSHPublicKey(models.Model):
Lukas Burgey's avatar
Lukas Burgey committed
398
399
400
401
402
403
404
    name = models.CharField(
        max_length=150,
        unique=True,
    )
    key = models.TextField(
        max_length=1000
    )
Lukas Burgey's avatar
Lukas Burgey committed
405
    # hidden field at the user
Lukas Burgey's avatar
Lukas Burgey committed
406
    user = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
407
408
409
        User,
        related_name='_ssh_keys',
    )
Lukas Burgey's avatar
Lukas Burgey committed
410

Lukas Burgey's avatar
Lukas Burgey committed
411
412
    # has the user triggered the deletion of this key
    deleted = models.BooleanField(
Lukas Burgey's avatar
Lukas Burgey committed
413
414
415
        default=False,
        editable=False,
    )
416

Lukas Burgey's avatar
Lukas Burgey committed
417
418
    def msg(self, msg):
        return '[SSHPublicKey:{}] {}'.format(self, msg)
419

420
421
422
    # does not directly delete the key if the key is deployed or withdrawn
    # somewhere
    # the receiver 'delete_withdrawn_ssh_key' does the actual deletion
423
    def delete_key(self):
Lukas Burgey's avatar
Lukas Burgey committed
424
        if (not self.tasks.exists() and not self.deployments.exists()):
Lukas Burgey's avatar
Lukas Burgey committed
425
            LOGGER.info(self.msg('Direct deletion of key'))
426
427
428
            self.delete()
            return

Lukas Burgey's avatar
Lukas Burgey committed
429
        LOGGER.info(self.msg('Deletion of key started'))
430
431
432
        self.deleted = True
        self.save()

Lukas Burgey's avatar
Lukas Burgey committed
433
        # delete implies withdrawing the key from all clients
434
435
436
        for deployment in self.deployments.all():
            deployment.withdraw_key(self)

Lukas Burgey's avatar
Lukas Burgey committed
437
438
    # 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
439
        if (self.deleted and not self.tasks.exists()):
Lukas Burgey's avatar
Lukas Burgey committed
440
            LOGGER.info(self.msg(
Lukas Burgey's avatar
Lukas Burgey committed
441
                'All clients have withdrawn this key. Final deletion'))
Lukas Burgey's avatar
Lukas Burgey committed
442
443
444
            self.delete()
            return

Lukas Burgey's avatar
Lukas Burgey committed
445
    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
446
447
        if self.deleted:
            return "DELETED: {}".format(self.name)
Lukas Burgey's avatar
Lukas Burgey committed
448
449
450
        return self.name


451
# Deployment describes the credential state per user as it is supposed to be
452
453
454
455
#
# (exception: if is_active=False the ssh_keys contain the keys to be deployed
# if the deployment is reactivated)
#
456
457
# 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
458
459
class Deployment(models.Model):
    user = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
460
461
462
463
        User,
        related_name='deployments',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
464
    service = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
465
466
467
468
        Service,
        related_name='deployments',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
469
    ssh_keys = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
470
471
472
473
        SSHPublicKey,
        related_name='deployments',
        blank=True,
    )
474
    ssh_keys_to_withdraw = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
475
476
477
478
        SSHPublicKey,
        related_name='withdrawn_deployments',
        blank=True,
    )
479
    is_active = models.BooleanField(
Lukas Burgey's avatar
Lukas Burgey committed
480
481
        default=True,
    )
482

483
484
485
    @property
    def withdrawals(self):
        return self.tasks.filter(action='withdraw')
Lukas Burgey's avatar
Lukas Burgey committed
486

487
488
489
    @property
    def deploys(self):
        return self.tasks.filter(action='deploy')
Lukas Burgey's avatar
Lukas Burgey committed
490

491
492
    def __str__(self):
        return '{}:{}'.format(self.service, self.user)
493

494
495
    def msg(self, msg):
        return '[Deployment:{}] {}'.format(self, msg)
496

497
498
499
    # deploy credentials which were deployed prior to deactivation
    def activate(self):
        if self.is_active:
Lukas Burgey's avatar
Lukas Burgey committed
500
            LOGGER.error(self.msg('already active'))
501
502
            return

Lukas Burgey's avatar
Lukas Burgey committed
503
        LOGGER.debug(self.msg(str(self.ssh_keys.all())))
504
505
506
507
508
        for key in self.ssh_keys.all():
            self._deploy_key(key)

        self.is_active = True
        self.save()
Lukas Burgey's avatar
Lukas Burgey committed
509
        LOGGER.info(self.msg('activated'))
510
511
512
513

    # withdraw all credentials
    def deactivate(self):
        if not self.is_active:
Lukas Burgey's avatar
Lukas Burgey committed
514
            LOGGER.error(self.msg('already deactivated'))
515
516
517
            return

        self.is_active = False
518
        self.save()
519

520
521
522
        for key in self.ssh_keys.all():
            self._withdraw_key(key)

Lukas Burgey's avatar
Lukas Burgey committed
523
        LOGGER.info(self.msg('deactivated'))
524
525
526
527

    # only deploy the key
    def _deploy_key(self, key):
        # delete outstanding tasks which are made obsolete by this task
528
        for withdrawal in self.withdrawals.filter(key=key):
Lukas Burgey's avatar
Lukas Burgey committed
529
            LOGGER.debug(withdrawal.msg('now obsolete'))
Lukas Burgey's avatar
Lukas Burgey committed
530
            withdrawal.delete()
531
532
533

        # generate task
        task = DeploymentTask(
Lukas Burgey's avatar
Lukas Burgey committed
534
535
536
537
            action='deploy',
            deployment=self,
            key=key,
        )
538
        task.save()
Lukas Burgey's avatar
Lukas Burgey committed
539
        LOGGER.debug(task.msg('generated'))
540
541
542
543

        # generate task items
        for site in self.service.site.all():
            deploy = DeploymentTaskItem(
Lukas Burgey's avatar
Lukas Burgey committed
544
545
546
                task=task,
                site=site,
            )
547
            deploy.save()
Lukas Burgey's avatar
Lukas Burgey committed
548
            LOGGER.debug(deploy.msg('generated'))
549
550
551

        # publish the task
        task.publish()
552

553
554
    def _withdraw_key(self, key):
        # delete outstanding tasks which are made obsolete by this task
555
        for deploy in self.deploys.filter(key=key):
Lukas Burgey's avatar
Lukas Burgey committed
556
            LOGGER.debug(deploy.msg("now obsolete"))
Lukas Burgey's avatar
Lukas Burgey committed
557
            deploy.delete()
Lukas Burgey's avatar
Lukas Burgey committed
558

559
560
        # generate task
        task = DeploymentTask(
Lukas Burgey's avatar
Lukas Burgey committed
561
562
563
564
            action='withdraw',
            deployment=self,
            key=key,
        )
565
        task.save()
Lukas Burgey's avatar
Lukas Burgey committed
566
        LOGGER.debug(task.msg('generated'))
Lukas Burgey's avatar
Lukas Burgey committed
567

568
569
570
        # generate task items
        for site in self.service.site.all():
            withdrawal = DeploymentTaskItem(
Lukas Burgey's avatar
Lukas Burgey committed
571
572
573
                task=task,
                site=site,
            )
574
            withdrawal.save()
Lukas Burgey's avatar
Lukas Burgey committed
575
            LOGGER.debug(withdrawal.msg('generated'))
576

577
578
        # publish the task
        task.publish()
Lukas Burgey's avatar
Lukas Burgey committed
579

580
581
582
    # 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
583
            LOGGER.error(self.msg('cannot deploy while deactivated'))
584
585
586
587
588
589
590
591
592
593
594
595
596
            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
597
            LOGGER.error(self.msg('cannot withdraw while deactivated'))
598
599
600
601
602
603
604
605
606
607
            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
608

609
610
class DeploymentTask(models.Model):
    ACTION_CHOICES = (
Lukas Burgey's avatar
Lukas Burgey committed
611
612
613
        ('deploy', 'deploy'),
        ('withdraw', 'withdraw'),
    )
614
    action = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
615
616
617
        max_length=10,
        choices=ACTION_CHOICES,
    )
618
    key = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
619
620
621
622
        SSHPublicKey,
        related_name='tasks',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
623
    deployment = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
624
625
626
627
        Deployment,
        related_name='tasks',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
628
629
630
631
632
633
634
635
636

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

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

637
    def __str__(self):
638
        return "{}:{}:{} - {}".format(
Lukas Burgey's avatar
Lukas Burgey committed
639
640
641
642
643
            self.deployment.service,
            self.deployment.user,
            self.key,
            self.action,
        )
644

Lukas Burgey's avatar
Lukas Burgey committed
645
646
    def msg(self, msg):
        return '[DeploymentTask:{}] {}'.format(self, msg)
647
648

    def publish(self):
649
        # FIXME mitigating circular dependencies here
650
651
652
        from .clientapi.serializers import DeploymentTaskSerializer
        msg = json.dumps(DeploymentTaskSerializer(self).data)

Lukas Burgey's avatar
Lukas Burgey committed
653
        RABBITMQ_INSTANCE.publish_by_service(
Lukas Burgey's avatar
Lukas Burgey committed
654
655
656
            self.service,
            msg,
        )
657
658
659

    # 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
660
        item = self.task_items.get(site=site)
Lukas Burgey's avatar
Lukas Burgey committed
661
        LOGGER.debug(item.msg('done'))
Lukas Burgey's avatar
Lukas Burgey committed
662
        item.delete()
663
664
665
666
667
668

        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
669
        LOGGER.info(self.msg('done'))
Lukas Burgey's avatar
Lukas Burgey committed
670
        self.delete()
671
672
673
674
675
676
677
678

        # 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
679
680
681
682
        DeploymentTask,
        related_name='task_items',
        on_delete=models.CASCADE,
    )
683
    site = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
684
685
686
687
        Site,
        related_name='task_items',
        on_delete=models.CASCADE,
    )
688

Lukas Burgey's avatar
Lukas Burgey committed
689
    def __str__(self):
690
        return "{}@{}".format(
Lukas Burgey's avatar
Lukas Burgey committed
691
692
693
            self.task,
            self.site,
        )
694

Lukas Burgey's avatar
Lukas Burgey committed
695
696
    def msg(self, msg):
        return '[DeploymentTaskItem:{}] {}'.format(self, msg)
697

Lukas Burgey's avatar
Lukas Burgey committed
698

699
700
701
#
# RECEIVERS
#
Lukas Burgey's avatar
Lukas Burgey committed
702
703
704
705
706
707
708
709

@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
710
def register_at_rabbitmq(sender, instance=None, created=False, **kwargs):
Lukas Burgey's avatar
Lukas Burgey committed
711
712
713
714
715
716
717
    if not created:
        return

    RabbitMQInstance().register_site(instance)


@receiver(pre_delete, sender=Site)
Lukas Burgey's avatar
Lukas Burgey committed
718
def deregister_at_rabbitmq(sender, instance=None, **kwargs):
Lukas Burgey's avatar
Lukas Burgey committed
719
    RabbitMQInstance().deregister_site(instance)
720
721
722
723
724
725
726


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

727
    if not instance.is_active and instance.is_active_at_clients:
728
729
730
731
732
733
734
735
        instance.deactivate()


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

736
    if instance.is_active and not instance.is_active_at_clients:
737
        instance.activate()