Browse Source

Add basic VRW support

V1
Joshua Rubingh 2 years ago
parent
commit
96f630bc3f
  1. 1
      VRE/VRE/settings.py
  2. 2
      VRE/apps/api/urls.py
  3. 69
      VRE/apps/virtual_machine/signals.py
  4. 1
      VRE/apps/vrw/__init__.py
  5. 17
      VRE/apps/vrw/admin.py
  6. 18
      VRE/apps/vrw/apps.py
  7. 47
      VRE/apps/vrw/migrations/0001_initial.py
  8. 0
      VRE/apps/vrw/migrations/__init__.py
  9. 69
      VRE/apps/vrw/models.py
  10. 14
      VRE/apps/vrw/permissions.py
  11. 54
      VRE/apps/vrw/serializers.py
  12. 16
      VRE/apps/vrw/signals.py
  13. 3
      VRE/apps/vrw/tests.py
  14. 12
      VRE/apps/vrw/urls.py
  15. 39
      VRE/apps/vrw/views.py
  16. 20
      VRE/lib/models/cloud.py

1
VRE/VRE/settings.py

@ -47,6 +47,7 @@ INSTALLED_APPS = [ @@ -47,6 +47,7 @@ INSTALLED_APPS = [
'apps.storage',
'apps.study',
'apps.virtual_machine',
'apps.vrw',
'djoser',
'rest_framework',

2
VRE/apps/api/urls.py

@ -57,6 +57,8 @@ api_router_v1.register(r'virtualmachines/gpu', VirtualMachineGPUViewSet) @@ -57,6 +57,8 @@ api_router_v1.register(r'virtualmachines/gpu', VirtualMachineGPUViewSet)
api_router_v1.register(r'virtualmachines/os', VirtualMachineOperatingSystemViewSet)
api_router_v1.register(r'virtualmachines', VirtualMachineViewSet)
api_router_v1.urls.append(path('vrw/', include('apps.vrw.urls')))
# Main namespace for the API urls
app_name = 'api'
urlpatterns = [

69
VRE/apps/virtual_machine/signals.py

@ -1,36 +1,45 @@ @@ -1,36 +1,45 @@
from django.db.models.signals import post_save, post_delete
from django.db.models.signals import post_save, post_delete, pre_save
from django.dispatch import receiver
from .models import VirtualMachine, VirtualNetworkType, VirtualMachineAccess
from .tasks import create_virtual_machine_task, delete_virtual_machine_task
@receiver(post_save, sender=VirtualMachine)
def create_virtual_machine(sender, instance, created, **kwargs):
"""
| A signal that is fired when a new virtual machine is created in the DB. This will trigger the action to create a VPS on a cloud platform
Arguments
----------
sender : sender
The model that has triggered the signal
instance: :attr:`VirtualMachine`
The newly created virtual machine object
created : boolean
Wether the object was created or updated. When true it is newly created
"""
if instance.remote_id:
# We have already a connection with the cloud instance... For now we do nothing.
return
create_virtual_machine_task(instance.pk)
@receiver(post_delete, sender=VirtualMachine)
def terminate_virtual_machine(sender, instance, **kwargs):
if instance.remote_id:
# If we have a remote_id in the instance, then we need to delete the VPS from the cloud provider
delete_virtual_machine_task(instance.remote_id)
# @receiver(pre_save, sender=VirtualMachine)
# def create_virtual_machine(sender, instance, **kwargs):
# """
# | A signal that is fired when a new virtual machine is created in the DB. This will trigger the action to create a VPS on a cloud platform
# Arguments
# ----------
# sender : sender
# The model that has triggered the signal
# instance: :attr:`VirtualMachine`
# The newly created virtual machine object
# created : boolean
# Wether the object was created or updated. When true it is newly created
# """
# print('Virtual machine instance... Here we check if it is VRW (Windows) or HPC (Linux)')
# print(instance)
# print(dir(instance))
# print(instance.operating_system)
# print(dir(instance.operating_system))
# if instance.remote_id:
# # We have already a connection with the cloud instance... For now we do nothing.
# return
# if 'linux' in instance.operating_system.name.lower():
# create_virtual_machine_task(instance.pk)
# @receiver(post_delete, sender=VirtualMachine)
# def terminate_virtual_machine(sender, instance, **kwargs):
# if instance.remote_id:
# # If we have a remote_id in the instance, then we need to delete the VPS from the cloud provider
# pass
# #delete_virtual_machine_task(instance.remote_id)

1
VRE/apps/vrw/__init__.py

@ -0,0 +1 @@ @@ -0,0 +1 @@
default_app_config = 'apps.vrw.apps.VrwConfig'

17
VRE/apps/vrw/admin.py

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
from django.contrib import admin
from .models import WorkspacePart, Workspace
# Register your models here.
@admin.register(WorkspacePart)
class VRWPartAdmin(admin.ModelAdmin):
list_display = ('name', 'created_at')
search_fields = ('name', )
ordering = ('-created_at', 'name', )
readonly_fields = ('created_at', 'updated_at')
@admin.register(Workspace)
class VRWWorkspaceAdmin(admin.ModelAdmin):
list_display = ('virtual_machine', 'created_at')
ordering = ('-created_at', )
readonly_fields = ('created_at', 'updated_at')

18
VRE/apps/vrw/apps.py

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
from django.apps import AppConfig
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
class VrwConfig(AppConfig):
name = 'apps.vrw'
label = 'vrw'
verbose_name = _('VRW')
verbose_name_plural = _('VRW')
try:
assert settings.VRW_API_GROUP
except AttributeError:
# We only load this setting, if it is not available in the overall settings.py file
settings.VRW_API_GROUP = 'vrw-api'
def ready(self):
import apps.vrw.signals

47
VRE/apps/vrw/migrations/0001_initial.py

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
# Generated by Django 3.1.7 on 2021-04-01 10:42
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('virtual_machine', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Workspace',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, help_text='The date and time this model has been created', verbose_name='Date created')),
('updated_at', models.DateTimeField(auto_now=True, help_text='The date and time this model has been updated', verbose_name='Date updated')),
('remote_id', models.CharField(blank=True, help_text='The remote ID of this virtual machine on the cloud platform', max_length=50, null=True)),
('virtual_machine', models.OneToOneField(help_text='The virtual machine configuration.', on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='virtual_machine.virtualmachine')),
('starting_at', models.DateTimeField(help_text='Enter the start date when this machine should be created', verbose_name='Starting at')),
('ending_at', models.DateTimeField(help_text='Enter the end date when this machine should be terminated', verbose_name='Ending at')),
('status', models.CharField(choices=[('NEW', 'New'), ('UPDATING', 'Updating'), ('DONE', 'Done'), ('ERROR', 'Error'), ('OFFLINE', 'Offline'), ('DELETE', 'Delete'), ('TERMINATED', 'Terminated')], default='NEW', help_text='The status of the workspace.', max_length=10, verbose_name='Status')),
],
options={
'verbose_name': 'virtual workspace',
'verbose_name_plural': 'virtual workspaces',
},
),
migrations.CreateModel(
name='WorkspacePart',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='The date and time this model has been created', verbose_name='Date created')),
('updated_at', models.DateTimeField(auto_now=True, help_text='The date and time this model has been updated', verbose_name='Date updated')),
('name', models.CharField(help_text='test.....', max_length=100, verbose_name='Name')),
('object_id', models.PositiveIntegerField()),
('content_type', models.ForeignKey(limit_choices_to={'app_label': 'virtual_machine'}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
],
options={
'abstract': False,
},
),
]

