Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
feudal
feudalBackend
Commits
bcbed68b
Commit
bcbed68b
authored
Aug 13, 2018
by
Lukas Burgey
Browse files
Refactor the deployment hierarchy and adapt tests
parent
17501f71
Changes
10
Hide whitespace changes
Inline
Side-by-side
feudal/backend/admin.py
View file @
bcbed68b
...
...
@@ -33,6 +33,5 @@ admin.site.register(models.Site)
admin
.
site
.
register
(
models
.
Service
)
admin
.
site
.
register
(
models
.
SSHPublicKey
)
admin
.
site
.
register
(
models
.
Deployment
)
admin
.
site
.
register
(
models
.
DeploymentState
)
admin
.
site
.
register
(
models
.
DeploymentStateItem
)
admin
.
site
.
register
(
models
.
NewDeployment
)
admin
.
site
.
register
(
models
.
NewDeploymentStateItem
)
feudal/backend/models/__init__.py
View file @
bcbed68b
...
...
@@ -12,14 +12,22 @@ from .users import User, SSHPublicKey
LOGGER
=
getLogger
(
__name__
)
DEPLOYMENT_PENDING
=
'deployment_pending'
REMOVAL_PENDING
=
'removal_pending'
NOT_DEPLOYED
=
'not_deployed'
DEPLOYED
=
'deployed'
QUESTIONNAIRE
=
'questionnaire'
FAILED
=
'failed'
REJECTED
=
'rejected'
STATE_CHOICES
=
(
(
'deployment_pending'
,
'Deployment Pending'
),
(
'removal_pending'
,
'Removal Pending'
),
(
'deployed'
,
'Deployed'
),
(
'not_deployed'
,
'Not Deployed'
),
(
'questionnaire'
,
'Questionnaire'
),
(
'failed'
,
'Failed'
),
(
'rejected'
,
'Rejected'
),
(
DEPLOYMENT_PENDING
,
'Deployment Pending'
),
(
REMOVAL_PENDING
,
'Removal Pending'
),
(
DEPLOYED
,
'Deployed'
),
(
NOT_DEPLOYED
,
'Not Deployed'
),
(
QUESTIONNAIRE
,
'Questionnaire'
),
(
FAILED
,
'Failed'
),
(
REJECTED
,
'Rejected'
),
)
def
questionnaire_default
():
return
{}
...
...
@@ -82,6 +90,26 @@ class Service(models.Model):
blank
=
True
,
)
@
classmethod
def
get_service
(
cls
,
name
,
description
=
''
,
sites
=
None
,
groups
=
None
):
try
:
return
cls
.
objects
.
get
(
name
=
name
)
except
cls
.
DoesNotExist
:
service
=
cls
(
name
=
name
,
description
=
description
,
)
service
.
save
()
if
sites
is
not
None
:
for
site
in
sites
:
service
.
site
.
add
(
site
)
if
groups
is
not
None
:
for
group
in
groups
:
service
.
groups
.
add
(
group
)
return
service
def
__str__
(
self
):
return
self
.
name
...
...
@@ -98,27 +126,12 @@ class Service(models.Model):
try
:
deployment
=
user
.
deployments
.
get
(
group
=
group
)
deployment
.
service_added
(
self
)
except
Deployment
.
DoesNotExist
:
except
New
Deployment
.
DoesNotExist
:
LOGGER
.
error
(
'Inconsistency of group deployment'
)
raise
# 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
):
class
NewDeployment
(
models
.
Model
):
user
=
models
.
ForeignKey
(
User
,
related_name
=
'deployments'
,
...
...
@@ -139,279 +152,144 @@ class Deployment(models.Model):
null
=
True
,
blank
=
True
,
)
ssh_keys
=
models
.
ManyToManyField
(
SSHPublicKey
,
related_name
=
'deployments'
,
blank
=
True
,
# which state do we currently want to reach?
state_target
=
models
.
CharField
(
max_length
=
50
,
choices
=
STATE_CHOICES
,
default
=
NOT_DEPLOYED
,
)
is_active
=
models
.
BooleanField
(
default
=
True
,
)
# only used when group is not None and service is None
# credentials provided by the backend to the clients
@
property
def
user_credentials
(
self
):
return
self
.
user
.
credentials
@
property
def
state
(
self
):
if
self
.
state_items
.
exists
():
_state
=
''
for
state
in
self
.
state_items
.
all
():
LOGGER
.
debug
(
'FOO: %s'
,
state
)
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
@
property
def
services
(
self
):
if
self
.
group
is
not
None
:
return
self
.
group
.
services
.
all
()
return
None
return
[
self
.
service
]
# only used when group is not None and service is None
@
property
def
sites
(
self
):
return
Site
.
objects
.
filter
(
services__groups
=
self
.
group
).
distinct
()
return
[
service
.
site
for
service
in
self
.
services
]
def
create_state_items
(
self
,
site
=
None
):
for
service
in
self
.
services
:
LOGGER
.
debug
(
'create_state_items: creating NewDeploymentStateItem for service %s at sites %s'
,
service
,
service
.
site
.
all
())
if
site
is
None
:
if
not
service
.
site
.
exists
():
raise
ValueError
(
'Cannot create state item for service without site'
)
for
service_site
in
service
.
site
.
all
():
# LOGGER.debug('create_state_items: creating NewDeploymentStateItems for service %s at site %s', service, service_site)
NewDeploymentStateItem
.
get_state_item
(
parent
=
self
,
site
=
service_site
,
service
=
service
,
).
save
()
else
:
NewDeploymentStateItem
.
get_state_item
(
parent
=
self
,
site
=
site
,
service
=
service
,
).
save
()
# 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
):
def
get_deployment
(
cls
,
user
,
service
=
None
,
group
=
None
,
site
=
None
):
if
service
is
None
and
group
is
None
:
raise
ValueError
(
'get_deployment needs a service or a group'
)
try
:
dep
=
None
if
service
is
not
None
:
return
cls
.
objects
.
get
(
dep
=
cls
.
objects
.
get
(
user
=
user
,
service
=
service
,
)
LOGGER
.
debug
(
'service hit'
)
elif
group
is
not
None
:
return
cls
.
objects
.
get
(
dep
=
cls
.
objects
.
get
(
user
=
user
,
group
=
group
,
)
else
:
raise
ValueError
(
'Unable to create Deployment without service and group'
)
LOGGER
.
debug
(
'group hit'
)
return
dep
except
cls
.
DoesNotExist
:
deployment
=
None
if
service
is
not
None
:
if
service
is
not
None
and
group
is
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
()
deployment
.
create_state_items
(
site
=
site
)
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'
,
)
# credentials provided by the backend to the clients
@
property
def
credentials
(
self
):
# FIXME hacky
ssh_keys
=
[{
'name'
:
key
.
name
,
'value'
:
key
.
key
}
for
key
in
self
.
user
.
ssh_keys
.
all
()]
return
{
'ssh_key'
:
ssh_keys
}
@
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
()
def
user_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
return
state
def
user_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
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
(
# create new
New
DeploymentStateItems
item
=
New
DeploymentStateItem
.
get_state_item
(
parent
=
self
,
site
=
site
,
service
=
service
,
...
...
@@ -420,25 +298,11 @@ class DeploymentState(models.Model):
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
DeploymentS
tateS
erializer
data
=
DeploymentS
tateS
erializer
(
self
).
data
data
[
'credentials'
]
=
self
.
credentials
from
.serializers.clients
import
New
DeploymentSerializer
data
=
New
DeploymentSerializer
(
self
).
data
data
[
'credentials'
]
=
self
.
user_
credentials
msg
=
dumps
(
data
)
if
self
.
service
is
not
None
:
...
...
@@ -452,7 +316,7 @@ class DeploymentState(models.Model):
msg
,
)
else
:
LOGGER
.
error
(
'Deployment as neither a group or a service'
)
LOGGER
.
error
(
'Deployment
h
as neither a group or a service'
)
# update the state of the remote webpage
def
publish_to_user
(
self
):
...
...
@@ -467,7 +331,7 @@ class DeploymentState(models.Model):
)
def
msg
(
self
,
msg
):
return
'[D
State
:{}] {}'
.
format
(
self
,
msg
)
return
'[D
eploy
:{}] {}'
.
format
(
self
,
msg
)
def
_set_target
(
self
,
target
):
self
.
state_target
=
target
...
...
@@ -478,21 +342,19 @@ class DeploymentState(models.Model):
if
self
.
service
is
not
None
:
return
'{}:{}#{}'
.
format
(
self
.
service
,
self
.
key
.
name
,
self
.
user
,
self
.
id
,
)
return
'{}:{}#{}'
.
format
(
self
.
group
,
self
.
key
.
name
,
self
.
user
,
self
.
id
,
)
# DeploymentStateItem: knows:
# user, service, key, state_target, _and_ site
class
DeploymentStateItem
(
models
.
Model
):
class
NewDeploymentStateItem
(
models
.
Model
):
parent
=
models
.
ForeignKey
(
Deployment
State
,
New
Deployment
,
related_name
=
'state_items'
,
on_delete
=
models
.
CASCADE
,
)
...
...
@@ -509,7 +371,7 @@ class DeploymentStateItem(models.Model):
state
=
models
.
CharField
(
max_length
=
50
,
choices
=
STATE_CHOICES
,
default
=
'deployment_pending'
,
default
=
NOT_DEPLOYED
,
)
# message for the user
...
...
@@ -537,8 +399,8 @@ class DeploymentStateItem(models.Model):
return
self
.
parent
.
user
@
property
def
key
(
self
):
return
self
.
parent
.
key
def
user_credentials
(
self
):
return
self
.
parent
.
credentials
@
property
def
group
(
self
):
...
...
@@ -547,10 +409,14 @@ class DeploymentStateItem(models.Model):
@
classmethod
def
get_state_item
(
cls
,
parent
=
None
,
site
=
None
,
service
=
None
):
try
:
return
parent
.
state_items
.
get
(
item
=
cls
.
objects
.
get
(
parent
=
parent
,
site
=
site
,
service
=
service
,
)
LOGGER
.
debug
(
'get_state_item hit'
)
return
item
except
cls
.
DoesNotExist
:
item
=
cls
(
parent
=
parent
,
...
...
@@ -656,16 +522,14 @@ class DeploymentStateItem(models.Model):
def
__str__
(
self
):
if
self
.
group
is
not
None
:
return
'{}:{}@{}
:{}
#{}'
.
format
(
return
'{}:{}@{}#{}'
.
format
(
self
.
group
,
self
.
service
,
self
.
site
,
self
.
key
.
name
,
self
.
id
,
)
return
'{}:@{}
:{}
#{}'
.
format
(
return
'{}:@{}#{}'
.
format
(
self
.
service
,
self
.
site
,
self
.
key
.
name
,
self
.
id
,
)
feudal/backend/models/serializers/__init__.py
View file @
bcbed68b
...
...
@@ -37,5 +37,5 @@ class SSHPublicKeyRefSerializer(serializers.ModelSerializer):
# "exports"
from
.webpage
import
DeploymentS
tateS
erializer
from
.webpage
import
New
DeploymentSerializer
from
.clients
import
RabbitMQInstanceSerializer
feudal/backend/models/serializers/clients.py
View file @
bcbed68b
...
...
@@ -5,7 +5,7 @@ from django_mysql.models import JSONField
from
rest_framework
import
serializers
from
...
import
models
from
.
import
GroupSerializer
,
SSHPublicKeySerializer
from
.
import
GroupSerializer
class
ServiceSerializer
(
serializers
.
ModelSerializer
):
...
...
@@ -25,51 +25,33 @@ class UserSerializer(serializers.ModelSerializer):
fields
=
[
'email'
,
'groups'
,
'userinfo'
]
class
DeploymentSerializer
(
serializers
.
ModelSerializer
):