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

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

    def __str__(self):
        return self.host

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

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

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

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

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

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

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

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

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

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

            services = services + prefix + service.name

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

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

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

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

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

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

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

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

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

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

    # PUBLIC API

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


360
361
362
363
364
365
366
367
368
369
370
    @classmethod
    def construct_from_user_info(cls, user_info, idp):
        LOGGER.debug('User: constructing from %s', user_info)
        return cls(
            sub=user_info.get('sub', ''),
            first_name=user_info.get('given_name', ''),
            last_name=user_info.get('family_name', ''),
            email=user_info.get('email', ''),
            username=user_info.get('email', ''),
            idp=idp,
        )
Lukas Burgey's avatar
Lukas Burgey committed
371
372
373


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

    def __str__(self):
        return self.name

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

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # publish the task
        task.publish()
566

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Lukas Burgey's avatar
Lukas Burgey committed
713

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

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

    RabbitMQInstance().register_site(instance)


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


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

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


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

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