138 lines
3.5 KiB
Python
138 lines
3.5 KiB
Python
"""Instance model - represents a deployed orchestrator with licensing."""
|
|
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING, Optional
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, Integer, String
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.models.base import Base, TimestampMixin, UUIDMixin
|
|
|
|
if TYPE_CHECKING:
|
|
from app.models.client import Client
|
|
|
|
|
|
class Instance(UUIDMixin, TimestampMixin, Base):
|
|
"""
|
|
A deployed orchestrator instance with licensing.
|
|
|
|
Each instance is tied to a client and requires a valid license to operate.
|
|
The Hub issues license keys and tracks activation status.
|
|
"""
|
|
|
|
__tablename__ = "instances"
|
|
|
|
# Client relationship
|
|
client_id: Mapped[UUID] = mapped_column(
|
|
ForeignKey("clients.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
|
|
# Instance identification
|
|
instance_id: Mapped[str] = mapped_column(
|
|
String(255),
|
|
unique=True,
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
# e.g., "acme-orchestrator"
|
|
|
|
# === LICENSING ===
|
|
license_key_hash: Mapped[str] = mapped_column(
|
|
String(64),
|
|
nullable=False,
|
|
)
|
|
# SHA-256 hash of the license key (lb_inst_...)
|
|
|
|
license_key_prefix: Mapped[str] = mapped_column(
|
|
String(12),
|
|
nullable=False,
|
|
)
|
|
# First 12 chars for display: "lb_inst_abc1"
|
|
|
|
license_status: Mapped[str] = mapped_column(
|
|
String(50),
|
|
default="active",
|
|
nullable=False,
|
|
)
|
|
# "active", "suspended", "expired", "revoked"
|
|
|
|
license_issued_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=False,
|
|
)
|
|
|
|
license_expires_at: Mapped[Optional[datetime]] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=True,
|
|
)
|
|
# None = no expiry (perpetual)
|
|
|
|
# === ACTIVATION STATE ===
|
|
activated_at: Mapped[Optional[datetime]] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=True,
|
|
)
|
|
# Set when instance first calls /activate
|
|
|
|
last_activation_at: Mapped[Optional[datetime]] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=True,
|
|
)
|
|
# Updated on each activation call
|
|
|
|
activation_count: Mapped[int] = mapped_column(
|
|
Integer,
|
|
default=0,
|
|
nullable=False,
|
|
)
|
|
|
|
# === TELEMETRY ===
|
|
hub_api_key_hash: Mapped[Optional[str]] = mapped_column(
|
|
String(64),
|
|
nullable=True,
|
|
)
|
|
# Generated on activation, used for telemetry auth
|
|
|
|
# === METADATA ===
|
|
region: Mapped[Optional[str]] = mapped_column(
|
|
String(50),
|
|
nullable=True,
|
|
)
|
|
# e.g., "eu-west-1"
|
|
|
|
version: Mapped[Optional[str]] = mapped_column(
|
|
String(50),
|
|
nullable=True,
|
|
)
|
|
# Last reported orchestrator version
|
|
|
|
last_seen_at: Mapped[Optional[datetime]] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=True,
|
|
)
|
|
# Last telemetry or heartbeat
|
|
|
|
status: Mapped[str] = mapped_column(
|
|
String(50),
|
|
default="pending",
|
|
nullable=False,
|
|
)
|
|
# "pending" (created, not yet activated), "active", "inactive", "suspended"
|
|
|
|
# Relationships
|
|
client: Mapped["Client"] = relationship(back_populates="instances")
|
|
|
|
def is_license_valid(self) -> bool:
|
|
"""Check if the license is currently valid."""
|
|
from app.models.base import utc_now
|
|
|
|
if self.license_status not in ("active",):
|
|
return False
|
|
|
|
if self.license_expires_at and self.license_expires_at < utc_now():
|
|
return False
|
|
|
|
return True
|