models.py 20.2 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
from django.contrib.auth.models import AbstractUser, Group
11
from django.core.cache import cache
Lukas Burgey's avatar
Lukas Burgey committed
12
from django.db import models
13
from django.db.models.signals import post_save, pre_delete
Lukas Burgey's avatar
Lukas Burgey committed
14
from django.dispatch import receiver
Lukas Burgey's avatar
Lukas Burgey committed
15
from rest_framework.authtoken.models import Token
Lukas Burgey's avatar
Lukas Burgey committed
16
from .auth.v1.models import OIDCConfig
Lukas Burgey's avatar
Lukas Burgey committed
17

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

Lukas Burgey's avatar
Lukas Burgey committed
20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# singleton for simple configs
# https://steelkiwi.com/blog/practical-application-singleton-design-pattern/
class SingletonModel(models.Model):

    class Meta:
        abstract = True

    def set_cache(self):
        cache.set(self.__class__.__name__, self)

    # pylint: disable=invalid-name, arguments-differ
    def save(self, *args, **kwargs):
        self.pk = 1
        super(SingletonModel, self).save(*args, **kwargs)
        self.set_cache()

    @classmethod
    def load(cls):
        if cache.get(cls.__name__) is None:
            obj, created = cls.objects.get_or_create(pk=1)
            if not created:
                obj.set_cache()
        return cache.get(cls.__name__)


Lukas Burgey's avatar
Lukas Burgey committed
46
47
# clients are registerred at rabbitmq, when they are assigned to a site
# (because we only then know what services they provide)
48
class RabbitMQInstance(SingletonModel):
Lukas Burgey's avatar
Lukas Burgey committed
49
    host = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
50
51
52
        max_length=150,
        default='localhost',
    )
Lukas Burgey's avatar
Lukas Burgey committed
53
54
55
56
    vhost = models.CharField(
        max_length=150,
        default='%2f',
    )
Lukas Burgey's avatar
Lukas Burgey committed
57
    exchange = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
58
59
60
        max_length=150,
        default='deployments',
    )
Lukas Burgey's avatar
Lukas Burgey committed
61
    port = models.IntegerField(
Lukas Burgey's avatar
Lukas Burgey committed
62
        default=15672,
Lukas Burgey's avatar
Lukas Burgey committed
63
    )
Lukas Burgey's avatar
Lukas Burgey committed
64
    path = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
65
66
67
        max_length=150,
        default='api',
    )
Lukas Burgey's avatar
Lukas Burgey committed
68
    username = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
69
70
71
        max_length=150,
        default='guest',
    )
Lukas Burgey's avatar
Lukas Burgey committed
72
    password = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
73
74
75
        max_length=150,
        default='guest',
    )
Lukas Burgey's avatar
Lukas Burgey committed
76
77
    is_active = models.BooleanField(
        default=True,
Lukas Burgey's avatar
Lukas Burgey committed
78
    )
Lukas Burgey's avatar
Lukas Burgey committed
79
80
81
82

    def __str__(self):
        return self.host

Lukas Burgey's avatar
Lukas Burgey committed
83
    def _msg(self, msg):
Lukas Burgey's avatar
Lukas Burgey committed
84
        return '[RabbitMQ:{}] {}'.format(self.host, msg)
Lukas Burgey's avatar
Lukas Burgey committed
85
86
87
88

    @property
    def auth(self):
        return HTTPBasicAuth(
Lukas Burgey's avatar
Lukas Burgey committed
89
            self.username,
Lukas Burgey's avatar
Lukas Burgey committed
90
            self.password,
Lukas Burgey's avatar
Lukas Burgey committed
91
        )
Lukas Burgey's avatar
Lukas Burgey committed
92
93

    @property
Lukas Burgey's avatar
Lukas Burgey committed
94
95
96
97
98
    def _connection_parameters(self):
        return pika.ConnectionParameters(
            host=self.host,
            ssl=True,
        )
99
100
101

    @property
    def connection(self):
Lukas Burgey's avatar
Lukas Burgey committed
102
103
104
105
        LOGGER.debug(self._msg('opened connection'))
        return pika.BlockingConnection(
            self._connection_parameters,
        )
106
107
108

    @property
    def channel(self):
Lukas Burgey's avatar
Lukas Burgey committed
109
110
111
112
113
114
115
116
117
118
119
120
121
122
        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
123
124
125
            self.host,
            self.port,
            self.path,
Lukas Burgey's avatar
Lukas Burgey committed
126
            path,
