Add migrations and final model changes

This commit is contained in:
Tobias Kunze 2025-12-02 16:09:10 +01:00
parent 5cee0194f5
commit 2bbd643cf9
4 changed files with 411 additions and 40 deletions

View file

@ -0,0 +1,309 @@
# Generated by Django 5.2.8 on 2025-12-02 09:51
from decimal import Decimal
import django.core.validators
import django.db.models.deletion
import rules.contrib.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0015_add_hide_expert_mode_to_service_definition"),
]
operations = [
migrations.CreateModel(
name="ComputePlan",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created"),
),
(
"updated_at",
models.DateTimeField(auto_now=True, verbose_name="Last updated"),
),
("name", models.CharField(max_length=100, verbose_name="Name")),
(
"description",
models.TextField(blank=True, verbose_name="Description"),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Whether this plan is available for selection",
verbose_name="Is active",
),
),
(
"memory_requests",
models.CharField(
max_length=20,
verbose_name="Memory requests",
),
),
(
"memory_limits",
models.CharField(
max_length=20,
verbose_name="Memory limits",
),
),
(
"cpu_requests",
models.CharField(
max_length=20,
verbose_name="CPU requests",
),
),
(
"cpu_limits",
models.CharField(
max_length=20,
verbose_name="CPU limits",
),
),
],
options={
"verbose_name": "Compute Plan",
"verbose_name_plural": "Compute Plans",
"ordering": ["name"],
},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.AddField(
model_name="controlplane",
name="storage_plan_odoo_product_id",
field=models.IntegerField(
blank=True,
help_text="ID of the storage product in Odoo",
null=True,
verbose_name="Storage plan Odoo product ID",
),
),
migrations.AddField(
model_name="controlplane",
name="storage_plan_odoo_unit_id",
field=models.IntegerField(
blank=True,
help_text="ID of the unit of measure in Odoo (uom.uom)",
null=True,
verbose_name="Storage plan Odoo unit ID",
),
),
migrations.AddField(
model_name="controlplane",
name="storage_plan_price_per_gib",
field=models.DecimalField(
blank=True,
decimal_places=2,
help_text="Price per GiB of storage",
max_digits=10,
null=True,
verbose_name="Storage plan price per GiB",
),
),
migrations.CreateModel(
name="ComputePlanAssignment",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created"),
),
(
"updated_at",
models.DateTimeField(auto_now=True, verbose_name="Last updated"),
),
(
"sla",
models.CharField(
choices=[
("besteffort", "Best Effort"),
("guaranteed", "Guaranteed Availability"),
],
help_text="Service Level Agreement",
max_length=20,
verbose_name="SLA",
),
),
(
"odoo_product_id",
models.IntegerField(
help_text="ID of the product in Odoo (product.product or product.template)",
verbose_name="Odoo product ID",
),
),
(
"odoo_unit_id",
models.IntegerField(
help_text="ID of the unit of measure in Odoo (uom.uom)",
verbose_name="Odoo unit ID",
),
),
(
"price",
models.DecimalField(
decimal_places=2,
help_text="Price per unit",
max_digits=10,
validators=[
django.core.validators.MinValueValidator(Decimal("0.00"))
],
verbose_name="Price",
),
),
(
"minimum_service_size",
models.PositiveIntegerField(
default=1,
help_text="Minimum value for spec.parameters.instances (Guaranteed Availability may require multiple instances)",
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="Minimum service size",
),
),
(
"sort_order",
models.PositiveIntegerField(
default=0,
help_text="Order in which plans are displayed to users",
verbose_name="Sort order",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Whether this plan is available for this CRD",
verbose_name="Is active",
),
),
(
"compute_plan",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="assignments",
to="core.computeplan",
verbose_name="Compute plan",
),
),
(
"control_plane_crd",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="compute_plan_assignments",
to="core.controlplanecrd",
verbose_name="Control plane CRD",
),
),
],
options={
"verbose_name": "Compute Plan Assignment",
"verbose_name_plural": "Compute Plan Assignments",
"ordering": ["sort_order", "compute_plan__name", "sla"],
"unique_together": {("compute_plan", "control_plane_crd", "sla")},
},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.AddField(
model_name="serviceinstance",
name="compute_plan_assignment",
field=models.ForeignKey(
blank=True,
help_text="Compute plan with SLA for this instance",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="instances",
to="core.computeplanassignment",
verbose_name="Compute plan assignment",
),
),
migrations.CreateModel(
name="OdooObjectCache",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created"),
),
(
"updated_at",
models.DateTimeField(auto_now=True, verbose_name="Last updated"),
),
(
"odoo_model",
models.CharField(
help_text="Odoo model name: 'product.product', 'product.template', 'uom.uom', etc.",
max_length=100,
verbose_name="Odoo model",
),
),
(
"odoo_id",
models.PositiveIntegerField(
help_text="ID in the Odoo model", verbose_name="Odoo ID"
),
),
(
"data",
models.JSONField(
help_text="Cached Odoo data including price, reporting_product_id, etc.",
verbose_name="Cached data",
),
),
(
"expires_at",
models.DateTimeField(
blank=True,
help_text="When cache should be refreshed (null = never expires)",
null=True,
verbose_name="Expires at",
),
),
],
options={
"verbose_name": "Odoo Object Cache",
"verbose_name_plural": "Odoo Object Caches",
"indexes": [
models.Index(
fields=["odoo_model", "odoo_id"],
name="core_odooob_odoo_mo_51e258_idx",
),
models.Index(
fields=["expires_at"], name="core_odooob_expires_8fc00b_idx"
),
],
"unique_together": {("odoo_model", "odoo_id")},
},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]

View file

@ -0,0 +1,68 @@
# Generated by Django 5.2.8 on 2025-12-02 10:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0016_computeplan_and_more"),
]
operations = [
migrations.AddField(
model_name="computeplanassignment",
name="unit",
field=models.CharField(
choices=[
("hour", "Hour"),
("day", "Day"),
("month", "Month (30 days)"),
("year", "Year"),
],
default="hour",
help_text="Unit for the price (e.g., price per hour)",
max_length=10,
verbose_name="Billing unit",
),
),
migrations.AlterField(
model_name="computeplanassignment",
name="odoo_product_id",
field=models.CharField(
help_text="Product ID in Odoo (e.g., 'openshift-exoscale-workervcpu-standard')",
max_length=255,
verbose_name="Odoo product ID",
),
),
migrations.AlterField(
model_name="computeplanassignment",
name="odoo_unit_id",
field=models.CharField(
max_length=255,
verbose_name="Odoo unit ID",
),
),
migrations.AlterField(
model_name="controlplane",
name="storage_plan_odoo_product_id",
field=models.CharField(
blank=True,
help_text="Storage product ID in Odoo",
max_length=255,
null=True,
verbose_name="Storage plan Odoo product ID",
),
),
migrations.AlterField(
model_name="controlplane",
name="storage_plan_odoo_unit_id",
field=models.CharField(
blank=True,
help_text="Unit of measure ID in Odoo",
max_length=255,
null=True,
verbose_name="Storage plan Odoo unit ID",
),
),
]

View file

