models.py 20.8 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
Lukas Burgey's avatar
Lukas Burgey committed
6

7
from django.contrib.auth.models import Group
Lukas Burgey's avatar
Lukas Burgey committed
8
from django.db import models
Lukas Burgey's avatar
Lukas Burgey committed
9
from django.db.models.signals import post_save
Lukas Burgey's avatar
Lukas Burgey committed
10
from django.dispatch import receiver
11
from django_mysql.models import JSONField
Lukas Burgey's avatar
Lukas Burgey committed
12

13
from .broker_models import RabbitMQInstance
14
from .user_models import User
Lukas Burgey's avatar
Lukas Burgey committed
15

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

18

19
20
21
22
23
24
STATE_CHOICES = (
    ('deployment_pending', 'Deployment Pending'),
    ('removal_pending', 'Removal Pending'),
    ('deployed', 'Deployed'),
    ('not_deployed', 'Not Deployed'),
    ('questionnaire', 'Questionnaire'),
25
26
    ('failed', 'Failed'),
    ('rejected', 'Rejected'),
27
)
Lukas Burgey's avatar
Lukas Burgey committed
28
29
30
31
32
def questionnaire_default():
    return {}
def credential_default():
    return {}

Lukas Burgey's avatar
Lukas Burgey committed
33
34

class Site(models.Model):
35
    client = models.OneToOneField(
Lukas Burgey's avatar
Lukas Burgey committed
36
37
        User,
        related_name='site',
38
39
40
41
42
43
44
45
46
47
48
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    name = models.CharField(
        max_length=150,
        unique=True,
    )
    description = models.TextField(
        max_length=300,
        blank=True,
Lukas Burgey's avatar
Lukas Burgey committed
49
    )
Lukas Burgey's avatar
Lukas Burgey committed
50

Lukas Burgey's avatar
Lukas Burgey committed
51
52
    @property
    def pending_tasks(self):
Lukas Burgey's avatar
Lukas Burgey committed
53
54
55
56
57
58
59
60
61
62
63
64
        tasks = {}
        pending_items = self.state_items.filter(
            state='deployment_pending',
        ) | self.state_items.filter(
            state='removal_pending',
        ) | self.state_items.filter(
            state='failed',
        )
        for item in pending_items.all():
            tasks[item.parent.id] = item.parent

        return tasks.values()
Lukas Burgey's avatar
Lukas Burgey committed
65

Lukas Burgey's avatar
Lukas Burgey committed
66
67
68
69
70
    def __str__(self):
        return self.name


class Service(models.Model):
71
72
73
74
75
76
77
78
    name = models.CharField(
        max_length=150,
        unique=True,
    )
    description = models.TextField(
        max_length=300,
        blank=True,
    )
79
    site = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
80
        Site,
81
82
        related_name='services',
    )
Lukas Burgey's avatar
Lukas Burgey committed
83
    groups = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
84
85
        Group,
        related_name='services',
86
87
        blank=True,
    )
Lukas Burgey's avatar
Lukas Burgey committed
88
89

    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
90
        return self.name
Lukas Burgey's avatar
Lukas Burgey committed
91
92


93
94
95
96
97
98
99
    def handle_group_deployments(self):
        for group in self.groups.all():

            # users that have group deployments for this group
            for user in User.objects.filter(
                    deployments__group=group,
            ).distinct():
Lukas Burgey's avatar
Lukas Burgey committed
100
                LOGGER.debug(user.msg('New service for group. Adding to deployment'))
101
102
103

                # all group deployments have the same keys
                # TODO check that assumption
Lukas Burgey's avatar
Lukas Burgey committed
104
105
106
107
                try:
                    deployment = user.deployments.get(group=group)
                    deployment.service_added(self)
                except Deployment.DoesNotExist:
108
                    LOGGER.error('Inconsistency of group deployment')
Lukas Burgey's avatar
Lukas Burgey committed
109
                    raise
110
111


Lukas Burgey's avatar
Lukas Burgey committed
112
class SSHPublicKey(models.Model):
Lukas Burgey's avatar
Lukas Burgey committed
113
114
115
116
117
118
    name = models.CharField(
        max_length=150,
    )
    key = models.TextField(
        max_length=1000
    )
