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}"