Files
HyperNode-AI/app/services/datacenter.py
T

144 lines
5.4 KiB
Python

"""机房大客户申请,以及网段内禁止混入 10 系显卡、硬件指纹去重。"""
from __future__ import annotations
import re
from sqlalchemy import case, select
from sqlalchemy.orm import Session, selectinload
from app.clock import utcnow
from app.errors import AppError
from app.models import ComputeNode, DatacenterApplication, DatacenterStatus
from app.services.hardware import GENERATION_10, GENERATION_30, GENERATION_40, classify_generation
from app.services.ip_guard import DATACENTER_BATCH_MESSAGE, add_whitelist_ip, matching_whitelist, normalize_admin_ip
MIN_DATACENTER_NODES = 5
FINGERPRINT_CLASH = "硬件指纹重复,疑似虚拟机克隆或刷量"
_FINGERPRINT = re.compile(r"^[a-f0-9]{64}$")
_LOW_END = re.compile(r"10\s*系|20\s*系|GTX|核显|1050|1060|1070|1080|1650|1660|2060|2070|2080", re.IGNORECASE)
def assert_fingerprint(session: Session, fingerprint: str | None, node_id: int | None = None) -> str | None:
"""同一块物理机不能挂第二台节点。空指纹不占唯一索引。"""
text = (fingerprint or "").strip().lower()
if not text:
return None
if _FINGERPRINT.fullmatch(text) is None:
raise AppError("硬件指纹格式不正确")
query = select(ComputeNode.id).where(ComputeNode.hardware_fingerprint == text)
if node_id is not None:
query = query.where(ComputeNode.id != node_id)
if session.scalar(query) is not None:
raise AppError(FINGERPRINT_CLASH, 409)
return text
def assert_subnet_gpu(session: Session, ip: str, gpu_model: str | None) -> None:
"""已经批准的机房网段里,不允许再挂 10 系及更低的卡。"""
address = (ip or "").strip()
if not address or "/" in address:
return
entry = matching_whitelist(session, address)
if entry is None or "/" not in entry:
return
model = (gpu_model or "").strip()
if not model:
return
generation, _days = classify_generation(model)
if generation == GENERATION_10:
raise AppError(DATACENTER_BATCH_MESSAGE, 409)
def submit_application(
session: Session,
user_id: int,
contact: str,
node_count: int,
subnet_ip: str,
hardware_proof: str,
gpu_generation: str,
) -> DatacenterApplication:
subnet = _require_subnet(subnet_ip)
_require_threshold(node_count, gpu_generation, hardware_proof)
existing = session.scalar(
select(DatacenterApplication.id).where(
DatacenterApplication.subnet_ip == subnet,
DatacenterApplication.status.in_((DatacenterStatus.PENDING, DatacenterStatus.APPROVED)),
)
)
if existing is not None:
raise AppError("这个网段已有待审核或已通过的机房申请", 409)
row = DatacenterApplication(
user_id=user_id,
contact=contact.strip(),
node_count=node_count,
subnet_ip=subnet,
hardware_proof=hardware_proof.strip(),
gpu_generation=gpu_generation,
status=DatacenterStatus.PENDING,
created_at=utcnow(),
)
session.add(row)
session.flush()
return row
def list_applications(session: Session, user_id: int | None = None) -> list[DatacenterApplication]:
pending_first = case((DatacenterApplication.status == DatacenterStatus.PENDING, 0), else_=1)
query = select(DatacenterApplication).options(selectinload(DatacenterApplication.user))
if user_id is not None:
query = query.where(DatacenterApplication.user_id == user_id)
return list(session.scalars(query.order_by(pending_first, DatacenterApplication.id.desc())).all())
def approve_application(session: Session, application_id: int) -> DatacenterApplication:
row = _pending(session, application_id)
_require_threshold(row.node_count, row.gpu_generation, row.hardware_proof)
try:
add_whitelist_ip(session, row.subnet_ip, note=f"机房申请 #{row.id} {row.contact}")
except AppError as exc:
if exc.status_code != 409:
raise
row.status = DatacenterStatus.APPROVED
row.reviewed_at = utcnow()
session.flush()
return row
def reject_application(session: Session, application_id: int, note: str | None = None) -> DatacenterApplication:
row = _pending(session, application_id)
row.status = DatacenterStatus.REJECTED
row.note = (note or "").strip() or None
row.reviewed_at = utcnow()
session.flush()
return row
def _pending(session: Session, application_id: int) -> DatacenterApplication:
row = session.scalar(
select(DatacenterApplication)
.options(selectinload(DatacenterApplication.user))
.where(DatacenterApplication.id == application_id)
)
if row is None:
raise AppError("机房申请不存在", 404)
if row.status != DatacenterStatus.PENDING:
raise AppError("只有待审核的机房申请可以处理", 409)
return row
def _require_subnet(value: str) -> str:
text = value.strip()
if "/" not in text:
raise AppError("请填写机房出口网段,例如 203.0.113.0/24")
return normalize_admin_ip(text)
def _require_threshold(node_count: int, gpu_generation: str, hardware_proof: str) -> None:
generation = (gpu_generation or "").strip()
if node_count < MIN_DATACENTER_NODES or generation not in {GENERATION_30, GENERATION_40}:
raise AppError(DATACENTER_BATCH_MESSAGE, 400)
if _LOW_END.search(hardware_proof or ""):
raise AppError(DATACENTER_BATCH_MESSAGE, 400)