Lukas Burgey's avatar
Lukas Burgey committed
119
    # hidden field at the user
120
    # TODO checks: if the user is null
Lukas Burgey's avatar
Lukas Burgey committed
121
    user = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
122
123
        User,
        related_name='_ssh_keys',
124
125
        on_delete=models.SET_NULL,
        null=True,
Lukas Burgey's avatar
Lukas Burgey committed
126
    )
Lukas Burgey's avatar
Lukas Burgey committed
127

128
    # has the user triggered the deletion of this key?
Lukas Burgey's avatar
Lukas Burgey committed
129
    deleted = models.BooleanField(
Lukas Burgey's avatar
Lukas Burgey committed
130
131
132
        default=False,
        editable=False,
    )
133

Lukas Burgey's avatar
Lukas Burgey committed
134
135
    @property
    def deployed_anywhere(self):
136
        for state in self.states.all():
Lukas Burgey's avatar
Lukas Burgey committed
137
138
139
140
            for item in state.state_items.all():
                if item.state == 'deployed' or item.state == 'removal_pending':
                    return True
        return False
141

Lukas Burgey's avatar
Lukas Burgey committed
142
    # does not directly delete the key if the key is deployed or removen
143
    # somewhere
Lukas Burgey's avatar
Lukas Burgey committed
144
    # the receiver 'delete_removen_ssh_key' does the actual deletion
145
    def delete_key(self):
146
        # if this key is not deployed anywhere we delete it now
Lukas Burgey's avatar
Lukas Burgey committed
147
        if not self.deployed_anywhere:
Lukas Burgey's avatar
Lukas Burgey committed
148
            LOGGER.info(self.msg('Direct deletion of key'))
149
150
151
            self.delete()
            return

Lukas Burgey's avatar
Lukas Burgey committed
152
        LOGGER.info(self.msg('Deletion of key started'))
153
154
155
        self.deleted = True
        self.save()

Lukas Burgey's avatar
Lukas Burgey committed
156
        # delete implies removeing the key from all clients
157
        for deployment in self.deployments.all():
158
            deployment.remove_key(self)
159

Lukas Burgey's avatar
Lukas Burgey committed
160
    # when a key is removen by a client we try to finally delete it
Lukas Burgey's avatar
Lukas Burgey committed
161
    def try_final_deletion(self):
Lukas Burgey's avatar
Lukas Burgey committed
162
163
164
        if self.deleted:
            if not self.deployed_anywhere:

Lukas Burgey's avatar
Lukas Burgey committed
165
                LOGGER.info(self.msg('All clients have removen this key. Final deletion'))
Lukas Burgey's avatar
Lukas Burgey committed
166
167
168
169
170
171
172
173
174
                self._final_deletion()

    def _final_deletion(self):
        _self = self
        for state in self.states.all():
            #for item in state.state_items.all():
            #    item.delete()
            state.delete()
        _self.delete()
Lukas Burgey's avatar
Lukas Burgey committed
175

Lukas Burgey's avatar
Lukas Burgey committed
176
    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
177
        if self.deleted:
Lukas Burgey's avatar
Lukas Burgey committed
178
            return 'DELETED: {}'.format(self.name)
Lukas Burgey's avatar
Lukas Burgey committed
179
180
        return self.name

181
182
183
    def msg(self, msg):
        return '[SSHKey:{}] {}'.format(self, msg)

Lukas Burgey's avatar
Lukas Burgey committed
184

Lukas Burgey's avatar
Lukas Burgey committed
185
186
187
# Deployment describes the supposed state of the users ssh keys at either:
#   - a group (and and the services associated with the group)
#   - a single service
188
#
Lukas Burgey's avatar
Lukas Burgey committed
189
190
191
192
193
194
195
# DeploymentState track the state of a single ssh key at either:
#   - a group (and and the services associated with the group)
#   - a single service

