deployments.py 20.9 KB
Newer Older
Lukas Burgey's avatar
Lukas Burgey committed
1
2
3
4
5

from logging import getLogger

from django.conf import settings
from django.db import models
6
from django.db.models import Q
Lukas Burgey's avatar
Lukas Burgey committed
7
8
from django.db.models.signals import post_delete
from django.dispatch import receiver
9

Lukas Burgey's avatar
Lukas Burgey committed
10
11
12
from django_mysql.models import JSONField
from polymorphic.models import PolymorphicModel

13
14
15
16
from feudal.backend.models import Site, Service
from feudal.backend.models.brokers import publish_to_user, RabbitMQInstance
from feudal.backend.models.users import User, SSHPublicKey
from feudal.backend.models.auth.vos import VO
Lukas Burgey's avatar
Lukas Burgey committed
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34


LOGGER = getLogger(__name__)


DEPLOYMENT_PENDING = 'deployment_pending'
REMOVAL_PENDING = 'removal_pending'
NOT_DEPLOYED = 'not_deployed'
DEPLOYED = 'deployed'
QUESTIONNAIRE = 'questionnaire'
FAILED = 'failed'
REJECTED = 'rejected'

TARGET_CHOICES = (
    (DEPLOYED, 'Deployed'),
    (NOT_DEPLOYED, 'Not Deployed'),
)
STATE_CHOICES = (
Lukas Burgey's avatar
Lukas Burgey committed
35
    (DEPLOYMENT_PENDING, 'Deployment Pending'),
Lukas Burgey's avatar
Lukas Burgey committed
36
37
38
39
40
41
42
43
44
45
46
47
48
    (REMOVAL_PENDING, 'Removal Pending'),
    (DEPLOYED, 'Deployed'),
    (NOT_DEPLOYED, 'Not Deployed'),
    (QUESTIONNAIRE, 'Questionnaire'),
    (FAILED, 'Failed'),
    (REJECTED, 'Rejected'),
)


def questionnaire_default():
    return {}


49
50
51
52
def answers_default():
    return {}


Lukas Burgey's avatar
Lukas Burgey committed
53
54
55
56
def credential_default():
    return {}


57
58
59
60
61
def get_deployment(user, vo=None, service=None):
    if vo is not None and service is not None:
        raise ValueError('Cannot create deployment for both vo and service')

    if vo is not None:
62
        # get_deployment updates automatically
63
64
65
        return VODeployment.get_deployment(user, vo)

    if service is not None:
66
        # get_deployment updates automatically
67
68
        return ServiceDeployment.get_deployment(user, service)

69
70
71
72
73
    deps = Deployment.objects.filter(user=user)
    for dep in deps:
        dep.update()
    return deps

74

Lukas Burgey's avatar
Lukas Burgey committed
75
76
77
78
class Deployment(PolymorphicModel):
    user = models.ForeignKey(
        User,
        related_name='deployments',
Lukas Burgey's avatar
Lukas Burgey committed
79
80
81
        # TODO check what repercussions the change to set null has
        on_delete=models.SET_NULL,
        null=True,
Lukas Burgey's avatar
Lukas Burgey committed
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
    )

    # which state do we currently want to reach?
    state_target = models.CharField(
        max_length=50,
        choices=TARGET_CHOICES,
        default=NOT_DEPLOYED,
    )

    is_active = models.BooleanField(
        default=True,
    )

    # credentials provided by the backend to the clients
    @property
    def credentials(self):
        return self.user.credentials

    @property
    def state(self):
102
        if self.states.exists():
Lukas Burgey's avatar
Lukas Burgey committed
103
            _state = ''
104
            for state in self.states.all():
Lukas Burgey's avatar
Lukas Burgey committed
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
                if _state == '':
                    _state = state.state
                elif _state != 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

    def _set_target(self, target):
120
121
        if str(self.state_target) == str(target):
            return
122

123
        LOGGER.debug(self.msg('Target: {} -> {} '.format(self.state_target, target)))
Lukas Burgey's avatar
Lukas Burgey committed
124

125
        self.state_target = target
Lukas Burgey's avatar
Lukas Burgey committed
126
127
        self.save()

128
        self.publish_to_user()
129

130
    def _assure_states_exist(self):
Lukas Burgey's avatar
Lukas Burgey committed
131
        # overridden in the specific implementations
132
        pass
133

134
135
    def update(self):
        self._assure_states_exist()
136
        self.publish_to_user()
137

138
139
140
141
    # call when you changed Deployment.state_target
    def target_changed(self):
        LOGGER.debug(self.msg('target_changed: {}'.format(self.state_target)))

142
        self._assure_states_exist()
143
144
145
146
147
148
149
150
151
152
        for item in self.states.filter(~Q(state=self.state_target)):
            item.dep_target_changed()

        self.publish_to_user()

        # publish if there are pending states
        for item in self.states.all():
            if item.is_pending or item.is_credential_pending:
                self.publish_to_client()
                return
Lukas Burgey's avatar
Lukas Burgey committed
153
154

    def user_credential_added(self, key):
155
        for item in self.states.all():
Lukas Burgey's avatar
Lukas Burgey committed
156
157
            item.user_credential_added(key)

158
        # TODO is this publishing needed?
159
160
        if self.state_target == DEPLOYED:
            self.publish()
Lukas Burgey's avatar
Lukas Burgey committed
161
162

    def user_credential_removed(self, key):
163
        for item in self.states.all():
Lukas Burgey's avatar
Lukas Burgey committed
164
165
            item.user_credential_removed(key)

166
        # TODO is this publishing needed?
167
168
        if self.state_target == DEPLOYED:
            self.publish()
Lukas Burgey's avatar
Lukas Burgey committed
169
170

    def publish_to_client(self):
171
172
        for state in self.states.all():
            state.publish_to_client()
Lukas Burgey's avatar
Lukas Burgey committed
173
174
175
176
177
178

    # sends a state update via RabbitMQ / STOMP to the users webpage instance
    def publish_to_user(self):
        if self.user is None:
            return

Lukas Burgey's avatar
Lukas Burgey committed
179
        if settings.DEBUG_PUBLISHING:
180
            LOGGER.debug(self.msg('publish_to_user: {}'.format(self.state_target)))
181

182
        publish_to_user(
Lukas Burgey's avatar
Lukas Burgey committed
183
            self.user,
184
185
186
            {
                'deployment': self,
            },
Lukas Burgey's avatar
Lukas Burgey committed
187
188
        )

189
    # TODO is it good to publish from here to the client?
Lukas Burgey's avatar
Lukas Burgey committed
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
    def publish(self):
        self.publish_to_user()
        self.publish_to_client()

    def msg(self, msg):
        return '{} - {}'.format(self, msg)

    def __str__(self):
        return 'Abstr. Deployment:({})#{}'.format(
            self.user,
            self.id,
        )


class VODeployment(Deployment):
    vo = models.ForeignKey(
        VO,
        related_name='vo_deployments',
        on_delete=models.CASCADE,
    )

    @property
    def services(self):
        return self.vo.services.all()

215
216
217
218
219
220
221
222
    @property
    def broker_exchange(self):
        return self.vo.broker_exchange

    @property
    def routing_key(self):
        return self.vo.name

223
    def _assure_states_exist(self):
Lukas Burgey's avatar
Lukas Burgey committed
224
225
        for service in self.services:
            DeploymentState.get_state_item(
226
227
228
                self.user,
                service.site,
                service,
229
                deployments=[self],
Lukas Burgey's avatar
Lukas Burgey committed
230
231
232
233
234
235
236
237
238
            )

    @classmethod
    def get_deployment(cls, user, vo):
        try:
            deployment = cls.objects.get(
                user=user,
                vo=vo,
            )
239
            deployment.update()
