"""Seed / upsert the "AWS SG (Singapore)" cloud integration. Idempotent: creates the integration the first time, refreshes credentials on subsequent runs. Works against both local MySQL and TiDB Cloud — whichever ``DATABASE_URL`` points at. **Never commits AK/SK to git.** Credentials come from env: AWS_SG_ACCESS_KEY_ID=AKIA... AWS_SG_SECRET_ACCESS_KEY=... [optional] AWS_SG_DEFAULT_REGION=ap-southeast-1 [optional] AWS_SG_SLUG=aws-sg [optional] AWS_SG_NAME="AWS SG (Singapore)" Run from the server/ dir so the app package is on the path: cd server source ../.venv/bin/activate # or wherever your env is AWS_SG_ACCESS_KEY_ID=... AWS_SG_SECRET_ACCESS_KEY=... \\ CREDENTIAL_ENCRYPTION_KEY=... \\ python ../scripts/seed_aws_sg.py For production: ssh prod-host cd /srv/helix/server AWS_SG_ACCESS_KEY_ID= AWS_SG_SECRET_ACCESS_KEY= \\ python ../scripts/seed_aws_sg.py The script creates or updates exactly one row. The per-capability ``scheduled_jobs`` rows get auto-provisioned by ``IntegrationService.create()`` / ``update()`` — no separate seed needed for them. """ from __future__ import annotations import asyncio import os import sys import uuid from pathlib import Path def _ensure_path() -> None: here = Path(__file__).resolve() server = here.parent.parent / "server" if server.exists() and str(server) not in sys.path: sys.path.insert(0, str(server)) _ensure_path() async def _main() -> None: ak = os.environ.get("AWS_SG_ACCESS_KEY_ID", "").strip() sk = os.environ.get("AWS_SG_SECRET_ACCESS_KEY", "").strip() if not ak or not sk: print( "error: AWS_SG_ACCESS_KEY_ID and AWS_SG_SECRET_ACCESS_KEY must be set", file=sys.stderr, ) sys.exit(2) region = os.environ.get("AWS_SG_DEFAULT_REGION", "ap-southeast-1").strip() or "ap-southeast-1" slug = os.environ.get("AWS_SG_SLUG", "aws-sg").strip() or "aws-sg" name = os.environ.get("AWS_SG_NAME", "AWS SG (Singapore)").strip() or "AWS SG (Singapore)" from sqlalchemy import select from app.core.database import async_session_factory from app.integrations.models import Integration from app.integrations.service import IntegrationService async with async_session_factory() as session: existing = (await session.execute( select(Integration).where(Integration.slug == slug) )).scalar_one_or_none() # We need an actor_user_id for the audit log; reuse the first # it_admin we can find. In fresh envs there's always at least # one from the seed. from app.identity.person_models import IdentityPerson actor_row = (await session.execute( select(IdentityPerson).limit(1) )).scalar_one_or_none() actor_id = actor_row.id if actor_row else uuid.uuid4() service = IntegrationService(session) config = { "access_key_id": ak, "secret_access_key": sk, "default_region": region, } trace_id = f"seed-aws-sg-{uuid.uuid4().hex[:12]}" if existing is None: print(f"[seed] creating cloud integration slug={slug!r}") resp = await service.create( kind="cloud", provider="aws", name=name, slug=slug, description=( "AWS Singapore 租户 — CMDB 同步 + IAM 撤销 + " "Bedrock API Key 审批通过后自动发 IAM 用户。" ), is_primary=False, enabled=True, capabilities=[ "servers", "domains", "dns_records", "load_balancers", "cdn", "object_storage", "iam", ], config=config, trace_id=trace_id, actor_user_id=actor_id, ) else: print(f"[seed] updating cloud integration slug={slug!r}") resp = await service.update( existing.slug, name=name, description=None, enabled=True, is_primary=None, capabilities=[ "servers", "domains", "dns_records", "load_balancers", "cdn", "object_storage", "iam", ], config=config, trace_id=trace_id, actor_user_id=actor_id, ) await session.commit() print(f"[seed] done. integration_id={resp.id}") print( "[seed] scheduled_jobs auto-provisioned for each capability + " "monthly billing. Verify at /admin/scheduled-jobs." ) if __name__ == "__main__": asyncio.run(_main())