0
VRE/apps/vrw/migrations/__init__.py

69
VRE/apps/vrw/models.py

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from lib.models.base import MetaDataModel
from lib.models.cloud import CloudBasicDataModel
from apps.virtual_machine.models import VirtualMachine
# Create your models here.
class WorkspacePart(MetaDataModel):
name = models.CharField(_('Name'), max_length=100, help_text=_('test.....'))
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, limit_choices_to={'app_label': 'virtual_machine'})
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
class WorkspaceStatus(models.TextChoices):
"""
A class for defining workspace status as choices. Currently the following statusses are supported:
.. data:: NEW
This is the state where every Workspace starts with. This means that the Workspace config is created, but it has not been created yet by the cloud provider
.. data:: UPDATING
The Workspace is being created on the cloud provider infrastructure.
.. data:: DONE
The Workspace is created by the cloud provider and can be used by the customer
.. data:: ERROR
The Workspace could not be created by the cloud provider. There was an (unknown) error.
.. data:: OFFLINE
The Workspace is offline and not reachable. Reasons are not known. But this can be used for trigger an investigation
.. data:: DELETE
The Workspace is marked for deleting fromt the cloud platform
.. data:: TERMINATED
The Workspace is closed/deleted by the cloud provider wihtout reason.
"""
NEW = ('NEW',_('New'))
UPDATING = ('UPDATING',_('Updating'))
DONE = ('DONE',_('Done'))
ERROR = ('ERROR',_('Error'))
OFFLINE = ('OFFLINE',_('Offline'))
DELETE = ('DELETE',_('Delete'))
TERMINATED = ('TERMINATED',_('Terminated'))
class Workspace(MetaDataModel, CloudBasicDataModel):
class Meta:
verbose_name = _('virtual workspace')
verbose_name_plural = _('virtual workspaces')
virtual_machine = models.OneToOneField(VirtualMachine, on_delete=models.CASCADE, primary_key=True, help_text=_('The virtual machine configuration.'))
starting_at = models.DateTimeField(_('Starting at'), help_text=_('Enter the start date when this machine should be created'))
ending_at = models.DateTimeField(_('Ending at'), help_text=_('Enter the end date when this machine should be terminated'))
status = models.CharField(_('Status'), default=WorkspaceStatus.NEW, max_length=10, choices=WorkspaceStatus.choices, help_text=_('The status of the workspace.'))