Lukas Burgey's avatar
Lukas Burgey committed
240
241
242
243
244
245
246
247
248
249

            return deployment

        except cls.DoesNotExist:
            deployment = cls(
                user=user,
                vo=vo,
            )

            deployment.save()
250
            deployment.update()
Lukas Burgey's avatar
Lukas Burgey committed
251

252
            LOGGER.debug(deployment.msg('Created'))
Lukas Burgey's avatar
Lukas Burgey committed
253
254
255
256
            return deployment

    def service_added(self, service):
        LOGGER.debug(self.msg('Adding service {}'.format(service)))
Lukas Burgey's avatar
Lukas Burgey committed
257
        state = DeploymentState.get_state_item(
258
259
260
            self.user,
            service.site,
            service,
261
            deployments=[self],
Lukas Burgey's avatar
Lukas Burgey committed
262
        )
Lukas Burgey's avatar
Lukas Burgey committed
263
264
265
        if self.state_target == DEPLOYED:
            state.publish_to_client()
            state.publish_to_user()
Lukas Burgey's avatar
Lukas Burgey committed
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284

    def msg(self, msg):
        return '{} - {}'.format(self, msg)

    def __str__(self):
        return 'VO-Dep: ({}:{})#{}'.format(
            self.vo,
            self.user,
            self.id,
        )


class ServiceDeployment(Deployment):
    service = models.ForeignKey(
        Service,
        related_name='service_deployments',
        on_delete=models.CASCADE,
    )

285
286
287
288
289
290
291
292
    @property
    def broker_exchange(self):
        return 'services'

    @property
    def routing_key(self):
        return self.service.name

293
    def _assure_states_exist(self):
Lukas Burgey's avatar
Lukas Burgey committed
294
        DeploymentState.get_state_item(
295
296
297
            self.user,
            self.service.site,
            self.service,
298
            deployments=[self],
Lukas Burgey's avatar
Lukas Burgey committed
299
300
301
302
303
304
305
306
307
        )

    @classmethod
    def get_deployment(cls, user, service):
        try:
            deployment = cls.objects.get(
                user=user,
                service=service,
            )
308
            deployment.update()
Lukas Burgey's avatar
Lukas Burgey committed
309
310
311
312
313
314
315
316
317

            return deployment

        except cls.DoesNotExist:
            deployment = cls(
                user=user,
                service=service
            )
            deployment.save()
318
            deployment.update()
Lukas Burgey's avatar
Lukas Burgey committed
319

320
            LOGGER.debug(deployment.msg('Created'))
Lukas Burgey's avatar
Lukas Burgey committed
321
322
323
324
325
326
327
328
329
330
331
332
333
            return deployment

    def msg(self, msg):
        return '{} - {}'.format(self, msg)

    def __str__(self):
        return 'Service-Dep: ({}:{})#{}'.format(
            self.service,
            self.user,
            self.id,
        )


Lukas Burgey's avatar
Lukas Burgey committed
334
335
336
337
338
339
340
341
342
343
344
# pylint: disable=unused-argument
@receiver(post_delete, sender=VODeployment)
@receiver(post_delete, sender=ServiceDeployment)
def publish_deletion(sender, instance=None, **kwargs):
    if instance is None:
        return

    LOGGER.debug('Publishing deletion of %s', instance)
    instance.publish()


Lukas Burgey's avatar
Lukas Burgey committed
345
class DeploymentState(models.Model):
346
    deployments = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
347
        Deployment,
348
        related_name='states',