Lukas Burgey's avatar
Lukas Burgey committed
127
128
        )

Lukas Burgey's avatar
Lukas Burgey committed
129
    def _rest_get(self, api_path):
Lukas Burgey's avatar
Lukas Burgey committed
130
        req = requests.get(
Lukas Burgey's avatar
Lukas Burgey committed
131
            self._get_api_uri(api_path),
Lukas Burgey's avatar
Lukas Burgey committed
132
            auth=self.auth)
Lukas Burgey's avatar
Lukas Burgey committed
133
134
        req.raise_for_status()
        return req.json()
Lukas Burgey's avatar
Lukas Burgey committed
135
136
137

    # send a rest call with path and data to the rest interface of
    # the rabbitmq instance
Lukas Burgey's avatar
Lukas Burgey committed
138
    def _rest_put(self, api_path, data):
Lukas Burgey's avatar
Lukas Burgey committed
139
        req = requests.put(
Lukas Burgey's avatar
Lukas Burgey committed
140
            self._get_api_uri(api_path),
Lukas Burgey's avatar
Lukas Burgey committed
141
142
            json=data,
            auth=self.auth)
Lukas Burgey's avatar
Lukas Burgey committed
143
144
        req.raise_for_status()
        return req
Lukas Burgey's avatar
Lukas Burgey committed
145

Lukas Burgey's avatar
Lukas Burgey committed
146
    def _rest_del(self, api_path):
Lukas Burgey's avatar
Lukas Burgey committed
147
        req = requests.delete(
Lukas Burgey's avatar
Lukas Burgey committed
148
            self._get_api_uri(api_path),
Lukas Burgey's avatar
Lukas Burgey committed
149
            auth=self.auth)
Lukas Burgey's avatar
Lukas Burgey committed
150
151
        req.raise_for_status()
        return req
Lukas Burgey's avatar
Lukas Burgey committed
152

