proxsync 2.0.2__tar.gz → 2.0.4__tar.gz
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.
- {proxsync-2.0.2/proxsync.egg-info → proxsync-2.0.4}/PKG-INFO +1 -1
- {proxsync-2.0.2 → proxsync-2.0.4/proxsync.egg-info}/PKG-INFO +1 -1
- {proxsync-2.0.2 → proxsync-2.0.4}/proxsync.egg-info/SOURCES.txt +4 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync.py +149 -1
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/api/serializers.py +17 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/api/urls.py +2 -1
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/api/views.py +7 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/choices.py +13 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/filtersets.py +25 -0
- proxsync-2.0.4/pve_sync_plugin/migrations/0015_pvevmtasklog.py +55 -0
- proxsync-2.0.4/pve_sync_plugin/migrations/0016_pvevmtasklog_status_textfield.py +18 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/models.py +57 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/navigation.py +6 -1
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/tables.py +25 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/template_content.py +35 -2
- proxsync-2.0.4/pve_sync_plugin/templates/pve_sync/inc/vm_task_log_panel.html +57 -0
- proxsync-2.0.4/pve_sync_plugin/templates/pve_sync/pvevmtasklog_list.html +10 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/urls.py +8 -1
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/views.py +29 -7
- {proxsync-2.0.2 → proxsync-2.0.4}/pyproject.toml +1 -1
- {proxsync-2.0.2 → proxsync-2.0.4}/MANIFEST.in +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/README.md +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/config.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pbs_sync.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/proxsync.egg-info/dependency_links.txt +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/proxsync.egg-info/requires.txt +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/proxsync.egg-info/top_level.txt +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/__init__.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/api/__init__.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/apps.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/forms.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/management/__init__.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/management/commands/__init__.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/management/commands/pve_sync.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/management/commands/run_scheduled_syncs.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0001_initial.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0002_pvepluginsettings.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0003_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0004_model_verbose_names.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0007_rename_verbose_names_to_proxmox_sync.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0009_add_every_3h_schedule.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0010_pvedriftevent.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0014_vmprovisioninglog.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/__init__.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/signals.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/sync/__init__.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/sync/config_bridge.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/sync/engine.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/tasks.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/dashboard.html +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/pbsserverconfig.html +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/pvebackupstatus.html +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/pveclusterconfig.html +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/pvedriftevent.html +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/pvesyncjob.html +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/pvewebhookevent.html +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/settings.html +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templatetags/__init__.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templatetags/pve_sync_tags.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/utils.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/view_helpers.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/setup.cfg +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/state_db.py +0 -0
- {proxsync-2.0.2 → proxsync-2.0.4}/tests/test_plugin_contracts.py +0 -0
|
@@ -47,6 +47,8 @@ pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py
|
|
|
47
47
|
pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py
|
|
48
48
|
pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py
|
|
49
49
|
pve_sync_plugin/migrations/0014_vmprovisioninglog.py
|
|
50
|
+
pve_sync_plugin/migrations/0015_pvevmtasklog.py
|
|
51
|
+
pve_sync_plugin/migrations/0016_pvevmtasklog_status_textfield.py
|
|
50
52
|
pve_sync_plugin/migrations/__init__.py
|
|
51
53
|
pve_sync_plugin/sync/__init__.py
|
|
52
54
|
pve_sync_plugin/sync/config_bridge.py
|
|
@@ -57,11 +59,13 @@ pve_sync_plugin/templates/pve_sync/pvebackupstatus.html
|
|
|
57
59
|
pve_sync_plugin/templates/pve_sync/pveclusterconfig.html
|
|
58
60
|
pve_sync_plugin/templates/pve_sync/pvedriftevent.html
|
|
59
61
|
pve_sync_plugin/templates/pve_sync/pvesyncjob.html
|
|
62
|
+
pve_sync_plugin/templates/pve_sync/pvevmtasklog_list.html
|
|
60
63
|
pve_sync_plugin/templates/pve_sync/pvewebhookevent.html
|
|
61
64
|
pve_sync_plugin/templates/pve_sync/settings.html
|
|
62
65
|
pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html
|
|
63
66
|
pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html
|
|
64
67
|
pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html
|
|
68
|
+
pve_sync_plugin/templates/pve_sync/inc/vm_task_log_panel.html
|
|
65
69
|
pve_sync_plugin/templatetags/__init__.py
|
|
66
70
|
pve_sync_plugin/templatetags/pve_sync_tags.py
|
|
67
71
|
tests/test_plugin_contracts.py
|
|
@@ -1235,6 +1235,145 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1235
1235
|
print(f"⚠️ 清理孤兒 VM 失敗: {exc}")
|
|
1236
1236
|
traceback.print_exc()
|
|
1237
1237
|
|
|
1238
|
+
def _apply_log_retention(self):
|
|
1239
|
+
"""依 PvePluginSettings.log_retention_days 清理過期記錄。"""
|
|
1240
|
+
try:
|
|
1241
|
+
import datetime as _dt
|
|
1242
|
+
from django.utils import timezone
|
|
1243
|
+
from pve_sync_plugin.models import PveDriftEvent, PvePluginSettings, PveVmTaskLog
|
|
1244
|
+
days = PvePluginSettings.load().log_retention_days
|
|
1245
|
+
if not days:
|
|
1246
|
+
return
|
|
1247
|
+
cutoff = timezone.now() - _dt.timedelta(days=days)
|
|
1248
|
+
d_del, _ = PveDriftEvent.objects.filter(created__lt=cutoff).delete()
|
|
1249
|
+
t_del, _ = PveVmTaskLog.objects.filter(start_time__lt=cutoff).delete()
|
|
1250
|
+
if d_del or t_del:
|
|
1251
|
+
print(f" 🧹 Log 清理:刪除 {d_del} 筆漂移事件、{t_del} 筆操作記錄(>{days} 天)")
|
|
1252
|
+
except Exception as exc:
|
|
1253
|
+
print(f" ⚠️ Log 清理失敗: {exc}")
|
|
1254
|
+
|
|
1255
|
+
def sync_vm_tasks(self):
|
|
1256
|
+
"""從 PVE 各節點抓取 VM 操作記錄(建立/刪除/Clone/遷移)並同步到 NetBox。"""
|
|
1257
|
+
try:
|
|
1258
|
+
from django.utils import timezone
|
|
1259
|
+
from pve_sync_plugin.models import PveVmTaskLog
|
|
1260
|
+
except ImportError:
|
|
1261
|
+
return
|
|
1262
|
+
|
|
1263
|
+
VM_TASK_TYPES = {
|
|
1264
|
+
"qmcreate", "qmdestroy", "qmclone", "qmmigrate", "qmrestore",
|
|
1265
|
+
"qmstart", "qmstop", "qmshutdown", "qmreboot",
|
|
1266
|
+
}
|
|
1267
|
+
NOTIFY_TYPES = {"qmdestroy", "qmclone", "qmrestore", "qmshutdown", "qmstop"}
|
|
1268
|
+
TASK_LABELS = {
|
|
1269
|
+
"qmcreate": "🆕 VM 建立",
|
|
1270
|
+
"qmdestroy": "🗑️ VM 刪除",
|
|
1271
|
+
"qmclone": "📋 VM Clone",
|
|
1272
|
+
"qmmigrate": "🔀 VM 遷移",
|
|
1273
|
+
"qmrestore": "♻️ VM 還原",
|
|
1274
|
+
"qmstart": "▶️ VM 開機",
|
|
1275
|
+
"qmstop": "⏹️ VM 強制關機",
|
|
1276
|
+
"qmshutdown": "🔽 VM 正常關機",
|
|
1277
|
+
"qmreboot": "🔄 VM 重新開機",
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
# 從 NetBox cache 建立 vmid → vm_name 對照
|
|
1281
|
+
vmid_to_name: dict = {}
|
|
1282
|
+
for key, vm in self.nb_cache.get("virtual_machines_by_serial", {}).items():
|
|
1283
|
+
try:
|
|
1284
|
+
vmid_to_name[int(vm.serial)] = vm.name
|
|
1285
|
+
except (ValueError, TypeError, AttributeError):
|
|
1286
|
+
pass
|
|
1287
|
+
|
|
1288
|
+
# 只通知最近 2 小時內的操作(避免首次匯入時大量通知)
|
|
1289
|
+
notify_cutoff = time.time() - 7200
|
|
1290
|
+
|
|
1291
|
+
# 從上次已存記錄的最新時間起算(+1 秒避免邊界重複);首次則抓 30 天
|
|
1292
|
+
latest = PveVmTaskLog.objects.filter(
|
|
1293
|
+
cluster_name=self.cluster_name
|
|
1294
|
+
).order_by("-start_time").values_list("start_time", flat=True).first()
|
|
1295
|
+
if latest:
|
|
1296
|
+
import calendar
|
|
1297
|
+
since_ts = int(calendar.timegm(latest.utctimetuple())) + 1
|
|
1298
|
+
else:
|
|
1299
|
+
since_ts = int(time.time()) - 30 * 86400
|
|
1300
|
+
new_count = 0
|
|
1301
|
+
|
|
1302
|
+
for node_name in self.pve_cache.get("vms_by_node", {}):
|
|
1303
|
+
try:
|
|
1304
|
+
tasks = self.pve_api.nodes(node_name).tasks.get(since=since_ts, limit=1000)
|
|
1305
|
+
except Exception as exc:
|
|
1306
|
+
print(f" ⚠️ 無法取得節點 {node_name} 的操作記錄: {exc}")
|
|
1307
|
+
continue
|
|
1308
|
+
|
|
1309
|
+
for task in tasks:
|
|
1310
|
+
if task.get("type") not in VM_TASK_TYPES:
|
|
1311
|
+
continue
|
|
1312
|
+
|
|
1313
|
+
upid = task.get("upid", "")
|
|
1314
|
+
if not upid:
|
|
1315
|
+
continue
|
|
1316
|
+
|
|
1317
|
+
vmid_str = task.get("id", "")
|
|
1318
|
+
try:
|
|
1319
|
+
vmid = int(vmid_str)
|
|
1320
|
+
except (ValueError, TypeError):
|
|
1321
|
+
continue
|
|
1322
|
+
|
|
1323
|
+
# UPID 去重
|
|
1324
|
+
if PveVmTaskLog.objects.filter(upid=upid).exists():
|
|
1325
|
+
continue
|
|
1326
|
+
|
|
1327
|
+
task_type = task.get("type")
|
|
1328
|
+
operator = task.get("user", "unknown")
|
|
1329
|
+
start_ts = task.get("starttime")
|
|
1330
|
+
end_ts = task.get("endtime")
|
|
1331
|
+
status = task.get("status", "") or ""
|
|
1332
|
+
vm_name = vmid_to_name.get(vmid, f"VM-{vmid}")
|
|
1333
|
+
|
|
1334
|
+
import datetime as _dt
|
|
1335
|
+
_utc = _dt.timezone.utc
|
|
1336
|
+
start_dt = _dt.datetime.fromtimestamp(start_ts, tz=_utc) if start_ts else timezone.now()
|
|
1337
|
+
end_dt = _dt.datetime.fromtimestamp(end_ts, tz=_utc) if end_ts else None
|
|
1338
|
+
|
|
1339
|
+
log = PveVmTaskLog.objects.create(
|
|
1340
|
+
upid=upid,
|
|
1341
|
+
vmid=vmid,
|
|
1342
|
+
vm_name=vm_name,
|
|
1343
|
+
cluster_name=self.cluster_name,
|
|
1344
|
+
node=node_name,
|
|
1345
|
+
task_type=task_type,
|
|
1346
|
+
operator=operator,
|
|
1347
|
+
start_time=start_dt,
|
|
1348
|
+
end_time=end_dt,
|
|
1349
|
+
status=status,
|
|
1350
|
+
notified_telegram=False,
|
|
1351
|
+
)
|
|
1352
|
+
new_count += 1
|
|
1353
|
+
|
|
1354
|
+
# Telegram 通知(僅近期且已完成的操作)
|
|
1355
|
+
should_notify = (
|
|
1356
|
+
task_type in NOTIFY_TYPES
|
|
1357
|
+
and status == "OK"
|
|
1358
|
+
and start_ts and start_ts >= notify_cutoff
|
|
1359
|
+
)
|
|
1360
|
+
if should_notify:
|
|
1361
|
+
label = TASK_LABELS.get(task_type, task_type)
|
|
1362
|
+
msg = (
|
|
1363
|
+
f"{label}\n\n"
|
|
1364
|
+
f"🖥️ VM: <b>{vm_name}</b> (VMID: {vmid})\n"
|
|
1365
|
+
f"👤 操作者: <code>{operator}</code>\n"
|
|
1366
|
+
f"📌 節點: <code>{node_name}</code>\n"
|
|
1367
|
+
f"🔀 叢集: {self.cluster_name}\n"
|
|
1368
|
+
f"📅 時間: {start_dt.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
1369
|
+
)
|
|
1370
|
+
self.send_telegram_notification(msg)
|
|
1371
|
+
log.notified_telegram = True
|
|
1372
|
+
log.save(update_fields=["notified_telegram"])
|
|
1373
|
+
|
|
1374
|
+
if new_count > 0:
|
|
1375
|
+
print(f" 📋 新增 {new_count} 筆 VM 操作記錄")
|
|
1376
|
+
|
|
1238
1377
|
def detect_config_drift(self, vm_id: int, vm_name: str, vm_config: Dict[str, Any],
|
|
1239
1378
|
current_tags: List[str], network_interfaces: List[Dict] = None,
|
|
1240
1379
|
current_node: str = None) -> List[Dict]:
|
|
@@ -1576,14 +1715,18 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1576
1715
|
# 但 state_db 仍有舊 hash 導致增量跳過。強制往下完整同步。
|
|
1577
1716
|
print(f" ⚠️ VM {original_vm_name} 不在 NetBox 中,忽略增量記錄強制建立")
|
|
1578
1717
|
else:
|
|
1579
|
-
# VM 存在;確保 device
|
|
1718
|
+
# VM 存在;確保 device 關聯、cluster 關聯、電源狀態都是最新的
|
|
1580
1719
|
current_device_id = getattr(getattr(cached_vm, 'device', None), 'id', None)
|
|
1581
1720
|
current_cluster_id = getattr(getattr(cached_vm, 'cluster', None), 'id', None)
|
|
1721
|
+
current_status = getattr(getattr(cached_vm, 'status', None), 'value', None) \
|
|
1722
|
+
or str(getattr(cached_vm, 'status', ''))
|
|
1582
1723
|
fix = {}
|
|
1583
1724
|
if current_device_id != device.id:
|
|
1584
1725
|
fix['device'] = device.id
|
|
1585
1726
|
if current_cluster_id != cluster['id']:
|
|
1586
1727
|
fix['cluster'] = cluster['id']
|
|
1728
|
+
if current_status != status:
|
|
1729
|
+
fix['status'] = status
|
|
1587
1730
|
if fix:
|
|
1588
1731
|
try:
|
|
1589
1732
|
cached_vm.update(fix)
|
|
@@ -1591,6 +1734,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1591
1734
|
print(f" ✓ VM {original_vm_name} 叢集關聯已修正: → {cluster['name']}")
|
|
1592
1735
|
if 'device' in fix:
|
|
1593
1736
|
print(f" ✓ VM {original_vm_name} 設備關聯已修正: → {device.name}")
|
|
1737
|
+
if 'status' in fix:
|
|
1738
|
+
print(f" ✓ VM {original_vm_name} 電源狀態更新: {current_status} → {status}")
|
|
1594
1739
|
except Exception as e:
|
|
1595
1740
|
print(f" ✗ 更新 VM {original_vm_name} 關聯失敗: {e}")
|
|
1596
1741
|
print(f" ℹ️ VM {original_vm_name} 無配置變更,跳過同步")
|
|
@@ -1809,6 +1954,9 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1809
1954
|
print("\n" + "=" * 50)
|
|
1810
1955
|
print("✓ 節點同步成功")
|
|
1811
1956
|
vms_success, success_count, total_count = self.sync_pve_virtual_machines(devices, cluster)
|
|
1957
|
+
print("\n同步 VM 操作記錄...")
|
|
1958
|
+
self.sync_vm_tasks()
|
|
1959
|
+
self._apply_log_retention()
|
|
1812
1960
|
elapsed = time.time() - start_time
|
|
1813
1961
|
self.stats['elapsed_time'] = elapsed
|
|
1814
1962
|
self.stats['total_vms'] = total_count
|
|
@@ -9,6 +9,7 @@ from pve_sync_plugin.models import (
|
|
|
9
9
|
PveClusterConfig,
|
|
10
10
|
PvePluginSettings,
|
|
11
11
|
PveSyncJob,
|
|
12
|
+
PveVmTaskLog,
|
|
12
13
|
PveWebhookEvent,
|
|
13
14
|
VmProvisioningLog,
|
|
14
15
|
)
|
|
@@ -177,3 +178,19 @@ class VmProvisioningLogSerializer(NetBoxModelSerializer):
|
|
|
177
178
|
"status", "notes", "checklist",
|
|
178
179
|
"created", "last_updated",
|
|
179
180
|
]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class PveVmTaskLogSerializer(NetBoxModelSerializer):
|
|
184
|
+
url = serializers.HyperlinkedIdentityField(
|
|
185
|
+
view_name="plugins-api:pve_sync_plugin-api:pvevmtasklog-detail",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
class Meta:
|
|
189
|
+
model = PveVmTaskLog
|
|
190
|
+
fields = [
|
|
191
|
+
"id", "url", "display",
|
|
192
|
+
"upid", "vmid", "vm_name", "cluster_name", "node",
|
|
193
|
+
"task_type", "operator", "start_time", "end_time",
|
|
194
|
+
"status", "notified_telegram",
|
|
195
|
+
"created", "last_updated",
|
|
196
|
+
]
|
|
@@ -9,7 +9,7 @@ from netbox.api.routers import NetBoxRouter
|
|
|
9
9
|
from pve_sync_plugin import views as plugin_views
|
|
10
10
|
|
|
11
11
|
from . import views as api_views
|
|
12
|
-
from .views import VmProvisioningLogViewSet
|
|
12
|
+
from .views import PveVmTaskLogViewSet, VmProvisioningLogViewSet
|
|
13
13
|
|
|
14
14
|
app_name = "pve_sync_plugin-api"
|
|
15
15
|
|
|
@@ -22,6 +22,7 @@ router.register("clusters", api_views.PveClusterConfigViewSet)
|
|
|
22
22
|
router.register("backup-status", api_views.PveBackupStatusViewSet)
|
|
23
23
|
router.register("settings", api_views.PvePluginSettingsViewSet)
|
|
24
24
|
router.register("provisioning", api_views.VmProvisioningLogViewSet)
|
|
25
|
+
router.register("task-logs", api_views.PveVmTaskLogViewSet)
|
|
25
26
|
|
|
26
27
|
urlpatterns = [
|
|
27
28
|
path("webhook/", plugin_views.webhook_receiver, name="webhook"),
|
|
@@ -17,6 +17,7 @@ from pve_sync_plugin.models import (
|
|
|
17
17
|
PveClusterConfig,
|
|
18
18
|
PvePluginSettings,
|
|
19
19
|
PveSyncJob,
|
|
20
|
+
PveVmTaskLog,
|
|
20
21
|
PveWebhookEvent,
|
|
21
22
|
VmProvisioningLog,
|
|
22
23
|
)
|
|
@@ -26,6 +27,7 @@ from .serializers import (
|
|
|
26
27
|
PveClusterConfigSerializer,
|
|
27
28
|
PvePluginSettingsSerializer,
|
|
28
29
|
PveSyncJobSerializer,
|
|
30
|
+
PveVmTaskLogSerializer,
|
|
29
31
|
PveWebhookEventSerializer,
|
|
30
32
|
VmProvisioningLogSerializer,
|
|
31
33
|
)
|
|
@@ -99,3 +101,8 @@ class PvePluginSettingsViewSet(NetBoxModelViewSet):
|
|
|
99
101
|
class VmProvisioningLogViewSet(NetBoxModelViewSet):
|
|
100
102
|
queryset = VmProvisioningLog.objects.order_by("-created")
|
|
101
103
|
serializer_class = VmProvisioningLogSerializer
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PveVmTaskLogViewSet(NetBoxModelViewSet):
|
|
107
|
+
queryset = PveVmTaskLog.objects.order_by("-start_time")
|
|
108
|
+
serializer_class = PveVmTaskLogSerializer
|
|
@@ -45,6 +45,19 @@ class SyncScheduleChoices(models.TextChoices):
|
|
|
45
45
|
WEEKLY = "weekly", "Weekly"
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
class VmTaskTypeChoices(models.TextChoices):
|
|
49
|
+
CREATE = "qmcreate", "建立 VM"
|
|
50
|
+
DESTROY = "qmdestroy", "刪除 VM"
|
|
51
|
+
CLONE = "qmclone", "Clone VM"
|
|
52
|
+
MIGRATE = "qmmigrate", "遷移 VM"
|
|
53
|
+
RESTORE = "qmrestore", "還原 VM"
|
|
54
|
+
BACKUP = "vzdump", "備份 VM"
|
|
55
|
+
START = "qmstart", "開機"
|
|
56
|
+
STOP = "qmstop", "強制關機"
|
|
57
|
+
SHUTDOWN = "qmshutdown", "正常關機"
|
|
58
|
+
REBOOT = "qmreboot", "重新開機"
|
|
59
|
+
|
|
60
|
+
|
|
48
61
|
class DriftTypeChoices(models.TextChoices):
|
|
49
62
|
HARDWARE = "hardware", "硬體配置變更"
|
|
50
63
|
MIGRATION = "migration", "VM 遷移"
|
|
@@ -10,6 +10,7 @@ from .choices import (
|
|
|
10
10
|
SyncJobStatusChoices,
|
|
11
11
|
SyncJobTriggerChoices,
|
|
12
12
|
SyncScheduleChoices,
|
|
13
|
+
VmTaskTypeChoices,
|
|
13
14
|
WebhookEventChoices,
|
|
14
15
|
)
|
|
15
16
|
from .models import (
|
|
@@ -18,6 +19,7 @@ from .models import (
|
|
|
18
19
|
PveClusterConfig,
|
|
19
20
|
PveDriftEvent,
|
|
20
21
|
PveSyncJob,
|
|
22
|
+
PveVmTaskLog,
|
|
21
23
|
PveWebhookEvent,
|
|
22
24
|
)
|
|
23
25
|
|
|
@@ -120,6 +122,29 @@ class PveDriftEventFilterSet(NetBoxModelFilterSet):
|
|
|
120
122
|
return queryset.filter(vm_name__icontains=value) | queryset.filter(cluster_name__icontains=value)
|
|
121
123
|
|
|
122
124
|
|
|
125
|
+
class PveVmTaskLogFilterSet(NetBoxModelFilterSet):
|
|
126
|
+
"""Filter VM task logs by type, operator, cluster, and VM."""
|
|
127
|
+
|
|
128
|
+
task_type = django_filters.MultipleChoiceFilter(choices=VmTaskTypeChoices.choices)
|
|
129
|
+
vmid = django_filters.NumberFilter()
|
|
130
|
+
cluster_name = django_filters.CharFilter(lookup_expr="icontains")
|
|
131
|
+
operator = django_filters.CharFilter(lookup_expr="icontains")
|
|
132
|
+
node = django_filters.CharFilter(lookup_expr="icontains")
|
|
133
|
+
|
|
134
|
+
class Meta:
|
|
135
|
+
model = PveVmTaskLog
|
|
136
|
+
fields = ["id", "vmid", "task_type", "cluster_name", "operator", "node"]
|
|
137
|
+
|
|
138
|
+
def search(self, queryset, name, value):
|
|
139
|
+
if not value.strip():
|
|
140
|
+
return queryset
|
|
141
|
+
return (
|
|
142
|
+
queryset.filter(vm_name__icontains=value)
|
|
143
|
+
| queryset.filter(operator__icontains=value)
|
|
144
|
+
| queryset.filter(cluster_name__icontains=value)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
123
148
|
class PveBackupStatusFilterSet(NetBoxModelFilterSet):
|
|
124
149
|
"""Filter backup statuses by state."""
|
|
125
150
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Generated by Django 6.0.5 on 2026-06-22 16:57
|
|
2
|
+
|
|
3
|
+
import netbox.models.deletion
|
|
4
|
+
import taggit.managers
|
|
5
|
+
import utilities.json
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
('extras', '0138_customfieldchoiceset_choice_colors'),
|
|
13
|
+
('pve_sync_plugin', '0014_vmprovisioninglog'),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.AddField(
|
|
18
|
+
model_name='vmprovisioninglog',
|
|
19
|
+
name='tags',
|
|
20
|
+
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
|
21
|
+
),
|
|
22
|
+
migrations.AlterField(
|
|
23
|
+
model_name='vmprovisioninglog',
|
|
24
|
+
name='custom_field_data',
|
|
25
|
+
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
|
26
|
+
),
|
|
27
|
+
migrations.CreateModel(
|
|
28
|
+
name='PveVmTaskLog',
|
|
29
|
+
fields=[
|
|
30
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
31
|
+
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
|
32
|
+
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
|
33
|
+
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
|
34
|
+
('upid', models.CharField(max_length=300, unique=True)),
|
|
35
|
+
('vmid', models.IntegerField()),
|
|
36
|
+
('vm_name', models.CharField(blank=True, max_length=200)),
|
|
37
|
+
('cluster_name', models.CharField(max_length=100)),
|
|
38
|
+
('node', models.CharField(max_length=100)),
|
|
39
|
+
('task_type', models.CharField(max_length=30)),
|
|
40
|
+
('operator', models.CharField(max_length=200)),
|
|
41
|
+
('start_time', models.DateTimeField()),
|
|
42
|
+
('end_time', models.DateTimeField(blank=True, null=True)),
|
|
43
|
+
('status', models.CharField(blank=True, max_length=50)),
|
|
44
|
+
('notified_telegram', models.BooleanField(default=False)),
|
|
45
|
+
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
|
46
|
+
],
|
|
47
|
+
options={
|
|
48
|
+
'verbose_name': 'VM Task Log',
|
|
49
|
+
'verbose_name_plural': 'VM Task Logs',
|
|
50
|
+
'ordering': ['-start_time'],
|
|
51
|
+
'indexes': [models.Index(fields=['vmid', 'cluster_name'], name='pve_sync_pl_vmid_00b115_idx'), models.Index(fields=['task_type'], name='pve_sync_pl_task_ty_0f86cb_idx'), models.Index(fields=['operator'], name='pve_sync_pl_operato_6b34b5_idx'), models.Index(fields=['start_time'], name='pve_sync_pl_start_t_dac39f_idx')],
|
|
52
|
+
},
|
|
53
|
+
bases=(netbox.models.deletion.DeleteMixin, models.Model),
|
|
54
|
+
),
|
|
55
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.5 on 2026-06-23 00:57
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('pve_sync_plugin', '0015_pvevmtasklog'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterField(
|
|
14
|
+
model_name='pvevmtasklog',
|
|
15
|
+
name='status',
|
|
16
|
+
field=models.TextField(blank=True),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -12,6 +12,7 @@ from .choices import (
|
|
|
12
12
|
SyncJobStatusChoices,
|
|
13
13
|
SyncJobTriggerChoices,
|
|
14
14
|
SyncScheduleChoices,
|
|
15
|
+
VmTaskTypeChoices,
|
|
15
16
|
WebhookEventChoices,
|
|
16
17
|
)
|
|
17
18
|
|
|
@@ -514,3 +515,59 @@ class VmProvisioningLog(NetBoxModel):
|
|
|
514
515
|
("chk_qemu_install", "安裝 qemu-guest-agent 套件"),
|
|
515
516
|
("chk_qemu_enable", "啟用並啟動 qemu-guest-agent 服務"),
|
|
516
517
|
]
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class PveVmTaskLog(NetBoxModel):
|
|
521
|
+
"""Records VM lifecycle operations fetched from PVE task history."""
|
|
522
|
+
|
|
523
|
+
upid = models.CharField(max_length=300, unique=True, verbose_name="UPID")
|
|
524
|
+
vmid = models.IntegerField(verbose_name="VM ID")
|
|
525
|
+
vm_name = models.CharField(max_length=200, blank=True, verbose_name="VM 名稱")
|
|
526
|
+
cluster_name = models.CharField(max_length=100, verbose_name="叢集")
|
|
527
|
+
node = models.CharField(max_length=100, verbose_name="節點")
|
|
528
|
+
task_type = models.CharField(max_length=30, choices=VmTaskTypeChoices, verbose_name="操作類型")
|
|
529
|
+
operator = models.CharField(max_length=200, verbose_name="操作者")
|
|
530
|
+
start_time = models.DateTimeField(verbose_name="開始時間")
|
|
531
|
+
end_time = models.DateTimeField(null=True, blank=True, verbose_name="結束時間")
|
|
532
|
+
status = models.TextField(blank=True, verbose_name="狀態")
|
|
533
|
+
notified_telegram = models.BooleanField(default=False, verbose_name="已通知")
|
|
534
|
+
|
|
535
|
+
NOTIFY_TYPES = {"qmdestroy", "qmclone", "qmrestore"}
|
|
536
|
+
|
|
537
|
+
TASK_ICONS = {
|
|
538
|
+
"qmcreate": ("mdi mdi-plus-circle", "success"),
|
|
539
|
+
"qmdestroy": ("mdi mdi-delete", "danger"),
|
|
540
|
+
"qmclone": ("mdi mdi-content-copy", "info"),
|
|
541
|
+
"qmmigrate": ("mdi mdi-transfer", "warning"),
|
|
542
|
+
"qmrestore": ("mdi mdi-restore", "primary"),
|
|
543
|
+
"vzdump": ("mdi mdi-backup-restore", "secondary"),
|
|
544
|
+
"qmstart": ("mdi mdi-play-circle", "success"),
|
|
545
|
+
"qmstop": ("mdi mdi-stop-circle", "danger"),
|
|
546
|
+
"qmshutdown": ("mdi mdi-power", "warning"),
|
|
547
|
+
"qmreboot": ("mdi mdi-restart", "info"),
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
class Meta:
|
|
551
|
+
ordering = ["-start_time"]
|
|
552
|
+
indexes = [
|
|
553
|
+
models.Index(fields=["vmid", "cluster_name"]),
|
|
554
|
+
models.Index(fields=["task_type"]),
|
|
555
|
+
models.Index(fields=["operator"]),
|
|
556
|
+
models.Index(fields=["start_time"]),
|
|
557
|
+
]
|
|
558
|
+
verbose_name = "VM Task Log"
|
|
559
|
+
verbose_name_plural = "VM Task Logs"
|
|
560
|
+
|
|
561
|
+
def __str__(self):
|
|
562
|
+
return f"{self.get_task_type_display()} — {self.vm_name} (VMID {self.vmid}) by {self.operator}"
|
|
563
|
+
|
|
564
|
+
def get_absolute_url(self):
|
|
565
|
+
return reverse("plugins:pve_sync_plugin:pvevmtasklog_list")
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def task_icon(self):
|
|
569
|
+
return self.TASK_ICONS.get(self.task_type, ("mdi mdi-history", "secondary"))
|
|
570
|
+
|
|
571
|
+
@property
|
|
572
|
+
def status_ok(self):
|
|
573
|
+
return self.status == "OK"
|
|
@@ -75,6 +75,11 @@ vm_provisioning_item = PluginMenuItem(
|
|
|
75
75
|
link_text="VM 佈建",
|
|
76
76
|
)
|
|
77
77
|
|
|
78
|
+
vm_task_log_item = PluginMenuItem(
|
|
79
|
+
link="plugins:pve_sync_plugin:pvevmtasklog_list",
|
|
80
|
+
link_text="VM 操作記錄",
|
|
81
|
+
)
|
|
82
|
+
|
|
78
83
|
menu = PluginMenu(
|
|
79
84
|
label="Proxmox Sync",
|
|
80
85
|
groups=(
|
|
@@ -92,7 +97,7 @@ menu = PluginMenu(
|
|
|
92
97
|
),
|
|
93
98
|
(
|
|
94
99
|
"VM Operations",
|
|
95
|
-
(vm_provisioning_item,),
|
|
100
|
+
(vm_provisioning_item, vm_task_log_item),
|
|
96
101
|
),
|
|
97
102
|
(
|
|
98
103
|
"Configuration",
|
|
@@ -10,6 +10,7 @@ from .models import (
|
|
|
10
10
|
PveClusterConfig,
|
|
11
11
|
PveDriftEvent,
|
|
12
12
|
PveSyncJob,
|
|
13
|
+
PveVmTaskLog,
|
|
13
14
|
PveWebhookEvent,
|
|
14
15
|
VmProvisioningLog,
|
|
15
16
|
)
|
|
@@ -299,3 +300,27 @@ class VmProvisioningLogTable(NetBoxTable):
|
|
|
299
300
|
"management_ip", "internet_ip", "created")
|
|
300
301
|
default_columns = ("vm_name", "vmid", "cluster_name", "node", "status",
|
|
301
302
|
"management_ip", "internet_ip")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
# PveVmTaskLog
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
class PveVmTaskLogTable(NetBoxTable):
|
|
310
|
+
vm_name = tables.Column(verbose_name="VM 名稱")
|
|
311
|
+
vmid = tables.Column(verbose_name="VM ID")
|
|
312
|
+
cluster_name = tables.Column(verbose_name="叢集")
|
|
313
|
+
node = tables.Column(verbose_name="節點")
|
|
314
|
+
task_type = columns.ChoiceFieldColumn(verbose_name="操作類型")
|
|
315
|
+
operator = tables.Column(verbose_name="操作者")
|
|
316
|
+
start_time = columns.DateTimeColumn(verbose_name="時間")
|
|
317
|
+
status = tables.Column(verbose_name="結果")
|
|
318
|
+
notified_telegram = columns.BooleanColumn(verbose_name="已通知")
|
|
319
|
+
actions = columns.ActionsColumn(actions=("delete",))
|
|
320
|
+
|
|
321
|
+
class Meta(NetBoxTable.Meta):
|
|
322
|
+
model = PveVmTaskLog
|
|
323
|
+
fields = ("pk", "vm_name", "vmid", "cluster_name", "node",
|
|
324
|
+
"task_type", "operator", "start_time", "status", "notified_telegram")
|
|
325
|
+
default_columns = ("vm_name", "vmid", "cluster_name", "node",
|
|
326
|
+
"task_type", "operator", "start_time", "status")
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from netbox.plugins import PluginTemplateExtension
|
|
4
4
|
|
|
5
|
-
from .models import PveClusterConfig, PveSyncJob
|
|
5
|
+
from .models import PveClusterConfig, PveSyncJob, PveVmTaskLog
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class VirtualMachineSyncButton(PluginTemplateExtension):
|
|
@@ -41,4 +41,37 @@ class VirtualMachineSyncButton(PluginTemplateExtension):
|
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
class VmTaskLogPanel(PluginTemplateExtension):
|
|
45
|
+
"""Inject a VM operation history panel into NetBox VM detail pages."""
|
|
46
|
+
|
|
47
|
+
models = ["virtualization.virtualmachine"]
|
|
48
|
+
|
|
49
|
+
def full_width_page(self):
|
|
50
|
+
vm = self.context["object"]
|
|
51
|
+
|
|
52
|
+
if not vm.serial:
|
|
53
|
+
return ""
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
vmid = int(vm.serial)
|
|
57
|
+
except (ValueError, TypeError):
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
qs = PveVmTaskLog.objects.filter(vmid=vmid)
|
|
61
|
+
cluster_config = None
|
|
62
|
+
if vm.cluster_id:
|
|
63
|
+
cluster_config = PveClusterConfig.objects.filter(
|
|
64
|
+
netbox_cluster_id=vm.cluster_id, enabled=True,
|
|
65
|
+
).first()
|
|
66
|
+
if cluster_config:
|
|
67
|
+
qs = qs.filter(cluster_name=cluster_config.name)
|
|
68
|
+
|
|
69
|
+
task_logs = qs.order_by("-start_time")[:30]
|
|
70
|
+
|
|
71
|
+
return self.render(
|
|
72
|
+
"pve_sync/inc/vm_task_log_panel.html",
|
|
73
|
+
extra_context={"task_logs": task_logs, "vmid": vmid},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
template_extensions = [VirtualMachineSyncButton, VmTaskLogPanel]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{% load helpers %}
|
|
2
|
+
|
|
3
|
+
<div class="card mt-3">
|
|
4
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
5
|
+
<h5 class="mb-0"><i class="mdi mdi-history me-2"></i>PVE 操作記錄</h5>
|
|
6
|
+
<a href="{% url 'plugins:pve_sync_plugin:pvevmtasklog_list' %}?vmid={{ vmid }}"
|
|
7
|
+
class="btn btn-sm btn-outline-secondary">查看全部</a>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="card-body p-0">
|
|
10
|
+
{% if task_logs %}
|
|
11
|
+
<table class="table table-hover table-sm mb-0">
|
|
12
|
+
<thead class="table-secondary">
|
|
13
|
+
<tr>
|
|
14
|
+
<th>操作類型</th>
|
|
15
|
+
<th>操作者</th>
|
|
16
|
+
<th>節點</th>
|
|
17
|
+
<th>時間</th>
|
|
18
|
+
<th>結果</th>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
{% for log in task_logs %}
|
|
23
|
+
<tr>
|
|
24
|
+
<td>
|
|
25
|
+
{% with log.task_icon as icon %}
|
|
26
|
+
<i class="{{ icon.0 }} text-{{ icon.1 }} me-1"></i>
|
|
27
|
+
{{ log.get_task_type_display }}
|
|
28
|
+
{% endwith %}
|
|
29
|
+
</td>
|
|
30
|
+
<td><code>{{ log.operator }}</code></td>
|
|
31
|
+
<td>{{ log.node }}</td>
|
|
32
|
+
<td>
|
|
33
|
+
<span title="{{ log.start_time }}">
|
|
34
|
+
{{ log.start_time|date:"Y-m-d H:i:s" }}
|
|
35
|
+
</span>
|
|
36
|
+
</td>
|
|
37
|
+
<td>
|
|
38
|
+
{% if log.status_ok %}
|
|
39
|
+
<span class="badge bg-success">OK</span>
|
|
40
|
+
{% elif log.status %}
|
|
41
|
+
<span class="badge bg-danger" title="{{ log.status }}">ERROR</span>
|
|
42
|
+
{% else %}
|
|
43
|
+
<span class="badge bg-secondary">—</span>
|
|
44
|
+
{% endif %}
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
{% endfor %}
|
|
48
|
+
</tbody>
|
|
49
|
+
</table>
|
|
50
|
+
{% else %}
|
|
51
|
+
<div class="text-center text-muted py-4">
|
|
52
|
+
<i class="mdi mdi-history mdi-48px d-block mb-2"></i>
|
|
53
|
+
尚無操作記錄(下次同步後自動載入)
|
|
54
|
+
</div>
|
|
55
|
+
{% endif %}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{% extends 'generic/object_list.html' %}
|
|
2
|
+
{% load helpers %}
|
|
3
|
+
|
|
4
|
+
{% block title %}VM 操作記錄{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block extra_controls %}
|
|
7
|
+
<div class="d-flex gap-2 align-items-center">
|
|
8
|
+
<span class="text-muted small">由 PVE Task History API 自動同步</span>
|
|
9
|
+
</div>
|
|
10
|
+
{% endblock %}
|
|
@@ -10,7 +10,7 @@ from django.urls import path
|
|
|
10
10
|
from netbox.views.generic import ObjectChangeLogView
|
|
11
11
|
|
|
12
12
|
from . import views
|
|
13
|
-
from .models import PbsServerConfig, PveBackupStatus, PveClusterConfig, PveDriftEvent, PveSyncJob, PveWebhookEvent, VmProvisioningLog
|
|
13
|
+
from .models import PbsServerConfig, PveBackupStatus, PveClusterConfig, PveDriftEvent, PveSyncJob, PveVmTaskLog, PveWebhookEvent, VmProvisioningLog
|
|
14
14
|
|
|
15
15
|
app_name = "pve_sync_plugin"
|
|
16
16
|
|
|
@@ -181,6 +181,13 @@ urlpatterns = [
|
|
|
181
181
|
kwargs={"model": PveDriftEvent},
|
|
182
182
|
),
|
|
183
183
|
|
|
184
|
+
# --- PveVmTaskLog ---
|
|
185
|
+
path("task-logs/", views.PveVmTaskLogListView.as_view(), name="pvevmtasklog_list"),
|
|
186
|
+
path("task-logs/<int:pk>/delete/", views.PveVmTaskLogDeleteView.as_view(), name="pvevmtasklog_delete"),
|
|
187
|
+
path("task-logs/delete/", views.PveVmTaskLogBulkDeleteView.as_view(), name="pvevmtasklog_bulk_delete"),
|
|
188
|
+
path("task-logs/<int:pk>/changelog/", ObjectChangeLogView.as_view(),
|
|
189
|
+
name="pvevmtasklog_changelog", kwargs={"model": PveVmTaskLog}),
|
|
190
|
+
|
|
184
191
|
# IP probe APIs (used by provisioning form)
|
|
185
192
|
path("provisioning/free-ips/<int:range_id>/", views.VmPlannerFreeIpsApi.as_view(), name="vm-planner-free-ips"),
|
|
186
193
|
path("provisioning/check-ip/", views.VmPlannerCheckIpApi.as_view(), name="vm-planner-check-ip"),
|
|
@@ -30,6 +30,7 @@ from .filtersets import (
|
|
|
30
30
|
PveClusterConfigFilterSet,
|
|
31
31
|
PveDriftEventFilterSet,
|
|
32
32
|
PveSyncJobFilterSet,
|
|
33
|
+
PveVmTaskLogFilterSet,
|
|
33
34
|
PveWebhookEventFilterSet,
|
|
34
35
|
)
|
|
35
36
|
from .forms import (
|
|
@@ -49,6 +50,7 @@ from .models import (
|
|
|
49
50
|
PveDriftEvent,
|
|
50
51
|
PvePluginSettings,
|
|
51
52
|
PveSyncJob,
|
|
53
|
+
PveVmTaskLog,
|
|
52
54
|
PveWebhookEvent,
|
|
53
55
|
VmProvisioningLog,
|
|
54
56
|
)
|
|
@@ -58,6 +60,7 @@ from .tables import (
|
|
|
58
60
|
PveClusterConfigTable,
|
|
59
61
|
PveDriftEventTable,
|
|
60
62
|
PveSyncJobTable,
|
|
63
|
+
PveVmTaskLogTable,
|
|
61
64
|
PveWebhookEventTable,
|
|
62
65
|
VmProvisioningLogTable,
|
|
63
66
|
)
|
|
@@ -342,6 +345,26 @@ class PveDriftEventBulkDeleteView(generic.BulkDeleteView):
|
|
|
342
345
|
table = PveDriftEventTable
|
|
343
346
|
|
|
344
347
|
|
|
348
|
+
# ============================================================================
|
|
349
|
+
# PveVmTaskLog — read-only list from PVE task history
|
|
350
|
+
# ============================================================================
|
|
351
|
+
|
|
352
|
+
class PveVmTaskLogListView(generic.ObjectListView):
|
|
353
|
+
queryset = PveVmTaskLog.objects.order_by("-start_time")
|
|
354
|
+
table = PveVmTaskLogTable
|
|
355
|
+
filterset = PveVmTaskLogFilterSet
|
|
356
|
+
template_name = "pve_sync/pvevmtasklog_list.html"
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class PveVmTaskLogDeleteView(generic.ObjectDeleteView):
|
|
360
|
+
queryset = PveVmTaskLog.objects.all()
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class PveVmTaskLogBulkDeleteView(generic.BulkDeleteView):
|
|
364
|
+
queryset = PveVmTaskLog.objects.all()
|
|
365
|
+
table = PveVmTaskLogTable
|
|
366
|
+
|
|
367
|
+
|
|
345
368
|
# ============================================================================
|
|
346
369
|
# Plugin Settings (singleton)
|
|
347
370
|
# ============================================================================
|
|
@@ -645,13 +668,12 @@ class VmPlannerCheckIpApi(PermissionRequiredMixin, View):
|
|
|
645
668
|
ip_str = str(ip_obj)
|
|
646
669
|
|
|
647
670
|
# ── 1. IPAM check (authoritative) ──
|
|
648
|
-
#
|
|
649
|
-
#
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
break
|
|
671
|
+
# Filter by host bits using __net_host_contained: finds any /N record
|
|
672
|
+
# whose host address matches ip_str, without a full table scan.
|
|
673
|
+
ipam_qs = NetBoxIPAddress.objects.filter(
|
|
674
|
+
address__net_host_contained=ip_str
|
|
675
|
+
).values_list("address", flat=True).first()
|
|
676
|
+
ipam_entry = str(ipam_qs) if ipam_qs else None
|
|
655
677
|
|
|
656
678
|
if ipam_entry:
|
|
657
679
|
return JsonResponse({
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/management/commands/run_scheduled_syncs.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py
RENAMED
|
File without changes
|
{proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html
RENAMED
|
File without changes
|
{proxsync-2.0.2 → proxsync-2.0.4}/pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|