@ -1,5 +1,6 @@
from decimal import Decimal from decimal import Decimal
from auditlog.registry import auditlog
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -29,26 +30,21 @@ class ComputePlan(ServalaModelMixin):
help_text=_("Whether this plan is available for selection"), help_text=_("Whether this plan is available for selection"),
) )
# Kubernetes resource specifications (use Kubernetes format: "2Gi", "500m")
memory_requests = models.CharField( memory_requests = models.CharField(
max_length=20, max_length=20,
verbose_name=_("Memory requests"), verbose_name=_("Memory requests"),
help_text=_("e.g., '2Gi', '512Mi'"),
) )
memory_limits = models.CharField( memory_limits = models.CharField(
max_length=20, max_length=20,
verbose_name=_("Memory limits"), verbose_name=_("Memory limits"),
help_text=_("e.g., '4Gi', '1Gi'"),
) )
cpu_requests = models.CharField( cpu_requests = models.CharField(
max_length=20, max_length=20,
verbose_name=_("CPU requests"), verbose_name=_("CPU requests"),
help_text=_("e.g., '500m', '1', '2'"),
) )
cpu_limits = models.CharField( cpu_limits = models.CharField(
max_length=20, max_length=20,
verbose_name=_("CPU limits"), verbose_name=_("CPU limits"),
help_text=_("e.g., '2000m', '2', '4'"),
) )
class Meta: class Meta:
@ -60,12 +56,6 @@ class ComputePlan(ServalaModelMixin):
return self.name return self.name
def get_resource_summary(self): def get_resource_summary(self):
"""
Get a human-readable summary of resources.
Returns:
String like "2 vCPU, 4Gi RAM"
"""
return f"{self.cpu_limits} vCPU, {self.memory_limits} RAM" return f"{self.cpu_limits} vCPU, {self.memory_limits} RAM"
@ -95,26 +85,23 @@ class ComputePlanAssignment(ServalaModelMixin):
related_name="compute_plan_assignments", related_name="compute_plan_assignments",
verbose_name=_("Control plane CRD"), verbose_name=_("Control plane CRD"),
) )
# Service Level Agreement
sla = models.CharField( sla = models.CharField(
max_length=20, max_length=20,
choices=SLA_CHOICES, choices=SLA_CHOICES,
verbose_name=_("SLA"), verbose_name=_("SLA"),
help_text=_("Service Level Agreement"), help_text=_("Service Level Agreement"),
) )
odoo_product_id = models.CharField(
# Odoo product reference max_length=255,
odoo_product_id = models.IntegerField(
verbose_name=_("Odoo product ID"), verbose_name=_("Odoo product ID"),
help_text=_("ID of the product in Odoo (product.product or product.template)"), help_text=_(
"Product ID in Odoo (e.g., 'openshift-exoscale-workervcpu-standard')"
),
) )
odoo_unit_id = models.IntegerField( odoo_unit_id = models.CharField(
max_length=255,
verbose_name=_("Odoo unit ID"), verbose_name=_("Odoo unit ID"),
help_text=_("ID of the unit of measure in Odoo (uom.uom)"),
) )
# Pricing
price = models.DecimalField( price = models.DecimalField(
max_digits=10, max_digits=10,
decimal_places=2, decimal_places=2,
@ -123,7 +110,20 @@ class ComputePlanAssignment(ServalaModelMixin):
help_text=_("Price per unit"), help_text=_("Price per unit"),
) )
# Service constraints BILLING_UNIT_CHOICES = [
("hour", _("Hour")),
("day", _("Day")),
("month", _("Month (30 days / 720 hours)")),
("year", _("Year")),
]
unit = models.CharField(
max_length=10,
choices=BILLING_UNIT_CHOICES,
default="hour",
verbose_name=_("Billing unit"),
help_text=_("Unit for the price (e.g., price per hour)"),
)
minimum_service_size = models.PositiveIntegerField( minimum_service_size = models.PositiveIntegerField(
default=1, default=1,
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
@ -133,15 +133,11 @@ class ComputePlanAssignment(ServalaModelMixin):
"(Guaranteed Availability may require multiple instances)" "(Guaranteed Availability may require multiple instances)"
), ),
) )
# Display ordering in UI
sort_order = models.PositiveIntegerField( sort_order = models.PositiveIntegerField(
default=0, default=0,
verbose_name=_("Sort order"), verbose_name=_("Sort order"),
help_text=_("Order in which plans are displayed to users"), help_text=_("Order in which plans are displayed to users"),
) )
# Allow per-assignment activation
is_active = models.BooleanField( is_active = models.BooleanField(
default=True, default=True,
verbose_name=_("Is active"), verbose_name=_("Is active"),
@ -158,15 +154,12 @@ class ComputePlanAssignment(ServalaModelMixin):
return f"{self.compute_plan.name} ({self.get_sla_display()}) → {self.control_plane_crd}" return f"{self.compute_plan.name} ({self.get_sla_display()}) → {self.control_plane_crd}"
def get_odoo_reporting_product_id(self): def get_odoo_reporting_product_id(self):
"""
Get the reporting product ID for this plan.
In the future, this will query Odoo based on invoicing policy.
For now, returns the product ID directly.
Returns:
The Odoo product ID to use for billing
"""
# TODO: Implement Odoo cache lookup when OdooObjectCache is integrated # TODO: Implement Odoo cache lookup when OdooObjectCache is integrated
# For now, just return the product ID # For now, just return the product ID
return self.odoo_product_id return self.odoo_product_id
auditlog.register(ComputePlan, exclude_fields=["updated_at"], serialize_data=True)
auditlog.register(
ComputePlanAssignment, exclude_fields=["updated_at"], serialize_data=True
)

View file

@ -170,18 +170,19 @@ class ControlPlane(ServalaModelMixin, models.Model):
), ),
) )
# Storage plan configuration (hardcoded per control plane) storage_plan_odoo_product_id = models.CharField(
storage_plan_odoo_product_id = models.IntegerField( max_length=255,
null=True, null=True,
blank=True, blank=True,
verbose_name=_("Storage plan Odoo product ID"), verbose_name=_("Storage plan Odoo product ID"),
help_text=_("ID of the storage product in Odoo"), help_text=_("Storage product ID in Odoo"),
) )
storage_plan_odoo_unit_id = models.IntegerField( storage_plan_odoo_unit_id = models.CharField(
max_length=255,
null=True, null=True,
blank=True, blank=True,
verbose_name=_("Storage plan Odoo unit ID"), verbose_name=_("Storage plan Odoo unit ID"),
help_text=_("ID of the unit of measure in Odoo (uom.uom)"), help_text=_("Unit of measure ID in Odoo"),
) )
storage_plan_price_per_gib = models.DecimalField( storage_plan_price_per_gib = models.DecimalField(
max_digits=10, max_digits=10,