uncloud/uncloud/models.py

210 lines
7.3 KiB
Python

from django.db import models
from django.db.models import JSONField, Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import FieldError
from uncloud import COUNTRIES
from .selectors import filter_for_when
class UncloudModel(models.Model):
"""
This class extends the standard model with an
extra_data field that can be used to include public,
but internal information.
For instance if you migrate from an existing virtualisation
framework to uncloud.
The extra_data attribute should be considered a hack and whenever
data is necessary for running uncloud, it should **not** be stored
in there.
"""
extra_data = JSONField(editable=False, blank=True, null=True)
class Meta:
abstract = True
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
class UncloudStatus(models.TextChoices):
PENDING = 'PENDING', _('Pending')
AWAITING_PAYMENT = 'AWAITING_PAYMENT', _('Awaiting payment')
BEING_CREATED = 'BEING_CREATED', _('Being created')
SCHEDULED = 'SCHEDULED', _('Scheduled') # resource selected, waiting for dispatching
ACTIVE = 'ACTIVE', _('Active')
MODIFYING = 'MODIFYING', _('Modifying') # Resource is being changed
DELETED = 'DELETED', _('Deleted') # Resource has been deleted
DISABLED = 'DISABLED', _('Disabled') # Is usable, but cannot be used for new things
UNUSABLE = 'UNUSABLE', _('Unusable'), # Has some kind of error
###
# General address handling
class CountryField(models.CharField):
def __init__(self, *args, **kwargs):
kwargs.setdefault('choices', COUNTRIES)
kwargs.setdefault('default', 'CH')
kwargs.setdefault('max_length', 2)
super().__init__(*args, **kwargs)
def get_internal_type(self):
return "CharField"
class UncloudAddress(models.Model):
full_name = models.CharField(max_length=256, null=False)
organization = models.CharField(max_length=256, blank=True, null=True)
street = models.CharField(max_length=256, null=False)
city = models.CharField(max_length=256, null=False)
postal_code = models.CharField(max_length=64)
country = CountryField(blank=False, null=False)
class Meta:
abstract = True
class UncloudValidTimeFrame(models.Model):
"""
A model that allows to limit validity of something to a certain
time frame. Used for versioning basically.
Logic:
"""
class Meta:
abstract = True
constraints = [
models.UniqueConstraint(fields=['owner'],
condition=models.Q(active=True),
name='one_active_card_per_user')
]
valid_from = models.DateTimeField(default=timezone.now, null=True, blank=True)
valid_to = models.DateTimeField(null=True, blank=True)
@classmethod
def get_current(cls, *args, **kwargs):
now = timezone.now()
# With both given
cls.objects.filter(valid_from__lte=now,
valid_to__gte=now)
# With to missing
cls.objects.filter(valid_from__lte=now,
valid_to__isnull=true)
# With from missing
cls.objects.filter(valid_from__isnull=true,
valid_to__gte=now)
# Both missing
cls.objects.filter(valid_from__isnull=true,
valid_to__gte=now)
###
# UncloudNetworks are used as identifiers - such they are a base of uncloud
class UncloudNetwork(models.Model):
"""
Storing IP networks
"""
network_address = models.GenericIPAddressField(null=False, unique=True)
network_mask = models.IntegerField(null=False,
validators=[MinValueValidator(0),
MaxValueValidator(128)]
)
description = models.CharField(max_length=256)
@classmethod
def populate_db_defaults(cls):
for net, desc in [
( "2a0a:e5c0:11::", "uncloud Billing" ),
( "2a0a:e5c0:11:1::", "uncloud Referral" ),
( "2a0a:e5c0:11:2::", "uncloud Coupon" )
]:
obj, created = cls.objects.get_or_create(network_address=net,
defaults= {
'network_mask': 64,
'description': desc
}
)
def save(self, *args, **kwargs):
if not ':' in self.network_address and self.network_mask > 32:
raise FieldError("Mask cannot exceed 32 for IPv4")
super().save(*args, **kwargs)
def __str__(self):
return f"{self.network_address}/{self.network_mask} {self.description}"
###
# Who is running / providing this instance of uncloud?
class UncloudProvider(UncloudAddress):
"""
A class resembling who is running this uncloud instance.
This might change over time so we allow starting/ending dates
This also defines the taxation rules.
starting/ending date define from when to when this is valid. This way
we can model address changes and have it correct in the bills.
"""
# Meta:
# FIXMe: only allow non overlapping time frames -- how to define this as a constraint?
starting_date = models.DateField()
ending_date = models.DateField(blank=True, null=True)
billing_network = models.ForeignKey(UncloudNetwork, related_name="uncloudproviderbill", on_delete=models.CASCADE)
referral_network = models.ForeignKey(UncloudNetwork, related_name="uncloudproviderreferral", on_delete=models.CASCADE)
coupon_network = models.ForeignKey(UncloudNetwork, related_name="uncloudprovidercoupon", on_delete=models.CASCADE)
@classmethod
def get_provider(cls, when=None):
"""
Find active provide at a certain time - if there was any
"""
return cls.objects.get(Q(starting_date__gte=when, ending_date__lte=when) |
Q(starting_date__gte=when, ending_date__isnull=True))
@classmethod
def populate_db_defaults(cls):
obj, created = cls.objects.get_or_create(full_name="ungleich glarus ag",
street="Bahnhofstrasse 1",
postal_code="8783",
city="Linthal",
country="CH",
starting_date=timezone.now(),
billing_network=UncloudNetwork.objects.get(description="uncloud Billing"),
referral_network=UncloudNetwork.objects.get(description="uncloud Referral"),
coupon_network=UncloudNetwork.objects.get(description="uncloud Coupon")
)
def __str__(self):
return f"{self.full_name} {self.country}"