"""Seed / upsert the Azure 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 client secrets to git.** Credentials come from env: AZURE_TENANT_ID=... AZURE_CLIENT_ID=... AZURE_CLIENT_SECRET=... [optional] AZURE_SLUG=azure-primary [optional] AZURE_NAME="Microsoft Azure" Run from the server/ dir: cd server source ../.venv/bin/activate AZURE_TENANT_ID=... AZURE_CLIENT_ID=... AZURE_CLIENT_SECRET=... \\ CREDENTIAL_ENCRYPTION_KEY=... \\ python ../scripts/seed_azure.py The script creates or updates exactly one row. Per-capability ``scheduled_jobs`` rows are auto-provisioned by ``IntegrationService.create/update``. """ 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: tenant = os.environ.get("AZURE_TENANT_ID", "").strip() client_id = os.environ.get("AZURE_CLIENT_ID", "").strip() client_secret = os.environ.get("AZURE_CLIENT_SECRET", "").strip() if not tenant or not client_id or not client_secret: print( "error: AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET must be set", file=sys.stderr, ) sys.exit(2) slug = os.environ.get("AZURE_SLUG", "azure-primary").strip() or "azure-primary" name = os.environ.get("AZURE_NAME", "Microsoft Azure").strip() or "Microsoft Azure" capabilities = ["servers", "object_storage", "user_revoke"] from sqlalchemy import select import app.integrations.drivers # noqa: F401 — register @register_driver from app.auth.models import AuthUser # noqa: F401 — imported for ORM setup from app.core.database import async_session_factory from app.identity.person_models import IdentityPerson 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() actor_row = ( await session.execute(select(IdentityPerson).limit(1)) ).scalar_one_or_none() actor_id = actor_row.id if actor_row else uuid.uuid4() config = { "tenant_id": tenant, "client_id": client_id, "client_secret": client_secret, } trace_id = f"seed-azure-{uuid.uuid4().hex[:12]}" service = IntegrationService(session) if existing is None: print(f"[seed] creating cloud integration slug={slug!r}") resp = await service.create( kind="cloud", provider="azure", name=name, slug=slug, description=( "Azure 主租户 — VM / Storage 同步 + Subscription / AAD " "生命周期 + 离职全订阅撤销。" ), is_primary=False, enabled=True, capabilities=capabilities, 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=capabilities, 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. " "Verify at /admin/scheduled-jobs." ) if __name__ == "__main__": asyncio.run(_main())