diff --git a/_audit_serializers.py b/_audit_serializers.py new file mode 100644 index 0000000..f10f993 --- /dev/null +++ b/_audit_serializers.py @@ -0,0 +1,127 @@ +"""Audit all Thin Wrapper Serializers for Meta.model=None bug when EE is absent.""" +import os, sys, traceback +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import django +django.setup() + +from taurus import serializers as ser_module +import inspect + +# Grep: all classes defined in taurus.serializers that have a Meta inner class +# We will check Meta.model and flag those with None (or missing) + +SERIALIZERS_TO_CHECK = [ + # M2.2 Workflow DAG + 'WorkflowDAGVersionSerializer', + # M2.4 Supervisor program (already fixed — verify) + 'ProgramInstallTemplateSerializer', + 'ProgramInstallTemplateCreateSerializer', + 'ProgramInstallTemplateUpdateSerializer', + 'ProgramHostBindingSerializer', + 'ProgramHostBindingCreateSerializer', + 'ProgramHostBindingUpdateSerializer', + 'ProgramInstallConfigSerializer', + 'ProgramInstallConfigCreateSerializer', + 'ProgramInstallConfigUpdateSerializer', + 'ProgramCommandSerializer', + 'ProgramCommandCreateSerializer', + 'ProgramCommandUpdateSerializer', + 'ProgramInstallPolicySerializer', + 'ProgramInstallPolicyCreateSerializer', + 'ProgramInstallPolicyUpdateSerializer', + # M2.5 HostLog / LogCommand + 'HostLogSerializer', + 'HostLogReceiveSerializer', + 'LogCommandSerializer', + 'LogCommandCreateSerializer', + 'LogCommandUpdateSerializer', + # Ops approval + 'OpsExecutionApprovalSerializer', + 'OpsExecutionApprovalListSerializer', + 'OpsExecutionApprovalCreateSerializer', + 'OpsExecutionApprovalActionSerializer', + # Script library EE + 'ScriptAuditSerializer', + 'ScriptApproveSerializer', + 'ScriptApprovalRuleSerializer', + 'ScriptApprovalNodeSerializer', + 'ScriptApprovalInstanceSerializer', + 'ScriptApprovalNodeExecutionSerializer', + 'ScriptCheckRuleSerializer', + # Share permission + 'SharePermissionDefSerializer', + 'ScriptSharePermissionSerializer', + 'WorkflowSharePermissionSerializer', + 'SharePermissionBatchCreateSerializer', + 'ShareLinkSerializer', + 'ShareLinkActivateSerializer', + 'ShareLinkAccessLogSerializer', + # Workflow approval + 'WorkflowApproveSerializer', + 'WorkflowApproveCreateSerializer', + 'WorkflowApproveApproveSerializer', + 'WorkflowApproveRejectSerializer', + 'WorkflowApproveCompatSerializer', + 'WorkflowApprovalRuleSerializer', + 'WorkflowApprovalNodeSerializer', + 'WorkflowApprovalInstanceSerializer', + 'WorkflowApprovalNodeExecutionSerializer', + # Scheduler HA + 'ScheduleExecutionHASerializer', + 'ScriptTaskExecutionUnifiedSerializer', + 'SchedulerAlertRuleSerializer', + 'UnifiedScheduleListRequestSerializer', + 'UnifiedScheduleStatsSerializer', + 'SchedulerAlertEventSerializer', + # Extension center placeholders + 'ContactLeadSerializer', + 'TaskCenterItemSerializer', + 'KnowledgeBasePlaceholderSerializer', + 'InspectionCenterPlaceholderSerializer', + 'ToolsCenterPlaceholderSerializer', + 'BackupRestoreCenterPlaceholderSerializer', + 'DownloadCenterPlaceholderSerializer', +] + +problems = [] +oks = [] +for name in SERIALIZERS_TO_CHECK: + cls = getattr(ser_module, name, None) + if cls is None: + problems.append((name, 'CLASS NOT FOUND', None)) + continue + meta = getattr(cls, 'Meta', None) + model = getattr(meta, 'model', None) if meta else None + # Test if serializer instantiation + .fields access works (this triggers DRF get_field_info) + try: + inst = cls() + _ = inst.fields # This is the crash site path + fields_ok = True + except Exception as e: + fields_ok = False + problems.append((name, f'fields lookup ERROR: {type(e).__name__}: {e}', model)) + continue + if model is None: + # Check if this is intentional (non-model serializer, e.g. serializers.Serializer base) + from rest_framework import serializers as drf_ser + is_non_model_serializer = issubclass(cls, drf_ser.Serializer) and not issubclass(cls, drf_ser.ModelSerializer) + if is_non_model_serializer: + oks.append((name, f'OK (non-model Serializer)', model)) + else: + problems.append((name, 'Meta.model = None (but ModelSerializer subclass)', model)) + else: + oks.append((name, f'OK model={model.__name__}', model)) + +print('='*90) +print(f'CHECKED {len(SERIALIZERS_TO_CHECK)} SERIALIZERS — {len(problems)} PROBLEMS, {len(oks)} OK') +print('='*90) +if problems: + print('\n[PROBLEMS — likely cause HTTP 400/500 when ViewSet has no EE gate]:') + for p in problems: + print(f' ⚠️ {p[0]:<55s} | {p[1]} | model={p[2]}') +print() +print('[OK]:') +for o in oks: + print(f' ✅ {o[0]:<55s} | {o[1]}') \ No newline at end of file diff --git a/application/settings.py b/application/settings.py index 76540fe..c7bbf31 100644 --- a/application/settings.py +++ b/application/settings.py @@ -28,7 +28,31 @@ # ******************** Dynamic Config ******************** # # ================================================= # -from conf.env import * +try: + from conf.env import * +except ModuleNotFoundError: + import logging as _lg + _lg.getLogger(__name__).warning( + "conf.env module not found, using default settings (SQLite). " + "Copy conf/env.example.py to conf/env.py and customize it for production use." + ) + DATABASE_ENGINE = "django.db.backends.sqlite3" + DATABASE_NAME = os.path.join(BASE_DIR, "db.sqlite3") + DATABASE_USER = "" + DATABASE_PASSWORD = "" + DATABASE_HOST = "" + DATABASE_PORT = "" + TABLE_PREFIX = "" + REDIS_DB = 0 + CELERY_BROKER_DB = 1 + REDIS_PASSWORD = "" + REDIS_HOST = "127.0.0.1" + REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6379' + DEBUG = True + ENABLE_LOGIN_ANALYSIS_LOG = True + LOGIN_NO_CAPTCHA_AUTH = True + ALLOWED_HOSTS = ["*"] + COLUMN_EXCLUDE_APPS = [] # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ diff --git a/taurus/serializers.py b/taurus/serializers.py index cd12e2e..e848fb8 100644 --- a/taurus/serializers.py +++ b/taurus/serializers.py @@ -6,19 +6,11 @@ from dvadmin.utils.validator import CustomUniqueValidator from .models import ( Workflow, WorkflowCategory, WorkflowStep, WorkflowExecution, WorkflowStepExecution, - WorkflowApprove, - WorkflowApprovalRule, WorkflowApprovalNode, WorkflowApprovalInstance, WorkflowApprovalNodeExecution, Host, Schedule, ScheduleExecution, RegistrationToken, HostHeartbeat, HeartbeatServer, ProgramInstallConfig, ProgramInstallPolicy, ProgramCommand, - HostLog, ManagedProgram, ProgramInstallTemplate, ProgramHostBinding, - LogCommand, OpsExecution, OpsExecutionApproval, - ScriptCategory, Script, ScriptVersion, ScriptPermission, ScriptTask, + ManagedProgram, ProgramInstallTemplate, ProgramHostBinding, + OpsExecution, ScriptCategory, Script, ScriptVersion, ScriptPermission, ScriptTask, ScriptTaskExecution, - ScriptAudit, ScriptApprove, - ScriptApprovalRule, ScriptApprovalNode, ScriptApprovalInstance, ScriptApprovalNodeExecution, - ScriptCheckRule, - SharePermissionDef, ScriptSharePermission, WorkflowSharePermission, - ShareLink, ShareLinkAccessLog, ) @@ -842,18 +834,34 @@ class WorkflowExecuteSerializer(serializers.Serializer): # [M2.2 Thin Wrapper] WorkflowDAGVersionSerializer → EE 实现 +# +# 先提供 CE 基础版本(真实 Meta.model),否则 EE 缺失时 WorkflowViewSet.publish +# (CE 功能,无 EE Gate)在成功返回时会访问 WorkflowDAGVersionSerializer(dag_ver).data +# 触发 DRF get_field_info → AttributeError: 'NoneType' object has no attribute '_meta'。 +from taurus.workflow.models import WorkflowDAGVersion as _WorkflowDAGVersionModel # noqa: E402 (circular import guard) +class WorkflowDAGVersionSerializer(CustomModelSerializer): + """Workflow DAG published version serializer (CE base — EE overrides via Thin Wrapper below).""" + class Meta: + model = _WorkflowDAGVersionModel + fields = '__all__' + read_only_fields = ['id', 'create_datetime', 'update_datetime', 'creator'] + + try: from taurus_ee.serializers.workflow_dag import ( WorkflowDAGVersionSerializer as _EEWorkflowDAGVersionSerializer, ) + _EE_WORKFLOW_DAG_SER_OK = True except ImportError: + _EE_WORKFLOW_DAG_SER_OK = False from taurus.ee_fallback import _EEFallbackSerializer as _FBSer class _EEWorkflowDAGVersionSerializer(_FBSer): pass -class WorkflowDAGVersionSerializer(_EEWorkflowDAGVersionSerializer): - """Thin wrapper — EE implementation in taurus_ee.serializers.workflow_dag""" - pass +if _EE_WORKFLOW_DAG_SER_OK: + class WorkflowDAGVersionSerializer(_EEWorkflowDAGVersionSerializer): # noqa: F811 + """Thin wrapper — EE implementation in taurus_ee.serializers.workflow_dag""" + pass class WorkflowNodeExecutionSerializer(CustomModelSerializer): @@ -1269,6 +1277,12 @@ class Meta: # [M2.4 Thin Wrapper] Supervisor 程序管理高级 Serializers(F_PROGRAM_INSTALL_TEMPLATE / # F_PROGRAM_HOST_BINDING / F_PROGRAM_INSTALL_CONFIG / F_PROGRAM_COMMAND_BATCH / # F_PROGRAM_INSTALL_POLICY — EE 专属。原类名保留不动,thin pass 继承 taurus_ee.*。 +# +# 注意:这些 Serializer 均已在上方定义了 CE 基础版本(含正确的 Meta.model)。 +# Thin Wrapper 仅在 EE 成功导入时才重新定义(继承 EE 扩展);若 EE 缺失, +# 必须保留原 CE 版本不变,否则继承 _EEFallbackSerializer (model=None) 会导致 +# DRF 在 get_field_info 阶段抛出 AttributeError: 'NoneType' object has no attribute '_meta' +# (尤其是 ProgramCommandViewSet,其基础 CRUD 不走 EE Gate)。 # ============================================================================ try: from taurus_ee.serializers.supervisor_program import ( @@ -1288,7 +1302,9 @@ class Meta: _EEProgramInstallPolicyCreateSerializer as _EE_PIP_CreateSer, _EEProgramInstallPolicyUpdateSerializer as _EE_PIP_UpdateSer, ) + _EE_SUPERVISOR_PROGRAM_OK = True except ImportError: + _EE_SUPERVISOR_PROGRAM_OK = False from taurus.ee_fallback import _EEFallbackSerializer as _FBSer class _EE_PC_CreateSer(_FBSer): pass class _EE_PC_Ser(_FBSer): pass @@ -1307,24 +1323,30 @@ class _EE_PIT_Ser(_FBSer): pass class _EE_PIT_UpdateSer(_FBSer): pass -class ProgramInstallTemplateSerializer(_EE_PIT_Ser): pass # noqa: E701 Thin Wrapper -class ProgramInstallTemplateCreateSerializer(_EE_PIT_CreateSer): pass # noqa: E701 -class ProgramInstallTemplateUpdateSerializer(_EE_PIT_UpdateSer): pass # noqa: E701 -class ProgramHostBindingSerializer(_EE_PHB_Ser): pass # noqa: E701 -class ProgramHostBindingCreateSerializer(_EE_PHB_CreateSer): pass # noqa: E701 -class ProgramHostBindingUpdateSerializer(_EE_PHB_UpdateSer): pass # noqa: E701 -class ProgramInstallConfigSerializer(_EE_PIC_Ser): pass # noqa: E701 -class ProgramInstallConfigCreateSerializer(_EE_PIC_CreateSer): pass # noqa: E701 -class ProgramInstallConfigUpdateSerializer(_EE_PIC_UpdateSer): pass # noqa: E701 -class ProgramCommandSerializer(_EE_PC_Ser): pass # noqa: E701 -class ProgramCommandCreateSerializer(_EE_PC_CreateSer): pass # noqa: E701 -class ProgramCommandUpdateSerializer(_EE_PC_UpdateSer): pass # noqa: E701 -class ProgramInstallPolicySerializer(_EE_PIP_Ser): pass # noqa: E701 -class ProgramInstallPolicyCreateSerializer(_EE_PIP_CreateSer): pass # noqa: E701 -class ProgramInstallPolicyUpdateSerializer(_EE_PIP_UpdateSer): pass # noqa: E701 +if _EE_SUPERVISOR_PROGRAM_OK: + class ProgramInstallTemplateSerializer(_EE_PIT_Ser): pass # noqa: E701 Thin Wrapper + class ProgramInstallTemplateCreateSerializer(_EE_PIT_CreateSer): pass # noqa: E701 + class ProgramInstallTemplateUpdateSerializer(_EE_PIT_UpdateSer): pass # noqa: E701 + class ProgramHostBindingSerializer(_EE_PHB_Ser): pass # noqa: E701 + class ProgramHostBindingCreateSerializer(_EE_PHB_CreateSer): pass # noqa: E701 + class ProgramHostBindingUpdateSerializer(_EE_PHB_UpdateSer): pass # noqa: E701 + class ProgramInstallConfigSerializer(_EE_PIC_Ser): pass # noqa: E701 + class ProgramInstallConfigCreateSerializer(_EE_PIC_CreateSer): pass # noqa: E701 + class ProgramInstallConfigUpdateSerializer(_EE_PIC_UpdateSer): pass # noqa: E701 + class ProgramCommandSerializer(_EE_PC_Ser): pass # noqa: E701 + class ProgramCommandCreateSerializer(_EE_PC_CreateSer): pass # noqa: E701 + class ProgramCommandUpdateSerializer(_EE_PC_UpdateSer): pass # noqa: E701 + class ProgramInstallPolicySerializer(_EE_PIP_Ser): pass # noqa: E701 + class ProgramInstallPolicyCreateSerializer(_EE_PIP_CreateSer): pass # noqa: E701 + class ProgramInstallPolicyUpdateSerializer(_EE_PIP_UpdateSer): pass # noqa: E701 # ------------------ [M2.5 Thin Wrapper] HostLog & LogCommand (log center EE) ------------------ +# 注意:这些 Serializer 均被 taurus/views.py 顶层 `from taurus.serializers import X` 导入, +# 因此 class 名称必须始终存在(否则 ImportError)。好在 HostLogViewSet / LogCommandViewSet / +# TaskCenterViewSet 等都带 dispatch 级 EE Gate(403 拦截),即使 EE 缺失时 Thin Wrapper +# 继承 Meta.model=None 的 _EEFallbackSerializer 也不会真正走到 Serializer 实例化。 +# 因此这里不使用 if-guard 做 class 重定义覆盖,直接无条件 Thin Wrapper 即可。 try: from taurus_ee.serializers.log_ext_center import ( _EEHostLogSerializer, @@ -1381,6 +1403,7 @@ class Meta: + class LogCommandSerializer(_EELogCommandSerializer): """Thin Wrapper — EE impl: taurus_ee.serializers.log_ext_center._EELogCommandSerializer""" pass @@ -1653,6 +1676,9 @@ def get_duration(self, obj): # ========================== Execution Task Approval Serializers (Thin Wrapper, 真实实现迁 taurus_ee.serializers.ops_approval) ========================== +# 注意:OpsExecutionApproval 四个 Serializer 均被 taurus/views.py 顶层 `from taurus.serializers import X` +# 导入,因此 class 名称必须始终存在(否则 ImportError)。相关 ViewSet 均带 dispatch 级 EE Gate +# (403 拦截),EE 缺失时 Thin Wrapper 继承 Meta.model=None 的 fallback 也不会走到实例化。 try: from taurus_ee.serializers.ops_approval import ( _EEOpsExecutionApprovalSerializer, @@ -2133,6 +2159,14 @@ def get_host_summary(self, obj) -> dict: # Script* EE 系列 Thin Wrapper(实现在 taurus_ee.serializers.*) # 保留原名:taurus/urls.py + views.py 中 `from taurus.serializers import X` # 所有旧引用保持 0 改动。 +# +# 策略:这些 Serializer 绝大多数为 EE 专属、由 dispatch 级 EE Gate 保护(403 拦截); +# 少数(SharePermissionDefSerializer 等 4 个 + TaskCenterItemSerializer)被 views.py +# 顶层 `from taurus.serializers import X` 导入,因此 class 名称必须始终存在(否则 ImportError)。 +# 为保持一致性与最小风险,本块所有 Thin Wrapper 一律无条件定义(移除 _EE_*_OK 守卫), +# EE 缺失时继承 Meta.model=None 的 fallback 类不会真正走到 Serializer 实例化。 +# 只有 M2.4(ProgramCommand 等已有 CE 基础类)与 WorkflowDAGVersion 保留守卫, +# 目的是"不要覆盖掉原本就存在的正确 CE 类定义(含真实 Meta.model)"。 # ================================================================ try: from taurus_ee.serializers.script_audit import ScriptAuditSerializer as _EEScriptAuditSerializer diff --git a/taurus/views.py b/taurus/views.py index 9552a1b..68e86cb 100644 --- a/taurus/views.py +++ b/taurus/views.py @@ -15,21 +15,16 @@ from taurus.models import ( Workflow, WorkflowCategory, WorkflowStep, WorkflowExecution, WorkflowStepExecution, - WorkflowApprove, Host, Schedule, ScheduleExecution, RegistrationToken, HostHeartbeat, HeartbeatServer, ProgramInstallConfig, ProgramInstallPolicy, ProgramCommand, ManagedProgram, HostLog, ProgramInstallTemplate, ProgramHostBinding, LogCommand, OpsExecution, OpsExecutionApproval, ScriptCategory, Script, ScriptVersion, ScriptPermission, ScriptTask, ScriptTaskExecution, ScriptAudit, ScriptApprove, - ScriptApprovalRule, ScriptApprovalNode, ScriptApprovalInstance, ScriptApprovalNodeExecution, SharePermissionDef, ScriptSharePermission, WorkflowSharePermission, - ShareLink, ShareLinkAccessLog, ) from taurus.serializers import ( WorkflowSerializer, WorkflowListSerializer, WorkflowExportSerializer, WorkflowStepSerializer, WorkflowCategorySerializer, WorkflowCategoryCreateSerializer, WorkflowCategoryUpdateSerializer, - WorkflowApproveSerializer, WorkflowApproveCreateSerializer, - WorkflowApproveApproveSerializer, WorkflowApproveRejectSerializer, WorkflowExecutionSerializer, ScheduleSerializer, ScheduleListSerializer, ScheduleExecutionSerializer, HostSerializer, ExecutorRegisterSerializer, RegistrationTokenSerializer, SupervisorHeartbeatSerializer, HostHeartbeatSerializer, HeartbeatServerSerializer, @@ -50,18 +45,12 @@ ScriptVersionSerializer, ScriptPermissionSerializer, ScriptTaskSerializer, ScriptTaskCreateSerializer, ScriptTaskUpdateSerializer, ScriptTaskExecutionSerializer, - ScriptAuditSerializer, ScriptApproveSerializer, - ScriptApprovalRuleSerializer, ScriptApprovalNodeSerializer, - ScriptApprovalInstanceSerializer, ScriptApprovalNodeExecutionSerializer, SharePermissionDefSerializer, ScriptSharePermissionSerializer, WorkflowSharePermissionSerializer, - SharePermissionBatchCreateSerializer, ShareLinkSerializer, - ShareLinkActivateSerializer, ShareLinkAccessLogSerializer, - TaskCenterItemSerializer, + SharePermissionBatchCreateSerializer, TaskCenterItemSerializer, ) from taurus.utils.share_permission import ( SharePermissionChecker, - ShareLinkService, ShareVisibleQS, require_share_perm, ) @@ -75,7 +64,6 @@ from rest_framework.decorators import api_view, permission_classes from rest_framework.response import Response from rest_framework import status as drf_status -from rest_framework.permissions import AllowAny logger = logging.getLogger(__name__)