# DeploymentStateItem track the acknowledgements from the clients for either :
#   - the sites that handle the associated group (i.e provide a service for members of the group)
#   - the sites that provide the associated service
196
#
Lukas Burgey's avatar
Lukas Burgey committed
197
198
199
# Note: two possible kinds of Deployment:
#   (group is None and service is not None) or
#   (group is not None and service is None)
Lukas Burgey's avatar
Lukas Burgey committed
200
201
class Deployment(models.Model):
    user = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
202
203
        User,
        related_name='deployments',
Lukas Burgey's avatar
Lukas Burgey committed
204
205
        on_delete=models.SET_NULL,
        null=True,
Lukas Burgey's avatar
Lukas Burgey committed
206
    )
207
    group = models.ForeignKey(
208
209
        Group,
        related_name='deployments',
210
211
212
213
        on_delete=models.CASCADE,
        null=True,
        blank=True,
    )
Lukas Burgey's avatar
Lukas Burgey committed
214
    service = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
215
216
217
        Service,
        related_name='deployments',
        on_delete=models.CASCADE,
218
219
        null=True,
        blank=True,
Lukas Burgey's avatar
Lukas Burgey committed
220
    )
Lukas Burgey's avatar
Lukas Burgey committed
221
    ssh_keys = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
222
223
224
225
        SSHPublicKey,
        related_name='deployments',
        blank=True,
    )
226
    is_active = models.BooleanField(
Lukas Burgey's avatar
Lukas Burgey committed
227
228
        default=True,
    )
229

Lukas Burgey's avatar
Lukas Burgey committed
230
231
232
233
234
235
236
237
238
239
240
241
    # only used when group is not None and service is None
    @property
    def services(self):
        if self.group is not None:
            return self.group.services.all()
        return None

    # only used when group is not None and service is None
    @property
    def sites(self):
        return Site.objects.filter(services__groups=self.group).distinct()

Lukas Burgey's avatar
Lukas Burgey committed
242
243
244
    # get a deployment for a user/service.
    # if it does not exist it is created
    @classmethod
245
    def get_deployment(cls, user, service=None, group=None):
Lukas Burgey's avatar
Lukas Burgey committed
246
247
248
249
250
251
        try:
            if service is not None:
                return cls.objects.get(
                    user=user,
                    service=service,
                )
Lukas Burgey's avatar
Lukas Burgey committed
252

Lukas Burgey's avatar
Lukas Burgey committed
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
            elif group is not None:
                return cls.objects.get(
                    user=user,
                    group=group,
                )
            else:
                raise ValueError('Unable to create Deployment without service and group')

        except cls.DoesNotExist:
            deployment = None
            if service is not None:
                deployment = cls(
                    user=user,
                    service=service,
                )
            elif group is not None:
                deployment = cls(
                    user=user,
                    group=group,
                )
                if not group.services.exists():
                    LOGGER.info(deployment.msg('No services for group'))
275

Lukas Burgey's avatar
Lukas Burgey committed
276
277
278
            deployment.save()
            LOGGER.debug(deployment.msg('created'))
            return deployment
Lukas Burgey's avatar
Lukas Burgey committed
279

280
281
282
    # deploy credentials which were deployed prior to deactivation
    def activate(self):
        if self.is_active:
Lukas Burgey's avatar
Lukas Burgey committed
283
            LOGGER.error(self.msg('already active'))
284
285
286
287
288
289
290
            return

        for key in self.ssh_keys.all():
            self._deploy_key(key)

        self.is_active = True
        self.save()
Lukas Burgey's avatar
Lukas Burgey committed
291
        LOGGER.info(self.msg('activated'))
292

Lukas Burgey's avatar
Lukas Burgey committed
293
    # remove all credentials
294
295
    def deactivate(self):
        if not self.is_active:
Lukas Burgey's avatar
Lukas Burgey committed
296
            LOGGER.error(self.msg('already deactivated'))
297
298
299
            return

        self.is_active = False
300
        self.save()
301

302
        for key in self.ssh_keys.all():
303
            self._remove_key(key)
304

Lukas Burgey's avatar
Lukas Burgey committed
305
        LOGGER.info(self.msg('deactivated'))
306
307
308
309

    # 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
310
            LOGGER.error(self.msg('cannot deploy while deactivated'))
