Merge branch 'master' of code.ungleich.ch:nico/meow-pay

This commit is contained in:
Nico Schottelius 2020-02-26 11:16:46 +01:00
commit df851bee08
32 changed files with 560 additions and 444 deletions

View file

@ -1,11 +0,0 @@
import uuid
from django.db import models
from django.contrib.auth import get_user_model
class VM(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
vmid = models.IntegerField()
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
data = models.CharField(max_length=65536, null=True)

View file

@ -1,59 +0,0 @@
import json
from rest_framework import generics
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from .models import VM
from .serializers import VMSerializer
class VMList(generics.ListAPIView):
authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [IsAuthenticated, IsAdminUser]
queryset = VM.objects.all()
serializer_class = VMSerializer
class VMDetail(generics.RetrieveAPIView):
authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [IsAuthenticated, IsAdminUser]
lookup_field = 'uuid'
queryset = VM.objects.all()
serializer_class = VMSerializer
class UserVMList(generics.ListAPIView):
authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [IsAuthenticated]
serializer_class = VMSerializer
def get_queryset(self):
user_email = self.request.user.ldap_user.attrs.data['mail']
vms = []
for mail in user_email:
vms += VM.objects.filter(owner__username=mail)
for vm in vms:
data = json.loads(vm.data)
vm_template = data['TEMPLATE']
vm.data = {
'cpu': vm_template['VCPU'],
'ram': vm_template['MEMORY'],
'nic': vm_template['NIC'],
'disks': vm_template['DISK']
}
return vms
#######################################
# Following for quick experimentation #
#######################################
# from django.http import HttpResponse
#
# def test(request):
# user_email = request.user.ldap_user.attrs.data['mail']
# vms = []
# for mail in user_email:
# vms += VM.objects.filter(owner__username=mail)
# return HttpResponse("Hello World")

View file

@ -1,125 +0,0 @@
import uuid
from django.db import models
from django.contrib.auth import get_user_model
# Product in DB vs. product in code
# DB:
# - need to define params (+param types) in db -> messy?
# - get /products/ is easy / automatic
#
# code
# - can have serializer/verification of fields easily in DRF
# - can have per product side effects / extra code running
# - might (??) make features easier??
# - how to setup / query the recurring period (?)
# - could get products list via getattr() + re ...Product() classes
# -> this could include the url for ordering => /order/vm_snapshot (params)
# ---> this would work with urlpatterns
# Combination: create specific product in DB (?)
# - a table per product (?) with 1 entry?
# Orders
# define state in DB
# select a price from a product => product might change, order stays
# params:
# - the product uuid or name (?) => productuuid
# - the product parameters => for each feature
#
# logs
# Should have a log = ... => 1:n field for most models!
class Product(models.Model):
# override these fields by default
description = ""
recurring_period = "not_recurring"
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
status = models.CharField(
max_length=256, choices=(
('pending', 'Pending'),
('being_created', 'Being created'),
('created_active', 'Created'),
('deleted', 'Deleted')
),
default='pending'
)
def __str__(self):
return "{}".format(self.name)
class VMSnapshotProduct(Product):
price_per_gb_ssd = 0.35
price_per_gb_hdd = 1.5/100
sample_ssd = 10
sample_hdd = 100
def recurring_price(self):
return 0
def one_time_price(self):
return 0
@classmethod
def sample_price(cls):
return cls.sample_ssd * cls.price_per_gb_ssd + cls.sample_hdd * cls.price_per_gb_hdd
description = "Create snapshot of a VM"
recurring_period = "monthly"
@classmethod
def pricing_model(cls):
return """
Pricing is on monthly basis and storage prices are equivalent to the storage
price in the VM.
Price per GB SSD is: {}
Price per GB HDD is: {}
Sample price for a VM with {} GB SSD and {} GB HDD VM is: {}.
""".format(cls.price_per_gb_ssd, cls.price_per_gb_hdd,
cls.sample_ssd, cls.sample_hdd, cls.sample_price())
gb_ssd = models.FloatField()
gb_hdd = models.FloatField()
class Feature(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=256)
recurring_price = models.FloatField(default=0)
one_time_price = models.FloatField()
product = models.ForeignKey(Product, on_delete=models.CASCADE)
# params for "cpu": cpu_count -> int
# each feature can only have one parameters
# could call this "value" and set whether it is user usable
# has_value = True/False
# value = string -> int (?)
# value_int
# value_str
# value_float
def __str__(self):
return "'{}' - '{}'".format(self.product, self.name)
class Order(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
owner = models.ForeignKey(get_user_model(),
on_delete=models.CASCADE)
product = models.ForeignKey(Product,
on_delete=models.CASCADE)
class VMSnapshotOrder(Order):
pass

View file

@ -1,83 +0,0 @@
from django.shortcuts import render
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from rest_framework import viewsets, permissions, generics
from .serializers import UserSerializer, GroupSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
class CreditCardViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows credit cards to be listed
"""
queryset = get_user_model().objects.all().order_by('-date_joined')
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = get_user_model().objects.all().order_by('-date_joined')
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
class GroupViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
"""
queryset = Group.objects.all()
serializer_class = GroupSerializer
permission_classes = [permissions.IsAuthenticated]
class GroupViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
"""
queryset = Group.objects.all()
serializer_class = GroupSerializer
permission_classes = [permissions.IsAuthenticated]
# POST /vm/snapshot/ vmuuid=... => create snapshot, returns snapshot uuid
# GET /vm/snapshot => list
# DEL /vm/snapshot/<uuid:uuid> => delete
# create-list -> get, post => ListCreateAPIView
# del on other!
class VMSnapshotView(generics.ListCreateAPIView):
#lookup_field = 'uuid'
permission_classes = [permissions.IsAuthenticated]
import inspect
import sys
import re
# Next: create /order/<productname> urls
# Next: strip off "Product" at the end
class ProductsView(APIView):
def get(self, request, format=None):
clsmembers = inspect.getmembers(sys.modules['uncloud_api.models'], inspect.isclass)
products = []
for name, c in clsmembers:
# Include everything that ends in Product, but not Product itself
m = re.match(r'(?P<pname>.+)Product$', name)
if m:
products.append({
'name': m.group('pname'),
'description': c.description,
'recurring_period': c.recurring_period,
'pricing_model': c.pricing_model()
}
)
return Response(products)

1
uncloud/.gitignore vendored
View file

@ -1,3 +1,4 @@
db.sqlite3 db.sqlite3
uncloud/secrets.py uncloud/secrets.py
debug.log debug.log
uncloud/local_settings.py

View file

@ -39,9 +39,27 @@ Then create the database owner by the new role:
postgres=# create database uncloud owner nico; postgres=# create database uncloud owner nico;
``` ```
Installing the postgresql service is os dependent, but some hints:
* Alpine: `apk add postgresql-server && rc-update add postgresql && rc-service postgresql start`
### Secrets ### Secrets
cp `uncloud/secrets_sample.py` to `uncloud/secrets.py` and replace the cp `uncloud/secrets_sample.py` to `uncloud/secrets.py` and replace the
sample values with real values. sample values with real values.
## Flows / Orders
### Creating a VMHost
### Creating a VM
* Create a VMHost
* Create a VM on a VMHost
### Creating a VM Snapshot

View file

@ -1,15 +1,18 @@
import os
import json import json
import uncloud.secrets as secrets
from xmlrpc.client import ServerProxy as RPCClient
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from xmlrpc.client import ServerProxy as RPCClient
from xmltodict import parse from xmltodict import parse
from opennebula.models import VM as VMModel from opennebula.models import VM as VMModel
import uncloud.secrets from django_auth_ldap.backend import LDAPBackend
class Command(BaseCommand): class Command(BaseCommand):
help = 'Syncronize VM information from OpenNebula' help = 'Syncronize VM information from OpenNebula'
@ -18,29 +21,29 @@ class Command(BaseCommand):
pass pass
def handle(self, *args, **options): def handle(self, *args, **options):
with RPCClient(uncloud.secrets.OPENNEBULA_URL) as rpc_client: with RPCClient(secrets.OPENNEBULA_URL) as rpc_client:
success, response, *_ = rpc_client.one.vmpool.infoextended( success, response, *_ = rpc_client.one.vmpool.infoextended(
uncloud.secrets.OPENNEBULA_USER_PASS, -2, -1, -1, -1 secrets.OPENNEBULA_USER_PASS, -2, -1, -1, -1
) )
if success: if success:
vms = json.loads(json.dumps(parse(response)))['VM_POOL']['VM'] vms = json.loads(json.dumps(parse(response)))['VM_POOL']['VM']
for i, vm in enumerate(vms): unknown_user = set()
backend = LDAPBackend()
for vm in vms:
vm_id = vm['ID'] vm_id = vm['ID']
vm_owner_email = vm['UNAME'] vm_owner = vm['UNAME']
try: user = backend.populate_user(username=vm_owner)
user = get_user_model().objects.get(email=vm_owner_email)
except get_user_model().DoesNotExist:
print("Skipping VM import for unknown user with email: {}".format(vm_owner_email))
continue
# user = get_user_model().objects.create_user(username=vm_owner)
if not user:
unknown_user.add(vm_owner)
else:
VMModel.objects.update_or_create( VMModel.objects.update_or_create(
defaults= { 'data': vm, vmid=vm_id,
'owner': user }, defaults={'data': vm, 'owner': user}
vmid=vm_id
) )
print('User not found in ldap:', unknown_user)
else: else:
print(response) print(response)
print(uncloud.secrets.OPENNEBULA_USER_PASS)

View file

@ -1,4 +1,4 @@
# Generated by Django 3.0.3 on 2020-02-23 17:08 # Generated by Django 3.0.3 on 2020-02-23 17:12
from django.conf import settings from django.conf import settings
import django.contrib.postgres.fields.jsonb import django.contrib.postgres.fields.jsonb

View file

@ -0,0 +1,27 @@
# Generated by Django 3.0.3 on 2020-02-25 13:35
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('opennebula', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='vm',
name='uuid',
),
migrations.RemoveField(
model_name='vm',
name='vmid',
),
migrations.AddField(
model_name='vm',
name='id',
field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, unique=True),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.0.3 on 2020-02-25 14:28
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('opennebula', '0002_auto_20200225_1335'),
]
operations = [
migrations.AlterField(
model_name='vm',
name='id',
field=models.CharField(default=uuid.uuid4, max_length=64, primary_key=True, serialize=False, unique=True),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 3.0.3 on 2020-02-25 18:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('opennebula', '0003_auto_20200225_1428'),
]
operations = [
migrations.RemoveField(
model_name='vm',
name='id',
),
migrations.AddField(
model_name='vm',
name='vmid',
field=models.IntegerField(default=42, primary_key=True, serialize=False),
preserve_default=False,
),
]

View file

@ -1,15 +1,17 @@
import uuid import uuid
from django.db import models from django.db import models
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
class VM(models.Model): class VM(models.Model):
vmid = models.IntegerField(primary_key=True) vmid = models.IntegerField(primary_key=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
data = JSONField() data = JSONField()
def save(self, *args, **kwargs):
self.id = 'opennebula' + str(self.data.get("ID"))
super().save(*args, **kwargs)
@property @property
def cores(self): def cores(self):
@ -32,7 +34,6 @@ class VM(models.Model):
disks = [] disks = []
if 'DISK' in self.data['TEMPLATE']: if 'DISK' in self.data['TEMPLATE']:
if type(self.data['TEMPLATE']['DISK']) is dict: if type(self.data['TEMPLATE']['DISK']) is dict:
disks = [ self.data['TEMPLATE']['DISK'] ] disks = [ self.data['TEMPLATE']['DISK'] ]
else: else:
@ -48,3 +49,11 @@ class VM(models.Model):
] ]
return disks return disks
@property
def last_host(self):
return ((self.data.get('HISTORY_RECORDS', {}) or {}).get('HISTORY', {}) or {}).get('HOSTNAME', None)
@property
def graphics(self):
return self.data.get('TEMPLATE', {}).get('GRAPHICS', {})

View file

@ -5,10 +5,10 @@ from opennebula.models import VM
class VMSerializer(serializers.HyperlinkedModelSerializer): class VMSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = VM model = VM
fields = ['vmid', 'owner', 'data'] fields = ['id', 'owner', 'data']
class OpenNebulaVMSerializer(serializers.HyperlinkedModelSerializer): class OpenNebulaVMSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = VM model = VM
fields = ['vmid', 'owner', 'cores', 'ram_in_gb', 'disks' ] fields = ['id', 'owner', 'cores', 'ram_in_gb', 'disks', 'last_host', 'graphics']

View file

@ -1,18 +1,18 @@
from rest_framework import viewsets, generics, permissions from rest_framework import viewsets, permissions
from rest_framework.response import Response from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from django.contrib.auth import get_user_model
from .models import VM from .models import VM
from .serializers import VMSerializer, OpenNebulaVMSerializer from .serializers import VMSerializer, OpenNebulaVMSerializer
class RawVMViewSet(viewsets.ModelViewSet): class RawVMViewSet(viewsets.ModelViewSet):
queryset = VM.objects.all() queryset = VM.objects.all()
serializer_class = VMSerializer serializer_class = VMSerializer
permission_classes = [permissions.IsAdminUser] permission_classes = [permissions.IsAdminUser]
class VMViewSet(viewsets.ModelViewSet): class VMViewSet(viewsets.ViewSet):
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
def list(self, request): def list(self, request):
@ -22,6 +22,6 @@ class VMViewSet(viewsets.ModelViewSet):
def retrieve(self, request, pk=None): def retrieve(self, request, pk=None):
queryset = VM.objects.filter(owner=request.user) queryset = VM.objects.filter(owner=request.user)
user = get_object_or_404(queryset, pk=pk) vm = get_object_or_404(queryset, pk=pk)
serializer = OpenNebulaVMSerializer(queryset) serializer = OpenNebulaVMSerializer(vm, context={'request': request})
return Response(serializer.data) return Response(serializer.data)

View file

@ -12,18 +12,26 @@ https://docs.djangoproject.com/en/3.0/ref/settings/
import os import os
import stripe
import ldap
# Uncommitted file with secrets # Uncommitted file with secrets
import uncloud.secrets import uncloud.secrets
import stripe from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
import ldap
import uncloud.secrets as secrets
from django_auth_ldap.config import LDAPSearch
# Uncommitted file with local settings i.e logging
try:
from uncloud.local_settings import LOGGING, DATABASES
except ModuleNotFoundError:
LOGGING = {}
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': uncloud.secrets.POSTGRESQL_DB_NAME,
}
}
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@ -54,6 +62,7 @@ INSTALLED_APPS = [
'rest_framework', 'rest_framework',
'uncloud_api', 'uncloud_api',
'uncloud_auth', 'uncloud_auth',
'uncloud_vm',
'opennebula' 'opennebula'
] ]
@ -88,8 +97,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'uncloud.wsgi.application' WSGI_APPLICATION = 'uncloud.wsgi.application'
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
@ -123,9 +130,7 @@ AUTH_LDAP_USER_ATTR_MAP = {
AUTH_LDAP_BIND_DN = uncloud.secrets.LDAP_ADMIN_DN AUTH_LDAP_BIND_DN = uncloud.secrets.LDAP_ADMIN_DN
AUTH_LDAP_BIND_PASSWORD = uncloud.secrets.LDAP_ADMIN_PASSWORD AUTH_LDAP_BIND_PASSWORD = uncloud.secrets.LDAP_ADMIN_PASSWORD
AUTH_LDAP_USER_SEARCH = LDAPSearch( AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=ungleich,dc=ch", ldap.SCOPE_SUBTREE, "(uid=%(user)s)")
"dc=ungleich,dc=ch", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"
)
################################################################################ ################################################################################
@ -142,6 +147,7 @@ AUTH_USER_MODEL = 'uncloud_auth.User'
# AUTH/REST # AUTH/REST
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [ 'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
] ]
} }
@ -167,37 +173,4 @@ USE_TZ = True
STATIC_URL = '/static/' STATIC_URL = '/static/'
stripe.api_key = secrets.STRIPE_KEY stripe.api_key = uncloud.secrets.STRIPE_KEY
# FIXME: not sure if we really need this
# LOGGING = {
# 'version': 1,
# 'disable_existing_loggers': False,
# 'handlers': {
# 'file': {
# 'level': 'DEBUG',
# 'class': 'logging.FileHandler',
# 'filename': 'debug.log',
# },
# },
# 'loggers': {
# 'django': {
# 'handlers': ['file'],
# 'level': 'DEBUG',
# 'propagate': True,
# },
# 'django_auth_ldap': {
# 'handlers': ['file'],
# 'level': 'DEBUG',
# 'propagate': True
# }
# },
# }
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': uncloud.secrets.POSTGRESQL_DB_NAME,
}
}

View file

@ -17,20 +17,28 @@ from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from rest_framework import routers from rest_framework import routers
from uncloud_api import views
from uncloud_api import views as apiviews
from uncloud_vm import views as vmviews
from opennebula import views as oneviews from opennebula import views as oneviews
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'opennebula', oneviews.VMViewSet, basename='opennebula') router.register(r'user', apiviews.UserViewSet, basename='user')
router.register(r'opennebula_raw', oneviews.RawVMViewSet)
router.register(r'vm/snapshot', apiviews.VMSnapshotView, basename='VMSnapshot')
router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vmproduct')
# admin/staff urls
router.register(r'admin/vmhost', vmviews.VMHostViewSet)
router.register(r'admin/opennebula', oneviews.VMViewSet, basename='opennebula')
router.register(r'admin/opennebula_raw', oneviews.RawVMViewSet)
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API. # Additionally, we include login URLs for the browsable API.
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
path('admin/', admin.site.urls), path('admin/', admin.site.urls), # login to django itself
path('products/', views.ProductsView.as_view(), name='products'), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) # for login to REST API
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
] ]

View file

@ -1,4 +1,4 @@
# Generated by Django 3.0.3 on 2020-02-23 17:09 # Generated by Django 3.0.3 on 2020-02-23 17:12
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View file

@ -0,0 +1,19 @@
# Generated by Django 3.0.3 on 2020-02-25 18:16
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('uncloud_api', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='vmsnapshotproduct',
name='vm_uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False),
),
]

View file

@ -0,0 +1,36 @@
# Generated by Django 3.0.3 on 2020-02-25 19:50
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('uncloud_api', '0002_vmsnapshotproduct_vm_uuid'),
]
operations = [
migrations.AlterField(
model_name='vmsnapshotproduct',
name='gb_hdd',
field=models.FloatField(editable=False),
),
migrations.AlterField(
model_name='vmsnapshotproduct',
name='gb_ssd',
field=models.FloatField(editable=False),
),
migrations.AlterField(
model_name='vmsnapshotproduct',
name='owner',
field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='vmsnapshotproduct',
name='vm_uuid',
field=models.UUIDField(),
),
]

View file

@ -34,7 +34,8 @@ from django.contrib.auth import get_user_model
class Product(models.Model): class Product(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
owner = models.ForeignKey(get_user_model(), owner = models.ForeignKey(get_user_model(),
on_delete=models.CASCADE) on_delete=models.CASCADE,
editable=False)
# override these fields by default # override these fields by default
@ -52,8 +53,8 @@ class Product(models.Model):
) )
# This is calculated by each product and saved in the DB # This is calculated by each product and saved in the DB
recurring_price = models.FloatField() recurring_price = models.FloatField(editable=False)
one_time_price = models.FloatField() one_time_price = models.FloatField(editable=False)
# FIXME: need recurring_time_frame # FIXME: need recurring_time_frame
@ -69,14 +70,13 @@ class VMSnapshotProduct(Product):
price_per_gb_hdd = 1.5/100 price_per_gb_hdd = 1.5/100
# This we need to get from the VM # This we need to get from the VM
gb_ssd = models.FloatField() gb_ssd = models.FloatField(editable=False)
gb_hdd = models.FloatField() gb_hdd = models.FloatField(editable=False)
vm_uuid = models.UUIDField() vm_uuid = models.UUIDField()
# Need to setup recurring_price and one_time_price and recurring period # Need to setup recurring_price and one_time_price and recurring period
sample_ssd = 10 sample_ssd = 10
sample_hdd = 100 sample_hdd = 100
@ -137,13 +137,3 @@ class Feature(models.Model):
def __str__(self): def __str__(self):
return "'{}' - '{}'".format(self.product, self.name) return "'{}' - '{}'".format(self.product, self.name)
# class Order(models.Model):
# uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# owner = models.ForeignKey(get_user_model(),
# on_delete=models.CASCADE)
# product = models.ForeignKey(Product,
# on_delete=models.CASCADE)

View file

@ -3,17 +3,24 @@ from django.contrib.auth import get_user_model
from rest_framework import serializers from rest_framework import serializers
from .models import VMSnapshotProduct
class UserSerializer(serializers.HyperlinkedModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = get_user_model() model = get_user_model()
fields = ['url', 'username', 'email', 'groups'] fields = ['url', 'username', 'email']
class GroupSerializer(serializers.HyperlinkedModelSerializer): class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Group model = Group
fields = ['url', 'name'] fields = ['url', 'name']
class VMSnapshotSerializer(serializers.Serializer): class VMSnapshotSerializer(serializers.HyperlinkedModelSerializer):
pass class Meta:
model = VMSnapshotProduct
fields = ['uuid', 'status', 'recurring_price', 'one_time_price' ]
class VMSnapshotCreateSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = VMSnapshotProduct
fields = '__all__'

View file

@ -3,52 +3,92 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from rest_framework import viewsets, permissions, generics from rest_framework import viewsets, permissions, generics
from .serializers import UserSerializer, GroupSerializer
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from uncloud_vm.models import VMProduct
from .models import VMSnapshotProduct
from .serializers import UserSerializer, GroupSerializer, VMSnapshotSerializer, VMSnapshotCreateSerializer
import inspect import inspect
import sys import sys
import re import re
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = get_user_model().objects.all().order_by('-date_joined')
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
# POST /vm/snapshot/ vmuuid=... => create snapshot, returns snapshot uuid # POST /vm/snapshot/ vmuuid=... => create snapshot, returns snapshot uuid
# GET /vm/snapshot => list # GET /vm/snapshot => list
# DEL /vm/snapshot/<uuid:uuid> => delete # DEL /vm/snapshot/<uuid:uuid> => delete
# create-list -> get, post => ListCreateAPIView # create-list -> get, post => ListCreateAPIView
# del on other! # del on other!
class VMSnapshotView(generics.ListCreateAPIView): class VMSnapshotView(viewsets.ViewSet):
#lookup_field = 'uuid'
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
def list(self, request):
queryset = VMSnapshotProduct.objects.filter(owner=request.user)
serializer = VMSnapshotSerializer(queryset, many=True, context={'request': request})
return Response(serializer.data)
# Next: create /order/<productname> urls def retrieve(self, request, pk=None):
# Next: strip off "Product" at the end queryset = VMSnapshotProduct.objects.filter(owner=request.user)
class ProductsView(APIView): vm = get_object_or_404(queryset, pk=pk)
def get(self, request, format=None): serializer = VMSnapshotSerializer(vm, context={'request': request})
clsmembers = inspect.getmembers(sys.modules['uncloud_api.models'], inspect.isclass) return Response(serializer.data)
products = []
for name, c in clsmembers: def create(self, request):
# Include everything that ends in Product, but not Product itself print(request.data)
m = re.match(r'(?P<pname>.+)Product$', name) serializer = VMSnapshotCreateSerializer(data=request.data)
if m:
products.append({ serializer.gb_ssd = 12
'name': m.group('pname'), serializer.gb_hdd = 120
'description': c.description, print("F")
'recurring_period': c.recurring_period, serializer.is_valid(raise_exception=True)
'pricing_model': c.pricing_model()
} print(serializer)
) print("A")
serializer.save()
print("B")
return Response(products) # snapshot = VMSnapshotProduct(owner=request.user,
# **serialzer.data)
return Response(serializer.data)
# maybe drop or not --- we need something to guide the user!
# class ProductsViewSet(viewsets.ViewSet):
# permission_classes = [permissions.IsAuthenticated]
# def list(self, request):
# clsmembers = []
# for modules in [ 'uncloud_api.models', 'uncloud_vm.models' ]:
# clsmembers.extend(inspect.getmembers(sys.modules[modules], inspect.isclass))
# products = []
# for name, c in clsmembers:
# # Include everything that ends in Product, but not Product itself
# m = re.match(r'(?P<pname>.+)Product$', name)
# if m:
# products.append({
# 'name': m.group('pname'),
# 'description': c.description,
# 'recurring_period': c.recurring_period,
# 'pricing_model': c.pricing_model()
# }
# )
# return Response(products)
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return self.request.user

View file

@ -1,4 +1,4 @@
# Generated by Django 3.0.3 on 2020-02-23 17:08 # Generated by Django 3.0.3 on 2020-02-23 17:11
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators

View file

@ -0,0 +1,21 @@
import json
import uncloud.secrets as secrets
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from uncloud_vm.models import VMProduct, VMHost
class Command(BaseCommand):
help = 'Select VM Host for VMs'
def add_arguments(self, parser):
pass
def handle(self, *args, **options):
pending_vms = VMProduct.objects.filter(vmhost__isnull=True)
vmhosts = VMHost.objects.filter(status='active')
for vm in pending_vms:
print(vm)
# FIXME: implement smart placement

View file

@ -0,0 +1,26 @@
import json
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from uncloud_vm.models import VMProduct, VMHost
class Command(BaseCommand):
help = 'Check health of VMs and VMHosts'
def add_arguments(self, parser):
pass
def handle(self, *args, **options):
pending_vms = VMProduct.objects.filter(vmhost__isnull=True)
vmhosts = VMHost.objects.filter(status='active')
# 1. Check that all active hosts reported back N seconds ago
# 2. Check that no VM is running on a dead host
# 3. Migrate VMs if necessary
# 4. Check that no VMs have been pending for longer than Y seconds
# If VM snapshots exist without a VM -> notify user (?)
print("Nothing is good, you should implement me")

View file

@ -0,0 +1,75 @@
# Generated by Django 3.0.3 on 2020-02-25 19:50
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='VMDiskProduct',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('size_in_gb', models.FloatField()),
('storage_class', models.CharField(choices=[('hdd', 'HDD'), ('ssd', 'SSD')], default='ssd', max_length=32)),
],
),
migrations.CreateModel(
name='VMHost',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('hostname', models.CharField(max_length=253)),
('physical_cores', models.IntegerField()),
('usable_cores', models.IntegerField()),
('usable_ram_in_gb', models.FloatField()),
('status', models.CharField(choices=[('pending', 'Pending'), ('active', 'Active'), ('unusable', 'Unusable')], default='pending', max_length=32)),
],
),
migrations.CreateModel(
name='VMProduct',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('cores', models.IntegerField()),
('ram_in_gb', models.FloatField()),
('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('vmhost', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMHost')),
],
),
migrations.CreateModel(
name='OperatingSystemDisk',
fields=[
('vmdiskproduct_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='uncloud_vm.VMDiskProduct')),
('os_name', models.CharField(max_length=128)),
],
bases=('uncloud_vm.vmdiskproduct',),
),
migrations.CreateModel(
name='VMWithOSProduct',
fields=[
('vmproduct_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='uncloud_vm.VMProduct')),
],
bases=('uncloud_vm.vmproduct',),
),
migrations.CreateModel(
name='VMNetworkCard',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mac_address', models.IntegerField()),
('vm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMProduct')),
],
),
migrations.AddField(
model_name='vmdiskproduct',
name='vm',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMProduct'),
),
]

View file

@ -0,0 +1,38 @@
# Generated by Django 3.0.3 on 2020-02-25 19:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('uncloud_vm', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='vmhost',
name='hostname',
field=models.CharField(max_length=253, unique=True),
),
migrations.AlterField(
model_name='vmhost',
name='physical_cores',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='vmhost',
name='status',
field=models.CharField(choices=[('pending', 'Pending'), ('active', 'Active'), ('unusable', 'Unusable'), ('deleted', 'Deleted')], default='pending', max_length=32),
),
migrations.AlterField(
model_name='vmhost',
name='usable_cores',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='vmhost',
name='usable_ram_in_gb',
field=models.FloatField(default=0),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.0.3 on 2020-02-25 20:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('uncloud_vm', '0002_auto_20200225_1952'),
]
operations = [
migrations.AlterField(
model_name='vmproduct',
name='vmhost',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMHost'),
),
]

View file

@ -1,20 +1,22 @@
from django.db import models from django.db import models
from django.contrib.auth import get_user_model
import uuid
class VMHost(models.Model): class VMHost(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# 253 is the maximum DNS name length # 253 is the maximum DNS name length
hostname = models.CharField(max_length=253) hostname = models.CharField(max_length=253, unique=True)
# indirectly gives a maximum number of cores / VM - f.i. 32 # indirectly gives a maximum number of cores / VM - f.i. 32
physical_cores = models.IntegerField() physical_cores = models.IntegerField(default=0)
# determines the maximum usable cores - f.i. 320 if you overbook by a factor of 10 # determines the maximum usable cores - f.i. 320 if you overbook by a factor of 10
usable_cores = models.IntegerField() usable_cores = models.IntegerField(default=0)
# ram that can be used of the server # ram that can be used of the server
usable_ram_in_gb = models.FloatField() usable_ram_in_gb = models.FloatField(default=0)
status = models.CharField(max_length=32, status = models.CharField(max_length=32,
@ -22,24 +24,35 @@ class VMHost(models.Model):
('pending', 'Pending'), ('pending', 'Pending'),
('active', 'Active'), ('active', 'Active'),
('unusable', 'Unusable'), ('unusable', 'Unusable'),
('deleted', 'Deleted'),
), ),
default='pending' default='pending'
) )
class VM(models.Model): class VMProduct(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) uuid = models.UUIDField(primary_key=True,
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) default=uuid.uuid4,
editable=False)
owner = models.ForeignKey(get_user_model(),
on_delete=models.CASCADE,
editable=False)
vmhost = models.ForeignKey(VMHost,
on_delete=models.CASCADE,
editable=False,
blank=True,
null=True)
cores = models.IntegerField() cores = models.IntegerField()
ram_in_gb = models.FloatField() ram_in_gb = models.FloatField()
vmhost = models.ForeignKey(VMHost, on_delete=models.CASCADE)
class VMWithOSProduct(VMProduct):
pass
class VMDisk(models.Model): class VMDiskProduct(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
vm = models.ForeignKey(VM, on_delete=models.CASCADE) vm = models.ForeignKey(VMProduct, on_delete=models.CASCADE)
size_in_gb = models.FloatField() size_in_gb = models.FloatField()
storage_class = models.CharField(max_length=32, storage_class = models.CharField(max_length=32,
@ -49,3 +62,12 @@ class VMDisk(models.Model):
), ),
default='ssd' default='ssd'
) )
class OperatingSystemDisk(VMDiskProduct):
""" Defines an Operating System Disk that can be cloned for a VM """
os_name = models.CharField(max_length=128)
class VMNetworkCard(models.Model):
vm = models.ForeignKey(VMProduct, on_delete=models.CASCADE)
mac_address = models.IntegerField()

View file

@ -0,0 +1,15 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
from .models import VMHost, VMProduct
class VMHostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = VMHost
fields = '__all__'
class VMProductSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = VMProduct
fields = '__all__'

View file

@ -1,24 +1,29 @@
from django.shortcuts import render from django.shortcuts import render
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from myapps.serializers import UserSerializer
from rest_framework import viewsets from rest_framework import viewsets, permissions
from rest_framework.response import Response from rest_framework.response import Response
from opennebula.models import VM as OpenNebulaVM from .models import VMHost, VMProduct
from .serializers import VMHostSerializer, VMProductSerializer
class VMViewSet(viewsets.ViewSet): class VMHostViewSet(viewsets.ModelViewSet):
def list(self, request): serializer_class = VMHostSerializer
queryset = User.objects.all() queryset = VMHost.objects.all()
serializer = UserSerializer(queryset, many=True) permission_classes = [permissions.IsAdminUser]
return Response(serializer.data)
def retrieve(self, request, pk=None):
queryset = User.objects.all()
user = get_object_or_404(queryset, pk=pk)
serializer = UserSerializer(user)
return Response(serializer.data)
class VMProductViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
serializer_class = VMProductSerializer
def get_queryset(self):
return VMProduct.objects.filter(owner=self.request.user)
def create(self, request):
serializer = VMProductSerializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
serializer.save(owner=request.user)
return Response(serializer.data)