Commit 99fba34f authored by Lukas Burgey's avatar Lukas Burgey
Browse files

Implement the questionnaire logic

parent d45e4e76
......@@ -5,3 +5,4 @@ db.cnf
static
deployment
deploy
runtest
......@@ -7,6 +7,7 @@ from django.contrib.sessions.models import Session
from ...models import User, RabbitMQInstance
LOGGER = logging.getLogger(__name__)
CLIENT_DEBUGGING = False
ALLOW = HttpResponse('allow')
DENY = HttpResponse('deny')
......@@ -144,6 +145,7 @@ def resource_endpoint(request):
_webpage_client_userid(request)
and _resource_authorized_webpage_client(request)
):
if CLIENT_DEBUGGING:
LOGGER.debug(
'Granted %s access to resource %s %s to client',
permission,
......@@ -156,6 +158,7 @@ def resource_endpoint(request):
_apiclient_valid(request)
and _resource_authorized_apiclient(request)
):
if CLIENT_DEBUGGING:
LOGGER.debug(
'Granted %s access to resource %s %s to client',
permission,
......@@ -187,6 +190,7 @@ def topic_endpoint(request):
routing_key == webpage_client_userid
and not 'write' in permission
):
if CLIENT_DEBUGGING:
LOGGER.debug(
'Granted %s access to %s %s to client',
permission,
......
......@@ -3,7 +3,7 @@ import logging
from rest_framework import generics, views
from rest_framework.authentication import BasicAuthentication
from rest_framework.response import Response
from .serializers import SiteSerializer, ServiceSerializer, RabbitMQInstanceSerializer
from .serializers import DeploymentTaskSerializer, ServiceSerializer, RabbitMQInstanceSerializer
from ..models import RabbitMQInstance
LOGGER = logging.getLogger(__name__)
......@@ -13,12 +13,12 @@ LOGGER = logging.getLogger(__name__)
AUTHENTICATION_CLASSES = (BasicAuthentication, )
class DeploymentsView(generics.RetrieveAPIView):
class DeploymentsView(generics.ListAPIView):
authentication_classes = AUTHENTICATION_CLASSES
serializer_class = SiteSerializer
serializer_class = DeploymentTaskSerializer
def get_object(self):
return self.request.user.site
def get_queryset(self):
return self.request.user.site.tasks
# the client has to fetch the configuration (like services etc.) here
......@@ -45,8 +45,7 @@ class AckView(views.APIView):
# find the corresponding task for this item
for item in request.user.site.task_items.all():
if item.task.id == int(task_id):
item.task.item_finished(request.user.site)
LOGGER.debug('Got acknowledgement for task %s', task_id)
item.success()
return Response({'ok': True})
# this is no critical
......@@ -71,15 +70,15 @@ class ResponseView(views.APIView):
if task_item is not None:
if status == 'success':
task_item.task.item_finished(request.user.site)
task_item.done()
return Response({'ok': True})
elif status == 'fail':
task_item.task.item_failed(request.user.site)
task_item.failed()
return Response({'ok': True})
elif status == 'reject':
task_item.task.item_rejected(request.user.site)
task_item.rejected(request.data['output']['questionnaire'])
return Response({'ok': True})
LOGGER.info('%s executed the obsolete task#%s', request.user, task_id)
......
......@@ -24,6 +24,7 @@ class DeploymentTaskItemSerializer(serializers.ModelSerializer):
service = ServiceSerializer()
key = backend_serializers.SSHPublicKeySerializerB()
site = SiteSerializer()
questionnaire = serializers.JSONField()
class Meta:
model = models.DeploymentTaskItem
......@@ -33,6 +34,8 @@ class DeploymentTaskItemSerializer(serializers.ModelSerializer):
'service',
'site',
'state',
'questionnaire',
'id',
]
......@@ -45,6 +48,7 @@ class DeploymentTaskSerializer(serializers.ModelSerializer):
'action',
'key',
'service',
'id',
]
......
......@@ -6,4 +6,5 @@ URLPATTERNS = [
url(r'^sshkey', views.SSHPublicKeyView.as_view()),
url(r'^deployments', views.DeploymentView.as_view()),
url(r'^delete_user', views.UserDeletionView.as_view()),
url(r'^questionnaire', views.QuestionnaireView.as_view()),
]
......@@ -129,6 +129,21 @@ class DeploymentView(views.APIView):
return _api_state_response(request)
class QuestionnaireView(views.APIView):
def post(self, request):
task_item_id = request.query_params.get('id', '')
if task_item_id != '':
item = models.DeploymentTaskItem.objects.filter(id=int(task_item_id))
if item.exists():
item.first().questionnaire_answered(
answers=request.data,
)
else:
LOGGER.error('Received questionnaire answer for non existing item')
return _api_state_response(request)
class UserDeletionView(views.APIView):
def delete(self, request):
# this also logs out the user
......
......@@ -160,7 +160,8 @@ class RabbitMQInstance(SingletonModel):
)
def publish_to_webpage(self, user, msg):
LOGGER.debug('Signalling webpage of user %s', user)
# noise
# LOGGER.debug('Signalling webpage of user %s', user)
self._publish(
'update',
str(user.id),
......@@ -239,7 +240,7 @@ class User(AbstractUser):
@classmethod
def construct_from_userinfo(cls, userinfo, idp):
LOGGER.debug('User: constructing from %s', userinfo)
LOGGER.debug('Constructing User from:\n%s', userinfo)
if 'sub' not in userinfo:
raise Exception('Missing attribute in userinfo: sub')
......@@ -385,9 +386,12 @@ class Site(models.Model):
# tasks which are still to be executed on this site
@property
def tasks(self):
task_items = self.task_items.filter(state='pending')\
| self.task_items.filter(state='failed')\
| self.task_items.filter(state='answered')
return [item.task
for item
in self.task_items.all()]
in task_items]
class Service(models.Model):
......@@ -465,7 +469,7 @@ class SSHPublicKey(models.Model):
return self.name
# Deployment describes the credential state per user as it is supposed to be
# Deployment describes the credential state per user (and site) as it is supposed to be
#
# (exception: if is_active=False the ssh_keys contain the keys to be deployed
# if the deployment is reactivated)
......@@ -504,7 +508,6 @@ class Deployment(models.Model):
def get_deployment(cls, user, service):
query = cls.objects.filter(
user=user,
).filter(
service=service,
)
if query.exists():
......@@ -607,6 +610,15 @@ class Deployment(models.Model):
self._withdraw_key(key)
def invert_action(action):
if action == 'deploy':
return 'withdraw'
elif action == 'withdraw':
return 'deploy'
# DeploymentTask: knows:
# user, service, key, action
class DeploymentTask(models.Model):
ACTION_CHOICES = (
('deploy', 'deploy'),
......@@ -632,13 +644,47 @@ class DeploymentTask(models.Model):
on_delete=models.CASCADE,
)
# the inverse action of this task is requirred
# so we invert the task and manage its task_items accordingly
def invert_task(self):
LOGGER.debug(self.msg('inverting'))
previous_action = self.action
self.action = invert_action(previous_action)
self.save()
pending_sites = [task.site for task in self.task_items.all()]
self.chancel_items()
# sites which already executed the task
# we have to send them an order to rollback the changes
for site in self.deployment.service.site.all():
if site not in pending_sites:
deploy = DeploymentTaskItem(
task=self,
site=site,
user=self.deployment.user
)
deploy.save()
LOGGER.debug(deploy.msg('pending'))
@classmethod
def construct_deployment_task(cls, deployment, key):
# delete outstanding tasks which are made obsolete by this task
for withdrawal in deployment.withdrawals.filter(key=key):
LOGGER.debug(withdrawal.msg('now obsolete'))
withdrawal.delete()
# does a task exist for this key?
query = deployment.tasks.filter(key=key)
if query.exists():
if len(query) > 1:
raise Exception('Unexpected query result')
task = query.first()
if task.action == 'deploy':
raise Exception('Constructing deployment task when one already exists')
task.invert_task()
return task
else:
#create new task
task = cls(
action='deploy',
deployment=deployment,
......@@ -646,7 +692,7 @@ class DeploymentTask(models.Model):
user=deployment.user,
)
task.save()
LOGGER.debug(task.msg('generated'))
LOGGER.debug(task.msg('pending'))
# generate task items
for site in deployment.service.site.all():
......@@ -656,17 +702,27 @@ class DeploymentTask(models.Model):
user=deployment.user
)
deploy.save()
LOGGER.debug(deploy.msg('generated'))
LOGGER.debug(deploy.msg('pending'))
return task
@classmethod
def construct_withdrawal_task(cls, deployment, key):
# delete outstanding tasks which are made obsolete by this task
for withdrawal in deployment.deploys.filter(key=key):
LOGGER.debug(withdrawal.msg('now obsolete'))
withdrawal.delete()
# does a task exist for this key?
query = deployment.tasks.filter(key=key)
if query.exists():
if len(query) > 1:
raise Exception('Unexpected query result')
task = query.first()
if task.action == 'withdraw':
raise Exception('Constructing deployment task when one already exists')
task.invert_task()
return task
else:
# create a new task
task = cls(
action='withdraw',
deployment=deployment,
......@@ -674,7 +730,7 @@ class DeploymentTask(models.Model):
user=deployment.user,
)
task.save()
LOGGER.debug(task.msg('generated'))
LOGGER.debug(task.msg('pending'))
# generate task items
for site in deployment.service.site.all():
......@@ -684,19 +740,26 @@ class DeploymentTask(models.Model):
user=deployment.user
)
deploy.save()
LOGGER.debug(deploy.msg('generated'))
LOGGER.debug(deploy.msg('pending'))
return task
def chancel_items(self):
for item in self.task_items.all():
item.chancel()
def chancel(self):
self.chancel_items()
LOGGER.debug(self.msg("chanceled"))
self.delete()
@property
def service(self):
return self.deployment.service
def __str__(self):
return "{}:{}:{} - {}#{}".format(
return "{}:{}#{}".format(
self.deployment.service,
self.deployment.user,
self.key,
self.action,
self.id,
)
......@@ -725,42 +788,10 @@ class DeploymentTask(models.Model):
content,
)
# the client acked the receipt and execution of the task for his site
def item_finished(self, site):
item = self.task_items.get(site=site)
# LOGGER.debug(item.msg('done'))
item.delete()
def try_finished(self):
if not self.task_items.exists():
# finished sends its own message
self._finished()
else:
self.send_state_update()
# the client failed to execute the item
# the client can try again later
# we signal the user about the failure
def item_failed(self, site):
item = self.task_items.get(site=site)
item.state = 'failed'
item.save()
self.send_state_update()
# TODO implement
# the client failed to execute the item
# the client needs additional information from the user to try again
# we have to ask the user for data
def item_rejected(self, site):
item = self.task_items.get(site=site)
item.state = 'rejected'
item.save()
self.send_state_update()
# TODO implement
# maintenance after all task items are done
def _finished(self):
......@@ -789,6 +820,11 @@ class DeploymentTask(models.Model):
)
def questionnaire_default():
return {}
# DeploymentTaskItem: knows:
# user, service, key, action, _and_ site
class DeploymentTaskItem(models.Model):
task = models.ForeignKey(
DeploymentTask,
......@@ -811,6 +847,7 @@ class DeploymentTaskItem(models.Model):
('chanceled', 'Chanceled'),
('failed', 'Failed'),
('rejected', 'Rejected'),
('answered', 'Answered'),
)
state = models.CharField(
max_length=20,
......@@ -818,6 +855,12 @@ class DeploymentTaskItem(models.Model):
default='pending',
)
questionnaire = JSONField(
default=questionnaire_default,
null=True,
blank=True,
)
@property
def service(self):
return self.task.service
......@@ -830,6 +873,53 @@ class DeploymentTaskItem(models.Model):
def action(self):
return self.deployment.key
# the client acked the receipt and execution of the task for his site
def success(self):
task = self.task
LOGGER.debug(self.msg('success'))
self.delete()
task.send_state_update()
task.try_finished()
# the user changed the deployment
# chancel (delete) this task item
def chancel(self):
LOGGER.debug(self.msg('chanceled'))
self.delete()
# no update on chancel
# the next task will send an update
#task.send_state_update()
# the client failed to execute the item
# the client can try again later
# we signal the user about the failure
def failed(self, site):
LOGGER.debug(self.msg('failed'))
self.state = 'failed'
self.save()
self.task.send_state_update()
# the client failed to execute the item
# the client needs additional information from the user to try again
# we have to ask the user for data
def rejected(self, questionnaire=None):
LOGGER.debug(self.msg('rejected'))
self.state = 'rejected'
self.questionnaire = questionnaire
self.save()
self.task.send_state_update()
def questionnaire_answered(self, answers=None):
LOGGER.debug('%s %s', self.msg('answers'), answers)
self.state = 'answered'
self.questionnaire = answers
self.save()
def __str__(self):
return "{}@{}#{}".format(
self.task,
......@@ -838,7 +928,8 @@ class DeploymentTaskItem(models.Model):
)
def msg(self, msg):
return '[DeploymentTaskItem:{}] {}'.format(self, msg)
return '[Depl. TaskItem:{}] {}'.format(self, msg)
#
......
......@@ -15,6 +15,8 @@ TEST_USERINFO = {
'name': TEST_NAME,
'sub': TEST_SUB,
}
TEST_SERVICE = 'test_service'
TEST_SITE = 'test_site'
def setup_fixture():
......@@ -41,14 +43,14 @@ def setup_fixture():
user.save()
site = models.Site(
name=TEST_NAME,
description=TEST_NAME,
name=TEST_SITE,
description=TEST_SITE,
)
site.save()
service = models.Service(
name=TEST_NAME,
description=TEST_NAME,
name=TEST_SERVICE,
description=TEST_SERVICE,
)
service.save()
service.site.add(site)
......@@ -107,11 +109,12 @@ class DeploymentTest(TestCase):
def test_deployment(self):
user = models.User.objects.get(username=TEST_NAME)
key = models.SSHPublicKey.objects.get(name=TEST_NAME)
service = models.Service.objects.get(name=TEST_NAME)
site = models.Site.objects.get(name=TEST_NAME)
service = models.Service.objects.get(name=TEST_SERVICE)
deployment = models.Deployment.get_deployment(user, service)
self.assertIsNotNone(deployment)
# no tasks exist yet
self.assertFalse(deployment.deploys.exists())
self.assertFalse(deployment.withdrawals.exists())
......@@ -119,16 +122,23 @@ class DeploymentTest(TestCase):
deployment=deployment,
key=key,
)
# one deploy should exist
self.assertTrue(deployment.deploys.exists())
self.assertFalse(deployment.withdrawals.exists())
withdrawal_task = models.DeploymentTask.construct_withdrawal_task(
models.DeploymentTask.construct_withdrawal_task(
deployment=deployment,
key=key,
)
# one withdraw should exist
self.assertFalse(deployment.deploys.exists())
self.assertTrue(deployment.withdrawals.exists())
withdrawal_task.item_finished(site)
# "execute" the withdrawals
for task in deployment.withdrawals.all():
for item in task.task_items.all():
item.success()
self.assertFalse(deployment.deploys.exists())
self.assertFalse(deployment.withdrawals.exists())
Markdown is supported
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