311
312
313
314
315
316
317
            raise Exception('deployment deactivated')

        self.ssh_keys.add(key)
        self.save()

        self._deploy_key(key)

318
319
    def service_added(self, service):
        # a new service for this group was added and we may have to deploy some keys
Lukas Burgey's avatar
Lukas Burgey committed
320
        LOGGER.debug(self.msg('Adding service {}'.format(service)))
321
322
323
324

        for state in self.states.all():
            state.service_added(service)

Lukas Burgey's avatar
Lukas Burgey committed
325
    # remove key and track changes in the key lists
326
    def remove_key(self, key):
327
        if not self.is_active:
Lukas Burgey's avatar
Lukas Burgey committed
328
            LOGGER.error(self.msg('cannot remove while deactivated'))
329
330
331
332
333
            raise Exception('deployment deactivated')

        self.ssh_keys.remove(key)
        self.save()

334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
        self._remove_key(key)

    # only deploy the key
    def _deploy_key(self, key):
        state = DeploymentState.get_state(
            deployment=self,
            key=key,
        )
        state.save()
        state.deploy()

    def _remove_key(self, key):
        state = DeploymentState.get_state(
            deployment=self,
            key=key,
        )
        state.save()
        state.remove()
352

353
    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
354
355
356
        if self.service is not None:
            return '{}:{}'.format(self.service, self.user)
        return '{}:{}'.format(self.group, self.user)
Lukas Burgey's avatar
Lukas Burgey committed
357

358
    def msg(self, msg):
Lukas Burgey's avatar
Lukas Burgey committed
359
        return '[Depl.m:{}] {}'.format(self, msg)
360
361


Lukas Burgey's avatar
Lukas Burgey committed
362
# DeploymentState: knows:
363
# user, service, key, state_target
Lukas Burgey's avatar
Lukas Burgey committed
364
class DeploymentState(models.Model):
365
    key = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
366
        SSHPublicKey,
Lukas Burgey's avatar
Lukas Burgey committed
367
        related_name='states',
368
369
        # deleting the key leaves us without references about its deployments
        # we _HAVE_ to remove all deployments prior to deleting key
Lukas Burgey's avatar
Lukas Burgey committed
370
371
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
372
    deployment = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
373
        Deployment,
Lukas Burgey's avatar
Lukas Burgey committed
374
        related_name='states',
Lukas Burgey's avatar
Lukas Burgey committed
375
376
        on_delete=models.CASCADE,
    )
Lukas Burgey's avatar
Lukas Burgey committed
377

378
379
380
381
382
383
    # which state do we currently want to reach?
    state_target = models.CharField(
        max_length=50,
        choices=STATE_CHOICES,
        default='deployed',
    )
Lukas Burgey's avatar
Lukas Burgey committed
384

Lukas Burgey's avatar
Lukas Burgey committed
385
386
387
388
    @property
    def user(self):
        return self.deployment.user

389
390
391
    @property
    def states(self):
        return [item.state for item in self.state_items.all()]
392

393
394
    @property
    def state(self):
Lukas Burgey's avatar
Lukas Burgey committed
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
        if self.states:
            _state = ''
            for state in self.states:
                if _state == '':
                    _state = state
                elif _state != state:
                    return 'mixed'

            return _state

        # if we have no states we have nothing to do
        return self.state_target

    @property
    def target_reached(self):
        return self.state_target == self.state
411

412
413
414
    @property
    def service(self):
        return self.deployment.service
Lukas Burgey's avatar
Lukas Burgey committed
415

Lukas Burgey's avatar
Lukas Burgey committed
416
417
418
419
    @property
    def services(self):
        return self.deployment.services

420
421
422
423
424
    @property
    def group(self):
        return self.deployment.group

    # get a state for a user/service.
425
    # if it does not exist it is created
Lukas Burgey's avatar
Lukas Burgey committed
426
    @classmethod
427
428
    def get_state(cls, deployment, key):
        # check if a state does already exist
Lukas Burgey's avatar
Lukas Burgey committed
429
        state = None
