Files
TermiNexus 53af9884df refactor(msync): msync 整体搬迁至 SDK 并消除对 gvsds 的依赖
- msync 全部实现迁入 gvsdsdk(models/registry/ogm/transaction/auth +
  Django 集成层 middleware/sio/views/serializers/urls/admin/apps/migrations)
- 新增 gvsdsdk/secrets.py(secrets.ini 读取)与 serializer_base.py
  (PascalCase DRF 序列化器基类)
- msync 0001 迁移改用 gvsdsdk.models.UUIDField 并与模型 deep_deconstruct
  完全对齐(消除 makemigrations 漂移),dependencies 置空
- msync apps.py 移除 gvsds.app_config 软依赖,域名仅经环境变量/settings 解析
- 验证:屏蔽 gvsds 包后 50 个 gvsdsdk 子模块独立导入通过
2026-08-10 22:23:52 +08:00

264 lines
8.2 KiB
Python

"""模块:服务注册与发现
提供服务节点的注册、注销、发现、心跳等操作。"""
import uuid
import datetime
import logging
from gvsdsdk.msync.models import ServiceNode, _utcnow
logger = logging.getLogger(__name__)
class RegistryError(Exception):
pass
class ServiceRegistry:
"""服务注册中心——管理微服务的注册、发现与生命周期"""
SERVICE_TYPE_WORKER = 0
SERVICE_TYPE_MASTER = 1
DEFAULT_SCOPE = ['ogm:read', 'ogm:write', 'registry:read']
@staticmethod
def Register(name, domain, secret_hash, raw_secret=None, scope=None, service_type=0, metadata=None):
"""注册新的服务节点(已存在则验证密钥哈希并更新域信息)
Args:
name: 服务名称(唯一标识)
domain: 服务域名或 URL
secret_hash: 服务密钥的 SHA256 哈希(客户端计算,不再传输明文)
raw_secret: 服务原始密钥(用于加密存储,认证时需要)
scope: 权限范围列表
service_type: 服务类型(0=工作节点, 1=主节点)
metadata: 服务元数据
Returns:
ServiceNode 实例
Raises:
RegistryError: 注册失败
"""
import hashlib
# 密钥强制:raw_secret 与 secret_hash 必须自洽;
# 新服务节点必须携带明文密钥,否则后续 HMAC 认证无法完成
if raw_secret:
computed_hash = hashlib.sha256(raw_secret.encode('utf-8')).hexdigest()
if secret_hash and computed_hash != secret_hash:
raise RegistryError('服务密钥哈希不一致')
secret_hash = secret_hash or computed_hash
elif not secret_hash:
raise RegistryError('服务密钥缺失')
existing = ServiceNode.objects.filter(ServiceName=name).first()
if existing:
if not existing.ServiceSecretHash or existing.ServiceSecretHash != secret_hash:
raise RegistryError('服务密钥验证失败')
existing.ServiceDomain = domain
existing.ServiceScope = scope or ServiceRegistry.DEFAULT_SCOPE
existing.ServiceStatus = 1
existing.ServiceType = service_type
existing.ServiceMetadata = metadata or {}
if raw_secret:
existing.SetSecret(raw_secret)
existing.save()
logger.info(f'服务重新注册: {name} ({domain})')
return existing
if not raw_secret:
raise RegistryError('新服务注册必须提供 ServiceSecret 密钥')
node = ServiceNode(
ServiceUUID=uuid.uuid4().bytes,
ServiceName=name,
ServiceDomain=domain,
ServiceScope=scope or ServiceRegistry.DEFAULT_SCOPE,
ServiceStatus=1,
ServiceType=service_type,
ServiceMetadata=metadata or {},
ServiceSecretHash=secret_hash,
)
if raw_secret:
node.SetSecret(raw_secret)
node.save()
logger.info(f'服务注册成功: {name} ({domain})')
return node
@staticmethod
def Deregister(name):
"""注销服务节点(软删除,设置状态为禁用)
Args:
name: 服务名称
Returns:
True 如果成功
Raises:
RegistryError: 服务不存在
"""
try:
node = ServiceNode.objects.get(ServiceName=name)
except ServiceNode.DoesNotExist:
raise RegistryError(f'服务不存在: {name}')
node.ServiceStatus = 0
node.save(update_fields=['ServiceStatus'])
logger.info(f'服务已注销: {name}')
return True
@staticmethod
def Discover(name):
"""发现指定服务
Args:
name: 服务名称
Returns:
服务信息字典或 None
"""
try:
node = ServiceNode.objects.get(ServiceName=name, ServiceStatus=1)
except ServiceNode.DoesNotExist:
return None
return {
'ServiceName': node.ServiceName,
'ServiceDomain': node.ServiceDomain,
'ServiceScope': node.ServiceScope,
'ServiceType': node.ServiceType,
'ServiceMetadata': node.ServiceMetadata,
'ServiceStatus': node.ServiceStatus,
'LastHeartbeat': node.LastHeartbeat.isoformat() if node.LastHeartbeat else None,
}
@staticmethod
def GetNode(name):
"""获取服务节点实例(不按状态过滤,已注销也可查询);不存在返回 None"""
return ServiceNode.objects.filter(ServiceName=name).first()
@staticmethod
def DiscoverAll(active_only=True, page=1, page_size=50):
"""发现所有已注册服务(分页)
Args:
active_only: 是否只返回活跃服务
page: 页码(从1开始)
page_size: 每页数量
Returns:
包含分页元数据和服务列表的字典
"""
qs = ServiceNode.objects.all()
if active_only:
qs = qs.filter(ServiceStatus=1)
total = qs.count()
offset = (page - 1) * page_size
page_qs = qs[offset:offset + page_size]
services = [
{
'ServiceName': node.ServiceName,
'ServiceDomain': node.ServiceDomain,
'ServiceScope': node.ServiceScope,
'ServiceType': node.ServiceType,
'ServiceMetadata': node.ServiceMetadata,
'ServiceStatus': node.ServiceStatus,
'LastHeartbeat': node.LastHeartbeat.isoformat() if node.LastHeartbeat else None,
}
for node in page_qs
]
return {
'total': total,
'page': page,
'page_size': page_size,
'services': services,
}
@staticmethod
def Heartbeat(name):
"""服务心跳——更新最后心跳时间
Args:
name: 服务名称
Returns:
True 如果成功
Raises:
RegistryError: 服务不存在
"""
try:
node = ServiceNode.objects.get(ServiceName=name, ServiceStatus=1)
except ServiceNode.DoesNotExist:
raise RegistryError(f'服务不存在或已禁用: {name}')
node.LastHeartbeat = _utcnow()
node.save(update_fields=['LastHeartbeat'])
return True
@staticmethod
def UpdateScope(name, scope):
"""更新服务权限范围
Args:
name: 服务名称
scope: 新的权限范围列表
Raises:
RegistryError: 服务不存在
"""
try:
node = ServiceNode.objects.get(ServiceName=name, ServiceStatus=1)
except ServiceNode.DoesNotExist:
raise RegistryError(f'服务不存在或已禁用: {name}')
node.ServiceScope = scope
node.save(update_fields=['ServiceScope'])
logger.info(f'服务权限更新: {name} -> {scope}')
return True
@staticmethod
def RotateSecret(name, new_secret):
"""轮换服务密钥
Args:
name: 服务名称
new_secret: 新密钥(明文)
Raises:
RegistryError: 服务不存在
"""
try:
node = ServiceNode.objects.get(ServiceName=name, ServiceStatus=1)
except ServiceNode.DoesNotExist:
raise RegistryError(f'服务不存在或已禁用: {name}')
node.SetSecret(new_secret)
node.save(update_fields=['ServiceSecretHash', 'ServiceSecretEncrypted'])
from gvsdsdk.msync.models import ServiceToken
ServiceToken.objects.filter(ServiceUUID=node, TokenStatus=1).update(TokenStatus=0)
logger.info(f'服务密钥已轮换: {name}')
return True
@staticmethod
def GetInactiveServices(timeout_minutes=5):
"""获取超时未心跳的服务
Args:
timeout_minutes: 超时分钟数
Returns:
ServiceNode QuerySet
"""
threshold = _utcnow() - datetime.timedelta(minutes=timeout_minutes)
return ServiceNode.objects.filter(
ServiceStatus=1,
LastHeartbeat__lt=threshold,
)