Lukas Burgey's avatar
Lukas Burgey committed
153
    def _set_topic_permissions(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
154
155
        username = site.client.username
        path = 'topic-permissions/{}/{}/'.format(
Lukas Burgey's avatar
Lukas Burgey committed
156
157
158
            self.vhost,
            username,
        )
Lukas Burgey's avatar
Lukas Burgey committed
159
160
161
162

        # 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
163
        omit_bar = True
Lukas Burgey's avatar
Lukas Burgey committed
164
165
        for service in site.services.all():
            prefix = '|'
Lukas Burgey's avatar
Lukas Burgey committed
166
            if omit_bar:
Lukas Burgey's avatar
Lukas Burgey committed
167
                prefix = ''
Lukas Burgey's avatar
Lukas Burgey committed
168
                omit_bar = False
Lukas Burgey's avatar
Lukas Burgey committed
169
170
171
172

            services = services + prefix + service.name

        set_topic_permission_data = {
Lukas Burgey's avatar
Lukas Burgey committed
173
174
            'exchange': self.exchange,
            'write': '^$',
Lukas Burgey's avatar
Lukas Burgey committed
175
            'read': r'^service\.({})$'.format(services),
Lukas Burgey's avatar
Lukas Burgey committed
176
        }
Lukas Burgey's avatar
Lukas Burgey committed
177

Lukas Burgey's avatar
Lukas Burgey committed
178
        return self._rest_put(path, set_topic_permission_data)
Lukas Burgey's avatar
Lukas Burgey committed
179
180

    # set permissions for the user
Lukas Burgey's avatar
Lukas Burgey committed
181
    def _set_permissions(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
182
183
        username = site.client.username
        path = 'permissions/{}/{}/'.format(
Lukas Burgey's avatar
Lukas Burgey committed
184
185
186
            self.vhost,
            username,
        )
Lukas Burgey's avatar
Lukas Burgey committed
187
        permission = r'^(amq\.gen.*|{})'.format(self.exchange)
Lukas Burgey's avatar
Lukas Burgey committed
188
        set_permission_data = {
Lukas Burgey's avatar
Lukas Burgey committed
189
190
191
192
            'configure': permission,
            'write': permission,
            'read': permission,
        }
Lukas Burgey's avatar
Lukas Burgey committed
193

Lukas Burgey's avatar
Lukas Burgey committed
194
        return self._rest_put(path, set_permission_data)
Lukas Burgey's avatar
Lukas Burgey committed
195
196

    # create user at the rabbitmq instance
Lukas Burgey's avatar
Lukas Burgey committed
197
    def _create_user(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
198
199
200
201
        username = site.client.username
        path = 'users/{}/'.format(username)

        user_creation_data = {
Lukas Burgey's avatar
Lukas Burgey committed
202
203
204
            'password': str(site.client.auth_token.key),
            'tags': '',
        }
Lukas Burgey's avatar
Lukas Burgey committed
205

Lukas Burgey's avatar
Lukas Burgey committed
206
        return self._rest_put(path, user_creation_data)
Lukas Burgey's avatar
Lukas Burgey committed
207
208

    # delete user at the rabbitmq instance
Lukas Burgey's avatar
Lukas Burgey committed
209
    def _delete_user(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
210
211
212
        username = site.client.username
        path = 'users/{}/'.format(username)

Lukas Burgey's avatar
Lukas Burgey committed
213
214
215
216
217
        return self._rest_del(path)

    def _disconnect(self):
        LOGGER.debug(self._msg('closing connection'))
        self.connection.close()
Lukas Burgey's avatar
Lukas Burgey committed
218
219
220
221

    # PUBLIC API

    def register_site(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
222
223
224
225
        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
226
227

    def update_site(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
228
229
        self._set_topic_permissions(site)
        LOGGER.info(self._msg('updated permissions for {}'.format(site.client)))
Lukas Burgey's avatar
Lukas Burgey committed
230
231

    def deregister_site(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
232
233
        # TODO implement
        LOGGER.info(self._msg('deregistered {}'.format(site.client)))
Lukas Burgey's avatar
Lukas Burgey committed
234
235

    def is_client_connected(self, site):
Lukas Burgey's avatar
Lukas Burgey committed
236
        connections = self._rest_get("connections/")
Lukas Burgey's avatar
Lukas Burgey committed
237
238
239
240
241
242
243
244
245
246
        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)]

247
    def publish_by_service(self, service, msg):
248
        # FIXME publish can fail -> catch error
249
        return self.channel.basic_publish(
Lukas Burgey's avatar
Lukas Burgey committed
250
            exchange=self.exchange,
251
            routing_key=service.routing_key,
Lukas Burgey's avatar
Lukas Burgey committed
252
253
254
255
256
            body=msg,
            properties=pika.BasicProperties(
                delivery_mode=1,
            ),
        )
Lukas Burgey's avatar
Lukas Burgey committed
257
258


Lukas Burgey's avatar
Lukas Burgey committed
259
class User(AbstractUser):
260
    TYPE_CHOICES = (
Lukas Burgey's avatar
Lukas Burgey committed
261
262
263
264
        ('apiclient', 'API-Client'),
        ('oidcuser', 'OIDC User'),
        ('admin', 'Admin'),
    )
265
    user_type = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
266
267
268
269
        max_length=20,
        choices=TYPE_CHOICES,
        default='oidcuser',
    )
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
    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
286
287
288
289
290
291
    # the idp which authenticated the user
    idp = models.ForeignKey(
        OIDCConfig,
        related_name='users',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
292

Lukas Burgey's avatar
Lukas Burgey committed
293
    # we hide deleted keys here
294
    # the full list of ssh keys is self._ssh_keys
Lukas Burgey's avatar
Lukas Burgey committed
295
296
297
298
    @property
    def ssh_keys(self):
        return self._ssh_keys.filter(deleted=False)

299
300
301
302
    @property
    def is_active_at_clients(self):
        return self._is_active

303
304
305
    def __str__(self):
        if self.user_type == 'admin':
            return 'ADMIN {}'.format(self.username)
Lukas Burgey's avatar
Lukas Burgey committed
306
        elif self.user_type == 'oidcuser':
307
308
309
            if not self.is_active:
                return 'DEACTIVATED USER {}'.format(self.username)
            return 'USER {}'.format(self.username)
Lukas Burgey's avatar
Lukas Burgey committed
310
        elif self.user_type == 'apiclient':
311
            return 'APICLIENT {}@{}'.format(self.username, self.site)
Lukas Burgey's avatar
Lukas Burgey committed
312
313
        else:
            raise Exception()
Lukas Burgey's avatar
Lukas Burgey committed
314

Lukas Burgey's avatar
Lukas Burgey committed
315
    def _msg(self, msg):
316
317
318
319
320
321
        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
322
            LOGGER.info(self._msg('Deleting'))
323
324
325
326
327
328
329

            # 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):
330
        if self._is_active:
Lukas Burgey's avatar
Lukas Burgey committed
331
            LOGGER.error(self._msg('already activated'))
332
333
334
335
            return

        if self.user_type == 'oidcuser':
            self.is_active = True
336
            self._is_active = True
337
338
339
340
341
            self.save()

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

Lukas Burgey's avatar
Lukas Burgey committed
342
            LOGGER.info(self._msg('activated'))
343
344
345

    # oidcuser: withdraw all credentials
    def deactivate(self):
346
        if not self._is_active:
Lukas Burgey's avatar
Lukas Burgey committed
347
            LOGGER.error(self._msg('already deactivated'))
348
349
350
351
            return

        if self.user_type == 'oidcuser':
            self.is_active = False
352
            self._is_active = False
353
354
355
356
357
            self.save()

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

Lukas Burgey's avatar
Lukas Burgey committed
358
            LOGGER.info(self._msg('deactivated'))
359
360


Lukas Burgey's avatar
Lukas Burgey committed
361
362
def construct_user(user_info):
    return User(
Lukas Burgey's avatar
Lukas Burgey committed
363
364
365
366
367
368
369
        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
370
371
372


class Site(models.Model):
373
    client = models.OneToOneField(
Lukas Burgey's avatar
Lukas Burgey committed
374
375
376
        User,
        related_name='site',
    )
Lukas Burgey's avatar
Lukas Burgey committed
377
378
379
380
381
382
    name = models.CharField(max_length=150, unique=True)
    description = models.TextField(max_length=300, blank=True)

    def __str__(self):
        return self.name

383
384
385
386
387
388
    # 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
389

Lukas Burgey's avatar
Lukas Burgey committed
390
391
392
393

class Service(models.Model):
    name = models.CharField(max_length=150, unique=True)
    description = models.TextField(max_length=300, blank=True)
394
    site = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
395
396
        Site,
        related_name='services')
Lukas Burgey's avatar
Lukas Burgey committed
397
    groups = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
398
399
400
        Group,
        related_name='services',
        blank=True)
Lukas Burgey's avatar
Lukas Burgey committed
401

402
403
404
405
    @property
    def routing_key(self):
        return 'service.{}'.format(self.name)

Lukas Burgey's avatar
Lukas Burgey committed
406
    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
407
        return self.name
Lukas Burgey's avatar
Lukas Burgey committed
408
409
410


class SSHPublicKey(models.Model):
Lukas Burgey's avatar
Lukas Burgey committed
411
412
413
414
415
416
417
    name = models.CharField(
        max_length=150,
        unique=True,
    )
    key = models.TextField(
        max_length=1000
    )
Lukas Burgey's avatar
Lukas Burgey committed
418
    # hidden field at the user
Lukas Burgey's avatar
Lukas Burgey committed
419
    user = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
420
421
422
        User,
        related_name='_ssh_keys',
    )
Lukas Burgey's avatar
Lukas Burgey committed
423

Lukas Burgey's avatar
Lukas Burgey committed
424
425
    # has the user triggered the deletion of this key
    deleted = models.BooleanField(
Lukas Burgey's avatar
Lukas Burgey committed
426
427
428
        default=False,
        editable=False,
    )
429

Lukas Burgey's avatar
Lukas Burgey committed
430
    def _msg(self, msg):
Lukas Burgey's avatar
Lukas Burgey committed
431
        return '[SSHPublicKey:{}] {}'.format(self, msg)
432

433
434
435
    # does not directly delete the key if the key is deployed or withdrawn
    # somewhere
    # the receiver 'delete_withdrawn_ssh_key' does the actual deletion
436
    def delete_key(self):
Lukas Burgey's avatar
Lukas Burgey committed
437
        if (not self.tasks.exists() and not self.deployments.exists()):
Lukas Burgey's avatar
Lukas Burgey committed
438
            LOGGER.info(self._msg('Direct deletion of key'))
439
440
441
            self.delete()
            return

Lukas Burgey's avatar
Lukas Burgey committed
442
        LOGGER.info(self._msg('Deletion of key started'))
443
444
445
        self.deleted = True
        self.save()

Lukas Burgey's avatar
Lukas Burgey committed
446
        # delete implies withdrawing the key from all clients
447
448
449
        for deployment in self.deployments.all():
            deployment.withdraw_key(self)

Lukas Burgey's avatar
Lukas Burgey committed
450
451
    # 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
452
        if (self.deleted and not self.tasks.exists()):
Lukas Burgey's avatar
Lukas Burgey committed
453
            LOGGER.info(self._msg(
Lukas Burgey's avatar
Lukas Burgey committed
454
                'All clients have withdrawn this key. Final deletion'))
Lukas Burgey's avatar
Lukas Burgey committed
455
456
457
            self.delete()
            return

Lukas Burgey's avatar
Lukas Burgey committed
458
    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
459
460
        if self.deleted:
            return "DELETED: {}".format(self.name)
Lukas Burgey's avatar
Lukas Burgey committed
461
462
463
        return self.name


464
# Deployment describes the credential state per user as it is supposed to be
465
466
467
468
#
# (exception: if is_active=False the ssh_keys contain the keys to be deployed
# if the deployment is reactivated)
#
469
470
# 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
471
472
class Deployment(models.Model):
    user = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
473
474
475
476
        User,
        related_name='deployments',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
477
    service = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
478
479
480
481
        Service,
        related_name='deployments',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
482
    ssh_keys = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
483
484
485
486
        SSHPublicKey,
        related_name='deployments',
        blank=True,
    )
487
    ssh_keys_to_withdraw = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
488
489
490
491
        SSHPublicKey,
        related_name='withdrawn_deployments',
        blank=True,
    )
492
    is_active = models.BooleanField(
Lukas Burgey's avatar
Lukas Burgey committed
493
494
        default=True,
    )
495

496
497
498
    @property
    def withdrawals(self):
        return self.tasks.filter(action='withdraw')
Lukas Burgey's avatar
Lukas Burgey committed
499

500
501
502
    @property
    def deploys(self):
        return self.tasks.filter(action='deploy')
Lukas Burgey's avatar
Lukas Burgey committed
503

504
505
    def __str__(self):
        return '{}:{}'.format(self.service, self.user)
506

Lukas Burgey's avatar
Lukas Burgey committed
507
    def _msg(self, msg):
508
        return '[Deployment:{}] {}'.format(self, msg)
509

510
511
512
    # deploy credentials which were deployed prior to deactivation
    def activate(self):
        if self.is_active:
Lukas Burgey's avatar
Lukas Burgey committed
513
            LOGGER.error(self._msg('already active'))
514
515
            return

Lukas Burgey's avatar
Lukas Burgey committed
516
        LOGGER.debug(self._msg(str(self.ssh_keys.all())))
517
518
519
520
521
        for key in self.ssh_keys.all():
            self._deploy_key(key)

        self.is_active = True
        self.save()
Lukas Burgey's avatar
Lukas Burgey committed
522
        LOGGER.info(self._msg('activated'))
523
524
525
526

    # withdraw all credentials
    def deactivate(self):
        if not self.is_active:
Lukas Burgey's avatar
Lukas Burgey committed
527
            LOGGER.error(self._msg('already deactivated'))
528
529
530
            return

        self.is_active = False
531
        self.save()
532

533
534
535
        for key in self.ssh_keys.all():
            self._withdraw_key(key)

Lukas Burgey's avatar
Lukas Burgey committed
536
        LOGGER.info(self._msg('deactivated'))
537
538
539
540

    # only deploy the key
    def _deploy_key(self, key):
        # delete outstanding tasks which are made obsolete by this task
541
        for withdrawal in self.withdrawals.filter(key=key):
Lukas Burgey's avatar
Lukas Burgey committed
542
            LOGGER.debug(withdrawal._msg('now obsolete'))
Lukas Burgey's avatar
Lukas Burgey committed
543
            withdrawal.delete()
544
545
546

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

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

        # publish the task
        task.publish()
565

566
567
    def _withdraw_key(self, key):
        # delete outstanding tasks which are made obsolete by this task
568
        for deploy in self.deploys.filter(key=key):
Lukas Burgey's avatar
Lukas Burgey committed
569
            LOGGER.debug(deploy._msg("now obsolete"))
Lukas Burgey's avatar
Lukas Burgey committed
570
            deploy.delete()
Lukas Burgey's avatar
Lukas Burgey committed
571

572
573
        # generate task
        task = DeploymentTask(
Lukas Burgey's avatar
Lukas Burgey committed
574
575
576
577
            action='withdraw',
            deployment=self,
            key=key,
        )
578
        task.save()
Lukas Burgey's avatar
Lukas Burgey committed
579
        LOGGER.debug(task._msg('generated'))
Lukas Burgey's avatar
Lukas Burgey committed
580

581
582
583
        # generate task items
        for site in self.service.site.all():
            withdrawal = DeploymentTaskItem(
Lukas Burgey's avatar
Lukas Burgey committed
584
585
586
                task=task,
                site=site,
            )
587
            withdrawal.save()
Lukas Burgey's avatar
Lukas Burgey committed
588
            LOGGER.debug(withdrawal._msg('generated'))
589

590
591
        # publish the task
        task.publish()
Lukas Burgey's avatar
Lukas Burgey committed
592

593
594
595
    # 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
596
            LOGGER.error(self._msg('cannot deploy while deactivated'))
597
598
599
600
601
602
603
604
605
606
607
608
609
            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
610
            LOGGER.error(self._msg('cannot withdraw while deactivated'))
611
612
613
614
615
616
617
618
619
620
            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
621

622
623
class DeploymentTask(models.Model):
    ACTION_CHOICES = (
Lukas Burgey's avatar
Lukas Burgey committed
624
625
626
        ('deploy', 'deploy'),
        ('withdraw', 'withdraw'),
    )
627
    action = models.CharField(
Lukas Burgey's avatar
Lukas Burgey committed
628
629
630
        max_length=10,
        choices=ACTION_CHOICES,
    )
631
    key = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
632
633
634
635
        SSHPublicKey,
        related_name='tasks',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
636
    deployment = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
637
638
639
640
        Deployment,
        related_name='tasks',
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
641
642
643
644
645
646
647
648
649

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

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

650
    def __str__(self):
651
        return "{}:{}:{} - {}".format(
Lukas Burgey's avatar
Lukas Burgey committed
652
653
654
655
656
            self.deployment.service,
            self.deployment.user,
            self.key,
            self.action,
        )
657

Lukas Burgey's avatar
Lukas Burgey committed
658
    def _msg(self, msg):
Lukas Burgey's avatar
Lukas Burgey committed
659
        return '[DeploymentTask:{}] {}'.format(self, msg)
660
661

    def publish(self):
662
        # FIXME mitigating circular dependencies here
663
664
665
        from .clientapi.serializers import DeploymentTaskSerializer
        msg = json.dumps(DeploymentTaskSerializer(self).data)

Lukas Burgey's avatar
Lukas Burgey committed
666
        # FIXME select the rabbitmq instance more meaningful
667
        RabbitMQInstance.load().publish_by_service(
Lukas Burgey's avatar
Lukas Burgey committed
668
669
670
            self.service,
            msg,
        )
671
672
673

    # 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
674
        item = self.task_items.get(site=site)
Lukas Burgey's avatar
Lukas Burgey committed
675
        LOGGER.debug(item._msg('done'))
Lukas Burgey's avatar
Lukas Burgey committed
676
        item.delete()
677
678
679
680
681
682

        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
683
        LOGGER.info(self._msg('done'))
Lukas Burgey's avatar
Lukas Burgey committed
684
        self.delete()
685
686
687
688
689
690
691
692

        # 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
693
694
695
696
        DeploymentTask,
        related_name='task_items',
        on_delete=models.CASCADE,
    )