Lukas Burgey's avatar
Lukas Burgey committed
430
431
432
433
434
435
436
437
        try:
            state = cls.objects.get(
                deployment=deployment,
                key=key,
            )
            return state

        except cls.DoesNotExist:
Lukas Burgey's avatar
Lukas Burgey committed
438
439
440
441
442
443
            state = cls(
                deployment=deployment,
                key=key,
            )
            state.save()
            LOGGER.debug(state.msg('created'))
444

Lukas Burgey's avatar
Lukas Burgey committed
445
446
447
448
449
450
451
        except cls.MultipleObjectsReturned:
            LOGGER.error(
                deployment.msg('to many DeploymentState objects for key {}'.format(key.name))
            )
            raise


452
        # generate state items
453
454
        if deployment.service is not None:
            for site in deployment.service.site.all():
Lukas Burgey's avatar
Lukas Burgey committed
455
                DeploymentStateItem.get_state_item(
456
457
                    parent=state,
                    site=site,
Lukas Burgey's avatar
Lukas Burgey committed
458
459
                    service=deployment.service,
                ).save()
460
        elif deployment.group is not None:
Lukas Burgey's avatar
Lukas Burgey committed
461
462
            # every site which provides a service for group
            for site in deployment.sites:
463
464
465
466
                for service in Service.objects.filter(
                        groups=deployment.group,
                        site=site,
                ):
Lukas Burgey's avatar
Lukas Burgey committed
467
                    DeploymentStateItem.get_state_item(
468
469
470
                        parent=state,
                        site=site,
                        service=service,
Lukas Burgey's avatar
Lukas Burgey committed
471
                    ).save()
472

473
        return state
Lukas Burgey's avatar
Lukas Burgey committed
474

475
    def service_added(self, service):
Lukas Burgey's avatar
Lukas Burgey committed
476
        LOGGER.debug(self.msg('Adding service {}'.format(service)))
477
478
479
480
481
482
483
484
485
486
487
        for site in service.site.all():
            # create new DeploymentStateItems
            item = DeploymentStateItem.get_state_item(
                parent=self,
                site=site,
                service=service,
            )
            item.save()
            if self.state_target == 'deployed':
                item.user_deploy()

488
489
490
491
492
    def deploy(self):
        self._set_target('deployed')
        for item in self.state_items.all():
            item.user_deploy()
        self.publish_to_client()
Lukas Burgey's avatar
Lukas Burgey committed
493
        # each state item publishes its state to the user
494

495
496
497
498
499
    def remove(self):
        self._set_target('not_deployed')
        for item in self.state_items.all():
            item.user_remove()
        self.publish_to_client()
Lukas Burgey's avatar
Lukas Burgey committed
500
        # each state item publishes its state to the user
501

502
    def publish_to_client(self):
Lukas Burgey's avatar
Lukas Burgey committed
503
504
505
506
        # mitigating circular dependencies here
        from .clientapi.serializers import DeploymentStateSerializer
        msg = json.dumps(DeploymentStateSerializer(self).data)

Lukas Burgey's avatar
Lukas Burgey committed
507
        if self.service is not None:
Lukas Burgey's avatar
Lukas Burgey committed
508
509
510
511
512
513
514
515
516
517
            RabbitMQInstance.load().publish_by_service(
                self.service,
                msg,
            )
        elif self.group is not None:
            RabbitMQInstance.load().publish_by_group(
                self.group,
                msg,
            )
        else:
Lukas Burgey's avatar
Lukas Burgey committed
518
            LOGGER.error('Deployment as neither a group or a service')
519

520
    # update the state of the remote webpage
521
    def publish_to_user(self):
Lukas Burgey's avatar
Lukas Burgey committed
522
523
        if self.user is not None:
            from .frontend.views import user_state
Lukas Burgey's avatar
Lukas Burgey committed
524
            msg = json.dumps({
Lukas Burgey's avatar
Lukas Burgey committed
525
                'user_state': user_state(self.user),
Lukas Burgey's avatar
Lukas Burgey committed
526
            })
Lukas Burgey's avatar
Lukas Burgey committed
527
528
            RabbitMQInstance.load().publish_to_user(
                self.user,
Lukas Burgey's avatar
Lukas Burgey committed
529
                msg,
Lukas Burgey's avatar
Lukas Burgey committed
530
            )
