Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
feudal
feudalBackend
Commits
99fba34f
Commit
99fba34f
authored
Jun 15, 2018
by
Lukas Burgey
Browse files
Implement the questionnaire logic
parent
d45e4e76
Changes
8
Hide whitespace changes
Inline
Side-by-side
.gitignore
View file @
99fba34f
...
...
@@ -5,3 +5,4 @@ db.cnf
static
deployment
deploy
runtest
django_backend/backend/auth/v1/client_views.py
View file @
99fba34f
...
...
@@ -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,24 +145,26 @@ def resource_endpoint(request):
_webpage_client_userid
(
request
)
and
_resource_authorized_webpage_client
(
request
)
):
LOGGER
.
debug
(
'Granted %s access to resource %s %s to client'
,
permission
,
resource
,
name
,
)
if
CLIENT_DEBUGGING
:
LOGGER
.
debug
(
'Granted %s access to resource %s %s to client'
,
permission
,
resource
,
name
,
)
return
ALLOW
if
(
_apiclient_valid
(
request
)
and
_resource_authorized_apiclient
(
request
)
):
LOGGER
.
debug
(
'Granted %s access to resource %s %s to client'
,
permission
,
resource
,
name
,
)
if
CLIENT_DEBUGGING
:
LOGGER
.
debug
(
'Granted %s access to resource %s %s to client'
,
permission
,
resource
,
name
,
)
return
ALLOW
LOGGER
.
error
(
...
...
@@ -187,12 +190,13 @@ def topic_endpoint(request):
routing_key
==
webpage_client_userid
and
not
'write'
in
permission
):
LOGGER
.
debug
(
'Granted %s access to %s %s to client'
,
permission
,
resource
,
routing_key
,
)
if
CLIENT_DEBUGGING
:
LOGGER
.
debug
(
'Granted %s access to %s %s to client'
,
permission
,
resource
,
routing_key
,
)
return
ALLOW
LOGGER
.
error
(
...
...
django_backend/backend/clientapi/views.py
View file @
99fba34f
...
...
@@ -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
Site
Serializer
,
ServiceSerializer
,
RabbitMQInstanceSerializer
from
.serializers
import
DeploymentTask
Serializer
,
ServiceSerializer
,
RabbitMQInstanceSerializer
from
..models
import
RabbitMQInstance
LOGGER
=
logging
.
getLogger
(
__name__
)
...
...
@@ -13,12 +13,12 @@ LOGGER = logging.getLogger(__name__)
AUTHENTICATION_CLASSES
=
(
BasicAuthentication
,
)
class
DeploymentsView
(
generics
.
Retrieve
APIView
):
class
DeploymentsView
(
generics
.
List
APIView
):
authentication_classes
=
AUTHENTICATION_CLASSES
serializer_class
=
Site
Serializer
serializer_class
=
DeploymentTask
Serializer
def
get_
objec
t
(
self
):
return
self
.
request
.
user
.
site
def
get_
queryse
t
(
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
)
...
...
django_backend/backend/frontend/serializers.py
View file @
99fba34f
...
...
@@ -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'
,
]
...
...
django_backend/backend/frontend/urls.py
View file @
99fba34f
...
...
@@ -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
()),
]
django_backend/backend/frontend/views.py
View file @
99fba34f
...
...
@@ -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
...
...
django_backend/backend/models.py
View file @
99fba34f
...
...
@@ -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: c
onstructing from
%s'
,
userinfo
)
LOGGER
.
debug
(
'
C
onstructing
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,71 +644,122 @@ 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
):
# d
elete outstanding tasks which are made obsolete by
this
task
for
withdrawal
in
deployment
.
withdrawal
s
.
filter
(
key
=
key
)
:
LOGGER
.
debug
(
withdrawal
.
msg
(
'now obsolete'
))
withdrawal
.
delete
()
task
=
cls
(
action
=
'deploy'
,
deployment
=
deploy
ment
,
key
=
key
,
user
=
deployment
.
user
,
)
task
.
save
()
LOGGER
.
debug
(
task
.
msg
(
'generated'
))
# generate task items
for
site
in
deployment
.
service
.
site
.
all
():
deploy
=
DeploymentTaskItem
(
task
=
task
,
site
=
site
,
user
=
deployment
.
user
# d
oes a task exist for
this
key?
query
=
deployment
.
task
s
.
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
,
key
=
key
,
user
=
deployment
.
user
,
)
deploy
.
save
()
LOGGER
.
debug
(
deploy
.
msg
(
'generated'
))
task
.
save
()
LOGGER
.
debug
(
task
.
msg
(
'pending'
))
# generate task items
for
site
in
deployment
.
service
.
site
.
all
():
deploy
=
DeploymentTaskItem
(
task
=
task
,
site
=
site
,
user
=
deployment
.
user
)
deploy
.
save
()
LOGGER
.
debug
(
deploy
.
msg
(
'pending'
))
return
task
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
()
task
=
cls
(
action
=
'withdraw'
,
deployment
=
deployment
,
key
=
key
,
user
=
deployment
.
user
,
)
task
.
save
()
LOGGER
.
debug
(
task
.
msg
(
'generated'
))
# generate task items
for
site
in
deployment
.
service
.
site
.
all
():
deploy
=
DeploymentTaskItem
(
task
=
task
,
site
=
site
,
user
=
deployment
.
user
)
deploy
.
save
()
LOGGER
.
debug
(
deploy
.
msg
(
'generated'
))
# 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'
)
return
task
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
,
key
=
key
,
user
=
deployment
.
user
,
)
task
.
save
()
LOGGER
.
debug
(
task
.
msg
(
'pending'
))
# generate task items
for
site
in
deployment
.
service
.
site
.
all
():
deploy
=
DeploymentTaskItem
(
task
=
task
,
site
=
site
,
user
=
deployment
.
user
)
deploy
.
save
()
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
)
#
...
...
django_backend/backend/test_models.py
View file @
99fba34f
...
...
@@ -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_
NAM
E
,
description
=
TEST_
NAM
E
,
name
=
TEST_
SIT
E
,
description
=
TEST_
SIT
E
,
)
site
.
save
()
service
=
models
.
Service
(
name
=
TEST_
NAM
E
,
description
=
TEST_
NAM
E
,
name
=
TEST_
SERVIC
E
,
description
=
TEST_
SERVIC
E
,
)
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
,