Commit 68fcd2de authored by Lukas Burgey's avatar Lukas Burgey
Browse files

Rework the deployments module

parent 54394644
# Generated by Django 3.0.3 on 2020-02-13 10:29
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('backend', '0013_auto_20200130_1528'),
]
operations = [
migrations.RenameField(
model_name='credentialstate',
old_name='target',
new_name='deployment_state',
),
migrations.AlterField(
model_name='deployment',
name='user',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='deployments', to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]
......@@ -76,9 +76,8 @@ class Deployment(PolymorphicModel):
user = models.ForeignKey(
User,
related_name='deployments',
# TODO check what repercussions the change to set null has
on_delete=models.SET_NULL,
null=True,
# deleting all deployments causes the DeploymentState to be orphaned -> removes deployments from the sites
on_delete=models.CASCADE,
)
# which state do we currently want to reach?
......@@ -128,8 +127,7 @@ class Deployment(PolymorphicModel):
self.publish_to_user()
def _assure_states_exist(self):
# overridden in the specific implementations
pass
raise NotImplementedError('Should be overwritten in subclass')
def update(self):
self._assure_states_exist()
......@@ -155,7 +153,6 @@ class Deployment(PolymorphicModel):
for item in self.states.all():
item.user_credential_added(key)
# TODO is this publishing needed?
if self.state_target == DEPLOYED:
self.publish()
......@@ -163,7 +160,6 @@ class Deployment(PolymorphicModel):
for item in self.states.all():
item.user_credential_removed(key)
# TODO is this publishing needed?
if self.state_target == DEPLOYED:
self.publish()
......@@ -173,9 +169,6 @@ class Deployment(PolymorphicModel):
# sends a state update via RabbitMQ / STOMP to the users webpage instance
def publish_to_user(self):
if self.user is None:
return
if settings.DEBUG_PUBLISHING:
LOGGER.debug(self.msg('publish_to_user: {}'.format(self.state_target)))
......@@ -186,7 +179,6 @@ class Deployment(PolymorphicModel):
},
)
# TODO is it good to publish from here to the client?
def publish(self):
self.publish_to_user()
self.publish_to_client()
......@@ -220,37 +212,38 @@ class VODeployment(Deployment):
def routing_key(self):
return self.vo.name
# _assure_states_exist creates missing DeploymentState for this deployment
# returns True if new states were created
def _assure_states_exist(self):
created_new_states = False
for service in self.services:
DeploymentState.get_state_item(
self.user,
service.site,
service,
deployments=[self],
state, created = DeploymentState.objects.get_or_create(
user=self.user,
site=service.site,
service=service,
)
if created:
LOGGER.debug(state.msg('Created'))
state.bind_to_deployment(self) # bind new states to us
created_new_states = created
return created_new_states
@classmethod
def get_deployment(cls, user, vo):
try:
deployment = cls.objects.get(
user=user,
vo=vo,
)
deployment.update()
return deployment
deployment, created = cls.objects.get_or_create(
user=user,
vo=vo,
)
except cls.DoesNotExist:
deployment = cls(
user=user,
vo=vo,
)
if created:
LOGGER.debug(deployment.msg('Created'))
deployment.save()
deployment.update()
deployment.update() # creates deployment states and publishes the creation
LOGGER.debug(deployment.msg('Created'))
return deployment
return deployment
def service_added(self, service):
LOGGER.debug(self.msg('Adding service {}'.format(service)))
......@@ -300,6 +293,12 @@ class ServiceDeployment(Deployment):
@classmethod
def get_deployment(cls, user, service):
deployment, created = cls.object.get_or_create(user=user, service=service)
if created:
LOGGER.debug(deployment.msg('Created'))
deployment.update()
try:
deployment = cls.objects.get(
user=user,
......@@ -317,7 +316,6 @@ class ServiceDeployment(Deployment):
deployment.save()
deployment.update()
LOGGER.debug(deployment.msg('Created'))
return deployment
def msg(self, msg):
......@@ -362,7 +360,7 @@ class DeploymentState(models.Model):
on_delete=models.CASCADE,
)
# FIXME deleting a service currently deletes all deployment states without removing deployments!
# Deleting a service deletes all deployment states without removing deployments!
service = models.ForeignKey(
Service,
related_name='states',
......@@ -403,10 +401,6 @@ class DeploymentState(models.Model):
@property
def state_target(self):
# this instance needs to be saved before accessing a many to many field
if self.pk is None:
self.save()
for deployment in self.deployments.all():
if deployment.state_target == DEPLOYED:
return DEPLOYED
......@@ -415,10 +409,13 @@ class DeploymentState(models.Model):
@property
def is_orphaned(self):
return not self.deployments.exists()
return not self.deployments.exists() or self.user is None
@property
def is_pending(self):
if self.user is None:
return True # pending until deleted
# When in these states we are never pending
if self.state in [QUESTIONNAIRE, REJECTED]:
return False
......@@ -437,58 +434,53 @@ class DeploymentState(models.Model):
return False
# TODO not accessible when the user is deleted
@property
def user_credentials(self):
if self.user is None:
LOGGER.debug(self.msg('User is None'))
raise User.DoesNotExist
return self.user.credentials
# get_state_item returns a state item for a given user, site and service
# if the state does not exist it is created
@classmethod
def get_state_item(cls, user, site, service, deployments=None):
try:
item = cls.objects.get(
user=user,
site=site,
service=service,
)
# connect state item to the provided deployments
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)
return item
item, created = cls.objects.get_or_create(
user=user,
site=site,
service=service,
)
except cls.DoesNotExist:
item = cls(
user=user,
site=site,
service=service,
)
item.save()
if deployments is not None:
for deployment in deployments:
# connect state item to the provided deployments
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)
if created:
LOGGER.debug(item.msg('Created'))
item.dep_target_changed()
return item
return item
# adds the deployment to our related deployments
def bind_to_deployment(self, deployment):
if not self.deployments.filter(id=deployment.id).exists():
LOGGER.debug(self.msg('Binding to deployment {}'.format(deployment)))
self.deployments.add(deployment)
# we call this so the state_target of the new deployment gets propagated
self.dep_target_changed()
# starts tracking this the credential for this item
def user_credential_added(self, credential):
if settings.DEBUG_CREDENTIALS:
LOGGER.debug(self.msg('Adding credential {}'.format(credential.name)))
CredentialState.get_credential_state(
credential,
self,
)
CredentialState.get_credential_state(credential, self)
def user_credential_removed(self, credential):
if settings.DEBUG_CREDENTIALS:
......@@ -503,7 +495,7 @@ class DeploymentState(models.Model):
# STATE TRANSITIONS
# called when one of our deployments changes its state_target
# called when one of our deployments changes its state_target or when we are first created
def dep_target_changed(self):
LOGGER.debug(self.msg('dep_target_changed: {}'.format(self.state_target)))
......@@ -513,29 +505,30 @@ class DeploymentState(models.Model):
if self.state_target == DEPLOYED:
# handle the 7 states we can be in
if self.state == NOT_DEPLOYED or self.state == REMOVAL_PENDING:
if self.state in [NOT_DEPLOYED, 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 in [DEPLOYED, DEPLOYMENT_PENDING]:
pass # already reached correct state
elif self.state == QUESTIONNAIRE:
LOGGER.debug(self.msg('is questionnaire! Need answers first'))
self.publish_to_user()
self.publish_to_user() # needs answers
elif self.state == REJECTED:
LOGGER.debug(self.msg('is rejected! The site will never execute this deployement'))
pass # client will never execute this deployment
elif self.state == FAILED:
LOGGER.debug(self.msg('is failed! We will retry it'))
pass # client will retry the deployment
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:
if self.state in [DEPLOYED, 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 in [NOT_DEPLOYED, REMOVAL_PENDING]:
pass # already reached correct state
elif self.state == FAILED or self.state == REJECTED or self.state == QUESTIONNAIRE:
self._set_state(NOT_DEPLOYED)
LOGGER.debug(self.msg('State: {} Target: {}'.format(self. state, self.state_target)))
def state_changed(self):
LOGGER.debug(self.msg('State: {} Target: {}'.format(self. state, self.state_target)))
self.save()
......@@ -566,15 +559,12 @@ class DeploymentState(models.Model):
def _assure_credential_states_exist(self):
# 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')
if self.user is None:
LOGGER.debug(self.msg('_assure_credential_states_exist: User is None'))
return
for key in self.user.ssh_keys.all():
CredentialState.get_credential_state(key, self)
def _set_state(self, state, publish=False):
self._assure_credential_states_exist()
......@@ -595,42 +585,41 @@ class DeploymentState(models.Model):
self.publish_to_user()
def publish_to_user(self):
if self.user is not None:
if settings.DEBUG_PUBLISHING:
LOGGER.debug(self.msg('publish_to_user'))
publish_to_user(
self.user,
{
'deployment_state': self,
},
)
if self.user is None:
LOGGER.debug(self.msg('publish_to_user: User is None'))
return
if settings.DEBUG_PUBLISHING:
LOGGER.debug(self.msg('publish_to_user'))
publish_to_user(
self.user,
{
'deployment_state': self,
},
)
def publish_to_client(self):
# no need to publish if not pending
if not self.is_pending:
return
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,
)
if self.user is not None:
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,
)
if settings.DEBUG_PUBLISHING:
LOGGER.debug(self.msg('publish_to_client'))
RabbitMQInstance.load().publish_deployment_state(self)
# 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()
def msg(self, msg):
return ' {} - {}'.format(self, msg)
......@@ -672,8 +661,7 @@ class CredentialState(models.Model):
on_delete=models.CASCADE,
)
# TODO target is a stupid field name. Change it
target = models.ForeignKey(
deployment_state = models.ForeignKey(
DeploymentState,
related_name='credential_states',
on_delete=models.CASCADE,
......@@ -688,26 +676,20 @@ class CredentialState(models.Model):
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,
state_target=target.state_target,
)
new_state.save()
def get_credential_state(cls, credential, deployment_state):
credential_state, created = cls.objects.get_or_create(
credential=credential,
deployment_state=deployment_state,
defaults={
'state': NOT_DEPLOYED,
'state_target': deployment_state.state_target,
},
)
if settings.DEBUG_CREDENTIALS:
LOGGER.debug(new_state.msg('Created'))
if settings.DEBUG_CREDENTIALS and created:
LOGGER.debug(credential_state.msg('Created'))
return new_state
return credential_state
def set_target(self, target):
if str(self.state_target) == str(target):
......@@ -727,10 +709,7 @@ class CredentialState(models.Model):
self._delete_state()
return
if str(self.state) == str(state):
return
if state == self.state:
if str(self.state) == str(state) or state == self.state:
return
if settings.DEBUG_CREDENTIALS:
......@@ -761,7 +740,7 @@ class CredentialState(models.Model):
def __str__(self):
return 'Cred-St: ({}:{})#{}'.format(
self.target.id,
self.deployment_state.id,
self.credential,
self.id,
)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment