truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- truthound_dashboard/api/alerts.py +75 -86
- truthound_dashboard/api/anomaly.py +7 -13
- truthound_dashboard/api/cross_alerts.py +38 -52
- truthound_dashboard/api/drift.py +49 -59
- truthound_dashboard/api/drift_monitor.py +234 -79
- truthound_dashboard/api/enterprise_sampling.py +498 -0
- truthound_dashboard/api/history.py +57 -5
- truthound_dashboard/api/lineage.py +3 -48
- truthound_dashboard/api/maintenance.py +104 -49
- truthound_dashboard/api/mask.py +1 -2
- truthound_dashboard/api/middleware.py +2 -1
- truthound_dashboard/api/model_monitoring.py +435 -311
- truthound_dashboard/api/notifications.py +227 -191
- truthound_dashboard/api/notifications_advanced.py +21 -20
- truthound_dashboard/api/observability.py +586 -0
- truthound_dashboard/api/plugins.py +2 -433
- truthound_dashboard/api/profile.py +199 -37
- truthound_dashboard/api/quality_reporter.py +701 -0
- truthound_dashboard/api/reports.py +7 -16
- truthound_dashboard/api/router.py +66 -0
- truthound_dashboard/api/rule_suggestions.py +5 -5
- truthound_dashboard/api/scan.py +17 -19
- truthound_dashboard/api/schedules.py +85 -50
- truthound_dashboard/api/schema_evolution.py +6 -6
- truthound_dashboard/api/schema_watcher.py +667 -0
- truthound_dashboard/api/sources.py +98 -27
- truthound_dashboard/api/tiering.py +1323 -0
- truthound_dashboard/api/triggers.py +14 -11
- truthound_dashboard/api/validations.py +12 -11
- truthound_dashboard/api/versioning.py +1 -6
- truthound_dashboard/core/__init__.py +129 -3
- truthound_dashboard/core/actions/__init__.py +62 -0
- truthound_dashboard/core/actions/custom.py +426 -0
- truthound_dashboard/core/actions/notifications.py +910 -0
- truthound_dashboard/core/actions/storage.py +472 -0
- truthound_dashboard/core/actions/webhook.py +281 -0
- truthound_dashboard/core/anomaly.py +262 -67
- truthound_dashboard/core/anomaly_explainer.py +4 -3
- truthound_dashboard/core/backends/__init__.py +67 -0
- truthound_dashboard/core/backends/base.py +299 -0
- truthound_dashboard/core/backends/errors.py +191 -0
- truthound_dashboard/core/backends/factory.py +423 -0
- truthound_dashboard/core/backends/mock_backend.py +451 -0
- truthound_dashboard/core/backends/truthound_backend.py +718 -0
- truthound_dashboard/core/checkpoint/__init__.py +87 -0
- truthound_dashboard/core/checkpoint/adapters.py +814 -0
- truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
- truthound_dashboard/core/checkpoint/runner.py +270 -0
- truthound_dashboard/core/connections.py +645 -23
- truthound_dashboard/core/converters/__init__.py +14 -0
- truthound_dashboard/core/converters/truthound.py +620 -0
- truthound_dashboard/core/cross_alerts.py +540 -320
- truthound_dashboard/core/datasource_factory.py +1672 -0
- truthound_dashboard/core/drift_monitor.py +216 -20
- truthound_dashboard/core/enterprise_sampling.py +1291 -0
- truthound_dashboard/core/interfaces/__init__.py +225 -0
- truthound_dashboard/core/interfaces/actions.py +652 -0
- truthound_dashboard/core/interfaces/base.py +247 -0
- truthound_dashboard/core/interfaces/checkpoint.py +676 -0
- truthound_dashboard/core/interfaces/protocols.py +664 -0
- truthound_dashboard/core/interfaces/reporters.py +650 -0
- truthound_dashboard/core/interfaces/routing.py +646 -0
- truthound_dashboard/core/interfaces/triggers.py +619 -0
- truthound_dashboard/core/lineage.py +407 -71
- truthound_dashboard/core/model_monitoring.py +431 -3
- truthound_dashboard/core/notifications/base.py +4 -0
- truthound_dashboard/core/notifications/channels.py +501 -1203
- truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
- truthound_dashboard/core/notifications/deduplication/service.py +131 -348
- truthound_dashboard/core/notifications/dispatcher.py +202 -11
- truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
- truthound_dashboard/core/notifications/escalation/engine.py +168 -358
- truthound_dashboard/core/notifications/routing/__init__.py +88 -128
- truthound_dashboard/core/notifications/routing/engine.py +90 -317
- truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
- truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
- truthound_dashboard/core/notifications/throttling/builder.py +117 -255
- truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
- truthound_dashboard/core/phase5/collaboration.py +1 -1
- truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
- truthound_dashboard/core/quality_reporter.py +1359 -0
- truthound_dashboard/core/report_history.py +0 -6
- truthound_dashboard/core/reporters/__init__.py +175 -14
- truthound_dashboard/core/reporters/adapters.py +943 -0
- truthound_dashboard/core/reporters/base.py +0 -3
- truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
- truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
- truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
- truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
- truthound_dashboard/core/reporters/compat.py +266 -0
- truthound_dashboard/core/reporters/csv_reporter.py +2 -35
- truthound_dashboard/core/reporters/factory.py +526 -0
- truthound_dashboard/core/reporters/interfaces.py +745 -0
- truthound_dashboard/core/reporters/registry.py +1 -10
- truthound_dashboard/core/scheduler.py +165 -0
- truthound_dashboard/core/schema_evolution.py +3 -3
- truthound_dashboard/core/schema_watcher.py +1528 -0
- truthound_dashboard/core/services.py +595 -76
- truthound_dashboard/core/store_manager.py +810 -0
- truthound_dashboard/core/streaming_anomaly.py +169 -4
- truthound_dashboard/core/tiering.py +1309 -0
- truthound_dashboard/core/triggers/evaluators.py +178 -8
- truthound_dashboard/core/truthound_adapter.py +2620 -197
- truthound_dashboard/core/unified_alerts.py +23 -20
- truthound_dashboard/db/__init__.py +8 -0
- truthound_dashboard/db/database.py +8 -2
- truthound_dashboard/db/models.py +944 -25
- truthound_dashboard/db/repository.py +2 -0
- truthound_dashboard/main.py +15 -0
- truthound_dashboard/schemas/__init__.py +177 -16
- truthound_dashboard/schemas/base.py +44 -23
- truthound_dashboard/schemas/collaboration.py +19 -6
- truthound_dashboard/schemas/cross_alerts.py +19 -3
- truthound_dashboard/schemas/drift.py +61 -55
- truthound_dashboard/schemas/drift_monitor.py +67 -23
- truthound_dashboard/schemas/enterprise_sampling.py +653 -0
- truthound_dashboard/schemas/lineage.py +0 -33
- truthound_dashboard/schemas/mask.py +10 -8
- truthound_dashboard/schemas/model_monitoring.py +89 -10
- truthound_dashboard/schemas/notifications_advanced.py +13 -0
- truthound_dashboard/schemas/observability.py +453 -0
- truthound_dashboard/schemas/plugins.py +0 -280
- truthound_dashboard/schemas/profile.py +154 -247
- truthound_dashboard/schemas/quality_reporter.py +403 -0
- truthound_dashboard/schemas/reports.py +2 -2
- truthound_dashboard/schemas/rule_suggestion.py +8 -1
- truthound_dashboard/schemas/scan.py +4 -24
- truthound_dashboard/schemas/schedule.py +11 -3
- truthound_dashboard/schemas/schema_watcher.py +727 -0
- truthound_dashboard/schemas/source.py +17 -2
- truthound_dashboard/schemas/tiering.py +822 -0
- truthound_dashboard/schemas/triggers.py +16 -0
- truthound_dashboard/schemas/unified_alerts.py +7 -0
- truthound_dashboard/schemas/validation.py +0 -13
- truthound_dashboard/schemas/validators/base.py +41 -21
- truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
- truthound_dashboard/schemas/validators/localization_validators.py +273 -0
- truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
- truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
- truthound_dashboard/schemas/validators/referential_validators.py +312 -0
- truthound_dashboard/schemas/validators/registry.py +93 -8
- truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
- truthound_dashboard/schemas/versioning.py +1 -6
- truthound_dashboard/static/index.html +2 -2
- truthound_dashboard-1.5.1.dist-info/METADATA +312 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/RECORD +149 -148
- truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
- truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
- truthound_dashboard/core/plugins/hooks/manager.py +0 -403
- truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
- truthound_dashboard/core/reporters/junit_reporter.py +0 -233
- truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
- truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
- truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
- truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
- truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
- truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
- truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
- truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
- truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
- truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
- truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
- truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
- truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
- truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
- truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
- truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
- truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
- truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
- truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
- truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
- truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
- truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
- truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
- truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
- truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
- truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
- truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
- truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
- truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
- truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
- truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
- truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
- truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
- truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
- truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
- truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
- truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
- truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
- truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
- truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
- truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
- truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
- truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
- truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
- truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
- truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
- truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
- truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1323 @@
|
|
|
1
|
+
"""Storage tiering API endpoints.
|
|
2
|
+
|
|
3
|
+
This module provides REST API endpoints for managing storage tiering:
|
|
4
|
+
- Storage tiers (hot, warm, cold, archive)
|
|
5
|
+
- Tier policies (age-based, access-based, size-based, scheduled, composite, custom)
|
|
6
|
+
- Tiering configurations
|
|
7
|
+
- Migration history
|
|
8
|
+
|
|
9
|
+
Based on truthound 1.2.10+ storage tiering capabilities.
|
|
10
|
+
|
|
11
|
+
Endpoints:
|
|
12
|
+
Storage Tiers:
|
|
13
|
+
GET /tiering/tiers - List storage tiers
|
|
14
|
+
POST /tiering/tiers - Create storage tier
|
|
15
|
+
GET /tiering/tiers/{id} - Get storage tier
|
|
16
|
+
PUT /tiering/tiers/{id} - Update storage tier
|
|
17
|
+
DELETE /tiering/tiers/{id} - Delete storage tier
|
|
18
|
+
|
|
19
|
+
Tier Policies:
|
|
20
|
+
GET /tiering/policies - List tier policies
|
|
21
|
+
POST /tiering/policies - Create tier policy
|
|
22
|
+
GET /tiering/policies/{id} - Get tier policy
|
|
23
|
+
PUT /tiering/policies/{id} - Update tier policy
|
|
24
|
+
DELETE /tiering/policies/{id} - Delete tier policy
|
|
25
|
+
GET /tiering/policies/{id}/tree - Get policy with children (for composite)
|
|
26
|
+
GET /tiering/policies/types - Get available policy types
|
|
27
|
+
|
|
28
|
+
Tiering Configurations:
|
|
29
|
+
GET /tiering/configs - List tiering configurations
|
|
30
|
+
POST /tiering/configs - Create tiering configuration
|
|
31
|
+
GET /tiering/configs/{id} - Get tiering configuration
|
|
32
|
+
PUT /tiering/configs/{id} - Update tiering configuration
|
|
33
|
+
DELETE /tiering/configs/{id} - Delete tiering configuration
|
|
34
|
+
|
|
35
|
+
Migration History:
|
|
36
|
+
GET /tiering/migrations - List migration history
|
|
37
|
+
GET /tiering/migrations/{id} - Get migration details
|
|
38
|
+
|
|
39
|
+
Statistics:
|
|
40
|
+
GET /tiering/stats - Get tiering statistics
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
48
|
+
from sqlalchemy import func, select
|
|
49
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
50
|
+
from sqlalchemy.orm import selectinload
|
|
51
|
+
|
|
52
|
+
from ..db import get_db_session as get_session
|
|
53
|
+
from ..db.models import (
|
|
54
|
+
MigrationDirection,
|
|
55
|
+
StorageTierModel,
|
|
56
|
+
TierMigrationHistoryModel,
|
|
57
|
+
TierPolicyModel,
|
|
58
|
+
TierPolicyType,
|
|
59
|
+
TierType,
|
|
60
|
+
TieringConfigModel,
|
|
61
|
+
)
|
|
62
|
+
from ..schemas.base import MessageResponse
|
|
63
|
+
from ..schemas.tiering import (
|
|
64
|
+
AgeBasedPolicyConfig,
|
|
65
|
+
AccessBasedPolicyConfig,
|
|
66
|
+
CompositePolicyConfig,
|
|
67
|
+
CustomPolicyConfig,
|
|
68
|
+
MigrationHistoryListResponse,
|
|
69
|
+
MigrationHistoryResponse,
|
|
70
|
+
MigrationItemResponse,
|
|
71
|
+
MigrateItemRequest,
|
|
72
|
+
PolicyExecutionRequest,
|
|
73
|
+
PolicyExecutionResponse,
|
|
74
|
+
PolicyExecutionSummary,
|
|
75
|
+
PolicyTypeInfo,
|
|
76
|
+
PolicyTypesResponse,
|
|
77
|
+
ProcessPoliciesResponse,
|
|
78
|
+
ScheduledPolicyConfig,
|
|
79
|
+
SizeBasedPolicyConfig,
|
|
80
|
+
StorageTierCreate,
|
|
81
|
+
StorageTierListResponse,
|
|
82
|
+
StorageTierResponse,
|
|
83
|
+
StorageTierUpdate,
|
|
84
|
+
TierPolicyCreate,
|
|
85
|
+
TierPolicyListResponse,
|
|
86
|
+
TierPolicyResponse,
|
|
87
|
+
TierPolicyType as TierPolicyTypeEnum,
|
|
88
|
+
TierPolicyUpdate,
|
|
89
|
+
TierPolicyWithChildren,
|
|
90
|
+
TierStatistics,
|
|
91
|
+
TierType as TierTypeEnum,
|
|
92
|
+
TieringConfigCreate,
|
|
93
|
+
TieringConfigListResponse,
|
|
94
|
+
TieringConfigResponse,
|
|
95
|
+
TieringConfigUpdate,
|
|
96
|
+
TieringStatistics,
|
|
97
|
+
TieringStatusResponse,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
router = APIRouter(prefix="/tiering", tags=["tiering"])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# =============================================================================
|
|
104
|
+
# Helper Functions
|
|
105
|
+
# =============================================================================
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _tier_to_response(tier: StorageTierModel) -> StorageTierResponse:
|
|
109
|
+
"""Convert storage tier model to response schema."""
|
|
110
|
+
return StorageTierResponse(
|
|
111
|
+
id=tier.id,
|
|
112
|
+
name=tier.name,
|
|
113
|
+
tier_type=TierTypeEnum(tier.tier_type),
|
|
114
|
+
store_type=tier.store_type,
|
|
115
|
+
store_config=tier.store_config,
|
|
116
|
+
priority=tier.priority,
|
|
117
|
+
cost_per_gb=tier.cost_per_gb,
|
|
118
|
+
retrieval_time_ms=tier.retrieval_time_ms,
|
|
119
|
+
metadata=tier.tier_metadata,
|
|
120
|
+
is_active=tier.is_active,
|
|
121
|
+
created_at=tier.created_at,
|
|
122
|
+
updated_at=tier.updated_at,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _policy_to_response(
|
|
127
|
+
policy: TierPolicyModel,
|
|
128
|
+
from_tier_name: str | None = None,
|
|
129
|
+
to_tier_name: str | None = None,
|
|
130
|
+
) -> TierPolicyResponse:
|
|
131
|
+
"""Convert tier policy model to response schema."""
|
|
132
|
+
return TierPolicyResponse(
|
|
133
|
+
id=policy.id,
|
|
134
|
+
name=policy.name,
|
|
135
|
+
description=policy.description,
|
|
136
|
+
policy_type=TierPolicyTypeEnum(policy.policy_type),
|
|
137
|
+
from_tier_id=policy.from_tier_id,
|
|
138
|
+
to_tier_id=policy.to_tier_id,
|
|
139
|
+
direction=policy.direction,
|
|
140
|
+
config=policy.config,
|
|
141
|
+
is_active=policy.is_active,
|
|
142
|
+
priority=policy.priority,
|
|
143
|
+
parent_id=policy.parent_id,
|
|
144
|
+
child_count=policy.child_count,
|
|
145
|
+
from_tier_name=from_tier_name or (policy.from_tier.name if policy.from_tier else None),
|
|
146
|
+
to_tier_name=to_tier_name or (policy.to_tier.name if policy.to_tier else None),
|
|
147
|
+
created_at=policy.created_at,
|
|
148
|
+
updated_at=policy.updated_at,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _policy_to_tree(policy: TierPolicyModel) -> TierPolicyWithChildren:
|
|
153
|
+
"""Convert tier policy model to tree response with children."""
|
|
154
|
+
return TierPolicyWithChildren(
|
|
155
|
+
id=policy.id,
|
|
156
|
+
name=policy.name,
|
|
157
|
+
description=policy.description,
|
|
158
|
+
policy_type=TierPolicyTypeEnum(policy.policy_type),
|
|
159
|
+
from_tier_id=policy.from_tier_id,
|
|
160
|
+
to_tier_id=policy.to_tier_id,
|
|
161
|
+
direction=policy.direction,
|
|
162
|
+
config=policy.config,
|
|
163
|
+
is_active=policy.is_active,
|
|
164
|
+
priority=policy.priority,
|
|
165
|
+
parent_id=policy.parent_id,
|
|
166
|
+
child_count=policy.child_count,
|
|
167
|
+
from_tier_name=policy.from_tier.name if policy.from_tier else None,
|
|
168
|
+
to_tier_name=policy.to_tier.name if policy.to_tier else None,
|
|
169
|
+
created_at=policy.created_at,
|
|
170
|
+
updated_at=policy.updated_at,
|
|
171
|
+
children=[_policy_to_tree(child) for child in (policy.children or [])],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _config_to_response(config: TieringConfigModel) -> TieringConfigResponse:
|
|
176
|
+
"""Convert tiering configuration model to response schema."""
|
|
177
|
+
return TieringConfigResponse(
|
|
178
|
+
id=config.id,
|
|
179
|
+
name=config.name,
|
|
180
|
+
description=config.description,
|
|
181
|
+
default_tier_id=config.default_tier_id,
|
|
182
|
+
enable_promotion=config.enable_promotion,
|
|
183
|
+
promotion_threshold=config.promotion_threshold,
|
|
184
|
+
check_interval_hours=config.check_interval_hours,
|
|
185
|
+
batch_size=config.batch_size,
|
|
186
|
+
enable_parallel_migration=config.enable_parallel_migration,
|
|
187
|
+
max_parallel_migrations=config.max_parallel_migrations,
|
|
188
|
+
is_active=config.is_active,
|
|
189
|
+
default_tier_name=config.default_tier.name if config.default_tier else None,
|
|
190
|
+
created_at=config.created_at,
|
|
191
|
+
updated_at=config.updated_at,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _migration_to_response(
|
|
196
|
+
migration: TierMigrationHistoryModel,
|
|
197
|
+
from_tier_name: str | None = None,
|
|
198
|
+
to_tier_name: str | None = None,
|
|
199
|
+
policy_name: str | None = None,
|
|
200
|
+
) -> MigrationHistoryResponse:
|
|
201
|
+
"""Convert migration history model to response schema."""
|
|
202
|
+
return MigrationHistoryResponse(
|
|
203
|
+
id=migration.id,
|
|
204
|
+
policy_id=migration.policy_id,
|
|
205
|
+
item_id=migration.item_id,
|
|
206
|
+
from_tier_id=migration.from_tier_id,
|
|
207
|
+
to_tier_id=migration.to_tier_id,
|
|
208
|
+
size_bytes=migration.size_bytes,
|
|
209
|
+
status=migration.status,
|
|
210
|
+
error_message=migration.error_message,
|
|
211
|
+
started_at=migration.started_at,
|
|
212
|
+
completed_at=migration.completed_at,
|
|
213
|
+
duration_ms=migration.duration_ms,
|
|
214
|
+
from_tier_name=from_tier_name,
|
|
215
|
+
to_tier_name=to_tier_name,
|
|
216
|
+
policy_name=policy_name,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# =============================================================================
|
|
221
|
+
# Storage Tiers Endpoints
|
|
222
|
+
# =============================================================================
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@router.get("/tiers", response_model=StorageTierListResponse)
|
|
226
|
+
async def list_storage_tiers(
|
|
227
|
+
offset: int = Query(default=0, ge=0),
|
|
228
|
+
limit: int = Query(default=50, ge=1, le=100),
|
|
229
|
+
active_only: bool = Query(default=False),
|
|
230
|
+
tier_type: str | None = Query(default=None),
|
|
231
|
+
session: AsyncSession = Depends(get_session),
|
|
232
|
+
) -> StorageTierListResponse:
|
|
233
|
+
"""List all storage tiers with optional filtering."""
|
|
234
|
+
query = select(StorageTierModel)
|
|
235
|
+
|
|
236
|
+
if active_only:
|
|
237
|
+
query = query.where(StorageTierModel.is_active == True) # noqa: E712
|
|
238
|
+
|
|
239
|
+
if tier_type:
|
|
240
|
+
query = query.where(StorageTierModel.tier_type == tier_type)
|
|
241
|
+
|
|
242
|
+
# Get total count
|
|
243
|
+
count_query = select(func.count()).select_from(query.subquery())
|
|
244
|
+
total = (await session.execute(count_query)).scalar() or 0
|
|
245
|
+
|
|
246
|
+
# Apply pagination and ordering
|
|
247
|
+
query = query.order_by(StorageTierModel.priority).offset(offset).limit(limit)
|
|
248
|
+
result = await session.execute(query)
|
|
249
|
+
tiers = result.scalars().all()
|
|
250
|
+
|
|
251
|
+
return StorageTierListResponse(
|
|
252
|
+
items=[_tier_to_response(tier) for tier in tiers],
|
|
253
|
+
total=total,
|
|
254
|
+
offset=offset,
|
|
255
|
+
limit=limit,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@router.post("/tiers", response_model=StorageTierResponse, status_code=201)
|
|
260
|
+
async def create_storage_tier(
|
|
261
|
+
request: StorageTierCreate,
|
|
262
|
+
session: AsyncSession = Depends(get_session),
|
|
263
|
+
) -> StorageTierResponse:
|
|
264
|
+
"""Create a new storage tier."""
|
|
265
|
+
# Check for duplicate name
|
|
266
|
+
existing = await session.execute(
|
|
267
|
+
select(StorageTierModel).where(StorageTierModel.name == request.name)
|
|
268
|
+
)
|
|
269
|
+
if existing.scalar_one_or_none():
|
|
270
|
+
raise HTTPException(
|
|
271
|
+
status_code=400,
|
|
272
|
+
detail=f"Storage tier with name '{request.name}' already exists",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
tier = StorageTierModel(
|
|
276
|
+
name=request.name,
|
|
277
|
+
tier_type=request.tier_type.value,
|
|
278
|
+
store_type=request.store_type,
|
|
279
|
+
store_config=request.store_config,
|
|
280
|
+
priority=request.priority,
|
|
281
|
+
cost_per_gb=request.cost_per_gb,
|
|
282
|
+
retrieval_time_ms=request.retrieval_time_ms,
|
|
283
|
+
tier_metadata=request.metadata,
|
|
284
|
+
is_active=request.is_active,
|
|
285
|
+
)
|
|
286
|
+
session.add(tier)
|
|
287
|
+
await session.commit()
|
|
288
|
+
await session.refresh(tier)
|
|
289
|
+
|
|
290
|
+
return _tier_to_response(tier)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@router.get("/tiers/{tier_id}", response_model=StorageTierResponse)
|
|
294
|
+
async def get_storage_tier(
|
|
295
|
+
tier_id: str,
|
|
296
|
+
session: AsyncSession = Depends(get_session),
|
|
297
|
+
) -> StorageTierResponse:
|
|
298
|
+
"""Get a storage tier by ID."""
|
|
299
|
+
result = await session.execute(
|
|
300
|
+
select(StorageTierModel).where(StorageTierModel.id == tier_id)
|
|
301
|
+
)
|
|
302
|
+
tier = result.scalar_one_or_none()
|
|
303
|
+
|
|
304
|
+
if not tier:
|
|
305
|
+
raise HTTPException(status_code=404, detail="Storage tier not found")
|
|
306
|
+
|
|
307
|
+
return _tier_to_response(tier)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@router.put("/tiers/{tier_id}", response_model=StorageTierResponse)
|
|
311
|
+
async def update_storage_tier(
|
|
312
|
+
tier_id: str,
|
|
313
|
+
request: StorageTierUpdate,
|
|
314
|
+
session: AsyncSession = Depends(get_session),
|
|
315
|
+
) -> StorageTierResponse:
|
|
316
|
+
"""Update a storage tier."""
|
|
317
|
+
result = await session.execute(
|
|
318
|
+
select(StorageTierModel).where(StorageTierModel.id == tier_id)
|
|
319
|
+
)
|
|
320
|
+
tier = result.scalar_one_or_none()
|
|
321
|
+
|
|
322
|
+
if not tier:
|
|
323
|
+
raise HTTPException(status_code=404, detail="Storage tier not found")
|
|
324
|
+
|
|
325
|
+
# Check for duplicate name if updating
|
|
326
|
+
if request.name is not None and request.name != tier.name:
|
|
327
|
+
existing = await session.execute(
|
|
328
|
+
select(StorageTierModel).where(StorageTierModel.name == request.name)
|
|
329
|
+
)
|
|
330
|
+
if existing.scalar_one_or_none():
|
|
331
|
+
raise HTTPException(
|
|
332
|
+
status_code=400,
|
|
333
|
+
detail=f"Storage tier with name '{request.name}' already exists",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Apply updates
|
|
337
|
+
if request.name is not None:
|
|
338
|
+
tier.name = request.name
|
|
339
|
+
if request.tier_type is not None:
|
|
340
|
+
tier.tier_type = request.tier_type.value
|
|
341
|
+
if request.store_type is not None:
|
|
342
|
+
tier.store_type = request.store_type
|
|
343
|
+
if request.store_config is not None:
|
|
344
|
+
tier.store_config = request.store_config
|
|
345
|
+
if request.priority is not None:
|
|
346
|
+
tier.priority = request.priority
|
|
347
|
+
if request.cost_per_gb is not None:
|
|
348
|
+
tier.cost_per_gb = request.cost_per_gb
|
|
349
|
+
if request.retrieval_time_ms is not None:
|
|
350
|
+
tier.retrieval_time_ms = request.retrieval_time_ms
|
|
351
|
+
if request.metadata is not None:
|
|
352
|
+
tier.tier_metadata = request.metadata
|
|
353
|
+
if request.is_active is not None:
|
|
354
|
+
tier.is_active = request.is_active
|
|
355
|
+
|
|
356
|
+
await session.commit()
|
|
357
|
+
await session.refresh(tier)
|
|
358
|
+
|
|
359
|
+
return _tier_to_response(tier)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@router.delete("/tiers/{tier_id}", response_model=MessageResponse)
|
|
363
|
+
async def delete_storage_tier(
|
|
364
|
+
tier_id: str,
|
|
365
|
+
session: AsyncSession = Depends(get_session),
|
|
366
|
+
) -> MessageResponse:
|
|
367
|
+
"""Delete a storage tier."""
|
|
368
|
+
result = await session.execute(
|
|
369
|
+
select(StorageTierModel).where(StorageTierModel.id == tier_id)
|
|
370
|
+
)
|
|
371
|
+
tier = result.scalar_one_or_none()
|
|
372
|
+
|
|
373
|
+
if not tier:
|
|
374
|
+
raise HTTPException(status_code=404, detail="Storage tier not found")
|
|
375
|
+
|
|
376
|
+
await session.delete(tier)
|
|
377
|
+
await session.commit()
|
|
378
|
+
|
|
379
|
+
return MessageResponse(message=f"Storage tier '{tier.name}' deleted successfully")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# =============================================================================
|
|
383
|
+
# Tier Policies Endpoints
|
|
384
|
+
# =============================================================================
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@router.get("/policies", response_model=TierPolicyListResponse)
|
|
388
|
+
async def list_tier_policies(
|
|
389
|
+
offset: int = Query(default=0, ge=0),
|
|
390
|
+
limit: int = Query(default=50, ge=1, le=100),
|
|
391
|
+
active_only: bool = Query(default=False),
|
|
392
|
+
policy_type: str | None = Query(default=None),
|
|
393
|
+
from_tier_id: str | None = Query(default=None),
|
|
394
|
+
to_tier_id: str | None = Query(default=None),
|
|
395
|
+
parent_id: str | None = Query(default=None),
|
|
396
|
+
root_only: bool = Query(default=False, description="Only show root policies (no parent)"),
|
|
397
|
+
session: AsyncSession = Depends(get_session),
|
|
398
|
+
) -> TierPolicyListResponse:
|
|
399
|
+
"""List all tier policies with optional filtering."""
|
|
400
|
+
query = select(TierPolicyModel).options(
|
|
401
|
+
selectinload(TierPolicyModel.from_tier),
|
|
402
|
+
selectinload(TierPolicyModel.to_tier),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if active_only:
|
|
406
|
+
query = query.where(TierPolicyModel.is_active == True) # noqa: E712
|
|
407
|
+
|
|
408
|
+
if policy_type:
|
|
409
|
+
query = query.where(TierPolicyModel.policy_type == policy_type)
|
|
410
|
+
|
|
411
|
+
if from_tier_id:
|
|
412
|
+
query = query.where(TierPolicyModel.from_tier_id == from_tier_id)
|
|
413
|
+
|
|
414
|
+
if to_tier_id:
|
|
415
|
+
query = query.where(TierPolicyModel.to_tier_id == to_tier_id)
|
|
416
|
+
|
|
417
|
+
if parent_id:
|
|
418
|
+
query = query.where(TierPolicyModel.parent_id == parent_id)
|
|
419
|
+
|
|
420
|
+
if root_only:
|
|
421
|
+
query = query.where(TierPolicyModel.parent_id == None) # noqa: E711
|
|
422
|
+
|
|
423
|
+
# Get total count
|
|
424
|
+
count_query = select(func.count()).select_from(query.subquery())
|
|
425
|
+
total = (await session.execute(count_query)).scalar() or 0
|
|
426
|
+
|
|
427
|
+
# Apply pagination and ordering
|
|
428
|
+
query = query.order_by(TierPolicyModel.priority, TierPolicyModel.name).offset(offset).limit(limit)
|
|
429
|
+
result = await session.execute(query)
|
|
430
|
+
policies = result.scalars().all()
|
|
431
|
+
|
|
432
|
+
return TierPolicyListResponse(
|
|
433
|
+
items=[_policy_to_response(policy) for policy in policies],
|
|
434
|
+
total=total,
|
|
435
|
+
offset=offset,
|
|
436
|
+
limit=limit,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@router.post("/policies", response_model=TierPolicyResponse, status_code=201)
|
|
441
|
+
async def create_tier_policy(
|
|
442
|
+
request: TierPolicyCreate,
|
|
443
|
+
session: AsyncSession = Depends(get_session),
|
|
444
|
+
) -> TierPolicyResponse:
|
|
445
|
+
"""Create a new tier policy."""
|
|
446
|
+
# Validate from_tier exists
|
|
447
|
+
from_tier = await session.execute(
|
|
448
|
+
select(StorageTierModel).where(StorageTierModel.id == request.from_tier_id)
|
|
449
|
+
)
|
|
450
|
+
if not from_tier.scalar_one_or_none():
|
|
451
|
+
raise HTTPException(status_code=400, detail="Source tier not found")
|
|
452
|
+
|
|
453
|
+
# Validate to_tier exists
|
|
454
|
+
to_tier = await session.execute(
|
|
455
|
+
select(StorageTierModel).where(StorageTierModel.id == request.to_tier_id)
|
|
456
|
+
)
|
|
457
|
+
if not to_tier.scalar_one_or_none():
|
|
458
|
+
raise HTTPException(status_code=400, detail="Destination tier not found")
|
|
459
|
+
|
|
460
|
+
# Validate parent if specified
|
|
461
|
+
if request.parent_id:
|
|
462
|
+
parent = await session.execute(
|
|
463
|
+
select(TierPolicyModel).where(TierPolicyModel.id == request.parent_id)
|
|
464
|
+
)
|
|
465
|
+
parent_policy = parent.scalar_one_or_none()
|
|
466
|
+
if not parent_policy:
|
|
467
|
+
raise HTTPException(status_code=400, detail="Parent policy not found")
|
|
468
|
+
if parent_policy.policy_type != TierPolicyType.COMPOSITE.value:
|
|
469
|
+
raise HTTPException(
|
|
470
|
+
status_code=400,
|
|
471
|
+
detail="Parent policy must be a composite policy",
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
policy = TierPolicyModel(
|
|
475
|
+
name=request.name,
|
|
476
|
+
description=request.description,
|
|
477
|
+
policy_type=request.policy_type.value,
|
|
478
|
+
from_tier_id=request.from_tier_id,
|
|
479
|
+
to_tier_id=request.to_tier_id,
|
|
480
|
+
direction=request.direction.value,
|
|
481
|
+
config=request.config,
|
|
482
|
+
is_active=request.is_active,
|
|
483
|
+
priority=request.priority,
|
|
484
|
+
parent_id=request.parent_id,
|
|
485
|
+
)
|
|
486
|
+
session.add(policy)
|
|
487
|
+
await session.commit()
|
|
488
|
+
await session.refresh(policy, ["from_tier", "to_tier"])
|
|
489
|
+
|
|
490
|
+
return _policy_to_response(policy)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@router.get("/policies/types", response_model=PolicyTypesResponse)
|
|
494
|
+
async def get_policy_types() -> PolicyTypesResponse:
|
|
495
|
+
"""Get available policy types with their configuration schemas."""
|
|
496
|
+
policy_types = [
|
|
497
|
+
PolicyTypeInfo(
|
|
498
|
+
type=TierPolicyTypeEnum.AGE_BASED,
|
|
499
|
+
name="Age-Based",
|
|
500
|
+
description="Migrate items based on age (days/hours since creation)",
|
|
501
|
+
config_schema=AgeBasedPolicyConfig.model_json_schema(),
|
|
502
|
+
),
|
|
503
|
+
PolicyTypeInfo(
|
|
504
|
+
type=TierPolicyTypeEnum.ACCESS_BASED,
|
|
505
|
+
name="Access-Based",
|
|
506
|
+
description="Migrate based on access patterns (inactive days or access count)",
|
|
507
|
+
config_schema=AccessBasedPolicyConfig.model_json_schema(),
|
|
508
|
+
),
|
|
509
|
+
PolicyTypeInfo(
|
|
510
|
+
type=TierPolicyTypeEnum.SIZE_BASED,
|
|
511
|
+
name="Size-Based",
|
|
512
|
+
description="Migrate based on item size or tier capacity",
|
|
513
|
+
config_schema=SizeBasedPolicyConfig.model_json_schema(),
|
|
514
|
+
),
|
|
515
|
+
PolicyTypeInfo(
|
|
516
|
+
type=TierPolicyTypeEnum.SCHEDULED,
|
|
517
|
+
name="Scheduled",
|
|
518
|
+
description="Migrate on a schedule (specific days/times)",
|
|
519
|
+
config_schema=ScheduledPolicyConfig.model_json_schema(),
|
|
520
|
+
),
|
|
521
|
+
PolicyTypeInfo(
|
|
522
|
+
type=TierPolicyTypeEnum.COMPOSITE,
|
|
523
|
+
name="Composite",
|
|
524
|
+
description="Combine multiple policies with AND/OR logic",
|
|
525
|
+
config_schema=CompositePolicyConfig.model_json_schema(),
|
|
526
|
+
),
|
|
527
|
+
PolicyTypeInfo(
|
|
528
|
+
type=TierPolicyTypeEnum.CUSTOM,
|
|
529
|
+
name="Custom",
|
|
530
|
+
description="Define custom migration logic with a predicate expression",
|
|
531
|
+
config_schema=CustomPolicyConfig.model_json_schema(),
|
|
532
|
+
),
|
|
533
|
+
]
|
|
534
|
+
return PolicyTypesResponse(policy_types=policy_types)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@router.get("/policies/{policy_id}", response_model=TierPolicyResponse)
|
|
538
|
+
async def get_tier_policy(
|
|
539
|
+
policy_id: str,
|
|
540
|
+
session: AsyncSession = Depends(get_session),
|
|
541
|
+
) -> TierPolicyResponse:
|
|
542
|
+
"""Get a tier policy by ID."""
|
|
543
|
+
result = await session.execute(
|
|
544
|
+
select(TierPolicyModel)
|
|
545
|
+
.options(
|
|
546
|
+
selectinload(TierPolicyModel.from_tier),
|
|
547
|
+
selectinload(TierPolicyModel.to_tier),
|
|
548
|
+
)
|
|
549
|
+
.where(TierPolicyModel.id == policy_id)
|
|
550
|
+
)
|
|
551
|
+
policy = result.scalar_one_or_none()
|
|
552
|
+
|
|
553
|
+
if not policy:
|
|
554
|
+
raise HTTPException(status_code=404, detail="Tier policy not found")
|
|
555
|
+
|
|
556
|
+
return _policy_to_response(policy)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@router.get("/policies/{policy_id}/tree", response_model=TierPolicyWithChildren)
|
|
560
|
+
async def get_tier_policy_tree(
|
|
561
|
+
policy_id: str,
|
|
562
|
+
session: AsyncSession = Depends(get_session),
|
|
563
|
+
) -> TierPolicyWithChildren:
|
|
564
|
+
"""Get a tier policy with all nested children (for composite policies)."""
|
|
565
|
+
result = await session.execute(
|
|
566
|
+
select(TierPolicyModel)
|
|
567
|
+
.options(
|
|
568
|
+
selectinload(TierPolicyModel.from_tier),
|
|
569
|
+
selectinload(TierPolicyModel.to_tier),
|
|
570
|
+
selectinload(TierPolicyModel.children).selectinload(TierPolicyModel.from_tier),
|
|
571
|
+
selectinload(TierPolicyModel.children).selectinload(TierPolicyModel.to_tier),
|
|
572
|
+
selectinload(TierPolicyModel.children).selectinload(TierPolicyModel.children),
|
|
573
|
+
)
|
|
574
|
+
.where(TierPolicyModel.id == policy_id)
|
|
575
|
+
)
|
|
576
|
+
policy = result.scalar_one_or_none()
|
|
577
|
+
|
|
578
|
+
if not policy:
|
|
579
|
+
raise HTTPException(status_code=404, detail="Tier policy not found")
|
|
580
|
+
|
|
581
|
+
return _policy_to_tree(policy)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@router.put("/policies/{policy_id}", response_model=TierPolicyResponse)
|
|
585
|
+
async def update_tier_policy(
|
|
586
|
+
policy_id: str,
|
|
587
|
+
request: TierPolicyUpdate,
|
|
588
|
+
session: AsyncSession = Depends(get_session),
|
|
589
|
+
) -> TierPolicyResponse:
|
|
590
|
+
"""Update a tier policy."""
|
|
591
|
+
result = await session.execute(
|
|
592
|
+
select(TierPolicyModel)
|
|
593
|
+
.options(
|
|
594
|
+
selectinload(TierPolicyModel.from_tier),
|
|
595
|
+
selectinload(TierPolicyModel.to_tier),
|
|
596
|
+
)
|
|
597
|
+
.where(TierPolicyModel.id == policy_id)
|
|
598
|
+
)
|
|
599
|
+
policy = result.scalar_one_or_none()
|
|
600
|
+
|
|
601
|
+
if not policy:
|
|
602
|
+
raise HTTPException(status_code=404, detail="Tier policy not found")
|
|
603
|
+
|
|
604
|
+
# Validate from_tier if updating
|
|
605
|
+
if request.from_tier_id is not None:
|
|
606
|
+
from_tier = await session.execute(
|
|
607
|
+
select(StorageTierModel).where(StorageTierModel.id == request.from_tier_id)
|
|
608
|
+
)
|
|
609
|
+
if not from_tier.scalar_one_or_none():
|
|
610
|
+
raise HTTPException(status_code=400, detail="Source tier not found")
|
|
611
|
+
|
|
612
|
+
# Validate to_tier if updating
|
|
613
|
+
if request.to_tier_id is not None:
|
|
614
|
+
to_tier = await session.execute(
|
|
615
|
+
select(StorageTierModel).where(StorageTierModel.id == request.to_tier_id)
|
|
616
|
+
)
|
|
617
|
+
if not to_tier.scalar_one_or_none():
|
|
618
|
+
raise HTTPException(status_code=400, detail="Destination tier not found")
|
|
619
|
+
|
|
620
|
+
# Apply updates
|
|
621
|
+
if request.name is not None:
|
|
622
|
+
policy.name = request.name
|
|
623
|
+
if request.description is not None:
|
|
624
|
+
policy.description = request.description
|
|
625
|
+
if request.policy_type is not None:
|
|
626
|
+
policy.policy_type = request.policy_type.value
|
|
627
|
+
if request.from_tier_id is not None:
|
|
628
|
+
policy.from_tier_id = request.from_tier_id
|
|
629
|
+
if request.to_tier_id is not None:
|
|
630
|
+
policy.to_tier_id = request.to_tier_id
|
|
631
|
+
if request.direction is not None:
|
|
632
|
+
policy.direction = request.direction.value
|
|
633
|
+
if request.config is not None:
|
|
634
|
+
policy.config = request.config
|
|
635
|
+
if request.is_active is not None:
|
|
636
|
+
policy.is_active = request.is_active
|
|
637
|
+
if request.priority is not None:
|
|
638
|
+
policy.priority = request.priority
|
|
639
|
+
if request.parent_id is not None:
|
|
640
|
+
policy.parent_id = request.parent_id
|
|
641
|
+
|
|
642
|
+
await session.commit()
|
|
643
|
+
await session.refresh(policy, ["from_tier", "to_tier"])
|
|
644
|
+
|
|
645
|
+
return _policy_to_response(policy)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@router.delete("/policies/{policy_id}", response_model=MessageResponse)
|
|
649
|
+
async def delete_tier_policy(
|
|
650
|
+
policy_id: str,
|
|
651
|
+
session: AsyncSession = Depends(get_session),
|
|
652
|
+
) -> MessageResponse:
|
|
653
|
+
"""Delete a tier policy."""
|
|
654
|
+
result = await session.execute(
|
|
655
|
+
select(TierPolicyModel).where(TierPolicyModel.id == policy_id)
|
|
656
|
+
)
|
|
657
|
+
policy = result.scalar_one_or_none()
|
|
658
|
+
|
|
659
|
+
if not policy:
|
|
660
|
+
raise HTTPException(status_code=404, detail="Tier policy not found")
|
|
661
|
+
|
|
662
|
+
policy_name = policy.name
|
|
663
|
+
await session.delete(policy)
|
|
664
|
+
await session.commit()
|
|
665
|
+
|
|
666
|
+
return MessageResponse(message=f"Tier policy '{policy_name}' deleted successfully")
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
# =============================================================================
|
|
670
|
+
# Tiering Configurations Endpoints
|
|
671
|
+
# =============================================================================
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
@router.get("/configs", response_model=TieringConfigListResponse)
|
|
675
|
+
async def list_tiering_configs(
|
|
676
|
+
offset: int = Query(default=0, ge=0),
|
|
677
|
+
limit: int = Query(default=50, ge=1, le=100),
|
|
678
|
+
active_only: bool = Query(default=False),
|
|
679
|
+
session: AsyncSession = Depends(get_session),
|
|
680
|
+
) -> TieringConfigListResponse:
|
|
681
|
+
"""List all tiering configurations."""
|
|
682
|
+
query = select(TieringConfigModel).options(
|
|
683
|
+
selectinload(TieringConfigModel.default_tier)
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
if active_only:
|
|
687
|
+
query = query.where(TieringConfigModel.is_active == True) # noqa: E712
|
|
688
|
+
|
|
689
|
+
# Get total count
|
|
690
|
+
count_query = select(func.count()).select_from(query.subquery())
|
|
691
|
+
total = (await session.execute(count_query)).scalar() or 0
|
|
692
|
+
|
|
693
|
+
# Apply pagination and ordering
|
|
694
|
+
query = query.order_by(TieringConfigModel.name).offset(offset).limit(limit)
|
|
695
|
+
result = await session.execute(query)
|
|
696
|
+
configs = result.scalars().all()
|
|
697
|
+
|
|
698
|
+
return TieringConfigListResponse(
|
|
699
|
+
items=[_config_to_response(config) for config in configs],
|
|
700
|
+
total=total,
|
|
701
|
+
offset=offset,
|
|
702
|
+
limit=limit,
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
@router.post("/configs", response_model=TieringConfigResponse, status_code=201)
|
|
707
|
+
async def create_tiering_config(
|
|
708
|
+
request: TieringConfigCreate,
|
|
709
|
+
session: AsyncSession = Depends(get_session),
|
|
710
|
+
) -> TieringConfigResponse:
|
|
711
|
+
"""Create a new tiering configuration."""
|
|
712
|
+
# Check for duplicate name
|
|
713
|
+
existing = await session.execute(
|
|
714
|
+
select(TieringConfigModel).where(TieringConfigModel.name == request.name)
|
|
715
|
+
)
|
|
716
|
+
if existing.scalar_one_or_none():
|
|
717
|
+
raise HTTPException(
|
|
718
|
+
status_code=400,
|
|
719
|
+
detail=f"Tiering configuration with name '{request.name}' already exists",
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
# Validate default_tier if specified
|
|
723
|
+
if request.default_tier_id:
|
|
724
|
+
default_tier = await session.execute(
|
|
725
|
+
select(StorageTierModel).where(StorageTierModel.id == request.default_tier_id)
|
|
726
|
+
)
|
|
727
|
+
if not default_tier.scalar_one_or_none():
|
|
728
|
+
raise HTTPException(status_code=400, detail="Default tier not found")
|
|
729
|
+
|
|
730
|
+
config = TieringConfigModel(
|
|
731
|
+
name=request.name,
|
|
732
|
+
description=request.description,
|
|
733
|
+
default_tier_id=request.default_tier_id,
|
|
734
|
+
enable_promotion=request.enable_promotion,
|
|
735
|
+
promotion_threshold=request.promotion_threshold,
|
|
736
|
+
check_interval_hours=request.check_interval_hours,
|
|
737
|
+
batch_size=request.batch_size,
|
|
738
|
+
enable_parallel_migration=request.enable_parallel_migration,
|
|
739
|
+
max_parallel_migrations=request.max_parallel_migrations,
|
|
740
|
+
is_active=request.is_active,
|
|
741
|
+
)
|
|
742
|
+
session.add(config)
|
|
743
|
+
await session.commit()
|
|
744
|
+
await session.refresh(config, ["default_tier"])
|
|
745
|
+
|
|
746
|
+
return _config_to_response(config)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
@router.get("/configs/{config_id}", response_model=TieringConfigResponse)
|
|
750
|
+
async def get_tiering_config(
|
|
751
|
+
config_id: str,
|
|
752
|
+
session: AsyncSession = Depends(get_session),
|
|
753
|
+
) -> TieringConfigResponse:
|
|
754
|
+
"""Get a tiering configuration by ID."""
|
|
755
|
+
result = await session.execute(
|
|
756
|
+
select(TieringConfigModel)
|
|
757
|
+
.options(selectinload(TieringConfigModel.default_tier))
|
|
758
|
+
.where(TieringConfigModel.id == config_id)
|
|
759
|
+
)
|
|
760
|
+
config = result.scalar_one_or_none()
|
|
761
|
+
|
|
762
|
+
if not config:
|
|
763
|
+
raise HTTPException(status_code=404, detail="Tiering configuration not found")
|
|
764
|
+
|
|
765
|
+
return _config_to_response(config)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
@router.put("/configs/{config_id}", response_model=TieringConfigResponse)
|
|
769
|
+
async def update_tiering_config(
|
|
770
|
+
config_id: str,
|
|
771
|
+
request: TieringConfigUpdate,
|
|
772
|
+
session: AsyncSession = Depends(get_session),
|
|
773
|
+
) -> TieringConfigResponse:
|
|
774
|
+
"""Update a tiering configuration."""
|
|
775
|
+
result = await session.execute(
|
|
776
|
+
select(TieringConfigModel)
|
|
777
|
+
.options(selectinload(TieringConfigModel.default_tier))
|
|
778
|
+
.where(TieringConfigModel.id == config_id)
|
|
779
|
+
)
|
|
780
|
+
config = result.scalar_one_or_none()
|
|
781
|
+
|
|
782
|
+
if not config:
|
|
783
|
+
raise HTTPException(status_code=404, detail="Tiering configuration not found")
|
|
784
|
+
|
|
785
|
+
# Check for duplicate name if updating
|
|
786
|
+
if request.name is not None and request.name != config.name:
|
|
787
|
+
existing = await session.execute(
|
|
788
|
+
select(TieringConfigModel).where(TieringConfigModel.name == request.name)
|
|
789
|
+
)
|
|
790
|
+
if existing.scalar_one_or_none():
|
|
791
|
+
raise HTTPException(
|
|
792
|
+
status_code=400,
|
|
793
|
+
detail=f"Tiering configuration with name '{request.name}' already exists",
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# Apply updates
|
|
797
|
+
if request.name is not None:
|
|
798
|
+
config.name = request.name
|
|
799
|
+
if request.description is not None:
|
|
800
|
+
config.description = request.description
|
|
801
|
+
if request.default_tier_id is not None:
|
|
802
|
+
config.default_tier_id = request.default_tier_id
|
|
803
|
+
if request.enable_promotion is not None:
|
|
804
|
+
config.enable_promotion = request.enable_promotion
|
|
805
|
+
if request.promotion_threshold is not None:
|
|
806
|
+
config.promotion_threshold = request.promotion_threshold
|
|
807
|
+
if request.check_interval_hours is not None:
|
|
808
|
+
config.check_interval_hours = request.check_interval_hours
|
|
809
|
+
if request.batch_size is not None:
|
|
810
|
+
config.batch_size = request.batch_size
|
|
811
|
+
if request.enable_parallel_migration is not None:
|
|
812
|
+
config.enable_parallel_migration = request.enable_parallel_migration
|
|
813
|
+
if request.max_parallel_migrations is not None:
|
|
814
|
+
config.max_parallel_migrations = request.max_parallel_migrations
|
|
815
|
+
if request.is_active is not None:
|
|
816
|
+
config.is_active = request.is_active
|
|
817
|
+
|
|
818
|
+
await session.commit()
|
|
819
|
+
await session.refresh(config, ["default_tier"])
|
|
820
|
+
|
|
821
|
+
return _config_to_response(config)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
@router.delete("/configs/{config_id}", response_model=MessageResponse)
|
|
825
|
+
async def delete_tiering_config(
|
|
826
|
+
config_id: str,
|
|
827
|
+
session: AsyncSession = Depends(get_session),
|
|
828
|
+
) -> MessageResponse:
|
|
829
|
+
"""Delete a tiering configuration."""
|
|
830
|
+
result = await session.execute(
|
|
831
|
+
select(TieringConfigModel).where(TieringConfigModel.id == config_id)
|
|
832
|
+
)
|
|
833
|
+
config = result.scalar_one_or_none()
|
|
834
|
+
|
|
835
|
+
if not config:
|
|
836
|
+
raise HTTPException(status_code=404, detail="Tiering configuration not found")
|
|
837
|
+
|
|
838
|
+
config_name = config.name
|
|
839
|
+
await session.delete(config)
|
|
840
|
+
await session.commit()
|
|
841
|
+
|
|
842
|
+
return MessageResponse(message=f"Tiering configuration '{config_name}' deleted successfully")
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
# =============================================================================
|
|
846
|
+
# Migration History Endpoints
|
|
847
|
+
# =============================================================================
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
@router.get("/migrations", response_model=MigrationHistoryListResponse)
|
|
851
|
+
async def list_migration_history(
|
|
852
|
+
offset: int = Query(default=0, ge=0),
|
|
853
|
+
limit: int = Query(default=50, ge=1, le=100),
|
|
854
|
+
policy_id: str | None = Query(default=None),
|
|
855
|
+
status: str | None = Query(default=None),
|
|
856
|
+
from_tier_id: str | None = Query(default=None),
|
|
857
|
+
to_tier_id: str | None = Query(default=None),
|
|
858
|
+
session: AsyncSession = Depends(get_session),
|
|
859
|
+
) -> MigrationHistoryListResponse:
|
|
860
|
+
"""List migration history with optional filtering."""
|
|
861
|
+
query = select(TierMigrationHistoryModel)
|
|
862
|
+
|
|
863
|
+
if policy_id:
|
|
864
|
+
query = query.where(TierMigrationHistoryModel.policy_id == policy_id)
|
|
865
|
+
|
|
866
|
+
if status:
|
|
867
|
+
query = query.where(TierMigrationHistoryModel.status == status)
|
|
868
|
+
|
|
869
|
+
if from_tier_id:
|
|
870
|
+
query = query.where(TierMigrationHistoryModel.from_tier_id == from_tier_id)
|
|
871
|
+
|
|
872
|
+
if to_tier_id:
|
|
873
|
+
query = query.where(TierMigrationHistoryModel.to_tier_id == to_tier_id)
|
|
874
|
+
|
|
875
|
+
# Get total count
|
|
876
|
+
count_query = select(func.count()).select_from(query.subquery())
|
|
877
|
+
total = (await session.execute(count_query)).scalar() or 0
|
|
878
|
+
|
|
879
|
+
# Apply pagination and ordering
|
|
880
|
+
query = query.order_by(TierMigrationHistoryModel.started_at.desc()).offset(offset).limit(limit)
|
|
881
|
+
result = await session.execute(query)
|
|
882
|
+
migrations = result.scalars().all()
|
|
883
|
+
|
|
884
|
+
# Get tier names for display
|
|
885
|
+
tier_ids = set()
|
|
886
|
+
policy_ids = set()
|
|
887
|
+
for m in migrations:
|
|
888
|
+
tier_ids.add(m.from_tier_id)
|
|
889
|
+
tier_ids.add(m.to_tier_id)
|
|
890
|
+
if m.policy_id:
|
|
891
|
+
policy_ids.add(m.policy_id)
|
|
892
|
+
|
|
893
|
+
# Fetch tier names
|
|
894
|
+
tier_names: dict[str, str] = {}
|
|
895
|
+
if tier_ids:
|
|
896
|
+
tiers_result = await session.execute(
|
|
897
|
+
select(StorageTierModel).where(StorageTierModel.id.in_(tier_ids))
|
|
898
|
+
)
|
|
899
|
+
for tier in tiers_result.scalars():
|
|
900
|
+
tier_names[tier.id] = tier.name
|
|
901
|
+
|
|
902
|
+
# Fetch policy names
|
|
903
|
+
policy_names: dict[str, str] = {}
|
|
904
|
+
if policy_ids:
|
|
905
|
+
policies_result = await session.execute(
|
|
906
|
+
select(TierPolicyModel).where(TierPolicyModel.id.in_(policy_ids))
|
|
907
|
+
)
|
|
908
|
+
for policy in policies_result.scalars():
|
|
909
|
+
policy_names[policy.id] = policy.name
|
|
910
|
+
|
|
911
|
+
return MigrationHistoryListResponse(
|
|
912
|
+
items=[
|
|
913
|
+
_migration_to_response(
|
|
914
|
+
m,
|
|
915
|
+
from_tier_name=tier_names.get(m.from_tier_id),
|
|
916
|
+
to_tier_name=tier_names.get(m.to_tier_id),
|
|
917
|
+
policy_name=policy_names.get(m.policy_id) if m.policy_id else None,
|
|
918
|
+
)
|
|
919
|
+
for m in migrations
|
|
920
|
+
],
|
|
921
|
+
total=total,
|
|
922
|
+
offset=offset,
|
|
923
|
+
limit=limit,
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
@router.get("/migrations/{migration_id}", response_model=MigrationHistoryResponse)
|
|
928
|
+
async def get_migration_history(
|
|
929
|
+
migration_id: str,
|
|
930
|
+
session: AsyncSession = Depends(get_session),
|
|
931
|
+
) -> MigrationHistoryResponse:
|
|
932
|
+
"""Get a migration history entry by ID."""
|
|
933
|
+
result = await session.execute(
|
|
934
|
+
select(TierMigrationHistoryModel).where(TierMigrationHistoryModel.id == migration_id)
|
|
935
|
+
)
|
|
936
|
+
migration = result.scalar_one_or_none()
|
|
937
|
+
|
|
938
|
+
if not migration:
|
|
939
|
+
raise HTTPException(status_code=404, detail="Migration history entry not found")
|
|
940
|
+
|
|
941
|
+
# Get tier names
|
|
942
|
+
tier_names: dict[str, str] = {}
|
|
943
|
+
tiers_result = await session.execute(
|
|
944
|
+
select(StorageTierModel).where(
|
|
945
|
+
StorageTierModel.id.in_([migration.from_tier_id, migration.to_tier_id])
|
|
946
|
+
)
|
|
947
|
+
)
|
|
948
|
+
for tier in tiers_result.scalars():
|
|
949
|
+
tier_names[tier.id] = tier.name
|
|
950
|
+
|
|
951
|
+
# Get policy name
|
|
952
|
+
policy_name = None
|
|
953
|
+
if migration.policy_id:
|
|
954
|
+
policy_result = await session.execute(
|
|
955
|
+
select(TierPolicyModel).where(TierPolicyModel.id == migration.policy_id)
|
|
956
|
+
)
|
|
957
|
+
policy = policy_result.scalar_one_or_none()
|
|
958
|
+
if policy:
|
|
959
|
+
policy_name = policy.name
|
|
960
|
+
|
|
961
|
+
return _migration_to_response(
|
|
962
|
+
migration,
|
|
963
|
+
from_tier_name=tier_names.get(migration.from_tier_id),
|
|
964
|
+
to_tier_name=tier_names.get(migration.to_tier_id),
|
|
965
|
+
policy_name=policy_name,
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
# =============================================================================
|
|
970
|
+
# Statistics Endpoints
|
|
971
|
+
# =============================================================================
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
@router.get("/stats", response_model=TieringStatistics)
|
|
975
|
+
async def get_tiering_statistics(
|
|
976
|
+
session: AsyncSession = Depends(get_session),
|
|
977
|
+
) -> TieringStatistics:
|
|
978
|
+
"""Get overall tiering statistics."""
|
|
979
|
+
# Count tiers
|
|
980
|
+
total_tiers = (
|
|
981
|
+
await session.execute(select(func.count()).select_from(StorageTierModel))
|
|
982
|
+
).scalar() or 0
|
|
983
|
+
active_tiers = (
|
|
984
|
+
await session.execute(
|
|
985
|
+
select(func.count())
|
|
986
|
+
.select_from(StorageTierModel)
|
|
987
|
+
.where(StorageTierModel.is_active == True) # noqa: E712
|
|
988
|
+
)
|
|
989
|
+
).scalar() or 0
|
|
990
|
+
|
|
991
|
+
# Count policies
|
|
992
|
+
total_policies = (
|
|
993
|
+
await session.execute(select(func.count()).select_from(TierPolicyModel))
|
|
994
|
+
).scalar() or 0
|
|
995
|
+
active_policies = (
|
|
996
|
+
await session.execute(
|
|
997
|
+
select(func.count())
|
|
998
|
+
.select_from(TierPolicyModel)
|
|
999
|
+
.where(TierPolicyModel.is_active == True) # noqa: E712
|
|
1000
|
+
)
|
|
1001
|
+
).scalar() or 0
|
|
1002
|
+
composite_policies = (
|
|
1003
|
+
await session.execute(
|
|
1004
|
+
select(func.count())
|
|
1005
|
+
.select_from(TierPolicyModel)
|
|
1006
|
+
.where(TierPolicyModel.policy_type == TierPolicyType.COMPOSITE.value)
|
|
1007
|
+
)
|
|
1008
|
+
).scalar() or 0
|
|
1009
|
+
|
|
1010
|
+
# Count migrations
|
|
1011
|
+
total_migrations = (
|
|
1012
|
+
await session.execute(select(func.count()).select_from(TierMigrationHistoryModel))
|
|
1013
|
+
).scalar() or 0
|
|
1014
|
+
successful_migrations = (
|
|
1015
|
+
await session.execute(
|
|
1016
|
+
select(func.count())
|
|
1017
|
+
.select_from(TierMigrationHistoryModel)
|
|
1018
|
+
.where(TierMigrationHistoryModel.status == "completed")
|
|
1019
|
+
)
|
|
1020
|
+
).scalar() or 0
|
|
1021
|
+
failed_migrations = (
|
|
1022
|
+
await session.execute(
|
|
1023
|
+
select(func.count())
|
|
1024
|
+
.select_from(TierMigrationHistoryModel)
|
|
1025
|
+
.where(TierMigrationHistoryModel.status == "failed")
|
|
1026
|
+
)
|
|
1027
|
+
).scalar() or 0
|
|
1028
|
+
|
|
1029
|
+
# Total bytes migrated
|
|
1030
|
+
total_bytes_migrated = (
|
|
1031
|
+
await session.execute(
|
|
1032
|
+
select(func.sum(TierMigrationHistoryModel.size_bytes))
|
|
1033
|
+
.where(TierMigrationHistoryModel.status == "completed")
|
|
1034
|
+
)
|
|
1035
|
+
).scalar() or 0
|
|
1036
|
+
|
|
1037
|
+
# Per-tier statistics
|
|
1038
|
+
tier_stats: list[TierStatistics] = []
|
|
1039
|
+
tiers_result = await session.execute(select(StorageTierModel))
|
|
1040
|
+
for tier in tiers_result.scalars():
|
|
1041
|
+
# Count policies for this tier
|
|
1042
|
+
policy_count = (
|
|
1043
|
+
await session.execute(
|
|
1044
|
+
select(func.count())
|
|
1045
|
+
.select_from(TierPolicyModel)
|
|
1046
|
+
.where(TierPolicyModel.from_tier_id == tier.id)
|
|
1047
|
+
)
|
|
1048
|
+
).scalar() or 0
|
|
1049
|
+
|
|
1050
|
+
tier_stats.append(
|
|
1051
|
+
TierStatistics(
|
|
1052
|
+
tier_id=tier.id,
|
|
1053
|
+
tier_name=tier.name,
|
|
1054
|
+
tier_type=TierTypeEnum(tier.tier_type),
|
|
1055
|
+
item_count=0, # Would need to track items per tier
|
|
1056
|
+
total_size_bytes=0,
|
|
1057
|
+
total_size_gb=0.0,
|
|
1058
|
+
estimated_cost=None,
|
|
1059
|
+
policy_count=policy_count,
|
|
1060
|
+
)
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
return TieringStatistics(
|
|
1064
|
+
total_tiers=total_tiers,
|
|
1065
|
+
active_tiers=active_tiers,
|
|
1066
|
+
total_policies=total_policies,
|
|
1067
|
+
active_policies=active_policies,
|
|
1068
|
+
composite_policies=composite_policies,
|
|
1069
|
+
total_migrations=total_migrations,
|
|
1070
|
+
successful_migrations=successful_migrations,
|
|
1071
|
+
failed_migrations=failed_migrations,
|
|
1072
|
+
total_bytes_migrated=total_bytes_migrated,
|
|
1073
|
+
tier_stats=tier_stats,
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
# =============================================================================
|
|
1078
|
+
# Policy Execution Endpoints
|
|
1079
|
+
# =============================================================================
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
@router.post("/policies/{policy_id}/execute", response_model=PolicyExecutionResponse)
|
|
1083
|
+
async def execute_policy(
|
|
1084
|
+
policy_id: str,
|
|
1085
|
+
request: PolicyExecutionRequest | None = None,
|
|
1086
|
+
session: AsyncSession = Depends(get_session),
|
|
1087
|
+
) -> PolicyExecutionResponse:
|
|
1088
|
+
"""Execute a tier policy to migrate eligible items.
|
|
1089
|
+
|
|
1090
|
+
This endpoint triggers the actual migration of items based on the policy
|
|
1091
|
+
rules. Use dry_run=True to preview what would be migrated without making
|
|
1092
|
+
actual changes.
|
|
1093
|
+
|
|
1094
|
+
Args:
|
|
1095
|
+
policy_id: Policy ID to execute.
|
|
1096
|
+
request: Execution options (dry_run, batch_size).
|
|
1097
|
+
session: Database session.
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
Execution result with migration details.
|
|
1101
|
+
"""
|
|
1102
|
+
from ..core.tiering import TieringService
|
|
1103
|
+
|
|
1104
|
+
dry_run = request.dry_run if request else False
|
|
1105
|
+
batch_size = request.batch_size if request else 100
|
|
1106
|
+
|
|
1107
|
+
service = TieringService(session)
|
|
1108
|
+
|
|
1109
|
+
try:
|
|
1110
|
+
result = await service.execute_policy(
|
|
1111
|
+
policy_id=policy_id,
|
|
1112
|
+
dry_run=dry_run,
|
|
1113
|
+
batch_size=batch_size,
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
return PolicyExecutionResponse(
|
|
1117
|
+
policy_id=policy_id,
|
|
1118
|
+
dry_run=result.dry_run,
|
|
1119
|
+
start_time=result.start_time,
|
|
1120
|
+
end_time=result.end_time,
|
|
1121
|
+
duration_seconds=result.duration_seconds,
|
|
1122
|
+
items_scanned=result.items_scanned,
|
|
1123
|
+
items_migrated=result.items_migrated,
|
|
1124
|
+
items_failed=result.items_failed,
|
|
1125
|
+
bytes_migrated=result.bytes_migrated,
|
|
1126
|
+
success_rate=result.success_rate,
|
|
1127
|
+
errors=result.errors,
|
|
1128
|
+
migrations=[
|
|
1129
|
+
MigrationItemResponse(
|
|
1130
|
+
item_id=m.item_id,
|
|
1131
|
+
from_tier=m.from_tier,
|
|
1132
|
+
to_tier=m.to_tier,
|
|
1133
|
+
success=m.success,
|
|
1134
|
+
size_bytes=m.size_bytes,
|
|
1135
|
+
error_message=m.error_message,
|
|
1136
|
+
duration_ms=m.duration_ms,
|
|
1137
|
+
)
|
|
1138
|
+
for m in result.migrations
|
|
1139
|
+
],
|
|
1140
|
+
)
|
|
1141
|
+
except ValueError as e:
|
|
1142
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
1143
|
+
except Exception as e:
|
|
1144
|
+
raise HTTPException(status_code=500, detail=f"Policy execution failed: {str(e)}")
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
@router.post("/items/{item_id}/migrate", response_model=MigrationItemResponse)
|
|
1148
|
+
async def migrate_item(
|
|
1149
|
+
item_id: str,
|
|
1150
|
+
request: MigrateItemRequest,
|
|
1151
|
+
session: AsyncSession = Depends(get_session),
|
|
1152
|
+
) -> MigrationItemResponse:
|
|
1153
|
+
"""Migrate a single item between tiers.
|
|
1154
|
+
|
|
1155
|
+
Args:
|
|
1156
|
+
item_id: Item ID to migrate.
|
|
1157
|
+
request: Migration request with source and destination tiers.
|
|
1158
|
+
session: Database session.
|
|
1159
|
+
|
|
1160
|
+
Returns:
|
|
1161
|
+
Migration result.
|
|
1162
|
+
"""
|
|
1163
|
+
from ..core.tiering import TieringService
|
|
1164
|
+
|
|
1165
|
+
service = TieringService(session)
|
|
1166
|
+
|
|
1167
|
+
try:
|
|
1168
|
+
result = await service.migrate_item(
|
|
1169
|
+
item_id=item_id,
|
|
1170
|
+
from_tier_id=request.from_tier_id,
|
|
1171
|
+
to_tier_id=request.to_tier_id,
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
return MigrationItemResponse(
|
|
1175
|
+
item_id=result.item_id,
|
|
1176
|
+
from_tier=result.from_tier,
|
|
1177
|
+
to_tier=result.to_tier,
|
|
1178
|
+
success=result.success,
|
|
1179
|
+
size_bytes=result.size_bytes,
|
|
1180
|
+
error_message=result.error_message,
|
|
1181
|
+
duration_ms=result.duration_ms,
|
|
1182
|
+
)
|
|
1183
|
+
except ValueError as e:
|
|
1184
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
1185
|
+
except Exception as e:
|
|
1186
|
+
raise HTTPException(status_code=500, detail=f"Migration failed: {str(e)}")
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
@router.post("/items/{item_id}/access", response_model=MessageResponse)
|
|
1190
|
+
async def record_item_access(
|
|
1191
|
+
item_id: str,
|
|
1192
|
+
tier_id: str = Query(..., description="Current tier ID of the item"),
|
|
1193
|
+
session: AsyncSession = Depends(get_session),
|
|
1194
|
+
) -> MessageResponse:
|
|
1195
|
+
"""Record an access to an item for intelligent tiering.
|
|
1196
|
+
|
|
1197
|
+
Call this when an item is accessed to track access patterns for
|
|
1198
|
+
access-based tiering policies.
|
|
1199
|
+
|
|
1200
|
+
Args:
|
|
1201
|
+
item_id: Accessed item ID.
|
|
1202
|
+
tier_id: Current tier ID.
|
|
1203
|
+
session: Database session.
|
|
1204
|
+
|
|
1205
|
+
Returns:
|
|
1206
|
+
Success message.
|
|
1207
|
+
"""
|
|
1208
|
+
from ..core.tiering import TieringService
|
|
1209
|
+
|
|
1210
|
+
service = TieringService(session)
|
|
1211
|
+
await service.record_access(item_id, tier_id)
|
|
1212
|
+
|
|
1213
|
+
return MessageResponse(message=f"Access recorded for item '{item_id}'")
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
@router.post("/process", response_model=ProcessPoliciesResponse)
|
|
1217
|
+
async def process_all_policies(
|
|
1218
|
+
session: AsyncSession = Depends(get_session),
|
|
1219
|
+
) -> ProcessPoliciesResponse:
|
|
1220
|
+
"""Process all active tiering policies.
|
|
1221
|
+
|
|
1222
|
+
This endpoint triggers evaluation and execution of all active policies.
|
|
1223
|
+
Use this for manual triggering or testing. In production, this is typically
|
|
1224
|
+
called by the background scheduler.
|
|
1225
|
+
|
|
1226
|
+
Returns:
|
|
1227
|
+
Summary of all policy executions.
|
|
1228
|
+
"""
|
|
1229
|
+
from ..core.tiering import TieringService
|
|
1230
|
+
|
|
1231
|
+
service = TieringService(session)
|
|
1232
|
+
|
|
1233
|
+
try:
|
|
1234
|
+
results = await service.process_due_policies()
|
|
1235
|
+
|
|
1236
|
+
total_scanned = sum(r.items_scanned for r in results)
|
|
1237
|
+
total_migrated = sum(r.items_migrated for r in results)
|
|
1238
|
+
total_failed = sum(r.items_failed for r in results)
|
|
1239
|
+
total_bytes = sum(r.bytes_migrated for r in results)
|
|
1240
|
+
all_errors = [e for r in results for e in r.errors]
|
|
1241
|
+
|
|
1242
|
+
return ProcessPoliciesResponse(
|
|
1243
|
+
policies_executed=len(results),
|
|
1244
|
+
total_items_scanned=total_scanned,
|
|
1245
|
+
total_items_migrated=total_migrated,
|
|
1246
|
+
total_items_failed=total_failed,
|
|
1247
|
+
total_bytes_migrated=total_bytes,
|
|
1248
|
+
errors=all_errors,
|
|
1249
|
+
policy_results=[
|
|
1250
|
+
PolicyExecutionSummary(
|
|
1251
|
+
items_scanned=r.items_scanned,
|
|
1252
|
+
items_migrated=r.items_migrated,
|
|
1253
|
+
items_failed=r.items_failed,
|
|
1254
|
+
bytes_migrated=r.bytes_migrated,
|
|
1255
|
+
duration_seconds=r.duration_seconds,
|
|
1256
|
+
success_rate=r.success_rate,
|
|
1257
|
+
errors=r.errors,
|
|
1258
|
+
)
|
|
1259
|
+
for r in results
|
|
1260
|
+
],
|
|
1261
|
+
)
|
|
1262
|
+
except Exception as e:
|
|
1263
|
+
raise HTTPException(status_code=500, detail=f"Policy processing failed: {str(e)}")
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
@router.get("/status", response_model=TieringStatusResponse)
|
|
1267
|
+
async def get_tiering_status(
|
|
1268
|
+
session: AsyncSession = Depends(get_session),
|
|
1269
|
+
) -> TieringStatusResponse:
|
|
1270
|
+
"""Get the current tiering system status.
|
|
1271
|
+
|
|
1272
|
+
Returns information about truthound integration, active configurations,
|
|
1273
|
+
and system health.
|
|
1274
|
+
"""
|
|
1275
|
+
from ..core.tiering import get_tiering_adapter
|
|
1276
|
+
|
|
1277
|
+
adapter = get_tiering_adapter()
|
|
1278
|
+
|
|
1279
|
+
# Get active config
|
|
1280
|
+
config_result = await session.execute(
|
|
1281
|
+
select(TieringConfigModel).where(TieringConfigModel.is_active == True) # noqa: E712
|
|
1282
|
+
)
|
|
1283
|
+
active_config = config_result.scalar_one_or_none()
|
|
1284
|
+
|
|
1285
|
+
# Get counts
|
|
1286
|
+
active_tiers = (
|
|
1287
|
+
await session.execute(
|
|
1288
|
+
select(func.count())
|
|
1289
|
+
.select_from(StorageTierModel)
|
|
1290
|
+
.where(StorageTierModel.is_active == True) # noqa: E712
|
|
1291
|
+
)
|
|
1292
|
+
).scalar() or 0
|
|
1293
|
+
|
|
1294
|
+
active_policies = (
|
|
1295
|
+
await session.execute(
|
|
1296
|
+
select(func.count())
|
|
1297
|
+
.select_from(TierPolicyModel)
|
|
1298
|
+
.where(TierPolicyModel.is_active == True) # noqa: E712
|
|
1299
|
+
)
|
|
1300
|
+
).scalar() or 0
|
|
1301
|
+
|
|
1302
|
+
# Recent migrations
|
|
1303
|
+
recent_migrations = (
|
|
1304
|
+
await session.execute(
|
|
1305
|
+
select(func.count())
|
|
1306
|
+
.select_from(TierMigrationHistoryModel)
|
|
1307
|
+
.where(
|
|
1308
|
+
TierMigrationHistoryModel.started_at
|
|
1309
|
+
>= func.datetime("now", "-24 hours")
|
|
1310
|
+
)
|
|
1311
|
+
)
|
|
1312
|
+
).scalar() or 0
|
|
1313
|
+
|
|
1314
|
+
return TieringStatusResponse(
|
|
1315
|
+
truthound_available=adapter.is_available,
|
|
1316
|
+
tiering_enabled=active_config is not None and active_config.is_active,
|
|
1317
|
+
active_config_id=active_config.id if active_config else None,
|
|
1318
|
+
active_config_name=active_config.name if active_config else None,
|
|
1319
|
+
check_interval_hours=active_config.check_interval_hours if active_config else None,
|
|
1320
|
+
active_tiers=active_tiers,
|
|
1321
|
+
active_policies=active_policies,
|
|
1322
|
+
migrations_last_24h=recent_migrations,
|
|
1323
|
+
)
|