14
VRE/apps/vrw/permissions.py

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
from rest_framework import permissions
from django.conf import settings
class IsVRWAPIUser(permissions.BasePermission):
"""
Global user check if the API user is a member of the VRE API group. Else access is denied.
The group needs to be added in the settings and created in the Django admin.
"""
def has_permission(self, request, view):
if request.user and request.user.groups.filter(name=settings.VRW_API_GROUP):
return True
return False

54
VRE/apps/vrw/serializers.py

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
from rest_framework import serializers
from .models import Workspace, WorkspaceStatus
from apps.researcher.models import Researcher
from apps.study.models import Study
from datetime import timedelta
class WorkspaceStudySerializer(serializers.ModelSerializer):
class Meta:
model = Study
fields = ['id','name']
class WorkspaceResearcherSerializer(serializers.ModelSerializer):
# 'Proxy' some data from the user model to our Researcher model.
first_name = serializers.CharField(source='user.first_name')
last_name = serializers.CharField(source='user.last_name')
email_address = serializers.CharField(source='user.email')
# Add P/S number....?
class Meta:
model = Researcher
fields = ['first_name','last_name','email_address']
class WorkspaceSerializer(serializers.Serializer):
id = serializers.IntegerField(source='pk', read_only=True)
study = WorkspaceStudySerializer(source='virtual_machine.study', read_only=True)
researcher = WorkspaceResearcherSerializer(source='virtual_machine.researcher')
type = serializers.CharField(source='virtual_machine.profile', read_only=True)
# cpu = serializers.IntegerField(source='virtual_machine.profile', read_only=True)
disk = serializers.IntegerField(source='virtual_machine.total_storage', read_only=True)
memory = serializers.IntegerField(source='virtual_machine.total_memory', read_only=True)
gpu = serializers.IntegerField(source='virtual_machine.additional_gpu_amount', read_only=True)
request_date = serializers.DateTimeField(source='created_at', read_only=True)
start_date = serializers.DateTimeField(source='starting_at', read_only=True)
end_data = serializers.DateTimeField(source='ending_at', read_only=True)
status = serializers.ChoiceField(choices=WorkspaceStatus.choices)
remote_id = serializers.CharField(read_only=True)
class WorkspaceStatusUpdateSerializer(serializers.Serializer):
status = serializers.ChoiceField(choices=WorkspaceStatus.choices)
remote_id = serializers.CharField(required=False)
def update(self, instance, validated_data):
instance.status = validated_data.get('status', instance.status)
instance.remote_id = validated_data.get('remote_id', instance.remote_id)
instance.save()
return instance