531

532
533
    def msg(self, msg):
        return '[DState:{}] {}'.format(self, msg)
534

535
536
537
538
    def _set_target(self, target):
        self.state_target = target
        LOGGER.debug(self.msg('target: '+target))
        self.save()
539

540
    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
541
        if self.service is not None:
Lukas Burgey's avatar
Lukas Burgey committed
542
            return '{}:{}#{}'.format(
Lukas Burgey's avatar
Lukas Burgey committed
543
                self.service,
544
                self.key.name,
Lukas Burgey's avatar
Lukas Burgey committed
545
546
                self.id,
            )
Lukas Burgey's avatar
Lukas Burgey committed
547
        return '{}:{}#{}'.format(
Lukas Burgey's avatar
Lukas Burgey committed
548
            self.group,
549
            self.key.name,
550
            self.id,
551
552
        )

553

Lukas Burgey's avatar
Lukas Burgey committed
554
# DeploymentStateItem: knows:
555
# user, service, key, state_target, _and_ site
Lukas Burgey's avatar
Lukas Burgey committed
556
class DeploymentStateItem(models.Model):
557
    parent = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
558
559
        DeploymentState,
        related_name='state_items',
Lukas Burgey's avatar
Lukas Burgey committed
560
561
        on_delete=models.CASCADE,
    )
562
    site = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
563
        Site,
Lukas Burgey's avatar
Lukas Burgey committed
564
        related_name='state_items',
Lukas Burgey's avatar
Lukas Burgey committed
565
566
        on_delete=models.CASCADE,
    )
567
568
569
570
571
    service = models.ForeignKey(
        Service,
        related_name='state_items',
        on_delete=models.CASCADE,
    )
572
    state = models.CharField(
573
        max_length=50,
574
        choices=STATE_CHOICES,
575
        default='deployment_pending',
576
    )
Lukas Burgey's avatar
Lukas Burgey committed
577

578
579
580
    # message for the user
    message = models.TextField(
        max_length=300,
581
        default='',
582
583
    )

584
    # questions for the user (needed for deployment
585
586
    questionnaire = JSONField(
        default=questionnaire_default,
587
588
        null=True,
        blank=True,
589
    )
590
591
    # credentials for the service
    # only valid when state == deployed
592
593
    credentials = JSONField(
        default=credential_default,
594
595
        null=True,
        blank=True,
596
597
    )

Lukas Burgey's avatar
Lukas Burgey committed
598
599
600
601
    @property
    def user(self):
        return self.parent.user

Lukas Burgey's avatar
Lukas Burgey committed
602
603
604
605
    @property
    def key(self):
        return self.parent.key

Lukas Burgey's avatar
Lukas Burgey committed
606
    @property
Lukas Burgey's avatar
Lukas Burgey committed
607
608
609
    def group(self):
        return self.parent.group

Lukas Burgey's avatar
Lukas Burgey committed
610
611
    @classmethod
    def get_state_item(cls, parent=None, site=None, service=None):
Lukas Burgey's avatar
Lukas Burgey committed
612
613
614
615
616
617
        try:
            return parent.state_items.get(
                site=site,
                service=service,
            )
        except cls.DoesNotExist:
Lukas Burgey's avatar
Lukas Burgey committed
618
619
620
621
622
            item = cls(
                parent=parent,
                site=site,
                service=service,
            )
623
624
            item.save()
            LOGGER.debug(item.msg('created'))
Lukas Burgey's avatar
Lukas Burgey committed
625
626
627
            return item


628
629
630
631
632
    # STATE transitions
    # user: deployment requested
    def user_deploy(self):
        if self.state == 'removal_pending':
            self._set_state('deployed')
Lukas Burgey's avatar
Lukas Burgey committed
633
            # TODO this is now valid
634
            return
Lukas Burgey's avatar
Lukas Burgey committed
635

Lukas Burgey's avatar
Lukas Burgey committed
636
637
638
        if self.state == 'deployed':
            LOGGER.info(self.msg('ignoring invalid state transition user_deploy'))
            return
