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
f20e7cfc
Commit
f20e7cfc
authored
Nov 26, 2018
by
Lukas Burgey
Browse files
Add polymorphic deployments
parent
549b7bef
Changes
13
Expand all
Hide whitespace changes
Inline
Side-by-side
feudal/backend/admin.py
View file @
f20e7cfc
...
...
@@ -5,6 +5,7 @@ from django.contrib.auth.admin import UserAdmin
from
polymorphic.admin
import
PolymorphicParentModelAdmin
,
PolymorphicChildModelAdmin
from
.
import
models
from
.models
import
deployments
from
.auth.v1.models
import
OIDCConfig
from
.auth.v1.models.vo
import
VO
,
Group
,
Entitlement
,
EntitlementNameSpace
...
...
@@ -54,8 +55,26 @@ admin.site.register(models.User, ClientAdmin)
admin
.
site
.
register
(
models
.
Site
)
admin
.
site
.
register
(
models
.
Service
)
admin
.
site
.
register
(
models
.
SSHPublicKey
)
admin
.
site
.
register
(
models
.
CredentialState
)
admin
.
site
.
register
(
models
.
Deployment
)
admin
.
site
.
register
(
models
.
DeploymentState
)
@
admin
.
register
(
deployments
.
VODeployment
)
class
VODeploymentAdmin
(
PolymorphicChildModelAdmin
):
show_in_index
=
True
@
admin
.
register
(
deployments
.
ServiceDeployment
)
class
ServiceDeploymentAdmin
(
PolymorphicChildModelAdmin
):
show_in_index
=
True
@
admin
.
register
(
deployments
.
Deployment
)
class
DeploymentAdmin
(
PolymorphicChildModelAdmin
):
base_model
=
Entitlement
# Explicitly set here!
show_in_index
=
True
# makes child model admin visible in main admin site
child_models
=
(
deployments
.
VODeployment
,
deployments
.
ServiceDeployment
)
admin
.
site
.
register
([
deployments
.
DeploymentState
,
deployments
.
CredentialState
,
])
feudal/backend/auth/v1/models/serializers/vo.py
View file @
f20e7cfc
...
...
@@ -40,7 +40,6 @@ class EntitlementSerializer(serializers.ModelSerializer):
fields
=
VO_FIELDS
+
(
'name_space'
,
'group_authority'
,
'role'
,
)
...
...
feudal/backend/migrations/0026_delete_deployment.py
0 → 100644
View file @
f20e7cfc
from
django.conf
import
settings
from
django.db
import
migrations
,
models
import
django.db.models.deletion
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'backend'
,
'0025_auto_20181121_1149'
),
]
operations
=
[
migrations
.
DeleteModel
(
'Deployment'
),
migrations
.
DeleteModel
(
'DeploymentState'
),
migrations
.
DeleteModel
(
'CredentialState'
),
]
feudal/backend/migrations/0027_auto_20181126_1314.py
0 → 100644
View file @
f20e7cfc
# Generated by Django 2.1.3 on 2018-11-26 12:14
from
django.conf
import
settings
from
django.db
import
migrations
,
models
import
django.db.models.deletion
import
django_mysql.models
import
feudal.backend.models.deployments
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'contenttypes'
,
'0002_remove_content_type_name'
),
(
'backend'
,
'0026_delete_deployment'
),
]
operations
=
[
migrations
.
CreateModel
(
name
=
'CredentialState'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
auto_created
=
True
,
primary_key
=
True
,
serialize
=
False
,
verbose_name
=
'ID'
)),
(
'state_target'
,
models
.
CharField
(
choices
=
[(
'deployed'
,
'Deployed'
),
(
'not_deployed'
,
'Not Deployed'
)],
default
=
'not_deployed'
,
max_length
=
50
)),
(
'state'
,
models
.
CharField
(
choices
=
[(
'deployment_pending'
,
'VODeployment Pending'
),
(
'removal_pending'
,
'Removal Pending'
),
(
'deployed'
,
'Deployed'
),
(
'not_deployed'
,
'Not Deployed'
),
(
'questionnaire'
,
'Questionnaire'
),
(
'failed'
,
'Failed'
),
(
'rejected'
,
'Rejected'
)],
default
=
'not_deployed'
,
max_length
=
50
)),
(
'_credential_deleted'
,
models
.
BooleanField
(
default
=
False
)),
(
'credential'
,
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'credential_states'
,
to
=
'backend.SSHPublicKey'
)),
],
),
migrations
.
CreateModel
(
name
=
'Deployment'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
auto_created
=
True
,
primary_key
=
True
,
serialize
=
False
,
verbose_name
=
'ID'
)),
(
'state_target'
,
models
.
CharField
(
choices
=
[(
'deployed'
,
'Deployed'
),
(
'not_deployed'
,
'Not Deployed'
)],
default
=
'not_deployed'
,
max_length
=
50
)),
(
'is_active'
,
models
.
BooleanField
(
default
=
True
)),
],
options
=
{
'abstract'
:
False
,
'base_manager_name'
:
'objects'
,
},
),
migrations
.
CreateModel
(
name
=
'DeploymentState'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
auto_created
=
True
,
primary_key
=
True
,
serialize
=
False
,
verbose_name
=
'ID'
)),
(
'state'
,
models
.
CharField
(
choices
=
[(
'deployment_pending'
,
'VODeployment Pending'
),
(
'removal_pending'
,
'Removal Pending'
),
(
'deployed'
,
'Deployed'
),
(
'not_deployed'
,
'Not Deployed'
),
(
'questionnaire'
,
'Questionnaire'
),
(
'failed'
,
'Failed'
),
(
'rejected'
,
'Rejected'
)],
default
=
'not_deployed'
,
max_length
=
50
)),
(
'message'
,
models
.
TextField
(
default
=
''
,
max_length
=
300
)),
(
'questionnaire'
,
django_mysql
.
models
.
JSONField
(
blank
=
True
,
default
=
feudal
.
backend
.
models
.
deployments
.
questionnaire_default
,
null
=
True
)),
(
'credentials'
,
django_mysql
.
models
.
JSONField
(
blank
=
True
,
default
=
feudal
.
backend
.
models
.
deployments
.
credential_default
,
null
=
True
)),
],
),
migrations
.
CreateModel
(
name
=
'ServiceDeployment'
,
fields
=
[
(
'deployment_ptr'
,
models
.
OneToOneField
(
auto_created
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
parent_link
=
True
,
primary_key
=
True
,
serialize
=
False
,
to
=
'backend.Deployment'
)),
(
'service'
,
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'service_deployments'
,
to
=
'backend.Service'
)),
],
options
=
{
'abstract'
:
False
,
'base_manager_name'
:
'objects'
,
},
bases
=
(
'backend.deployment'
,),
),
migrations
.
CreateModel
(
name
=
'VODeployment'
,
fields
=
[
(
'deployment_ptr'
,
models
.
OneToOneField
(
auto_created
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
parent_link
=
True
,
primary_key
=
True
,
serialize
=
False
,
to
=
'backend.Deployment'
)),
(
'vo'
,
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'vo_deployments'
,
to
=
'backend.VO'
)),
],
options
=
{
'abstract'
:
False
,
'base_manager_name'
:
'objects'
,
},
bases
=
(
'backend.deployment'
,),
),
migrations
.
AddField
(
model_name
=
'deploymentstate'
,
name
=
'parent'
,
field
=
models
.
ForeignKey
(
null
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
SET_NULL
,
related_name
=
'state_items'
,
to
=
'backend.Deployment'
),
),
migrations
.
AddField
(
model_name
=
'deploymentstate'
,
name
=
'service'
,
field
=
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'state_items'
,
to
=
'backend.Service'
),
),
migrations
.
AddField
(
model_name
=
'deploymentstate'
,
name
=
'site'
,
field
=
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'state_items'
,
to
=
'backend.Site'
),
),
migrations
.
AddField
(
model_name
=
'deploymentstate'
,
name
=
'user'
,
field
=
models
.
ForeignKey
(
blank
=
True
,
null
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
SET_NULL
,
related_name
=
'state_items'
,
to
=
settings
.
AUTH_USER_MODEL
),
),
migrations
.
AddField
(
model_name
=
'deployment'
,
name
=
'polymorphic_ctype'
,
field
=
models
.
ForeignKey
(
editable
=
False
,
null
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'polymorphic_backend.deployment_set+'
,
to
=
'contenttypes.ContentType'
),
),
migrations
.
AddField
(
model_name
=
'deployment'
,
name
=
'user'
,
field
=
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'deployments'
,
to
=
settings
.
AUTH_USER_MODEL
),
),
migrations
.
AddField
(
model_name
=
'credentialstate'
,
name
=
'target'
,
field
=
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'credential_states'
,
to
=
'backend.DeploymentState'
),
),
]
feudal/backend/models/__init__.py
View file @
f20e7cfc
from
json
import
dumps
from
logging
import
getLogger
from
django.db
import
models
from
django_mysql.models
import
JSONField
from
django.conf
import
settings
# these imports are exports!
from
..auth.v1.models.vo
import
VO
from
.brokers
import
RabbitMQInstance
from
.users
import
User
,
SSHPublicKey
from
.users
import
User
LOGGER
=
getLogger
(
__name__
)
DEPLOYMENT_PENDING
=
'deployment_pending'
REMOVAL_PENDING
=
'removal_pending'
NOT_DEPLOYED
=
'not_deployed'
DEPLOYED
=
'deployed'
QUESTIONNAIRE
=
'questionnaire'
FAILED
=
'failed'
REJECTED
=
'rejected'
TARGET_CHOICES
=
(
(
DEPLOYED
,
'Deployed'
),
(
NOT_DEPLOYED
,
'Not Deployed'
),
)
STATE_CHOICES
=
(
(
DEPLOYMENT_PENDING
,
'Deployment Pending'
),
(
REMOVAL_PENDING
,
'Removal Pending'
),
(
DEPLOYED
,
'Deployed'
),
(
NOT_DEPLOYED
,
'Not Deployed'
),
(
QUESTIONNAIRE
,
'Questionnaire'
),
(
FAILED
,
'Failed'
),
(
REJECTED
,
'Rejected'
),
)
def
questionnaire_default
():
return
{}
...
...
@@ -140,7 +114,8 @@ class Service(models.Model):
return
self
.
name
def
remove_service
(
self
):
for
deployment
in
Deployment
.
objects
.
filter
(
from
.
import
deployments
for
deployment
in
deployments
.
Deployment
.
objects
.
filter
(
vo__services
=
self
,
):
deployment
.
service_removed
(
self
)
...
...
@@ -158,593 +133,6 @@ class Service(models.Model):
try
:
deployment
=
user
.
deployments
.
get
(
vo
=
vo
)
deployment
.
service_added
(
self
)
except
Deployment
.
DoesNotExist
:
except
deployments
.
Deployment
.
DoesNotExist
:
LOGGER
.
error
(
'Inconsistency of vo deployment'
)
raise
class
Deployment
(
models
.
Model
):
user
=
models
.
ForeignKey
(
User
,
related_name
=
'deployments'
,
on_delete
=
models
.
CASCADE
,
)
vo
=
models
.
ForeignKey
(
VO
,
related_name
=
'deployments'
,
on_delete
=
models
.
CASCADE
,
)
# which state do we currently want to reach?
state_target
=
models
.
CharField
(
max_length
=
50
,
choices
=
TARGET_CHOICES
,
default
=
NOT_DEPLOYED
,
)
is_active
=
models
.
BooleanField
(
default
=
True
,
)
# credentials provided by the backend to the clients
@
property
def
credentials
(
self
):
return
self
.
user
.
credentials
@
property
def
state
(
self
):
if
self
.
state_items
.
exists
():
_state
=
''
for
state
in
self
.
state_items
.
all
():
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
):
return
self
.
vo
.
services
.
all
()
def
create_state_items
(
self
):
for
service
in
self
.
services
:
LOGGER
.
debug
(
'create_state_items: creating DeploymentState for service %s at site %s'
,
service
,
service
.
site
)
DeploymentState
.
get_state_item
(
parent
=
self
,
user
=
self
.
user
,
site
=
service
.
site
,
service
=
service
,
)
@
classmethod
def
get_deployment
(
cls
,
user
,
vo
):
try
:
deployment
=
cls
.
objects
.
get
(
user
=
user
,
vo
=
vo
,
)
deployment
.
create_state_items
()
return
deployment
except
cls
.
DoesNotExist
:
deployment
=
cls
(
user
=
user
,
vo
=
vo
,
)
deployment
.
save
()
deployment
.
create_state_items
()
LOGGER
.
debug
(
deployment
.
msg
(
'created'
))
return
deployment
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
def
user_remove
(
self
):
for
item
in
self
.
state_items
.
all
():
if
item
.
state
!=
NOT_DEPLOYED
:
item
.
user_remove
()
self
.
_set_target
(
NOT_DEPLOYED
)
self
.
publish_to_client
()
# each state item publishes its state to the user
def
user_credential_added
(
self
,
key
):
for
item
in
self
.
state_items
.
all
():
item
.
user_credential_added
(
key
)
self
.
publish
()
def
user_credential_removed
(
self
,
key
):
for
item
in
self
.
state_items
.
all
():
item
.
user_credential_removed
(
key
)
self
.
publish
()
def
service_added
(
self
,
service
):
LOGGER
.
debug
(
self
.
msg
(
'Adding service {}'
.
format
(
service
)))
item
=
DeploymentState
.
get_state_item
(
parent
=
self
,
site
=
service
.
site
,
service
=
service
,
)
if
self
.
state_target
==
'deployed'
:
item
.
user_deploy
()
def
service_removed
(
self
,
service
):
LOGGER
.
debug
(
self
.
msg
(
'Removing service {}'
.
format
(
service
)))
LOGGER
.
debug
(
'TODO implement service removal'
)
def
publish_to_client
(
self
):
# avoiding circular dependencies here
from
.serializers.clients
import
DeploymentSerializer
data
=
DeploymentSerializer
(
self
).
data
msg
=
dumps
(
data
)
RabbitMQInstance
.
load
().
publish_by_vo
(
self
.
vo
,
msg
,
)
# sends a state update via RabbitMQ / STOMP to the users webpage instance
def
publish_to_user
(
self
):
if
self
.
user
is
None
:
return
# avoiding circular dependencies here
from
.serializers.webpage
import
DeploymentSerializer
msg
=
dumps
({
'deployment'
:
DeploymentSerializer
(
self
).
data
,
})
RabbitMQInstance
.
load
().
publish_to_user
(
self
.
user
,
msg
,
)
def
publish
(
self
):
self
.
publish_to_user
()
self
.
publish_to_client
()
def
msg
(
self
,
msg
):
return
'{} - {}'
.
format
(
self
,
msg
)
def
_set_target
(
self
,
target
):
self
.
state_target
=
target
for
credential_state
in
CredentialState
.
objects
.
filter
(
target__parent
=
self
,
):
credential_state
.
set_target
(
target
)
LOGGER
.
debug
(
self
.
msg
(
'Target changed to '
+
target
))
self
.
save
()
def
__str__
(
self
):
return
'VO-Dep: ({}:{})#{}'
.
format
(
self
.
vo
,
self
.
user
,
self
.
id
,
)
class
DeploymentState
(
models
.
Model
):
parentless
=
ValueError
(
'Tried to access parent of parentless deployment state'
)
parent
=
models
.
ForeignKey
(
Deployment
,
related_name
=
'state_items'
,
on_delete
=
models
.
SET_NULL
,
null
=
True
)
user
=
models
.
ForeignKey
(
User
,
related_name
=
'state_items'
,
on_delete
=
models
.
SET_NULL
,
blank
=
True
,
null
=
True
,
)
site
=
models
.
ForeignKey
(
Site
,
related_name
=
'state_items'
,
on_delete
=
models
.
CASCADE
,
)
service
=
models
.
ForeignKey
(
Service
,
related_name
=
'state_items'
,
on_delete
=
models
.
CASCADE
,
)
state
=
models
.
CharField
(
max_length
=
50
,
choices
=
STATE_CHOICES
,
default
=
NOT_DEPLOYED
,
)
# message for the user
message
=
models
.
TextField
(
max_length
=
300
,
default
=
''
,
)
# questions for the user (needed for deployment
questionnaire
=
JSONField
(
default
=
questionnaire_default
,
null
=
True
,
blank
=
True
,
)
# credentials for the service
# only valid when state == deployed
credentials
=
JSONField
(
default
=
credential_default
,
null
=
True
,
blank
=
True
,
)
@
property
def
is_pending
(
self
):
# TODO
# pending because we are orphaned -> pending until removed everywhere
if
self
.
parent
is
None
:
return
True
# pending because the state target is not reached
if
self
.
parent
.
state_target
!=
self
.
state
:
return
True
return
False
@
property
def
is_credential_pending
(
self
):
for
credential_state
in
self
.
credential_states
.
all
():
if
credential_state
.
is_pending
:
return
True
return
False
@
property
def
user_credentials
(
self
):
return
self
.
user
.
credentials
@
property
def
vo
(
self
):
if
self
.
parent
is
not
None
:
return
self
.
parent
.
vo
else
:
raise
self
.
parentless
@
classmethod
def
get_state_item
(
cls
,
parent
=
None
,
user
=
None
,
site
=
None
,
service
=
None
):
try
:
item
=
cls
.
objects
.
get
(
parent
=
parent
,
user
=
user
,
site
=
site
,
service
=
service
,
)
return
item
except
cls
.
DoesNotExist
:
item
=
cls
(
parent
=
parent
,
user
=
user
,
site
=
site
,
service
=
service
,
)
item
.
save
()
LOGGER
.
debug
(
'get_state_item: created %s'
,
item
)
return
item
# starts tracking this the credential for this item
def
user_credential_added
(
self
,
credential
):
if
settings
.
DEBUG_CREDENTIALS
:
LOGGER
.
debug
(
'user_credential_added: %s %s'
,
self
,
credential
)
CredentialState
.
get_credential_state
(
credential
,
self
,
)
def
user_credential_removed
(
self
,
credential
):
if
settings
.
DEBUG_CREDENTIALS
:
LOGGER
.
debug
(
'user_credential_removed: %s %s'
,
self
,
credential
)
try
:
credential_state
=
self
.
credential_states
.
get
(
credential
=
credential
)
credential_state
.
credential_deleted
()
except
CredentialState
.
DoesNotExist
:
LOGGER
.
error
(
self
.
msg
(
'Credential {} has no CredentialState'
.
format
(
credential
)))
# STATE TRANSITIONS
# user: provisioning requested
def
user_deploy
(
self
):
if
self
.
state
==
REMOVAL_PENDING
:
self
.
_set_state
(
DEPLOYED
)
return
if
self
.
state
==
DEPLOYED
:
LOGGER
.
info
(
self
.
msg
(
'ignoring invalid state transition user_deploy'
))
return
self
.
_set_state
(
DEPLOYMENT_PENDING
,
publish
=
False
,
# the post response already contains the update
)
# user: deprovisioning requested
def
user_remove
(
self
):
if
self
.
parent
is
None
:
LOGGER
.
error
(
'user_remove: parentless'
)
return
if
(
self
.
parent
.
state_target
==
DEPLOYED
and
(
self
.
state
==
FAILED
or
self
.
state
==
REJECTED
)
):
self
.
_reset
()
self
.
_set_state
(
NOT_DEPLOYED
,
publish
=
False
)
return
if
self
.
state
==
NOT_DEPLOYED
:
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
or
self
.
state
==
QUESTIONNAIRE
):
self
.
_set_state
(
NOT_DEPLOYED
)
return
self
.
_set_state
(
REMOVAL_PENDING
,
publish
=
False
,
# the post response already contains the update
)
# user: questionnaire answered
def
user_answers
(
self
,
answers
=
None
):
if
self
.
parent
is