Lukas Burgey's avatar
Lukas Burgey committed
349
350
351
352
    )

    user = models.ForeignKey(
        User,
353
        related_name='states',
Lukas Burgey's avatar
Lukas Burgey committed
354
355
356
357
358
359
360
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

    site = models.ForeignKey(
        Site,
361
        related_name='states',
Lukas Burgey's avatar
Lukas Burgey committed
362
363
364
        on_delete=models.CASCADE,
    )

Lukas Burgey's avatar
Lukas Burgey committed
365
    # FIXME deleting a service currently deletes all deployment states without removing deployments!
Lukas Burgey's avatar
Lukas Burgey committed
366
367
    service = models.ForeignKey(
        Service,
368
        related_name='states',
Lukas Burgey's avatar
Lukas Burgey committed
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
        on_delete=models.CASCADE,
    )

    state = models.CharField(
        max_length=50,
        choices=STATE_CHOICES,
        default=NOT_DEPLOYED,
    )

    # message for the user
    message = models.TextField(
        max_length=300,
        default='',
    )

    # questions for the user (needed for deployment
    questionnaire = JSONField(
386
387
388
389
390
391
        null=True,
        blank=True,
    )

    # the users answers for the questionnaire
    answers = JSONField(
Lukas Burgey's avatar
Lukas Burgey committed
392
393
394
395
396
397
398
399
400
401
402
403
        null=True,
        blank=True,
    )

    # credentials for the service
    # only valid when state == deployed
    credentials = JSONField(
        default=credential_default,
        null=True,
        blank=True,
    )

404
405
    @property
    def state_target(self):
Lukas Burgey's avatar
Lukas Burgey committed
406
        # this instance needs to be saved before accessing a many to many field
407
408
409
410
411
412
        if self.pk is None:
            self.save()

        for deployment in self.deployments.all():
            if deployment.state_target == DEPLOYED:
                return DEPLOYED
413
414
415

        return NOT_DEPLOYED

416
417
418
419
    @property
    def is_orphaned(self):
        return not self.deployments.exists()

Lukas Burgey's avatar
Lukas Burgey committed
420
421
    @property
    def is_pending(self):
Lukas Burgey's avatar
Lukas Burgey committed
422
423
        # When in these states we are never pending
        if self.state in [QUESTIONNAIRE, REJECTED]:
424
            return False
Lukas Burgey's avatar
Lukas Burgey committed
425

Lukas Burgey's avatar
Lukas Burgey committed
426
        return self.is_orphaned or self.state_target != self.state or self.is_credential_pending
Lukas Burgey's avatar
Lukas Burgey committed
427
428
429

    @property
    def is_credential_pending(self):
430
        # little hack to not confuse the user
Lukas Burgey's avatar
Lukas Burgey committed
431
        if self.state in [QUESTIONNAIRE, REJECTED]:
432
433
            return False

Lukas Burgey's avatar
Lukas Burgey committed
434
435
436
        for credential_state in self.credential_states.all():
            if credential_state.is_pending:
                return True
Lukas Burgey's avatar
Lukas Burgey committed
437

Lukas Burgey's avatar
Lukas Burgey committed
438
439
        return False

Lukas Burgey's avatar
Lukas Burgey committed
440
    # TODO not accessible when the user is deleted
Lukas Burgey's avatar
Lukas Burgey committed
441
442
443
444
    @property
    def user_credentials(self):
        return self.user.credentials

Lukas Burgey's avatar
Lukas Burgey committed
445
446
    # get_state_item returns a state item for a given user, site and service
    # if the state does not exist it is created
Lukas Burgey's avatar
Lukas Burgey committed
447
    @classmethod
Lukas Burgey's avatar
Lukas Burgey committed
448
    def get_state_item(cls, user, site, service, deployments=None):
Lukas Burgey's avatar
Lukas Burgey committed
449
450
451
452
453
454
        try:
            item = cls.objects.get(
                user=user,
                site=site,
                service=service,
            )
455

Lukas Burgey's avatar
Lukas Burgey committed
456
            # connect state item to the provided deployments
Lukas Burgey's avatar
Lukas Burgey committed
457
458
459
460
461
            if deployments is not None:
                for deployment in deployments:
                    if not item.deployments.filter(id=deployment.id).exists():
                        LOGGER.debug(item.msg('Binding to deployment {}'.format(deployment)))
                        item.deployments.add(deployment)
462

Lukas Burgey's avatar
Lukas Burgey committed
463
464
465
466
467
468
469
470
471
            return item

        except cls.DoesNotExist:
            item = cls(
                user=user,
                site=site,
                service=service,
            )
            item.save()
Lukas Burgey's avatar
Lukas Burgey committed
472
473
474
475

            if deployments is not None:
                for deployment in deployments:
                    item.deployments.add(deployment)
Lukas Burgey's avatar
Lukas Burgey committed
476

477
            LOGGER.debug(item.msg('Created'))
Lukas Burgey's avatar
Lukas Burgey committed
478
479
480

            item.dep_target_changed()

Lukas Burgey's avatar
Lukas Burgey committed
481
482
483
484
485
            return item

    # starts tracking this the credential for this item
    def user_credential_added(self, credential):
        if settings.DEBUG_CREDENTIALS:
486
            LOGGER.debug(self.msg('Adding credential {}'.format(credential.name)))
Lukas Burgey's avatar
Lukas Burgey committed
487
488
489
490
491
492
493
494

        CredentialState.get_credential_state(
            credential,
            self,
        )

    def user_credential_removed(self, credential):
        if settings.DEBUG_CREDENTIALS:
495
            LOGGER.debug(self.msg('Removing credential {}'.format(credential.name)))
Lukas Burgey's avatar
Lukas Burgey committed
496
497
498
499
500
501
502
503
504
505

        try:
            credential_state = self.credential_states.get(credential=credential)
            credential_state.credential_deleted()

        except CredentialState.DoesNotExist:
            LOGGER.error(self.msg('Credential {} has no CredentialState'.format(credential)))

    # STATE TRANSITIONS

Lukas Burgey's avatar
Lukas Burgey committed
506
    # called when one of our deployments changes its state_target
507
    def dep_target_changed(self):
Lukas Burgey's avatar
Lukas Burgey committed
508
        LOGGER.debug(self.msg('dep_target_changed: {}'.format(self.state_target)))
509
510
511
512
513
514
515
516
517
518
519
520
521

        self._assure_credential_states_exist()
        for cred_state in self.credential_states.all():
            cred_state.set_target(self.state_target)

        if self.state_target == DEPLOYED:
            # handle the 7 states we can be in
            if self.state == NOT_DEPLOYED or self.state == REMOVAL_PENDING:
                self._set_state(DEPLOYMENT_PENDING)
            elif self.state == DEPLOYED or self.state == DEPLOYMENT_PENDING:
                LOGGER.debug(self.msg('has already reached the correct state'))
            elif self.state == QUESTIONNAIRE:
                LOGGER.debug(self.msg('is questionnaire! Need answers first'))
522
                self.publish_to_user()
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
            elif self.state == REJECTED:
                LOGGER.debug(self.msg('is rejected! The site will never execute this deployement'))
            elif self.state == FAILED:
                LOGGER.debug(self.msg('is failed! We will retry it'))

        elif self.state_target == NOT_DEPLOYED:
            self._reset()

            # handle the 7 states we can be in
            if self.state == DEPLOYED or self.state == DEPLOYMENT_PENDING:
                self._set_state(REMOVAL_PENDING)
            elif self.state == NOT_DEPLOYED or self.state == REMOVAL_PENDING:
                LOGGER.debug(self.msg('has already reached the correct state'))
            elif self.state == FAILED or self.state == REJECTED or self.state == QUESTIONNAIRE:
                self._set_state(NOT_DEPLOYED)

Lukas Burgey's avatar
Lukas Burgey committed
539
540
541
542
    def state_changed(self):
        LOGGER.debug(self.msg('State: {} Target: {}'.format(self. state, self.state_target)))
        self.save()

543
    # called when the answers has changed
544
545
546
    def answers_changed(self):
        if self.state == QUESTIONNAIRE or self.state == DEPLOYED:
            LOGGER.debug(self.msg('Changed answers cause republishing'))
547
            self._set_state(DEPLOYMENT_PENDING)
548
            self.publish_to_client()
549

Lukas Burgey's avatar
Lukas Burgey committed
550
551
    def client_credential_states(self, credential_states):
        # maps ssh key names to their state
Lukas Burgey's avatar
Lukas Burgey committed
552
        ssh_key_states = credential_states.get('ssh_key', {})
Lukas Burgey's avatar
Lukas Burgey committed
553
554
555
556

        # ASSUMPTION:
        # if we hear nothing about a credential we assume it got deprovisioned!
        for credential_state in self.credential_states.all():
Lukas Burgey's avatar
Lukas Burgey committed
557
            if credential_state.credential.name in ssh_key_states:
Lukas Burgey's avatar
Lukas Burgey committed
558
559
560
561
562
563
564
565
566
                credential_state.set(ssh_key_states[credential_state.credential.name])
            else:
                credential_state.set(NOT_DEPLOYED)

    # resets all client sent values
    def _reset(self):
        self.credentials = credential_default()
        self.message = ''

567
    def _assure_credential_states_exist(self):
Lukas Burgey's avatar
Lukas Burgey committed
568
569
570
571
572
573
574
575
576
577
578
        # assure all user credentials have a state
        if self.user is not None:
            for key in self.user.ssh_keys.all():
                try:
                    CredentialState.get_credential_state(
                        credential=key,
                        target=self,
                    )
                except CredentialState.DoesNotExist:
                    LOGGER.error('CredentialState.DoesNotExist in _set_state')

579
    def _set_state(self, state, publish=False):
580
581
        self._assure_credential_states_exist()

582
        # not trans
583
        if str(self.state) == str(state):
584
585
586
            # publish to user (even if the state did not change!)
            if publish:
                self.publish_to_user()
587
588
589
            return

        LOGGER.debug(self.msg('State: {} -> {} - Target: {}'.format(self.state, state, self.state_target)))
Lukas Burgey's avatar
Lukas Burgey committed
590
591
592

        self.state = state
        self.save()
593

594
        if publish:
595
596
597
            self.publish_to_user()

    def publish_to_user(self):
598
599
600
601
        if self.user is not None:
            if settings.DEBUG_PUBLISHING:
                LOGGER.debug(self.msg('publish_to_user'))

602
            publish_to_user(
603
604
605
606
607
                self.user,
                {
                    'deployment_state': self,
                },
            )
608
609

    def publish_to_client(self):
610
611
612
        # no need to publish if not pending
        if not self.is_pending:
            return
613

614
615
616
617
618
619
620
621
622
623
        LOGGER.log(
            settings.AUDIT_LOG_LEVEL,
            '%s@%s - %s @ %s - request - %s',
            self.user.sub,
            self.user.idp.issuer_uri,
            self.service.name,
            self.site.name,
            self.state_target,
        )

624
625
626
        if settings.DEBUG_PUBLISHING:
            LOGGER.debug(self.msg('publish_to_client'))

627
        RabbitMQInstance.load().publish_deployment_state(self)
628
629
630
631
632

        # DEPRECATED
        # only publish to the client using deployments with our target
        # for deployment in self.deployments.filter(state_target=self.state_target):
        #     deployment.publish_to_client()
633

Lukas Burgey's avatar
Lukas Burgey committed
634
    def msg(self, msg):
635
        return ' {} - {}'.format(self, msg)
Lukas Burgey's avatar
Lukas Burgey committed
636
637

    def __str__(self):
638
639
640
641
642
643
644
645
646
647
648
649
        # causes a loop
        # if self.deployments.exists():
        #     deployment_names = [str(deployment.id) for deployment in self.deployments.all()]

        #     return 'Dep-St: ({}:{}:{})#{}'.format(
        #         ','.join(deployment_names),
        #         self.service,
        #         self.site,
        #         self.id,
        #     )

        return 'Dep-St: ({}:{})#{}'.format(
Lukas Burgey's avatar
Lukas Burgey committed
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
            self.service,
            self.site,
            self.id,
        )


class CredentialState(models.Model):
    state_target = models.CharField(
        max_length=50,
        choices=TARGET_CHOICES,
        default=NOT_DEPLOYED
    )

    state = models.CharField(
        max_length=50,
        choices=STATE_CHOICES,
        default=NOT_DEPLOYED
    )

    credential = models.ForeignKey(
        SSHPublicKey,
        related_name='credential_states',
        on_delete=models.CASCADE,
    )

675
    # TODO target is a stupid field name. Change it
Lukas Burgey's avatar
Lukas Burgey committed
676
677
678
679
680
681
    target = models.ForeignKey(
        DeploymentState,
        related_name='credential_states',
        on_delete=models.CASCADE,
    )

Lukas Burgey's avatar
Lukas Burgey committed
682
683
684
685
    @property
    def _credential_deleted(self):
        return self.credential.deleted

Lukas Burgey's avatar
Lukas Burgey committed
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
    @property
    def is_pending(self):
        return self.state != self.state_target

    @classmethod
    def get_credential_state(cls, credential, target):
        try:
            return cls.objects.get(
                credential=credential,
                target=target,
            )
        except cls.DoesNotExist:

            new_state = cls(
                credential=credential,
                target=target,
                state=NOT_DEPLOYED,
703
                state_target=target.state_target,
Lukas Burgey's avatar
Lukas Burgey committed
704
705
706
707
708
709
710
711
712
            )
            new_state.save()

            if settings.DEBUG_CREDENTIALS:
                LOGGER.debug(new_state.msg('Created'))

            return new_state

    def set_target(self, target):
713
714
715
        if str(self.state_target) == str(target):
            return

Lukas Burgey's avatar
Lukas Burgey committed
716
717
718
719
        # state_target is locked, since we are marked for deletion
        if self._credential_deleted:
            return

720
721
        LOGGER.debug(self.msg('Target: {} -> {}'.format(self.state_target, target)))

Lukas Burgey's avatar
Lukas Burgey committed
722
723
724
725
726
727
728
729
        self.state_target = target
        self.save()

    def set(self, state):
        if state == NOT_DEPLOYED and self._credential_deleted:
            self._delete_state()
            return

Lukas Burgey's avatar
Lukas Burgey committed
730
731
732
        if str(self.state) == str(state):
            return

Lukas Burgey's avatar
Lukas Burgey committed
733
734
735
        if state == self.state:
            return

736
737
738
        if settings.DEBUG_CREDENTIALS:
            LOGGER.debug(self.msg('State: {} -> {}'.format(self.state, state)))

Lukas Burgey's avatar
Lukas Burgey committed
739
740
        self.state = state
        self.save()
741

Lukas Burgey's avatar
Lukas Burgey committed
742
743
744
745
746
747
748
749
750
751
752
    def credential_deleted(self):
        if self.state == NOT_DEPLOYED:
            self._delete_state()

        self.state_target = NOT_DEPLOYED
        self.save()

        if settings.DEBUG_CREDENTIALS:
            LOGGER.debug(self.msg('Marked as deleted'))

    def _delete_state(self):
Lukas Burgey's avatar
Lukas Burgey committed
753
        LOGGER.debug(self.msg('Deleting credential state'))
Lukas Burgey's avatar
Lukas Burgey committed
754
755
756
757
758
759
        credential = self.credential
        self.delete()

        credential.try_delete_key()

    def msg(self, message):
760
        return '  {} - {}'.format(self, message)
Lukas Burgey's avatar
Lukas Burgey committed
761
762

    def __str__(self):
763
        return 'Cred-St: ({}:{})#{}'.format(
Lukas Burgey's avatar
Lukas Burgey committed
764
765
766
767
            self.target.id,
            self.credential,
            self.id,
        )