697
    site = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
698
699
700
701
        Site,
        related_name='task_items',
        on_delete=models.CASCADE,
    )
702

Lukas Burgey's avatar
Lukas Burgey committed
703
    def __str__(self):
704
        return "{}@{}".format(
Lukas Burgey's avatar
Lukas Burgey committed
705
706
707
            self.task,
            self.site,
        )
708

Lukas Burgey's avatar
Lukas Burgey committed
709
    def _msg(self, msg):
Lukas Burgey's avatar
Lukas Burgey committed
710
        return '[DeploymentTaskItem:{}] {}'.format(self, msg)
711

Lukas Burgey's avatar
Lukas Burgey committed
712

713
714
715
#
# RECEIVERS
#
Lukas Burgey's avatar
Lukas Burgey committed
716
717
718
719
720
721
722
723

@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
724
def register_at_rabbitmq(sender, instance=None, created=False, **kwargs):
Lukas Burgey's avatar
Lukas Burgey committed
725
726
727
728
729
730
731
    if not created:
        return

    RabbitMQInstance().register_site(instance)


@receiver(pre_delete, sender=Site)
Lukas Burgey's avatar
Lukas Burgey committed
732
def deregister_at_rabbitmq(sender, instance=None, **kwargs):
Lukas Burgey's avatar
Lukas Burgey committed
733
    RabbitMQInstance().deregister_site(instance)
734
735
736
737
738
739
740


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

741
    if not instance.is_active and instance.is_active_at_clients:
742
743
744
745
746
747
748
749
        instance.deactivate()


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

750
    if instance.is_active and not instance.is_active_at_clients:
751
        instance.activate()