639

Lukas Burgey's avatar
Lukas Burgey committed
640
641
642
643
        self._set_state(
            'deployment_pending',
            publish=False, # the post response already contains the update
        )
644

645
646
    # user: removal requested
    def user_remove(self):
Lukas Burgey's avatar
Lukas Burgey committed
647
648
649
650
        if self.state == 'not_deployed':
            LOGGER.info(self.msg('ignoring invalid state transition user_remove'))
            return

651
652
653
654
        if (
                self.state == 'deployment_pending'
                or self.state == 'questionnaire'
        ):
Lukas Burgey's avatar
Lukas Burgey committed
655
            # TODO this is not valid
656
657
            self._set_state('not_deployed')
            return
658

Lukas Burgey's avatar
Lukas Burgey committed
659
660
661
662
        self._set_state(
            'removal_pending',
            publish=False, # the post response already contains the update
        )
663

664
665
666
    # user: questionnaire answered
    def user_answers(self, answers=None):
        self.questionnaire = answers
Lukas Burgey's avatar
Lukas Burgey committed
667
        self._set_state('deployment_pending', publish=False)
Lukas Burgey's avatar
Lukas Burgey committed
668
        self.parent.publish_to_client()
669

670
671
    # returns None on success, or a string describing an error
    def client_response(self, output):
672
673
674
675
676
        status = output.get('state', 'undefined')

        self.message = output.get('message', '')
        self.save()

677
678
679
680
        if status != 'undefined':
            # update values
            if status == 'deployed':
                self.credentials = output.get('credentials', {})
681
682
                self.save()
            elif status == 'not_deployed':
683
684
                # reset credentials and questi
                self._reset()
685
                self.save()
686
687
            elif status == 'questionnaire':
                self.questionnaire = output.get('questionnaire', {})
688
                self.save()
689
690
691
692
693
            elif status == 'rejected':
                pass
            elif status == 'failed':
                pass
            else:
694
                return 'unknown state \''+status+'\''
695
696
697
698

            self._set_state(status)
            return None
        return 'missing status in output'
699

Lukas Burgey's avatar
Lukas Burgey committed
700

701
702
703
704
705
706
    # resets all client sent values
    def _reset(self):
        self.credentials = credential_default()
        self.questionnaire = questionnaire_default()
        self.message = ''

707
708
709
    def msg(self, msg):
        return '[DSItem:{}] {}'.format(self, msg)

Lukas Burgey's avatar
Lukas Burgey committed
710
    def _set_state(self, state, publish=True):
711
712
713
714
715
        if self.state == 'rejected':
            LOGGER.info('refusing to change state of rejected state_item')
            self.save()
            return

716
717
718
        self.state = state
        self.save()
        LOGGER.debug(self.msg('state: '+self.state))
Lukas Burgey's avatar
Lukas Burgey committed
719
720
        if publish:
            self.parent.publish_to_user()
721

Lukas Burgey's avatar
Lukas Burgey committed
722
    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
723
        if self.group is not None:
Lukas Burgey's avatar
Lukas Burgey committed
724
            return '{}:{}@{}:{}#{}'.format(
Lukas Burgey's avatar
Lukas Burgey committed
725
726
727
                self.group,
                self.service,
                self.site,
728
                self.key.name,
Lukas Burgey's avatar
Lukas Burgey committed
729
730
                self.id,
            )
Lukas Burgey's avatar
Lukas Burgey committed
731
        return '{}:@{}:{}#{}'.format(
Lukas Burgey's avatar
Lukas Burgey committed
732
            self.service,
Lukas Burgey's avatar
Lukas Burgey committed
733
            self.site,
734
            self.key.name,
735
            self.id,
Lukas Burgey's avatar
Lukas Burgey committed
736
        )
737
738
739
740
741


#
# RECEIVERS
#
Lukas Burgey's avatar
Lukas Burgey committed
742

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

748
    if not instance.is_active and instance.is_active_at_clients:
749
750
751
752
753
754
755
756
        instance.deactivate()


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

757
    if instance.is_active and not instance.is_active_at_clients:
758
        instance.activate()