Files
platform-api/app/services/trial_monitor.py
Platform CI f9bfc3afbd init
2026-02-21 17:54:44 -05:00

97 lines
3.5 KiB
Python

"""
Trial Monitor — APScheduler job that runs every hour.
- Warns tenants 7 days before trial expiry
- Suspends vClusters when trial expires
- Deletes vClusters after a grace period (configurable)
"""
import logging
from datetime import datetime, timedelta
from sqlalchemy import select, update as sa_update
from ..core.database import get_session_factory
from ..models.tenant import Tenant, PlatformSetting, ProvisioningJob
from ..services import provisioner
log = logging.getLogger(__name__)
async def _get_setting(db, key: str, default: str) -> str:
row = await db.get(PlatformSetting, key)
return row.value if row else default
async def check_trials():
"""Called by APScheduler every hour."""
factory = get_session_factory()
async with factory() as db:
now = datetime.utcnow()
warn_days_str = await _get_setting(db, "trial_warning_days", "7")
warn_days = int(warn_days_str)
# ── Warn approaching expiry ───────────────────────────────
warn_before = now + timedelta(days=warn_days)
approaching = (await db.execute(
select(Tenant)
.where(
Tenant.status == "trial",
Tenant.trial_ends_at <= warn_before,
Tenant.trial_ends_at > now,
)
)).scalars().all()
for t in approaching:
days_left = max(0, (t.trial_ends_at - now).days)
log.info("Trial warning: %s expires in %d days", t.slug, days_left)
# TODO: send email notification
# ── Expire trials ─────────────────────────────────────────
expired = (await db.execute(
select(Tenant)
.where(
Tenant.status == "trial",
Tenant.trial_ends_at <= now,
Tenant.vcluster_status == "ready",
)
)).scalars().all()
for t in expired:
log.info("Trial expired: %s — suspending vCluster", t.slug)
await db.execute(
sa_update(Tenant)
.where(Tenant.id == t.id)
.values(status="suspended", vcluster_status="suspended")
)
db.add(ProvisioningJob(
tenant_id=t.id,
job_type="suspend_vcluster",
payload={"slug": t.slug, "reason": "trial_expired"},
))
await db.commit()
# ── Delete suspended vclusters after grace period (7 days) ─
grace_days = int(await _get_setting(db, "suspension_grace_days", "7"))
grace_before = now - timedelta(days=grace_days)
old_suspended = (await db.execute(
select(Tenant)
.where(
Tenant.status == "suspended",
Tenant.updated_at <= grace_before,
Tenant.vcluster_status == "suspended",
)
)).scalars().all()
for t in old_suspended:
log.info("Grace period ended: %s — deprovisioning", t.slug)
try:
await provisioner.deprovision_tenant(t.slug)
await db.execute(
sa_update(Tenant)
.where(Tenant.id == t.id)
.values(vcluster_status="deleted")
)
except Exception as exc:
log.error("Failed to deprovision %s: %s", t.slug, exc)
await db.commit()