Commit 3a72b221 authored by lukas.burgey's avatar lukas.burgey

Merge branch 'deployment-mode' into dev

parents f43065aa af65c461
Pipeline #110857 passed with stage
in 1 minute and 24 seconds
......@@ -10,7 +10,7 @@ from rest_framework.test import APIClient
from feudal.backend.brokers import RabbitMQInstance
from feudal.backend.models import Site, Service
from feudal.backend.models.deployments import get_deployment, DEPLOYED
from feudal.backend.models.deployments import DEPLOYED, VODeployment, ServiceDeployment
from feudal.backend.models.users import User
from feudal.backend.models.auth import OIDCConfig
from feudal.backend.models.auth.vos import VO, Group, Entitlement
......@@ -187,7 +187,7 @@ def service(site, group, entitlement, vo):
# a deployment with a state
@pytest.fixture
def dep(user, service):
return get_deployment(user, service=service)
return ServiceDeployment.get_or_create(user, service)
@pytest.fixture
def dep_state(dep):
......@@ -261,9 +261,9 @@ def _typed_deployed_deployment(deployment_type, user, service, vo, downstream_te
# get a deployment for the user
dep = None
if deployment_type == 'vo':
dep = get_deployment(user, vo=vo)
dep = VODeployment.get_or_create(user, vo)
else:
dep = get_deployment(user, service=service)
dep = ServiceDeployment.get_or_create(user, service)
assert dep is not None
......@@ -320,10 +320,10 @@ def _typed_pending_deployment(deployment_type, user, service, vo):
# get a deployment for the user
dep = None
if deployment_type == 'vo':
dep = get_deployment(user, vo=vo)
dep = VODeployment.get_or_create(user, vo)
assert dep.vo == vo
else:
dep = get_deployment(user, service=service)
dep = ServiceDeployment.get_or_create(user, service)
assert dep is not None
......
# Generated by Django 3.0.8 on 2020-09-24 09:38
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('backend', '0020_auto_20200922_1333'),
]
operations = [
migrations.CreateModel(
name='UserPreferences',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('deployment_mode', models.CharField(choices=[('both', 'Pick a combination of services and VOs for deployment.'), ('services-only', 'Pick individual services for deployment'), ('vos-only', 'Pick individual VOs for deployment')], default='both', max_length=20)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='preferences', to=settings.AUTH_USER_MODEL)),
],
),
]
This diff is collapsed.
......@@ -3,7 +3,7 @@ from rest_framework.serializers import ModelSerializer, Serializer, CharField, J
from rest_polymorphic.serializers import PolymorphicSerializer
from feudal.backend.models import Site, Service
from feudal.backend.models.users import User, SSHPublicKey
from feudal.backend.models.users import User, SSHPublicKey, UserPreferences
from feudal.backend.models.deployments import CredentialState, DeploymentState, Deployment, VODeployment, ServiceDeployment
from feudal.backend.models.auth.serializers import VOSerializer
......@@ -158,12 +158,22 @@ class DeploymentSerializer(PolymorphicSerializer):
}
class UserPreferencesSerializer(ModelSerializer):
class Meta:
model = UserPreferences
fields = (
'deployment_mode',
)
class UserStateSerializer(ModelSerializer):
deployments = DeploymentSerializer(many=True)
services = ServiceSerializer(many=True)
ssh_keys = SSHPublicKeySerializer(many=True)
states = DeploymentStateSerializer(many=True)
vos = VOSerializer(many=True)
preferences = UserPreferencesSerializer()
class Meta:
model = User
......@@ -176,6 +186,7 @@ class UserStateSerializer(ModelSerializer):
'states',
'userinfo',
'vos',
'preferences',
)
......
......@@ -5,7 +5,7 @@ import logging
from feudal.backend.tests import BaseTestCase
from feudal.backend.models import Service
from feudal.backend.models.deployments import (
Deployment, get_deployment,
Deployment, VODeployment,
DEPLOYED, NOT_DEPLOYED, DEPLOYMENT_PENDING, REMOVAL_PENDING,
)
......@@ -130,22 +130,22 @@ class DeploymentTest(BaseTestCase):
self.assertEqual(deployment.state, NOT_DEPLOYED)
def test_group_with_no_service(self):
deployment = get_deployment(self.user, self.group_none)
deployment = VODeployment.get_or_create(self.user, self.group_none)
self.deployment_run(deployment, 0)
def test_group_with_service(self):
deployment = get_deployment(self.user, vo=self.group_one)
deployment = VODeployment.get_or_create(self.user, self.group_one)
self.deployment_run(deployment, 1)
def test_group_with_two_services(self):
deployment = get_deployment(self.user, vo=self.group_two)
deployment = VODeployment.get_or_create(self.user, self.group_two)
self.deployment_run(deployment, 2)
# a vo with one service gets another service *after* the user requested the deployment
def test_group_with_delayed_service(self):
deployment = get_deployment(self.user, self.group_one)
deployment = VODeployment.get_or_create(self.user, self.group_one)
self.deployment_run_delayed_service(deployment, self.group_one, 1)
# a vo with two services loses one service *after* the user requested the deployment
def test_group_with_vanishing_service(self):
get_deployment(self.user, vo=self.group_two)
VODeployment.get_or_create(self.user, self.group_two)
......@@ -6,7 +6,7 @@ import copy
from django.urls import reverse
from feudal.backend.models import Service
from feudal.backend.models.users import User, SSHPublicKey
from feudal.backend.models.users import User, SSHPublicKey, UserPreferences
from feudal.backend.models.deployments import DEPLOYED, NOT_DEPLOYED, CredentialState, Deployment, DeploymentState
from feudal.backend.models.auth.vos import Group, Entitlement
......@@ -259,3 +259,16 @@ def test_construct_from_userinfo(idp, userinfo):
}
user = User.construct_from_userinfo(userinfo, idp)
assert user.userinfo['test_value'] == test_value
assert user.preferences.deployment_mode == UserPreferences.DEPLOYMENT_MODE_BOTH
def test_user_deletion_cascade(user):
""" the user deletion must cascade to its preferences """
prefs_pk = user.preferences.pk
assert UserPreferences.objects.filter(pk=prefs_pk).exists()
user.delete()
assert not UserPreferences.objects.filter(pk=prefs_pk).exists()
......@@ -141,6 +141,13 @@ class User(AbstractUser):
user.update_userinfo(userinfo)
user.save()
_, createdPrefs = UserPreferences.objects.get_or_create(
user=user,
)
if createdPrefs:
LOGGER.info('construct_from_userinfo: Created preferences for user %s', user)
return user
@classmethod
......@@ -449,11 +456,7 @@ class User(AbstractUser):
def delete(self, using=None, keep_parents=False):
if self.user_type == self.TYPE_CHOICE_USER:
LOGGER.info('Deleting User %s', self)
# TODO
# these deletions are a hack. django should (TM) be able to delete them itself
# but there seems to be a bug
LOGGER.info(self.msg('Preparing user deletion'))
self._delete_deployments()
......@@ -461,9 +464,7 @@ class User(AbstractUser):
for key in self.ssh_keys.all():
key.delete_key()
for state in self.states.all():
state.publish_to_client()
LOGGER.info(self.msg('Deleting right now'))
super().delete(using, keep_parents)
def add_key(self, key):
......@@ -554,3 +555,29 @@ def activate_user(sender, instance=None, created=False, **kwargs):
if instance.is_active and not instance.is_active_at_clients:
instance.activate()
class UserPreferences(models.Model):
user = models.OneToOneField(
User,
related_name='preferences',
on_delete=models.CASCADE,
)
DEPLOYMENT_MODE_BOTH = 'both'
DEPLOYMENT_MODE_SERVICES_ONLY = 'services-only'
DEPLOYMENT_MODE_VOS_ONLY = 'vos-only'
DEPLOYMENT_MODE_DEFAULT = DEPLOYMENT_MODE_BOTH
DEPLOYMENT_MODE_CHOICES = (
(DEPLOYMENT_MODE_BOTH, 'Pick a combination of services and VOs for deployment.'),
(DEPLOYMENT_MODE_SERVICES_ONLY, 'Pick individual services for deployment'),
(DEPLOYMENT_MODE_VOS_ONLY, 'Pick individual VOs for deployment'),
)
deployment_mode = models.CharField(
max_length=20,
choices=DEPLOYMENT_MODE_CHOICES,
default=DEPLOYMENT_MODE_DEFAULT,
)
......@@ -93,19 +93,3 @@ class BaseTestCase(TestCase):
user=cls.user,
).save()
# the user is logged in using session authentication
class LoggedInTest(BaseTestCase):
client = None
def setUp(self):
self.client = Client()
user = authenticate(
username=self.USER_NAME,
password=self.USER_PASSWORD,
)
self.assertIsNotNone(user)
self.client.force_login(
user=user,
)
......@@ -86,10 +86,6 @@ class ConfigurationView(views.APIView):
except VO.DoesNotExist:
service.vos.add(vo)
# we need to update the Deployments here
for dep in vo.vo_deployments.all():
dep.update()
# returns the service ID to service mapping contained in the request
def parse_sid_to_service(self, request):
self.sid_to_service = {}
......
......@@ -10,9 +10,11 @@ from rest_framework.permissions import AllowAny
from feudal.backend.views.renderers import PlainTextRenderer
from feudal.backend.models import Service
from feudal.backend.models.users import UserPreferences
from feudal.backend.models.serializers import (
UserStateSerializer, ServiceSerializer, SSHPublicKeySerializer,
DeploymentSerializer, DeploymentStateSerializer
DeploymentSerializer, DeploymentStateSerializer,
UserPreferencesSerializer
)
from feudal.backend.models.deployments import VODeployment, ServiceDeployment
from feudal.backend.models.auth.vos import VO
......@@ -83,6 +85,33 @@ class UserView(generics.RetrieveDestroyAPIView):
instance.delete()
class UserPreferencesView(generics.RetrieveUpdateAPIView):
permission_classes = PERMISSION_CLASSES
serializer_class = UserPreferencesSerializer
def get_object(self):
return self.request.user.preferences
def perform_update(self, serializer):
preferences = serializer.save()
LOGGER.debug(self.request.user.msg('Updated preferences'))
if preferences.deployment_mode == UserPreferences.DEPLOYMENT_MODE_SERVICES_ONLY:
qs = self.request.user.deployments.instance_of(VODeployment)
if qs.exists():
LOGGER.debug(self.request.user.msg('Removing my VODeployments because of a new preference'))
qs.delete()
elif preferences.deployment_mode == UserPreferences.DEPLOYMENT_MODE_VOS_ONLY:
qs = self.request.user.deployments.instance_of(ServiceDeployment)
if qs.exists():
LOGGER.debug(self.request.user.msg('Removing my ServiceDeployments because of a new preference'))
qs.delete()
class ServiceListView(generics.ListAPIView):
permission_classes = PERMISSION_CLASSES
serializer_class = ServiceSerializer
......@@ -96,8 +125,10 @@ class ServiceView(generics.RetrieveAPIView):
serializer_class = ServiceSerializer
def get_object(self):
available_services = Service.objects.filter(vos__user=self.request.user).distinct()
LOGGER.debug('Available services: %s', available_services)
return get_object_or_404(
Service.objects.filter(vos__user=self.request.user),
available_services,
id=self.kwargs['id'],
)
......@@ -180,14 +211,7 @@ class DeploymentView(generics.RetrieveUpdateAPIView):
if self.kwargs['type'] == 'vo':
try:
vo = self.request.user.vos.get(id=dep_id)
dep, created = VODeployment.objects.get_or_create(
user=self.request.user,
vo=vo,
defaults={},
)
if created:
LOGGER.debug('Created new VO deployment')
return dep
return VODeployment.get_or_create(self.request.user, vo)
except VO.DoesNotExist:
raise exceptions.ValidationError('You have no VO with id "{}"'.format(dep_id))
......@@ -197,23 +221,13 @@ class DeploymentView(generics.RetrieveUpdateAPIView):
service = None
for s in self.request.user.services:
if s.id == int(dep_id):
if service is None:
service = s
else:
raise Exception('user has multiple services with identical id')
service = s
break
if service is None:
raise exceptions.ValidationError('You are not permitted to access a service with id "{}"'.format(dep_id))
dep, created = ServiceDeployment.objects.get_or_create(
user=self.request.user,
service=service,
defaults={},
)
if created:
LOGGER.debug('Created new ServiceDeployment')
return dep
return ServiceDeployment.get_or_create(self.request.user, service)
raise exceptions.ValidationError('Type must be either "service" or "vo"')
......@@ -224,6 +238,8 @@ class DeploymentView(generics.RetrieveUpdateAPIView):
if dep.state_target != old_state_target:
dep.target_changed()
else:
LOGGER.debug(dep.msg('Patched but the state_target was not changed'))
class DeploymentStateListView(generics.ListCreateAPIView):
......@@ -265,22 +281,25 @@ class DeploymentStateView(generics.RetrieveUpdateAPIView):
URLPATTERNS = [
path('user', UserView.as_view(), name='user'),
path('services', ServiceListView.as_view(), name='user-services'),
path('user-prefs', UserPreferencesView.as_view(), name='user-prefs'),
path('services', ServiceListView.as_view(), name='user-service-list'),
re_path(r'^service/(?P<id>[0-9]+)$', ServiceView.as_view(), name='user-service'),
path('vos', VOListView.as_view(), name='user-vos'),
path('vos', VOListView.as_view(), name='user-vo-list'),
re_path(r'^vo/(?P<id>[0-9]+)$', VOView.as_view(), name='user-vo'),
path('ssh-keys', SSHPublicKeyListView.as_view(), name='user-sshkeys'),
re_path(r'^ssh-key/(?P<id>[0-9]+)$', SSHPublicKeyView.as_view(), name='user-sshkey'),
path('ssh-keys', SSHPublicKeyListView.as_view(), name='user-ssh-key-list'),
re_path(r'^ssh-key/(?P<id>[0-9]+)$', SSHPublicKeyView.as_view(), name='user-ssh-key'),
# type is either 'vo' or 'service'
re_path(r'^deployments/?(?P<type>.+)?$', DeploymentListView.as_view(), name='user-deps'),
re_path(r'^deployments/?(?P<type>.+)?$', DeploymentListView.as_view(), name='user-dep-list'),
re_path(r'^deployment/(?P<type>.+)/(?P<id>[0-9]+)$', DeploymentView.as_view(), name='user-dep'),
path('states', DeploymentStateListView.as_view(), name='user-dep-states'),
path('states', DeploymentStateListView.as_view(), name='user-dep-state-list'),
re_path(r'^state/(?P<id>[0-9]+)$', DeploymentStateView.as_view(), name='user-dep-state'),
# this catch all must be last in the list!
path('', HelpView.as_view(), name='user-help'),
re_path(r'.*$', HelpView.as_view()),
]
This diff is collapsed.
......@@ -118,7 +118,7 @@ HANDLER_BACKUP_COUNT = 3
# audit log
DEBUGX_LOG_LEVEL = 9 # less then debug
logging.addLevelName(DEBUGX_LOG_LEVEL, 'DEBUGX')
logging.addLevelName(DEBUGX_LOG_LEVEL, 'DEBUX')
AUDIT_LOG_LEVEL = 5 # lowest log level
logging.addLevelName(AUDIT_LOG_LEVEL, 'AUDIT')
......@@ -181,7 +181,7 @@ LOGGING = {
'filters': ['audit_only'], # display no other messages
},
'debugx': {
'level': 'DEBUGX',
'level': 'DEBUX',
'class': FILE_HANDLER,
'maxBytes': HANDLER_MAX_BYTES,
'backupCount': HANDLER_BACKUP_COUNT,
......@@ -189,7 +189,7 @@ LOGGING = {
'formatter': 'standard',
},
'compact-debugx': {
'level': 'DEBUGX',
'level': 'DEBUX',
'class': FILE_HANDLER,
'maxBytes': HANDLER_MAX_BYTES,
'backupCount': HANDLER_BACKUP_COUNT,
......
Subproject commit af900e6c6a4b0792eb60585dde36bff351a1c443
Subproject commit c75a3200b2c8546ff9933c9667548cf280abf349
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment