97 lines
3.5 KiB
Python
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()
|