16
VRE/apps/vrw/signals.py

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from apps.virtual_machine.models import VirtualMachine
from .models import Workspace
from django.utils import timezone
from datetime import timedelta
@receiver(post_save, sender=VirtualMachine)
def create_virtual_machine_vrw(sender, instance, created, **kwargs):
# For now, just a stupid OS name test
if not hasattr(instance,'workspace') and 'windows' in instance.operating_system.name.lower():
# And default only for 1 year
new_workspace = Workspace(virtual_machine=instance, starting_at=timezone.now(), ending_at=timezone.now() + timedelta(days=365))
new_workspace.save()

3
VRE/apps/vrw/tests.py

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
VRE/apps/vrw/urls.py

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
from django.urls import path
from .views import WorkspaceStatusUpdate,WorkspaceList, WorkspaceStatusUpdate, WorkspaceDetail
urlpatterns = [
path('list/', WorkspaceList.as_view(), name='workspace-list'),
path('list/<str:status>/', WorkspaceList.as_view(), name='workspace-list-filtered'),
path('<int:pk>/', WorkspaceDetail.as_view(), name='workspace-status-update'),
path('<int:pk>/status/', WorkspaceStatusUpdate.as_view(), name='workspace-status-update')
]

39
VRE/apps/vrw/views.py

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
from rest_framework import generics
from rest_framework.exceptions import NotFound
from .permissions import IsVRWAPIUser
from .models import Workspace, WorkspaceStatus
from .serializers import WorkspaceSerializer, WorkspaceStatusUpdateSerializer
from rest_framework.response import Response
# Create your views here.
class WorkspaceStatusUpdate(generics.UpdateAPIView):
permission_classes = [IsVRWAPIUser]
queryset = Workspace.objects.all()
serializer_class = WorkspaceStatusUpdateSerializer
class WorkspaceList(generics.ListAPIView):
permission_classes = [IsVRWAPIUser]
serializer_class = WorkspaceSerializer
def get_queryset(self):
objects = Workspace.objects
status_filter = self.kwargs.get('status', None)
if status_filter is not None:
try:
status_filter = WorkspaceStatus(status_filter.upper())
except ValueError:
# Invalid status state
raise NotFound(f'Status filter \'{status_filter}\' is not a valid filter.')
objects = objects.filter(status=status_filter)
return objects.order_by('created_at')
class WorkspaceDetail(generics.RetrieveAPIView):
permission_classes = [IsVRWAPIUser]
queryset = Workspace.objects.all()
serializer_class = WorkspaceSerializer

20
VRE/lib/models/cloud.py

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class CloudBasicDataModel(models.Model):
"""
This is an abstract Django model with some general meta fields that can be used for other models.
Attributes
----------
created_at : datetime
The date and time when the model has been created. This will be automatically set once during creating.
updated_at : datetime
The date and time when the model has been updated. This will be automatically updated when the model is updated.
"""
remote_id = models.CharField(max_length=50, blank=True, null=True, help_text=_('The remote ID of this virtual machine on the cloud platform'))
class Meta:
abstract = True
Loading…
Cancel
Save