Commit 1dacf811 authored by Lukas Burgey's avatar Lukas Burgey
Browse files

Move some code to module initializers

parent 2038fdda
......@@ -2,8 +2,9 @@ from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group
from . import models
from .models import brokers as broker_models
from .auth.v1.models import OIDCConfig
from .models import models, brokers as broker_models
class TypeFilter(admin.SimpleListFilter):
......
......@@ -7,7 +7,7 @@ from django.http import HttpResponse
from django.contrib.auth import authenticate
from django.contrib.sessions.models import Session
from ....models.models import Site
from ....models import Site
from ....models.brokers import RabbitMQInstance
from ....models.users import User
......
from json import dumps
from logging import getLogger
from django.contrib.auth.models import Group
from django.db import models
from django_mysql.models import JSONField
from .brokers import RabbitMQInstance
from .users import User
LOGGER = getLogger(__name__)
STATE_CHOICES = (
('deployment_pending', 'Deployment Pending'),
('removal_pending', 'Removal Pending'),
('deployed', 'Deployed'),
('not_deployed', 'Not Deployed'),
('questionnaire', 'Questionnaire'),
('failed', 'Failed'),
('rejected', 'Rejected'),
)
def questionnaire_default():
return {}
def credential_default():
return {}
class Site(models.Model):
client = models.OneToOneField(
User,
related_name='site',
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,
)
@property
def pending_tasks(self):
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()
def __str__(self):
return self.name
class Service(models.Model):
name = models.CharField(
max_length=150,
unique=True,
)
description = models.TextField(
max_length=300,
blank=True,
)
site = models.ManyToManyField(
Site,
related_name='services',
)
groups = models.ManyToManyField(
Group,
related_name='services',
blank=True,
)
def __str__(self):
return self.name
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():
LOGGER.debug(user.msg('New service for group. Adding to deployment'))
# all group deployments have the same keys
# TODO check that assumption
try:
deployment = user.deployments.get(group=group)
deployment.service_added(self)
except Deployment.DoesNotExist:
LOGGER.error('Inconsistency of group deployment')
raise
class SSHPublicKey(models.Model):
name = models.CharField(
max_length=150,
)
key = models.TextField(
max_length=1000
)
# hidden field at the user
# TODO checks: if the user is null
user = models.ForeignKey(
User,
related_name='_ssh_keys',
on_delete=models.SET_NULL,
null=True,
)
# has the user triggered the deletion of this key?
deleted = models.BooleanField(
default=False,
editable=False,
)
@property
def deployed_anywhere(self):
for state in self.states.all():
for item in state.state_items.all():
if item.state == 'deployed' or item.state == 'removal_pending':
return True
return False
# does not directly delete the key if the key is deployed or removen
# somewhere
# the receiver 'delete_removen_ssh_key' does the actual deletion
def delete_key(self):
# if this key is not deployed anywhere we delete it now
if not self.deployed_anywhere:
LOGGER.info(self.msg('Direct deletion of key'))
self.delete()
return
LOGGER.info(self.msg('Deletion of key started'))
self.deleted = True
self.save()
# delete implies removeing the key from all clients
for deployment in self.deployments.all():
deployment.remove_key(self)
# when a key is removen by a client we try to finally delete it
def try_final_deletion(self):
if self.deleted:
if not self.deployed_anywhere:
LOGGER.info(self.msg('All clients have removen this key. Final deletion'))
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()
def __str__(self):
if self.deleted:
return 'DELETED: {}'.format(self.name)
return self.name
def msg(self, msg):
return '[SSHKey:{}] {}'.format(self, msg)
# 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
#
# 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
#
# Note: two possible kinds of Deployment:
# (group is None and service is not None) or
# (group is not None and service is None)
class Deployment(models.Model):
user = models.ForeignKey(
User,
related_name='deployments',
on_delete=models.SET_NULL,
null=True,
)
group = models.ForeignKey(
Group,
related_name='deployments',
on_delete=models.CASCADE,
null=True,
blank=True,
)
service = models.ForeignKey(
Service,
related_name='deployments',
on_delete=models.CASCADE,
null=True,
blank=True,
)
ssh_keys = models.ManyToManyField(
SSHPublicKey,
related_name='deployments',
blank=True,
)
is_active = models.BooleanField(
default=True,
)
# 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()
# get a deployment for a user/service.
# if it does not exist it is created
@classmethod
def get_deployment(cls, user, service=None, group=None):
try:
if service is not None:
return cls.objects.get(
user=user,
service=service,
)
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'))
deployment.save()
LOGGER.debug(deployment.msg('created'))
return deployment
# deploy credentials which were deployed prior to deactivation
def activate(self):
if self.is_active:
LOGGER.error(self.msg('already active'))
return
for key in self.ssh_keys.all():
self._deploy_key(key)
self.is_active = True
self.save()
LOGGER.info(self.msg('activated'))
# remove all credentials
def deactivate(self):
if not self.is_active:
LOGGER.error(self.msg('already deactivated'))
return
self.is_active = False
self.save()
for key in self.ssh_keys.all():
self._remove_key(key)
LOGGER.info(self.msg('deactivated'))
# deploy key and track changes in the key lists
def deploy_key(self, key):
if not self.is_active:
LOGGER.error(self.msg('cannot deploy while deactivated'))
raise Exception('deployment deactivated')
self.ssh_keys.add(key)
self.save()
self._deploy_key(key)
def service_added(self, service):
# a new service for this group was added and we may have to deploy some keys
LOGGER.debug(self.msg('Adding service {}'.format(service)))
for state in self.states.all():
state.service_added(service)
# remove key and track changes in the key lists
def remove_key(self, key):
if not self.is_active:
LOGGER.error(self.msg('cannot remove while deactivated'))
raise Exception('deployment deactivated')
self.ssh_keys.remove(key)
self.save()
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()
def __str__(self):
if self.service is not None:
return '{}:{}'.format(self.service, self.user)
return '{}:{}'.format(self.group, self.user)
def msg(self, msg):
return '[Depl.m:{}] {}'.format(self, msg)
# DeploymentState: knows:
# user, service, key, state_target
class DeploymentState(models.Model):
key = models.ForeignKey(
SSHPublicKey,
related_name='states',
# deleting the key leaves us without references about its deployments
# we _HAVE_ to remove all deployments prior to deleting key
on_delete=models.CASCADE,
)
deployment = models.ForeignKey(
Deployment,
related_name='states',
on_delete=models.CASCADE,
)
# which state do we currently want to reach?
state_target = models.CharField(
max_length=50,
choices=STATE_CHOICES,
default='deployed',
)
@property
def user(self):
return self.deployment.user
@property
def states(self):
return [item.state for item in self.state_items.all()]
@property
def state(self):
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
@property
def service(self):
return self.deployment.service
@property
def services(self):
return self.deployment.services
@property
def group(self):
return self.deployment.group
# get a state for a user/service.
# if it does not exist it is created
@classmethod
def get_state(cls, deployment, key):
# check if a state does already exist
state = None
try:
state = cls.objects.get(
deployment=deployment,
key=key,
)
return state
except cls.DoesNotExist:
state = cls(
deployment=deployment,
key=key,
)
state.save()
LOGGER.debug(state.msg('created'))
except cls.MultipleObjectsReturned:
LOGGER.error(
deployment.msg('to many DeploymentState objects for key {}'.format(key.name))
)
raise
# generate state items
if deployment.service is not None:
for site in deployment.service.site.all():
DeploymentStateItem.get_state_item(
parent=state,
site=site,
service=deployment.service,
).save()
elif deployment.group is not None:
# every site which provides a service for group
for site in deployment.sites:
for service in Service.objects.filter(
groups=deployment.group,
site=site,
):
DeploymentStateItem.get_state_item(
parent=state,
site=site,
service=service,
).save()
return state
def service_added(self, service):
LOGGER.debug(self.msg('Adding service {}'.format(service)))
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()
def deploy(self):
self._set_target('deployed')
for item in self.state_items.all():
item.user_deploy()
self.publish_to_client()
# each state item publishes its state to the user
def remove(self):
self._set_target('not_deployed')
for item in self.state_items.all():
item.user_remove()
self.publish_to_client()
# each state item publishes its state to the user
def publish_to_client(self):
# mitigating circular dependencies here
from .serializers.clients import DeploymentStateSerializer
msg = dumps(DeploymentStateSerializer(self).data)
if self.service is not None:
RabbitMQInstance.load().publish_by_service(
self.service,
msg,
)
elif self.group is not None:
RabbitMQInstance.load().publish_by_group(
self.group,
msg,
)
else:
LOGGER.error('Deployment as neither a group or a service')
# update the state of the remote webpage
def publish_to_user(self):
if self.user is not None:
from ..views.webpage import user_state
msg = dumps({
'user_state': user_state(self.user),
})
RabbitMQInstance.load().publish_to_user(
self.user,
msg,
)
def msg(self, msg):
return '[DState:{}] {}'.format(self, msg)
def _set_target(self, target):
self.state_target = target
LOGGER.debug(self.msg('target: '+target))
self.save()
def __str__(self):
if self.service is not None:
return '{}:{}#{}'.format(
self.service,
self.key.name,
self.id,
)
return '{}:{}#{}'.format(
self.group,
self.key.name,
self.id,
)
# DeploymentStateItem: knows:
# user, service, key, state_target, _and_ site
class DeploymentStateItem(models.Model):
parent = models.ForeignKey(
DeploymentState,
related_name='state_items',
on_delete=models.CASCADE,
)
site = models.ForeignKey(
Site,
related_name='state_items',
on_delete=models.CASCADE,
)
service = models.ForeignKey(
Service,
related_name='state_items',
on_delete=models.CASCADE,
)
state = models.CharField(
max_length=50,
choices=STATE_CHOICES,
default='deployment_pending',
)
# message for the user
message = models.TextField(
max_length=300,
default='',
)
# questions for the user (needed for deployment
questionnaire = JSONField(
default=questionnaire_default,
null=True,
blank=True,
)
# credentials for the service