From 9410b7c56b61a2226871d263b5e42c862b0824f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 13:51:31 +0200 Subject: [PATCH 001/194] Make VM order-able again --- .../uncloud/uncloud_vm/models.py | 15 +-- .../uncloud/uncloud_vm/serializers.py | 92 ++++++++----- .../uncloud/uncloud_vm/views.py | 125 ++++++++++-------- 3 files changed, 132 insertions(+), 100 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_vm/models.py b/uncloud_django_based/uncloud/uncloud_vm/models.py index f56ed0d..5dacdbe 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/models.py +++ b/uncloud_django_based/uncloud/uncloud_vm/models.py @@ -72,6 +72,7 @@ class VMProduct(Product): primary_disk = models.ForeignKey('VMDiskProduct', on_delete=models.CASCADE, null=True) # Default recurring price is PER_MONTH, see uncloud_pay.models.Product. + @property def recurring_price(self): return self.cores * 3 + self.ram_in_gb * 4 @@ -153,17 +154,9 @@ class VMDiskProduct(Product): def description(self): return "Disk for VM '{}': {}GB".format(self.vm.name, self.size_in_gb) - # TODO: move magic numbers in variables - def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH): - # TODO: move magic numbers in variables - if recurring_period == RecurringPeriod.PER_MONTH: - return (self.size_in_gb / 10) * 3.5 - if recurring_period == RecurringPeriod.PER_YEAR: - return recurring_price(self, recurring_period.PER_MONTH) * 12 - if recurring_period == RecurringPeriod.PER_HOUR: - return recurring_price(self, recurring_period.PER_MONTH) / 25 - else: - raise Exception('Invalid recurring period for VM Disk Product pricing.') + @property + def recurring_price(self): + return (self.size_in_gb / 10) * 3.5 # Sample code for clean method diff --git a/uncloud_django_based/uncloud/uncloud_vm/serializers.py b/uncloud_django_based/uncloud/uncloud_vm/serializers.py index 9435de2..92c7fe8 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_vm/serializers.py @@ -3,7 +3,9 @@ from django.contrib.auth import get_user_model from rest_framework import serializers from .models import VMHost, VMProduct, VMSnapshotProduct, VMDiskProduct, VMDiskImageProduct, VMCluster -from uncloud_pay.models import RecurringPeriod +from uncloud_pay.models import RecurringPeriod, BillingAddress + +# XXX: does not seem to be used? GB_SSD_PER_DAY=0.012 GB_HDD_PER_DAY=0.0006 @@ -11,6 +13,8 @@ GB_HDD_PER_DAY=0.0006 GB_SSD_PER_DAY=0.012 GB_HDD_PER_DAY=0.0006 +### +# Admin views. class VMHostSerializer(serializers.HyperlinkedModelSerializer): vms = serializers.PrimaryKeyRelatedField(many=True, read_only=True) @@ -26,6 +30,9 @@ class VMClusterSerializer(serializers.HyperlinkedModelSerializer): fields = '__all__' +### +# Disks. + class VMDiskProductSerializer(serializers.ModelSerializer): class Meta: model = VMDiskProduct @@ -46,30 +53,6 @@ class VMDiskImageProductSerializer(serializers.ModelSerializer): model = VMDiskImageProduct fields = '__all__' -class DCLVMProductSerializer(serializers.HyperlinkedModelSerializer): - """ - Create an interface similar to standard DCL - """ - - # Custom field used at creation (= ordering) only. - recurring_period = serializers.ChoiceField( - choices=VMProduct.allowed_recurring_periods()) - - os_disk_uuid = serializers.UUIDField() - # os_disk_size = - - class Meta: - model = VMProduct - -class ManagedVMProductSerializer(serializers.ModelSerializer): - """ - Managed VM serializer used in ungleich_service app. - """ - primary_disk = CreateManagedVMDiskProductSerializer() - class Meta: - model = VMProduct - fields = [ 'cores', 'ram_in_gb', 'primary_disk'] - class VMSnapshotProductSerializer(serializers.ModelSerializer): class Meta: model = VMSnapshotProduct @@ -93,22 +76,61 @@ class VMSnapshotProductSerializer(serializers.ModelSerializer): pricing['per_gb_hdd'] = 0.0006 pricing['recurring_period'] = 'per_day' +### +# VMs + +# Helper used in uncloud_service for services allocating VM. +class ManagedVMProductSerializer(serializers.ModelSerializer): + """ + Managed VM serializer used in ungleich_service app. + """ + primary_disk = CreateManagedVMDiskProductSerializer() + class Meta: + model = VMProduct + fields = [ 'cores', 'ram_in_gb', 'primary_disk'] class VMProductSerializer(serializers.HyperlinkedModelSerializer): - # Custom field used at creation (= ordering) only. - recurring_period = serializers.ChoiceField( - choices=VMProduct.allowed_recurring_periods()) primary_disk = CreateVMDiskProductSerializer() + snapshots = VMSnapshotProductSerializer(many=True, read_only=True) + disks = VMDiskProductSerializer(many=True, read_only=True) class Meta: model = VMProduct - fields = ['uuid', 'order', 'owner', 'status', 'name', \ - 'cores', 'ram_in_gb', 'recurring_period', 'primary_disk', - 'snapshots', 'disks', 'extra_data' ] + fields = ['uuid', 'order', 'owner', 'status', 'name', 'cores', + 'ram_in_gb', 'primary_disk', 'snapshots', 'disks', 'extra_data'] read_only_fields = ['uuid', 'order', 'owner', 'status'] - snapshots = VMSnapshotProductSerializer(many=True, - read_only=True) +class OrderVMProductSerializer(VMProductSerializer): + recurring_period = serializers.ChoiceField( + choices=VMProduct.allowed_recurring_periods()) - disks = VMDiskProductSerializer(many=True, - read_only=True) + def __init__(self, *args, **kwargs): + super(VMProductSerializer, self).__init__(*args, **kwargs) + self.fields['billing_address'] = serializers.ChoiceField( + choices=BillingAddress.get_addresses_for( + self.context['request'].user) + ) + + class Meta: + model = VMProductSerializer.Meta.model + fields = VMProductSerializer.Meta.fields + [ + 'recurring_period', 'billing_address' + ] + read_only_fields = VMProductSerializer.Meta.read_only_fields + +# Nico's playground. + +class DCLVMProductSerializer(serializers.HyperlinkedModelSerializer): + """ + Create an interface similar to standard DCL + """ + + # Custom field used at creation (= ordering) only. + recurring_period = serializers.ChoiceField( + choices=VMProduct.allowed_recurring_periods()) + + os_disk_uuid = serializers.UUIDField() + # os_disk_size = + + class Meta: + model = VMProduct diff --git a/uncloud_django_based/uncloud/uncloud_vm/views.py b/uncloud_django_based/uncloud/uncloud_vm/views.py index 71ffe6d..1dead62 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/views.py +++ b/uncloud_django_based/uncloud/uncloud_vm/views.py @@ -15,20 +15,12 @@ from uncloud_pay.models import Order from .serializers import * from uncloud_pay.helpers import ProductViewSet - import datetime -class VMHostViewSet(viewsets.ModelViewSet): - serializer_class = VMHostSerializer - queryset = VMHost.objects.all() - permission_classes = [permissions.IsAdminUser] +### +# Generic disk image views. Do not require orders / billing. -class VMClusterViewSet(viewsets.ModelViewSet): - serializer_class = VMClusterSerializer - queryset = VMCluster.objects.all() - permission_classes = [permissions.IsAdminUser] - -class VMDiskImageProductViewSet(viewsets.ModelViewSet): +class VMDiskImageProductViewSet(ProductViewSet): permission_classes = [permissions.IsAuthenticated] serializer_class = VMDiskImageProductSerializer @@ -53,7 +45,6 @@ class VMDiskImageProductViewSet(viewsets.ModelViewSet): serializer.save(owner=request.user) return Response(serializer.data) - class VMDiskImageProductPublicViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [permissions.IsAuthenticated] serializer_class = VMDiskImageProductSerializer @@ -61,6 +52,9 @@ class VMDiskImageProductPublicViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): return VMDiskImageProduct.objects.filter(is_public=True) +### +# User VM disk and snapshots. + class VMDiskProductViewSet(viewsets.ModelViewSet): """ Let a user modify their own VMDisks @@ -92,48 +86,6 @@ class VMDiskProductViewSet(viewsets.ModelViewSet): serializer.save(owner=request.user, size_in_gb=size_in_gb) return Response(serializer.data) - - -class VMProductViewSet(ProductViewSet): - permission_classes = [permissions.IsAuthenticated] - serializer_class = VMProductSerializer - - def get_queryset(self): - if self.request.user.is_superuser: - obj = VMProduct.objects.all() - else: - obj = VMProduct.objects.filter(owner=self.request.user) - - return obj - - # Use a database transaction so that we do not get half-created structure - # if something goes wrong. - @transaction.atomic - def create(self, request): - # Extract serializer data. - serializer = VMProductSerializer(data=request.data, context={'request': request}) - serializer.is_valid(raise_exception=True) - order_recurring_period = serializer.validated_data.pop("recurring_period") - - # Create base order. - order = Order( - recurring_period=order_recurring_period, - owner=request.user, - starting_date=timezone.now() - ) - - # Create disk image. - disk = VMDiskProduct(owner=request.user, order=order, - **serializer.validated_data.pop("primary_disk")) - - # Create VM. - vm = serializer.save(owner=request.user, order=order, primary_disk=disk) - disk.vm = vm - disk.save() - - return Response(serializer.data) - - class VMSnapshotProductViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] serializer_class = VMSnapshotProductSerializer @@ -176,7 +128,72 @@ class VMSnapshotProductViewSet(viewsets.ModelViewSet): return Response(serializer.data) +### +# User VMs. +class VMProductViewSet(ProductViewSet): + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + if self.request.user.is_superuser: + obj = VMProduct.objects.all() + else: + obj = VMProduct.objects.filter(owner=self.request.user) + + return obj + + def get_serializer_class(self): + if self.action == 'create': + return OrderVMProductSerializer + else: + return VMProductSerializer + + # Use a database transaction so that we do not get half-created structure + # if something goes wrong. + @transaction.atomic + def create(self, request): + # Extract serializer data. + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + order_recurring_period = serializer.validated_data.pop("recurring_period") + order_billing_address = serializer.validated_data.pop("billing_address") + + # Create base order. + order = Order( + recurring_period=order_recurring_period, + billing_address=order_billing_address, + owner=request.user, + starting_date=timezone.now() + ) + order.save() + + # Create disk image. + disk = VMDiskProduct(owner=request.user, order=order, + **serializer.validated_data.pop("primary_disk")) + + # Create VM. + vm = serializer.save(owner=request.user, order=order, primary_disk=disk) + disk.vm = vm + disk.save() + + return Response(VMProductSerializer(vm, context={'request': request}).data) + + +### +# Admin stuff. + +class VMHostViewSet(viewsets.ModelViewSet): + serializer_class = VMHostSerializer + queryset = VMHost.objects.all() + permission_classes = [permissions.IsAdminUser] + +class VMClusterViewSet(viewsets.ModelViewSet): + serializer_class = VMClusterSerializer + queryset = VMCluster.objects.all() + permission_classes = [permissions.IsAdminUser] + +## +# Nico's playground. # Also create: # - /dcl/available_os From a15952862ad027efe9b823ffea6b0d3fd5bf067b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 13:51:31 +0200 Subject: [PATCH 002/194] Make VM order-able again --- .../uncloud/uncloud_vm/models.py | 15 +-- .../uncloud/uncloud_vm/serializers.py | 92 ++++++++----- .../uncloud/uncloud_vm/views.py | 125 ++++++++++-------- 3 files changed, 132 insertions(+), 100 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_vm/models.py b/uncloud_django_based/uncloud/uncloud_vm/models.py index f56ed0d..5dacdbe 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/models.py +++ b/uncloud_django_based/uncloud/uncloud_vm/models.py @@ -72,6 +72,7 @@ class VMProduct(Product): primary_disk = models.ForeignKey('VMDiskProduct', on_delete=models.CASCADE, null=True) # Default recurring price is PER_MONTH, see uncloud_pay.models.Product. + @property def recurring_price(self): return self.cores * 3 + self.ram_in_gb * 4 @@ -153,17 +154,9 @@ class VMDiskProduct(Product): def description(self): return "Disk for VM '{}': {}GB".format(self.vm.name, self.size_in_gb) - # TODO: move magic numbers in variables - def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH): - # TODO: move magic numbers in variables - if recurring_period == RecurringPeriod.PER_MONTH: - return (self.size_in_gb / 10) * 3.5 - if recurring_period == RecurringPeriod.PER_YEAR: - return recurring_price(self, recurring_period.PER_MONTH) * 12 - if recurring_period == RecurringPeriod.PER_HOUR: - return recurring_price(self, recurring_period.PER_MONTH) / 25 - else: - raise Exception('Invalid recurring period for VM Disk Product pricing.') + @property + def recurring_price(self): + return (self.size_in_gb / 10) * 3.5 # Sample code for clean method diff --git a/uncloud_django_based/uncloud/uncloud_vm/serializers.py b/uncloud_django_based/uncloud/uncloud_vm/serializers.py index 9435de2..92c7fe8 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_vm/serializers.py @@ -3,7 +3,9 @@ from django.contrib.auth import get_user_model from rest_framework import serializers from .models import VMHost, VMProduct, VMSnapshotProduct, VMDiskProduct, VMDiskImageProduct, VMCluster -from uncloud_pay.models import RecurringPeriod +from uncloud_pay.models import RecurringPeriod, BillingAddress + +# XXX: does not seem to be used? GB_SSD_PER_DAY=0.012 GB_HDD_PER_DAY=0.0006 @@ -11,6 +13,8 @@ GB_HDD_PER_DAY=0.0006 GB_SSD_PER_DAY=0.012 GB_HDD_PER_DAY=0.0006 +### +# Admin views. class VMHostSerializer(serializers.HyperlinkedModelSerializer): vms = serializers.PrimaryKeyRelatedField(many=True, read_only=True) @@ -26,6 +30,9 @@ class VMClusterSerializer(serializers.HyperlinkedModelSerializer): fields = '__all__' +### +# Disks. + class VMDiskProductSerializer(serializers.ModelSerializer): class Meta: model = VMDiskProduct @@ -46,30 +53,6 @@ class VMDiskImageProductSerializer(serializers.ModelSerializer): model = VMDiskImageProduct fields = '__all__' -class DCLVMProductSerializer(serializers.HyperlinkedModelSerializer): - """ - Create an interface similar to standard DCL - """ - - # Custom field used at creation (= ordering) only. - recurring_period = serializers.ChoiceField( - choices=VMProduct.allowed_recurring_periods()) - - os_disk_uuid = serializers.UUIDField() - # os_disk_size = - - class Meta: - model = VMProduct - -class ManagedVMProductSerializer(serializers.ModelSerializer): - """ - Managed VM serializer used in ungleich_service app. - """ - primary_disk = CreateManagedVMDiskProductSerializer() - class Meta: - model = VMProduct - fields = [ 'cores', 'ram_in_gb', 'primary_disk'] - class VMSnapshotProductSerializer(serializers.ModelSerializer): class Meta: model = VMSnapshotProduct @@ -93,22 +76,61 @@ class VMSnapshotProductSerializer(serializers.ModelSerializer): pricing['per_gb_hdd'] = 0.0006 pricing['recurring_period'] = 'per_day' +### +# VMs + +# Helper used in uncloud_service for services allocating VM. +class ManagedVMProductSerializer(serializers.ModelSerializer): + """ + Managed VM serializer used in ungleich_service app. + """ + primary_disk = CreateManagedVMDiskProductSerializer() + class Meta: + model = VMProduct + fields = [ 'cores', 'ram_in_gb', 'primary_disk'] class VMProductSerializer(serializers.HyperlinkedModelSerializer): - # Custom field used at creation (= ordering) only. - recurring_period = serializers.ChoiceField( - choices=VMProduct.allowed_recurring_periods()) primary_disk = CreateVMDiskProductSerializer() + snapshots = VMSnapshotProductSerializer(many=True, read_only=True) + disks = VMDiskProductSerializer(many=True, read_only=True) class Meta: model = VMProduct - fields = ['uuid', 'order', 'owner', 'status', 'name', \ - 'cores', 'ram_in_gb', 'recurring_period', 'primary_disk', - 'snapshots', 'disks', 'extra_data' ] + fields = ['uuid', 'order', 'owner', 'status', 'name', 'cores', + 'ram_in_gb', 'primary_disk', 'snapshots', 'disks', 'extra_data'] read_only_fields = ['uuid', 'order', 'owner', 'status'] - snapshots = VMSnapshotProductSerializer(many=True, - read_only=True) +class OrderVMProductSerializer(VMProductSerializer): + recurring_period = serializers.ChoiceField( + choices=VMProduct.allowed_recurring_periods()) - disks = VMDiskProductSerializer(many=True, - read_only=True) + def __init__(self, *args, **kwargs): + super(VMProductSerializer, self).__init__(*args, **kwargs) + self.fields['billing_address'] = serializers.ChoiceField( + choices=BillingAddress.get_addresses_for( + self.context['request'].user) + ) + + class Meta: + model = VMProductSerializer.Meta.model + fields = VMProductSerializer.Meta.fields + [ + 'recurring_period', 'billing_address' + ] + read_only_fields = VMProductSerializer.Meta.read_only_fields + +# Nico's playground. + +class DCLVMProductSerializer(serializers.HyperlinkedModelSerializer): + """ + Create an interface similar to standard DCL + """ + + # Custom field used at creation (= ordering) only. + recurring_period = serializers.ChoiceField( + choices=VMProduct.allowed_recurring_periods()) + + os_disk_uuid = serializers.UUIDField() + # os_disk_size = + + class Meta: + model = VMProduct diff --git a/uncloud_django_based/uncloud/uncloud_vm/views.py b/uncloud_django_based/uncloud/uncloud_vm/views.py index 71ffe6d..1dead62 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/views.py +++ b/uncloud_django_based/uncloud/uncloud_vm/views.py @@ -15,20 +15,12 @@ from uncloud_pay.models import Order from .serializers import * from uncloud_pay.helpers import ProductViewSet - import datetime -class VMHostViewSet(viewsets.ModelViewSet): - serializer_class = VMHostSerializer - queryset = VMHost.objects.all() - permission_classes = [permissions.IsAdminUser] +### +# Generic disk image views. Do not require orders / billing. -class VMClusterViewSet(viewsets.ModelViewSet): - serializer_class = VMClusterSerializer - queryset = VMCluster.objects.all() - permission_classes = [permissions.IsAdminUser] - -class VMDiskImageProductViewSet(viewsets.ModelViewSet): +class VMDiskImageProductViewSet(ProductViewSet): permission_classes = [permissions.IsAuthenticated] serializer_class = VMDiskImageProductSerializer @@ -53,7 +45,6 @@ class VMDiskImageProductViewSet(viewsets.ModelViewSet): serializer.save(owner=request.user) return Response(serializer.data) - class VMDiskImageProductPublicViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [permissions.IsAuthenticated] serializer_class = VMDiskImageProductSerializer @@ -61,6 +52,9 @@ class VMDiskImageProductPublicViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): return VMDiskImageProduct.objects.filter(is_public=True) +### +# User VM disk and snapshots. + class VMDiskProductViewSet(viewsets.ModelViewSet): """ Let a user modify their own VMDisks @@ -92,48 +86,6 @@ class VMDiskProductViewSet(viewsets.ModelViewSet): serializer.save(owner=request.user, size_in_gb=size_in_gb) return Response(serializer.data) - - -class VMProductViewSet(ProductViewSet): - permission_classes = [permissions.IsAuthenticated] - serializer_class = VMProductSerializer - - def get_queryset(self): - if self.request.user.is_superuser: - obj = VMProduct.objects.all() - else: - obj = VMProduct.objects.filter(owner=self.request.user) - - return obj - - # Use a database transaction so that we do not get half-created structure - # if something goes wrong. - @transaction.atomic - def create(self, request): - # Extract serializer data. - serializer = VMProductSerializer(data=request.data, context={'request': request}) - serializer.is_valid(raise_exception=True) - order_recurring_period = serializer.validated_data.pop("recurring_period") - - # Create base order. - order = Order( - recurring_period=order_recurring_period, - owner=request.user, - starting_date=timezone.now() - ) - - # Create disk image. - disk = VMDiskProduct(owner=request.user, order=order, - **serializer.validated_data.pop("primary_disk")) - - # Create VM. - vm = serializer.save(owner=request.user, order=order, primary_disk=disk) - disk.vm = vm - disk.save() - - return Response(serializer.data) - - class VMSnapshotProductViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] serializer_class = VMSnapshotProductSerializer @@ -176,7 +128,72 @@ class VMSnapshotProductViewSet(viewsets.ModelViewSet): return Response(serializer.data) +### +# User VMs. +class VMProductViewSet(ProductViewSet): + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + if self.request.user.is_superuser: + obj = VMProduct.objects.all() + else: + obj = VMProduct.objects.filter(owner=self.request.user) + + return obj + + def get_serializer_class(self): + if self.action == 'create': + return OrderVMProductSerializer + else: + return VMProductSerializer + + # Use a database transaction so that we do not get half-created structure + # if something goes wrong. + @transaction.atomic + def create(self, request): + # Extract serializer data. + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + order_recurring_period = serializer.validated_data.pop("recurring_period") + order_billing_address = serializer.validated_data.pop("billing_address") + + # Create base order. + order = Order( + recurring_period=order_recurring_period, + billing_address=order_billing_address, + owner=request.user, + starting_date=timezone.now() + ) + order.save() + + # Create disk image. + disk = VMDiskProduct(owner=request.user, order=order, + **serializer.validated_data.pop("primary_disk")) + + # Create VM. + vm = serializer.save(owner=request.user, order=order, primary_disk=disk) + disk.vm = vm + disk.save() + + return Response(VMProductSerializer(vm, context={'request': request}).data) + + +### +# Admin stuff. + +class VMHostViewSet(viewsets.ModelViewSet): + serializer_class = VMHostSerializer + queryset = VMHost.objects.all() + permission_classes = [permissions.IsAdminUser] + +class VMClusterViewSet(viewsets.ModelViewSet): + serializer_class = VMClusterSerializer + queryset = VMCluster.objects.all() + permission_classes = [permissions.IsAdminUser] + +## +# Nico's playground. # Also create: # - /dcl/available_os From 94932edebef2b2c0b53b0516a312b6be55aa0af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 15:11:02 +0200 Subject: [PATCH 003/194] Add user admin endpoint, import from LDAP --- uncloud_django_based/uncloud/uncloud/urls.py | 3 +- .../uncloud/uncloud_auth/serializers.py | 9 ++-- .../uncloud/uncloud_auth/views.py | 42 +++++++++++++++---- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud/urls.py b/uncloud_django_based/uncloud/uncloud/urls.py index 14a87e8..4d0ada1 100644 --- a/uncloud_django_based/uncloud/uncloud/urls.py +++ b/uncloud_django_based/uncloud/uncloud/urls.py @@ -67,12 +67,11 @@ router.register(r'admin/order', payviews.AdminOrderViewSet, basename='admin/orde router.register(r'admin/vmhost', vmviews.VMHostViewSet) router.register(r'admin/vmcluster', vmviews.VMClusterViewSet) router.register(r'admin/vpnpool', netviews.VPNPoolViewSet) - router.register(r'admin/opennebula', oneviews.VMViewSet, basename='opennebula') # User/Account router.register(r'user', authviews.UserViewSet, basename='user') - +router.register(r'admin/user', authviews.AdminUserViewSet, basename='useradmin') urlpatterns = [ path('', include(router.urls)), diff --git a/uncloud_django_based/uncloud/uncloud_auth/serializers.py b/uncloud_django_based/uncloud/uncloud_auth/serializers.py index de369c3..71aeb03 100644 --- a/uncloud_django_based/uncloud/uncloud_auth/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_auth/serializers.py @@ -5,11 +5,12 @@ from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS class UserSerializer(serializers.ModelSerializer): + balance = serializers.DecimalField(max_digits=AMOUNT_MAX_DIGITS, + decimal_places=AMOUNT_DECIMALS) + class Meta: model = get_user_model() fields = ['username', 'email', 'balance', 'maximum_credit' ] - - - balance = serializers.DecimalField(max_digits=AMOUNT_MAX_DIGITS, - decimal_places=AMOUNT_DECIMALS) +class ImportUserSerializer(serializers.Serializer): + username = serializers.CharField() diff --git a/uncloud_django_based/uncloud/uncloud_auth/views.py b/uncloud_django_based/uncloud/uncloud_auth/views.py index 2f78e1f..9c5bd1f 100644 --- a/uncloud_django_based/uncloud/uncloud_auth/views.py +++ b/uncloud_django_based/uncloud/uncloud_auth/views.py @@ -1,17 +1,41 @@ from rest_framework import viewsets, permissions, status from .serializers import * +from django_auth_ldap.backend import LDAPBackend +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework import mixins -class UserViewSet(viewsets.ReadOnlyModelViewSet): +class UserViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + permission_classes = [permissions.IsAuthenticated] serializer_class = UserSerializer + + def list(self, request, format=None): + # This is a bit stupid: we have a user, we create a queryset by + # matching on the username. But I don't know a "nicer" way. + # Nico, 2020-03-18 + user = get_user_model().objects.get( + username=self.request.user.username) + serializer = self.get_serializer(user, context = {'request': request}) + return Response(serializer.data) + +class AdminUserViewSet(viewsets.ReadOnlyModelViewSet): + # FIXME: make this admin permission_classes = [permissions.IsAuthenticated] - def get_queryset(self): - if self.request.user.is_superuser: - obj = get_user_model().objects.all() + def get_serializer_class(self): + if self.action == 'import_from_ldap': + return ImportUserSerializer else: - # This is a bit stupid: we have a user, we create a queryset by - # matching on the username. But I don't know a "nicer" way. - # Nico, 2020-03-18 - obj = get_user_model().objects.filter(username=self.request.user.username) + return UserSerializer - return obj + def get_queryset(self): + return get_user_model().objects.all() + + @action(detail=False, methods=['post'], url_path='import_from_ldap') + def import_from_ldap(self, request, pk=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + ldap_username = serializer.validated_data.pop("username") + user = LDAPBackend().populate_user(ldap_username) + + return Response(UserSerializer(user, context = {'request': request}).data) From 1cf20a2cb6c84b2db79d2e45e49d5fbb81b392e7 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Mon, 27 Apr 2020 18:25:27 +0200 Subject: [PATCH 004/194] Disable vat validator to get project back running --- uncloud_django_based/uncloud/uncloud_pay/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/views.py b/uncloud_django_based/uncloud/uncloud_pay/views.py index aaf90e2..82b5787 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/views.py +++ b/uncloud_django_based/uncloud/uncloud_pay/views.py @@ -7,8 +7,8 @@ from rest_framework.response import Response from rest_framework.decorators import action from rest_framework.reverse import reverse from rest_framework.decorators import renderer_classes -from vat_validator import validate_vat, vies -from vat_validator.countries import EU_COUNTRY_CODES +#from vat_validator import validate_vat, vies +#from vat_validator.countries import EU_COUNTRY_CODES import json import logging @@ -16,7 +16,7 @@ import logging from .models import * from .serializers import * from datetime import datetime -from vat_validator import sanitize_vat +#from vat_validator import sanitize_vat import uncloud_pay.stripe as uncloud_stripe logger = logging.getLogger(__name__) From 62d9ccbbef06706dcb7467d693ba31d732c882a7 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Mon, 27 Apr 2020 18:25:44 +0200 Subject: [PATCH 005/194] [vpn] begin to introduce save() method The save() and delete() method will create/manage the orders --- uncloud_django_based/uncloud/uncloud_net/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_net/models.py b/uncloud_django_based/uncloud/uncloud_net/models.py index 26a6eb8..8dfff05 100644 --- a/uncloud_django_based/uncloud/uncloud_net/models.py +++ b/uncloud_django_based/uncloud/uncloud_net/models.py @@ -173,11 +173,11 @@ class VPNNetwork(Product): wireguard_public_key = models.CharField(max_length=48) + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + def delete(self, *args, **kwargs): self.network.status = 'free' self.network.save() super().save(*args, **kwargs) print("deleted {}".format(self)) - -# managing deletion -# - record free network (?) From 2cda6441f41776d4605bf141b97c67b4baa767a7 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 2 May 2020 00:16:29 +0200 Subject: [PATCH 006/194] Refactor secret / local settings handling --- .../uncloud/uncloud/secrets_sample.py | 21 ------ .../uncloud/uncloud/settings.py | 73 +++++++++++-------- .../uncloud/uncloud_net/models.py | 3 - .../uncloud/uncloud_pay/models.py | 15 +++- .../uncloud/uncloud_pay/stripe.py | 8 +- 5 files changed, 60 insertions(+), 60 deletions(-) delete mode 100644 uncloud_django_based/uncloud/uncloud/secrets_sample.py diff --git a/uncloud_django_based/uncloud/uncloud/secrets_sample.py b/uncloud_django_based/uncloud/uncloud/secrets_sample.py deleted file mode 100644 index 150fefb..0000000 --- a/uncloud_django_based/uncloud/uncloud/secrets_sample.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.core.management.utils import get_random_secret_key - -# XML-RPC interface of opennebula -OPENNEBULA_URL = 'https://opennebula.ungleich.ch:2634/RPC2' - -# user:pass for accessing opennebula -OPENNEBULA_USER_PASS = 'user:password' - -POSTGRESQL_DB_NAME="uncloud" - -# See https://django-auth-ldap.readthedocs.io/en/latest/authentication.html -LDAP_ADMIN_DN="" -LDAP_ADMIN_PASSWORD="" -LDAP_SERVER_URI = "" - -# Stripe (Credit Card payments) -STRIPE_KEY="" -STRIPE_PUBLIC_KEY="" - -# The django secret key -SECRET_KEY=get_random_secret_key() diff --git a/uncloud_django_based/uncloud/uncloud/settings.py b/uncloud_django_based/uncloud/uncloud/settings.py index b525073..527749d 100644 --- a/uncloud_django_based/uncloud/uncloud/settings.py +++ b/uncloud_django_based/uncloud/uncloud/settings.py @@ -13,41 +13,32 @@ https://docs.djangoproject.com/en/3.0/ref/settings/ import os import ldap -# Uncommitted file with secrets -import uncloud.secrets - +from django.core.management.utils import get_random_secret_key from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion -# 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, - 'HOST': os.environ.get('DATABASE_HOST', '::1'), - 'USER': os.environ.get('DATABASE_USER', 'postgres'), - } - } + +LOGGING = {} + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# https://docs.djangoproject.com/en/3.0/ref/settings/#databases +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = uncloud.secrets.SECRET_KEY - # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] # Application definition @@ -123,7 +114,12 @@ AUTH_PASSWORD_VALIDATORS = [ ################################################################################ # AUTH/LDAP -AUTH_LDAP_SERVER_URI = uncloud.secrets.LDAP_SERVER_URI +AUTH_LDAP_SERVER_URI = "" +AUTH_LDAP_BIND_DN = "" +AUTH_LDAP_BIND_PASSWORD = "" +AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com", + ldap.SCOPE_SUBTREE, + "(uid=%(user)s)") AUTH_LDAP_USER_ATTR_MAP = { "first_name": "givenName", @@ -131,13 +127,6 @@ AUTH_LDAP_USER_ATTR_MAP = { "email": "mail" } - -AUTH_LDAP_BIND_DN = uncloud.secrets.LDAP_ADMIN_DN -AUTH_LDAP_BIND_PASSWORD = uncloud.secrets.LDAP_ADMIN_PASSWORD - -AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=ungleich,dc=ch", ldap.SCOPE_SUBTREE, "(uid=%(user)s)") - - ################################################################################ # AUTH/Django AUTHENTICATION_BACKENDS = [ @@ -158,7 +147,6 @@ REST_FRAMEWORK = { } - # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ @@ -177,3 +165,28 @@ USE_TZ = True # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static") ] + +# XML-RPC interface of opennebula +OPENNEBULA_URL = 'https://opennebula.example.com:2634/RPC2' + +# user:pass for accessing opennebula +OPENNEBULA_USER_PASS = 'user:password' + +# See https://django-auth-ldap.readthedocs.io/en/latest/authentication.html +LDAP_ADMIN_DN="" +LDAP_ADMIN_PASSWORD="" +LDAP_SERVER_URI = "" + +# Stripe (Credit Card payments) +STRIPE_KEY="" +STRIPE_PUBLIC_KEY="" + +# The django secret key +SECRET_KEY=get_random_secret_key() + + +# Overwrite settings with local settings, if existing +try: + from uncloud.local_settings import * +except (ModuleNotFoundError, ImportError): + pass diff --git a/uncloud_django_based/uncloud/uncloud_net/models.py b/uncloud_django_based/uncloud/uncloud_net/models.py index 8dfff05..e56b79c 100644 --- a/uncloud_django_based/uncloud/uncloud_net/models.py +++ b/uncloud_django_based/uncloud/uncloud_net/models.py @@ -173,9 +173,6 @@ class VPNNetwork(Product): wireguard_public_key = models.CharField(max_length=48) - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - def delete(self, *args, **kwargs): self.network.status = 'free' self.network.save() diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index bcce598..55cf1ea 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model from django.core.validators import MinValueValidator from django.utils.translation import gettext_lazy as _ from django.utils import timezone -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, ValidationError import uuid import logging @@ -811,7 +811,7 @@ class Order(models.Model): # TODO: enforce ending_date - starting_date to be larger than recurring_period. creation_date = models.DateTimeField(auto_now_add=True) - starting_date = models.DateTimeField() + starting_date = models.DateTimeField(default=timezone.now) ending_date = models.DateTimeField(blank=True, null=True) @@ -918,6 +918,17 @@ class Product(UncloudModel): # _state.adding is switched to false after super(...) call. being_created = self._state.adding + # First time saving - create an order + if not self.order: + billing_address = BillingAddress.get_preferred_address_for(self.owner) + + if not billing_address: + raise ValidationError("Cannot create order without a billing address") + + self.order = Order(owner=self.owner, + billing_address=billing_address) + + super(Product, self).save(*args, **kwargs) # Make sure we only create records on creation. diff --git a/uncloud_django_based/uncloud/uncloud_pay/stripe.py b/uncloud_django_based/uncloud/uncloud_pay/stripe.py index f23002b..2ed4ef2 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/stripe.py +++ b/uncloud_django_based/uncloud/uncloud_pay/stripe.py @@ -3,9 +3,9 @@ import stripe.error import logging from django.core.exceptions import ObjectDoesNotExist -import uncloud_pay.models +from django.conf import settings -import uncloud.secrets +import uncloud_pay.models # Static stripe configuration used below. CURRENCY = 'chf' @@ -14,7 +14,7 @@ CURRENCY = 'chf' # https://stripe.com/docs/payments/save-and-reuse # For internal use only. -stripe.api_key = uncloud.secrets.STRIPE_KEY +stripe.api_key = settings.STRIPE_KEY # Helper (decorator) used to catch errors raised by stripe logic. # Catch errors that should not be displayed to the end user, raise again. @@ -64,7 +64,7 @@ def handle_stripe_error(f): # Actual Stripe logic. def public_api_key(): - return uncloud.secrets.STRIPE_PUBLIC_KEY + return settings.STRIPE_PUBLIC_KEY def get_customer_id_for(user): try: From eea654a9f8b71d8dabc112b0c31476878edc6167 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 2 May 2020 19:15:48 +0200 Subject: [PATCH 007/194] Phase in new beta/vm view for creating vms + orders + bills --- .../uncloud/uncloud/settings.py | 5 +-- uncloud_django_based/uncloud/uncloud/urls.py | 4 ++ .../uncloud/uncloud_vm/models.py | 1 + .../uncloud/uncloud_vm/serializers.py | 19 +++++++++ .../uncloud/uncloud_vm/views.py | 40 +++++++++++++++++++ 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud/settings.py b/uncloud_django_based/uncloud/uncloud/settings.py index 527749d..884c370 100644 --- a/uncloud_django_based/uncloud/uncloud/settings.py +++ b/uncloud_django_based/uncloud/uncloud/settings.py @@ -172,10 +172,6 @@ OPENNEBULA_URL = 'https://opennebula.example.com:2634/RPC2' # user:pass for accessing opennebula OPENNEBULA_USER_PASS = 'user:password' -# See https://django-auth-ldap.readthedocs.io/en/latest/authentication.html -LDAP_ADMIN_DN="" -LDAP_ADMIN_PASSWORD="" -LDAP_SERVER_URI = "" # Stripe (Credit Card payments) STRIPE_KEY="" @@ -184,6 +180,7 @@ STRIPE_PUBLIC_KEY="" # The django secret key SECRET_KEY=get_random_secret_key() +ALLOWED_HOSTS = [] # Overwrite settings with local settings, if existing try: diff --git a/uncloud_django_based/uncloud/uncloud/urls.py b/uncloud_django_based/uncloud/uncloud/urls.py index 4d0ada1..5539846 100644 --- a/uncloud_django_based/uncloud/uncloud/urls.py +++ b/uncloud_django_based/uncloud/uncloud/urls.py @@ -31,12 +31,16 @@ from uncloud_service import views as serviceviews router = routers.DefaultRouter() +# Beta endpoints +router.register(r'beta/vm', vmviews.NicoVMProductViewSet, basename='nicovmproduct') + # VM router.register(r'vm/snapshot', vmviews.VMSnapshotProductViewSet, basename='vmsnapshotproduct') router.register(r'vm/diskimage', vmviews.VMDiskImageProductViewSet, basename='vmdiskimageproduct') router.register(r'vm/disk', vmviews.VMDiskProductViewSet, basename='vmdiskproduct') router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vmproduct') + # creates VM from os image #router.register(r'vm/ipv6onlyvm', vmviews.VMProductViewSet, basename='vmproduct') # ... AND adds IPv4 mapping diff --git a/uncloud_django_based/uncloud/uncloud_vm/models.py b/uncloud_django_based/uncloud/uncloud_vm/models.py index 5dacdbe..39a5f40 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/models.py +++ b/uncloud_django_based/uncloud/uncloud_vm/models.py @@ -69,6 +69,7 @@ class VMProduct(Product): cores = models.IntegerField() ram_in_gb = models.FloatField() + # Optional disk primary_disk = models.ForeignKey('VMDiskProduct', on_delete=models.CASCADE, null=True) # Default recurring price is PER_MONTH, see uncloud_pay.models.Product. diff --git a/uncloud_django_based/uncloud/uncloud_vm/serializers.py b/uncloud_django_based/uncloud/uncloud_vm/serializers.py index 92c7fe8..2c7137e 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_vm/serializers.py @@ -120,6 +120,25 @@ class OrderVMProductSerializer(VMProductSerializer): # Nico's playground. +class NicoVMProductSerializer(serializers.ModelSerializer): + primary_disk = CreateVMDiskProductSerializer() + snapshots = VMSnapshotProductSerializer(many=True, read_only=True) + disks = VMDiskProductSerializer(many=True, read_only=True) + + class Meta: + model = VMProduct + read_only_fields = ['uuid', 'order', 'owner', 'status', + 'vmhost', 'vmcluster', + 'extra_data' ] + fields = read_only_fields + [ 'name', + 'cores', + 'ram_in_gb', + 'primary_disk', + 'snapshots', + 'disks' ] + + + class DCLVMProductSerializer(serializers.HyperlinkedModelSerializer): """ Create an interface similar to standard DCL diff --git a/uncloud_django_based/uncloud/uncloud_vm/views.py b/uncloud_django_based/uncloud/uncloud_vm/views.py index 1dead62..39b7668 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/views.py +++ b/uncloud_django_based/uncloud/uncloud_vm/views.py @@ -179,6 +179,46 @@ class VMProductViewSet(ProductViewSet): return Response(VMProductSerializer(vm, context={'request': request}).data) +class NicoVMProductViewSet(ProductViewSet): + permission_classes = [permissions.IsAuthenticated] + serializer_class = NicoVMProductSerializer + + def get_queryset(self): + obj = VMProduct.objects.filter(owner=self.request.user) + + return obj + + # Use a database transaction so that we do not get half-created structure + # if something goes wrong. + @transaction.atomic + def create(self, request): + # Extract serializer data. + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + order_recurring_period = serializer.validated_data.pop("recurring_period") + order_billing_address = serializer.validated_data.pop("billing_address") + + # Create base order. + order = Order( + recurring_period=order_recurring_period, + billing_address=order_billing_address, + owner=request.user, + starting_date=timezone.now() + ) + order.save() + + # Create disk image. + disk = VMDiskProduct(owner=request.user, order=order, + **serializer.validated_data.pop("primary_disk")) + + # Create VM. + vm = serializer.save(owner=request.user, order=order, primary_disk=disk) + disk.vm = vm + disk.save() + + return Response(VMProductSerializer(vm, context={'request': request}).data) + + ### # Admin stuff. From 927fb206712d92ec33d9969c159b951a9a7ec7ca Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 2 May 2020 20:31:36 +0200 Subject: [PATCH 008/194] Versionise API and cleanups --- uncloud_django_based/uncloud/uncloud/urls.py | 44 +++++++++---------- .../uncloud/uncloud_pay/models.py | 6 ++- .../uncloud/uncloud_vm/models.py | 4 +- .../uncloud/uncloud_vm/serializers.py | 11 ++--- .../uncloud/uncloud_vm/views.py | 1 + 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud/urls.py b/uncloud_django_based/uncloud/uncloud/urls.py index 5539846..05b1f0f 100644 --- a/uncloud_django_based/uncloud/uncloud/urls.py +++ b/uncloud_django_based/uncloud/uncloud/urls.py @@ -35,10 +35,10 @@ router = routers.DefaultRouter() router.register(r'beta/vm', vmviews.NicoVMProductViewSet, basename='nicovmproduct') # VM -router.register(r'vm/snapshot', vmviews.VMSnapshotProductViewSet, basename='vmsnapshotproduct') -router.register(r'vm/diskimage', vmviews.VMDiskImageProductViewSet, basename='vmdiskimageproduct') -router.register(r'vm/disk', vmviews.VMDiskProductViewSet, basename='vmdiskproduct') -router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vmproduct') +router.register(r'v1/vm/snapshot', vmviews.VMSnapshotProductViewSet, basename='vmsnapshotproduct') +router.register(r'v1/vm/diskimage', vmviews.VMDiskImageProductViewSet, basename='vmdiskimageproduct') +router.register(r'v1/vm/disk', vmviews.VMDiskProductViewSet, basename='vmdiskproduct') +router.register(r'v1/vm/vm', vmviews.VMProductViewSet, basename='vmproduct') # creates VM from os image @@ -47,35 +47,35 @@ router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vmproduct') #router.register(r'vm/dualstackvm', vmviews.VMProductViewSet, basename='vmproduct') # Services -router.register(r'service/matrix', serviceviews.MatrixServiceProductViewSet, basename='matrixserviceproduct') -router.register(r'service/generic', serviceviews.GenericServiceProductViewSet, basename='genericserviceproduct') +router.register(r'v1/service/matrix', serviceviews.MatrixServiceProductViewSet, basename='matrixserviceproduct') +router.register(r'v1/service/generic', serviceviews.GenericServiceProductViewSet, basename='genericserviceproduct') # Net -router.register(r'net/vpn', netviews.VPNNetworkViewSet, basename='vpnnet') -router.register(r'net/vpnreservation', netviews.VPNNetworkReservationViewSet, basename='vpnnetreservation') +router.register(r'v1/net/vpn', netviews.VPNNetworkViewSet, basename='vpnnet') +router.register(r'v1/admin/vpnreservation', netviews.VPNNetworkReservationViewSet, basename='vpnnetreservation') # Pay -router.register(r'address', payviews.BillingAddressViewSet, basename='address') -router.register(r'bill', payviews.BillViewSet, basename='bill') -router.register(r'order', payviews.OrderViewSet, basename='order') -router.register(r'payment', payviews.PaymentViewSet, basename='payment') -router.register(r'payment-method', payviews.PaymentMethodViewSet, basename='payment-method') +router.register(r'v1/my/address', payviews.BillingAddressViewSet, basename='address') +router.register(r'v1/my/bill', payviews.BillViewSet, basename='bill') +router.register(r'v1/my/order', payviews.OrderViewSet, basename='order') +router.register(r'v1/my/payment', payviews.PaymentViewSet, basename='payment') +router.register(r'v1/my/payment-method', payviews.PaymentMethodViewSet, basename='payment-method') # admin/staff urls -router.register(r'admin/bill', payviews.AdminBillViewSet, basename='admin/bill') -router.register(r'admin/payment', payviews.AdminPaymentViewSet, basename='admin/payment') -router.register(r'admin/order', payviews.AdminOrderViewSet, basename='admin/order') -router.register(r'admin/vmhost', vmviews.VMHostViewSet) -router.register(r'admin/vmcluster', vmviews.VMClusterViewSet) -router.register(r'admin/vpnpool', netviews.VPNPoolViewSet) -router.register(r'admin/opennebula', oneviews.VMViewSet, basename='opennebula') +router.register(r'v1/admin/bill', payviews.AdminBillViewSet, basename='admin/bill') +router.register(r'v1/admin/payment', payviews.AdminPaymentViewSet, basename='admin/payment') +router.register(r'v1/admin/order', payviews.AdminOrderViewSet, basename='admin/order') +router.register(r'v1/admin/vmhost', vmviews.VMHostViewSet) +router.register(r'v1/admin/vmcluster', vmviews.VMClusterViewSet) +router.register(r'v1/admin/vpnpool', netviews.VPNPoolViewSet) +router.register(r'v1/admin/opennebula', oneviews.VMViewSet, basename='opennebula') # User/Account -router.register(r'user', authviews.UserViewSet, basename='user') -router.register(r'admin/user', authviews.AdminUserViewSet, basename='useradmin') +router.register(r'v1/my/user', authviews.UserViewSet, basename='user') +router.register(r'v1/admin/user', authviews.AdminUserViewSet, basename='useradmin') urlpatterns = [ path('', include(router.urls)), diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index 55cf1ea..b06473e 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -825,7 +825,11 @@ class Order(models.Model): # Trigger initial bill generation at order creation. def save(self, *args, **kwargs): - super(Order, self).save(*args, **kwargs) + if self.ending_date and self.ending_date < self.starting_date: + raise ValidationError("End date cannot be before starting date") + + super().save(*args, **kwargs) + Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner) @property diff --git a/uncloud_django_based/uncloud/uncloud_vm/models.py b/uncloud_django_based/uncloud/uncloud_vm/models.py index 39a5f40..06b5386 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/models.py +++ b/uncloud_django_based/uncloud/uncloud_vm/models.py @@ -69,9 +69,6 @@ class VMProduct(Product): cores = models.IntegerField() ram_in_gb = models.FloatField() - # Optional disk - primary_disk = models.ForeignKey('VMDiskProduct', on_delete=models.CASCADE, null=True) - # Default recurring price is PER_MONTH, see uncloud_pay.models.Product. @property def recurring_price(self): @@ -175,6 +172,7 @@ class VMDiskProduct(Product): super().save(*args, **kwargs) + class VMNetworkCard(models.Model): vm = models.ForeignKey(VMProduct, on_delete=models.CASCADE) diff --git a/uncloud_django_based/uncloud/uncloud_vm/serializers.py b/uncloud_django_based/uncloud/uncloud_vm/serializers.py index 2c7137e..a04af8f 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_vm/serializers.py @@ -121,22 +121,17 @@ class OrderVMProductSerializer(VMProductSerializer): # Nico's playground. class NicoVMProductSerializer(serializers.ModelSerializer): - primary_disk = CreateVMDiskProductSerializer() snapshots = VMSnapshotProductSerializer(many=True, read_only=True) - disks = VMDiskProductSerializer(many=True, read_only=True) class Meta: model = VMProduct read_only_fields = ['uuid', 'order', 'owner', 'status', - 'vmhost', 'vmcluster', + 'vmhost', 'vmcluster', 'snapshots', 'extra_data' ] fields = read_only_fields + [ 'name', 'cores', - 'ram_in_gb', - 'primary_disk', - 'snapshots', - 'disks' ] - + 'ram_in_gb' + ] class DCLVMProductSerializer(serializers.HyperlinkedModelSerializer): diff --git a/uncloud_django_based/uncloud/uncloud_vm/views.py b/uncloud_django_based/uncloud/uncloud_vm/views.py index 39b7668..2d0a693 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/views.py +++ b/uncloud_django_based/uncloud/uncloud_vm/views.py @@ -195,6 +195,7 @@ class NicoVMProductViewSet(ProductViewSet): # Extract serializer data. serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + order_recurring_period = serializer.validated_data.pop("recurring_period") order_billing_address = serializer.validated_data.pop("billing_address") From 7d708cfbb685c655fdaaf0672e9df6cebb5fbde9 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 2 May 2020 20:42:09 +0200 Subject: [PATCH 009/194] Fix empty vat number bug --- uncloud_django_based/uncloud/uncloud_pay/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/views.py b/uncloud_django_based/uncloud/uncloud_pay/views.py index 82b5787..59d310e 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/views.py +++ b/uncloud_django_based/uncloud/uncloud_pay/views.py @@ -215,10 +215,11 @@ class BillingAddressViewSet(mixins.CreateModelMixin, # Validate VAT numbers. country = serializer.validated_data["country"] - vat_number = serializer.validated_data["vat_number"] # We ignore empty VAT numbers. - if vat_number != "": + if 'vat_number' in serializer.validated_data and serializer.validated_data["vat_number"] != "": + vat_number = serializer.validated_data["vat_number"] + if not validate_vat(country, vat_number): return Response( {'error': 'Malformed VAT number.'}, From 736fe274935a33006d3eb84f21025872cf536155 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 2 May 2020 20:45:19 +0200 Subject: [PATCH 010/194] Add issues.org as a shortcut for registering issues --- issues.org | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 issues.org diff --git a/issues.org b/issues.org new file mode 100644 index 0000000..f79c1dc --- /dev/null +++ b/issues.org @@ -0,0 +1,5 @@ +* Intro + This file lists issues that should be handled, are small and likely + not yet high prio. +* Issues +** TODO Register prefered address in User model From 4097c2ce13bfe862a5312ff9952ae625a33c05b2 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 2 May 2020 21:20:14 +0200 Subject: [PATCH 011/194] BillingAddress: make mget_preferred_address a classmethod --- uncloud_django_based/uncloud/uncloud_pay/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index b06473e..3b4535c 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -455,9 +455,9 @@ class BillingAddress(models.Model): def get_addresses_for(user): return BillingAddress.objects.filter(owner=user) - @staticmethod - def get_preferred_address_for(user): - addresses = get_addresses_for(user) + @classmethod + def get_preferred_address_for(cls, user): + addresses = cls.get_addresses_for(user) if len(addresses) == 0: return None else: @@ -927,7 +927,7 @@ class Product(UncloudModel): billing_address = BillingAddress.get_preferred_address_for(self.owner) if not billing_address: - raise ValidationError("Cannot create order without a billing address") + raise ValidationError("Cannot order without a billing address") self.order = Order(owner=self.owner, billing_address=billing_address) From 9ef5309b91f2f60ae5dead5d02b9a95f83cda4d9 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 2 May 2020 21:21:29 +0200 Subject: [PATCH 012/194] +db migrations for pay/vm Signed-off-by: Nico Schottelius --- .../migrations/0008_auto_20200502_1921.py | 19 +++++++++++++++++++ .../0013_remove_vmproduct_primary_disk.py | 17 +++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 uncloud_django_based/uncloud/uncloud_pay/migrations/0008_auto_20200502_1921.py create mode 100644 uncloud_django_based/uncloud/uncloud_vm/migrations/0013_remove_vmproduct_primary_disk.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0008_auto_20200502_1921.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0008_auto_20200502_1921.py new file mode 100644 index 0000000..c244357 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0008_auto_20200502_1921.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.5 on 2020-05-02 19:21 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0007_auto_20200418_0737'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='starting_date', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0013_remove_vmproduct_primary_disk.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0013_remove_vmproduct_primary_disk.py new file mode 100644 index 0000000..849012d --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0013_remove_vmproduct_primary_disk.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.5 on 2020-05-02 19:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0012_auto_20200418_0641'), + ] + + operations = [ + migrations.RemoveField( + model_name='vmproduct', + name='primary_disk', + ), + ] From 028f1ebe6e3e80121e58fe1bf50ffb8535ba0e91 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 2 May 2020 22:03:34 +0200 Subject: [PATCH 013/194] Entry point /beta/vm/ works for creating VM + order --- issues.org | 1 + .../uncloud/uncloud_pay/models.py | 26 +++++++++------ .../uncloud/uncloud_vm/models.py | 6 ++++ .../uncloud/uncloud_vm/serializers.py | 1 + .../uncloud/uncloud_vm/views.py | 32 ++----------------- 5 files changed, 28 insertions(+), 38 deletions(-) diff --git a/issues.org b/issues.org index f79c1dc..840ec3c 100644 --- a/issues.org +++ b/issues.org @@ -3,3 +3,4 @@ not yet high prio. * Issues ** TODO Register prefered address in User model +** TODO Allow to specify different recurring periods diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index 3b4535c..a326810 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -855,6 +855,11 @@ class Order(models.Model): recurring_price=recurring_price, description=description) + def __str__(self): + return "Order {} created at {}, {}->{}, recurring period {}".format( + self.uuid, self.creation_date, + self.starting_date, self.ending_date, + self.recurring_period) class OrderRecord(models.Model): @@ -925,23 +930,26 @@ class Product(UncloudModel): # First time saving - create an order if not self.order: billing_address = BillingAddress.get_preferred_address_for(self.owner) + print(billing_address) if not billing_address: raise ValidationError("Cannot order without a billing address") - self.order = Order(owner=self.owner, + self.order = Order.objects.create(owner=self.owner, billing_address=billing_address) - super(Product, self).save(*args, **kwargs) + print("in save op: {}".format(self)) + print(self.order) + super().save(*args, **kwargs) - # Make sure we only create records on creation. - if being_created: - record = OrderRecord( - one_time_price=self.one_time_price, - recurring_price=self.recurring_price, - description=self.description) - self.order.orderrecord_set.add(record, bulk=False) + # # Make sure we only create records on creation. + # if being_created: + # record = OrderRecord( + # one_time_price=self.one_time_price, + # recurring_price=self.recurring_price, + # description=self.description) + # self.order.orderrecord_set.add(record, bulk=False) @property def recurring_price(self): diff --git a/uncloud_django_based/uncloud/uncloud_vm/models.py b/uncloud_django_based/uncloud/uncloud_vm/models.py index 06b5386..91c9291 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/models.py +++ b/uncloud_django_based/uncloud/uncloud_vm/models.py @@ -91,6 +91,12 @@ class VMProduct(Product): RecurringPeriod.PER_MONTH, RecurringPeriod.PER_HOUR], RecurringPeriod.choices)) + def __str__(self): + return "VM {} ({} Cores/{} GB RAM) running on {} in cluster {}".format( + self.uuid, self.cores, self.ram_in_gb, + self.vmhost, self.vmcluster) + + class VMWithOSProduct(VMProduct): pass diff --git a/uncloud_django_based/uncloud/uncloud_vm/serializers.py b/uncloud_django_based/uncloud/uncloud_vm/serializers.py index a04af8f..701b187 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_vm/serializers.py @@ -122,6 +122,7 @@ class OrderVMProductSerializer(VMProductSerializer): class NicoVMProductSerializer(serializers.ModelSerializer): snapshots = VMSnapshotProductSerializer(many=True, read_only=True) + order = serializers.StringRelatedField() class Meta: model = VMProduct diff --git a/uncloud_django_based/uncloud/uncloud_vm/views.py b/uncloud_django_based/uncloud/uncloud_vm/views.py index 2d0a693..cf90891 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/views.py +++ b/uncloud_django_based/uncloud/uncloud_vm/views.py @@ -185,40 +185,14 @@ class NicoVMProductViewSet(ProductViewSet): def get_queryset(self): obj = VMProduct.objects.filter(owner=self.request.user) - return obj - # Use a database transaction so that we do not get half-created structure - # if something goes wrong. - @transaction.atomic def create(self, request): - # Extract serializer data. - serializer = self.get_serializer(data=request.data) + serializer = self.serializer_class(data=request.data, context={'request': request}) serializer.is_valid(raise_exception=True) + vm = serializer.save(owner=request.user) - order_recurring_period = serializer.validated_data.pop("recurring_period") - order_billing_address = serializer.validated_data.pop("billing_address") - - # Create base order. - order = Order( - recurring_period=order_recurring_period, - billing_address=order_billing_address, - owner=request.user, - starting_date=timezone.now() - ) - order.save() - - # Create disk image. - disk = VMDiskProduct(owner=request.user, order=order, - **serializer.validated_data.pop("primary_disk")) - - # Create VM. - vm = serializer.save(owner=request.user, order=order, primary_disk=disk) - disk.vm = vm - disk.save() - - return Response(VMProductSerializer(vm, context={'request': request}).data) - + return Response(serializer.data) ### # Admin stuff. From c835c874d5e17da2a49d6ed1360556081244d945 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 2 May 2020 22:48:05 +0200 Subject: [PATCH 014/194] [BREAKING] make Order a stand-alone version I think that while the idea of an Orderrecord is good, we might get away / have a simpler implementation if we only use orders and reference them where needed. I saved the previous Order model for easy rollback, if my assumption is wrong. --- .../migrations/0009_auto_20200502_2047.py | 47 ++++++++++++++ .../uncloud/uncloud_pay/models.py | 65 ++++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 uncloud_django_based/uncloud/uncloud_pay/migrations/0009_auto_20200502_2047.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0009_auto_20200502_2047.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0009_auto_20200502_2047.py new file mode 100644 index 0000000..cb9cd78 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0009_auto_20200502_2047.py @@ -0,0 +1,47 @@ +# Generated by Django 3.0.5 on 2020-05-02 20:47 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0008_auto_20200502_1921'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='one_time_price', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + migrations.AddField( + model_name='order', + name='recurring_price', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + migrations.AddField( + model_name='order', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='uncloud_pay.Order'), + ), + migrations.CreateModel( + name='OrderTimothee', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('creation_date', models.DateTimeField(auto_now_add=True)), + ('starting_date', models.DateTimeField(default=django.utils.timezone.now)), + ('ending_date', models.DateTimeField(blank=True, null=True)), + ('recurring_period', models.CharField(choices=[('ONCE', 'Onetime'), ('YEAR', 'Per Year'), ('MONTH', 'Per Month'), ('WEEK', 'Per Week'), ('DAY', 'Per Day'), ('HOUR', 'Per Hour'), ('MINUTE', 'Per Minute'), ('SECOND', 'Per Second')], default='MONTH', max_length=32)), + ('bill', models.ManyToManyField(blank=True, editable=False, to='uncloud_pay.Bill')), + ('billing_address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.BillingAddress')), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index a326810..9a8a49a 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -809,6 +809,64 @@ class Order(models.Model): editable=False) billing_address = models.ForeignKey(BillingAddress, on_delete=models.CASCADE) + # TODO: enforce ending_date - starting_date to be larger than recurring_period. + creation_date = models.DateTimeField(auto_now_add=True) + starting_date = models.DateTimeField(default=timezone.now) + ending_date = models.DateTimeField(blank=True, + null=True) + + bill = models.ManyToManyField(Bill, + editable=False, + blank=True) + + recurring_period = models.CharField(max_length=32, + choices = RecurringPeriod.choices, + default = RecurringPeriod.PER_MONTH) + + one_time_price = models.DecimalField(default=0.0, + max_digits=AMOUNT_MAX_DIGITS, + decimal_places=AMOUNT_DECIMALS, + validators=[MinValueValidator(0)]) + + recurring_price = models.DecimalField(default=0.0, + max_digits=AMOUNT_MAX_DIGITS, + decimal_places=AMOUNT_DECIMALS, + validators=[MinValueValidator(0)]) + + replaced_by = models.ForeignKey('self', + on_delete=models.PROTECT, + blank=True, + null=True) + + # Trigger initial bill generation at order creation. + def save(self, *args, **kwargs): + if self.ending_date and self.ending_date < self.starting_date: + raise ValidationError("End date cannot be before starting date") + + super().save(*args, **kwargs) + + Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner) + + # Used by uncloud_pay tests. + @property + def bills(self): + return Bill.objects.filter(order=self) + + def __str__(self): + return "Order {} created at {}, {}->{}, recurring period {}. Price one time {}, recurring {}".format( + self.uuid, self.creation_date, + self.starting_date, self.ending_date, + self.recurring_period, + self.one_time_price, + self.recurring_price) + +class OrderTimothee(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + owner = models.ForeignKey(get_user_model(), + on_delete=models.CASCADE, + editable=False) + billing_address = models.ForeignKey(BillingAddress, on_delete=models.CASCADE) + # TODO: enforce ending_date - starting_date to be larger than recurring_period. creation_date = models.DateTimeField(auto_now_add=True) starting_date = models.DateTimeField(default=timezone.now) @@ -856,10 +914,13 @@ class Order(models.Model): description=description) def __str__(self): - return "Order {} created at {}, {}->{}, recurring period {}".format( + return "Order {} created at {}, {}->{}, recurring period {}. Price one time {}, recurring {}".format( self.uuid, self.creation_date, self.starting_date, self.ending_date, - self.recurring_period) + self.recurring_period, + self.one_time_price, + self.recurring_price) + class OrderRecord(models.Model): From 99a18232aaacc0872f294952291bc3dbeb5b5c80 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 2 May 2020 23:44:20 +0200 Subject: [PATCH 015/194] VMs now properly set their pricing --- .../uncloud/uncloud_pay/models.py | 19 +++++++++---------- .../uncloud/uncloud_vm/models.py | 3 ++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index 9a8a49a..aca226e 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -845,15 +845,15 @@ class Order(models.Model): super().save(*args, **kwargs) - Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner) +# Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner) - # Used by uncloud_pay tests. - @property - def bills(self): - return Bill.objects.filter(order=self) + # # Used by uncloud_pay tests. + # @property + # def bills(self): + # return Bill.objects.filter(order=self) def __str__(self): - return "Order {} created at {}, {}->{}, recurring period {}. Price one time {}, recurring {}".format( + return "Order {} created at {}, {}->{}, recurring period {}. One time price {}, recurring price {}".format( self.uuid, self.creation_date, self.starting_date, self.ending_date, self.recurring_period, @@ -997,11 +997,10 @@ class Product(UncloudModel): raise ValidationError("Cannot order without a billing address") self.order = Order.objects.create(owner=self.owner, - billing_address=billing_address) + billing_address=billing_address, + one_time_price=self.one_time_price, + recurring_price=self.recurring_price) - - print("in save op: {}".format(self)) - print(self.order) super().save(*args, **kwargs) # # Make sure we only create records on creation. diff --git a/uncloud_django_based/uncloud/uncloud_vm/models.py b/uncloud_django_based/uncloud/uncloud_vm/models.py index 91c9291..a542503 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/models.py +++ b/uncloud_django_based/uncloud/uncloud_vm/models.py @@ -69,7 +69,7 @@ class VMProduct(Product): cores = models.IntegerField() ram_in_gb = models.FloatField() - # Default recurring price is PER_MONTH, see uncloud_pay.models.Product. + @property def recurring_price(self): return self.cores * 3 + self.ram_in_gb * 4 @@ -79,6 +79,7 @@ class VMProduct(Product): self.name, self.cores, self.ram_in_gb) + @property def description(self): return "Virtual machine '{}': {} core(s), {}GB memory".format( From e3b28354fef7cff2afdc878030817b97153d4986 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Tue, 5 May 2020 15:19:04 +0200 Subject: [PATCH 016/194] ++notes --- .../doc/README-how-to-configure-remote-uncloud-clients.org | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/uncloud_django_based/uncloud/doc/README-how-to-configure-remote-uncloud-clients.org b/uncloud_django_based/uncloud/doc/README-how-to-configure-remote-uncloud-clients.org index 7217e1f..b48886b 100644 --- a/uncloud_django_based/uncloud/doc/README-how-to-configure-remote-uncloud-clients.org +++ b/uncloud_django_based/uncloud/doc/README-how-to-configure-remote-uncloud-clients.org @@ -10,6 +10,7 @@ | SSH -L tunnel | All nodes can use [::1]:5432 | SSH setup can be fragile | | ssh djangohost manage.py | All DB ops locally | Code is only executed on django host | | https + token | Rest alike / consistent access | Code is only executed on django host | +| from_django | Everything is on the django host | main host can become bottleneck | ** remote vs. local Django code execution - If manage.py is executed locally (= on the client), it can check/modify local configs @@ -19,3 +20,9 @@ - Remote execution (= on the primary django host) can acess the db via unix socket - However remote execution cannot check local state +** from_django + - might reuse existing methods like celery + - reduces the amount of things to be installed on the client to + almost zero + - follows the opennebula model + - has a single point of failurebin From 594f1a9b69016ad3d02bcf3fe1cc65761c513a8e Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Tue, 5 May 2020 15:19:50 +0200 Subject: [PATCH 017/194] +hacks --- uncloud_django_based/{ => hacks}/abk-hacks.py | 0 .../{ => hacks}/abkhack/opennebula_hacks.py | 0 uncloud_django_based/hacks/command-wrapper.sh | 18 ++++++++++++++++++ 3 files changed, 18 insertions(+) rename uncloud_django_based/{ => hacks}/abk-hacks.py (100%) rename uncloud_django_based/{ => hacks}/abkhack/opennebula_hacks.py (100%) create mode 100644 uncloud_django_based/hacks/command-wrapper.sh diff --git a/uncloud_django_based/abk-hacks.py b/uncloud_django_based/hacks/abk-hacks.py similarity index 100% rename from uncloud_django_based/abk-hacks.py rename to uncloud_django_based/hacks/abk-hacks.py diff --git a/uncloud_django_based/abkhack/opennebula_hacks.py b/uncloud_django_based/hacks/abkhack/opennebula_hacks.py similarity index 100% rename from uncloud_django_based/abkhack/opennebula_hacks.py rename to uncloud_django_based/hacks/abkhack/opennebula_hacks.py diff --git a/uncloud_django_based/hacks/command-wrapper.sh b/uncloud_django_based/hacks/command-wrapper.sh new file mode 100644 index 0000000..d6ddd13 --- /dev/null +++ b/uncloud_django_based/hacks/command-wrapper.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +dbhost=$1; shift + +ssh -L5432:localhost:5432 "$dbhost" & + +python manage.py "$@" + + + +# command only needs to be active while manage command is running + +# -T no pseudo terminal + + +# alternatively: commands output shell code + +# ssh uncloud@dbhost "python manage.py --hostname xxx ..." From aa8ade473033e81664a9eacf17c89af386c3644e Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Tue, 5 May 2020 16:01:47 +0200 Subject: [PATCH 018/194] Add readme about identifiers --- .../uncloud/doc/README-identifiers.org | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 uncloud_django_based/uncloud/doc/README-identifiers.org diff --git a/uncloud_django_based/uncloud/doc/README-identifiers.org b/uncloud_django_based/uncloud/doc/README-identifiers.org new file mode 100644 index 0000000..3dbb4b5 --- /dev/null +++ b/uncloud_django_based/uncloud/doc/README-identifiers.org @@ -0,0 +1,29 @@ +* Identifiers +** Problem description + Identifiers can be integers, strings or other objects. They should + be unique. +** Approach 1: integers + Integers are somewhat easy to remember, but also include + predictable growth, which might allow access to guessed hacking + (obivously proper permissions should prevent this). +** Approach 2: random uuids + UUIDs are 128 bit integers. Python supports uuid.uuid4() for random + uuids. +** Approach 3: IPv6 addresses + uncloud heavily depends on IPv6 in the first place. uncloud could + use a /48 to identify all objects. Objects that have IPv6 addresses + on their own, don't need to draw from the system /48. +*** Possible Subnetworks + Assuming uncloud uses a /48 to represent all resources. + + | Network | Name | Description | + |-----------------+-----------------+----------------------------------------------| + | 2001:db8::/48 | uncloud network | All identifiers drawn from here | + | 2001:db8:1::/64 | VM network | Every VM has an IPv6 address in this network | + | 2001:db8:2::/64 | Bill network | Every bill has an IPv6 address | + | 2001:db8:3::/64 | Order network | Every order has an IPv6 address | + | 2001:db8:5::/64 | Product network | Every product (?) has an IPv6 address | + | 2001:db8:4::/64 | Disk network | Every disk is identified | + +*** Tests + [15:47:37] black3.place6:~# rbd create -s 10G ssd/2a0a:e5c0:1::8 From 892b2b6f137c97bd863b9d0ef35fcf27be952389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 12:03:28 +0200 Subject: [PATCH 019/194] Revert "Disable vat validator to get project back running" This reverts commit 1cf20a2cb6c84b2db79d2e45e49d5fbb81b392e7. --- uncloud_django_based/uncloud/uncloud_pay/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/views.py b/uncloud_django_based/uncloud/uncloud_pay/views.py index 59d310e..0c68ac6 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/views.py +++ b/uncloud_django_based/uncloud/uncloud_pay/views.py @@ -7,8 +7,8 @@ from rest_framework.response import Response from rest_framework.decorators import action from rest_framework.reverse import reverse from rest_framework.decorators import renderer_classes -#from vat_validator import validate_vat, vies -#from vat_validator.countries import EU_COUNTRY_CODES +from vat_validator import validate_vat, vies +from vat_validator.countries import EU_COUNTRY_CODES import json import logging @@ -16,7 +16,7 @@ import logging from .models import * from .serializers import * from datetime import datetime -#from vat_validator import sanitize_vat +from vat_validator import sanitize_vat import uncloud_pay.stripe as uncloud_stripe logger = logging.getLogger(__name__) From db3c29d17ed9ee42b56f7d6d086524a4a40d7581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 12:05:26 +0200 Subject: [PATCH 020/194] Fix admin order creation --- .../uncloud/uncloud_pay/serializers.py | 32 +++++++++++++++++-- .../uncloud/uncloud_pay/views.py | 4 ++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/serializers.py b/uncloud_django_based/uncloud/uncloud_pay/serializers.py index 1b5db24..70f5c5e 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_pay/serializers.py @@ -1,5 +1,6 @@ from django.contrib.auth import get_user_model from rest_framework import serializers +from uncloud_auth.serializers import UserSerializer from .models import * ### @@ -41,11 +42,36 @@ class OrderRecordSerializer(serializers.ModelSerializer): class OrderSerializer(serializers.ModelSerializer): - records = OrderRecordSerializer(many=True, read_only=True) + owner = serializers.PrimaryKeyRelatedField(queryset=get_user_model().objects.all()) + + def __init__(self, *args, **kwargs): + # Don't pass the 'fields' arg up to the superclass + admin = kwargs.pop('admin', None) + + # Instantiate the superclass normally + super(OrderSerializer, self).__init__(*args, **kwargs) + + # Only allows owner in admin mode. + if not admin: + self.fields.pop('owner') + + def create(self, validated_data): + billing_address = BillingAddress.get_preferred_address_for(validated_data["owner"]) + instance = Order(billing_address=billing_address, **validated_data) + instance.save() + + return instance + + def validate_owner(self, value): + if BillingAddress.get_preferred_address_for(value) == None: + raise serializers.ValidationError("Owner does not have a valid billing address.") + + return value + class Meta: model = Order - fields = ['uuid', 'creation_date', 'starting_date', 'ending_date', - 'bill', 'recurring_period', 'records', 'recurring_price', 'one_time_price'] + fields = ['uuid', 'owner', 'creation_date', 'starting_date', 'ending_date', + 'bill', 'recurring_period', 'recurring_price', 'one_time_price'] ### diff --git a/uncloud_django_based/uncloud/uncloud_pay/views.py b/uncloud_django_based/uncloud/uncloud_pay/views.py index 0c68ac6..54ff2f0 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/views.py +++ b/uncloud_django_based/uncloud/uncloud_pay/views.py @@ -279,9 +279,11 @@ class AdminBillViewSet(viewsets.ModelViewSet): return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) class AdminOrderViewSet(viewsets.ModelViewSet): - serializer_class = OrderSerializer permission_classes = [permissions.IsAuthenticated] + def get_serializer(self, *args, **kwargs): + return OrderSerializer(*args, **kwargs, admin=True) + def get_queryset(self): return Order.objects.all() From 056006332639a94b2583877e6fc047349d76cb99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 12:08:18 +0200 Subject: [PATCH 021/194] Add description field to Orders --- .../migrations/0010_order_description.py | 19 +++++++++++++++++++ .../uncloud/uncloud_pay/models.py | 1 + .../uncloud/uncloud_pay/serializers.py | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 uncloud_django_based/uncloud/uncloud_pay/migrations/0010_order_description.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0010_order_description.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0010_order_description.py new file mode 100644 index 0000000..2613bff --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0010_order_description.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.6 on 2020-05-07 10:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0009_auto_20200502_2047'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='description', + field=models.TextField(default=''), + preserve_default=False, + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index aca226e..1294a54 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -808,6 +808,7 @@ class Order(models.Model): on_delete=models.CASCADE, editable=False) billing_address = models.ForeignKey(BillingAddress, on_delete=models.CASCADE) + description = models.TextField() # TODO: enforce ending_date - starting_date to be larger than recurring_period. creation_date = models.DateTimeField(auto_now_add=True) diff --git a/uncloud_django_based/uncloud/uncloud_pay/serializers.py b/uncloud_django_based/uncloud/uncloud_pay/serializers.py index 70f5c5e..ad50c68 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_pay/serializers.py @@ -70,7 +70,7 @@ class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order - fields = ['uuid', 'owner', 'creation_date', 'starting_date', 'ending_date', + fields = ['uuid', 'owner', 'description', 'creation_date', 'starting_date', 'ending_date', 'bill', 'recurring_period', 'recurring_price', 'one_time_price'] From 95d43f002f742090e644d76c9ba8c09a77397db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 12:12:35 +0200 Subject: [PATCH 022/194] Move django-based uncloud to top-level --- issues.org => archive/issues.org | 0 .../uncloud_django_based}/hacks/abk-hacks.py | 0 .../hacks/abkhack/opennebula_hacks.py | 0 .../uncloud_django_based}/hacks/command-wrapper.sh | 0 .../uncloud_django_based}/meow-payv1/README.md | 0 .../uncloud_django_based}/meow-payv1/config.py | 0 .../uncloud_django_based}/meow-payv1/hack-a-vpn.py | 0 .../uncloud_django_based}/meow-payv1/helper.py | 0 .../meow-payv1/products/ipv6-only-django.json | 0 .../meow-payv1/products/ipv6-only-vm.json | 0 .../meow-payv1/products/ipv6-only-vpn.json | 0 .../meow-payv1/products/ipv6box.json | 0 .../meow-payv1/products/membership.json | 0 .../meow-payv1/requirements.txt | 0 .../meow-payv1/sample-pay.conf | 0 .../uncloud_django_based}/meow-payv1/schemas.py | 0 .../uncloud_django_based}/meow-payv1/stripe_hack.py | 0 .../meow-payv1/stripe_utils.py | 0 .../uncloud_django_based}/meow-payv1/ucloud_pay.py | 0 .../uncloud_django_based}/notes-abk.md | 0 .../uncloud_django_based}/notes-nico.org | 0 .../uncloud_django_based}/plan.org | 0 .../uncloud_django_based}/uncloud/.gitignore | 0 .../uncloud_django_based}/vat_rates.csv | 0 .../uncloud_etcd_based}/bin/gen-version | 0 .../uncloud_etcd_based}/bin/uncloud | 0 .../uncloud_etcd_based}/bin/uncloud-run-reinstall | 0 .../uncloud_etcd_based}/conf/uncloud.conf | 0 .../uncloud_etcd_based}/docs/Makefile | 0 .../uncloud_etcd_based}/docs/README.md | 0 .../uncloud_etcd_based/docs}/__init__.py | 0 .../uncloud_etcd_based/docs/source}/__init__.py | 0 .../uncloud_etcd_based}/docs/source/admin-guide.rst | 0 .../uncloud_etcd_based}/docs/source/conf.py | 0 .../docs/source/diagram-code/ucloud | 0 .../uncloud_etcd_based}/docs/source/hacking.rst | 0 .../docs/source/images/ucloud.svg | 0 .../uncloud_etcd_based}/docs/source/index.rst | 0 .../docs/source/introduction.rst | 0 .../uncloud_etcd_based}/docs/source/misc/todo.rst | 0 .../docs/source/setup-install.rst | 0 .../docs/source/theory/summary.rst | 0 .../docs/source/troubleshooting.rst | 0 .../uncloud_etcd_based}/docs/source/user-guide.rst | 0 .../how-to-create-an-os-image-for-ucloud.rst | 0 .../uncloud_etcd_based}/docs/source/vm-images.rst | 0 .../uncloud_etcd_based}/scripts/uncloud | 0 .../uncloud_etcd_based}/setup.py | 0 .../uncloud_etcd_based/test}/__init__.py | 0 .../uncloud_etcd_based}/test/test_mac_local.py | 0 .../uncloud_etcd_based}/uncloud/__init__.py | 0 .../uncloud_etcd_based}/uncloud/api/README.md | 0 .../uncloud_etcd_based}/uncloud/api/__init__.py | 0 .../uncloud/api/common_fields.py | 0 .../uncloud/api/create_image_store.py | 0 .../uncloud_etcd_based}/uncloud/api/helper.py | 0 .../uncloud_etcd_based}/uncloud/api/main.py | 0 .../uncloud_etcd_based}/uncloud/api/schemas.py | 0 .../uncloud_etcd_based/uncloud/cli}/__init__.py | 0 .../uncloud_etcd_based}/uncloud/cli/helper.py | 0 .../uncloud_etcd_based}/uncloud/cli/host.py | 0 .../uncloud_etcd_based}/uncloud/cli/image.py | 0 .../uncloud_etcd_based}/uncloud/cli/main.py | 0 .../uncloud_etcd_based}/uncloud/cli/network.py | 0 .../uncloud_etcd_based}/uncloud/cli/user.py | 0 .../uncloud_etcd_based}/uncloud/cli/vm.py | 0 .../uncloud_etcd_based/uncloud/client}/__init__.py | 0 .../uncloud_etcd_based}/uncloud/client/main.py | 0 .../uncloud_etcd_based}/uncloud/common/__init__.py | 0 .../uncloud_etcd_based}/uncloud/common/classes.py | 0 .../uncloud_etcd_based}/uncloud/common/cli.py | 0 .../uncloud_etcd_based}/uncloud/common/counters.py | 0 .../uncloud/common/etcd_wrapper.py | 0 .../uncloud_etcd_based}/uncloud/common/host.py | 0 .../uncloud_etcd_based}/uncloud/common/network.py | 0 .../uncloud_etcd_based}/uncloud/common/parser.py | 0 .../uncloud_etcd_based}/uncloud/common/request.py | 0 .../uncloud_etcd_based}/uncloud/common/schemas.py | 0 .../uncloud_etcd_based}/uncloud/common/settings.py | 0 .../uncloud_etcd_based}/uncloud/common/shared.py | 0 .../uncloud/common/storage_handlers.py | 0 .../uncloud_etcd_based}/uncloud/common/vm.py | 0 .../uncloud/configure}/__init__.py | 0 .../uncloud_etcd_based}/uncloud/configure/main.py | 0 .../uncloud/filescanner/__init__.py | 0 .../uncloud_etcd_based}/uncloud/filescanner/main.py | 0 .../uncloud_etcd_based}/uncloud/hack/README.org | 0 .../uncloud_etcd_based}/uncloud/hack/__init__.py | 0 .../uncloud/hack/conf.d/ucloud-host | 0 .../uncloud_etcd_based}/uncloud/hack/config.py | 0 .../uncloud_etcd_based}/uncloud/hack/db.py | 0 .../uncloud/hack/hackcloud/.gitignore | 0 .../uncloud/hack/hackcloud/__init__.py | 0 .../uncloud/hack/hackcloud/etcd-client.sh | 0 .../uncloud/hack/hackcloud/ifdown.sh | 0 .../uncloud/hack/hackcloud/ifup.sh | 0 .../uncloud/hack/hackcloud/mac-last | 0 .../uncloud/hack/hackcloud/mac-prefix | 0 .../uncloud/hack/hackcloud/net.sh | 0 .../uncloud/hack/hackcloud/nftrules | 0 .../uncloud/hack/hackcloud/radvd.conf | 0 .../uncloud/hack/hackcloud/radvd.sh | 0 .../uncloud/hack/hackcloud/vm.sh | 0 .../uncloud_etcd_based}/uncloud/hack/host.py | 0 .../uncloud_etcd_based}/uncloud/hack/mac.py | 0 .../uncloud_etcd_based}/uncloud/hack/main.py | 0 .../uncloud_etcd_based}/uncloud/hack/net.py | 0 .../uncloud_etcd_based}/uncloud/hack/nftables.conf | 0 .../uncloud_etcd_based}/uncloud/hack/product.py | 0 .../uncloud/hack/rc-scripts/ucloud-api | 0 .../uncloud/hack/rc-scripts/ucloud-host | 0 .../uncloud/hack/rc-scripts/ucloud-metadata | 0 .../uncloud/hack/rc-scripts/ucloud-scheduler | 0 .../uncloud/hack/uncloud-hack-init-host | 0 .../uncloud_etcd_based}/uncloud/hack/uncloud-run-vm | 0 .../uncloud_etcd_based}/uncloud/hack/vm.py | 0 .../uncloud_etcd_based}/uncloud/host/__init__.py | 0 .../uncloud_etcd_based}/uncloud/host/main.py | 0 .../uncloud/host/virtualmachine.py | 0 .../uncloud/imagescanner/__init__.py | 0 .../uncloud/imagescanner/main.py | 0 .../uncloud/metadata/__init__.py | 0 .../uncloud_etcd_based}/uncloud/metadata/main.py | 0 .../uncloud_etcd_based}/uncloud/network/README | 0 .../uncloud_etcd_based/uncloud/network}/__init__.py | 0 .../uncloud/network/create-bridge.sh | 0 .../uncloud/network/create-tap.sh | 0 .../uncloud/network/create-vxlan.sh | 0 .../uncloud/network/radvd-template.conf | 0 .../uncloud_etcd_based}/uncloud/oneshot/__init__.py | 0 .../uncloud_etcd_based}/uncloud/oneshot/main.py | 0 .../uncloud/oneshot/virtualmachine.py | 0 .../uncloud/scheduler/__init__.py | 0 .../uncloud_etcd_based}/uncloud/scheduler/helper.py | 0 .../uncloud_etcd_based}/uncloud/scheduler/main.py | 0 .../uncloud/scheduler/tests}/__init__.py | 0 .../uncloud/scheduler/tests/test_basics.py | 0 .../scheduler/tests/test_dead_host_mechanism.py | 0 .../uncloud_etcd_based}/uncloud/version.py | 0 .../uncloud_etcd_based}/uncloud/vmm/__init__.py | 0 ...ADME-how-to-configure-remote-uncloud-clients.org | 0 .../uncloud/doc => doc}/README-identifiers.org | 0 .../uncloud/doc => doc}/README-object-relations.md | 0 .../uncloud/doc => doc}/README-postgresql.org | 0 .../uncloud/doc => doc}/README-products.md | 0 .../uncloud/doc => doc}/README-vpn.org | 0 {uncloud_django_based/uncloud/doc => doc}/README.md | 0 uncloud_django_based/uncloud/manage.py => manage.py | 0 .../uncloud/models.dot => models.dot | 0 .../uncloud/models.png => models.png | Bin .../uncloud_service => opennebula}/__init__.py | 0 .../uncloud/opennebula => opennebula}/admin.py | 0 .../uncloud/opennebula => opennebula}/apps.py | 0 .../management/commands/opennebula-synchosts.py | 0 .../management/commands/opennebula-syncvms.py | 0 .../management/commands/opennebula-to-uncloud.py | 0 .../migrations/0001_initial.py | 0 .../migrations/0002_auto_20200225_1335.py | 0 .../migrations/0003_auto_20200225_1428.py | 0 .../migrations/0004_auto_20200225_1816.py | 0 .../migrations/__init__.py | 0 .../uncloud/opennebula => opennebula}/models.py | 0 .../opennebula => opennebula}/serializers.py | 0 .../uncloud/opennebula => opennebula}/tests.py | 0 .../uncloud/opennebula => opennebula}/views.py | 0 .../uncloud/requirements.txt => requirements.txt | 0 .../uncloud/uncloud => uncloud}/.gitignore | 0 .../uncloud/uncloud => uncloud}/__init__.py | 0 .../uncloud/uncloud => uncloud}/asgi.py | 0 .../management/commands/uncloud.py | 0 .../uncloud/uncloud => uncloud}/models.py | 0 .../uncloud/uncloud => uncloud}/settings.py | 0 .../uncloud/uncloud => uncloud}/urls.py | 0 .../uncloud/uncloud => uncloud}/wsgi.py | 0 .../uncloud_storage => uncloud_auth}/__init__.py | 0 .../uncloud/uncloud_auth => uncloud_auth}/admin.py | 0 .../uncloud/uncloud_auth => uncloud_auth}/apps.py | 0 .../migrations/0001_initial.py | 0 .../migrations/0002_auto_20200318_1343.py | 0 .../migrations/0003_auto_20200318_1345.py | 0 .../migrations}/__init__.py | 0 .../uncloud/uncloud_auth => uncloud_auth}/models.py | 0 .../uncloud_auth => uncloud_auth}/serializers.py | 0 .../uncloud/uncloud_auth => uncloud_auth}/views.py | 0 .../migrations => uncloud_net}/__init__.py | 0 .../uncloud/uncloud_net => uncloud_net}/admin.py | 0 .../uncloud/uncloud_net => uncloud_net}/apps.py | 0 .../management/commands/vpn.py | 0 .../migrations/0001_initial.py | 0 .../migrations/0002_auto_20200409_1225.py | 0 .../migrations/0003_auto_20200417_0551.py | 0 .../docs => uncloud_net/migrations}/__init__.py | 0 .../uncloud/uncloud_net => uncloud_net}/models.py | 0 .../uncloud_net => uncloud_net}/serializers.py | 0 .../uncloud/uncloud_net => uncloud_net}/tests.py | 0 .../uncloud/uncloud_net => uncloud_net}/views.py | 0 .../docs/source => uncloud_pay}/__init__.py | 0 .../uncloud/uncloud_pay => uncloud_pay}/admin.py | 0 .../uncloud/uncloud_pay => uncloud_pay}/apps.py | 0 .../uncloud/uncloud_pay => uncloud_pay}/helpers.py | 0 .../management/commands/charge-negative-balance.py | 0 .../management/commands/generate-bills.py | 0 .../management/commands/handle-overdue-bills.py | 0 .../management/commands/import-vat-rates.py | 0 .../migrations/0001_initial.py | 0 .../migrations/0002_auto_20200305_1524.py | 0 .../migrations/0003_auto_20200305_1354.py | 0 .../migrations/0004_auto_20200409_1225.py | 0 .../migrations/0005_auto_20200413_0924.py | 0 .../migrations/0006_auto_20200415_1003.py | 0 .../migrations/0006_billingaddress.py | 0 .../migrations/0007_auto_20200418_0737.py | 0 .../migrations/0008_auto_20200502_1921.py | 0 .../migrations/0009_auto_20200502_2047.py | 0 .../migrations/0010_order_description.py | 0 .../test => uncloud_pay/migrations}/__init__.py | 0 .../uncloud/uncloud_pay => uncloud_pay}/models.py | 0 .../uncloud_pay => uncloud_pay}/serializers.py | 0 .../uncloud/uncloud_pay => uncloud_pay}/stripe.py | 0 .../uncloud_pay => uncloud_pay}/templates/bill.html | 0 .../templates/error.html.j2 | 0 .../templates/stripe-payment.html.j2 | 0 .../uncloud/uncloud_pay => uncloud_pay}/tests.py | 0 .../uncloud/uncloud_pay => uncloud_pay}/views.py | 0 .../uncloud/cli => uncloud_service}/__init__.py | 0 .../uncloud_service => uncloud_service}/admin.py | 0 .../uncloud_service => uncloud_service}/apps.py | 0 .../migrations/0001_initial.py | 0 .../migrations/0002_auto_20200418_0641.py | 0 .../migrations}/__init__.py | 0 .../uncloud_service => uncloud_service}/models.py | 0 .../serializers.py | 0 .../uncloud_service => uncloud_service}/tests.py | 0 .../uncloud_service => uncloud_service}/views.py | 0 .../configure => uncloud_storage}/__init__.py | 0 .../uncloud_storage => uncloud_storage}/admin.py | 0 .../uncloud_storage => uncloud_storage}/apps.py | 0 .../uncloud_storage => uncloud_storage}/models.py | 0 .../uncloud_storage => uncloud_storage}/tests.py | 0 .../uncloud_storage => uncloud_storage}/views.py | 0 .../uncloud/network => uncloud_vm}/__init__.py | 0 .../uncloud/uncloud_vm => uncloud_vm}/admin.py | 0 .../uncloud/uncloud_vm => uncloud_vm}/apps.py | 0 .../management/commands/vm.py | 0 .../migrations/0001_initial.py | 0 .../migrations/0002_auto_20200305_1321.py | 0 .../migrations/0003_remove_vmhost_vms.py | 0 .../migrations/0004_remove_vmproduct_vmid.py | 0 .../migrations/0004_vmproduct_primary_disk.py | 0 .../migrations/0005_auto_20200309_1258.py | 0 .../migrations/0005_auto_20200321_1058.py | 0 .../migrations/0006_auto_20200322_1758.py | 0 .../migrations/0007_vmhost_vmcluster.py | 0 .../migrations/0008_auto_20200403_1727.py | 0 .../migrations/0009_auto_20200417_0551.py | 0 .../migrations/0009_merge_20200413_0857.py | 0 .../migrations/0010_auto_20200413_0924.py | 0 .../migrations/0011_merge_20200418_0641.py | 0 .../migrations/0012_auto_20200418_0641.py | 0 .../0013_remove_vmproduct_primary_disk.py | 0 .../tests => uncloud_vm/migrations}/__init__.py | 0 .../uncloud/uncloud_vm => uncloud_vm}/models.py | 0 .../uncloud_vm => uncloud_vm}/serializers.py | 0 .../uncloud/uncloud_vm => uncloud_vm}/tests.py | 0 .../uncloud/uncloud_vm => uncloud_vm}/views.py | 0 265 files changed, 0 insertions(+), 0 deletions(-) rename issues.org => archive/issues.org (100%) rename {uncloud_django_based => archive/uncloud_django_based}/hacks/abk-hacks.py (100%) rename {uncloud_django_based => archive/uncloud_django_based}/hacks/abkhack/opennebula_hacks.py (100%) rename {uncloud_django_based => archive/uncloud_django_based}/hacks/command-wrapper.sh (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/README.md (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/config.py (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/hack-a-vpn.py (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/helper.py (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/products/ipv6-only-django.json (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/products/ipv6-only-vm.json (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/products/ipv6-only-vpn.json (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/products/ipv6box.json (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/products/membership.json (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/requirements.txt (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/sample-pay.conf (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/schemas.py (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/stripe_hack.py (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/stripe_utils.py (100%) rename {uncloud_django_based => archive/uncloud_django_based}/meow-payv1/ucloud_pay.py (100%) rename {uncloud_django_based => archive/uncloud_django_based}/notes-abk.md (100%) rename {uncloud_django_based => archive/uncloud_django_based}/notes-nico.org (100%) rename {uncloud_django_based => archive/uncloud_django_based}/plan.org (100%) rename {uncloud_django_based => archive/uncloud_django_based}/uncloud/.gitignore (100%) rename {uncloud_django_based => archive/uncloud_django_based}/vat_rates.csv (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/bin/gen-version (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/bin/uncloud (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/bin/uncloud-run-reinstall (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/conf/uncloud.conf (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/Makefile (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/README.md (100%) rename {uncloud_django_based/uncloud/opennebula => archive/uncloud_etcd_based/docs}/__init__.py (100%) rename {uncloud_django_based/uncloud/opennebula/migrations => archive/uncloud_etcd_based/docs/source}/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/admin-guide.rst (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/conf.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/diagram-code/ucloud (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/hacking.rst (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/images/ucloud.svg (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/index.rst (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/introduction.rst (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/misc/todo.rst (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/setup-install.rst (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/theory/summary.rst (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/troubleshooting.rst (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/user-guide.rst (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/user-guide/how-to-create-an-os-image-for-ucloud.rst (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/docs/source/vm-images.rst (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/scripts/uncloud (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/setup.py (100%) rename {uncloud_django_based/uncloud/uncloud_auth => archive/uncloud_etcd_based/test}/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/test/test_mac_local.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/api/README.md (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/api/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/api/common_fields.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/api/create_image_store.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/api/helper.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/api/main.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/api/schemas.py (100%) rename {uncloud_django_based/uncloud/uncloud_auth/migrations => archive/uncloud_etcd_based/uncloud/cli}/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/cli/helper.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/cli/host.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/cli/image.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/cli/main.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/cli/network.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/cli/user.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/cli/vm.py (100%) rename {uncloud_django_based/uncloud/uncloud_net => archive/uncloud_etcd_based/uncloud/client}/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/client/main.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/classes.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/cli.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/counters.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/etcd_wrapper.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/host.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/network.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/parser.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/request.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/schemas.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/settings.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/shared.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/storage_handlers.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/common/vm.py (100%) rename {uncloud_django_based/uncloud/uncloud_net/migrations => archive/uncloud_etcd_based/uncloud/configure}/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/configure/main.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/filescanner/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/filescanner/main.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/README.org (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/conf.d/ucloud-host (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/config.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/db.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/.gitignore (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/etcd-client.sh (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/ifdown.sh (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/ifup.sh (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/mac-last (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/mac-prefix (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/net.sh (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/nftrules (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/radvd.conf (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/radvd.sh (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/hackcloud/vm.sh (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/host.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/mac.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/main.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/net.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/nftables.conf (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/product.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/rc-scripts/ucloud-api (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/rc-scripts/ucloud-host (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/rc-scripts/ucloud-metadata (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/rc-scripts/ucloud-scheduler (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/uncloud-hack-init-host (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/uncloud-run-vm (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/hack/vm.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/host/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/host/main.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/host/virtualmachine.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/imagescanner/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/imagescanner/main.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/metadata/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/metadata/main.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/network/README (100%) rename {uncloud_django_based/uncloud/uncloud_pay => archive/uncloud_etcd_based/uncloud/network}/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/network/create-bridge.sh (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/network/create-tap.sh (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/network/create-vxlan.sh (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/network/radvd-template.conf (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/oneshot/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/oneshot/main.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/oneshot/virtualmachine.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/scheduler/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/scheduler/helper.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/scheduler/main.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay/migrations => archive/uncloud_etcd_based/uncloud/scheduler/tests}/__init__.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/scheduler/tests/test_basics.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/scheduler/tests/test_dead_host_mechanism.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/version.py (100%) rename {uncloud_etcd_based => archive/uncloud_etcd_based}/uncloud/vmm/__init__.py (100%) rename {uncloud_django_based/uncloud/doc => doc}/README-how-to-configure-remote-uncloud-clients.org (100%) rename {uncloud_django_based/uncloud/doc => doc}/README-identifiers.org (100%) rename {uncloud_django_based/uncloud/doc => doc}/README-object-relations.md (100%) rename {uncloud_django_based/uncloud/doc => doc}/README-postgresql.org (100%) rename {uncloud_django_based/uncloud/doc => doc}/README-products.md (100%) rename {uncloud_django_based/uncloud/doc => doc}/README-vpn.org (100%) rename {uncloud_django_based/uncloud/doc => doc}/README.md (100%) rename uncloud_django_based/uncloud/manage.py => manage.py (100%) rename uncloud_django_based/uncloud/models.dot => models.dot (100%) rename uncloud_django_based/uncloud/models.png => models.png (100%) rename {uncloud_django_based/uncloud/uncloud_service => opennebula}/__init__.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/admin.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/apps.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/management/commands/opennebula-synchosts.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/management/commands/opennebula-syncvms.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/management/commands/opennebula-to-uncloud.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/migrations/0001_initial.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/migrations/0002_auto_20200225_1335.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/migrations/0003_auto_20200225_1428.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/migrations/0004_auto_20200225_1816.py (100%) rename {uncloud_django_based/uncloud/uncloud_service => opennebula}/migrations/__init__.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/models.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/serializers.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/tests.py (100%) rename {uncloud_django_based/uncloud/opennebula => opennebula}/views.py (100%) rename uncloud_django_based/uncloud/requirements.txt => requirements.txt (100%) rename {uncloud_django_based/uncloud/uncloud => uncloud}/.gitignore (100%) rename {uncloud_django_based/uncloud/uncloud => uncloud}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud => uncloud}/asgi.py (100%) rename {uncloud_django_based/uncloud/uncloud => uncloud}/management/commands/uncloud.py (100%) rename {uncloud_django_based/uncloud/uncloud => uncloud}/models.py (100%) rename {uncloud_django_based/uncloud/uncloud => uncloud}/settings.py (100%) rename {uncloud_django_based/uncloud/uncloud => uncloud}/urls.py (100%) rename {uncloud_django_based/uncloud/uncloud => uncloud}/wsgi.py (100%) rename {uncloud_django_based/uncloud/uncloud_storage => uncloud_auth}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud_auth => uncloud_auth}/admin.py (100%) rename {uncloud_django_based/uncloud/uncloud_auth => uncloud_auth}/apps.py (100%) rename {uncloud_django_based/uncloud/uncloud_auth => uncloud_auth}/migrations/0001_initial.py (100%) rename {uncloud_django_based/uncloud/uncloud_auth => uncloud_auth}/migrations/0002_auto_20200318_1343.py (100%) rename {uncloud_django_based/uncloud/uncloud_auth => uncloud_auth}/migrations/0003_auto_20200318_1345.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_auth/migrations}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud_auth => uncloud_auth}/models.py (100%) rename {uncloud_django_based/uncloud/uncloud_auth => uncloud_auth}/serializers.py (100%) rename {uncloud_django_based/uncloud/uncloud_auth => uncloud_auth}/views.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm/migrations => uncloud_net}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud_net => uncloud_net}/admin.py (100%) rename {uncloud_django_based/uncloud/uncloud_net => uncloud_net}/apps.py (100%) rename {uncloud_django_based/uncloud/uncloud_net => uncloud_net}/management/commands/vpn.py (100%) rename {uncloud_django_based/uncloud/uncloud_net => uncloud_net}/migrations/0001_initial.py (100%) rename {uncloud_django_based/uncloud/uncloud_net => uncloud_net}/migrations/0002_auto_20200409_1225.py (100%) rename {uncloud_django_based/uncloud/uncloud_net => uncloud_net}/migrations/0003_auto_20200417_0551.py (100%) rename {uncloud_etcd_based/docs => uncloud_net/migrations}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud_net => uncloud_net}/models.py (100%) rename {uncloud_django_based/uncloud/uncloud_net => uncloud_net}/serializers.py (100%) rename {uncloud_django_based/uncloud/uncloud_net => uncloud_net}/tests.py (100%) rename {uncloud_django_based/uncloud/uncloud_net => uncloud_net}/views.py (100%) rename {uncloud_etcd_based/docs/source => uncloud_pay}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/admin.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/apps.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/helpers.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/management/commands/charge-negative-balance.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/management/commands/generate-bills.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/management/commands/handle-overdue-bills.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/management/commands/import-vat-rates.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/migrations/0001_initial.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/migrations/0002_auto_20200305_1524.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/migrations/0003_auto_20200305_1354.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/migrations/0004_auto_20200409_1225.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/migrations/0005_auto_20200413_0924.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/migrations/0006_auto_20200415_1003.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/migrations/0006_billingaddress.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/migrations/0007_auto_20200418_0737.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/migrations/0008_auto_20200502_1921.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/migrations/0009_auto_20200502_2047.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/migrations/0010_order_description.py (100%) rename {uncloud_etcd_based/test => uncloud_pay/migrations}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/models.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/serializers.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/stripe.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/templates/bill.html (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/templates/error.html.j2 (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/templates/stripe-payment.html.j2 (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/tests.py (100%) rename {uncloud_django_based/uncloud/uncloud_pay => uncloud_pay}/views.py (100%) rename {uncloud_etcd_based/uncloud/cli => uncloud_service}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud_service => uncloud_service}/admin.py (100%) rename {uncloud_django_based/uncloud/uncloud_service => uncloud_service}/apps.py (100%) rename {uncloud_django_based/uncloud/uncloud_service => uncloud_service}/migrations/0001_initial.py (100%) rename {uncloud_django_based/uncloud/uncloud_service => uncloud_service}/migrations/0002_auto_20200418_0641.py (100%) rename {uncloud_etcd_based/uncloud/client => uncloud_service/migrations}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud_service => uncloud_service}/models.py (100%) rename {uncloud_django_based/uncloud/uncloud_service => uncloud_service}/serializers.py (100%) rename {uncloud_django_based/uncloud/uncloud_service => uncloud_service}/tests.py (100%) rename {uncloud_django_based/uncloud/uncloud_service => uncloud_service}/views.py (100%) rename {uncloud_etcd_based/uncloud/configure => uncloud_storage}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud_storage => uncloud_storage}/admin.py (100%) rename {uncloud_django_based/uncloud/uncloud_storage => uncloud_storage}/apps.py (100%) rename {uncloud_django_based/uncloud/uncloud_storage => uncloud_storage}/models.py (100%) rename {uncloud_django_based/uncloud/uncloud_storage => uncloud_storage}/tests.py (100%) rename {uncloud_django_based/uncloud/uncloud_storage => uncloud_storage}/views.py (100%) rename {uncloud_etcd_based/uncloud/network => uncloud_vm}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/admin.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/apps.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/management/commands/vm.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0001_initial.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0002_auto_20200305_1321.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0003_remove_vmhost_vms.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0004_remove_vmproduct_vmid.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0004_vmproduct_primary_disk.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0005_auto_20200309_1258.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0005_auto_20200321_1058.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0006_auto_20200322_1758.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0007_vmhost_vmcluster.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0008_auto_20200403_1727.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0009_auto_20200417_0551.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0009_merge_20200413_0857.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0010_auto_20200413_0924.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0011_merge_20200418_0641.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0012_auto_20200418_0641.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/migrations/0013_remove_vmproduct_primary_disk.py (100%) rename {uncloud_etcd_based/uncloud/scheduler/tests => uncloud_vm/migrations}/__init__.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/models.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/serializers.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/tests.py (100%) rename {uncloud_django_based/uncloud/uncloud_vm => uncloud_vm}/views.py (100%) diff --git a/issues.org b/archive/issues.org similarity index 100% rename from issues.org rename to archive/issues.org diff --git a/uncloud_django_based/hacks/abk-hacks.py b/archive/uncloud_django_based/hacks/abk-hacks.py similarity index 100% rename from uncloud_django_based/hacks/abk-hacks.py rename to archive/uncloud_django_based/hacks/abk-hacks.py diff --git a/uncloud_django_based/hacks/abkhack/opennebula_hacks.py b/archive/uncloud_django_based/hacks/abkhack/opennebula_hacks.py similarity index 100% rename from uncloud_django_based/hacks/abkhack/opennebula_hacks.py rename to archive/uncloud_django_based/hacks/abkhack/opennebula_hacks.py diff --git a/uncloud_django_based/hacks/command-wrapper.sh b/archive/uncloud_django_based/hacks/command-wrapper.sh similarity index 100% rename from uncloud_django_based/hacks/command-wrapper.sh rename to archive/uncloud_django_based/hacks/command-wrapper.sh diff --git a/uncloud_django_based/meow-payv1/README.md b/archive/uncloud_django_based/meow-payv1/README.md similarity index 100% rename from uncloud_django_based/meow-payv1/README.md rename to archive/uncloud_django_based/meow-payv1/README.md diff --git a/uncloud_django_based/meow-payv1/config.py b/archive/uncloud_django_based/meow-payv1/config.py similarity index 100% rename from uncloud_django_based/meow-payv1/config.py rename to archive/uncloud_django_based/meow-payv1/config.py diff --git a/uncloud_django_based/meow-payv1/hack-a-vpn.py b/archive/uncloud_django_based/meow-payv1/hack-a-vpn.py similarity index 100% rename from uncloud_django_based/meow-payv1/hack-a-vpn.py rename to archive/uncloud_django_based/meow-payv1/hack-a-vpn.py diff --git a/uncloud_django_based/meow-payv1/helper.py b/archive/uncloud_django_based/meow-payv1/helper.py similarity index 100% rename from uncloud_django_based/meow-payv1/helper.py rename to archive/uncloud_django_based/meow-payv1/helper.py diff --git a/uncloud_django_based/meow-payv1/products/ipv6-only-django.json b/archive/uncloud_django_based/meow-payv1/products/ipv6-only-django.json similarity index 100% rename from uncloud_django_based/meow-payv1/products/ipv6-only-django.json rename to archive/uncloud_django_based/meow-payv1/products/ipv6-only-django.json diff --git a/uncloud_django_based/meow-payv1/products/ipv6-only-vm.json b/archive/uncloud_django_based/meow-payv1/products/ipv6-only-vm.json similarity index 100% rename from uncloud_django_based/meow-payv1/products/ipv6-only-vm.json rename to archive/uncloud_django_based/meow-payv1/products/ipv6-only-vm.json diff --git a/uncloud_django_based/meow-payv1/products/ipv6-only-vpn.json b/archive/uncloud_django_based/meow-payv1/products/ipv6-only-vpn.json similarity index 100% rename from uncloud_django_based/meow-payv1/products/ipv6-only-vpn.json rename to archive/uncloud_django_based/meow-payv1/products/ipv6-only-vpn.json diff --git a/uncloud_django_based/meow-payv1/products/ipv6box.json b/archive/uncloud_django_based/meow-payv1/products/ipv6box.json similarity index 100% rename from uncloud_django_based/meow-payv1/products/ipv6box.json rename to archive/uncloud_django_based/meow-payv1/products/ipv6box.json diff --git a/uncloud_django_based/meow-payv1/products/membership.json b/archive/uncloud_django_based/meow-payv1/products/membership.json similarity index 100% rename from uncloud_django_based/meow-payv1/products/membership.json rename to archive/uncloud_django_based/meow-payv1/products/membership.json diff --git a/uncloud_django_based/meow-payv1/requirements.txt b/archive/uncloud_django_based/meow-payv1/requirements.txt similarity index 100% rename from uncloud_django_based/meow-payv1/requirements.txt rename to archive/uncloud_django_based/meow-payv1/requirements.txt diff --git a/uncloud_django_based/meow-payv1/sample-pay.conf b/archive/uncloud_django_based/meow-payv1/sample-pay.conf similarity index 100% rename from uncloud_django_based/meow-payv1/sample-pay.conf rename to archive/uncloud_django_based/meow-payv1/sample-pay.conf diff --git a/uncloud_django_based/meow-payv1/schemas.py b/archive/uncloud_django_based/meow-payv1/schemas.py similarity index 100% rename from uncloud_django_based/meow-payv1/schemas.py rename to archive/uncloud_django_based/meow-payv1/schemas.py diff --git a/uncloud_django_based/meow-payv1/stripe_hack.py b/archive/uncloud_django_based/meow-payv1/stripe_hack.py similarity index 100% rename from uncloud_django_based/meow-payv1/stripe_hack.py rename to archive/uncloud_django_based/meow-payv1/stripe_hack.py diff --git a/uncloud_django_based/meow-payv1/stripe_utils.py b/archive/uncloud_django_based/meow-payv1/stripe_utils.py similarity index 100% rename from uncloud_django_based/meow-payv1/stripe_utils.py rename to archive/uncloud_django_based/meow-payv1/stripe_utils.py diff --git a/uncloud_django_based/meow-payv1/ucloud_pay.py b/archive/uncloud_django_based/meow-payv1/ucloud_pay.py similarity index 100% rename from uncloud_django_based/meow-payv1/ucloud_pay.py rename to archive/uncloud_django_based/meow-payv1/ucloud_pay.py diff --git a/uncloud_django_based/notes-abk.md b/archive/uncloud_django_based/notes-abk.md similarity index 100% rename from uncloud_django_based/notes-abk.md rename to archive/uncloud_django_based/notes-abk.md diff --git a/uncloud_django_based/notes-nico.org b/archive/uncloud_django_based/notes-nico.org similarity index 100% rename from uncloud_django_based/notes-nico.org rename to archive/uncloud_django_based/notes-nico.org diff --git a/uncloud_django_based/plan.org b/archive/uncloud_django_based/plan.org similarity index 100% rename from uncloud_django_based/plan.org rename to archive/uncloud_django_based/plan.org diff --git a/uncloud_django_based/uncloud/.gitignore b/archive/uncloud_django_based/uncloud/.gitignore similarity index 100% rename from uncloud_django_based/uncloud/.gitignore rename to archive/uncloud_django_based/uncloud/.gitignore diff --git a/uncloud_django_based/vat_rates.csv b/archive/uncloud_django_based/vat_rates.csv similarity index 100% rename from uncloud_django_based/vat_rates.csv rename to archive/uncloud_django_based/vat_rates.csv diff --git a/uncloud_etcd_based/bin/gen-version b/archive/uncloud_etcd_based/bin/gen-version similarity index 100% rename from uncloud_etcd_based/bin/gen-version rename to archive/uncloud_etcd_based/bin/gen-version diff --git a/uncloud_etcd_based/bin/uncloud b/archive/uncloud_etcd_based/bin/uncloud similarity index 100% rename from uncloud_etcd_based/bin/uncloud rename to archive/uncloud_etcd_based/bin/uncloud diff --git a/uncloud_etcd_based/bin/uncloud-run-reinstall b/archive/uncloud_etcd_based/bin/uncloud-run-reinstall similarity index 100% rename from uncloud_etcd_based/bin/uncloud-run-reinstall rename to archive/uncloud_etcd_based/bin/uncloud-run-reinstall diff --git a/uncloud_etcd_based/conf/uncloud.conf b/archive/uncloud_etcd_based/conf/uncloud.conf similarity index 100% rename from uncloud_etcd_based/conf/uncloud.conf rename to archive/uncloud_etcd_based/conf/uncloud.conf diff --git a/uncloud_etcd_based/docs/Makefile b/archive/uncloud_etcd_based/docs/Makefile similarity index 100% rename from uncloud_etcd_based/docs/Makefile rename to archive/uncloud_etcd_based/docs/Makefile diff --git a/uncloud_etcd_based/docs/README.md b/archive/uncloud_etcd_based/docs/README.md similarity index 100% rename from uncloud_etcd_based/docs/README.md rename to archive/uncloud_etcd_based/docs/README.md diff --git a/uncloud_django_based/uncloud/opennebula/__init__.py b/archive/uncloud_etcd_based/docs/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/__init__.py rename to archive/uncloud_etcd_based/docs/__init__.py diff --git a/uncloud_django_based/uncloud/opennebula/migrations/__init__.py b/archive/uncloud_etcd_based/docs/source/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/migrations/__init__.py rename to archive/uncloud_etcd_based/docs/source/__init__.py diff --git a/uncloud_etcd_based/docs/source/admin-guide.rst b/archive/uncloud_etcd_based/docs/source/admin-guide.rst similarity index 100% rename from uncloud_etcd_based/docs/source/admin-guide.rst rename to archive/uncloud_etcd_based/docs/source/admin-guide.rst diff --git a/uncloud_etcd_based/docs/source/conf.py b/archive/uncloud_etcd_based/docs/source/conf.py similarity index 100% rename from uncloud_etcd_based/docs/source/conf.py rename to archive/uncloud_etcd_based/docs/source/conf.py diff --git a/uncloud_etcd_based/docs/source/diagram-code/ucloud b/archive/uncloud_etcd_based/docs/source/diagram-code/ucloud similarity index 100% rename from uncloud_etcd_based/docs/source/diagram-code/ucloud rename to archive/uncloud_etcd_based/docs/source/diagram-code/ucloud diff --git a/uncloud_etcd_based/docs/source/hacking.rst b/archive/uncloud_etcd_based/docs/source/hacking.rst similarity index 100% rename from uncloud_etcd_based/docs/source/hacking.rst rename to archive/uncloud_etcd_based/docs/source/hacking.rst diff --git a/uncloud_etcd_based/docs/source/images/ucloud.svg b/archive/uncloud_etcd_based/docs/source/images/ucloud.svg similarity index 100% rename from uncloud_etcd_based/docs/source/images/ucloud.svg rename to archive/uncloud_etcd_based/docs/source/images/ucloud.svg diff --git a/uncloud_etcd_based/docs/source/index.rst b/archive/uncloud_etcd_based/docs/source/index.rst similarity index 100% rename from uncloud_etcd_based/docs/source/index.rst rename to archive/uncloud_etcd_based/docs/source/index.rst diff --git a/uncloud_etcd_based/docs/source/introduction.rst b/archive/uncloud_etcd_based/docs/source/introduction.rst similarity index 100% rename from uncloud_etcd_based/docs/source/introduction.rst rename to archive/uncloud_etcd_based/docs/source/introduction.rst diff --git a/uncloud_etcd_based/docs/source/misc/todo.rst b/archive/uncloud_etcd_based/docs/source/misc/todo.rst similarity index 100% rename from uncloud_etcd_based/docs/source/misc/todo.rst rename to archive/uncloud_etcd_based/docs/source/misc/todo.rst diff --git a/uncloud_etcd_based/docs/source/setup-install.rst b/archive/uncloud_etcd_based/docs/source/setup-install.rst similarity index 100% rename from uncloud_etcd_based/docs/source/setup-install.rst rename to archive/uncloud_etcd_based/docs/source/setup-install.rst diff --git a/uncloud_etcd_based/docs/source/theory/summary.rst b/archive/uncloud_etcd_based/docs/source/theory/summary.rst similarity index 100% rename from uncloud_etcd_based/docs/source/theory/summary.rst rename to archive/uncloud_etcd_based/docs/source/theory/summary.rst diff --git a/uncloud_etcd_based/docs/source/troubleshooting.rst b/archive/uncloud_etcd_based/docs/source/troubleshooting.rst similarity index 100% rename from uncloud_etcd_based/docs/source/troubleshooting.rst rename to archive/uncloud_etcd_based/docs/source/troubleshooting.rst diff --git a/uncloud_etcd_based/docs/source/user-guide.rst b/archive/uncloud_etcd_based/docs/source/user-guide.rst similarity index 100% rename from uncloud_etcd_based/docs/source/user-guide.rst rename to archive/uncloud_etcd_based/docs/source/user-guide.rst diff --git a/uncloud_etcd_based/docs/source/user-guide/how-to-create-an-os-image-for-ucloud.rst b/archive/uncloud_etcd_based/docs/source/user-guide/how-to-create-an-os-image-for-ucloud.rst similarity index 100% rename from uncloud_etcd_based/docs/source/user-guide/how-to-create-an-os-image-for-ucloud.rst rename to archive/uncloud_etcd_based/docs/source/user-guide/how-to-create-an-os-image-for-ucloud.rst diff --git a/uncloud_etcd_based/docs/source/vm-images.rst b/archive/uncloud_etcd_based/docs/source/vm-images.rst similarity index 100% rename from uncloud_etcd_based/docs/source/vm-images.rst rename to archive/uncloud_etcd_based/docs/source/vm-images.rst diff --git a/uncloud_etcd_based/scripts/uncloud b/archive/uncloud_etcd_based/scripts/uncloud similarity index 100% rename from uncloud_etcd_based/scripts/uncloud rename to archive/uncloud_etcd_based/scripts/uncloud diff --git a/uncloud_etcd_based/setup.py b/archive/uncloud_etcd_based/setup.py similarity index 100% rename from uncloud_etcd_based/setup.py rename to archive/uncloud_etcd_based/setup.py diff --git a/uncloud_django_based/uncloud/uncloud_auth/__init__.py b/archive/uncloud_etcd_based/test/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_auth/__init__.py rename to archive/uncloud_etcd_based/test/__init__.py diff --git a/uncloud_etcd_based/test/test_mac_local.py b/archive/uncloud_etcd_based/test/test_mac_local.py similarity index 100% rename from uncloud_etcd_based/test/test_mac_local.py rename to archive/uncloud_etcd_based/test/test_mac_local.py diff --git a/uncloud_etcd_based/uncloud/__init__.py b/archive/uncloud_etcd_based/uncloud/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/__init__.py rename to archive/uncloud_etcd_based/uncloud/__init__.py diff --git a/uncloud_etcd_based/uncloud/api/README.md b/archive/uncloud_etcd_based/uncloud/api/README.md similarity index 100% rename from uncloud_etcd_based/uncloud/api/README.md rename to archive/uncloud_etcd_based/uncloud/api/README.md diff --git a/uncloud_etcd_based/uncloud/api/__init__.py b/archive/uncloud_etcd_based/uncloud/api/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/api/__init__.py rename to archive/uncloud_etcd_based/uncloud/api/__init__.py diff --git a/uncloud_etcd_based/uncloud/api/common_fields.py b/archive/uncloud_etcd_based/uncloud/api/common_fields.py similarity index 100% rename from uncloud_etcd_based/uncloud/api/common_fields.py rename to archive/uncloud_etcd_based/uncloud/api/common_fields.py diff --git a/uncloud_etcd_based/uncloud/api/create_image_store.py b/archive/uncloud_etcd_based/uncloud/api/create_image_store.py similarity index 100% rename from uncloud_etcd_based/uncloud/api/create_image_store.py rename to archive/uncloud_etcd_based/uncloud/api/create_image_store.py diff --git a/uncloud_etcd_based/uncloud/api/helper.py b/archive/uncloud_etcd_based/uncloud/api/helper.py similarity index 100% rename from uncloud_etcd_based/uncloud/api/helper.py rename to archive/uncloud_etcd_based/uncloud/api/helper.py diff --git a/uncloud_etcd_based/uncloud/api/main.py b/archive/uncloud_etcd_based/uncloud/api/main.py similarity index 100% rename from uncloud_etcd_based/uncloud/api/main.py rename to archive/uncloud_etcd_based/uncloud/api/main.py diff --git a/uncloud_etcd_based/uncloud/api/schemas.py b/archive/uncloud_etcd_based/uncloud/api/schemas.py similarity index 100% rename from uncloud_etcd_based/uncloud/api/schemas.py rename to archive/uncloud_etcd_based/uncloud/api/schemas.py diff --git a/uncloud_django_based/uncloud/uncloud_auth/migrations/__init__.py b/archive/uncloud_etcd_based/uncloud/cli/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_auth/migrations/__init__.py rename to archive/uncloud_etcd_based/uncloud/cli/__init__.py diff --git a/uncloud_etcd_based/uncloud/cli/helper.py b/archive/uncloud_etcd_based/uncloud/cli/helper.py similarity index 100% rename from uncloud_etcd_based/uncloud/cli/helper.py rename to archive/uncloud_etcd_based/uncloud/cli/helper.py diff --git a/uncloud_etcd_based/uncloud/cli/host.py b/archive/uncloud_etcd_based/uncloud/cli/host.py similarity index 100% rename from uncloud_etcd_based/uncloud/cli/host.py rename to archive/uncloud_etcd_based/uncloud/cli/host.py diff --git a/uncloud_etcd_based/uncloud/cli/image.py b/archive/uncloud_etcd_based/uncloud/cli/image.py similarity index 100% rename from uncloud_etcd_based/uncloud/cli/image.py rename to archive/uncloud_etcd_based/uncloud/cli/image.py diff --git a/uncloud_etcd_based/uncloud/cli/main.py b/archive/uncloud_etcd_based/uncloud/cli/main.py similarity index 100% rename from uncloud_etcd_based/uncloud/cli/main.py rename to archive/uncloud_etcd_based/uncloud/cli/main.py diff --git a/uncloud_etcd_based/uncloud/cli/network.py b/archive/uncloud_etcd_based/uncloud/cli/network.py similarity index 100% rename from uncloud_etcd_based/uncloud/cli/network.py rename to archive/uncloud_etcd_based/uncloud/cli/network.py diff --git a/uncloud_etcd_based/uncloud/cli/user.py b/archive/uncloud_etcd_based/uncloud/cli/user.py similarity index 100% rename from uncloud_etcd_based/uncloud/cli/user.py rename to archive/uncloud_etcd_based/uncloud/cli/user.py diff --git a/uncloud_etcd_based/uncloud/cli/vm.py b/archive/uncloud_etcd_based/uncloud/cli/vm.py similarity index 100% rename from uncloud_etcd_based/uncloud/cli/vm.py rename to archive/uncloud_etcd_based/uncloud/cli/vm.py diff --git a/uncloud_django_based/uncloud/uncloud_net/__init__.py b/archive/uncloud_etcd_based/uncloud/client/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/__init__.py rename to archive/uncloud_etcd_based/uncloud/client/__init__.py diff --git a/uncloud_etcd_based/uncloud/client/main.py b/archive/uncloud_etcd_based/uncloud/client/main.py similarity index 100% rename from uncloud_etcd_based/uncloud/client/main.py rename to archive/uncloud_etcd_based/uncloud/client/main.py diff --git a/uncloud_etcd_based/uncloud/common/__init__.py b/archive/uncloud_etcd_based/uncloud/common/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/__init__.py rename to archive/uncloud_etcd_based/uncloud/common/__init__.py diff --git a/uncloud_etcd_based/uncloud/common/classes.py b/archive/uncloud_etcd_based/uncloud/common/classes.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/classes.py rename to archive/uncloud_etcd_based/uncloud/common/classes.py diff --git a/uncloud_etcd_based/uncloud/common/cli.py b/archive/uncloud_etcd_based/uncloud/common/cli.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/cli.py rename to archive/uncloud_etcd_based/uncloud/common/cli.py diff --git a/uncloud_etcd_based/uncloud/common/counters.py b/archive/uncloud_etcd_based/uncloud/common/counters.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/counters.py rename to archive/uncloud_etcd_based/uncloud/common/counters.py diff --git a/uncloud_etcd_based/uncloud/common/etcd_wrapper.py b/archive/uncloud_etcd_based/uncloud/common/etcd_wrapper.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/etcd_wrapper.py rename to archive/uncloud_etcd_based/uncloud/common/etcd_wrapper.py diff --git a/uncloud_etcd_based/uncloud/common/host.py b/archive/uncloud_etcd_based/uncloud/common/host.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/host.py rename to archive/uncloud_etcd_based/uncloud/common/host.py diff --git a/uncloud_etcd_based/uncloud/common/network.py b/archive/uncloud_etcd_based/uncloud/common/network.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/network.py rename to archive/uncloud_etcd_based/uncloud/common/network.py diff --git a/uncloud_etcd_based/uncloud/common/parser.py b/archive/uncloud_etcd_based/uncloud/common/parser.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/parser.py rename to archive/uncloud_etcd_based/uncloud/common/parser.py diff --git a/uncloud_etcd_based/uncloud/common/request.py b/archive/uncloud_etcd_based/uncloud/common/request.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/request.py rename to archive/uncloud_etcd_based/uncloud/common/request.py diff --git a/uncloud_etcd_based/uncloud/common/schemas.py b/archive/uncloud_etcd_based/uncloud/common/schemas.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/schemas.py rename to archive/uncloud_etcd_based/uncloud/common/schemas.py diff --git a/uncloud_etcd_based/uncloud/common/settings.py b/archive/uncloud_etcd_based/uncloud/common/settings.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/settings.py rename to archive/uncloud_etcd_based/uncloud/common/settings.py diff --git a/uncloud_etcd_based/uncloud/common/shared.py b/archive/uncloud_etcd_based/uncloud/common/shared.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/shared.py rename to archive/uncloud_etcd_based/uncloud/common/shared.py diff --git a/uncloud_etcd_based/uncloud/common/storage_handlers.py b/archive/uncloud_etcd_based/uncloud/common/storage_handlers.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/storage_handlers.py rename to archive/uncloud_etcd_based/uncloud/common/storage_handlers.py diff --git a/uncloud_etcd_based/uncloud/common/vm.py b/archive/uncloud_etcd_based/uncloud/common/vm.py similarity index 100% rename from uncloud_etcd_based/uncloud/common/vm.py rename to archive/uncloud_etcd_based/uncloud/common/vm.py diff --git a/uncloud_django_based/uncloud/uncloud_net/migrations/__init__.py b/archive/uncloud_etcd_based/uncloud/configure/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/migrations/__init__.py rename to archive/uncloud_etcd_based/uncloud/configure/__init__.py diff --git a/uncloud_etcd_based/uncloud/configure/main.py b/archive/uncloud_etcd_based/uncloud/configure/main.py similarity index 100% rename from uncloud_etcd_based/uncloud/configure/main.py rename to archive/uncloud_etcd_based/uncloud/configure/main.py diff --git a/uncloud_etcd_based/uncloud/filescanner/__init__.py b/archive/uncloud_etcd_based/uncloud/filescanner/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/filescanner/__init__.py rename to archive/uncloud_etcd_based/uncloud/filescanner/__init__.py diff --git a/uncloud_etcd_based/uncloud/filescanner/main.py b/archive/uncloud_etcd_based/uncloud/filescanner/main.py similarity index 100% rename from uncloud_etcd_based/uncloud/filescanner/main.py rename to archive/uncloud_etcd_based/uncloud/filescanner/main.py diff --git a/uncloud_etcd_based/uncloud/hack/README.org b/archive/uncloud_etcd_based/uncloud/hack/README.org similarity index 100% rename from uncloud_etcd_based/uncloud/hack/README.org rename to archive/uncloud_etcd_based/uncloud/hack/README.org diff --git a/uncloud_etcd_based/uncloud/hack/__init__.py b/archive/uncloud_etcd_based/uncloud/hack/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/hack/__init__.py rename to archive/uncloud_etcd_based/uncloud/hack/__init__.py diff --git a/uncloud_etcd_based/uncloud/hack/conf.d/ucloud-host b/archive/uncloud_etcd_based/uncloud/hack/conf.d/ucloud-host similarity index 100% rename from uncloud_etcd_based/uncloud/hack/conf.d/ucloud-host rename to archive/uncloud_etcd_based/uncloud/hack/conf.d/ucloud-host diff --git a/uncloud_etcd_based/uncloud/hack/config.py b/archive/uncloud_etcd_based/uncloud/hack/config.py similarity index 100% rename from uncloud_etcd_based/uncloud/hack/config.py rename to archive/uncloud_etcd_based/uncloud/hack/config.py diff --git a/uncloud_etcd_based/uncloud/hack/db.py b/archive/uncloud_etcd_based/uncloud/hack/db.py similarity index 100% rename from uncloud_etcd_based/uncloud/hack/db.py rename to archive/uncloud_etcd_based/uncloud/hack/db.py diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/.gitignore b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/.gitignore similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/.gitignore rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/.gitignore diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/__init__.py b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/__init__.py rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/__init__.py diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/etcd-client.sh b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/etcd-client.sh similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/etcd-client.sh rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/etcd-client.sh diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/ifdown.sh b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/ifdown.sh similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/ifdown.sh rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/ifdown.sh diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/ifup.sh b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/ifup.sh similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/ifup.sh rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/ifup.sh diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/mac-last b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/mac-last similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/mac-last rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/mac-last diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/mac-prefix b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/mac-prefix similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/mac-prefix rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/mac-prefix diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/net.sh b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/net.sh similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/net.sh rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/net.sh diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/nftrules b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/nftrules similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/nftrules rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/nftrules diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.conf b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.conf similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/radvd.conf rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.conf diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.sh b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.sh similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/radvd.sh rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.sh diff --git a/uncloud_etcd_based/uncloud/hack/hackcloud/vm.sh b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/vm.sh similarity index 100% rename from uncloud_etcd_based/uncloud/hack/hackcloud/vm.sh rename to archive/uncloud_etcd_based/uncloud/hack/hackcloud/vm.sh diff --git a/uncloud_etcd_based/uncloud/hack/host.py b/archive/uncloud_etcd_based/uncloud/hack/host.py similarity index 100% rename from uncloud_etcd_based/uncloud/hack/host.py rename to archive/uncloud_etcd_based/uncloud/hack/host.py diff --git a/uncloud_etcd_based/uncloud/hack/mac.py b/archive/uncloud_etcd_based/uncloud/hack/mac.py similarity index 100% rename from uncloud_etcd_based/uncloud/hack/mac.py rename to archive/uncloud_etcd_based/uncloud/hack/mac.py diff --git a/uncloud_etcd_based/uncloud/hack/main.py b/archive/uncloud_etcd_based/uncloud/hack/main.py similarity index 100% rename from uncloud_etcd_based/uncloud/hack/main.py rename to archive/uncloud_etcd_based/uncloud/hack/main.py diff --git a/uncloud_etcd_based/uncloud/hack/net.py b/archive/uncloud_etcd_based/uncloud/hack/net.py similarity index 100% rename from uncloud_etcd_based/uncloud/hack/net.py rename to archive/uncloud_etcd_based/uncloud/hack/net.py diff --git a/uncloud_etcd_based/uncloud/hack/nftables.conf b/archive/uncloud_etcd_based/uncloud/hack/nftables.conf similarity index 100% rename from uncloud_etcd_based/uncloud/hack/nftables.conf rename to archive/uncloud_etcd_based/uncloud/hack/nftables.conf diff --git a/uncloud_etcd_based/uncloud/hack/product.py b/archive/uncloud_etcd_based/uncloud/hack/product.py similarity index 100% rename from uncloud_etcd_based/uncloud/hack/product.py rename to archive/uncloud_etcd_based/uncloud/hack/product.py diff --git a/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-api b/archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-api similarity index 100% rename from uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-api rename to archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-api diff --git a/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-host b/archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-host similarity index 100% rename from uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-host rename to archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-host diff --git a/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-metadata b/archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-metadata similarity index 100% rename from uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-metadata rename to archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-metadata diff --git a/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-scheduler b/archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-scheduler similarity index 100% rename from uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-scheduler rename to archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-scheduler diff --git a/uncloud_etcd_based/uncloud/hack/uncloud-hack-init-host b/archive/uncloud_etcd_based/uncloud/hack/uncloud-hack-init-host similarity index 100% rename from uncloud_etcd_based/uncloud/hack/uncloud-hack-init-host rename to archive/uncloud_etcd_based/uncloud/hack/uncloud-hack-init-host diff --git a/uncloud_etcd_based/uncloud/hack/uncloud-run-vm b/archive/uncloud_etcd_based/uncloud/hack/uncloud-run-vm similarity index 100% rename from uncloud_etcd_based/uncloud/hack/uncloud-run-vm rename to archive/uncloud_etcd_based/uncloud/hack/uncloud-run-vm diff --git a/uncloud_etcd_based/uncloud/hack/vm.py b/archive/uncloud_etcd_based/uncloud/hack/vm.py similarity index 100% rename from uncloud_etcd_based/uncloud/hack/vm.py rename to archive/uncloud_etcd_based/uncloud/hack/vm.py diff --git a/uncloud_etcd_based/uncloud/host/__init__.py b/archive/uncloud_etcd_based/uncloud/host/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/host/__init__.py rename to archive/uncloud_etcd_based/uncloud/host/__init__.py diff --git a/uncloud_etcd_based/uncloud/host/main.py b/archive/uncloud_etcd_based/uncloud/host/main.py similarity index 100% rename from uncloud_etcd_based/uncloud/host/main.py rename to archive/uncloud_etcd_based/uncloud/host/main.py diff --git a/uncloud_etcd_based/uncloud/host/virtualmachine.py b/archive/uncloud_etcd_based/uncloud/host/virtualmachine.py similarity index 100% rename from uncloud_etcd_based/uncloud/host/virtualmachine.py rename to archive/uncloud_etcd_based/uncloud/host/virtualmachine.py diff --git a/uncloud_etcd_based/uncloud/imagescanner/__init__.py b/archive/uncloud_etcd_based/uncloud/imagescanner/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/imagescanner/__init__.py rename to archive/uncloud_etcd_based/uncloud/imagescanner/__init__.py diff --git a/uncloud_etcd_based/uncloud/imagescanner/main.py b/archive/uncloud_etcd_based/uncloud/imagescanner/main.py similarity index 100% rename from uncloud_etcd_based/uncloud/imagescanner/main.py rename to archive/uncloud_etcd_based/uncloud/imagescanner/main.py diff --git a/uncloud_etcd_based/uncloud/metadata/__init__.py b/archive/uncloud_etcd_based/uncloud/metadata/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/metadata/__init__.py rename to archive/uncloud_etcd_based/uncloud/metadata/__init__.py diff --git a/uncloud_etcd_based/uncloud/metadata/main.py b/archive/uncloud_etcd_based/uncloud/metadata/main.py similarity index 100% rename from uncloud_etcd_based/uncloud/metadata/main.py rename to archive/uncloud_etcd_based/uncloud/metadata/main.py diff --git a/uncloud_etcd_based/uncloud/network/README b/archive/uncloud_etcd_based/uncloud/network/README similarity index 100% rename from uncloud_etcd_based/uncloud/network/README rename to archive/uncloud_etcd_based/uncloud/network/README diff --git a/uncloud_django_based/uncloud/uncloud_pay/__init__.py b/archive/uncloud_etcd_based/uncloud/network/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/__init__.py rename to archive/uncloud_etcd_based/uncloud/network/__init__.py diff --git a/uncloud_etcd_based/uncloud/network/create-bridge.sh b/archive/uncloud_etcd_based/uncloud/network/create-bridge.sh similarity index 100% rename from uncloud_etcd_based/uncloud/network/create-bridge.sh rename to archive/uncloud_etcd_based/uncloud/network/create-bridge.sh diff --git a/uncloud_etcd_based/uncloud/network/create-tap.sh b/archive/uncloud_etcd_based/uncloud/network/create-tap.sh similarity index 100% rename from uncloud_etcd_based/uncloud/network/create-tap.sh rename to archive/uncloud_etcd_based/uncloud/network/create-tap.sh diff --git a/uncloud_etcd_based/uncloud/network/create-vxlan.sh b/archive/uncloud_etcd_based/uncloud/network/create-vxlan.sh similarity index 100% rename from uncloud_etcd_based/uncloud/network/create-vxlan.sh rename to archive/uncloud_etcd_based/uncloud/network/create-vxlan.sh diff --git a/uncloud_etcd_based/uncloud/network/radvd-template.conf b/archive/uncloud_etcd_based/uncloud/network/radvd-template.conf similarity index 100% rename from uncloud_etcd_based/uncloud/network/radvd-template.conf rename to archive/uncloud_etcd_based/uncloud/network/radvd-template.conf diff --git a/uncloud_etcd_based/uncloud/oneshot/__init__.py b/archive/uncloud_etcd_based/uncloud/oneshot/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/oneshot/__init__.py rename to archive/uncloud_etcd_based/uncloud/oneshot/__init__.py diff --git a/uncloud_etcd_based/uncloud/oneshot/main.py b/archive/uncloud_etcd_based/uncloud/oneshot/main.py similarity index 100% rename from uncloud_etcd_based/uncloud/oneshot/main.py rename to archive/uncloud_etcd_based/uncloud/oneshot/main.py diff --git a/uncloud_etcd_based/uncloud/oneshot/virtualmachine.py b/archive/uncloud_etcd_based/uncloud/oneshot/virtualmachine.py similarity index 100% rename from uncloud_etcd_based/uncloud/oneshot/virtualmachine.py rename to archive/uncloud_etcd_based/uncloud/oneshot/virtualmachine.py diff --git a/uncloud_etcd_based/uncloud/scheduler/__init__.py b/archive/uncloud_etcd_based/uncloud/scheduler/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/scheduler/__init__.py rename to archive/uncloud_etcd_based/uncloud/scheduler/__init__.py diff --git a/uncloud_etcd_based/uncloud/scheduler/helper.py b/archive/uncloud_etcd_based/uncloud/scheduler/helper.py similarity index 100% rename from uncloud_etcd_based/uncloud/scheduler/helper.py rename to archive/uncloud_etcd_based/uncloud/scheduler/helper.py diff --git a/uncloud_etcd_based/uncloud/scheduler/main.py b/archive/uncloud_etcd_based/uncloud/scheduler/main.py similarity index 100% rename from uncloud_etcd_based/uncloud/scheduler/main.py rename to archive/uncloud_etcd_based/uncloud/scheduler/main.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/__init__.py b/archive/uncloud_etcd_based/uncloud/scheduler/tests/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/__init__.py rename to archive/uncloud_etcd_based/uncloud/scheduler/tests/__init__.py diff --git a/uncloud_etcd_based/uncloud/scheduler/tests/test_basics.py b/archive/uncloud_etcd_based/uncloud/scheduler/tests/test_basics.py similarity index 100% rename from uncloud_etcd_based/uncloud/scheduler/tests/test_basics.py rename to archive/uncloud_etcd_based/uncloud/scheduler/tests/test_basics.py diff --git a/uncloud_etcd_based/uncloud/scheduler/tests/test_dead_host_mechanism.py b/archive/uncloud_etcd_based/uncloud/scheduler/tests/test_dead_host_mechanism.py similarity index 100% rename from uncloud_etcd_based/uncloud/scheduler/tests/test_dead_host_mechanism.py rename to archive/uncloud_etcd_based/uncloud/scheduler/tests/test_dead_host_mechanism.py diff --git a/uncloud_etcd_based/uncloud/version.py b/archive/uncloud_etcd_based/uncloud/version.py similarity index 100% rename from uncloud_etcd_based/uncloud/version.py rename to archive/uncloud_etcd_based/uncloud/version.py diff --git a/uncloud_etcd_based/uncloud/vmm/__init__.py b/archive/uncloud_etcd_based/uncloud/vmm/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/vmm/__init__.py rename to archive/uncloud_etcd_based/uncloud/vmm/__init__.py diff --git a/uncloud_django_based/uncloud/doc/README-how-to-configure-remote-uncloud-clients.org b/doc/README-how-to-configure-remote-uncloud-clients.org similarity index 100% rename from uncloud_django_based/uncloud/doc/README-how-to-configure-remote-uncloud-clients.org rename to doc/README-how-to-configure-remote-uncloud-clients.org diff --git a/uncloud_django_based/uncloud/doc/README-identifiers.org b/doc/README-identifiers.org similarity index 100% rename from uncloud_django_based/uncloud/doc/README-identifiers.org rename to doc/README-identifiers.org diff --git a/uncloud_django_based/uncloud/doc/README-object-relations.md b/doc/README-object-relations.md similarity index 100% rename from uncloud_django_based/uncloud/doc/README-object-relations.md rename to doc/README-object-relations.md diff --git a/uncloud_django_based/uncloud/doc/README-postgresql.org b/doc/README-postgresql.org similarity index 100% rename from uncloud_django_based/uncloud/doc/README-postgresql.org rename to doc/README-postgresql.org diff --git a/uncloud_django_based/uncloud/doc/README-products.md b/doc/README-products.md similarity index 100% rename from uncloud_django_based/uncloud/doc/README-products.md rename to doc/README-products.md diff --git a/uncloud_django_based/uncloud/doc/README-vpn.org b/doc/README-vpn.org similarity index 100% rename from uncloud_django_based/uncloud/doc/README-vpn.org rename to doc/README-vpn.org diff --git a/uncloud_django_based/uncloud/doc/README.md b/doc/README.md similarity index 100% rename from uncloud_django_based/uncloud/doc/README.md rename to doc/README.md diff --git a/uncloud_django_based/uncloud/manage.py b/manage.py similarity index 100% rename from uncloud_django_based/uncloud/manage.py rename to manage.py diff --git a/uncloud_django_based/uncloud/models.dot b/models.dot similarity index 100% rename from uncloud_django_based/uncloud/models.dot rename to models.dot diff --git a/uncloud_django_based/uncloud/models.png b/models.png similarity index 100% rename from uncloud_django_based/uncloud/models.png rename to models.png diff --git a/uncloud_django_based/uncloud/uncloud_service/__init__.py b/opennebula/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_service/__init__.py rename to opennebula/__init__.py diff --git a/uncloud_django_based/uncloud/opennebula/admin.py b/opennebula/admin.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/admin.py rename to opennebula/admin.py diff --git a/uncloud_django_based/uncloud/opennebula/apps.py b/opennebula/apps.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/apps.py rename to opennebula/apps.py diff --git a/uncloud_django_based/uncloud/opennebula/management/commands/opennebula-synchosts.py b/opennebula/management/commands/opennebula-synchosts.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/management/commands/opennebula-synchosts.py rename to opennebula/management/commands/opennebula-synchosts.py diff --git a/uncloud_django_based/uncloud/opennebula/management/commands/opennebula-syncvms.py b/opennebula/management/commands/opennebula-syncvms.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/management/commands/opennebula-syncvms.py rename to opennebula/management/commands/opennebula-syncvms.py diff --git a/uncloud_django_based/uncloud/opennebula/management/commands/opennebula-to-uncloud.py b/opennebula/management/commands/opennebula-to-uncloud.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/management/commands/opennebula-to-uncloud.py rename to opennebula/management/commands/opennebula-to-uncloud.py diff --git a/uncloud_django_based/uncloud/opennebula/migrations/0001_initial.py b/opennebula/migrations/0001_initial.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/migrations/0001_initial.py rename to opennebula/migrations/0001_initial.py diff --git a/uncloud_django_based/uncloud/opennebula/migrations/0002_auto_20200225_1335.py b/opennebula/migrations/0002_auto_20200225_1335.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/migrations/0002_auto_20200225_1335.py rename to opennebula/migrations/0002_auto_20200225_1335.py diff --git a/uncloud_django_based/uncloud/opennebula/migrations/0003_auto_20200225_1428.py b/opennebula/migrations/0003_auto_20200225_1428.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/migrations/0003_auto_20200225_1428.py rename to opennebula/migrations/0003_auto_20200225_1428.py diff --git a/uncloud_django_based/uncloud/opennebula/migrations/0004_auto_20200225_1816.py b/opennebula/migrations/0004_auto_20200225_1816.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/migrations/0004_auto_20200225_1816.py rename to opennebula/migrations/0004_auto_20200225_1816.py diff --git a/uncloud_django_based/uncloud/uncloud_service/migrations/__init__.py b/opennebula/migrations/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_service/migrations/__init__.py rename to opennebula/migrations/__init__.py diff --git a/uncloud_django_based/uncloud/opennebula/models.py b/opennebula/models.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/models.py rename to opennebula/models.py diff --git a/uncloud_django_based/uncloud/opennebula/serializers.py b/opennebula/serializers.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/serializers.py rename to opennebula/serializers.py diff --git a/uncloud_django_based/uncloud/opennebula/tests.py b/opennebula/tests.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/tests.py rename to opennebula/tests.py diff --git a/uncloud_django_based/uncloud/opennebula/views.py b/opennebula/views.py similarity index 100% rename from uncloud_django_based/uncloud/opennebula/views.py rename to opennebula/views.py diff --git a/uncloud_django_based/uncloud/requirements.txt b/requirements.txt similarity index 100% rename from uncloud_django_based/uncloud/requirements.txt rename to requirements.txt diff --git a/uncloud_django_based/uncloud/uncloud/.gitignore b/uncloud/.gitignore similarity index 100% rename from uncloud_django_based/uncloud/uncloud/.gitignore rename to uncloud/.gitignore diff --git a/uncloud_django_based/uncloud/uncloud/__init__.py b/uncloud/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud/__init__.py rename to uncloud/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud/asgi.py b/uncloud/asgi.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud/asgi.py rename to uncloud/asgi.py diff --git a/uncloud_django_based/uncloud/uncloud/management/commands/uncloud.py b/uncloud/management/commands/uncloud.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud/management/commands/uncloud.py rename to uncloud/management/commands/uncloud.py diff --git a/uncloud_django_based/uncloud/uncloud/models.py b/uncloud/models.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud/models.py rename to uncloud/models.py diff --git a/uncloud_django_based/uncloud/uncloud/settings.py b/uncloud/settings.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud/settings.py rename to uncloud/settings.py diff --git a/uncloud_django_based/uncloud/uncloud/urls.py b/uncloud/urls.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud/urls.py rename to uncloud/urls.py diff --git a/uncloud_django_based/uncloud/uncloud/wsgi.py b/uncloud/wsgi.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud/wsgi.py rename to uncloud/wsgi.py diff --git a/uncloud_django_based/uncloud/uncloud_storage/__init__.py b/uncloud_auth/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_storage/__init__.py rename to uncloud_auth/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_auth/admin.py b/uncloud_auth/admin.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_auth/admin.py rename to uncloud_auth/admin.py diff --git a/uncloud_django_based/uncloud/uncloud_auth/apps.py b/uncloud_auth/apps.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_auth/apps.py rename to uncloud_auth/apps.py diff --git a/uncloud_django_based/uncloud/uncloud_auth/migrations/0001_initial.py b/uncloud_auth/migrations/0001_initial.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_auth/migrations/0001_initial.py rename to uncloud_auth/migrations/0001_initial.py diff --git a/uncloud_django_based/uncloud/uncloud_auth/migrations/0002_auto_20200318_1343.py b/uncloud_auth/migrations/0002_auto_20200318_1343.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_auth/migrations/0002_auto_20200318_1343.py rename to uncloud_auth/migrations/0002_auto_20200318_1343.py diff --git a/uncloud_django_based/uncloud/uncloud_auth/migrations/0003_auto_20200318_1345.py b/uncloud_auth/migrations/0003_auto_20200318_1345.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_auth/migrations/0003_auto_20200318_1345.py rename to uncloud_auth/migrations/0003_auto_20200318_1345.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/__init__.py b/uncloud_auth/migrations/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/__init__.py rename to uncloud_auth/migrations/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_auth/models.py b/uncloud_auth/models.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_auth/models.py rename to uncloud_auth/models.py diff --git a/uncloud_django_based/uncloud/uncloud_auth/serializers.py b/uncloud_auth/serializers.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_auth/serializers.py rename to uncloud_auth/serializers.py diff --git a/uncloud_django_based/uncloud/uncloud_auth/views.py b/uncloud_auth/views.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_auth/views.py rename to uncloud_auth/views.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/__init__.py b/uncloud_net/__init__.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/__init__.py rename to uncloud_net/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_net/admin.py b/uncloud_net/admin.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/admin.py rename to uncloud_net/admin.py diff --git a/uncloud_django_based/uncloud/uncloud_net/apps.py b/uncloud_net/apps.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/apps.py rename to uncloud_net/apps.py diff --git a/uncloud_django_based/uncloud/uncloud_net/management/commands/vpn.py b/uncloud_net/management/commands/vpn.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/management/commands/vpn.py rename to uncloud_net/management/commands/vpn.py diff --git a/uncloud_django_based/uncloud/uncloud_net/migrations/0001_initial.py b/uncloud_net/migrations/0001_initial.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/migrations/0001_initial.py rename to uncloud_net/migrations/0001_initial.py diff --git a/uncloud_django_based/uncloud/uncloud_net/migrations/0002_auto_20200409_1225.py b/uncloud_net/migrations/0002_auto_20200409_1225.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/migrations/0002_auto_20200409_1225.py rename to uncloud_net/migrations/0002_auto_20200409_1225.py diff --git a/uncloud_django_based/uncloud/uncloud_net/migrations/0003_auto_20200417_0551.py b/uncloud_net/migrations/0003_auto_20200417_0551.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/migrations/0003_auto_20200417_0551.py rename to uncloud_net/migrations/0003_auto_20200417_0551.py diff --git a/uncloud_etcd_based/docs/__init__.py b/uncloud_net/migrations/__init__.py similarity index 100% rename from uncloud_etcd_based/docs/__init__.py rename to uncloud_net/migrations/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_net/models.py b/uncloud_net/models.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/models.py rename to uncloud_net/models.py diff --git a/uncloud_django_based/uncloud/uncloud_net/serializers.py b/uncloud_net/serializers.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/serializers.py rename to uncloud_net/serializers.py diff --git a/uncloud_django_based/uncloud/uncloud_net/tests.py b/uncloud_net/tests.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/tests.py rename to uncloud_net/tests.py diff --git a/uncloud_django_based/uncloud/uncloud_net/views.py b/uncloud_net/views.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_net/views.py rename to uncloud_net/views.py diff --git a/uncloud_etcd_based/docs/source/__init__.py b/uncloud_pay/__init__.py similarity index 100% rename from uncloud_etcd_based/docs/source/__init__.py rename to uncloud_pay/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/admin.py b/uncloud_pay/admin.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/admin.py rename to uncloud_pay/admin.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/apps.py b/uncloud_pay/apps.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/apps.py rename to uncloud_pay/apps.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/helpers.py b/uncloud_pay/helpers.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/helpers.py rename to uncloud_pay/helpers.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/management/commands/charge-negative-balance.py b/uncloud_pay/management/commands/charge-negative-balance.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/management/commands/charge-negative-balance.py rename to uncloud_pay/management/commands/charge-negative-balance.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/management/commands/generate-bills.py b/uncloud_pay/management/commands/generate-bills.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/management/commands/generate-bills.py rename to uncloud_pay/management/commands/generate-bills.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/management/commands/handle-overdue-bills.py b/uncloud_pay/management/commands/handle-overdue-bills.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/management/commands/handle-overdue-bills.py rename to uncloud_pay/management/commands/handle-overdue-bills.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/management/commands/import-vat-rates.py b/uncloud_pay/management/commands/import-vat-rates.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/management/commands/import-vat-rates.py rename to uncloud_pay/management/commands/import-vat-rates.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0001_initial.py b/uncloud_pay/migrations/0001_initial.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0001_initial.py rename to uncloud_pay/migrations/0001_initial.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0002_auto_20200305_1524.py b/uncloud_pay/migrations/0002_auto_20200305_1524.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0002_auto_20200305_1524.py rename to uncloud_pay/migrations/0002_auto_20200305_1524.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0003_auto_20200305_1354.py b/uncloud_pay/migrations/0003_auto_20200305_1354.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0003_auto_20200305_1354.py rename to uncloud_pay/migrations/0003_auto_20200305_1354.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0004_auto_20200409_1225.py b/uncloud_pay/migrations/0004_auto_20200409_1225.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0004_auto_20200409_1225.py rename to uncloud_pay/migrations/0004_auto_20200409_1225.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200413_0924.py b/uncloud_pay/migrations/0005_auto_20200413_0924.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200413_0924.py rename to uncloud_pay/migrations/0005_auto_20200413_0924.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_auto_20200415_1003.py b/uncloud_pay/migrations/0006_auto_20200415_1003.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0006_auto_20200415_1003.py rename to uncloud_pay/migrations/0006_auto_20200415_1003.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_billingaddress.py b/uncloud_pay/migrations/0006_billingaddress.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0006_billingaddress.py rename to uncloud_pay/migrations/0006_billingaddress.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0737.py b/uncloud_pay/migrations/0007_auto_20200418_0737.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0737.py rename to uncloud_pay/migrations/0007_auto_20200418_0737.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0008_auto_20200502_1921.py b/uncloud_pay/migrations/0008_auto_20200502_1921.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0008_auto_20200502_1921.py rename to uncloud_pay/migrations/0008_auto_20200502_1921.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0009_auto_20200502_2047.py b/uncloud_pay/migrations/0009_auto_20200502_2047.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0009_auto_20200502_2047.py rename to uncloud_pay/migrations/0009_auto_20200502_2047.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0010_order_description.py b/uncloud_pay/migrations/0010_order_description.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0010_order_description.py rename to uncloud_pay/migrations/0010_order_description.py diff --git a/uncloud_etcd_based/test/__init__.py b/uncloud_pay/migrations/__init__.py similarity index 100% rename from uncloud_etcd_based/test/__init__.py rename to uncloud_pay/migrations/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_pay/models.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/models.py rename to uncloud_pay/models.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/serializers.py b/uncloud_pay/serializers.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/serializers.py rename to uncloud_pay/serializers.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/stripe.py b/uncloud_pay/stripe.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/stripe.py rename to uncloud_pay/stripe.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/templates/bill.html b/uncloud_pay/templates/bill.html similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/templates/bill.html rename to uncloud_pay/templates/bill.html diff --git a/uncloud_django_based/uncloud/uncloud_pay/templates/error.html.j2 b/uncloud_pay/templates/error.html.j2 similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/templates/error.html.j2 rename to uncloud_pay/templates/error.html.j2 diff --git a/uncloud_django_based/uncloud/uncloud_pay/templates/stripe-payment.html.j2 b/uncloud_pay/templates/stripe-payment.html.j2 similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/templates/stripe-payment.html.j2 rename to uncloud_pay/templates/stripe-payment.html.j2 diff --git a/uncloud_django_based/uncloud/uncloud_pay/tests.py b/uncloud_pay/tests.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/tests.py rename to uncloud_pay/tests.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/views.py b/uncloud_pay/views.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_pay/views.py rename to uncloud_pay/views.py diff --git a/uncloud_etcd_based/uncloud/cli/__init__.py b/uncloud_service/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/cli/__init__.py rename to uncloud_service/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_service/admin.py b/uncloud_service/admin.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_service/admin.py rename to uncloud_service/admin.py diff --git a/uncloud_django_based/uncloud/uncloud_service/apps.py b/uncloud_service/apps.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_service/apps.py rename to uncloud_service/apps.py diff --git a/uncloud_django_based/uncloud/uncloud_service/migrations/0001_initial.py b/uncloud_service/migrations/0001_initial.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_service/migrations/0001_initial.py rename to uncloud_service/migrations/0001_initial.py diff --git a/uncloud_django_based/uncloud/uncloud_service/migrations/0002_auto_20200418_0641.py b/uncloud_service/migrations/0002_auto_20200418_0641.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_service/migrations/0002_auto_20200418_0641.py rename to uncloud_service/migrations/0002_auto_20200418_0641.py diff --git a/uncloud_etcd_based/uncloud/client/__init__.py b/uncloud_service/migrations/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/client/__init__.py rename to uncloud_service/migrations/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_service/models.py b/uncloud_service/models.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_service/models.py rename to uncloud_service/models.py diff --git a/uncloud_django_based/uncloud/uncloud_service/serializers.py b/uncloud_service/serializers.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_service/serializers.py rename to uncloud_service/serializers.py diff --git a/uncloud_django_based/uncloud/uncloud_service/tests.py b/uncloud_service/tests.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_service/tests.py rename to uncloud_service/tests.py diff --git a/uncloud_django_based/uncloud/uncloud_service/views.py b/uncloud_service/views.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_service/views.py rename to uncloud_service/views.py diff --git a/uncloud_etcd_based/uncloud/configure/__init__.py b/uncloud_storage/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/configure/__init__.py rename to uncloud_storage/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_storage/admin.py b/uncloud_storage/admin.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_storage/admin.py rename to uncloud_storage/admin.py diff --git a/uncloud_django_based/uncloud/uncloud_storage/apps.py b/uncloud_storage/apps.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_storage/apps.py rename to uncloud_storage/apps.py diff --git a/uncloud_django_based/uncloud/uncloud_storage/models.py b/uncloud_storage/models.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_storage/models.py rename to uncloud_storage/models.py diff --git a/uncloud_django_based/uncloud/uncloud_storage/tests.py b/uncloud_storage/tests.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_storage/tests.py rename to uncloud_storage/tests.py diff --git a/uncloud_django_based/uncloud/uncloud_storage/views.py b/uncloud_storage/views.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_storage/views.py rename to uncloud_storage/views.py diff --git a/uncloud_etcd_based/uncloud/network/__init__.py b/uncloud_vm/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/network/__init__.py rename to uncloud_vm/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/admin.py b/uncloud_vm/admin.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/admin.py rename to uncloud_vm/admin.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/apps.py b/uncloud_vm/apps.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/apps.py rename to uncloud_vm/apps.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/management/commands/vm.py b/uncloud_vm/management/commands/vm.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/management/commands/vm.py rename to uncloud_vm/management/commands/vm.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0001_initial.py b/uncloud_vm/migrations/0001_initial.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0001_initial.py rename to uncloud_vm/migrations/0001_initial.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0002_auto_20200305_1321.py b/uncloud_vm/migrations/0002_auto_20200305_1321.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0002_auto_20200305_1321.py rename to uncloud_vm/migrations/0002_auto_20200305_1321.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0003_remove_vmhost_vms.py b/uncloud_vm/migrations/0003_remove_vmhost_vms.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0003_remove_vmhost_vms.py rename to uncloud_vm/migrations/0003_remove_vmhost_vms.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0004_remove_vmproduct_vmid.py b/uncloud_vm/migrations/0004_remove_vmproduct_vmid.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0004_remove_vmproduct_vmid.py rename to uncloud_vm/migrations/0004_remove_vmproduct_vmid.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0004_vmproduct_primary_disk.py b/uncloud_vm/migrations/0004_vmproduct_primary_disk.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0004_vmproduct_primary_disk.py rename to uncloud_vm/migrations/0004_vmproduct_primary_disk.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0005_auto_20200309_1258.py b/uncloud_vm/migrations/0005_auto_20200309_1258.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0005_auto_20200309_1258.py rename to uncloud_vm/migrations/0005_auto_20200309_1258.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0005_auto_20200321_1058.py b/uncloud_vm/migrations/0005_auto_20200321_1058.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0005_auto_20200321_1058.py rename to uncloud_vm/migrations/0005_auto_20200321_1058.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0006_auto_20200322_1758.py b/uncloud_vm/migrations/0006_auto_20200322_1758.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0006_auto_20200322_1758.py rename to uncloud_vm/migrations/0006_auto_20200322_1758.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0007_vmhost_vmcluster.py b/uncloud_vm/migrations/0007_vmhost_vmcluster.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0007_vmhost_vmcluster.py rename to uncloud_vm/migrations/0007_vmhost_vmcluster.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0008_auto_20200403_1727.py b/uncloud_vm/migrations/0008_auto_20200403_1727.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0008_auto_20200403_1727.py rename to uncloud_vm/migrations/0008_auto_20200403_1727.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0009_auto_20200417_0551.py b/uncloud_vm/migrations/0009_auto_20200417_0551.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0009_auto_20200417_0551.py rename to uncloud_vm/migrations/0009_auto_20200417_0551.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0009_merge_20200413_0857.py b/uncloud_vm/migrations/0009_merge_20200413_0857.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0009_merge_20200413_0857.py rename to uncloud_vm/migrations/0009_merge_20200413_0857.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0010_auto_20200413_0924.py b/uncloud_vm/migrations/0010_auto_20200413_0924.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0010_auto_20200413_0924.py rename to uncloud_vm/migrations/0010_auto_20200413_0924.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0011_merge_20200418_0641.py b/uncloud_vm/migrations/0011_merge_20200418_0641.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0011_merge_20200418_0641.py rename to uncloud_vm/migrations/0011_merge_20200418_0641.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0012_auto_20200418_0641.py b/uncloud_vm/migrations/0012_auto_20200418_0641.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0012_auto_20200418_0641.py rename to uncloud_vm/migrations/0012_auto_20200418_0641.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0013_remove_vmproduct_primary_disk.py b/uncloud_vm/migrations/0013_remove_vmproduct_primary_disk.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/migrations/0013_remove_vmproduct_primary_disk.py rename to uncloud_vm/migrations/0013_remove_vmproduct_primary_disk.py diff --git a/uncloud_etcd_based/uncloud/scheduler/tests/__init__.py b/uncloud_vm/migrations/__init__.py similarity index 100% rename from uncloud_etcd_based/uncloud/scheduler/tests/__init__.py rename to uncloud_vm/migrations/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/models.py b/uncloud_vm/models.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/models.py rename to uncloud_vm/models.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/serializers.py b/uncloud_vm/serializers.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/serializers.py rename to uncloud_vm/serializers.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/tests.py b/uncloud_vm/tests.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/tests.py rename to uncloud_vm/tests.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/views.py b/uncloud_vm/views.py similarity index 100% rename from uncloud_django_based/uncloud/uncloud_vm/views.py rename to uncloud_vm/views.py From 1245c191c0083b92a07861709f00de8f2386bea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 12:13:48 +0200 Subject: [PATCH 023/194] Adapt CI to new structure --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index afdc4a1..33c1c06 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,8 +15,6 @@ run-tests: before_script: - dnf install -y python3-devel python3-pip python3-coverage libpq-devel openldap-devel gcc chromium script: - - cd uncloud_django_based/uncloud - pip install -r requirements.txt - - cp uncloud/secrets_sample.py uncloud/secrets.py - coverage run --source='.' ./manage.py test - coverage report From b512d42058104931ff77a839e52ccd4b2c05a4b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 12:21:49 +0200 Subject: [PATCH 024/194] Add devel environment setup instructions --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0e32f57..4ebdd8c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,49 @@ -# ucloud +# Uncloud -Checkout https://ungleich.ch/ucloud/ for the documentation of ucloud. +Cloud management platform, the ungleich way. + +## Development setup + +Install system dependencies: + * On Fedora, you will need the following packages: `python3-virtualenv python3-devel libpq-devel openldap-devel gcc chromium` + +You will need a Postgres database running locally: + * Install on configure PGSQL on your base system. + * OR use a container! `podman run --rm -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -it postgres:latest` + +NOTE: you will need to configure a LDAP server and credentials for authentication. See `uncloud/settings.py`. + +``` +# Initialize virtualenv. +» virtualenv .venv +Using base prefix '/usr' +New python executable in /home/fnux/Workspace/ungleich/uncloud/uncloud/.venv/bin/python3 +Also creating executable in /home/fnux/Workspace/ungleich/uncloud/uncloud/.venv/bin/python +Installing setuptools, pip, wheel... +done. + +# Enter virtualenv. +» source .venv/bin/activate + +# Install dependencies. +» pip install -r requirements.txt +[...] + +# Run migrations. +» ./manage.py migrate +Operations to perform: + Apply all migrations: admin, auth, contenttypes, opennebula, sessions, uncloud_auth, uncloud_net, uncloud_pay, uncloud_service, uncloud_vm +Running migrations: + [...] + +# Run webserver. +» ./manage.py runserver +Watching for file changes with StatReloader +Performing system checks... + +System check identified no issues (0 silenced). +May 07, 2020 - 10:17:08 +Django version 3.0.6, using settings 'uncloud.settings' +Starting development server at http://127.0.0.1:8000/ +Quit the server with CONTROL-C. +``` From ebd4e6fa1b06c17ab438a811df31b776be9f5dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 12:23:17 +0200 Subject: [PATCH 025/194] Add fancy CI badges to README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 4ebdd8c..ea4e87c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ Cloud management platform, the ungleich way. + +[![pipeline status](https://code.ungleich.ch/uncloud/uncloud/badges/master/pipeline.svg)](https://code.ungleich.ch/uncloud/uncloud/commits/master) + +[![coverage report](https://code.ungleich.ch/uncloud/uncloud/badges/master/coverage.svg)](https://code.ungleich.ch/uncloud/uncloud/commits/master) + ## Development setup Install system dependencies: From 221d98af4b0f9dcb08e0eff72e381118afe05f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 12:24:17 +0200 Subject: [PATCH 026/194] Inline CI badges --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ea4e87c..87f3067 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ Cloud management platform, the ungleich way. [![pipeline status](https://code.ungleich.ch/uncloud/uncloud/badges/master/pipeline.svg)](https://code.ungleich.ch/uncloud/uncloud/commits/master) - [![coverage report](https://code.ungleich.ch/uncloud/uncloud/badges/master/coverage.svg)](https://code.ungleich.ch/uncloud/uncloud/commits/master) ## Development setup From b8ac99acb68740c54bba5a79cc74931385306ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 12:25:05 +0200 Subject: [PATCH 027/194] On more small commit to fix README formatting --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 87f3067..cb5a25f 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,13 @@ Cloud management platform, the ungleich way. ## Development setup Install system dependencies: - * On Fedora, you will need the following packages: `python3-virtualenv python3-devel libpq-devel openldap-devel gcc chromium` + +* On Fedora, you will need the following packages: `python3-virtualenv python3-devel libpq-devel openldap-devel gcc chromium` You will need a Postgres database running locally: - * Install on configure PGSQL on your base system. - * OR use a container! `podman run --rm -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -it postgres:latest` + +* Install on configure PGSQL on your base system. +* OR use a container! `podman run --rm -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -it postgres:latest` NOTE: you will need to configure a LDAP server and credentials for authentication. See `uncloud/settings.py`. From 268e08c4dbe09fcdd1c1dc495e7fadd5ac54d107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 12:31:59 +0200 Subject: [PATCH 028/194] Adapt README for SQLite --- .gitignore | 1 + README.md | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index cbb171f..ab6a151 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ venv/ dist/ *.iso +*.sqlite3 diff --git a/README.md b/README.md index cb5a25f..6da7cdb 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,7 @@ Cloud management platform, the ungleich way. Install system dependencies: -* On Fedora, you will need the following packages: `python3-virtualenv python3-devel libpq-devel openldap-devel gcc chromium` - -You will need a Postgres database running locally: - -* Install on configure PGSQL on your base system. -* OR use a container! `podman run --rm -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -it postgres:latest` +* On Fedora, you will need the following packages: `python3-virtualenv python3-devel openldap-devel gcc chromium` NOTE: you will need to configure a LDAP server and credentials for authentication. See `uncloud/settings.py`. @@ -53,3 +48,11 @@ Django version 3.0.6, using settings 'uncloud.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. ``` + +### Note on PGSQL + +If you want to use Postgres: + +* Install on configure PGSQL on your base system. +* OR use a container! `podman run --rm -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -it postgres:latest` + From 718abab9d2ac842f19a08bd4f0f6acf53f2a355c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 12:45:06 +0200 Subject: [PATCH 029/194] Add make-admin command to uncloud_auth --- README.md | 6 +++++- uncloud_auth/management/commands/make-admin.py | 16 ++++++++++++++++ uncloud_pay/views.py | 4 ++-- 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 uncloud_auth/management/commands/make-admin.py diff --git a/README.md b/README.md index 6da7cdb..8c53654 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,11 @@ Cloud management platform, the ungleich way. [![pipeline status](https://code.ungleich.ch/uncloud/uncloud/badges/master/pipeline.svg)](https://code.ungleich.ch/uncloud/uncloud/commits/master) [![coverage report](https://code.ungleich.ch/uncloud/uncloud/badges/master/coverage.svg)](https://code.ungleich.ch/uncloud/uncloud/commits/master) +## Useful commands + +* `./manage.py import-vat-rates path/to/csv` +* `./manage.py make-admin username` + ## Development setup Install system dependencies: @@ -55,4 +60,3 @@ If you want to use Postgres: * Install on configure PGSQL on your base system. * OR use a container! `podman run --rm -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -it postgres:latest` - diff --git a/uncloud_auth/management/commands/make-admin.py b/uncloud_auth/management/commands/make-admin.py new file mode 100644 index 0000000..b750bc3 --- /dev/null +++ b/uncloud_auth/management/commands/make-admin.py @@ -0,0 +1,16 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +import sys + +class Command(BaseCommand): + help = 'Give Admin rights to existing user' + + def add_arguments(self, parser): + parser.add_argument('username', type=str) + + def handle(self, *args, **options): + user = get_user_model().objects.get(username=options['username']) + user.is_staff = True + user.save() + + print("{} is now admin.".format(user.username)) diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index 54ff2f0..8bb2280 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -243,7 +243,7 @@ class BillingAddressViewSet(mixins.CreateModelMixin, return Response(serializer.data) ### -# Old admin stuff. +# Admin stuff. class AdminPaymentViewSet(viewsets.ModelViewSet): serializer_class = PaymentSerializer @@ -279,7 +279,7 @@ class AdminBillViewSet(viewsets.ModelViewSet): return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) class AdminOrderViewSet(viewsets.ModelViewSet): - permission_classes = [permissions.IsAuthenticated] + permission_classes = [permissions.IsAdminUser] def get_serializer(self, *args, **kwargs): return OrderSerializer(*args, **kwargs, admin=True) From 56d98cbb55523739375abc007bc89a96b6c288b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 13:12:38 +0200 Subject: [PATCH 030/194] Implement Orders/Bills permissions, unpaid bill views --- uncloud_pay/views.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index 8bb2280..bb73cfb 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -182,8 +182,13 @@ class BillViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): return Bill.objects.filter(owner=self.request.user) + + @action(detail=False, methods=['get']) def unpaid(self, request): - return Bill.objects.filter(owner=self.request.user, paid=False) + serializer = self.get_serializer( + Bill.get_unpaid_for(self.request.user), + many=True) + return Response(serializer.data) class OrderViewSet(viewsets.ReadOnlyModelViewSet): @@ -247,7 +252,7 @@ class BillingAddressViewSet(mixins.CreateModelMixin, class AdminPaymentViewSet(viewsets.ModelViewSet): serializer_class = PaymentSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [permissions.IsAdminUser] def get_queryset(self): return Payment.objects.all() @@ -260,25 +265,28 @@ class AdminPaymentViewSet(viewsets.ModelViewSet): headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) -class AdminBillViewSet(viewsets.ModelViewSet): +# Bills are generated from orders and should not be created or updated by hand. +class AdminBillViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = BillSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [permissions.IsAdminUser] def get_queryset(self): return Bill.objects.all() + @action(detail=False, methods=['get']) def unpaid(self, request): - return Bill.objects.filter(owner=self.request.user, paid=False) + unpaid_bills = [] + # XXX: works but we can do better than number of users + 1 SQL requests... + for user in get_user_model().objects.all(): + unpaid_bills = unpaid_bills + Bill.get_unpaid_for(self.request.user) - def create(self, request): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - serializer.save(creation_date=datetime.now()) + serializer = self.get_serializer(unpaid_bills, many=True) + return Response(serializer.data) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - -class AdminOrderViewSet(viewsets.ModelViewSet): +class AdminOrderViewSet(mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + viewsets.GenericViewSet): permission_classes = [permissions.IsAdminUser] def get_serializer(self, *args, **kwargs): From 3874165189692604022b11bb3d661edb4d957c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 14:24:04 +0200 Subject: [PATCH 031/194] Fix bill generation --- uncloud_pay/models.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 1294a54..68016a9 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -521,9 +521,8 @@ class Bill(models.Model): bill_records = [] orders = Order.objects.filter(bill=self) for order in orders: - for order_record in order.records: - bill_record = BillRecord(self, order_record) - bill_records.append(bill_record) + bill_record = BillRecord(self, order) + bill_records.append(bill_record) return bill_records @@ -710,18 +709,18 @@ class Bill(models.Model): class BillRecord(): """ - Entry of a bill, dynamically generated from order records. + Entry of a bill, dynamically generated from an order. """ - def __init__(self, bill, order_record): + def __init__(self, bill, order): self.bill = bill - self.order = order_record.order - self.recurring_price = order_record.recurring_price - self.recurring_period = order_record.recurring_period - self.description = order_record.description + self.order = order + self.recurring_price = order.recurring_price + self.recurring_period = order.recurring_period + self.description = order.description if self.order.starting_date >= self.bill.starting_date: - self.one_time_price = order_record.one_time_price + self.one_time_price = order.one_time_price else: self.one_time_price = 0 @@ -779,7 +778,7 @@ class BillRecord(): return 0 else: raise Exception('Unsupported recurring period: {}.'. - format(record.recurring_period)) + format(self.order.recurring_period)) @property def vat_rate(self): From ae2bad57544ab257e7c7c94e89fa0242fa8be30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 7 May 2020 15:38:49 +0200 Subject: [PATCH 032/194] Generate bill PDFs from /my/bill --- uncloud/urls.py | 1 - .../0011_billingaddress_organization.py | 19 +++ uncloud_pay/models.py | 1 + uncloud_pay/serializers.py | 4 +- .../templates/{bill.html => bill.html.j2} | 156 ++++++------------ uncloud_pay/views.py | 30 ++-- 6 files changed, 96 insertions(+), 115 deletions(-) create mode 100644 uncloud_pay/migrations/0011_billingaddress_organization.py rename uncloud_pay/templates/{bill.html => bill.html.j2} (96%) diff --git a/uncloud/urls.py b/uncloud/urls.py index 05b1f0f..b20f136 100644 --- a/uncloud/urls.py +++ b/uncloud/urls.py @@ -81,7 +81,6 @@ urlpatterns = [ path('', include(router.urls)), # web/ = stuff to view in the browser - path('web/pdf/', payviews.MyPDFView.as_view(), name='pdf'), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), # for login to REST API path('openapi', get_schema_view( title="uncloud", diff --git a/uncloud_pay/migrations/0011_billingaddress_organization.py b/uncloud_pay/migrations/0011_billingaddress_organization.py new file mode 100644 index 0000000..ac36eee --- /dev/null +++ b/uncloud_pay/migrations/0011_billingaddress_organization.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.6 on 2020-05-07 13:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0010_order_description'), + ] + + operations = [ + migrations.AddField( + model_name='billingaddress', + name='organization', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 68016a9..92c58ab 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -444,6 +444,7 @@ class BillingAddress(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + organization = models.CharField(max_length=100) name = models.CharField(max_length=100) street = models.CharField(max_length=100) city = models.CharField(max_length=50) diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py index ad50c68..1d7dcdd 100644 --- a/uncloud_pay/serializers.py +++ b/uncloud_pay/serializers.py @@ -95,7 +95,7 @@ class BillRecordSerializer(serializers.Serializer): class BillingAddressSerializer(serializers.ModelSerializer): class Meta: model = BillingAddress - fields = ['uuid', 'name', 'street', 'city', 'postal_code', 'country', 'vat_number'] + fields = ['uuid', 'organization', 'name', 'street', 'city', 'postal_code', 'country', 'vat_number'] class BillSerializer(serializers.ModelSerializer): billing_address = BillingAddressSerializer(read_only=True) @@ -103,7 +103,7 @@ class BillSerializer(serializers.ModelSerializer): class Meta: model = Bill - fields = ['reference', 'owner', 'amount', 'vat_amount', 'total', + fields = ['uuid', 'reference', 'owner', 'amount', 'vat_amount', 'total', 'due_date', 'creation_date', 'starting_date', 'ending_date', 'records', 'final', 'billing_address'] diff --git a/uncloud_pay/templates/bill.html b/uncloud_pay/templates/bill.html.j2 similarity index 96% rename from uncloud_pay/templates/bill.html rename to uncloud_pay/templates/bill.html.j2 index 8f6c217..0ea7089 100644 --- a/uncloud_pay/templates/bill.html +++ b/uncloud_pay/templates/bill.html.j2 @@ -26,7 +26,7 @@ - Bill name + {{ bill.reference }} | {{ bill.uuid }} +{% endblock %} + +{% block body %} +
+

Registering Stripe Credit Card

+ + + +
+
+ +
+ + +
+
+ + + + + + + +{% endblock %} diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index edfb189..53d6ef4 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -1,3 +1,7 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic.base import TemplateView + + from django.shortcuts import render from django.db import transaction from django.contrib.auth import get_user_model @@ -43,6 +47,25 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): return Order.objects.filter(owner=self.request.user) + +class RegisterCard(LoginRequiredMixin, TemplateView): + login_url = '/login/' + + # This is not supposed to be "static" -- + # the idea is to be able to switch the provider when needed + template_name = "uncloud_pay/stripe.html" + + def get_context_data(self, **kwargs): + customer_id = uncloud_stripe.get_customer_id_for(self.request.user) + setup_intent = uncloud_stripe.create_setup_intent(customer_id) + + context = super().get_context_data(**kwargs) + context['client_secret'] = setup_intent.client_secret + context['username'] = self.request.user + context['stripe_pk'] = uncloud_stripe.public_api_key + return context + + class PaymentMethodViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] From 6efedcb38155598c8c054565ad57a521a47d9de7 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Fri, 25 Dec 2020 17:29:17 +0100 Subject: [PATCH 175/194] hackish way of registering works --- uncloud/urls.py | 2 + uncloud_pay/serializers.py | 12 +- uncloud_pay/stripe.py | 9 -- uncloud_pay/templates/uncloud_pay/stripe.html | 107 +++++++++++------- uncloud_pay/views.py | 6 +- 5 files changed, 80 insertions(+), 56 deletions(-) diff --git a/uncloud/urls.py b/uncloud/urls.py index 169be7f..9097b4c 100644 --- a/uncloud/urls.py +++ b/uncloud/urls.py @@ -86,9 +86,11 @@ urlpatterns = [ # web/ = stuff to view in the browser # path('web/vpn/create/', netviews.WireGuardVPNCreateView.as_view(), name="vpncreate"), + path('login/', authviews.LoginView.as_view(), name="login"), path('logout/', authviews.LogoutView.as_view(), name="logout"), path('admin/', admin.site.urls), path('cc/reg/', payviews.RegisterCard.as_view(), name="cc_register"), + path('cc/submit/', payviews.RegisterCard.as_view(), name="cc_register"), path('', uncloudviews.UncloudIndex.as_view(), name="uncloudindex"), ] diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py index 9214105..94f833e 100644 --- a/uncloud_pay/serializers.py +++ b/uncloud_pay/serializers.py @@ -5,6 +5,14 @@ from django.utils.translation import gettext_lazy as _ from .models import * +### +# Checked code + + +################################################################################ +# Unchecked code + + ### # Payments and Payment Methods. @@ -18,7 +26,7 @@ class PaymentMethodSerializer(serializers.ModelSerializer): class Meta: model = PaymentMethod - fields = ['uuid', 'source', 'description', 'primary', 'stripe_card_last4', 'active'] + fields = [ 'source', 'description', 'primary', 'stripe_card_last4', 'active'] class UpdatePaymentMethodSerializer(serializers.ModelSerializer): class Meta: @@ -30,10 +38,12 @@ class ChargePaymentMethodSerializer(serializers.Serializer): class CreatePaymentMethodSerializer(serializers.ModelSerializer): please_visit = serializers.CharField(read_only=True) + class Meta: model = PaymentMethod fields = ['source', 'description', 'primary', 'please_visit'] + ### # Orders & Products. diff --git a/uncloud_pay/stripe.py b/uncloud_pay/stripe.py index 2ed4ef2..f4c467a 100644 --- a/uncloud_pay/stripe.py +++ b/uncloud_pay/stripe.py @@ -7,17 +7,10 @@ from django.conf import settings import uncloud_pay.models -# Static stripe configuration used below. CURRENCY = 'chf' -# README: We use the Payment Intent API as described on -# https://stripe.com/docs/payments/save-and-reuse - -# For internal use only. stripe.api_key = settings.STRIPE_KEY -# Helper (decorator) used to catch errors raised by stripe logic. -# Catch errors that should not be displayed to the end user, raise again. def handle_stripe_error(f): def handle_problems(*args, **kwargs): response = { @@ -61,8 +54,6 @@ def handle_stripe_error(f): return handle_problems -# Actual Stripe logic. - def public_api_key(): return settings.STRIPE_PUBLIC_KEY diff --git a/uncloud_pay/templates/uncloud_pay/stripe.html b/uncloud_pay/templates/uncloud_pay/stripe.html index 3051bf0..775ed53 100644 --- a/uncloud_pay/templates/uncloud_pay/stripe.html +++ b/uncloud_pay/templates/uncloud_pay/stripe.html @@ -1,7 +1,9 @@ + {% extends 'uncloud/base.html' %} {% block header %} + + + {% endblock %} {% block body %}
-

Registering Stripe Credit Card

+

Registering Credit Card with Stripe

+

+ By submitting I authorise to send instructions to + the financial institution that issued my card to take + payments from my card account in accordance with the + terms of my agreement with you. +

-
-
- -
- -
+
+ + +
The card will be registered with stripe.
+
- - - - {% endblock %} diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index 53d6ef4..78a1a15 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -51,12 +51,12 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet): class RegisterCard(LoginRequiredMixin, TemplateView): login_url = '/login/' - # This is not supposed to be "static" -- - # the idea is to be able to switch the provider when needed template_name = "uncloud_pay/stripe.html" def get_context_data(self, **kwargs): + customer_id = uncloud_stripe.get_customer_id_for(self.request.user) + setup_intent = uncloud_stripe.create_setup_intent(customer_id) context = super().get_context_data(**kwargs) @@ -159,7 +159,7 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): # TODO: find a way to use reverse properly: # https://www.django-rest-framework.org/api-guide/reverse/ callback_path= "payment-method/{}/activate-stripe-cc/".format( - payment_method.uuid) + payment_method.id) callback = reverse('api-root', request=request) + callback_path # Render stripe card registration form. From f7c68b5ca53aa9c01d98da223e7e2aa469b54223 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Fri, 25 Dec 2020 17:33:01 +0100 Subject: [PATCH 176/194] Rename template --- .../uncloud_pay/register_stripe.html | 68 ++++++++++++++ uncloud_pay/templates/uncloud_pay/stripe.html | 93 ------------------- uncloud_pay/views.py | 2 +- 3 files changed, 69 insertions(+), 94 deletions(-) create mode 100644 uncloud_pay/templates/uncloud_pay/register_stripe.html delete mode 100644 uncloud_pay/templates/uncloud_pay/stripe.html diff --git a/uncloud_pay/templates/uncloud_pay/register_stripe.html b/uncloud_pay/templates/uncloud_pay/register_stripe.html new file mode 100644 index 0000000..9fd82ae --- /dev/null +++ b/uncloud_pay/templates/uncloud_pay/register_stripe.html @@ -0,0 +1,68 @@ +{% extends 'uncloud/base.html' %} + +{% block header %} + + + +{% endblock %} + +{% block body %} +
+

Register Credit Card with Stripe

+

+ By submitting I authorise to send instructions to + the financial institution that issued my card to take + payments from my card account in accordance with the + terms of my agreement with you. +

+ + + + +
+ + +
The card will be registered with stripe.
+ +
+ + + +{% endblock %} diff --git a/uncloud_pay/templates/uncloud_pay/stripe.html b/uncloud_pay/templates/uncloud_pay/stripe.html deleted file mode 100644 index 775ed53..0000000 --- a/uncloud_pay/templates/uncloud_pay/stripe.html +++ /dev/null @@ -1,93 +0,0 @@ - -{% extends 'uncloud/base.html' %} - -{% block header %} - - - - - -{% endblock %} - -{% block body %} -
-

Registering Credit Card with Stripe

-

- By submitting I authorise to send instructions to - the financial institution that issued my card to take - payments from my card account in accordance with the - terms of my agreement with you. -

- - - - -
- - -
The card will be registered with stripe.
- -
- - - - - -{% endblock %} diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index 78a1a15..99d176e 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -51,7 +51,7 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet): class RegisterCard(LoginRequiredMixin, TemplateView): login_url = '/login/' - template_name = "uncloud_pay/stripe.html" + template_name = "uncloud_pay/register_stripe.html" def get_context_data(self, **kwargs): From e51edab2f5fc8f107db0860c28337d9c668b1347 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 26 Dec 2020 11:22:51 +0100 Subject: [PATCH 177/194] cleanup/in between commit --- uncloud/settings.py | 4 + uncloud/tasks.py | 3 + uncloud/urls.py | 5 +- uncloud_pay/stripe.py | 26 ++++++- uncloud_pay/tasks.py | 11 +++ uncloud_pay/templates/error.html.j2 | 18 ----- uncloud_pay/templates/stripe-payment.html.j2 | 76 ------------------- .../templates/{ => uncloud_pay}/bill.html.j2 | 0 .../templates/uncloud_pay/list_stripe.html | 30 ++++++++ .../uncloud_pay/register_stripe.html | 3 + uncloud_pay/views.py | 30 ++++++++ 11 files changed, 107 insertions(+), 99 deletions(-) create mode 100644 uncloud_pay/tasks.py delete mode 100644 uncloud_pay/templates/error.html.j2 delete mode 100644 uncloud_pay/templates/stripe-payment.html.j2 rename uncloud_pay/templates/{ => uncloud_pay}/bill.html.j2 (100%) create mode 100644 uncloud_pay/templates/uncloud_pay/list_stripe.html diff --git a/uncloud/settings.py b/uncloud/settings.py index ae734dc..afc6d65 100644 --- a/uncloud/settings.py +++ b/uncloud/settings.py @@ -228,6 +228,10 @@ CELERY_BEAT_SCHEDULE = { 'cleanup_tasks': { 'task': 'uncloud.tasks.cleanup_tasks', 'schedule': 10 + }, + 'check_balance': { + 'task': 'uncloud_pay.tasks.check_balance', + 'schedule': 15 } } diff --git a/uncloud/tasks.py b/uncloud/tasks.py index 5a13ec5..a42f359 100644 --- a/uncloud/tasks.py +++ b/uncloud/tasks.py @@ -14,6 +14,9 @@ def cleanup_tasks(self): continue res = AsyncResult(id=str(task.task_id)) + print(f"Task {task}: {res.state}") if res.ready(): print(res.get()) task.delete() + + res.forget() diff --git a/uncloud/urls.py b/uncloud/urls.py index 9097b4c..343a83c 100644 --- a/uncloud/urls.py +++ b/uncloud/urls.py @@ -90,7 +90,10 @@ urlpatterns = [ path('login/', authviews.LoginView.as_view(), name="login"), path('logout/', authviews.LogoutView.as_view(), name="logout"), path('admin/', admin.site.urls), + path('cc/reg/', payviews.RegisterCard.as_view(), name="cc_register"), - path('cc/submit/', payviews.RegisterCard.as_view(), name="cc_register"), + path('cc/list/', payviews.ListCards.as_view(), name="cc_list"), + path('cc/delete/', payviews.DeleteCard.as_view(), name="cc_delete"), + path('', uncloudviews.UncloudIndex.as_view(), name="uncloudindex"), ] diff --git a/uncloud_pay/stripe.py b/uncloud_pay/stripe.py index f4c467a..a3dcb23 100644 --- a/uncloud_pay/stripe.py +++ b/uncloud_pay/stripe.py @@ -47,10 +47,6 @@ def handle_stripe_error(f): # XXX: maybe send email logging.error(str(e)) raise Exception(common_message) - except Exception as e: - # maybe send email - logging.error(str(e)) - raise Exception(common_message) return handle_problems @@ -103,3 +99,25 @@ def create_customer(name, email): @handle_stripe_error def get_customer(customer_id): return stripe.Customer.retrieve(customer_id) + +@handle_stripe_error +def get_customer_cards(customer_id): + print(f"getting cards for: {customer_id}") + + cards = [] + stripe_cards = stripe.PaymentMethod.list( + customer=customer_id, + type="card", + ) + + for stripe_card in stripe_cards["data"]: + card = {} + card['brand'] = stripe_card["card"]["brand"] + card['last4'] = stripe_card["card"]["last4"] + card['month'] = stripe_card["card"]["exp_month"] + card['year'] = stripe_card["card"]["exp_year"] + card['id'] = stripe_card["card"]["id"] + + cards.append(card) + + return cards diff --git a/uncloud_pay/tasks.py b/uncloud_pay/tasks.py new file mode 100644 index 0000000..b88f494 --- /dev/null +++ b/uncloud_pay/tasks.py @@ -0,0 +1,11 @@ +from celery import shared_task +from .models import * +import uuid + +from uncloud.models import UncloudTask + +@shared_task(bind=True) +def check_balance(self): + UncloudTask.objects.create(task_id=self.id) + print("for each user res is 50") + return 50 diff --git a/uncloud_pay/templates/error.html.j2 b/uncloud_pay/templates/error.html.j2 deleted file mode 100644 index ba9209c..0000000 --- a/uncloud_pay/templates/error.html.j2 +++ /dev/null @@ -1,18 +0,0 @@ - - - - Error - - - -
-

Error

-

{{ error }}

-
- - diff --git a/uncloud_pay/templates/stripe-payment.html.j2 b/uncloud_pay/templates/stripe-payment.html.j2 deleted file mode 100644 index 6c59740..0000000 --- a/uncloud_pay/templates/stripe-payment.html.j2 +++ /dev/null @@ -1,76 +0,0 @@ - - - - Stripe Card Registration - - - - - - - - -
-

Registering Stripe Credit Card

- - - -
-
- -
- - -
-
- - - - - - - - diff --git a/uncloud_pay/templates/bill.html.j2 b/uncloud_pay/templates/uncloud_pay/bill.html.j2 similarity index 100% rename from uncloud_pay/templates/bill.html.j2 rename to uncloud_pay/templates/uncloud_pay/bill.html.j2 diff --git a/uncloud_pay/templates/uncloud_pay/list_stripe.html b/uncloud_pay/templates/uncloud_pay/list_stripe.html new file mode 100644 index 0000000..b5cba17 --- /dev/null +++ b/uncloud_pay/templates/uncloud_pay/list_stripe.html @@ -0,0 +1,30 @@ +{% extends 'uncloud/base.html' %} + +{% block header %} + +{% endblock %} + +{% block body %} +
+

Your credit cards registered with Stripe

+ + + + +

List of stripe credit cards: +

    + {% for card in cards %} +
  • {{ card.brand }} ending in {{ card.last4 }} expiring + {{ card.year }}-{{ card.month }} + {% endfor %} + +
+

+
+ +{% endblock %} diff --git a/uncloud_pay/templates/uncloud_pay/register_stripe.html b/uncloud_pay/templates/uncloud_pay/register_stripe.html index 9fd82ae..82aca74 100644 --- a/uncloud_pay/templates/uncloud_pay/register_stripe.html +++ b/uncloud_pay/templates/uncloud_pay/register_stripe.html @@ -45,6 +45,9 @@ var clientSecret = '{{ client_secret }}'; cardButton.addEventListener('click', function(ev) { + document.getElementById("ungleichmessage").innerHTML + = "Registering card with Stripe, please wait ..." + stripe.confirmCardSetup( clientSecret, { diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index 99d176e..2f4ba8d 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -65,6 +65,36 @@ class RegisterCard(LoginRequiredMixin, TemplateView): context['stripe_pk'] = uncloud_stripe.public_api_key return context +class ListCards(LoginRequiredMixin, TemplateView): + login_url = '/login/' + + template_name = "uncloud_pay/list_stripe.html" + + def get_context_data(self, **kwargs): + customer_id = uncloud_stripe.get_customer_id_for(self.request.user) + cards = uncloud_stripe.get_customer_cards(customer_id) + + context = super().get_context_data(**kwargs) + context['cards'] = cards + context['username'] = self.request.user + + return context + +class DeleteCard(LoginRequiredMixin, TemplateView): + login_url = '/login/' + + template_name = "uncloud_pay/delete_stripe_card.html" + + def get_context_data(self, **kwargs): + customer_id = uncloud_stripe.get_customer_id_for(self.request.user) + cards = uncloud_stripe.get_customer_cards(customer_id) + + context = super().get_context_data(**kwargs) + context['cards'] = cards + context['username'] = self.request.user + + return context + class PaymentMethodViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] From 18d4c995717c64af75cdd6208bff153f2a00987c Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 26 Dec 2020 13:42:20 +0100 Subject: [PATCH 178/194] [doc] workers need access to the database --- doc/uncloud-manual-2020-08-01.org | 63 +++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/doc/uncloud-manual-2020-08-01.org b/doc/uncloud-manual-2020-08-01.org index 5c6a9f7..2fefca6 100644 --- a/doc/uncloud-manual-2020-08-01.org +++ b/doc/uncloud-manual-2020-08-01.org @@ -32,6 +32,8 @@ pip install -r requirements.txt The database can run on the same host as uncloud, but can also run a different server. Consult the usual postgresql documentation for a secure configuration. + + The database needs to be accessible from all worker nodes. **** Alpine #+BEGIN_SRC sh apk add postgresql-server @@ -60,6 +62,67 @@ postgres=# create database uncloud owner nico; python manage.py migrate #+END_SRC +*** Configuring remote access + - Get a letsencrypt certificate + - Expose SSL ports + - Create a user + + #+BEGIN_SRC sh + certbot certonly --standalone \ + -d -m your@email.come \ + --agree-tos --no-eff-email + #+END_SRC + + - Configuring postgresql.conf: + #+BEGIN_SRC sh +listen_addresses = '*' # what IP address(es) to listen on; +ssl = on +ssl_cert_file = '/etc/postgresql/server.crt' +ssl_key_file = '/etc/postgresql/server.key' + + #+END_SRC + + - Cannot load directly due to permission error: +2020-12-26 13:01:55.235 CET [27805] FATAL: could not load server +certificate file +"/etc/letsencrypt/live/2a0a-e5c0-0013-0000-9f4b-e619-efe5-a4ac.has-a.name/fullchain.pem": +Permission denied + + - hook + #+BEGIN_SRC sh +bridge:/etc/letsencrypt/renewal-hooks/deploy# cat /etc/letsencrypt/renewal-hooks/deploy/postgresql +#!/bin/sh + +umask 0177 +export DOMAIN=2a0a-e5c0-0013-0000-9f4b-e619-efe5-a4ac.has-a.name +export DATA_DIR=/etc/postgresql + +cp /etc/letsencrypt/live/$DOMAIN/fullchain.pem $DATA_DIR/server.crt +cp /etc/letsencrypt/live/$DOMAIN/privkey.pem $DATA_DIR/server.key +chown postgres:postgres $DATA_DIR/server.crt $DATA_DIR/server.key + #+END_SRC + + - Allowing access with md5 encrypted password encrypted via TLS + #+BEGIN_SRC sh +hostssl all all ::/0 md5 + #+END_SRC + + #+BEGIN_SRC sh + +postgres=# create role uncloud password '...'; +CREATE ROLE +postgres=# alter role uncloud login ; +ALTER ROLE + #+END_SRC + + Testing the connection: + + #+BEGIN_SRC sh +psql postgresql://uncloud@2a0a-e5c0-0013-0000-9f4b-e619-efe5-a4ac.has-a.name/uncloud?sslmode +=require + #+END_SRC + + ** Bootstrap - Login via a user so that the user object gets created - Run the following (replace nicocustomer with the username) From 93e5d39c7b4efe5480f58295868c390a3dd0de40 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 26 Dec 2020 14:42:53 +0100 Subject: [PATCH 179/194] moving vpn to direct configuration --- doc/uncloud-manual-2020-08-01.org | 22 ++++++++++++++++++++-- uncloud/models.py | 8 ++++++++ uncloud_net/models.py | 3 +++ uncloud_net/services.py | 8 ++++++-- uncloud_net/tasks.py | 28 ++++++++++++++++++++++++++-- uncloud_pay/tasks.py | 2 +- 6 files changed, 64 insertions(+), 7 deletions(-) diff --git a/doc/uncloud-manual-2020-08-01.org b/doc/uncloud-manual-2020-08-01.org index 2fefca6..21126bd 100644 --- a/doc/uncloud-manual-2020-08-01.org +++ b/doc/uncloud-manual-2020-08-01.org @@ -120,8 +120,7 @@ ALTER ROLE #+BEGIN_SRC sh psql postgresql://uncloud@2a0a-e5c0-0013-0000-9f4b-e619-efe5-a4ac.has-a.name/uncloud?sslmode =require - #+END_SRC - +g #+END_SRC ** Bootstrap - Login via a user so that the user object gets created @@ -145,6 +144,25 @@ psql postgresql://uncloud@2a0a-e5c0-0013-0000-9f4b-e619-efe5-a4ac.has-a.name/unc python manage.py import-vat-rates #+END_SRC +** Worker nodes + Nodes that realise services (VMHosts, VPNHosts, etc.) need to be + accessible from the main node and also need access to the database. + + Workers usually should have an "uncloud" user account, even though + strictly speaking the username can be any. + +*** WireGuardVPN Server + - Allow write access to /etc/wireguard for uncloud user + - Allow sudo access to "ip" and "wg" + + #+BEGIN_SRC sh + chown uncloud /etc/wireguard/ + [14:30] vpn-2a0ae5c1200:/etc/sudoers.d# cat uncloud + app ALL=(ALL) NOPASSWD:/sbin/ip + app ALL=(ALL) NOPASSWD:/usr/bin/wg + #+END_SRC + + * Testing / CLI Access Access via the commandline (CLI) can be done using curl or httpie. In our examples we will use httpie. diff --git a/uncloud/models.py b/uncloud/models.py index 5545303..535d920 100644 --- a/uncloud/models.py +++ b/uncloud/models.py @@ -170,3 +170,11 @@ class UncloudTask(models.Model): """ task_id = models.UUIDField(primary_key=True) + +# class UncloudRequestLog(models.Model): +# """ +# Class to store requests and logs +# """ + + +# log = models.CharField(max_length=256) diff --git a/uncloud_net/models.py b/uncloud_net/models.py index 0c8b02a..c768c17 100644 --- a/uncloud_net/models.py +++ b/uncloud_net/models.py @@ -48,6 +48,9 @@ class WireGuardVPNPool(models.Model): @property def ip_network(self): + """ + Return the IP network based on our address and mask + """ return ipaddress.ip_network(f"{self.network}/{self.network_mask}") def __str__(self): diff --git a/uncloud_net/services.py b/uncloud_net/services.py index 4f80c44..437601d 100644 --- a/uncloud_net/services.py +++ b/uncloud_net/services.py @@ -22,7 +22,6 @@ def create_wireguard_vpn(owner, public_key, network_mask): free_lease.delete() except WireGuardVPNFreeLeases.DoesNotExist: - # First object if count == 0: vpn = WireGuardVPN.objects.create(owner=owner, @@ -42,6 +41,11 @@ def create_wireguard_vpn(owner, public_key, network_mask): wireguard_public_key=public_key) + config = pool.wireguard_config + server = pool.vpn_server_hostname + wg_name = pool.wg_name + + configure_wireguard_server_on_host.apply_async((wg_name, config), + queue=server) - configure_wireguard_server(pool) return vpn diff --git a/uncloud_net/tasks.py b/uncloud_net/tasks.py index 78ae80c..f6b8038 100644 --- a/uncloud_net/tasks.py +++ b/uncloud_net/tasks.py @@ -8,6 +8,7 @@ import subprocess import logging import uuid + log = logging.getLogger(__name__) @shared_task @@ -15,7 +16,30 @@ def whereami(): print(os.uname()) return os.uname() -def configure_wireguard_server(wireguardvpnpool): +@shared_task +def configure_wireguard_server_on_host(wg_name, config): + """ + - Create wireguard config (DB query -> string) + - Submit config to cdist worker + - Change config locally on worker / commit / shared + """ + + # Write config + fname = f"/etc/wireguard/{{wg_name}}.conf" + with open(fname, "w") as fd: + fd.write(config) + + # Ensure the device exists + subprocess.run(f"ip link show {{wg_name}} || sudo ip link add {{wg_name}} type wireguard", + shell=True, check=True) + + # Ensure the config is correct + subprocess.run(f"sudo wg setconf {{wg_name}} {{fname}}", + shell=True, check=True) + + + +def configure_wireguard_server_via_cdist(wireguardvpnpool): """ - Create wireguard config (DB query -> string) - Submit config to cdist worker @@ -37,7 +61,7 @@ def cdist_configure_wireguard_server(config, server): """ Create config and configure server. - To be executed on the cdist workers. + To be executed on the cdist worker. """ dirname= "/home/app/.cdist/type/__ungleich_wireguard/files/" diff --git a/uncloud_pay/tasks.py b/uncloud_pay/tasks.py index b88f494..c372366 100644 --- a/uncloud_pay/tasks.py +++ b/uncloud_pay/tasks.py @@ -6,6 +6,6 @@ from uncloud.models import UncloudTask @shared_task(bind=True) def check_balance(self): - UncloudTask.objects.create(task_id=self.id) + UncloudTask.objects.create(task_id=self.request.id) print("for each user res is 50") return 50 From 74749bf07cdf6afed02fc45d0f6616d6ba939add Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 26 Dec 2020 14:45:28 +0100 Subject: [PATCH 180/194] fix templating --- uncloud_net/tasks.py | 11 +++-------- uncloud_net/views.py | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/uncloud_net/tasks.py b/uncloud_net/tasks.py index f6b8038..67cfa18 100644 --- a/uncloud_net/tasks.py +++ b/uncloud_net/tasks.py @@ -11,11 +11,6 @@ import uuid log = logging.getLogger(__name__) -@shared_task -def whereami(): - print(os.uname()) - return os.uname() - @shared_task def configure_wireguard_server_on_host(wg_name, config): """ @@ -25,16 +20,16 @@ def configure_wireguard_server_on_host(wg_name, config): """ # Write config - fname = f"/etc/wireguard/{{wg_name}}.conf" + fname = f"/etc/wireguard/{wg_name}.conf" with open(fname, "w") as fd: fd.write(config) # Ensure the device exists - subprocess.run(f"ip link show {{wg_name}} || sudo ip link add {{wg_name}} type wireguard", + subprocess.run(f"ip link show {wg_name} || sudo ip link add {{wg_name}} type wireguard", shell=True, check=True) # Ensure the config is correct - subprocess.run(f"sudo wg setconf {{wg_name}} {{fname}}", + subprocess.run(f"sudo wg setconf {wg_name} {fname}", shell=True, check=True) diff --git a/uncloud_net/views.py b/uncloud_net/views.py index 77ba952..7dadbf4 100644 --- a/uncloud_net/views.py +++ b/uncloud_net/views.py @@ -35,7 +35,7 @@ class WireGuardVPNViewSet(viewsets.ModelViewSet): public_key=serializer.validated_data['wireguard_public_key'], network_mask=serializer.validated_data['network_mask'] ) - configure_wireguard_server(vpn.vpnpool) + return Response(WireGuardVPNSerializer(vpn).data) From e2c4a19049fc1e8c97ef829db43888e02155916c Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 26 Dec 2020 14:48:10 +0100 Subject: [PATCH 181/194] Less verbose --- uncloud_net/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uncloud_net/tasks.py b/uncloud_net/tasks.py index 67cfa18..7d94f3b 100644 --- a/uncloud_net/tasks.py +++ b/uncloud_net/tasks.py @@ -25,7 +25,7 @@ def configure_wireguard_server_on_host(wg_name, config): fd.write(config) # Ensure the device exists - subprocess.run(f"ip link show {wg_name} || sudo ip link add {{wg_name}} type wireguard", + subprocess.run(f"ip link show {wg_name} >/dev/null || sudo ip link add {{wg_name}} type wireguard", shell=True, check=True) # Ensure the config is correct From e225bf1cc095c913d08f3e46aeb6871bd3f0675c Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Mon, 28 Dec 2020 23:35:34 +0100 Subject: [PATCH 182/194] implement credit card listing --- doc/uncloud-manual-2020-08-01.org | 4 ++ uncloud/templates/uncloud/index.html | 9 ++- uncloud/urls.py | 6 +- uncloud_net/models.py | 15 +++- uncloud_net/services.py | 15 ++++ uncloud_pay/admin.py | 2 +- uncloud_pay/migrations/0001_initial.py | 23 +++++- uncloud_pay/models.py | 53 +++++++++++--- uncloud_pay/serializers.py | 18 ++--- uncloud_pay/stripe.py | 27 +++++-- .../uncloud_pay/register_stripe.html | 3 +- uncloud_pay/views.py | 71 ++++++++++--------- 12 files changed, 183 insertions(+), 63 deletions(-) diff --git a/doc/uncloud-manual-2020-08-01.org b/doc/uncloud-manual-2020-08-01.org index 21126bd..381bb62 100644 --- a/doc/uncloud-manual-2020-08-01.org +++ b/doc/uncloud-manual-2020-08-01.org @@ -373,6 +373,10 @@ Q vpn-2a0ae5c1200.ungleich.ch *** 1.1 (cleanup 1) **** TODO [#C] Unify ValidationError, FieldError - define proper Exception - What do we use for model errors +**** TODO [#C] Cleanup the results handling in celery + - Remove the results broker? + - Setup app to ignore results? + - Actually use results? *** 1.0 (initial release) **** TODO [#C] Initial Generic product support - Product diff --git a/uncloud/templates/uncloud/index.html b/uncloud/templates/uncloud/index.html index b40c3b4..e5a8318 100644 --- a/uncloud/templates/uncloud/index.html +++ b/uncloud/templates/uncloud/index.html @@ -7,8 +7,13 @@ Welcome to uncloud, checkout the following locations: diff --git a/uncloud/urls.py b/uncloud/urls.py index 343a83c..f163136 100644 --- a/uncloud/urls.py +++ b/uncloud/urls.py @@ -72,10 +72,12 @@ router.register(r'v1/user/register', authviews.AccountManagementViewSet, basenam router.register(r'v2/net/wireguardvpn', netviews.WireGuardVPNViewSet, basename='wireguardvpnnetwork') router.register(r'v2/net/wireguardvpnsizes', netviews.WireGuardVPNSizes, basename='wireguardvpnnetworksizes') +# Payment related +router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename='credit-card') urlpatterns = [ - path(r'api/', include(router.urls)), + path(r'api/', include(router.urls), name='api'), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), # for login to REST API path('openapi', get_schema_view( @@ -92,8 +94,6 @@ urlpatterns = [ path('admin/', admin.site.urls), path('cc/reg/', payviews.RegisterCard.as_view(), name="cc_register"), - path('cc/list/', payviews.ListCards.as_view(), name="cc_list"), - path('cc/delete/', payviews.DeleteCard.as_view(), name="cc_delete"), path('', uncloudviews.UncloudIndex.as_view(), name="uncloudindex"), ] diff --git a/uncloud_net/models.py b/uncloud_net/models.py index c768c17..9865a08 100644 --- a/uncloud_net/models.py +++ b/uncloud_net/models.py @@ -6,7 +6,7 @@ from django.contrib.auth import get_user_model from django.core.validators import MinValueValidator, MaxValueValidator from django.core.exceptions import FieldError, ValidationError -from uncloud_pay.models import Order +from uncloud_pay.models import Order, Product class WireGuardVPNPool(models.Model): """ @@ -123,6 +123,19 @@ class WireGuardVPN(models.Model): def __str__(self): return f"{self.address} ({self.pool_index})" + def create_product(self): + """ + Ensure we have a product for the WireguardVPN + """ + + pass + + # Product.objects.get_or_create( + # name="WireGuardVPN", + # description="Wireguard VPN", + # currency=Currency.CHF, + # config= + class WireGuardVPNFreeLeases(models.Model): """ diff --git a/uncloud_net/services.py b/uncloud_net/services.py index 437601d..9149f01 100644 --- a/uncloud_net/services.py +++ b/uncloud_net/services.py @@ -4,9 +4,24 @@ from .models import * from .selectors import * from .tasks import * + @transaction.atomic def create_wireguard_vpn(owner, public_key, network_mask): + # Check if the user has a membership. + #------------------------------------ + # If yes, user is eligible for API access and 2 VPNs + # If user already has 2 VPNs, we deduct from the credit + # If deduction is higher than the allowed credit, we fail + + # + # Check if the user has suitable balance + # Create order + # + return create_wireguard_vpn_tech(owner, public_key, network_mask) + +@transaction.atomic +def create_wireguard_vpn_tech(owner, public_key, network_mask): pool = get_suitable_pools(network_mask)[0] count = pool.wireguardvpn_set.count() diff --git a/uncloud_pay/admin.py b/uncloud_pay/admin.py index 2c72274..eb82fb7 100644 --- a/uncloud_pay/admin.py +++ b/uncloud_pay/admin.py @@ -88,5 +88,5 @@ admin.site.register(Bill, BillAdmin) admin.site.register(ProductToRecurringPeriod) admin.site.register(Product, ProductAdmin) -for m in [ Order, BillRecord, BillingAddress, RecurringPeriod, VATRate, StripeCustomer ]: +for m in [ Order, BillRecord, BillingAddress, RecurringPeriod, VATRate, StripeCustomer, StripeCreditCard ]: admin.site.register(m) diff --git a/uncloud_pay/migrations/0001_initial.py b/uncloud_pay/migrations/0001_initial.py index b1b68c5..e65f3dd 100644 --- a/uncloud_pay/migrations/0001_initial.py +++ b/uncloud_pay/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1 on 2020-12-13 10:38 +# Generated by Django 3.1 on 2020-12-28 22:19 from django.conf import settings import django.core.validators @@ -83,6 +83,18 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True, default='')), ], ), + migrations.CreateModel( + name='StripeCreditCard', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('card_name', models.CharField(default='My credit card', max_length=128)), + ('card_id', models.CharField(max_length=32)), + ('last4', models.CharField(max_length=4)), + ('brand', models.CharField(max_length=64)), + ('expiry_date', models.DateField()), + ('owner', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), migrations.CreateModel( name='ProductToRecurringPeriod', fields=[ @@ -140,6 +152,15 @@ class Migration(migrations.Migration): ('replaces', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replaced_by', to='uncloud_pay.order')), ], ), + migrations.CreateModel( + name='Membership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('starting_date', models.DateField(blank=True, null=True)), + ('ending_date', models.DateField(blank=True, null=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), migrations.CreateModel( name='BillRecord', fields=[ diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 18e6f85..abf769c 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -17,7 +17,6 @@ from django.utils import timezone from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.conf import settings -import uncloud_pay.stripe from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS from uncloud.models import UncloudAddress @@ -92,6 +91,21 @@ class StripeCustomer(models.Model): def __str__(self): return self.owner.username + +class StripeCreditCard(models.Model): + owner = models.OneToOneField( get_user_model(), + on_delete=models.CASCADE) + + card_name = models.CharField(null=False, max_length=128, default="My credit card") + card_id = models.CharField(null=False, max_length=32) + last4 = models.CharField(null=False, max_length=4) + brand = models.CharField(null=False, max_length=64) + expiry_date = models.DateField(null=False) + + def __str__(self): + return f"{self.card_name}: {self.brand} {self.last4} ({self.expiry_date})" + + ### # Payments and Payment Methods. @@ -148,14 +162,14 @@ class PaymentMethod(models.Model): stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True) stripe_setup_intent_id = models.CharField(max_length=32, blank=True, null=True) - @property - def stripe_card_last4(self): - if self.source == 'stripe' and self.active: - payment_method = uncloud_pay.stripe.get_payment_method( - self.stripe_payment_method_id) - return payment_method.card.last4 - else: - return None + # @property + # def stripe_card_last4(self): + # if self.source == 'stripe' and self.active: + # payment_method = uncloud_pay.stripe.get_payment_method( + # self.stripe_payment_method_id) + # return payment_method.card.last4 + # else: + # return None @property def active(self): @@ -1261,3 +1275,24 @@ class ProductToRecurringPeriod(models.Model): def __str__(self): return f"{self.product} - {self.recurring_period} (default: {self.is_default})" + + +class Membership(models.Model): + owner = models.ForeignKey(get_user_model(), + on_delete=models.CASCADE) + + starting_date = models.DateField(blank=True, null=True) + ending_date = models.DateField(blank=True, null=True) + + + @classmethod + def user_has_membership(user, when): + """ + Return true if user has membership at a point of time, + return false if that is not the case + """ + + pass + + # cls.objects.filter(owner=user, + # starting_date) diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py index 94f833e..84a23fd 100644 --- a/uncloud_pay/serializers.py +++ b/uncloud_pay/serializers.py @@ -6,13 +6,20 @@ from django.utils.translation import gettext_lazy as _ from .models import * ### -# Checked code +# 2020-12 Checked code + + +class StripeCreditCardSerializer(serializers.ModelSerializer): + class Meta: + model = StripeCreditCard + exclude = ['card_id', "owner" ] + read_only_fields = [ "last4", "brand", "expiry_date" ] + ################################################################################ # Unchecked code - ### # Payments and Payment Methods. @@ -21,13 +28,6 @@ class PaymentSerializer(serializers.ModelSerializer): model = Payment fields = '__all__' -class PaymentMethodSerializer(serializers.ModelSerializer): - stripe_card_last4 = serializers.IntegerField() - - class Meta: - model = PaymentMethod - fields = [ 'source', 'description', 'primary', 'stripe_card_last4', 'active'] - class UpdatePaymentMethodSerializer(serializers.ModelSerializer): class Meta: model = PaymentMethod diff --git a/uncloud_pay/stripe.py b/uncloud_pay/stripe.py index a3dcb23..5b3bb00 100644 --- a/uncloud_pay/stripe.py +++ b/uncloud_pay/stripe.py @@ -1,11 +1,13 @@ import stripe import stripe.error import logging +import datetime from django.core.exceptions import ObjectDoesNotExist from django.conf import settings +from django.contrib.auth import get_user_model -import uncloud_pay.models +from .models import StripeCustomer, StripeCreditCard CURRENCY = 'chf' @@ -56,12 +58,12 @@ def public_api_key(): def get_customer_id_for(user): try: # .get() raise if there is no matching entry. - return uncloud_pay.models.StripeCustomer.objects.get(owner=user).stripe_id + return StripeCustomer.objects.get(owner=user).stripe_id except ObjectDoesNotExist: # No entry yet - making a new one. try: customer = create_customer(user.username, user.email) - uncloud_stripe_mapping = uncloud_pay.models.StripeCustomer.objects.create( + uncloud_stripe_mapping = StripeCustomer.objects.create( owner=user, stripe_id=customer.id) return uncloud_stripe_mapping.stripe_id except Exception as e: @@ -109,6 +111,7 @@ def get_customer_cards(customer_id): customer=customer_id, type="card", ) + print(stripe_cards["data"]) for stripe_card in stripe_cards["data"]: card = {} @@ -116,8 +119,24 @@ def get_customer_cards(customer_id): card['last4'] = stripe_card["card"]["last4"] card['month'] = stripe_card["card"]["exp_month"] card['year'] = stripe_card["card"]["exp_year"] - card['id'] = stripe_card["card"]["id"] + card['id'] = stripe_card["id"] cards.append(card) return cards + +def sync_cards_for_user(user): + customer_id = get_customer_id_for(user) + cards = get_customer_cards(customer_id) + + for card in cards: + StripeCreditCard.objects.get_or_create(card_id=card['id'], + owner = user, + defaults = { + 'last4': card['last4'], + 'brand': card['brand'], + 'expiry_date': datetime.date(card['year'], + card['month'], + 1) + } + ) diff --git a/uncloud_pay/templates/uncloud_pay/register_stripe.html b/uncloud_pay/templates/uncloud_pay/register_stripe.html index 82aca74..76265fa 100644 --- a/uncloud_pay/templates/uncloud_pay/register_stripe.html +++ b/uncloud_pay/templates/uncloud_pay/register_stripe.html @@ -63,7 +63,8 @@ } else { // Return to API on success. document.getElementById("ungleichmessage").innerHTML - = "Registered credit card with Stripe." + = "Registered credit card with + Stripe. Return to the main page." } }); }); diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index 2f4ba8d..246e922 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -31,22 +31,7 @@ import uncloud_pay.stripe as uncloud_stripe logger = logging.getLogger(__name__) ### -# Payments and Payment Methods. - -class PaymentViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = PaymentSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - return Payment.objects.filter(owner=self.request.user) - -class OrderViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = OrderSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - return Order.objects.filter(owner=self.request.user) - +# 2020-12 checked code class RegisterCard(LoginRequiredMixin, TemplateView): login_url = '/login/' @@ -65,6 +50,44 @@ class RegisterCard(LoginRequiredMixin, TemplateView): context['stripe_pk'] = uncloud_stripe.public_api_key return context + +class CreditCardViewSet(mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + + serializer_class = StripeCreditCardSerializer + permission_classes = [permissions.IsAuthenticated] + + def list(self, request): + uncloud_stripe.sync_cards_for_user(self.request.user) + return super().list(request) + + def get_queryset(self): + return StripeCreditCard.objects.filter(owner=self.request.user) + + + +### +# Payments and Payment Methods. + +class PaymentViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = PaymentSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Payment.objects.filter(owner=self.request.user) + +class OrderViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = OrderSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Order.objects.filter(owner=self.request.user) + + + class ListCards(LoginRequiredMixin, TemplateView): login_url = '/login/' @@ -80,22 +103,6 @@ class ListCards(LoginRequiredMixin, TemplateView): return context -class DeleteCard(LoginRequiredMixin, TemplateView): - login_url = '/login/' - - template_name = "uncloud_pay/delete_stripe_card.html" - - def get_context_data(self, **kwargs): - customer_id = uncloud_stripe.get_customer_id_for(self.request.user) - cards = uncloud_stripe.get_customer_cards(customer_id) - - context = super().get_context_data(**kwargs) - context['cards'] = cards - context['username'] = self.request.user - - return context - - class PaymentMethodViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] From 1b06d8ee0353ca1b4ef97afa930f38a553b3d05c Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Tue, 29 Dec 2020 01:43:33 +0100 Subject: [PATCH 183/194] [credit card] implement payment --- uncloud/urls.py | 12 ++-- uncloud_pay/admin.py | 15 +++-- .../migrations/0002_auto_20201228_2244.py | 24 +++++++ .../migrations/0003_auto_20201228_2256.py | 28 +++++++++ .../0004_stripecreditcard_active.py | 18 ++++++ .../migrations/0005_auto_20201228_2335.py | 18 ++++++ .../migrations/0006_auto_20201228_2337.py | 21 +++++++ .../migrations/0007_auto_20201228_2338.py | 17 +++++ .../0008_payment_external_reference.py | 18 ++++++ .../migrations/0009_auto_20201229_0037.py | 18 ++++++ .../migrations/0010_auto_20201229_0042.py | 19 ++++++ uncloud_pay/models.py | 45 +++++++------ uncloud_pay/serializers.py | 24 +++++-- uncloud_pay/stripe.py | 63 ++++++++++++++----- .../uncloud_pay/register_stripe.html | 3 +- uncloud_pay/views.py | 11 ++-- 16 files changed, 290 insertions(+), 64 deletions(-) create mode 100644 uncloud_pay/migrations/0002_auto_20201228_2244.py create mode 100644 uncloud_pay/migrations/0003_auto_20201228_2256.py create mode 100644 uncloud_pay/migrations/0004_stripecreditcard_active.py create mode 100644 uncloud_pay/migrations/0005_auto_20201228_2335.py create mode 100644 uncloud_pay/migrations/0006_auto_20201228_2337.py create mode 100644 uncloud_pay/migrations/0007_auto_20201228_2338.py create mode 100644 uncloud_pay/migrations/0008_payment_external_reference.py create mode 100644 uncloud_pay/migrations/0009_auto_20201229_0037.py create mode 100644 uncloud_pay/migrations/0010_auto_20201229_0042.py diff --git a/uncloud/urls.py b/uncloud/urls.py index f163136..3ee5988 100644 --- a/uncloud/urls.py +++ b/uncloud/urls.py @@ -47,12 +47,12 @@ router.register(r'v1/service/generic', serviceviews.GenericServiceProductViewSet router.register(r'v1/my/address', payviews.BillingAddressViewSet, basename='billingaddress') router.register(r'v1/my/bill', payviews.BillViewSet, basename='bill') router.register(r'v1/my/order', payviews.OrderViewSet, basename='order') -router.register(r'v1/my/payment', payviews.PaymentViewSet, basename='payment') +#router.register(r'v1/my/payment', payviews.PaymentViewSet, basename='payment') router.register(r'v1/my/payment-method', payviews.PaymentMethodViewSet, basename='payment-method') # admin/staff urls router.register(r'v1/admin/bill', payviews.AdminBillViewSet, basename='admin/bill') -router.register(r'v1/admin/payment', payviews.AdminPaymentViewSet, basename='admin/payment') +#router.register(r'v1/admin/payment', payviews.AdminPaymentViewSet, basename='admin/payment') router.register(r'v1/admin/order', payviews.AdminOrderViewSet, basename='admin/order') router.register(r'v1/admin/vmhost', vmviews.VMHostViewSet) router.register(r'v1/admin/vmcluster', vmviews.VMClusterViewSet) @@ -74,6 +74,7 @@ router.register(r'v2/net/wireguardvpnsizes', netviews.WireGuardVPNSizes, basenam # Payment related router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename='credit-card') +router.register(r'v2/payment/payment', payviews.PaymentViewSet, basename='payment') urlpatterns = [ @@ -83,16 +84,13 @@ urlpatterns = [ path('openapi', get_schema_view( title="uncloud", description="uncloud API", - version="1.0.0" + version="2.0.0" ), name='openapi-schema'), - # web/ = stuff to view in the browser -# path('web/vpn/create/', netviews.WireGuardVPNCreateView.as_view(), name="vpncreate"), + path('admin/', admin.site.urls), path('login/', authviews.LoginView.as_view(), name="login"), path('logout/', authviews.LogoutView.as_view(), name="logout"), - path('admin/', admin.site.urls), - path('cc/reg/', payviews.RegisterCard.as_view(), name="cc_register"), path('', uncloudviews.UncloudIndex.as_view(), name="uncloudindex"), diff --git a/uncloud_pay/admin.py b/uncloud_pay/admin.py index eb82fb7..d8b09da 100644 --- a/uncloud_pay/admin.py +++ b/uncloud_pay/admin.py @@ -10,10 +10,8 @@ from django.core.files.temp import NamedTemporaryFile from django.http import FileResponse from django.template.loader import render_to_string - from uncloud_pay.models import * - class BillRecordInline(admin.TabularInline): model = BillRecord @@ -85,8 +83,17 @@ class BillAdmin(admin.ModelAdmin): admin.site.register(Bill, BillAdmin) -admin.site.register(ProductToRecurringPeriod) admin.site.register(Product, ProductAdmin) -for m in [ Order, BillRecord, BillingAddress, RecurringPeriod, VATRate, StripeCustomer, StripeCreditCard ]: +for m in [ + BillRecord, + BillingAddress, + Order, + Payment, + ProductToRecurringPeriod, + RecurringPeriod, + StripeCreditCard, + StripeCustomer, + VATRate, +]: admin.site.register(m) diff --git a/uncloud_pay/migrations/0002_auto_20201228_2244.py b/uncloud_pay/migrations/0002_auto_20201228_2244.py new file mode 100644 index 0000000..4665553 --- /dev/null +++ b/uncloud_pay/migrations/0002_auto_20201228_2244.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1 on 2020-12-28 22:44 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='payment', + name='currency', + field=models.CharField(choices=[('CHF', 'Swiss Franc'), ('EUR', 'Euro'), ('USD', 'US Dollar')], default='CHF', max_length=32), + ), + migrations.AlterField( + model_name='payment', + name='amount', + field=models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/uncloud_pay/migrations/0003_auto_20201228_2256.py b/uncloud_pay/migrations/0003_auto_20201228_2256.py new file mode 100644 index 0000000..b516bd5 --- /dev/null +++ b/uncloud_pay/migrations/0003_auto_20201228_2256.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1 on 2020-12-28 22:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0002_auto_20201228_2244'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='currency', + field=models.CharField(choices=[('CHF', 'Swiss Franc')], default='CHF', max_length=32), + ), + migrations.AlterField( + model_name='payment', + name='currency', + field=models.CharField(choices=[('CHF', 'Swiss Franc')], default='CHF', max_length=32), + ), + migrations.AlterField( + model_name='product', + name='currency', + field=models.CharField(choices=[('CHF', 'Swiss Franc')], default='CHF', max_length=32), + ), + ] diff --git a/uncloud_pay/migrations/0004_stripecreditcard_active.py b/uncloud_pay/migrations/0004_stripecreditcard_active.py new file mode 100644 index 0000000..3fb8015 --- /dev/null +++ b/uncloud_pay/migrations/0004_stripecreditcard_active.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-12-28 23:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0003_auto_20201228_2256'), + ] + + operations = [ + migrations.AddField( + model_name='stripecreditcard', + name='active', + field=models.BooleanField(default=True), + ), + ] diff --git a/uncloud_pay/migrations/0005_auto_20201228_2335.py b/uncloud_pay/migrations/0005_auto_20201228_2335.py new file mode 100644 index 0000000..814752e --- /dev/null +++ b/uncloud_pay/migrations/0005_auto_20201228_2335.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-12-28 23:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0004_stripecreditcard_active'), + ] + + operations = [ + migrations.AlterField( + model_name='stripecreditcard', + name='active', + field=models.BooleanField(default=False), + ), + ] diff --git a/uncloud_pay/migrations/0006_auto_20201228_2337.py b/uncloud_pay/migrations/0006_auto_20201228_2337.py new file mode 100644 index 0000000..a164767 --- /dev/null +++ b/uncloud_pay/migrations/0006_auto_20201228_2337.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1 on 2020-12-28 23:37 + +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_pay', '0005_auto_20201228_2335'), + ] + + operations = [ + migrations.AlterField( + model_name='stripecreditcard', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/uncloud_pay/migrations/0007_auto_20201228_2338.py b/uncloud_pay/migrations/0007_auto_20201228_2338.py new file mode 100644 index 0000000..315a74b --- /dev/null +++ b/uncloud_pay/migrations/0007_auto_20201228_2338.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2020-12-28 23:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0006_auto_20201228_2337'), + ] + + operations = [ + migrations.AddConstraint( + model_name='stripecreditcard', + constraint=models.UniqueConstraint(condition=models.Q(active=True), fields=('owner',), name='one_active_card_per_user'), + ), + ] diff --git a/uncloud_pay/migrations/0008_payment_external_reference.py b/uncloud_pay/migrations/0008_payment_external_reference.py new file mode 100644 index 0000000..0de20b6 --- /dev/null +++ b/uncloud_pay/migrations/0008_payment_external_reference.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-12-29 00:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0007_auto_20201228_2338'), + ] + + operations = [ + migrations.AddField( + model_name='payment', + name='external_reference', + field=models.CharField(default='', max_length=256), + ), + ] diff --git a/uncloud_pay/migrations/0009_auto_20201229_0037.py b/uncloud_pay/migrations/0009_auto_20201229_0037.py new file mode 100644 index 0000000..fc195e4 --- /dev/null +++ b/uncloud_pay/migrations/0009_auto_20201229_0037.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-12-29 00:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0008_payment_external_reference'), + ] + + operations = [ + migrations.AlterField( + model_name='payment', + name='external_reference', + field=models.CharField(blank=True, default='', max_length=256, null=True), + ), + ] diff --git a/uncloud_pay/migrations/0010_auto_20201229_0042.py b/uncloud_pay/migrations/0010_auto_20201229_0042.py new file mode 100644 index 0000000..6dd6a60 --- /dev/null +++ b/uncloud_pay/migrations/0010_auto_20201229_0042.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2020-12-29 00:42 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0009_auto_20201229_0037'), + ] + + operations = [ + migrations.AlterField( + model_name='payment', + name='timestamp', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index abf769c..0c880c3 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -64,8 +64,8 @@ class Currency(models.TextChoices): Possible currencies to be billed """ CHF = 'CHF', _('Swiss Franc') - EUR = 'EUR', _('Euro') - USD = 'USD', _('US Dollar') +# EUR = 'EUR', _('Euro') +# USD = 'USD', _('US Dollar') def get_balance_for_user(user): @@ -93,28 +93,30 @@ class StripeCustomer(models.Model): class StripeCreditCard(models.Model): - owner = models.OneToOneField( get_user_model(), - on_delete=models.CASCADE) + owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) card_name = models.CharField(null=False, max_length=128, default="My credit card") card_id = models.CharField(null=False, max_length=32) last4 = models.CharField(null=False, max_length=4) brand = models.CharField(null=False, max_length=64) expiry_date = models.DateField(null=False) + active = models.BooleanField(default=False) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['owner'], + condition=Q(active=True), + name='one_active_card_per_user') + ] + def __str__(self): return f"{self.card_name}: {self.brand} {self.last4} ({self.expiry_date})" - -### -# Payments and Payment Methods. - class Payment(models.Model): - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE) + owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) amount = models.DecimalField( - default=0.0, max_digits=AMOUNT_MAX_DIGITS, decimal_places=AMOUNT_DECIMALS, validators=[MinValueValidator(0)]) @@ -128,21 +130,18 @@ class Payment(models.Model): ('unknown', 'Unknown') ), default='unknown') - timestamp = models.DateTimeField(editable=False, auto_now_add=True) - # We override save() in order to active products awaiting payment. - def save(self, *args, **kwargs): - # _state.adding is switched to false after super(...) call. - being_created = self._state.adding + timestamp = models.DateTimeField(default=timezone.now) - unpaid_bills_before_payment = Bill.get_unpaid_for(self.owner) - super(Payment, self).save(*args, **kwargs) # Save payment in DB. - unpaid_bills_after_payment = Bill.get_unpaid_for(self.owner) + currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF) - newly_paid_bills = list( - set(unpaid_bills_before_payment) - set(unpaid_bills_after_payment)) - for bill in newly_paid_bills: - bill.activate_products() + external_reference = models.CharField(max_length=256, default="", null=True, blank=True) + + def __str__(self): + return f"{self.amount}{self.currency} from {self.owner} via {self.source} on {self.timestamp}" + +### +# Payments and Payment Methods. class PaymentMethod(models.Model): diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py index 84a23fd..14cac0b 100644 --- a/uncloud_pay/serializers.py +++ b/uncloud_pay/serializers.py @@ -4,17 +4,33 @@ from uncloud_auth.serializers import UserSerializer from django.utils.translation import gettext_lazy as _ from .models import * +import uncloud_pay.stripe as uncloud_stripe ### # 2020-12 Checked code - class StripeCreditCardSerializer(serializers.ModelSerializer): class Meta: model = StripeCreditCard - exclude = ['card_id', "owner" ] + exclude = [ "card_id", "owner" ] read_only_fields = [ "last4", "brand", "expiry_date" ] +class PaymentSerializer(serializers.ModelSerializer): + owner = serializers.HiddenField(default=serializers.CurrentUserDefault()) + + class Meta: + model = Payment + fields = '__all__' + read_only_fields = [ "external_reference", "source", "timestamp" ] + + def validate(self, data): + payment_intent = uncloud_stripe.charge_customer(data['owner'], + data['amount']) + + data["external_reference"] = payment_intent["id"] + data["source"] = "stripe" + + return data ################################################################################ @@ -23,10 +39,6 @@ class StripeCreditCardSerializer(serializers.ModelSerializer): ### # Payments and Payment Methods. -class PaymentSerializer(serializers.ModelSerializer): - class Meta: - model = Payment - fields = '__all__' class UpdatePaymentMethodSerializer(serializers.ModelSerializer): class Meta: diff --git a/uncloud_pay/stripe.py b/uncloud_pay/stripe.py index 5b3bb00..ed95c82 100644 --- a/uncloud_pay/stripe.py +++ b/uncloud_pay/stripe.py @@ -3,7 +3,7 @@ import stripe.error import logging import datetime -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.conf import settings from django.contrib.auth import get_user_model @@ -80,20 +80,6 @@ def get_setup_intent(setup_intent_id): def get_payment_method(payment_method_id): return stripe.PaymentMethod.retrieve(payment_method_id) -@handle_stripe_error -def charge_customer(amount, customer_id, card_id): - # Amount is in CHF but stripes requires smallest possible unit. - # https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount - adjusted_amount = int(amount * 100) - return stripe.PaymentIntent.create( - amount=adjusted_amount, - currency=CURRENCY, - customer=customer_id, - payment_method=card_id, - off_session=True, - confirm=True, - ) - @handle_stripe_error def create_customer(name, email): return stripe.Customer.create(name=name, email=email) @@ -111,7 +97,6 @@ def get_customer_cards(customer_id): customer=customer_id, type="card", ) - print(stripe_cards["data"]) for stripe_card in stripe_cards["data"]: card = {} @@ -129,7 +114,21 @@ def sync_cards_for_user(user): customer_id = get_customer_id_for(user) cards = get_customer_cards(customer_id) + active_cards = StripeCreditCard.objects.filter(owner=user, + active=True) + + if len(active_cards) > 0: + has_active_card = True + else: + has_active_card = False + for card in cards: + active = False + + if not has_active_card: + active = True + has_active_card = True + StripeCreditCard.objects.get_or_create(card_id=card['id'], owner = user, defaults = { @@ -137,6 +136,36 @@ def sync_cards_for_user(user): 'brand': card['brand'], 'expiry_date': datetime.date(card['year'], card['month'], - 1) + 1), + 'active': active } ) + +@handle_stripe_error +def charge_customer(user, amount, currency='CHF'): + # Amount is in CHF but stripes requires smallest possible unit. + # https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount + # FIXME: might need to be adjusted for other currencies + + if currency == 'CHF': + adjusted_amount = int(amount * 100) + else: + return Exception("Programming error: unsupported currency") + + try: + card = StripeCreditCard.objects.get(owner=user, + active=True) + + except StripeCreditCard.DoesNotExist: + raise ValidationError("No active credit card - cannot create payment") + + customer_id = get_customer_id_for(user) + + return stripe.PaymentIntent.create( + amount=adjusted_amount, + currency=currency, + customer=customer_id, + payment_method=card.card_id, + off_session=True, + confirm=True, + ) diff --git a/uncloud_pay/templates/uncloud_pay/register_stripe.html b/uncloud_pay/templates/uncloud_pay/register_stripe.html index 76265fa..82aca74 100644 --- a/uncloud_pay/templates/uncloud_pay/register_stripe.html +++ b/uncloud_pay/templates/uncloud_pay/register_stripe.html @@ -63,8 +63,7 @@ } else { // Return to API on success. document.getElementById("ungleichmessage").innerHTML - = "Registered credit card with - Stripe. Return to the main page." + = "Registered credit card with Stripe." } }); }); diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index 246e922..48c24a8 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -68,17 +68,18 @@ class CreditCardViewSet(mixins.RetrieveModelMixin, return StripeCreditCard.objects.filter(owner=self.request.user) - -### -# Payments and Payment Methods. - -class PaymentViewSet(viewsets.ReadOnlyModelViewSet): +class PaymentViewSet(viewsets.ModelViewSet): serializer_class = PaymentSerializer permission_classes = [permissions.IsAuthenticated] def get_queryset(self): return Payment.objects.filter(owner=self.request.user) + +### +# Payments and Payment Methods. + + class OrderViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = OrderSerializer permission_classes = [permissions.IsAuthenticated] From 6c15d2086e00455447730a07e6d749b9da704a04 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Fri, 1 Jan 2021 12:41:54 +0100 Subject: [PATCH 184/194] implement balance getting --- uncloud/urls.py | 1 + uncloud_pay/models.py | 10 ------- uncloud_pay/selectors.py | 23 ++++++++++++++ uncloud_pay/serializers.py | 23 +++----------- .../templates/uncloud_pay/list_stripe.html | 30 ------------------- .../uncloud_pay/register_stripe.html | 18 +++++++---- uncloud_pay/views.py | 13 ++++++++ 7 files changed, 53 insertions(+), 65 deletions(-) create mode 100644 uncloud_pay/selectors.py delete mode 100644 uncloud_pay/templates/uncloud_pay/list_stripe.html diff --git a/uncloud/urls.py b/uncloud/urls.py index 3ee5988..15623bd 100644 --- a/uncloud/urls.py +++ b/uncloud/urls.py @@ -75,6 +75,7 @@ router.register(r'v2/net/wireguardvpnsizes', netviews.WireGuardVPNSizes, basenam # Payment related router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename='credit-card') router.register(r'v2/payment/payment', payviews.PaymentViewSet, basename='payment') +router.register(r'v2/payment/balance', payviews.BalanceViewSet, basename='payment-balance') urlpatterns = [ diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 0c880c3..4eb6698 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -68,16 +68,6 @@ class Currency(models.TextChoices): # USD = 'USD', _('US Dollar') -def get_balance_for_user(user): - bills = reduce( - lambda acc, entry: acc + entry.total, - Bill.objects.filter(owner=user), - 0) - payments = reduce( - lambda acc, entry: acc + entry.amount, - Payment.objects.filter(owner=user), - 0) - return payments - bills ### # Stripe diff --git a/uncloud_pay/selectors.py b/uncloud_pay/selectors.py new file mode 100644 index 0000000..ba53c74 --- /dev/null +++ b/uncloud_pay/selectors.py @@ -0,0 +1,23 @@ +from django.utils import timezone +from django.db import transaction + +from .models import * + +def get_payments_for_user(user): + payments = [ payment.amount for payment in Payment.objects.filter(owner=user) ] + + return sum(payments) + +def get_spendings_for_user(user): + orders = Order.objects.filter(owner=user) + + amount = 0 + for order in orders: + amount += order.one_time_price + amount += order.recurring_price * order.count_used(when=timezone.now()) + + return amount + +@transaction.atomic +def get_balance_for_user(user): + return get_payments_for_user(user) - get_spendings_for_user(user) diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py index 14cac0b..361ff1c 100644 --- a/uncloud_pay/serializers.py +++ b/uncloud_pay/serializers.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _ from .models import * import uncloud_pay.stripe as uncloud_stripe +from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS ### # 2020-12 Checked code @@ -32,29 +33,13 @@ class PaymentSerializer(serializers.ModelSerializer): return data +class BalanceSerializer(serializers.Serializer): + balance = serializers.DecimalField(max_digits=AMOUNT_MAX_DIGITS, decimal_places=AMOUNT_DECIMALS) + ################################################################################ # Unchecked code -### -# Payments and Payment Methods. - - -class UpdatePaymentMethodSerializer(serializers.ModelSerializer): - class Meta: - model = PaymentMethod - fields = ['description', 'primary'] - -class ChargePaymentMethodSerializer(serializers.Serializer): - amount = serializers.DecimalField(max_digits=10, decimal_places=2) - -class CreatePaymentMethodSerializer(serializers.ModelSerializer): - please_visit = serializers.CharField(read_only=True) - - class Meta: - model = PaymentMethod - fields = ['source', 'description', 'primary', 'please_visit'] - ### # Orders & Products. diff --git a/uncloud_pay/templates/uncloud_pay/list_stripe.html b/uncloud_pay/templates/uncloud_pay/list_stripe.html deleted file mode 100644 index b5cba17..0000000 --- a/uncloud_pay/templates/uncloud_pay/list_stripe.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends 'uncloud/base.html' %} - -{% block header %} - -{% endblock %} - -{% block body %} -
-

Your credit cards registered with Stripe

- - - - -

List of stripe credit cards: -

    - {% for card in cards %} -
  • {{ card.brand }} ending in {{ card.last4 }} expiring - {{ card.year }}-{{ card.month }} - {% endfor %} - -
-

-
- -{% endblock %} diff --git a/uncloud_pay/templates/uncloud_pay/register_stripe.html b/uncloud_pay/templates/uncloud_pay/register_stripe.html index 82aca74..eaf1da4 100644 --- a/uncloud_pay/templates/uncloud_pay/register_stripe.html +++ b/uncloud_pay/templates/uncloud_pay/register_stripe.html @@ -21,17 +21,17 @@ terms of my agreement with you.

- -
- - +
The card will be registered with stripe.
+ + {% endblock %} diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index 48c24a8..073f7c9 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -24,12 +24,15 @@ import logging from .models import * from .serializers import * +from .selectors import * + from datetime import datetime from vat_validator import sanitize_vat import uncloud_pay.stripe as uncloud_stripe logger = logging.getLogger(__name__) + ### # 2020-12 checked code @@ -75,6 +78,16 @@ class PaymentViewSet(viewsets.ModelViewSet): def get_queryset(self): return Payment.objects.filter(owner=self.request.user) +class BalanceViewSet(viewsets.ViewSet): + permission_classes = [permissions.IsAuthenticated] + + def list(self, request): + serializer = BalanceSerializer(data={ + 'balance': get_balance_for_user(self.request.user) + }) + serializer.is_valid() + return Response(serializer.data) + ### # Payments and Payment Methods. From 48ce21f833a812c797caca15ea2fec465ddf950e Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Fri, 1 Jan 2021 13:25:52 +0100 Subject: [PATCH 185/194] integrate bootstrap --- requirements.txt | 1 + uncloud/settings.py | 6 ++ uncloud/templates/uncloud/base.html | 16 ++++- uncloud/templates/uncloud/index.html | 60 ++++++++++++++++--- uncloud/urls.py | 41 +++++-------- uncloud_pay/serializers.py | 2 +- .../uncloud_pay/register_stripe.html | 7 --- 7 files changed, 88 insertions(+), 45 deletions(-) diff --git a/requirements.txt b/requirements.txt index adbda9c..2a84047 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ django djangorestframework django-auth-ldap +django-bootstrap4 psycopg2 ldap3 diff --git a/uncloud/settings.py b/uncloud/settings.py index afc6d65..2ac6d69 100644 --- a/uncloud/settings.py +++ b/uncloud/settings.py @@ -51,6 +51,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'django_extensions', 'rest_framework', + 'bootstrap4', 'uncloud', 'uncloud_pay', 'uncloud_auth', @@ -164,6 +165,11 @@ USE_TZ = True # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static") ] +STATICFILES_FINDERS = [ + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +] + # XML-RPC interface of opennebula OPENNEBULA_URL = 'https://opennebula.example.com:2634/RPC2' diff --git a/uncloud/templates/uncloud/base.html b/uncloud/templates/uncloud/base.html index 034fa7c..2273591 100644 --- a/uncloud/templates/uncloud/base.html +++ b/uncloud/templates/uncloud/base.html @@ -1,3 +1,8 @@ +{% extends 'bootstrap4/bootstrap4.html' %} + +{% load bootstrap4 %} + + @@ -5,10 +10,17 @@ - {% block title %}Welcome to uncloud{% endblock %} + {% block bootstrap4_title %}{% block title %}Welcome to uncloud{% endblock %}{% endblock %} + {% block header %}{% endblock %} - {% block body %}{% endblock %} +{% block bootstrap4_content %} +
+ {% block body %}{% endblock %} +
+ + {% autoescape off %}{% bootstrap_messages %}{% endautoescape %} +{% endblock %} diff --git a/uncloud/templates/uncloud/index.html b/uncloud/templates/uncloud/index.html index e5a8318..6a88f99 100644 --- a/uncloud/templates/uncloud/index.html +++ b/uncloud/templates/uncloud/index.html @@ -2,19 +2,61 @@ {% block title %}{% endblock %} {% block body %} -
+

Welcome to uncloud

- Welcome to uncloud, checkout the following locations: +
+ Welcome to uncloud, the Open Source cloud management + system by ungleich. + It is an API driven system with + some convience views provided by + the Django Rest + Framework. You can + freely access + the source code of uncloud. -
    -
  • The uncloud API -
  • Register a credit card - (this is required to be done via Javascript so that we never see - your credit card, but it is sent directly to stripe) - You can list your credit card via the API. +
    +

    Credit cards

    +
    + Credit cards are registered with stripe. We only save a the + last 4 digits and the expiry date of the card to make + identification for you easier. +
    +
    +
      +
    • Register a credit card + (this is required to be done via Javascript so that we never see + your credit card, but it is sent directly to stripe) +
    • You can list your + credit cards + By default the first credit card is used for charging + ("active: true") and later added cards will not be + used. To change this, first disable the active flag and + then set it on another credit card. +
    +
    +
    +

    Payments and Balance

    + To trigger a payment + + +
    +
    +

    Networking

    + With uncloud you can use a variety of network related services. + + +
    -
{% endblock %} diff --git a/uncloud/urls.py b/uncloud/urls.py index 15623bd..82bce86 100644 --- a/uncloud/urls.py +++ b/uncloud/urls.py @@ -26,38 +26,27 @@ router = routers.DefaultRouter() router.register(r'beta/vm', vmviews.NicoVMProductViewSet, basename='nicovmproduct') # VM -router.register(r'v1/vm/snapshot', vmviews.VMSnapshotProductViewSet, basename='vmsnapshotproduct') -router.register(r'v1/vm/diskimage', vmviews.VMDiskImageProductViewSet, basename='vmdiskimageproduct') -router.register(r'v1/vm/disk', vmviews.VMDiskProductViewSet, basename='vmdiskproduct') -router.register(r'v1/vm/vm', vmviews.VMProductViewSet, basename='vmproduct') - - -# creates VM from os image -#router.register(r'vm/ipv6onlyvm', vmviews.VMProductViewSet, basename='vmproduct') -# ... AND adds IPv4 mapping -#router.register(r'vm/dualstackvm', vmviews.VMProductViewSet, basename='vmproduct') +# router.register(r'v1/vm/snapshot', vmviews.VMSnapshotProductViewSet, basename='vmsnapshotproduct') +# router.register(r'v1/vm/diskimage', vmviews.VMDiskImageProductViewSet, basename='vmdiskimageproduct') +# router.register(r'v1/vm/disk', vmviews.VMDiskProductViewSet, basename='vmdiskproduct') +# router.register(r'v1/vm/vm', vmviews.VMProductViewSet, basename='vmproduct') # Services -router.register(r'v1/service/matrix', serviceviews.MatrixServiceProductViewSet, basename='matrixserviceproduct') -router.register(r'v1/service/generic', serviceviews.GenericServiceProductViewSet, basename='genericserviceproduct') - +# router.register(r'v1/service/matrix', serviceviews.MatrixServiceProductViewSet, basename='matrixserviceproduct') +# router.register(r'v1/service/generic', serviceviews.GenericServiceProductViewSet, basename='genericserviceproduct') # Pay -router.register(r'v1/my/address', payviews.BillingAddressViewSet, basename='billingaddress') -router.register(r'v1/my/bill', payviews.BillViewSet, basename='bill') -router.register(r'v1/my/order', payviews.OrderViewSet, basename='order') -#router.register(r'v1/my/payment', payviews.PaymentViewSet, basename='payment') -router.register(r'v1/my/payment-method', payviews.PaymentMethodViewSet, basename='payment-method') +# router.register(r'v1/my/address', payviews.BillingAddressViewSet, basename='billingaddress') +# router.register(r'v1/my/bill', payviews.BillViewSet, basename='bill') +# router.register(r'v1/my/order', payviews.OrderViewSet, basename='order') +# router.register(r'v1/my/payment-method', payviews.PaymentMethodViewSet, basename='payment-method') # admin/staff urls -router.register(r'v1/admin/bill', payviews.AdminBillViewSet, basename='admin/bill') -#router.register(r'v1/admin/payment', payviews.AdminPaymentViewSet, basename='admin/payment') -router.register(r'v1/admin/order', payviews.AdminOrderViewSet, basename='admin/order') -router.register(r'v1/admin/vmhost', vmviews.VMHostViewSet) -router.register(r'v1/admin/vmcluster', vmviews.VMClusterViewSet) -#router.register(r'v1/admin/vpnpool', netviews.VPNPoolViewSet) -#router.register(r'v1/admin/opennebula', oneviews.VMViewSet, basename='opennebula') +# router.register(r'v1/admin/bill', payviews.AdminBillViewSet, basename='admin/bill') +# router.register(r'v1/admin/order', payviews.AdminOrderViewSet, basename='admin/order') +# router.register(r'v1/admin/vmhost', vmviews.VMHostViewSet) +# router.register(r'v1/admin/vmcluster', vmviews.VMClusterViewSet) # User/Account router.register(r'v1/my/user', authviews.UserViewSet, basename='user') @@ -73,7 +62,7 @@ router.register(r'v2/net/wireguardvpn', netviews.WireGuardVPNViewSet, basename=' router.register(r'v2/net/wireguardvpnsizes', netviews.WireGuardVPNSizes, basename='wireguardvpnnetworksizes') # Payment related -router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename='credit-card') +router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename='stripecreditcard') router.register(r'v2/payment/payment', payviews.PaymentViewSet, basename='payment') router.register(r'v2/payment/balance', payviews.BalanceViewSet, basename='payment-balance') diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py index 361ff1c..00e969b 100644 --- a/uncloud_pay/serializers.py +++ b/uncloud_pay/serializers.py @@ -10,7 +10,7 @@ from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS ### # 2020-12 Checked code -class StripeCreditCardSerializer(serializers.ModelSerializer): +class StripeCreditCardSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = StripeCreditCard exclude = [ "card_id", "owner" ] diff --git a/uncloud_pay/templates/uncloud_pay/register_stripe.html b/uncloud_pay/templates/uncloud_pay/register_stripe.html index eaf1da4..7bb0110 100644 --- a/uncloud_pay/templates/uncloud_pay/register_stripe.html +++ b/uncloud_pay/templates/uncloud_pay/register_stripe.html @@ -2,13 +2,6 @@ {% block header %} - - {% endblock %} {% block body %} From 6b9b15e66375c8ff049f5ba7c5ccd8b9c853ab82 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sun, 17 Jan 2021 15:47:37 +0100 Subject: [PATCH 186/194] Add deploy.sh --- bin/deploy.sh | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100755 bin/deploy.sh diff --git a/bin/deploy.sh b/bin/deploy.sh new file mode 100755 index 0000000..5def21a --- /dev/null +++ b/bin/deploy.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# Nico Schottelius, 2021-01-17 + +set -e + +if [ $# -ne 1 ]; then + echo "$0 target-host" + exit 1 +fi + +target_host=$1; shift +user=app + +dir=${0%/*} +uncloud_base=$(cd ${dir}/.. && pwd -P) +conf_name=local_settings-${target_host}.py +conf_file=${uncloud_base}/uncloud/${conf_name} + +if [ ! -e ${conf_file} ]; then + echo "No settings for ${target_host}." + echo "Create ${conf_file} before using this script." + exit 1 +fi + +# Deploy +rsync -av \ + --exclude venv/ \ + --exclude '*.pyc' \ + --delete \ + ${uncloud_base}/ ${user}@${target_host}:app/ + +ssh "${user}@${target_host}" ". ~/pyvenv/bin/activate; cd ~/app; pip install -r requirements.txt" + +# Config +ssh "${user}@${target_host}" "cd ~/app/uncloud; ln -sf ${conf_name} local_settings.py" + +# Restart / Apply +ssh "${user}@${target_host}" "sudo /etc/init.d/uwsgi restart" From a92088710080f842630ccc14df08a8d42e055081 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sun, 17 Jan 2021 15:53:30 +0100 Subject: [PATCH 187/194] ++bridge update --- bin/make-migrations-from-scratch.sh | 0 doc/uncloud-manual-2020-08-01.org | 26 +++- uncloud/migrations/0004_auto_20210101_1308.py | 19 +++ uncloud/models.py | 2 +- uncloud/static/uncloud/uncloud.css | 4 + uncloud/templates/uncloud/base.html | 29 +++- uncloud/templates/uncloud/index.html | 137 +++++++++++++----- uncloud/urls.py | 10 +- .../templates/uncloud_auth/login.html | 13 +- uncloud_pay/admin.py | 12 +- .../migrations/0011_auto_20210101_1308.py | 19 +++ uncloud_pay/models.py | 85 +++-------- uncloud_pay/selectors.py | 3 + uncloud_pay/serializers.py | 10 +- uncloud_pay/services.py | 32 ++++ .../templates/uncloud_pay/bill.html.j2 | 12 +- 16 files changed, 275 insertions(+), 138 deletions(-) mode change 100644 => 100755 bin/make-migrations-from-scratch.sh create mode 100644 uncloud/migrations/0004_auto_20210101_1308.py create mode 100644 uncloud/static/uncloud/uncloud.css create mode 100644 uncloud_pay/migrations/0011_auto_20210101_1308.py create mode 100644 uncloud_pay/services.py diff --git a/bin/make-migrations-from-scratch.sh b/bin/make-migrations-from-scratch.sh old mode 100644 new mode 100755 diff --git a/doc/uncloud-manual-2020-08-01.org b/doc/uncloud-manual-2020-08-01.org index 381bb62..b997600 100644 --- a/doc/uncloud-manual-2020-08-01.org +++ b/doc/uncloud-manual-2020-08-01.org @@ -1,4 +1,4 @@ -* Bootstrap / Installation +* Bootstrap / Installation / Deployment ** Pre-requisites by operating system *** General To run uncloud you need: @@ -150,7 +150,6 @@ g #+END_SRC Workers usually should have an "uncloud" user account, even though strictly speaking the username can be any. - *** WireGuardVPN Server - Allow write access to /etc/wireguard for uncloud user - Allow sudo access to "ip" and "wg" @@ -161,7 +160,11 @@ g #+END_SRC app ALL=(ALL) NOPASSWD:/sbin/ip app ALL=(ALL) NOPASSWD:/usr/bin/wg #+END_SRC - +** Typical source code based deployment + - Deploy using bin/deploy.sh on a remote server + - Remote server should have + - postgresql running, accessible via TLS from outside + - rabbitmq-configured [in progress] * Testing / CLI Access Access via the commandline (CLI) can be done using curl or @@ -462,6 +465,21 @@ Q vpn-2a0ae5c1200.ungleich.ch - query on that flag - verify it every time - ***** TODO Generating bill for admins/staff - + + + + +**** Bill fixes needed +***** TODO Double bill in bill id +***** TODO Name the currency +***** TODO Maybe remove the chromium pdf rendering artefacts + - date on the top + - title on the top + - filename bottom left + - page number could even stay +***** TODO Try to shorten the timestamp (remove time zone?) +***** TODO Bill date might be required +***** TODO Total and VAT are empty +***** TODO Line below detail/ heading diff --git a/uncloud/migrations/0004_auto_20210101_1308.py b/uncloud/migrations/0004_auto_20210101_1308.py new file mode 100644 index 0000000..8385b16 --- /dev/null +++ b/uncloud/migrations/0004_auto_20210101_1308.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2021-01-01 13:08 + +from django.db import migrations +import uncloud.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud', '0003_auto_20201220_1728'), + ] + + operations = [ + migrations.AlterField( + model_name='uncloudprovider', + name='country', + field=uncloud.models.CountryField(choices=[('AD', 'Andorra'), ('AE', 'United Arab Emirates'), ('AF', 'Afghanistan'), ('AG', 'Antigua & Barbuda'), ('AI', 'Anguilla'), ('AL', 'Albania'), ('AM', 'Armenia'), ('AN', 'Netherlands Antilles'), ('AO', 'Angola'), ('AQ', 'Antarctica'), ('AR', 'Argentina'), ('AS', 'American Samoa'), ('AT', 'Austria'), ('AU', 'Australia'), ('AW', 'Aruba'), ('AZ', 'Azerbaijan'), ('BA', 'Bosnia and Herzegovina'), ('BB', 'Barbados'), ('BD', 'Bangladesh'), ('BE', 'Belgium'), ('BF', 'Burkina Faso'), ('BG', 'Bulgaria'), ('BH', 'Bahrain'), ('BI', 'Burundi'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BN', 'Brunei Darussalam'), ('BO', 'Bolivia'), ('BR', 'Brazil'), ('BS', 'Bahama'), ('BT', 'Bhutan'), ('BV', 'Bouvet Island'), ('BW', 'Botswana'), ('BY', 'Belarus'), ('BZ', 'Belize'), ('CA', 'Canada'), ('CC', 'Cocos (Keeling) Islands'), ('CF', 'Central African Republic'), ('CG', 'Congo'), ('CH', 'Switzerland'), ('CI', 'Ivory Coast'), ('CK', 'Cook Iislands'), ('CL', 'Chile'), ('CM', 'Cameroon'), ('CN', 'China'), ('CO', 'Colombia'), ('CR', 'Costa Rica'), ('CU', 'Cuba'), ('CV', 'Cape Verde'), ('CX', 'Christmas Island'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DE', 'Germany'), ('DJ', 'Djibouti'), ('DK', 'Denmark'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('DZ', 'Algeria'), ('EC', 'Ecuador'), ('EE', 'Estonia'), ('EG', 'Egypt'), ('EH', 'Western Sahara'), ('ER', 'Eritrea'), ('ES', 'Spain'), ('ET', 'Ethiopia'), ('FI', 'Finland'), ('FJ', 'Fiji'), ('FK', 'Falkland Islands (Malvinas)'), ('FM', 'Micronesia'), ('FO', 'Faroe Islands'), ('FR', 'France'), ('FX', 'France, Metropolitan'), ('GA', 'Gabon'), ('GB', 'United Kingdom (Great Britain)'), ('GD', 'Grenada'), ('GE', 'Georgia'), ('GF', 'French Guiana'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GL', 'Greenland'), ('GM', 'Gambia'), ('GN', 'Guinea'), ('GP', 'Guadeloupe'), ('GQ', 'Equatorial Guinea'), ('GR', 'Greece'), ('GS', 'South Georgia and the South Sandwich Islands'), ('GT', 'Guatemala'), ('GU', 'Guam'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HK', 'Hong Kong'), ('HM', 'Heard & McDonald Islands'), ('HN', 'Honduras'), ('HR', 'Croatia'), ('HT', 'Haiti'), ('HU', 'Hungary'), ('ID', 'Indonesia'), ('IE', 'Ireland'), ('IL', 'Israel'), ('IN', 'India'), ('IO', 'British Indian Ocean Territory'), ('IQ', 'Iraq'), ('IR', 'Islamic Republic of Iran'), ('IS', 'Iceland'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JO', 'Jordan'), ('JP', 'Japan'), ('KE', 'Kenya'), ('KG', 'Kyrgyzstan'), ('KH', 'Cambodia'), ('KI', 'Kiribati'), ('KM', 'Comoros'), ('KN', 'St. Kitts and Nevis'), ('KP', "Korea, Democratic People's Republic of"), ('KR', 'Korea, Republic of'), ('KW', 'Kuwait'), ('KY', 'Cayman Islands'), ('KZ', 'Kazakhstan'), ('LA', "Lao People's Democratic Republic"), ('LB', 'Lebanon'), ('LC', 'Saint Lucia'), ('LI', 'Liechtenstein'), ('LK', 'Sri Lanka'), ('LR', 'Liberia'), ('LS', 'Lesotho'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('LV', 'Latvia'), ('LY', 'Libyan Arab Jamahiriya'), ('MA', 'Morocco'), ('MC', 'Monaco'), ('MD', 'Moldova, Republic of'), ('MG', 'Madagascar'), ('MH', 'Marshall Islands'), ('ML', 'Mali'), ('MN', 'Mongolia'), ('MM', 'Myanmar'), ('MO', 'Macau'), ('MP', 'Northern Mariana Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MS', 'Monserrat'), ('MT', 'Malta'), ('MU', 'Mauritius'), ('MV', 'Maldives'), ('MW', 'Malawi'), ('MX', 'Mexico'), ('MY', 'Malaysia'), ('MZ', 'Mozambique'), ('NA', 'Namibia'), ('NC', 'New Caledonia'), ('NE', 'Niger'), ('NF', 'Norfolk Island'), ('NG', 'Nigeria'), ('NI', 'Nicaragua'), ('NL', 'Netherlands'), ('NO', 'Norway'), ('NP', 'Nepal'), ('NR', 'Nauru'), ('NU', 'Niue'), ('NZ', 'New Zealand'), ('OM', 'Oman'), ('PA', 'Panama'), ('PE', 'Peru'), ('PF', 'French Polynesia'), ('PG', 'Papua New Guinea'), ('PH', 'Philippines'), ('PK', 'Pakistan'), ('PL', 'Poland'), ('PM', 'St. Pierre & Miquelon'), ('PN', 'Pitcairn'), ('PR', 'Puerto Rico'), ('PT', 'Portugal'), ('PW', 'Palau'), ('PY', 'Paraguay'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('SA', 'Saudi Arabia'), ('SB', 'Solomon Islands'), ('SC', 'Seychelles'), ('SD', 'Sudan'), ('SE', 'Sweden'), ('SG', 'Singapore'), ('SH', 'St. Helena'), ('SI', 'Slovenia'), ('SJ', 'Svalbard & Jan Mayen Islands'), ('SK', 'Slovakia'), ('SL', 'Sierra Leone'), ('SM', 'San Marino'), ('SN', 'Senegal'), ('SO', 'Somalia'), ('SR', 'Suriname'), ('ST', 'Sao Tome & Principe'), ('SV', 'El Salvador'), ('SY', 'Syrian Arab Republic'), ('SZ', 'Swaziland'), ('TC', 'Turks & Caicos Islands'), ('TD', 'Chad'), ('TF', 'French Southern Territories'), ('TG', 'Togo'), ('TH', 'Thailand'), ('TJ', 'Tajikistan'), ('TK', 'Tokelau'), ('TM', 'Turkmenistan'), ('TN', 'Tunisia'), ('TO', 'Tonga'), ('TP', 'East Timor'), ('TR', 'Turkey'), ('TT', 'Trinidad & Tobago'), ('TV', 'Tuvalu'), ('TW', 'Taiwan, Province of China'), ('TZ', 'Tanzania, United Republic of'), ('UA', 'Ukraine'), ('UG', 'Uganda'), ('UM', 'United States Minor Outlying Islands'), ('US', 'United States of America'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VA', 'Vatican City State (Holy See)'), ('VC', 'St. Vincent & the Grenadines'), ('VE', 'Venezuela'), ('VG', 'British Virgin Islands'), ('VI', 'United States Virgin Islands'), ('VN', 'Viet Nam'), ('VU', 'Vanuatu'), ('WF', 'Wallis & Futuna Islands'), ('WS', 'Samoa'), ('YE', 'Yemen'), ('YT', 'Mayotte'), ('YU', 'Yugoslavia'), ('ZA', 'South Africa'), ('ZM', 'Zambia'), ('ZR', 'Zaire'), ('ZW', 'Zimbabwe')], default='CH', max_length=2), + ), + ] diff --git a/uncloud/models.py b/uncloud/models.py index 535d920..d956637 100644 --- a/uncloud/models.py +++ b/uncloud/models.py @@ -61,7 +61,7 @@ class UncloudAddress(models.Model): street = models.CharField(max_length=256) city = models.CharField(max_length=256) postal_code = models.CharField(max_length=64) - country = CountryField(blank=True) + country = CountryField(blank=False, null=False) class Meta: abstract = True diff --git a/uncloud/static/uncloud/uncloud.css b/uncloud/static/uncloud/uncloud.css new file mode 100644 index 0000000..51d93ef --- /dev/null +++ b/uncloud/static/uncloud/uncloud.css @@ -0,0 +1,4 @@ +#content { + width: 400px; + margin: auto; +} diff --git a/uncloud/templates/uncloud/base.html b/uncloud/templates/uncloud/base.html index 2273591..44b5008 100644 --- a/uncloud/templates/uncloud/base.html +++ b/uncloud/templates/uncloud/base.html @@ -2,7 +2,6 @@ {% load bootstrap4 %} - @@ -15,7 +14,33 @@ {% block header %}{% endblock %} -{% block bootstrap4_content %} + {% block bootstrap4_content %} + + +
{% block body %}{% endblock %}
diff --git a/uncloud/templates/uncloud/index.html b/uncloud/templates/uncloud/index.html index 6a88f99..97fda34 100644 --- a/uncloud/templates/uncloud/index.html +++ b/uncloud/templates/uncloud/index.html @@ -3,51 +3,109 @@ {% block body %}
-

Welcome to uncloud

-
- Welcome to uncloud, the Open Source cloud management - system by ungleich. - It is an API driven system with - some convience views provided by - the Django Rest - Framework. You can - freely access - the source code of uncloud. +
+
+

Welcome to uncloud

+
+
+
+

About uncloud

+
+

+ Welcome to uncloud, the Open Source cloud management + system by ungleich. + It is an API driven system with + some convience views provided by + the Django Rest + Framework. You can + freely access + the source code of uncloud. +

+
+
+
+

Getting started

+
+

uncloud is designed to be as easy as possible to use. However, + there are some "real world" requirements that need to be met to + start using uncloud: -

-

Credit cards

-
+
    +
  • First you need + to register an + account. If you already have one, you can + login. +
  • If you have forgotten your password or other issues with + logging in, you can contact the ungleich support + via support at ungleich.ch. + +
  • Secondy you will need to + create a billing + address. This is required for determining the correct + tax. +
  • Next you will need to + register a credit card + from which payments can be made. Your credit card will not + be charged without your consent. +
+
+
+ +
+

Credit cards

+
+

Credit cards are registered with stripe. We only save a the last 4 digits and the expiry date of the card to make identification for you easier. -

-
-
    -
  • Register a credit card - (this is required to be done via Javascript so that we never see - your credit card, but it is sent directly to stripe) -
  • You can list your - credit cards - By default the first credit card is used for charging - ("active: true") and later added cards will not be - used. To change this, first disable the active flag and - then set it on another credit card. -
-
-
-

Payments and Balance

- To trigger a payment - +

+
  • Register a credit card + (this is required to be done via Javascript so that we never see + your credit card, but it is sent directly to stripe) +
  • You can list your + credit cards + By default the first credit card is used for charging + ("active: true") and later added cards will not be + used. To change this, first disable the active flag and + then set it on another credit card. +
  • -
    -

    Networking

    - With uncloud you can use a variety of network related services. +
    +

    Billing Address, Payments and Balance

    +
    +

    Billing addresses behave similar to credit cards: you can + have many of them, but only one can be active. The active + billing address is taken for creating new orders.

    + +

    In uncloud we use the pre-paid model: you can add money to + your account via payments. You can always check your + balance. The products you use will automatically be charged from + your existing balance. +

    + +

    In the future you will be able opt-in to automatically + recharging your account at a certain time frame or whenever it + is below a certain amount

    + + + +
    +
    + +
    +

    Networking

    +
    +

    + With uncloud you can use a variety of network related + services. +

    +
    - -
    {% endblock %} diff --git a/uncloud/urls.py b/uncloud/urls.py index 82bce86..bf3672c 100644 --- a/uncloud/urls.py +++ b/uncloud/urls.py @@ -37,7 +37,7 @@ router.register(r'beta/vm', vmviews.NicoVMProductViewSet, basename='nicovmproduc # Pay -# router.register(r'v1/my/address', payviews.BillingAddressViewSet, basename='billingaddress') + # router.register(r'v1/my/bill', payviews.BillViewSet, basename='bill') # router.register(r'v1/my/order', payviews.OrderViewSet, basename='order') # router.register(r'v1/my/payment-method', payviews.PaymentMethodViewSet, basename='payment-method') @@ -49,9 +49,9 @@ router.register(r'beta/vm', vmviews.NicoVMProductViewSet, basename='nicovmproduc # router.register(r'v1/admin/vmcluster', vmviews.VMClusterViewSet) # User/Account -router.register(r'v1/my/user', authviews.UserViewSet, basename='user') -router.register(r'v1/admin/user', authviews.AdminUserViewSet, basename='useradmin') -router.register(r'v1/user/register', authviews.AccountManagementViewSet, basename='user/register') +# router.register(r'v1/my/user', authviews.UserViewSet, basename='user') +# router.register(r'v1/admin/user', authviews.AdminUserViewSet, basename='useradmin') +# router.register(r'v1/user/register', authviews.AccountManagementViewSet, basename='user/register') ################################################################################ @@ -65,7 +65,7 @@ router.register(r'v2/net/wireguardvpnsizes', netviews.WireGuardVPNSizes, basenam router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename='stripecreditcard') router.register(r'v2/payment/payment', payviews.PaymentViewSet, basename='payment') router.register(r'v2/payment/balance', payviews.BalanceViewSet, basename='payment-balance') - +router.register(r'v2/payment/address', payviews.BillingAddressViewSet, basename='billingaddress') urlpatterns = [ path(r'api/', include(router.urls), name='api'), diff --git a/uncloud_auth/templates/uncloud_auth/login.html b/uncloud_auth/templates/uncloud_auth/login.html index 04f9a15..887467b 100644 --- a/uncloud_auth/templates/uncloud_auth/login.html +++ b/uncloud_auth/templates/uncloud_auth/login.html @@ -1,13 +1,14 @@ {% extends 'uncloud/base.html' %} +{% load bootstrap4 %} {% block body %} -
    - -
    +

    Login to uncloud

    + {% csrf_token %} - {{ form }} - + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %}
    -
    {% endblock %} diff --git a/uncloud_pay/admin.py b/uncloud_pay/admin.py index d8b09da..f604283 100644 --- a/uncloud_pay/admin.py +++ b/uncloud_pay/admin.py @@ -47,9 +47,13 @@ class BillAdmin(admin.ModelAdmin): raise self._get_404_exception(object_id) output_file = NamedTemporaryFile() - bill_html = render_to_string("bill.html.j2", {'bill': bill, - 'bill_records': bill.billrecord_set.all() - }) + bill_html = render_to_string( + "uncloud_pay/bill.html.j2", + { + 'bill': bill, + 'bill_records': bill.billrecord_set.all() + } + ) bytestring_to_pdf(bill_html.encode('utf-8'), output_file) response = FileResponse(output_file, content_type="application/pdf") @@ -63,7 +67,7 @@ class BillAdmin(admin.ModelAdmin): if bill is None: raise self._get_404_exception(object_id) - return render(request, 'bill.html.j2', + return render(request, 'uncloud_pay/bill.html.j2', {'bill': bill, 'bill_records': bill.billrecord_set.all() }) diff --git a/uncloud_pay/migrations/0011_auto_20210101_1308.py b/uncloud_pay/migrations/0011_auto_20210101_1308.py new file mode 100644 index 0000000..942f430 --- /dev/null +++ b/uncloud_pay/migrations/0011_auto_20210101_1308.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2021-01-01 13:08 + +from django.db import migrations +import uncloud.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0010_auto_20201229_0042'), + ] + + operations = [ + migrations.AlterField( + model_name='billingaddress', + name='country', + field=uncloud.models.CountryField(choices=[('AD', 'Andorra'), ('AE', 'United Arab Emirates'), ('AF', 'Afghanistan'), ('AG', 'Antigua & Barbuda'), ('AI', 'Anguilla'), ('AL', 'Albania'), ('AM', 'Armenia'), ('AN', 'Netherlands Antilles'), ('AO', 'Angola'), ('AQ', 'Antarctica'), ('AR', 'Argentina'), ('AS', 'American Samoa'), ('AT', 'Austria'), ('AU', 'Australia'), ('AW', 'Aruba'), ('AZ', 'Azerbaijan'), ('BA', 'Bosnia and Herzegovina'), ('BB', 'Barbados'), ('BD', 'Bangladesh'), ('BE', 'Belgium'), ('BF', 'Burkina Faso'), ('BG', 'Bulgaria'), ('BH', 'Bahrain'), ('BI', 'Burundi'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BN', 'Brunei Darussalam'), ('BO', 'Bolivia'), ('BR', 'Brazil'), ('BS', 'Bahama'), ('BT', 'Bhutan'), ('BV', 'Bouvet Island'), ('BW', 'Botswana'), ('BY', 'Belarus'), ('BZ', 'Belize'), ('CA', 'Canada'), ('CC', 'Cocos (Keeling) Islands'), ('CF', 'Central African Republic'), ('CG', 'Congo'), ('CH', 'Switzerland'), ('CI', 'Ivory Coast'), ('CK', 'Cook Iislands'), ('CL', 'Chile'), ('CM', 'Cameroon'), ('CN', 'China'), ('CO', 'Colombia'), ('CR', 'Costa Rica'), ('CU', 'Cuba'), ('CV', 'Cape Verde'), ('CX', 'Christmas Island'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DE', 'Germany'), ('DJ', 'Djibouti'), ('DK', 'Denmark'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('DZ', 'Algeria'), ('EC', 'Ecuador'), ('EE', 'Estonia'), ('EG', 'Egypt'), ('EH', 'Western Sahara'), ('ER', 'Eritrea'), ('ES', 'Spain'), ('ET', 'Ethiopia'), ('FI', 'Finland'), ('FJ', 'Fiji'), ('FK', 'Falkland Islands (Malvinas)'), ('FM', 'Micronesia'), ('FO', 'Faroe Islands'), ('FR', 'France'), ('FX', 'France, Metropolitan'), ('GA', 'Gabon'), ('GB', 'United Kingdom (Great Britain)'), ('GD', 'Grenada'), ('GE', 'Georgia'), ('GF', 'French Guiana'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GL', 'Greenland'), ('GM', 'Gambia'), ('GN', 'Guinea'), ('GP', 'Guadeloupe'), ('GQ', 'Equatorial Guinea'), ('GR', 'Greece'), ('GS', 'South Georgia and the South Sandwich Islands'), ('GT', 'Guatemala'), ('GU', 'Guam'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HK', 'Hong Kong'), ('HM', 'Heard & McDonald Islands'), ('HN', 'Honduras'), ('HR', 'Croatia'), ('HT', 'Haiti'), ('HU', 'Hungary'), ('ID', 'Indonesia'), ('IE', 'Ireland'), ('IL', 'Israel'), ('IN', 'India'), ('IO', 'British Indian Ocean Territory'), ('IQ', 'Iraq'), ('IR', 'Islamic Republic of Iran'), ('IS', 'Iceland'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JO', 'Jordan'), ('JP', 'Japan'), ('KE', 'Kenya'), ('KG', 'Kyrgyzstan'), ('KH', 'Cambodia'), ('KI', 'Kiribati'), ('KM', 'Comoros'), ('KN', 'St. Kitts and Nevis'), ('KP', "Korea, Democratic People's Republic of"), ('KR', 'Korea, Republic of'), ('KW', 'Kuwait'), ('KY', 'Cayman Islands'), ('KZ', 'Kazakhstan'), ('LA', "Lao People's Democratic Republic"), ('LB', 'Lebanon'), ('LC', 'Saint Lucia'), ('LI', 'Liechtenstein'), ('LK', 'Sri Lanka'), ('LR', 'Liberia'), ('LS', 'Lesotho'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('LV', 'Latvia'), ('LY', 'Libyan Arab Jamahiriya'), ('MA', 'Morocco'), ('MC', 'Monaco'), ('MD', 'Moldova, Republic of'), ('MG', 'Madagascar'), ('MH', 'Marshall Islands'), ('ML', 'Mali'), ('MN', 'Mongolia'), ('MM', 'Myanmar'), ('MO', 'Macau'), ('MP', 'Northern Mariana Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MS', 'Monserrat'), ('MT', 'Malta'), ('MU', 'Mauritius'), ('MV', 'Maldives'), ('MW', 'Malawi'), ('MX', 'Mexico'), ('MY', 'Malaysia'), ('MZ', 'Mozambique'), ('NA', 'Namibia'), ('NC', 'New Caledonia'), ('NE', 'Niger'), ('NF', 'Norfolk Island'), ('NG', 'Nigeria'), ('NI', 'Nicaragua'), ('NL', 'Netherlands'), ('NO', 'Norway'), ('NP', 'Nepal'), ('NR', 'Nauru'), ('NU', 'Niue'), ('NZ', 'New Zealand'), ('OM', 'Oman'), ('PA', 'Panama'), ('PE', 'Peru'), ('PF', 'French Polynesia'), ('PG', 'Papua New Guinea'), ('PH', 'Philippines'), ('PK', 'Pakistan'), ('PL', 'Poland'), ('PM', 'St. Pierre & Miquelon'), ('PN', 'Pitcairn'), ('PR', 'Puerto Rico'), ('PT', 'Portugal'), ('PW', 'Palau'), ('PY', 'Paraguay'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('SA', 'Saudi Arabia'), ('SB', 'Solomon Islands'), ('SC', 'Seychelles'), ('SD', 'Sudan'), ('SE', 'Sweden'), ('SG', 'Singapore'), ('SH', 'St. Helena'), ('SI', 'Slovenia'), ('SJ', 'Svalbard & Jan Mayen Islands'), ('SK', 'Slovakia'), ('SL', 'Sierra Leone'), ('SM', 'San Marino'), ('SN', 'Senegal'), ('SO', 'Somalia'), ('SR', 'Suriname'), ('ST', 'Sao Tome & Principe'), ('SV', 'El Salvador'), ('SY', 'Syrian Arab Republic'), ('SZ', 'Swaziland'), ('TC', 'Turks & Caicos Islands'), ('TD', 'Chad'), ('TF', 'French Southern Territories'), ('TG', 'Togo'), ('TH', 'Thailand'), ('TJ', 'Tajikistan'), ('TK', 'Tokelau'), ('TM', 'Turkmenistan'), ('TN', 'Tunisia'), ('TO', 'Tonga'), ('TP', 'East Timor'), ('TR', 'Turkey'), ('TT', 'Trinidad & Tobago'), ('TV', 'Tuvalu'), ('TW', 'Taiwan, Province of China'), ('TZ', 'Tanzania, United Republic of'), ('UA', 'Ukraine'), ('UG', 'Uganda'), ('UM', 'United States Minor Outlying Islands'), ('US', 'United States of America'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VA', 'Vatican City State (Holy See)'), ('VC', 'St. Vincent & the Grenadines'), ('VE', 'Venezuela'), ('VG', 'British Virgin Islands'), ('VI', 'United States Virgin Islands'), ('VN', 'Viet Nam'), ('VU', 'Vanuatu'), ('WF', 'Wallis & Futuna Islands'), ('WS', 'Samoa'), ('YE', 'Yemen'), ('YT', 'Mayotte'), ('YU', 'Yugoslavia'), ('ZA', 'South Africa'), ('ZM', 'Zambia'), ('ZR', 'Zaire'), ('ZW', 'Zimbabwe')], default='CH', max_length=2), + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 4eb6698..adaabef 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -1,24 +1,23 @@ import logging -import itertools import datetime + from math import ceil from calendar import monthrange from decimal import Decimal -from functools import reduce -from django.db import models -from django.db.models import Q -from django.contrib.auth import get_user_model -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType -from django.utils.translation import gettext_lazy as _ -from django.core.validators import MinValueValidator -from django.utils import timezone -from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.conf import settings +from django.contrib.auth import get_user_model +from django.core.validators import MinValueValidator +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.utils import timezone + +# Verify whether or not to use them here +from django.core.exceptions import ObjectDoesNotExist, ValidationError from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS from uncloud.models import UncloudAddress +from .services import * # Used to generate bill due dates. BILL_PAYMENT_DELAY=datetime.timedelta(days=10) @@ -26,36 +25,6 @@ BILL_PAYMENT_DELAY=datetime.timedelta(days=10) # Initialize logger. logger = logging.getLogger(__name__) -def start_of_month(a_day): - """ Returns first of the month of a given datetime object""" - return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0) - -def end_of_month(a_day): - """ Returns first of the month of a given datetime object""" - - _, last_day = monthrange(a_day.year, a_day.month) - return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0) - -def start_of_this_month(): - """ Returns first of this month""" - a_day = timezone.now() - return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0) - -def end_of_this_month(): - """ Returns first of this month""" - a_day = timezone.now() - - _, last_day = monthrange(a_day.year, a_day.month) - return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0) - -def end_before(a_date): - """ Return suitable datetimefield for ending just before a_date """ - return a_date - datetime.timedelta(seconds=1) - -def start_after(a_date): - """ Return suitable datetimefield for starting just after a_date """ - return a_date + datetime.timedelta(seconds=1) - def default_payment_delay(): return timezone.now() + BILL_PAYMENT_DELAY @@ -68,7 +37,6 @@ class Currency(models.TextChoices): # USD = 'USD', _('US Dollar') - ### # Stripe @@ -95,7 +63,7 @@ class StripeCreditCard(models.Model): class Meta: constraints = [ models.UniqueConstraint(fields=['owner'], - condition=Q(active=True), + condition=models.Q(active=True), name='one_active_card_per_user') ] @@ -117,9 +85,7 @@ class Payment(models.Model): ('stripe', 'Stripe'), ('voucher', 'Voucher'), ('referral', 'Referral'), - ('unknown', 'Unknown') - ), - default='unknown') + )) timestamp = models.DateTimeField(default=timezone.now) @@ -135,6 +101,11 @@ class Payment(models.Model): class PaymentMethod(models.Model): + """ + Not sure if this is still in use + + """ + owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, editable=False) @@ -151,15 +122,6 @@ class PaymentMethod(models.Model): stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True) stripe_setup_intent_id = models.CharField(max_length=32, blank=True, null=True) - # @property - # def stripe_card_last4(self): - # if self.source == 'stripe' and self.active: - # payment_method = uncloud_pay.stripe.get_payment_method( - # self.stripe_payment_method_id) - # return payment_method.card.last4 - # else: - # return None - @property def active(self): if self.source == 'stripe' and self.stripe_payment_method_id != None: @@ -276,7 +238,7 @@ class BillingAddress(UncloudAddress): class Meta: constraints = [ models.UniqueConstraint(fields=['owner'], - condition=Q(active=True), + condition=models.Q(active=True), name='one_active_billing_address_per_user') ] @@ -297,18 +259,13 @@ class BillingAddress(UncloudAddress): if not billing_address: billing_address = cls.objects.create(owner=owner, organization="uncloud admins", - name="Uncloud Admin", + full_name="Uncloud Admin", street="Uncloudstreet. 42", city="Luchsingen", postal_code="8775", country="CH", active=True) - - @staticmethod - def get_address_for(user): - return BillingAddress.objects.get(owner=user, active=True) - def __str__(self): return "{} - {}, {}, {} {}, {}".format( self.owner, @@ -1186,7 +1143,7 @@ class Bill(models.Model): return bill def __str__(self): - return f"Bill {self.owner}-{self.id}" + return f"{self.owner}-{self.id}" class BillRecord(models.Model): @@ -1256,7 +1213,7 @@ class ProductToRecurringPeriod(models.Model): class Meta: constraints = [ models.UniqueConstraint(fields=['product'], - condition=Q(is_default=True), + condition=models.Q(is_default=True), name='one_default_recurring_period_per_product'), models.UniqueConstraint(fields=['product', 'recurring_period'], name='recurring_period_once_per_product') diff --git a/uncloud_pay/selectors.py b/uncloud_pay/selectors.py index ba53c74..5f86657 100644 --- a/uncloud_pay/selectors.py +++ b/uncloud_pay/selectors.py @@ -21,3 +21,6 @@ def get_spendings_for_user(user): @transaction.atomic def get_balance_for_user(user): return get_payments_for_user(user) - get_spendings_for_user(user) + +def get_billing_address_for_user(user): + return BillingAddress.objects.get(owner=user, active=True) diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py index 00e969b..3906482 100644 --- a/uncloud_pay/serializers.py +++ b/uncloud_pay/serializers.py @@ -36,6 +36,11 @@ class PaymentSerializer(serializers.ModelSerializer): class BalanceSerializer(serializers.Serializer): balance = serializers.DecimalField(max_digits=AMOUNT_MAX_DIGITS, decimal_places=AMOUNT_DECIMALS) +class BillingAddressSerializer(serializers.ModelSerializer): + class Meta: + model = BillingAddress + exclude = [ "owner" ] + ################################################################################ # Unchecked code @@ -96,11 +101,6 @@ class BillRecordSerializer(serializers.Serializer): amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) total = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) -class BillingAddressSerializer(serializers.ModelSerializer): - class Meta: - model = BillingAddress - fields = ['uuid', 'organization', 'name', 'street', 'city', 'postal_code', 'country', 'vat_number'] - class BillSerializer(serializers.ModelSerializer): billing_address = BillingAddressSerializer(read_only=True) records = BillRecordSerializer(many=True, read_only=True) diff --git a/uncloud_pay/services.py b/uncloud_pay/services.py new file mode 100644 index 0000000..ed97c39 --- /dev/null +++ b/uncloud_pay/services.py @@ -0,0 +1,32 @@ +from django.utils import timezone + + +def start_of_month(a_day): + """ Returns first of the month of a given datetime object""" + return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0) + +def end_of_month(a_day): + """ Returns first of the month of a given datetime object""" + + _, last_day = monthrange(a_day.year, a_day.month) + return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0) + +def start_of_this_month(): + """ Returns first of this month""" + a_day = timezone.now() + return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0) + +def end_of_this_month(): + """ Returns first of this month""" + a_day = timezone.now() + + _, last_day = monthrange(a_day.year, a_day.month) + return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0) + +def end_before(a_date): + """ Return suitable datetimefield for ending just before a_date """ + return a_date - datetime.timedelta(seconds=1) + +def start_after(a_date): + """ Return suitable datetimefield for starting just after a_date """ + return a_date + datetime.timedelta(seconds=1) diff --git a/uncloud_pay/templates/uncloud_pay/bill.html.j2 b/uncloud_pay/templates/uncloud_pay/bill.html.j2 index c227f43..7cf10f8 100644 --- a/uncloud_pay/templates/uncloud_pay/bill.html.j2 +++ b/uncloud_pay/templates/uncloud_pay/bill.html.j2 @@ -680,11 +680,9 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
    - {{ bill.starting_date|date:"c" }} - - {{ bill.ending_date|date:"c" }} -
    Bill id: {{ bill }} -
    Due: {{ bill.due_date }} - + Bill id: {{ bill }} +
    {{ bill.starting_date|date:"Ymd" }} - + {{ bill.ending_date|date:"Ymd" }}
    @@ -703,8 +701,8 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7 {% for record in bill_records %} - {{ record.starting_date|date:"c" }} - - {{ record.ending_date|date:"c" }} + {{ record.starting_date|date:"Ymd-H:i:s" }} + - {{ record.ending_date|date:"Ymd-H:i:s" }} {{ record.order }} {{ record.price|floatformat:2 }} From c8ce7dbb40ef5c1de4a13c8b496974bd61a48116 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sun, 17 Jan 2021 15:54:16 +0100 Subject: [PATCH 188/194] do not touch local_settings.py on deploy --- bin/deploy.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/deploy.sh b/bin/deploy.sh index 5def21a..99f7ba0 100755 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -26,6 +26,7 @@ fi rsync -av \ --exclude venv/ \ --exclude '*.pyc' \ + --exclude uncloud/local_settings.py \ --delete \ ${uncloud_base}/ ${user}@${target_host}:app/ From 49f52fd41d88281ad5033dd8b8c75fff55239ad8 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 13 Feb 2021 18:50:28 +0100 Subject: [PATCH 189/194] [bootstrap] update to bootstrap5 --- requirements.txt | 2 +- uncloud/settings.py | 2 +- uncloud/templates/uncloud/base.html | 72 ++---- uncloud/templates/uncloud/index.html | 235 ++++++++++-------- .../uncloud_pay/register_stripe.html | 34 +-- 5 files changed, 176 insertions(+), 169 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2a84047..0c153a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ django djangorestframework django-auth-ldap -django-bootstrap4 +django-bootstrap-v5 psycopg2 ldap3 diff --git a/uncloud/settings.py b/uncloud/settings.py index 2ac6d69..a103ca0 100644 --- a/uncloud/settings.py +++ b/uncloud/settings.py @@ -51,7 +51,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'django_extensions', 'rest_framework', - 'bootstrap4', + 'bootstrap5', 'uncloud', 'uncloud_pay', 'uncloud_auth', diff --git a/uncloud/templates/uncloud/base.html b/uncloud/templates/uncloud/base.html index 44b5008..8c5c867 100644 --- a/uncloud/templates/uncloud/base.html +++ b/uncloud/templates/uncloud/base.html @@ -1,51 +1,27 @@ -{% extends 'bootstrap4/bootstrap4.html' %} +{% extends 'bootstrap5/bootstrap5.html' %} +{% block bootstrap5_before_content %} -{% load bootstrap4 %} + {% endblock %} - - diff --git a/uncloud/templates/uncloud/index.html b/uncloud/templates/uncloud/index.html index 97fda34..19fc436 100644 --- a/uncloud/templates/uncloud/index.html +++ b/uncloud/templates/uncloud/index.html @@ -1,119 +1,148 @@ {% extends 'uncloud/base.html' %} -{% block title %}{% endblock %} +{% block title %}Welcome to uncloud [beta]{% endblock %} -{% block body %} -
    - -
    -
    -

    Welcome to uncloud

    +{% block bootstrap5_content %} +
    +
    +
    +
    +

    Welcome to uncloud [beta]

    +
    -
    -
    -

    About uncloud

    -
    -

    - Welcome to uncloud, the Open Source cloud management - system by ungleich. - It is an API driven system with - some convience views provided by - the Django Rest - Framework. You can - freely access - the source code of uncloud. -

    + +
    +

    About uncloud

    +
    +

    + Welcome to uncloud, the Open Source cloud management + system by ungleich. + It is an API driven system with + some convience views provided by + the Django Rest + Framework. You can + freely access + the source code of uncloud. + This is a BETA service. As such, some + functionality might not be very sophisticated. +

    +
    -
    -
    -

    Getting started

    -
    -

    uncloud is designed to be as easy as possible to use. However, - there are some "real world" requirements that need to be met to - start using uncloud: +

    +

    Getting started

    +
    +

    uncloud is designed to be as easy as possible to use. However, + there are some "real world" requirements that need to be met to + start using uncloud: -

      -
    • First you need - to register an - account. If you already have one, you can - login. -
    • If you have forgotten your password or other issues with - logging in, you can contact the ungleich support - via support at ungleich.ch. +
        +
      • First you need + to register an + account. If you already have one, you can + login. +
      • If you have forgotten your password or other issues with + logging in, you can contact the ungleich support + via support at ungleich.ch. -
      • Secondy you will need to - create a billing - address. This is required for determining the correct - tax. -
      • Next you will need to - register a credit card - from which payments can be made. Your credit card will not - be charged without your consent. -
      +
    • Secondy you will need to + create a billing + address. This is required for determining the correct + tax. +
    • Next you will need to + register a credit card + from which payments can be made. Your credit card will not + be charged without your consent. +
    +
    -
    - -
    -

    Credit cards

    -
    -

    - Credit cards are registered with stripe. We only save a the - last 4 digits and the expiry date of the card to make - identification for you easier. -

    -
      -
    • Register a credit card - (this is required to be done via Javascript so that we never see - your credit card, but it is sent directly to stripe) -
    • You can list your - credit cards - By default the first credit card is used for charging - ("active: true") and later added cards will not be - used. To change this, first disable the active flag and - then set it on another credit card. +
      +

      Introduction to uncloud concepts

      +
      +

      We plan to offer many services on uncloud ranging from + for free, for a small amount or regular charges. As transfer + fees are a major challenge for our business, we based uncloud + on the pre-paid account model. Which means + that you can charge your account and then use your balance to + pay for product usage.

      +
      -
    -
    -

    Billing Address, Payments and Balance

    -
    -

    Billing addresses behave similar to credit cards: you can - have many of them, but only one can be active. The active - billing address is taken for creating new orders.

    -

    In uncloud we use the pre-paid model: you can add money to - your account via payments. You can always check your - balance. The products you use will automatically be charged from - your existing balance. -

    - -

    In the future you will be able opt-in to automatically - recharging your account at a certain time frame or whenever it - is below a certain amount

    - - - +
    +

    Credit cards

    +
    +

    + Credit cards are registered with stripe. We only save a the + last 4 digits and the expiry date of the card to make + identification for you easier. +

    +
      +
    • Register a credit card + (this is required to be done via Javascript so that we never see + your credit card, but it is sent directly to stripe) +
    • You can list your + credit cards + By default the first credit card is used for charging + ("active: true") and later added cards will not be + used. To change this, first disable the active flag and + then set it on another credit card. +
    -
    +
    +

    Billing Address, Payments and Balance

    +
    +

    Billing addresses behave similar to credit cards: you can + have many of them, but only one can be active. The active + billing address is taken for creating new orders.

    -
    -

    Networking

    -
    -

    - With uncloud you can use a variety of network related - services. -

    +

    In uncloud we use the pre-paid model: you can add money to + your account via payments. You can always check your + balance. The products you use will automatically be charged from + your existing balance. +

    - +

    In the future you will be able opt-in to automatically + recharging your account at a certain time frame or whenever it + is below a certain amount

    + + + +
    + +
    +

    Networking

    +
    +

    + With uncloud you can use a variety of network related + services. +

    + + +
    +
    + +
    +

    Current limitations

    +
    +
      +
    • Payments are only possible in CHF. +
    • Bills are not yet visible (payments are, though) +
    +
    +
    +
    {% endblock %} diff --git a/uncloud_pay/templates/uncloud_pay/register_stripe.html b/uncloud_pay/templates/uncloud_pay/register_stripe.html index 7bb0110..7205261 100644 --- a/uncloud_pay/templates/uncloud_pay/register_stripe.html +++ b/uncloud_pay/templates/uncloud_pay/register_stripe.html @@ -1,28 +1,30 @@ {% extends 'uncloud/base.html' %} -{% block header %} +{% block bootstrap5_extra_head %} {% endblock %} -{% block body %} -
    -

    Register Credit Card with Stripe

    -

    - By submitting I authorise to send instructions to - the financial institution that issued my card to take - payments from my card account in accordance with the +{% block bootstrap5_content %} +

    + +
    +

    Register Credit Card with Stripe

    +

    + By submitting I authorise to send instructions to + the financial institution that issued my card to take + payments from my card account in accordance with the terms of my agreement with you. -

    +

    - -
    - -
    The card will be registered with stripe.
    + +
    + +
    The card will be registered with stripe.
    - -
    From 745abc48efdab2c7fbac74dc675c66d293329038 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Tue, 25 May 2021 19:55:13 +0200 Subject: [PATCH 190/194] Add balance if user is logged in --- uncloud/templates/uncloud/base.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uncloud/templates/uncloud/base.html b/uncloud/templates/uncloud/base.html index 8c5c867..5915bed 100644 --- a/uncloud/templates/uncloud/base.html +++ b/uncloud/templates/uncloud/base.html @@ -11,7 +11,8 @@
    @@ -138,11 +141,30 @@
    • Payments are only possible in CHF. -
    • Bills are not yet visible (payments are, though)
    - + {% if user.is_authenticated %} +
    +

    Account Settings

    +
    +
      +
      + {% csrf_token %} +
      + Delete User Account +

      Are you sure you want to delete your account? This will permanently delete your + profile and any orders you have generated.

      + {{ delete_form }} +
      +
      + +
      +
      +
    +
    +
    + {% endif %}
    {% endblock %} diff --git a/uncloud/urls.py b/uncloud/urls.py index f72a286..14e45fd 100644 --- a/uncloud/urls.py +++ b/uncloud/urls.py @@ -19,6 +19,7 @@ from uncloud_net import views as netviews from uncloud_pay import views as payviews from uncloud_vm import views as vmviews from uncloud_service import views as serviceviews +from matrixhosting import views as matrixviews router = routers.DefaultRouter() @@ -37,6 +38,9 @@ router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename= router.register(r'v2/payment/payment', payviews.PaymentViewSet, basename='payment') router.register(r'v2/payment/balance', payviews.BalanceViewSet, basename='payment-balance') router.register(r'v2/payment/address', payviews.BillingAddressViewSet, basename='billingaddress') +router.register(r'v2/orders', payviews.OrderViewSet, basename='orders') +router.register(r'v2/bill', payviews.BillViewSet, basename='bills') +router.register(r'v2/machines', matrixviews.MachineViewSet, basename='machines') # Generic helper views that are usually not needed router.register(r'v2/generic/vat-rate', payviews.VATRateViewSet, basename='vatrate') @@ -54,9 +58,8 @@ urlpatterns = [ path('admin/', admin.site.urls), - path('login/', authviews.LoginView.as_view(), name="login"), - path('logout/', authviews.LogoutView.as_view(), name="logout"), + path('accounts/', include('allauth.urls')), path('cc/reg/', payviews.RegisterCard.as_view(), name="cc_register"), - + path('matrix/', include('matrixhosting.urls', namespace='matrix')), path('', uncloudviews.UncloudIndex.as_view(), name="uncloudindex"), ] diff --git a/uncloud/views.py b/uncloud/views.py index 37542bb..a4bf683 100644 --- a/uncloud/views.py +++ b/uncloud/views.py @@ -1,13 +1,23 @@ from django.views.generic.base import TemplateView +from django.contrib import messages +from django.shortcuts import redirect from uncloud_pay.selectors import get_balance_for_user +from .forms import UserDeleteForm class UncloudIndex(TemplateView): template_name = "uncloud/index.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - print(context) if self.request.user.is_authenticated: context['balance'] = get_balance_for_user(self.request.user) + context['delete_form'] = UserDeleteForm(instance=self.request.user) return context + + def post(self, request, *args, **kwargs): + UserDeleteForm(request.POST, instance=request.user) + user = request.user + user.delete() + messages.info(request, 'Your account has been deleted.') + return redirect('uncloudindex') \ No newline at end of file diff --git a/uncloud_net/services.py b/uncloud_net/services.py index 9149f01..8449394 100644 --- a/uncloud_net/services.py +++ b/uncloud_net/services.py @@ -3,7 +3,7 @@ from django.db import transaction from .models import * from .selectors import * from .tasks import * - +from django_q.tasks import async_task, result @transaction.atomic def create_wireguard_vpn(owner, public_key, network_mask): @@ -60,7 +60,6 @@ def create_wireguard_vpn_tech(owner, public_key, network_mask): server = pool.vpn_server_hostname wg_name = pool.wg_name - configure_wireguard_server_on_host.apply_async((wg_name, config), - queue=server) + async_task(configure_wireguard_server_on_host, (wg_name, config), queue=server) return vpn diff --git a/uncloud_net/tasks.py b/uncloud_net/tasks.py index 7d94f3b..5684871 100644 --- a/uncloud_net/tasks.py +++ b/uncloud_net/tasks.py @@ -1,17 +1,14 @@ -from celery import shared_task from .models import * -from uncloud.models import UncloudTask - import os import subprocess import logging import uuid - +from django_q.tasks import async_task, result log = logging.getLogger(__name__) -@shared_task + def configure_wireguard_server_on_host(wg_name, config): """ - Create wireguard config (DB query -> string) @@ -47,11 +44,9 @@ def configure_wireguard_server_via_cdist(wireguardvpnpool): log.info(f"Configuring VPN server {server} (async)") - task_id = uuid.UUID(cdist_configure_wireguard_server.apply_async((config, server)).id) - UncloudTask.objects.create(task_id=task_id) + async_task(cdist_configure_wireguard_server,config, server).id -@shared_task def cdist_configure_wireguard_server(config, server): """ Create config and configure server. diff --git a/uncloud_net/tests.py b/uncloud_net/tests.py index 4491551..75bdafa 100644 --- a/uncloud_net/tests.py +++ b/uncloud_net/tests.py @@ -37,7 +37,7 @@ class VPNTests(TestCase): self.vpn_wireguard_public_key = 'B2b78eWBIXPMM1x4DDjkCDZepS0qDgcLN3T3PjcgXkY=' - self.vpnpool = VPNPool.objects.get_or_create(network=self.pool_network, + self.vpnpool = WireGuardVPNPool.objects.get_or_create(network=self.pool_network, network_size=self.pool_network_size, subnetwork_size=self.pool_subnetwork_size, vpn_hostname=self.pool_vpn_hostname, @@ -47,55 +47,6 @@ class VPNTests(TestCase): self.factory = APIRequestFactory() - def test_create_vpnpool(self): - url = reverse("vpnpool-list") - view = VPNPoolViewSet.as_view({'post': 'create'}) - request = self.factory.post(url, { 'network': self.pool_network2, - 'network_size': self.pool_network_size, - 'subnetwork_size': self.pool_subnetwork_size, - 'vpn_hostname': self.pool_vpn_hostname, - 'wireguard_private_key': self.pool_wireguard_private_key - - }) - force_authenticate(request, user=self.admin_user) - response = view(request) - - # This raises an exception if the request was not successful - # No assert needed - pool = VPNPool.objects.get(network=self.pool_network2) - - # def test_create_vpn(self): - # url = reverse("vpnnetwork-list") - # view = VPNNetworkViewSet.as_view({'post': 'create'}) - # request = self.factory.post(url, { 'network_size': self.pool_subnetwork_size, - # 'wireguard_public_key': self.vpn_wireguard_public_key - - # }) - # force_authenticate(request, user=self.user) - - - # # we don't have a billing address -> should raise an error - # # with self.assertRaises(ValidationError): - # # response = view(request) - - # addr = BillingAddress.objects.get_or_create( - # owner=self.user, - # active=True, - # defaults={'organization': 'ungleich', - # 'name': 'Nico Schottelius', - # 'street': 'Hauptstrasse 14', - # 'city': 'Luchsingen', - # 'postal_code': '8775', - # 'country': 'CH' } - # ) - - # # This should work now - # response = view(request) - - # # Verify that an order was created successfully - there should only be one order at - # # this point in time - # order = Order.objects.get(owner=self.user) - def tearDown(self): self.user.delete() diff --git a/uncloud_net/views.py b/uncloud_net/views.py index 7dadbf4..8e7e81b 100644 --- a/uncloud_net/views.py +++ b/uncloud_net/views.py @@ -61,10 +61,3 @@ class WireGuardVPNSizes(viewsets.ViewSet): print(sizes) return Response(WireGuardVPNSizesSerializer(sizes, many=True).data) - - - -# class VPNPoolViewSet(viewsets.ModelViewSet): -# serializer_class = VPNPoolSerializer -# permission_classes = [permissions.IsAdminUser] -# queryset = VPNPool.objects.all() diff --git a/uncloud_pay/admin.py b/uncloud_pay/admin.py index f604283..cb7b650 100644 --- a/uncloud_pay/admin.py +++ b/uncloud_pay/admin.py @@ -4,7 +4,6 @@ from django.urls import path from django.shortcuts import render from django.conf.urls import url -from uncloud_pay.views import BillViewSet from hardcopy import bytestring_to_pdf from django.core.files.temp import NamedTemporaryFile from django.http import FileResponse @@ -90,14 +89,15 @@ admin.site.register(Bill, BillAdmin) admin.site.register(Product, ProductAdmin) for m in [ - BillRecord, BillingAddress, Order, + BillRecord, Payment, ProductToRecurringPeriod, RecurringPeriod, StripeCreditCard, StripeCustomer, - VATRate, + PricingPlan, + VATRate ]: admin.site.register(m) diff --git a/uncloud_pay/management/commands/charge-negative-balance.py b/uncloud_pay/management/commands/charge-negative-balance.py index 8ee8736..8405bd3 100644 --- a/uncloud_pay/management/commands/charge-negative-balance.py +++ b/uncloud_pay/management/commands/charge-negative-balance.py @@ -1,6 +1,7 @@ from django.core.management.base import BaseCommand from uncloud_auth.models import User -from uncloud_pay.models import Order, Bill, PaymentMethod, get_balance_for_user +from uncloud_pay.models import Order, Bill, get_balance_for_user +import uncloud_pay.stripe as uncloud_stripe from datetime import timedelta from django.utils import timezone @@ -18,14 +19,10 @@ class Command(BaseCommand): balance = get_balance_for_user(user) if balance < 0: print("User {} has negative balance ({}), charging.".format(user.username, balance)) - payment_method = PaymentMethod.get_primary_for(user) - if payment_method != None: - amount_to_be_charged = abs(balance) - charge_ok = payment_method.charge(amount_to_be_charged) - if not charge_ok: - print("ERR: charging {} with method {} failed" - .format(user.username, payment_method.uuid) - ) - else: - print("ERR: no payment method registered for {}".format(user.username)) + amount_to_be_charged = abs(balance) + result = uncloud_stripe.charge_customer(user, amount_to_be_charged) + if result.status != 'succeeded': + print("ERR: charging {} with method {} failed" + .format(user.username, result) + ) print("=> Done.") diff --git a/uncloud_pay/management/commands/import-vat-rates.py b/uncloud_pay/management/commands/import-vat-rates.py index 46848cd..a741740 100644 --- a/uncloud_pay/management/commands/import-vat-rates.py +++ b/uncloud_pay/management/commands/import-vat-rates.py @@ -1,11 +1,14 @@ from django.core.management.base import BaseCommand from uncloud_pay.models import VATRate +import logging import urllib import csv import sys import io +logger = logging.getLogger(__name__) + class Command(BaseCommand): help = '''Imports VAT Rates. Assume vat rates of format https://github.com/kdeldycke/vat-rates/blob/master/vat_rates.csv''' vat_url = "https://raw.githubusercontent.com/ungleich/vat-rates/main/vat_rates.csv" @@ -23,13 +26,25 @@ class Command(BaseCommand): reader = csv.DictReader(csv_file) for row in reader: -# print(row) - obj, created = VATRate.objects.get_or_create( - starting_date=row["start_date"], - ending_date=row["stop_date"] if row["stop_date"] != "" else None, - territory_codes=row["territory_codes"], - currency_code=row["currency_code"], - rate=row["rate"], - rate_type=row["rate_type"], - description=row["description"] - ) + if row["territory_codes"] and len(row["territory_codes"].splitlines()) > 1: + for code in row["territory_codes"].splitlines(): + VATRate.objects.get_or_create( + starting_date=row["start_date"], + ending_date=row["stop_date"] if row["stop_date"] != "" else None, + territory_codes=code, + currency_code=row["currency_code"], + rate=row["rate"], + rate_type=row["rate_type"], + description=row["description"] + ) + else: + VATRate.objects.get_or_create( + starting_date=row["start_date"], + ending_date=row["stop_date"] if row["stop_date"] != "" else None, + territory_codes=row["territory_codes"], + currency_code=row["currency_code"], + rate=row["rate"], + rate_type=row["rate_type"], + description=row["description"] + ) + logger.info('All VAT Rates have been added!') diff --git a/uncloud_pay/migrations/0012_auto_20210630_0742.py b/uncloud_pay/migrations/0012_auto_20210630_0742.py new file mode 100644 index 0000000..45e3dfe --- /dev/null +++ b/uncloud_pay/migrations/0012_auto_20210630_0742.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.4 on 2021-06-30 07:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0011_auto_20210101_1308'), + ] + + operations = [ + migrations.AddField( + model_name='billingaddress', + name='vat_number_verified', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='payment', + name='source', + field=models.CharField(choices=[('wire', 'Wire Transfer'), ('stripe', 'Stripe'), ('voucher', 'Voucher'), ('referral', 'Referral')], max_length=256), + ), + ] diff --git a/uncloud_pay/migrations/0013_alter_billingaddress_owner.py b/uncloud_pay/migrations/0013_alter_billingaddress_owner.py new file mode 100644 index 0000000..7597129 --- /dev/null +++ b/uncloud_pay/migrations/0013_alter_billingaddress_owner.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.4 on 2021-07-03 15:23 + +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_pay', '0012_auto_20210630_0742'), + ] + + operations = [ + migrations.AlterField( + model_name='billingaddress', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='billing_addresses', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/uncloud_pay/migrations/0014_auto_20210703_1747.py b/uncloud_pay/migrations/0014_auto_20210703_1747.py new file mode 100644 index 0000000..1c004d0 --- /dev/null +++ b/uncloud_pay/migrations/0014_auto_20210703_1747.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.4 on 2021-07-03 17:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0013_alter_billingaddress_owner'), + ] + + operations = [ + migrations.AddField( + model_name='billingaddress', + name='stripe_tax_id', + field=models.CharField(blank=True, default='', max_length=100), + ), + migrations.AddField( + model_name='billingaddress', + name='vat_number_validated_on', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/uncloud_pay/migrations/0015_auto_20210705_0849.py b/uncloud_pay/migrations/0015_auto_20210705_0849.py new file mode 100644 index 0000000..dfb6d80 --- /dev/null +++ b/uncloud_pay/migrations/0015_auto_20210705_0849.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.4 on 2021-07-05 08:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0014_auto_20210703_1747'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='customer', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.stripecustomer'), + ), + migrations.AddField( + model_name='order', + name='status', + field=models.CharField(choices=[('draft', 'Draft'), ('declined', 'Declined'), ('approved', 'Approved')], default='draft', max_length=100), + ), + migrations.AddField( + model_name='order', + name='stripe_charge_id', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='order', + name='vm_id', + field=models.IntegerField(default=0), + ), + ] diff --git a/uncloud_pay/migrations/0016_pricingplan.py b/uncloud_pay/migrations/0016_pricingplan.py new file mode 100644 index 0000000..505c141 --- /dev/null +++ b/uncloud_pay/migrations/0016_pricingplan.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.4 on 2021-07-06 13:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0015_auto_20210705_0849'), + ] + + operations = [ + migrations.CreateModel( + name='PricingPlan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('vat_inclusive', models.BooleanField(default=True)), + ('vat_percentage', models.DecimalField(blank=True, decimal_places=5, default=0, max_digits=7)), + ('set_up_fees', models.DecimalField(decimal_places=2, default=0, max_digits=7)), + ('cores_unit_price', models.DecimalField(decimal_places=2, default=0, max_digits=7)), + ('ram_unit_price', models.DecimalField(decimal_places=2, default=0, max_digits=7)), + ('storage_unit_price', models.DecimalField(decimal_places=2, default=0, max_digits=7)), + ('discount_name', models.CharField(blank=True, max_length=255, null=True)), + ('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=6)), + ('stripe_coupon_id', models.CharField(blank=True, max_length=255, null=True)), + ], + ), + ] diff --git a/uncloud_pay/migrations/0017_auto_20210706_1728.py b/uncloud_pay/migrations/0017_auto_20210706_1728.py new file mode 100644 index 0000000..1571b10 --- /dev/null +++ b/uncloud_pay/migrations/0017_auto_20210706_1728.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.4 on 2021-07-06 17:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0016_pricingplan'), + ] + + operations = [ + migrations.RemoveField( + model_name='paymentmethod', + name='owner', + ), + migrations.DeleteModel( + name='Payment', + ), + migrations.DeleteModel( + name='PaymentMethod', + ), + ] diff --git a/uncloud_pay/migrations/0018_payment.py b/uncloud_pay/migrations/0018_payment.py new file mode 100644 index 0000000..47d6e3a --- /dev/null +++ b/uncloud_pay/migrations/0018_payment.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.4 on 2021-07-06 17:47 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0017_auto_20210706_1728'), + ] + + operations = [ + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), + ('source', models.CharField(choices=[('wire', 'Wire Transfer'), ('stripe', 'Stripe'), ('voucher', 'Voucher'), ('referral', 'Referral')], max_length=256)), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), + ('currency', models.CharField(choices=[('CHF', 'Swiss Franc')], default='CHF', max_length=32)), + ('external_reference', models.CharField(blank=True, default='', max_length=256, null=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/uncloud_pay/migrations/0019_order_pricing_plan.py b/uncloud_pay/migrations/0019_order_pricing_plan.py new file mode 100644 index 0000000..5392ce6 --- /dev/null +++ b/uncloud_pay/migrations/0019_order_pricing_plan.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.4 on 2021-07-06 19:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0018_payment'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='pricing_plan', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.pricingplan'), + ), + ] diff --git a/uncloud_pay/migrations/0020_rename_is_final_bill_is_closed.py b/uncloud_pay/migrations/0020_rename_is_final_bill_is_closed.py new file mode 100644 index 0000000..f3419eb --- /dev/null +++ b/uncloud_pay/migrations/0020_rename_is_final_bill_is_closed.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.4 on 2021-07-07 20:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0019_order_pricing_plan'), + ] + + operations = [ + migrations.RenameField( + model_name='bill', + old_name='is_final', + new_name='is_closed', + ), + ] diff --git a/uncloud_pay/migrations/0021_auto_20210709_0914.py b/uncloud_pay/migrations/0021_auto_20210709_0914.py new file mode 100644 index 0000000..66e3dcb --- /dev/null +++ b/uncloud_pay/migrations/0021_auto_20210709_0914.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.4 on 2021-07-09 09:14 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0020_rename_is_final_bill_is_closed'), + ] + + operations = [ + migrations.RemoveField( + model_name='order', + name='stripe_charge_id', + ), + migrations.RemoveField( + model_name='order', + name='vm_id', + ), + ] diff --git a/uncloud_pay/migrations/0022_remove_order_status.py b/uncloud_pay/migrations/0022_remove_order_status.py new file mode 100644 index 0000000..2b51be8 --- /dev/null +++ b/uncloud_pay/migrations/0022_remove_order_status.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.4 on 2021-07-11 08:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0021_auto_20210709_0914'), + ] + + operations = [ + migrations.RemoveField( + model_name='order', + name='status', + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 49e8da2..a4cf007 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -1,5 +1,6 @@ import logging import datetime +import json from math import ceil from calendar import monthrange @@ -9,18 +10,22 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.core.validators import MinValueValidator from django.db import models +from django.db.models import Q from django.utils.translation import gettext_lazy as _ from django.utils import timezone - +from django_q.tasks import schedule +from django_q.models import Schedule # Verify whether or not to use them here from django.core.exceptions import ObjectDoesNotExist, ValidationError +import uncloud_pay from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS -from uncloud.models import UncloudAddress +from uncloud.models import UncloudAddress, UncloudProvider +from uncloud.selectors import filter_for_when from .services import * # Used to generate bill due dates. -BILL_PAYMENT_DELAY=datetime.timedelta(days=10) +BILL_PAYMENT_DELAY=datetime.timedelta(days=settings.BILL_PAYMENT_DELAY) # Initialize logger. logger = logging.getLogger(__name__) @@ -96,84 +101,18 @@ class Payment(models.Model): def __str__(self): return f"{self.amount}{self.currency} from {self.owner} via {self.source} on {self.timestamp}" -### -# Payments and Payment Methods. - - -class PaymentMethod(models.Model): - """ - Not sure if this is still in use - - """ - - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE, - editable=False) - source = models.CharField(max_length=256, - choices = ( - ('stripe', 'Stripe'), - ('unknown', 'Unknown'), - ), - default='stripe') - description = models.TextField() - primary = models.BooleanField(default=False, editable=False) - - # Only used for "Stripe" source - stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True) - stripe_setup_intent_id = models.CharField(max_length=32, blank=True, null=True) - - @property - def active(self): - if self.source == 'stripe' and self.stripe_payment_method_id != None: - return True - else: - return False - - def charge(self, amount): - if not self.active: - raise Exception('This payment method is inactive.') - - if amount < 0: # Make sure we don't charge negative amount by errors... - raise Exception('Cannot charge negative amount.') - + def save(self, *args, **kwargs): + # Try to charge the user via the active card before saving otherwise throw payment Error if self.source == 'stripe': - stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id - stripe_payment = uncloud_pay.stripe.charge_customer( - amount, stripe_customer, self.stripe_payment_method_id) - if 'paid' in stripe_payment and stripe_payment['paid'] == False: - raise Exception(stripe_payment['error']) - else: - payment = Payment.objects.create( - owner=self.owner, source=self.source, amount=amount) - - return payment - else: - raise Exception('This payment method is unsupported/cannot be charged.') - - def set_as_primary_for(self, user): - methods = PaymentMethod.objects.filter(owner=user, primary=True) - for method in methods: - print(method) - method.primary = False - method.save() - - self.primary = True - self.save() - - def get_primary_for(user): - methods = PaymentMethod.objects.filter(owner=user) - for method in methods: - # Do we want to do something with non-primary method? - if method.active and method.primary: - return method - - return None - - class Meta: - # TODO: limit to one primary method per user. - # unique_together is no good since it won't allow more than one - # non-primary method. - pass + try: + result = uncloud_pay.stripe.charge_customer(self.owner, self.amount, self.currency,) + if not result.status or result.status != 'succeeded': + raise Exception("The payment has been failed, please try to activate another card") + super().save(*args, **kwargs) + except Exception as e: + raise e + + # See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types class RecurringPeriodDefaultChoices(models.IntegerChoices): @@ -231,9 +170,11 @@ class RecurringPeriod(models.Model): # Bills. class BillingAddress(UncloudAddress): - owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='billing_addresses') vat_number = models.CharField(max_length=100, default="", blank=True) vat_number_verified = models.BooleanField(default=False) + vat_number_validated_on = models.DateTimeField(blank=True, null=True) + stripe_tax_id = models.CharField(max_length=100, default="", blank=True) active = models.BooleanField(default=False) class Meta: @@ -272,6 +213,10 @@ class BillingAddress(UncloudAddress): self.owner, self.full_name, self.street, self.postal_code, self.city, self.country) + + @staticmethod + def get_address_for(user): + return BillingAddress.objects.get(owner=user) ### # VAT @@ -297,10 +242,44 @@ class VATRate(models.Model): logger.debug(str(dne)) logger.debug("Did not find VAT rate for %s, returning 0" % country_code) return 0 + + @staticmethod + def get_vat_rate(billing_address, when=None): + """ + Returns the VAT rate for business to customer. + + B2B is always 0% with the exception of trading within the own country + """ + + country = billing_address.country + + # Need to have a provider country + providers = UncloudProvider.objects.all() + vatrate = filter_for_when(VATRate.objects.filter(territory_codes=country), when).first() + + if not providers and not vatrate: + return 0 + + uncloud_provider = filter_for_when(providers).get() + + # By default we charge VAT. This affects: + # - Same country sales (VAT applied) + # - B2C to EU (VAT applied) + rate = vatrate.rate if vatrate else 0 + + # Exception: if... + # - the billing_address is in EU, + # - the vat_number has been set + # - the vat_number has been verified + # Then we do not charge VAT + + if uncloud_provider.country != country and billing_address.vat_number and billing_address.vat_number_verified: + rate = 0 + return rate def __str__(self): - return f"{self.territory_codes}: {self.starting_date} - {self.ending_date}: {self.rate_type}" + return f"{self.territory_codes}: {self.starting_date} - {self.ending_date or ''}: {self.rate_type}" ### # Products @@ -342,30 +321,20 @@ class Product(models.Model): 'features': { 'cores': { 'min': 1, - 'max': 48, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 3 + 'max': 48 }, 'ram_gb': { 'min': 1, - 'max': 256, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 4 + 'max': 256 }, 'ssd_gb': - { 'min': 10, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 0.35 + { 'min': 10 }, 'hdd_gb': { 'min': 0, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 15/1000 }, 'additional_ipv4_address': { 'min': 0, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 8 }, } } @@ -381,36 +350,23 @@ class Product(models.Model): 'base': { 'min': 1, 'max': 1, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 1 }, 'cores': { 'min': 1, 'max': 48, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 3 }, 'ram_gb': { 'min': 1, 'max': 256, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 4 }, 'ssd_gb': - { 'min': 10, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 0.35 + { 'min': 10 }, 'hdd_gb': - { 'min': 0, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 15/1000 + { 'min': 0 }, 'additional_ipv4_address': - { 'min': 0, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 9 - }, + { 'min': 0,}, } } ) @@ -433,7 +389,7 @@ class Product(models.Model): @property def recurring_orders(self): - return self.orders.order_by('id').exclude(recurring_period=RecurringPeriod.objects.get(name="ONE_TIME")) + return self.orders.order_by('id').exclude(recurring_price=0) @property def last_recurring_order(self): @@ -441,56 +397,12 @@ class Product(models.Model): @property def one_time_orders(self): - return self.orders.order_by('id').filter(recurring_period=RecurringPeriod.objects.get(name="ONE_TIME")) + return self.orders.order_by('id').filter(recurring_price=0) @property def last_one_time_order(self): return self.one_time_orders.last() - def create_order(self, when_to_start=None, recurring_period=None): - billing_address = BillingAddress.get_address_for(self.owner) - - if not billing_address: - raise ValidationError("Cannot order without a billing address") - - if not when_to_start: - when_to_start = timezone.now() - - if not recurring_period: - recurring_period = self.default_recurring_period - - - # Create one time order if we did not create one already - if self.one_time_price > 0 and not self.last_one_time_order: - one_time_order = Order.objects.create(owner=self.owner, - billing_address=billing_address, - starting_date=when_to_start, - price=self.one_time_price, - recurring_period=RecurringPeriod.objects.get(name="ONE_TIME"), - description=str(self)) - self.orders.add(one_time_order) - else: - one_time_order = None - - if recurring_period != RecurringPeriod.objects.get(name="ONE_TIME"): - if one_time_order: - recurring_order = Order.objects.create(owner=self.owner, - billing_address=billing_address, - starting_date=when_to_start, - price=self.recurring_price, - recurring_period=recurring_period, - depends_on=one_time_order, - description=str(self)) - else: - recurring_order = Order.objects.create(owner=self.owner, - billing_address=billing_address, - starting_date=when_to_start, - price=self.recurring_price, - recurring_period=recurring_period, - description=str(self)) - self.orders.add(recurring_order) - - # FIXME: this could/should be part of Order (?) def create_or_update_recurring_order(self, when_to_start=None, recurring_period=None): if not self.recurring_price: @@ -618,10 +530,83 @@ class Product(models.Model): super().save(*args, **kwargs) +### +# Pricing +###### +import logging + +from django.db import models + +logger = logging.getLogger(__name__) + +class PricingPlan(models.Model): + name = models.CharField(max_length=255, unique=True) + vat_inclusive = models.BooleanField(default=True) + vat_percentage = models.DecimalField( + max_digits=7, decimal_places=5, blank=True, default=0 + ) + set_up_fees = models.DecimalField( + max_digits=7, decimal_places=2, default=0 + ) + cores_unit_price = models.DecimalField( + max_digits=7, decimal_places=2, default=0 + ) + ram_unit_price = models.DecimalField( + max_digits=7, decimal_places=2, default=0 + ) + storage_unit_price = models.DecimalField( + max_digits=7, decimal_places=2, default=0 + ) + discount_name = models.CharField(max_length=255, null=True, blank=True) + discount_amount = models.DecimalField( + max_digits=6, decimal_places=2, default=0 + ) + stripe_coupon_id = models.CharField(max_length=255, null=True, blank=True) + + def __str__(self): + display_str = self.name + ' => ' + ' - '.join([ + '{} Setup'.format(self.set_up_fees.normalize()), + '{}/Core'.format(self.cores_unit_price.normalize()), + '{}/GB RAM'.format(self.ram_unit_price.normalize()), + '{}/GB SSD'.format(self.storage_unit_price.normalize()), + '{}% VAT'.format(self.vat_percentage.normalize()) + if not self.vat_inclusive else 'VAT-Incl', + ]) + if self.discount_amount: + display_str = ' - '.join([ + display_str, + '{} {}'.format( + self.discount_amount, + self.discount_name if self.discount_name else 'Discount' + ) + ]) + return display_str + + @classmethod + def get_by_name(cls, name): + try: + pricing = PricingPlan.objects.get(name=name) + except Exception as e: + logger.error( + "Error getting VMPricing with name {name}. " + "Details: {details}. Attempting to return default" + "pricing.".format(name=name, details=str(e)) + ) + pricing = PricingPlan.get_default_pricing() + return pricing + + @classmethod + def get_default_pricing(cls): + """ Returns the default pricing or None """ + try: + default_pricing = PricingPlan.objects.get(name='default') + except Exception as e: + logger.error(str(e)) + default_pricing = None + return default_pricing ### # Orders. - class Order(models.Model): """ Order are assumed IMMUTABLE and used as SOURCE OF TRUST for generating @@ -650,6 +635,8 @@ class Order(models.Model): billing_address = models.ForeignKey(BillingAddress, on_delete=models.CASCADE) + customer = models.ForeignKey(StripeCustomer, on_delete=models.CASCADE, null=True) + description = models.TextField() product = models.ForeignKey(Product, blank=False, null=False, on_delete=models.CASCADE) @@ -686,6 +673,7 @@ class Order(models.Model): on_delete=models.CASCADE, blank=True, null=True) + pricing_plan = models.ForeignKey(PricingPlan, blank=False, null=True, on_delete=models.CASCADE) should_be_billed = models.BooleanField(default=True) @@ -750,6 +738,17 @@ class Order(models.Model): """ return sum([ br.quantity for br in self.bill_records.all() ]) + + def cancel(self): + self.ending_date = timezone.now() + self.should_be_billed = False + self.save() + if self.instance_id: + last_bill_record = BillRecord.objects.filter(order=self).order_by('id').last() + schedule('matrixhosting.tasks.delete_instance', + self.instance_id, + schedule_type=Schedule.ONCE, + next_run=last_bill_record.ending_date or (timezone.now() + datetime.timedelta(hours=1))) def count_used(self, when=None): """ @@ -790,7 +789,7 @@ class Order(models.Model): @property def is_recurring(self): - return not self.recurring_period == RecurringPeriod.objects.get(name="ONE_TIME") + return self.recurring_price > 0 @property def is_one_time(self): @@ -814,14 +813,12 @@ class Order(models.Model): description=self.description, product=self.product, config=config, + pricing_plan=self.pricing_plan, starting_date=starting_date, currency=self.currency ) - (new_order.one_time_price, new_order.recurring_price, new_order.config) = new_order.calculate_prices_and_config() - - - + new_order.recurring_price = new_order.calculate_recurring_price() new_order.replaces = self new_order.save() @@ -830,26 +827,28 @@ class Order(models.Model): return new_order - + def create_bill_record(self, bill): br = None - # Note: check for != 0 not > 0, as we allow discounts to be expressed with < 0 - if self.one_time_price != 0 and self.billrecord_set.count() == 0: - br = BillRecord.objects.create(bill=bill, - order=self, - starting_date=self.starting_date, - ending_date=self.starting_date, - is_recurring_record=False) - if self.recurring_price != 0: - br = BillRecord.objects.filter(bill=bill, order=self, is_recurring_record=True).first() - - if br: - self.update_bill_record_for_recurring_order(br, bill) + records = BillRecord.objects.filter(order=self).all() + if not records: + if self.one_time_price: + br = BillRecord.objects.create(bill=bill, + order=self, + starting_date=self.starting_date, + ending_date=bill.ending_date, + is_recurring_record=False) + else: + br = self.create_new_bill_record_for_recurring_order(bill) else: - br = self.create_new_bill_record_for_recurring_order(bill) - + opened_recurring_record = BillRecord.objects.filter(bill=bill, order=self, is_recurring_record=True).first() + if opened_recurring_record: + br = opened_recurring_record + self.update_bill_record_for_recurring_order(br, bill) + else: + br = self.create_new_bill_record_for_recurring_order(bill) return br def update_bill_record_for_recurring_order(self, @@ -861,22 +860,21 @@ class Order(models.Model): # If the order has an ending date set, we might need to adjust the bill_record if self.ending_date: - if bill_record_for_this_bill.ending_date != self.ending_date: - bill_record_for_this_bill.ending_date = self.ending_date + if bill_record.ending_date != self.ending_date: + bill_record.ending_date = self.ending_date else: # recurring, not terminated, should go until at least end of bill - if bill_record_for_this_bill.ending_date < bill.ending_date: - bill_record_for_this_bill.ending_date = bill.ending_date + if bill_record.ending_date < bill.ending_date: + bill_record.ending_date = bill.ending_date - bill_record_for_this_bill.save() + bill_record.save() def create_new_bill_record_for_recurring_order(self, bill): """ Create a new bill record """ - - last_bill_record = BillRecord.objects.filter(order=self, is_recurring_record=True).order_by('id').last() + last_bill_record = BillRecord.objects.filter(order=self).order_by('id').last() starting_date=self.starting_date @@ -892,7 +890,6 @@ class Order(models.Model): return starting_date = start_after(last_bill_record.ending_date) - ending_date = self.get_ending_date_for_bill(bill) return BillRecord.objects.create(bill=bill, @@ -901,47 +898,27 @@ class Order(models.Model): ending_date=ending_date, is_recurring_record=True) - def calculate_prices_and_config(self): - one_time_price = 0 - recurring_price = 0 + def calculate_recurring_price(self): + try: + config = json.loads(self.config) + recurring_price = 0 + if 'cores' in config: + recurring_price += self.pricing_plan.cores_unit_price * int(config['cores']) + if 'memory' in config: + recurring_price += self.pricing_plan.ram_unit_price * int(config['memory']) + if 'storage' in config: + recurring_price += self.pricing_plan.storage_unit_price * int(config['storage']) - if self.config: - config = self.config - - if 'features' not in self.config: - self.config['features'] = {} - - else: - config = { - 'features': {} - } - - # FIXME: adjust prices to the selected recurring_period to the - - if 'features' in self.product.config: - for feature in self.product.config['features']: - - # Set min to 0 if not specified - min_val = self.product.config['features'][feature].get('min', 0) - - # We might not even have 'features' cannot use .get() on it - try: - value = self.config['features'][feature] - except (KeyError, TypeError): - value = self.product.config['features'][feature]['min'] - - # Set max to current value if not specified - max_val = self.product.config['features'][feature].get('max', value) - - - if value < min_val or value > max_val: - raise ValidationError(f"Feature '{feature}' must be at least {min_val} and at maximum {max_val}. Value is: {value}") - - one_time_price += self.product.config['features'][feature]['one_time_price_per_unit'] * value - recurring_price += self.product.config['features'][feature]['recurring_price_per_unit'] * value - config['features'][feature] = value - - return (one_time_price, recurring_price, config) + vat_rate = VATRate.get_vat_rate(self.billing_address) + vat_validation_status = "verified" if self.billing_address.vat_number_validated_on and self.billing_address.vat_number_verified else False + subtotal, subtotal_after_discount, price_after_discount_with_vat, vat, vat_percent, discount = uncloud_pay.utils.apply_vat_discount( + recurring_price, self.pricing_plan, + vat_rate=vat_rate * 100, vat_validation_status = vat_validation_status + ) + return price_after_discount_with_vat + except Exception as e: + logger.error("An error occurred while parsing the config obj", e) + return 0 def check_parameters(self): if 'parameters' in self.product.config: @@ -955,7 +932,7 @@ class Order(models.Model): # IMMUTABLE fields -- need to create new order to modify them # However this is not enforced here... if self._state.adding: - (self.one_time_price, self.recurring_price, self.config) = self.calculate_prices_and_config() + self.recurring_price = self.calculate_recurring_price() if self.recurring_period_id is None: self.recurring_period = self.product.default_recurring_period @@ -975,12 +952,7 @@ class Order(models.Model): def __str__(self): - try: - conf = " ".join([ f"{key}:{val}" for key,val in self.config['features'].items() if val != 0 ]) - except KeyError: - conf = "" - - return f"Order {self.id}: {self.description} {conf}" + return f"Order {self.id}: {self.description}" class Bill(models.Model): """ @@ -1003,7 +975,7 @@ class Bill(models.Model): # FIXME: editable=True -> is in the admin, but also editable in DRF # Maybe filter fields in the serializer? - is_final = models.BooleanField(default=False) + is_closed = models.BooleanField(default=False) class Meta: constraints = [ @@ -1017,8 +989,9 @@ class Bill(models.Model): """ Close/finish a bill """ - - self.is_final = True + self.is_closed = True + if not self.ending_date: + self.ending_date = timezone.now() self.save() @property @@ -1028,34 +1001,7 @@ class Bill(models.Model): @property def vat_rate(self): - """ - Handling VAT is a tricky business - thus we only implement the cases - that we clearly now and leave it open to fellow developers to implement - correct handling for other cases. - - Case CH: - - - If the customer is in .ch -> apply standard rate - - If the customer is in EU AND private -> apply country specific rate - - If the customer is in EU AND business -> do not apply VAT - - If the customer is outside EU and outside CH -> do not apply VAT - """ - - provider = UncloudProvider.objects.get() - - # Assume always VAT inside the country - if provider.country == self.billing_address.country: - vat_rate = VATRate.objects.get(country=provider.country, - when=self.ending_date) - elif self.billing_address.country in EU: - # FIXME: need to check for validated vat number - if self.billing_address.vat_number: - return 0 - else: - return VATRate.objects.get(country=self.biling_address.country, - when=self.ending_date) - else: # non-EU, non-national - return 0 + return VATRate.get_vat_rate(self.billing_address, when=self.ending_date) @classmethod @@ -1075,9 +1021,10 @@ class Bill(models.Model): """ bills = [] - for billing_address in BillingAddress.objects.filter(owner=owner): - bills.append(cls.create_next_bill_for_user_address(billing_address, ending_date)) + bill = cls.create_next_bill_for_user_address(billing_address, ending_date) + if bill: + bills.append(bill) return bills @@ -1089,15 +1036,18 @@ class Bill(models.Model): owner = billing_address.owner - all_orders = Order.objects.filter(owner=owner, - billing_address=billing_address).order_by('id') - - bill = cls.get_or_create_bill(billing_address, ending_date=ending_date) - - for order in all_orders: - order.create_bill_record(bill) - - return bill + all_orders = Order.objects.filter(Q(owner__id=owner.id), Q(should_be_billed=True), + Q(billing_address__id=billing_address.id) + ).order_by('id') + + if len(all_orders) > 0: + bill = cls.get_or_create_bill(billing_address, ending_date=ending_date) + for order in all_orders: + order.create_bill_record(bill) + return bill + else: + # This Customer Hasn't any active orders + return False @classmethod @@ -1117,7 +1067,7 @@ class Bill(models.Model): # Get date & bill from previous bill, if it exists if last_bill: - if not last_bill.is_final: + if not last_bill.is_closed: bill = last_bill starting_date = last_bill.starting_date ending_date = bill.ending_date @@ -1142,7 +1092,7 @@ class Bill(models.Model): return bill - + def __str__(self): return f"{self.owner}-{self.id}" @@ -1167,9 +1117,11 @@ class BillRecord(models.Model): if not self.is_recurring_record: return 1 - record_delta = self.ending_date - self.starting_date - - return record_delta.total_seconds()/self.order.recurring_period.duration_seconds + record_delta = self.ending_date.date() - self.starting_date.date() + if self.order.recurring_period and self.order.recurring_period.duration_seconds > 0: + return int(record_delta.total_seconds() / self.order.recurring_period.duration_seconds) + else: + return 1 @property def sum(self): diff --git a/uncloud_pay/selectors.py b/uncloud_pay/selectors.py index 2a5ad4a..a634e30 100644 --- a/uncloud_pay/selectors.py +++ b/uncloud_pay/selectors.py @@ -1,9 +1,5 @@ from django.utils import timezone from django.db import transaction -from django.db.models import Q - -from uncloud.selectors import filter_for_when -from uncloud.models import UncloudProvider from .models import * def get_payments_for_user(user): @@ -12,12 +8,11 @@ def get_payments_for_user(user): return sum(payments) def get_spendings_for_user(user): - orders = Order.objects.filter(owner=user) + bills = Bill.objects.filter(owner=user) amount = 0 - for order in orders: - amount += order.one_time_price - amount += order.recurring_price * order.count_used(when=timezone.now()) + for bill in bills: + amount += bill.sum return amount @@ -25,34 +20,12 @@ def get_spendings_for_user(user): def get_balance_for_user(user): return get_payments_for_user(user) - get_spendings_for_user(user) +@transaction.atomic +def has_enough_balance(user, due_amount): + balance = get_balance_for_user(user) + if balance >= due_amount: + return True + return False + def get_billing_address_for_user(user): - return BillingAddress.objects.get(owner=user, active=True) - -def get_vat_rate(billing_address, when=None): - """ - Returns the VAT rate for business to customer. - - B2B is always 0% with the exception of trading within the own country - """ - - country = billing_address.country - - # Need to have a provider country - uncloud_provider = filter_for_when(UncloudProvider.objects.all()).get() - vatrate = filter_for_when(VATRate.objects.filter(territory_codes=country), when).first() - - # By default we charge VAT. This affects: - # - Same country sales (VAT applied) - # - B2C to EU (VAT applied) - rate = vatrate.rate - - # Exception: if... - # - the billing_address is in EU, - # - the vat_number has been set - # - the vat_number has been verified - # Then we do not charge VAT - - if uncloud_provider.country != country and billing_address.vat_number and billing_address.vat_number_verified: - rate = 0 - - return rate + return BillingAddress.objects.filter(owner=user, active=True).first() diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py index b5de192..4ea4104 100644 --- a/uncloud_pay/serializers.py +++ b/uncloud_pay/serializers.py @@ -86,8 +86,9 @@ class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order read_only_fields = ['replaced_by', 'depends_on'] - fields = ['uuid', 'owner', 'description', 'creation_date', 'starting_date', 'ending_date', - 'bill', 'recurring_period', 'recurring_price', 'one_time_price'] + read_only_fields + fields = ['owner', 'description', 'creation_date', 'starting_date', 'ending_date', + 'recurring_period', 'recurring_price', 'one_time_price', + 'config', 'pricing_plan', 'should_be_billed'] + read_only_fields ### @@ -114,13 +115,13 @@ class BillSerializer(serializers.ModelSerializer): class Meta: model = Bill - fields = ['uuid', 'reference', 'owner', 'amount', 'vat_amount', 'total', + fields = ['owner', 'sum', 'vat_rate', 'due_date', 'creation_date', 'starting_date', 'ending_date', - 'records', 'final', 'billing_address'] + 'records', 'is_closed', 'billing_address'] # We do not want users to mutate the country / VAT number of an address, as it # will change VAT on existing bills. class UpdateBillingAddressSerializer(serializers.ModelSerializer): class Meta: model = BillingAddress - fields = ['uuid', 'street', 'city', 'postal_code'] + fields = ['street', 'city', 'postal_code'] diff --git a/uncloud_pay/services.py b/uncloud_pay/services.py index ed97c39..84a7c8d 100644 --- a/uncloud_pay/services.py +++ b/uncloud_pay/services.py @@ -1,3 +1,5 @@ +import datetime +from calendar import monthrange from django.utils import timezone diff --git a/uncloud_pay/stripe.py b/uncloud_pay/stripe.py index ed95c82..a59456e 100644 --- a/uncloud_pay/stripe.py +++ b/uncloud_pay/stripe.py @@ -9,6 +9,8 @@ from django.contrib.auth import get_user_model from .models import StripeCustomer, StripeCreditCard +logger = logging.getLogger(__name__) + CURRENCY = 'chf' stripe.api_key = settings.STRIPE_KEY @@ -77,9 +79,24 @@ def create_setup_intent(customer_id): def get_setup_intent(setup_intent_id): return stripe.SetupIntent.retrieve(setup_intent_id) +@handle_stripe_error def get_payment_method(payment_method_id): return stripe.PaymentMethod.retrieve(payment_method_id) +@handle_stripe_error +def get_card_from_payment(user, payment_method_id): + payment_method = stripe.PaymentMethod.retrieve(payment_method_id) + if payment_method: + if 'card' in payment_method: + sync_cards_for_user(user) + return payment_method['card'] + return False + + +@handle_stripe_error +def attach_payment_method(payment_method_id, customer_id): + return stripe.PaymentMethod.attach(payment_method_id, customer=customer_id) + @handle_stripe_error def create_customer(name, email): return stripe.Customer.create(name=name, email=email) @@ -142,7 +159,7 @@ def sync_cards_for_user(user): ) @handle_stripe_error -def charge_customer(user, amount, currency='CHF'): +def charge_customer(user, amount, currency='CHF', card=False): # Amount is in CHF but stripes requires smallest possible unit. # https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount # FIXME: might need to be adjusted for other currencies @@ -153,14 +170,14 @@ def charge_customer(user, amount, currency='CHF'): return Exception("Programming error: unsupported currency") try: - card = StripeCreditCard.objects.get(owner=user, + card = card or StripeCreditCard.objects.get(owner=user, active=True) except StripeCreditCard.DoesNotExist: raise ValidationError("No active credit card - cannot create payment") customer_id = get_customer_id_for(user) - + return stripe.PaymentIntent.create( amount=adjusted_amount, currency=currency, @@ -169,3 +186,64 @@ def charge_customer(user, amount, currency='CHF'): off_session=True, confirm=True, ) + +@handle_stripe_error +def get_payment_intent(user, amount, currency='CHF', card=False): + # Amount is in CHF but stripes requires smallest possible unit. + # https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount + # FIXME: might need to be adjusted for other currencies + + if currency == 'CHF': + adjusted_amount = int(amount * 100) + else: + return Exception("Programming error: unsupported currency") + + try: + card = card or StripeCreditCard.objects.get(owner=user, + active=True) + + except StripeCreditCard.DoesNotExist: + raise ValidationError("No active credit card - cannot create payment") + + customer_id = get_customer_id_for(user) + + return stripe.PaymentIntent.create( + amount=adjusted_amount, + currency=currency, + customer=customer_id, + payment_method=card.card_id, + setup_future_usage='off_session', + confirm=False, + ) + +@handle_stripe_error +def get_or_create_tax_id_for_user(stripe_customer_id, vat_number, + type="eu_vat", country=""): + def compare_vat_numbers(vat1, vat2): + _vat1 = vat1.replace(" ", "").replace(".", "").replace("-","") + _vat2 = vat2.replace(" ", "").replace(".", "").replace("-","") + return True if _vat1 == _vat2 else False + + tax_ids_list = stripe.Customer.list_tax_ids( + stripe_customer_id, + limit=100, + ) + for tax_id_obj in tax_ids_list.data: + if compare_vat_numbers(tax_id_obj.value, vat_number): + return tax_id_obj + else: + logger.debug( + "{val1} is not equal to {val2} or {con1} not same as " + "{con2}".format(val1=tax_id_obj.value, val2=vat_number, + con1=tax_id_obj.country.lower(), + con2=country.lower().strip())) + logger.debug( + "tax id obj does not exist for {val}. Creating a new one".format( + val=vat_number + )) + tax_id_obj = stripe.Customer.create_tax_id( + stripe_customer_id, + type=type, + value=vat_number, + ) + return tax_id_obj diff --git a/uncloud_pay/tasks.py b/uncloud_pay/tasks.py deleted file mode 100644 index c372366..0000000 --- a/uncloud_pay/tasks.py +++ /dev/null @@ -1,11 +0,0 @@ -from celery import shared_task -from .models import * -import uuid - -from uncloud.models import UncloudTask - -@shared_task(bind=True) -def check_balance(self): - UncloudTask.objects.create(task_id=self.request.id) - print("for each user res is 50") - return 50 diff --git a/uncloud_pay/templates/uncloud_pay/register_stripe.html b/uncloud_pay/templates/uncloud_pay/register_stripe.html index 9613701..0eed76a 100644 --- a/uncloud_pay/templates/uncloud_pay/register_stripe.html +++ b/uncloud_pay/templates/uncloud_pay/register_stripe.html @@ -1,12 +1,11 @@ {% extends 'uncloud/base.html' %} - {% block bootstrap5_extra_head %} + {% endblock %} - {% block bootstrap5_content %}
    - + {% csrf_token %}

    Register Credit Card with Stripe

    @@ -18,10 +17,15 @@ -

    - - - + + +
    +
    + + +
    The card will be registered with stripe.