letsbe-hub/app/models/instance.py

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