Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
feudalBackend
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
16
Issues
16
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Incidents
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
feudal
feudalBackend
Commits
d84890f9
Commit
d84890f9
authored
Nov 28, 2018
by
Lukas Burgey
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement simultaneous VO and Service Deployments
Makes Deployment and DeploymentState many to many.
parent
5197f6c6
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
267 additions
and
143 deletions
+267
-143
feudal/backend/migrations/0028_auto_20181128_1400.py
feudal/backend/migrations/0028_auto_20181128_1400.py
+39
-0
feudal/backend/models/__init__.py
feudal/backend/models/__init__.py
+8
-4
feudal/backend/models/deployments.py
feudal/backend/models/deployments.py
+177
-100
feudal/backend/models/serializers/__init__.py
feudal/backend/models/serializers/__init__.py
+12
-11
feudal/backend/models/test_models.py
feudal/backend/models/test_models.py
+12
-12
feudal/backend/models/users.py
feudal/backend/models/users.py
+6
-10
feudal/backend/views/clients.py
feudal/backend/views/clients.py
+6
-5
feudal/backend/views/user_rest.py
feudal/backend/views/user_rest.py
+7
-1
No files found.
feudal/backend/migrations/0028_auto_20181128_1400.py
0 → 100644
View file @
d84890f9
# Generated by Django 2.1.3 on 2018-11-28 13:00
from
django.conf
import
settings
from
django.db
import
migrations
,
models
import
django.db.models.deletion
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'backend'
,
'0027_auto_20181126_1314'
),
]
operations
=
[
migrations
.
RemoveField
(
model_name
=
'deploymentstate'
,
name
=
'parent'
,
),
migrations
.
AddField
(
model_name
=
'deploymentstate'
,
name
=
'deployments'
,
field
=
models
.
ManyToManyField
(
related_name
=
'states'
,
to
=
'backend.Deployment'
),
),
migrations
.
AlterField
(
model_name
=
'deploymentstate'
,
name
=
'service'
,
field
=
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'states'
,
to
=
'backend.Service'
),
),
migrations
.
AlterField
(
model_name
=
'deploymentstate'
,
name
=
'site'
,
field
=
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'states'
,
to
=
'backend.Site'
),
),
migrations
.
AlterField
(
model_name
=
'deploymentstate'
,
name
=
'user'
,
field
=
models
.
ForeignKey
(
blank
=
True
,
null
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
SET_NULL
,
related_name
=
'states'
,
to
=
settings
.
AUTH_USER_MODEL
),
),
]
feudal/backend/models/__init__.py
View file @
d84890f9
...
@@ -43,15 +43,19 @@ class Site(models.Model):
...
@@ -43,15 +43,19 @@ class Site(models.Model):
# ignores orphaned deployment states
# ignores orphaned deployment states
dep_states
=
[
dep_states
=
[
state
state
for
state
in
self
.
state
_item
s
.
all
()
for
state
in
self
.
states
.
all
()
if
(
if
(
state
.
is_pending
or
state
.
is_credential_pending
state
.
is_pending
or
state
.
is_credential_pending
)
and
state
.
parent
is
not
None
)
and
state
.
deployments
.
exists
()
]
]
# make the deployments unique here
# make the deployments unique here
for
item
in
dep_states
:
for
state
in
dep_states
:
deployments
[
item
.
parent
.
id
]
=
item
.
parent
# we filter for deployments of the same state_target here
# if we have multiple deployments per deployment state this causes only the
# deployments with the same state_target to be pending
for
deployment
in
state
.
deployments
.
filter
(
state_target
=
state
.
state_target
):
deployments
[
deployment
.
id
]
=
deployment
return
deployments
.
values
()
return
deployments
.
values
()
...
...
feudal/backend/models/deployments.py
View file @
d84890f9
...
@@ -4,6 +4,7 @@ from logging import getLogger
...
@@ -4,6 +4,7 @@ from logging import getLogger
from
django.conf
import
settings
from
django.conf
import
settings
from
django.db
import
models
from
django.db
import
models
from
django.db.models
import
Q
from
django_mysql.models
import
JSONField
from
django_mysql.models
import
JSONField
from
polymorphic.models
import
PolymorphicModel
from
polymorphic.models
import
PolymorphicModel
...
@@ -86,9 +87,9 @@ class Deployment(PolymorphicModel):
...
@@ -86,9 +87,9 @@ class Deployment(PolymorphicModel):
@
property
@
property
def
state
(
self
):
def
state
(
self
):
if
self
.
state
_item
s
.
exists
():
if
self
.
states
.
exists
():
_state
=
''
_state
=
''
for
state
in
self
.
state
_item
s
.
all
():
for
state
in
self
.
states
.
all
():
if
_state
==
''
:
if
_state
==
''
:
_state
=
state
.
state
_state
=
state
.
state
elif
_state
!=
state
.
state
:
elif
_state
!=
state
.
state
:
...
@@ -104,44 +105,69 @@ class Deployment(PolymorphicModel):
...
@@ -104,44 +105,69 @@ class Deployment(PolymorphicModel):
return
self
.
state_target
==
self
.
state
return
self
.
state_target
==
self
.
state
def
_set_target
(
self
,
target
):
def
_set_target
(
self
,
target
):
LOGGER
.
debug
(
self
.
msg
(
'Target changed to '
+
target
))
if
str
(
self
.
state_target
)
==
str
(
target
):
return
self
.
state_target
=
target
LOGGER
.
debug
(
self
.
msg
(
'Target: {} -> {} '
.
format
(
self
.
state_target
,
target
)))
for
credential_state
in
CredentialState
.
objects
.
filter
(
self
.
state_target
=
target
target__parent
=
self
,
):
credential_state
.
set_target
(
target
)
self
.
save
()
self
.
save
()
# FIXME this is breaking things when one wants deploy and another gets a user_remove
# # set the target to all credentials
# for state in self.states.all():
# state.assure_credential_states_exist()
# for credential_state in CredentialState.objects.filter(
# target__deployments=self,
# ):
# credential_state.set_target(target)
# Deployment.user_deploy
def
user_deploy
(
self
):
def
user_deploy
(
self
):
self
.
_set_target
(
'deployed'
)
LOGGER
.
debug
(
self
.
msg
(
'user_deploy'
))
for
item
in
self
.
state_items
.
all
():
self
.
_set_target
(
DEPLOYED
)
# states which are not DEPLOYED
for
item
in
self
.
states
.
filter
(
~
Q
(
state
=
DEPLOYED
)):
item
.
user_deploy
()
item
.
user_deploy
()
self
.
publish_to_client
()
# each state item publishes its state to the user
self
.
publish
()
# Deployment.user_remove
def
user_remove
(
self
):
def
user_remove
(
self
):
for
item
in
self
.
state_items
.
all
():
LOGGER
.
debug
(
self
.
msg
(
'user_remove'
))
if
item
.
state
!=
NOT_DEPLOYED
:
can_publish
=
False
item
.
user_remove
()
self
.
_set_target
(
NOT_DEPLOYED
)
self
.
_set_target
(
NOT_DEPLOYED
)
self
.
publish_to_client
()
# each state item publishes its state to the user
# states which are not NOT_DEPLOYED
for
item
in
self
.
states
.
filter
(
~
Q
(
state
=
NOT_DEPLOYED
)):
if
item
.
user_remove
():
can_publish
=
True
# we only publish to the clients if allowed
# we are not allowed to publish a removal if another deployment for our
# DeploymentStates exists and has the target DEPLOYED
if
can_publish
:
self
.
publish
()
else
:
self
.
publish_to_user
()
def
user_credential_added
(
self
,
key
):
def
user_credential_added
(
self
,
key
):
for
item
in
self
.
state
_item
s
.
all
():
for
item
in
self
.
states
.
all
():
item
.
user_credential_added
(
key
)
item
.
user_credential_added
(
key
)
self
.
publish
()
self
.
publish
()
def
user_credential_removed
(
self
,
key
):
def
user_credential_removed
(
self
,
key
):
for
item
in
self
.
state
_item
s
.
all
():
for
item
in
self
.
states
.
all
():
item
.
user_credential_removed
(
key
)
item
.
user_credential_removed
(
key
)
self
.
publish
()
self
.
publish
()
def
publish_to_client
(
self
):
def
publish_to_client
(
self
):
LOGGER
.
debug
(
self
.
msg
(
'publish_to_client'
))
from
.serializers
import
clients
from
.serializers
import
clients
data
=
clients
.
DeploymentSerializer
(
self
).
data
data
=
clients
.
DeploymentSerializer
(
self
).
data
msg
=
dumps
(
data
)
msg
=
dumps
(
data
)
...
@@ -156,6 +182,8 @@ class Deployment(PolymorphicModel):
...
@@ -156,6 +182,8 @@ class Deployment(PolymorphicModel):
if
self
.
user
is
None
:
if
self
.
user
is
None
:
return
return
LOGGER
.
debug
(
self
.
msg
(
'publish_to_user'
))
from
.
import
serializers
from
.
import
serializers
msg
=
dumps
({
msg
=
dumps
({
'deployment'
:
serializers
.
DeploymentSerializer
(
self
).
data
,
'deployment'
:
serializers
.
DeploymentSerializer
(
self
).
data
,
...
@@ -198,13 +226,13 @@ class VODeployment(Deployment):
...
@@ -198,13 +226,13 @@ class VODeployment(Deployment):
def
routing_key
(
self
):
def
routing_key
(
self
):
return
self
.
vo
.
name
return
self
.
vo
.
name
def
create_state
_item
s
(
self
):
def
create_states
(
self
):
for
service
in
self
.
services
:
for
service
in
self
.
services
:
DeploymentState
.
get_state_item
(
DeploymentState
.
get_state_item
(
self
,
self
.
user
,
self
.
user
,
service
.
site
,
service
.
site
,
service
,
service
,
deployments
=
[
self
],
)
)
@
classmethod
@
classmethod
...
@@ -214,7 +242,7 @@ class VODeployment(Deployment):
...
@@ -214,7 +242,7 @@ class VODeployment(Deployment):
user
=
user
,
user
=
user
,
vo
=
vo
,
vo
=
vo
,
)
)
deployment
.
create_state
_item
s
()
deployment
.
create_states
()
return
deployment
return
deployment
...
@@ -225,7 +253,7 @@ class VODeployment(Deployment):
...
@@ -225,7 +253,7 @@ class VODeployment(Deployment):
)
)
deployment
.
save
()
deployment
.
save
()
deployment
.
create_state
_item
s
()
deployment
.
create_states
()
LOGGER
.
debug
(
deployment
.
msg
(
'Created'
))
LOGGER
.
debug
(
deployment
.
msg
(
'Created'
))
return
deployment
return
deployment
...
@@ -233,12 +261,12 @@ class VODeployment(Deployment):
...
@@ -233,12 +261,12 @@ class VODeployment(Deployment):
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
)))
item
=
DeploymentState
.
get_state_item
(
item
=
DeploymentState
.
get_state_item
(
self
,
self
.
user
,
self
.
user
,
service
.
site
,
service
.
site
,
service
,
service
,
deployments
=
[
self
],
)
)
if
s
elf
.
state_target
==
'deployed'
:
if
s
tr
(
self
.
state_target
)
==
'deployed'
:
item
.
user_deploy
()
item
.
user_deploy
()
def
service_removed
(
self
,
service
):
def
service_removed
(
self
,
service
):
...
@@ -272,12 +300,11 @@ class ServiceDeployment(Deployment):
...
@@ -272,12 +300,11 @@ class ServiceDeployment(Deployment):
return
self
.
service
.
name
return
self
.
service
.
name
def
create_state_item
(
self
):
def
create_state_item
(
self
):
LOGGER
.
debug
(
'create_state_item: creating DeploymentState for service %s at site %s'
,
self
.
service
,
self
.
service
.
site
)
DeploymentState
.
get_state_item
(
DeploymentState
.
get_state_item
(
self
,
self
.
user
,
self
.
user
,
self
.
service
.
site
,
self
.
service
.
site
,
self
.
service
,
self
.
service
,
deployments
=
[
self
],
)
)
@
classmethod
@
classmethod
...
@@ -315,18 +342,14 @@ class ServiceDeployment(Deployment):
...
@@ -315,18 +342,14 @@ class ServiceDeployment(Deployment):
class
DeploymentState
(
models
.
Model
):
class
DeploymentState
(
models
.
Model
):
parentless
=
ValueError
(
'Tried to access parent of parentless deployment state'
)
deployments
=
models
.
ManyToManyField
(
parent
=
models
.
ForeignKey
(
Deployment
,
Deployment
,
related_name
=
'state_items'
,
related_name
=
'states'
,
on_delete
=
models
.
SET_NULL
,
null
=
True
)
)
user
=
models
.
ForeignKey
(
user
=
models
.
ForeignKey
(
User
,
User
,
related_name
=
'state
_item
s'
,
related_name
=
'states'
,
on_delete
=
models
.
SET_NULL
,
on_delete
=
models
.
SET_NULL
,
blank
=
True
,
blank
=
True
,
null
=
True
,
null
=
True
,
...
@@ -334,13 +357,13 @@ class DeploymentState(models.Model):
...
@@ -334,13 +357,13 @@ class DeploymentState(models.Model):
site
=
models
.
ForeignKey
(
site
=
models
.
ForeignKey
(
Site
,
Site
,
related_name
=
'state
_item
s'
,
related_name
=
'states'
,
on_delete
=
models
.
CASCADE
,
on_delete
=
models
.
CASCADE
,
)
)
service
=
models
.
ForeignKey
(
service
=
models
.
ForeignKey
(
Service
,
Service
,
related_name
=
'state
_item
s'
,
related_name
=
'states'
,
on_delete
=
models
.
CASCADE
,
on_delete
=
models
.
CASCADE
,
)
)
...
@@ -371,15 +394,23 @@ class DeploymentState(models.Model):
...
@@ -371,15 +394,23 @@ class DeploymentState(models.Model):
blank
=
True
,
blank
=
True
,
)
)
@
property
def
state_target
(
self
):
for
deployment
in
self
.
deployments
.
all
():
if
deployment
.
state_target
==
DEPLOYED
:
return
DEPLOYED
return
NOT_DEPLOYED
@
property
@
property
def
is_pending
(
self
):
def
is_pending
(
self
):
# TODO
# TODO
# pending because we are orphaned -> pending until removed everywhere
# pending because we are orphaned -> pending until removed everywhere
if
self
.
parent
is
None
:
if
not
self
.
deployments
.
exists
()
:
return
True
return
True
# pending because the state target is not reached
# pending because the state target is not reached
if
self
.
parent
.
state_target
!=
self
.
state
:
if
self
.
state_target
!=
self
.
state
:
return
True
return
True
return
False
return
False
...
@@ -396,24 +427,30 @@ class DeploymentState(models.Model):
...
@@ -396,24 +427,30 @@ class DeploymentState(models.Model):
return
self
.
user
.
credentials
return
self
.
user
.
credentials
@
classmethod
@
classmethod
def
get_state_item
(
cls
,
parent
,
user
,
site
,
service
):
def
get_state_item
(
cls
,
user
,
site
,
service
,
deployments
=
[]
):
try
:
try
:
item
=
cls
.
objects
.
get
(
item
=
cls
.
objects
.
get
(
parent
=
parent
,
user
=
user
,
user
=
user
,
site
=
site
,
site
=
site
,
service
=
service
,
service
=
service
,
)
)
for
deployment
in
deployments
:
if
not
item
.
deployments
.
filter
(
id
=
deployment
.
id
).
exists
():
LOGGER
.
debug
(
item
.
msg
(
'Binding to deployment {}'
.
format
(
deployment
)))
item
.
deployments
.
add
(
deployment
)
return
item
return
item
except
cls
.
DoesNotExist
:
except
cls
.
DoesNotExist
:
item
=
cls
(
item
=
cls
(
parent
=
parent
,
user
=
user
,
user
=
user
,
site
=
site
,
site
=
site
,
service
=
service
,
service
=
service
,
)
)
item
.
save
()
item
.
save
()
for
deployment
in
deployments
:
item
.
deployments
.
add
(
deployment
)
LOGGER
.
debug
(
item
.
msg
(
'Created'
))
LOGGER
.
debug
(
item
.
msg
(
'Created'
))
return
item
return
item
...
@@ -441,14 +478,20 @@ class DeploymentState(models.Model):
...
@@ -441,14 +478,20 @@ class DeploymentState(models.Model):
# STATE TRANSITIONS
# STATE TRANSITIONS
#
user: provisioning requested
#
DeploymentState.user_deploy
def
user_deploy
(
self
):
def
user_deploy
(
self
):
if
self
.
state
==
REMOVAL_PENDING
:
LOGGER
.
debug
(
self
.
msg
(
'user_deploy'
))
self
.
_assure_credential_states_exist
()
for
cred_state
in
self
.
credential_states
.
all
():
cred_state
.
set_target
(
DEPLOYED
)
if
str
(
self
.
state
)
==
REMOVAL_PENDING
:
self
.
_set_state
(
DEPLOYED
)
self
.
_set_state
(
DEPLOYED
)
return
return
if
s
elf
.
state
==
DEPLOYED
:
if
s
tr
(
self
.
state
)
==
DEPLOYED
:
LOGGER
.
info
(
self
.
msg
(
'ignoring invalid state transition user_deploy
'
))
LOGGER
.
debug
(
self
.
msg
(
'State: already deployed
'
))
return
return
self
.
_set_state
(
self
.
_set_state
(
...
@@ -456,46 +499,56 @@ class DeploymentState(models.Model):
...
@@ -456,46 +499,56 @@ class DeploymentState(models.Model):
publish
=
False
,
# the post response already contains the update
publish
=
False
,
# the post response already contains the update
)
)
# user: deprovisioning requested
# DeploymentState.user_remove
# returns True if no other deployment needs this state_item to be deployed
def
user_remove
(
self
):
def
user_remove
(
self
):
if
self
.
parent
is
None
:
if
str
(
self
.
state_target
)
==
DEPLOYED
:
LOGGER
.
error
(
'user_remove: parentless'
)
LOGGER
.
debug
(
self
.
msg
(
'user_remove: Not removing: another deployment has target deployed'
))
return
# False: signal the callee that a publish_to_client is *not* permitted
return
False
if
(
LOGGER
.
debug
(
self
.
msg
(
'user_remove'
))
self
.
parent
.
state_target
==
DEPLOYED
and
(
self
.
state
==
FAILED
or
self
.
state
==
REJECTED
)
for
cred_state
in
self
.
credential_states
.
all
():
cred_state
.
set_target
(
NOT_DEPLOYED
)
if
str
(
self
.
state
)
==
NOT_DEPLOYED
:
LOGGER
.
debug
(
self
.
msg
(
'State: already not_deployed'
))
elif
(
self
.
state_target
==
DEPLOYED
and
(
self
.
state
==
FAILED
or
self
.
state
==
REJECTED
)
):
):
self
.
_reset
()
self
.
_reset
()
self
.
_set_state
(
NOT_DEPLOYED
,
publish
=
False
)
self
.
_set_state
(
NOT_DEPLOYED
,
publish
=
False
)
return
if
self
.
state
==
NOT_DEPLOYED
:
elif
(
LOGGER
.
info
(
self
.
msg
(
'ignoring invalid state transition user_remove'
))
return
# FIXME this will break if the client 'finishes' the deployment, after user_remove
if
(
self
.
state
==
DEPLOYMENT_PENDING
self
.
state
==
DEPLOYMENT_PENDING
or
self
.
state
==
QUESTIONNAIRE
or
self
.
state
==
QUESTIONNAIRE
):
):
self
.
_set_state
(
NOT_DEPLOYED
)
self
.
_set_state
(
NOT_DEPLOYED
)
return
self
.
_set_state
(
else
:
REMOVAL_PENDING
,
# default: start the removal process
publish
=
False
,
# the post response already contains the update
self
.
_set_state
(
)
REMOVAL_PENDING
,
publish
=
False
,
# the post response already contains the update
)
# True: signal the callee that a publish_to_client is permitted
return
True
# user: questionnaire answered
# user: questionnaire answered
def
user_answers
(
self
,
answers
=
None
):
def
user_answers
(
self
,
answers
=
None
):
if
self
.
parent
is
None
:
if
not
self
.
deployments
.
exists
()
:
LOGGER
.
error
(
'user_
answers: parentles
s'
)
LOGGER
.
error
(
'user_
remove: no deployment
s'
)
return
return
self
.
questionnaire
=
answers
self
.
questionnaire
=
answers
self
.
_set_state
(
DEPLOYMENT_PENDING
,
publish
=
False
)
self
.
_set_state
(
DEPLOYMENT_PENDING
,
publish
=
False
)
self
.
p
arent
.
p
ublish_to_client
()
self
.
publish_to_client
()
def
client_credential_states
(
self
,
credential_states
):
def
client_credential_states
(
self
,
credential_states
):
# maps ssh key names to their state
# maps ssh key names to their state
...
@@ -512,9 +565,16 @@ class DeploymentState(models.Model):
...
@@ -512,9 +565,16 @@ class DeploymentState(models.Model):
else
:
else
:
credential_state
.
set
(
NOT_DEPLOYED
)
credential_state
.
set
(
NOT_DEPLOYED
)
# returns None on success, or a string describing an error
#
client_response
returns None on success, or a string describing an error
def
client_response
(
self
,
output
):
def
client_response
(
self
,
output
):
state
=
output
.
get
(
'state'
,
None
)
if
'state'
not
in
output
:
return
'field "state" is missing in output'
state
=
output
.
get
(
'state'
,
''
)
LOGGER
.
debug
(
self
.
msg
(
'Client response: {}'
.
format
(
state
)))
self
.
_set_state
(
state
)
credential_states
=
output
.
get
(
'user_credential_states'
,
None
)
credential_states
=
output
.
get
(
'user_credential_states'
,
None
)
if
credential_states
is
not
None
:
if
credential_states
is
not
None
:
...
@@ -523,27 +583,16 @@ class DeploymentState(models.Model):
...
@@ -523,27 +583,16 @@ class DeploymentState(models.Model):
self
.
message
=
output
.
get
(
'message'
,
''
)
self
.
message
=
output
.
get
(
'message'
,
''
)
self
.
save
()
self
.
save
()
if
state
is
None
:
return
'missing state in output'
# update values
# update values
if
state
==
DEPLOYED
:
if
state
==
DEPLOYED
:
self
.
credentials
=
output
.
get
(
'credentials'
,
{})
self
.
credentials
=
output
.
get
(
'credentials'
,
{})
self
.
save
()
self
.
save
()
# the client completed a deployment after the user wished to remove the deployment
if
self
.
parent
is
not
None
and
self
.
parent
.
state_target
==
NOT_DEPLOYED
:
self
.
user_remove
()
elif
state
==
NOT_DEPLOYED
:
elif
state
==
NOT_DEPLOYED
:
# reset credentials and questionnaire
# reset credentials and questionnaire