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)
...
@@ -33,6 +33,5 @@ admin.site.register(models.Site)
admin
.
site
.
register
(
models
.
Service
)
admin
.
site
.
register
(
models
.
Service
)
admin
.
site
.
register
(
models
.
SSHPublicKey
)
admin
.
site
.
register
(
models
.
SSHPublicKey
)
admin
.
site
.
register
(
models
.
Deployment
)
admin
.
site
.
register
(
models
.
NewDeployment
)
admin
.
site
.
register
(
models
.
DeploymentState
)
admin
.
site
.
register
(
models
.
NewDeploymentStateItem
)
admin
.
site
.
register
(
models
.
DeploymentStateItem
)
feudal/backend/models/__init__.py
View file @
bcbed68b
...
@@ -12,14 +12,22 @@ from .users import User, SSHPublicKey
...
@@ -12,14 +12,22 @@ from .users import User, SSHPublicKey
LOGGER
=
getLogger
(
__name__
)
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
=
(
STATE_CHOICES
=
(
(
'deployment_pending'
,
'Deployment Pending'
),
(
DEPLOYMENT_PENDING
,
'Deployment Pending'
),
(
'removal_pending'
,
'Removal Pending'
),
(
REMOVAL_PENDING
,
'Removal Pending'
),
(
'deployed'
,
'Deployed'
),
(
DEPLOYED
,
'Deployed'
),
(
'not_deployed'
,
'Not Deployed'
),
(
NOT_DEPLOYED
,
'Not Deployed'
),
(
'questionnaire'
,
'Questionnaire'
),
(
QUESTIONNAIRE
,
'Questionnaire'
),
(
'failed'
,
'Failed'
),
(
FAILED
,
'Failed'
),
(
'rejected'
,
'Rejected'
),
(
REJECTED
,
'Rejected'
),
)
)
def
questionnaire_default
():
def
questionnaire_default
():
return
{}
return
{}
...
@@ -82,6 +90,26 @@ class Service(models.Model):
...
@@ -82,6 +90,26 @@ class Service(models.Model):
blank
=
True
,
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
):
def
__str__
(
self
):
return
self
.
name
return
self
.
name
...
@@ -98,27 +126,12 @@ class Service(models.Model):
...
@@ -98,27 +126,12 @@ class Service(models.Model):
try
:
try
:
deployment
=
user
.
deployments
.
get
(
group
=
group
)
deployment
=
user
.
deployments
.
get
(
group
=
group
)
deployment
.
service_added
(
self
)
deployment
.
service_added
(
self
)
except
Deployment
.
DoesNotExist
:
except
New
Deployment
.
DoesNotExist
:
LOGGER
.
error
(
'Inconsistency of group deployment'
)
LOGGER
.
error
(
'Inconsistency of group deployment'
)
raise
raise
# Deployment describes the supposed state of the users ssh keys at either:
class
NewDeployment
(
models
.
Model
):
# - 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
=
models
.
ForeignKey
(
User
,
User
,
related_name
=
'deployments'
,
related_name
=
'deployments'
,
...
@@ -139,279 +152,144 @@ class Deployment(models.Model):
...
@@ -139,279 +152,144 @@ class Deployment(models.Model):
null
=
True
,
null
=
True
,
blank
=
True
,
blank
=
True
,
)
)
ssh_keys
=
models
.
ManyToManyField
(
SSHPublicKey
,
# which state do we currently want to reach?
related_name
=
'deployments'
,
state_target
=
models
.
CharField
(
blank
=
True
,
max_length
=
50
,
choices
=
STATE_CHOICES
,
default
=
NOT_DEPLOYED
,
)
)
is_active
=
models
.
BooleanField
(
is_active
=
models
.
BooleanField
(
default
=
True
,
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
@
property
def
services
(
self
):
def
services
(
self
):
if
self
.
group
is
not
None
:
if
self
.
group
is
not
None
:
return
self
.
group
.
services
.
all
()
return
self
.
group
.
services
.
all
()
return
None
return
[
self
.
service
]
# only used when group is not None and service is None
@
property
@
property
def
sites
(
self
):
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
@
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
:
try
:
dep
=
None
if
service
is
not
None
:
if
service
is
not
None
:
return
cls
.
objects
.
get
(
dep
=
cls
.
objects
.
get
(
user
=
user
,
user
=
user
,
service
=
service
,
service
=
service
,
)
)
LOGGER
.
debug
(
'service hit'
)
elif
group
is
not
None
:
elif
group
is
not
None
:
return
cls
.
objects
.
get
(
dep
=
cls
.
objects
.
get
(
user
=
user
,
user
=
user
,
group
=
group
,
group
=
group
,
)
)
else
:
LOGGER
.
debug
(
'group hit'
)
raise
ValueError
(
'Unable to create Deployment without service and group'
)
return
dep
except
cls
.
DoesNotExist
:
except
cls
.
DoesNotExist
:
deployment
=
None
deployment
=
None
if
service
is
not
None
:
if
service
is
not
None
and
group
is
None
:
deployment
=
cls
(
deployment
=
cls
(
user
=
user
,
user
=
user
,
service
=
service
,
service
=
service
,
)
)
elif
group
is
not
None
:
elif
group
is
not
None
:
deployment
=
cls
(
deployment
=
cls
(
user
=
user
,
user
=
user
,
group
=
group
,
group
=
group
,
)
)
if
not
group
.
services
.
exists
():
LOGGER
.
info
(
deployment
.
msg
(
'No services for group'
))
deployment
.
save
()
deployment
.
save
()
deployment
.
create_state_items
(
site
=
site
)
LOGGER
.
debug
(
deployment
.
msg
(
'created'
))
LOGGER
.
debug
(
deployment
.
msg
(
'created'
))
return
deployment
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
def
user_deploy
(
self
):
self
.
_set_target
(
'deployed'
)
# if we have no states we have nothing to do
for
item
in
self
.
state_items
.
all
():
return
self
.
state_target
item
.
user_deploy
()
self
.
publish_to_client
()
@
property
# each state item publishes its state to the user
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
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
):
def
service_added
(
self
,
service
):
LOGGER
.
debug
(
self
.
msg
(
'Adding service {}'
.
format
(
service
)))
LOGGER
.
debug
(
self
.
msg
(
'Adding service {}'
.
format
(
service
)))
for
site
in
service
.
site
.
all
():
for
site
in
service
.
site
.
all
():
# create new DeploymentStateItems
# create new
New
DeploymentStateItems
item
=
DeploymentStateItem
.
get_state_item
(
item
=
New
DeploymentStateItem
.
get_state_item
(
parent
=
self
,
parent
=
self
,
site
=
site
,
site
=
site
,
service
=
service
,
service
=
service
,
...
@@ -420,25 +298,11 @@ class DeploymentState(models.Model):
...
@@ -420,25 +298,11 @@ class DeploymentState(models.Model):
if
self
.
state_target
==
'deployed'
:
if
self
.
state_target
==
'deployed'
:
item
.
user_deploy
()
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
):
def
publish_to_client
(
self
):
# mitigating circular dependencies here
# mitigating circular dependencies here
from
.serializers.clients
import
DeploymentS
tateS
erializer
from
.serializers.clients
import
New
DeploymentSerializer
data
=
DeploymentS
tateS
erializer
(
self
).
data
data
=
New
DeploymentSerializer
(
self
).
data
data
[
'credentials'
]
=
self
.
credentials
data
[
'credentials'
]
=
self
.
user_
credentials
msg
=
dumps
(
data
)
msg
=
dumps
(
data
)
if
self
.
service
is
not
None
:
if
self
.
service
is
not
None
:
...
@@ -452,7 +316,7 @@ class DeploymentState(models.Model):
...
@@ -452,7 +316,7 @@ class DeploymentState(models.Model):
msg
,
msg
,
)
)
else
:
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
# update the state of the remote webpage
def
publish_to_user
(
self
):
def
publish_to_user
(
self
):
...
@@ -467,7 +331,7 @@ class DeploymentState(models.Model):
...
@@ -467,7 +331,7 @@ class DeploymentState(models.Model):
)
)
def
msg
(
self
,
msg
):
def
msg
(
self
,
msg
):
return
'[D
State
:{}] {}'
.
format
(
self
,
msg
)
return
'[D
eploy
:{}] {}'
.
format
(
self
,
msg
)
def
_set_target
(
self
,
target
):
def
_set_target
(
self
,
target
):
self
.
state_target
=
target
self
.
state_target
=
target
...
@@ -478,21 +342,19 @@ class DeploymentState(models.Model):
...
@@ -478,21 +342,19 @@ class DeploymentState(models.Model):
if
self
.
service
is
not
None
:
if
self
.
service
is
not
None
:
return
'{}:{}#{}'
.
format
(
return
'{}:{}#{}'
.
format
(
self
.
service
,
self
.
service
,
self
.
key
.
name
,
self
.
user
,
self
.
id
,
self
.
id
,
)
)
return
'{}:{}#{}'
.
format
(
return
'{}:{}#{}'
.
format
(
self
.
group
,
self
.
group
,
self
.
key
.
name
,
self
.
user
,
self
.
id
,
self
.
id
,
)
)
# DeploymentStateItem: knows:
class
NewDeploymentStateItem
(
models
.
Model
):
# user, service, key, state_target, _and_ site
class
DeploymentStateItem
(
models
.
Model
):
parent
=
models
.
ForeignKey
(
parent
=