proxsync 2.0.1__tar.gz → 2.0.3__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.1/proxsync.egg-info → proxsync-2.0.3}/PKG-INFO +1 -1
- {proxsync-2.0.1 → proxsync-2.0.3/proxsync.egg-info}/PKG-INFO +1 -1
- {proxsync-2.0.1 → proxsync-2.0.3}/proxsync.egg-info/SOURCES.txt +7 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync.py +231 -33
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/api/serializers.py +35 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/api/urls.py +3 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/api/views.py +14 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/choices.py +13 -0
- proxsync-2.0.3/pve_sync_plugin/migrations/0014_vmprovisioninglog.py +41 -0
- proxsync-2.0.3/pve_sync_plugin/migrations/0015_pvevmtasklog.py +55 -0
- proxsync-2.0.3/pve_sync_plugin/migrations/0016_pvevmtasklog_status_textfield.py +18 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/models.py +108 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/navigation.py +14 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/tables.py +48 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/template_content.py +35 -2
- proxsync-2.0.3/pve_sync_plugin/templates/pve_sync/inc/vm_task_log_panel.html +57 -0
- proxsync-2.0.3/pve_sync_plugin/templates/pve_sync/pvevmtasklog_list.html +10 -0
- proxsync-2.0.3/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html +334 -0
- proxsync-2.0.3/pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html +114 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/templatetags/pve_sync_tags.py +14 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/urls.py +19 -1
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/views.py +283 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pyproject.toml +1 -1
- {proxsync-2.0.1 → proxsync-2.0.3}/MANIFEST.in +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/README.md +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/config.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pbs_sync.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/proxsync.egg-info/dependency_links.txt +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/proxsync.egg-info/requires.txt +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/proxsync.egg-info/top_level.txt +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/__init__.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/api/__init__.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/apps.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/filtersets.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/forms.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/management/__init__.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/management/commands/__init__.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/management/commands/pve_sync.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/management/commands/run_scheduled_syncs.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0001_initial.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0002_pvepluginsettings.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0003_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0004_model_verbose_names.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0007_rename_verbose_names_to_proxmox_sync.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0009_add_every_3h_schedule.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0010_pvedriftevent.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/migrations/__init__.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/signals.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/sync/__init__.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/sync/config_bridge.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/sync/engine.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/tasks.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/templates/pve_sync/dashboard.html +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/templates/pve_sync/pbsserverconfig.html +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/templates/pve_sync/pvebackupstatus.html +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/templates/pve_sync/pveclusterconfig.html +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/templates/pve_sync/pvedriftevent.html +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/templates/pve_sync/pvesyncjob.html +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/templates/pve_sync/pvewebhookevent.html +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/templates/pve_sync/settings.html +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/templatetags/__init__.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/utils.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/pve_sync_plugin/view_helpers.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/setup.cfg +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/state_db.py +0 -0
- {proxsync-2.0.1 → proxsync-2.0.3}/tests/test_plugin_contracts.py +0 -0
|
@@ -46,6 +46,9 @@ pve_sync_plugin/migrations/0010_pvedriftevent.py
|
|
|
46
46
|
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
|
+
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
|
|
49
52
|
pve_sync_plugin/migrations/__init__.py
|
|
50
53
|
pve_sync_plugin/sync/__init__.py
|
|
51
54
|
pve_sync_plugin/sync/config_bridge.py
|
|
@@ -56,9 +59,13 @@ pve_sync_plugin/templates/pve_sync/pvebackupstatus.html
|
|
|
56
59
|
pve_sync_plugin/templates/pve_sync/pveclusterconfig.html
|
|
57
60
|
pve_sync_plugin/templates/pve_sync/pvedriftevent.html
|
|
58
61
|
pve_sync_plugin/templates/pve_sync/pvesyncjob.html
|
|
62
|
+
pve_sync_plugin/templates/pve_sync/pvevmtasklog_list.html
|
|
59
63
|
pve_sync_plugin/templates/pve_sync/pvewebhookevent.html
|
|
60
64
|
pve_sync_plugin/templates/pve_sync/settings.html
|
|
65
|
+
pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html
|
|
66
|
+
pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html
|
|
61
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
|
|
62
69
|
pve_sync_plugin/templatetags/__init__.py
|
|
63
70
|
pve_sync_plugin/templatetags/pve_sync_tags.py
|
|
64
71
|
tests/test_plugin_contracts.py
|
|
@@ -119,6 +119,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
119
119
|
'vms_by_node': {},
|
|
120
120
|
'pools': {},
|
|
121
121
|
'node_networks': {},
|
|
122
|
+
'failed_nodes': set(), # nodes where VM API call failed — skip cleanup for these
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
self.error_log = []
|
|
@@ -361,6 +362,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
361
362
|
except Exception as e:
|
|
362
363
|
print(f" 加載節點 {node_name} 的虛擬機失敗: {e}")
|
|
363
364
|
self.pve_cache['vms_by_node'][node_name] = []
|
|
365
|
+
self.pve_cache['failed_nodes'].add(node_name.lower())
|
|
364
366
|
elapsed = time.time() - start_time
|
|
365
367
|
print(f"✓ PVE 數據加載完成,耗時 {elapsed:.2f} 秒")
|
|
366
368
|
print(f" 節點: {len(self.pve_cache['nodes'])} 個")
|
|
@@ -1124,40 +1126,228 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1124
1126
|
self.send_telegram_notification(new_msg)
|
|
1125
1127
|
self._write_drift_event(vm_name, vm_id, 'vm_created', 'vmid', '', str(vm_id), notified=True)
|
|
1126
1128
|
|
|
1127
|
-
def detect_deleted_vms(self, seen_vmids: set):
|
|
1128
|
-
"""
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if
|
|
1129
|
+
def detect_deleted_vms(self, seen_vmids: set, cluster_id: int = None):
|
|
1130
|
+
"""偵測並清理 NetBox 中已從 PVE 刪除的 VM(含 IP)。"""
|
|
1131
|
+
|
|
1132
|
+
# ── 1. 舊有:drift 通知(依賴 state_db)──
|
|
1133
|
+
if self.enhanced_mode and self.state_db and self._known_vmids:
|
|
1134
|
+
deleted_vmids = set(self._known_vmids.keys()) - seen_vmids
|
|
1135
|
+
for vmid in deleted_vmids:
|
|
1136
|
+
info = self._known_vmids[vmid]
|
|
1137
|
+
vm_name = info.get('vm_name') or f"VM-{vmid}"
|
|
1138
|
+
node_name = info.get('node') or 'unknown'
|
|
1139
|
+
try:
|
|
1140
|
+
from pve_sync_plugin.models import PveDriftEvent
|
|
1141
|
+
if PveDriftEvent.objects.filter(
|
|
1142
|
+
vmid=vmid, cluster_name=self.cluster_name, drift_type='vm_deleted'
|
|
1143
|
+
).exists():
|
|
1144
|
+
continue
|
|
1145
|
+
except Exception:
|
|
1146
|
+
pass
|
|
1147
|
+
print(f"🗑️ VM {vm_name} 刪除偵測 (ID: {vmid}, 上次節點: {node_name})")
|
|
1148
|
+
self.stats['config_drifts_detected'] += 1
|
|
1149
|
+
del_msg = (
|
|
1150
|
+
f"🗑️ <b>VM 刪除偵測</b>\n\n"
|
|
1151
|
+
f"🖥️ 名稱: <b>{vm_name}</b> (ID: {vmid})\n"
|
|
1152
|
+
f"🔀 叢集: {self.cluster_name}\n"
|
|
1153
|
+
f"📌 上次節點: <code>{node_name}</code>\n"
|
|
1154
|
+
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1155
|
+
f"⚠️ 此 VM 已從 Proxmox 中移除。"
|
|
1156
|
+
)
|
|
1157
|
+
self.send_telegram_notification(del_msg)
|
|
1158
|
+
self._write_drift_event(vm_name, vmid, 'vm_deleted', 'vmid', str(vmid), '', notified=True)
|
|
1159
|
+
|
|
1160
|
+
# ── 2. 主動清理:刪除 NetBox 中已不存在於 PVE 的 VM 及其 IP ──
|
|
1161
|
+
if not cluster_id:
|
|
1132
1162
|
return
|
|
1133
|
-
|
|
1134
|
-
|
|
1163
|
+
|
|
1164
|
+
failed_nodes = self.pve_cache.get('failed_nodes', set())
|
|
1165
|
+
if failed_nodes:
|
|
1166
|
+
print(f"⚠️ 節點 API 失敗,跳過其 VM 清理: {failed_nodes}")
|
|
1167
|
+
|
|
1168
|
+
try:
|
|
1169
|
+
from django.contrib.contenttypes.models import ContentType
|
|
1170
|
+
from virtualization.models import VirtualMachine, VMInterface
|
|
1171
|
+
from ipam.models import IPAddress
|
|
1172
|
+
|
|
1173
|
+
seen_serials = {str(v) for v in seen_vmids}
|
|
1174
|
+
stale_vms = list(
|
|
1175
|
+
VirtualMachine.objects
|
|
1176
|
+
.filter(cluster_id=cluster_id)
|
|
1177
|
+
.exclude(serial='')
|
|
1178
|
+
.exclude(serial__isnull=True)
|
|
1179
|
+
.exclude(serial__in=seen_serials)
|
|
1180
|
+
.select_related('device')
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
if not stale_vms:
|
|
1184
|
+
return
|
|
1185
|
+
|
|
1186
|
+
from pve_sync_plugin.models import PveDriftEvent
|
|
1187
|
+
|
|
1188
|
+
vm_ct = ContentType.objects.get_for_model(VMInterface)
|
|
1189
|
+
for vm in stale_vms:
|
|
1190
|
+
device_name = vm.device.name.lower() if vm.device else None
|
|
1191
|
+
if device_name and device_name in failed_nodes:
|
|
1192
|
+
print(f" ⏭️ 跳過 {vm.name} (VMID {vm.serial}):節點 {vm.device.name} API 失敗")
|
|
1193
|
+
continue
|
|
1194
|
+
|
|
1195
|
+
vm_name_str = vm.name
|
|
1196
|
+
vmid_int = int(vm.serial)
|
|
1197
|
+
|
|
1198
|
+
iface_ids = list(
|
|
1199
|
+
VMInterface.objects.filter(virtual_machine=vm).values_list('id', flat=True)
|
|
1200
|
+
)
|
|
1201
|
+
ip_deleted = 0
|
|
1202
|
+
if iface_ids:
|
|
1203
|
+
ip_deleted, _ = IPAddress.objects.filter(
|
|
1204
|
+
assigned_object_type=vm_ct,
|
|
1205
|
+
assigned_object_id__in=iface_ids,
|
|
1206
|
+
).delete()
|
|
1207
|
+
|
|
1208
|
+
vm.delete()
|
|
1209
|
+
print(f" 🗑️ 已刪除 VM {vm_name_str} (VMID {vmid_int}) 及 {ip_deleted} 個 IP")
|
|
1210
|
+
self.stats['config_drifts_detected'] += 1
|
|
1211
|
+
|
|
1212
|
+
# 寫入漂移事件(避免與 section 1 重複)
|
|
1213
|
+
already_logged = PveDriftEvent.objects.filter(
|
|
1214
|
+
vmid=vmid_int,
|
|
1215
|
+
cluster_name=self.cluster_name,
|
|
1216
|
+
drift_type='vm_deleted',
|
|
1217
|
+
).exists()
|
|
1218
|
+
if not already_logged:
|
|
1219
|
+
self._write_drift_event(
|
|
1220
|
+
vm_name_str, vmid_int, 'vm_deleted', 'vmid',
|
|
1221
|
+
str(vmid_int), '', notified=False,
|
|
1222
|
+
)
|
|
1223
|
+
del_msg = (
|
|
1224
|
+
f"🗑️ <b>VM 已從 NetBox 清除</b>\n\n"
|
|
1225
|
+
f"🖥️ 名稱: <b>{vm_name_str}</b> (VMID: {vmid_int})\n"
|
|
1226
|
+
f"🔀 叢集: {self.cluster_name}\n"
|
|
1227
|
+
f"🧹 已清除 IP 記錄: {ip_deleted} 個\n"
|
|
1228
|
+
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1229
|
+
f"⚠️ 此 VM 已從 Proxmox 刪除,NetBox 記錄已自動移除。"
|
|
1230
|
+
)
|
|
1231
|
+
self.send_telegram_notification(del_msg)
|
|
1232
|
+
|
|
1233
|
+
except Exception as exc:
|
|
1234
|
+
import traceback
|
|
1235
|
+
print(f"⚠️ 清理孤兒 VM 失敗: {exc}")
|
|
1236
|
+
traceback.print_exc()
|
|
1237
|
+
|
|
1238
|
+
def sync_vm_tasks(self):
|
|
1239
|
+
"""從 PVE 各節點抓取 VM 操作記錄(建立/刪除/Clone/遷移)並同步到 NetBox。"""
|
|
1240
|
+
try:
|
|
1241
|
+
from django.utils import timezone
|
|
1242
|
+
from pve_sync_plugin.models import PveVmTaskLog
|
|
1243
|
+
except ImportError:
|
|
1135
1244
|
return
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1245
|
+
|
|
1246
|
+
VM_TASK_TYPES = {
|
|
1247
|
+
"qmcreate", "qmdestroy", "qmclone", "qmmigrate", "qmrestore",
|
|
1248
|
+
"qmstart", "qmstop", "qmshutdown", "qmreboot",
|
|
1249
|
+
}
|
|
1250
|
+
NOTIFY_TYPES = {"qmdestroy", "qmclone", "qmrestore", "qmshutdown", "qmstop"}
|
|
1251
|
+
TASK_LABELS = {
|
|
1252
|
+
"qmcreate": "🆕 VM 建立",
|
|
1253
|
+
"qmdestroy": "🗑️ VM 刪除",
|
|
1254
|
+
"qmclone": "📋 VM Clone",
|
|
1255
|
+
"qmmigrate": "🔀 VM 遷移",
|
|
1256
|
+
"qmrestore": "♻️ VM 還原",
|
|
1257
|
+
"qmstart": "▶️ VM 開機",
|
|
1258
|
+
"qmstop": "⏹️ VM 強制關機",
|
|
1259
|
+
"qmshutdown": "🔽 VM 正常關機",
|
|
1260
|
+
"qmreboot": "🔄 VM 重新開機",
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
# 從 NetBox cache 建立 vmid → vm_name 對照
|
|
1264
|
+
vmid_to_name: dict = {}
|
|
1265
|
+
for key, vm in self.nb_cache.get("virtual_machines_by_serial", {}).items():
|
|
1141
1266
|
try:
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
vmid=vmid, cluster_name=self.cluster_name, drift_type='vm_deleted'
|
|
1145
|
-
).exists():
|
|
1146
|
-
continue
|
|
1147
|
-
except Exception:
|
|
1267
|
+
vmid_to_name[int(vm.serial)] = vm.name
|
|
1268
|
+
except (ValueError, TypeError, AttributeError):
|
|
1148
1269
|
pass
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1270
|
+
|
|
1271
|
+
# 只通知最近 2 小時內的操作(避免首次匯入時大量通知)
|
|
1272
|
+
notify_cutoff = time.time() - 7200
|
|
1273
|
+
|
|
1274
|
+
since_ts = int(time.time()) - 30 * 86400 # 抓最近 30 天
|
|
1275
|
+
new_count = 0
|
|
1276
|
+
|
|
1277
|
+
for node_name in self.pve_cache.get("vms_by_node", {}):
|
|
1278
|
+
try:
|
|
1279
|
+
tasks = self.pve_api.nodes(node_name).tasks.get(since=since_ts, limit=1000)
|
|
1280
|
+
except Exception as exc:
|
|
1281
|
+
print(f" ⚠️ 無法取得節點 {node_name} 的操作記錄: {exc}")
|
|
1282
|
+
continue
|
|
1283
|
+
|
|
1284
|
+
for task in tasks:
|
|
1285
|
+
if task.get("type") not in VM_TASK_TYPES:
|
|
1286
|
+
continue
|
|
1287
|
+
|
|
1288
|
+
upid = task.get("upid", "")
|
|
1289
|
+
if not upid:
|
|
1290
|
+
continue
|
|
1291
|
+
|
|
1292
|
+
vmid_str = task.get("id", "")
|
|
1293
|
+
try:
|
|
1294
|
+
vmid = int(vmid_str)
|
|
1295
|
+
except (ValueError, TypeError):
|
|
1296
|
+
continue
|
|
1297
|
+
|
|
1298
|
+
# UPID 去重
|
|
1299
|
+
if PveVmTaskLog.objects.filter(upid=upid).exists():
|
|
1300
|
+
continue
|
|
1301
|
+
|
|
1302
|
+
task_type = task.get("type")
|
|
1303
|
+
operator = task.get("user", "unknown")
|
|
1304
|
+
start_ts = task.get("starttime")
|
|
1305
|
+
end_ts = task.get("endtime")
|
|
1306
|
+
status = task.get("status", "") or ""
|
|
1307
|
+
vm_name = vmid_to_name.get(vmid, f"VM-{vmid}")
|
|
1308
|
+
|
|
1309
|
+
import datetime as _dt
|
|
1310
|
+
_utc = _dt.timezone.utc
|
|
1311
|
+
start_dt = _dt.datetime.fromtimestamp(start_ts, tz=_utc) if start_ts else timezone.now()
|
|
1312
|
+
end_dt = _dt.datetime.fromtimestamp(end_ts, tz=_utc) if end_ts else None
|
|
1313
|
+
|
|
1314
|
+
log = PveVmTaskLog.objects.create(
|
|
1315
|
+
upid=upid,
|
|
1316
|
+
vmid=vmid,
|
|
1317
|
+
vm_name=vm_name,
|
|
1318
|
+
cluster_name=self.cluster_name,
|
|
1319
|
+
node=node_name,
|
|
1320
|
+
task_type=task_type,
|
|
1321
|
+
operator=operator,
|
|
1322
|
+
start_time=start_dt,
|
|
1323
|
+
end_time=end_dt,
|
|
1324
|
+
status=status,
|
|
1325
|
+
notified_telegram=False,
|
|
1326
|
+
)
|
|
1327
|
+
new_count += 1
|
|
1328
|
+
|
|
1329
|
+
# Telegram 通知(僅近期且已完成的操作)
|
|
1330
|
+
should_notify = (
|
|
1331
|
+
task_type in NOTIFY_TYPES
|
|
1332
|
+
and status == "OK"
|
|
1333
|
+
and start_ts and start_ts >= notify_cutoff
|
|
1334
|
+
)
|
|
1335
|
+
if should_notify:
|
|
1336
|
+
label = TASK_LABELS.get(task_type, task_type)
|
|
1337
|
+
msg = (
|
|
1338
|
+
f"{label}\n\n"
|
|
1339
|
+
f"🖥️ VM: <b>{vm_name}</b> (VMID: {vmid})\n"
|
|
1340
|
+
f"👤 操作者: <code>{operator}</code>\n"
|
|
1341
|
+
f"📌 節點: <code>{node_name}</code>\n"
|
|
1342
|
+
f"🔀 叢集: {self.cluster_name}\n"
|
|
1343
|
+
f"📅 時間: {start_dt.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
1344
|
+
)
|
|
1345
|
+
self.send_telegram_notification(msg)
|
|
1346
|
+
log.notified_telegram = True
|
|
1347
|
+
log.save(update_fields=["notified_telegram"])
|
|
1348
|
+
|
|
1349
|
+
if new_count > 0:
|
|
1350
|
+
print(f" 📋 新增 {new_count} 筆 VM 操作記錄")
|
|
1161
1351
|
|
|
1162
1352
|
def detect_config_drift(self, vm_id: int, vm_name: str, vm_config: Dict[str, Any],
|
|
1163
1353
|
current_tags: List[str], network_interfaces: List[Dict] = None,
|
|
@@ -1500,14 +1690,18 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1500
1690
|
# 但 state_db 仍有舊 hash 導致增量跳過。強制往下完整同步。
|
|
1501
1691
|
print(f" ⚠️ VM {original_vm_name} 不在 NetBox 中,忽略增量記錄強制建立")
|
|
1502
1692
|
else:
|
|
1503
|
-
# VM 存在;確保 device
|
|
1693
|
+
# VM 存在;確保 device 關聯、cluster 關聯、電源狀態都是最新的
|
|
1504
1694
|
current_device_id = getattr(getattr(cached_vm, 'device', None), 'id', None)
|
|
1505
1695
|
current_cluster_id = getattr(getattr(cached_vm, 'cluster', None), 'id', None)
|
|
1696
|
+
current_status = getattr(getattr(cached_vm, 'status', None), 'value', None) \
|
|
1697
|
+
or str(getattr(cached_vm, 'status', ''))
|
|
1506
1698
|
fix = {}
|
|
1507
1699
|
if current_device_id != device.id:
|
|
1508
1700
|
fix['device'] = device.id
|
|
1509
1701
|
if current_cluster_id != cluster['id']:
|
|
1510
1702
|
fix['cluster'] = cluster['id']
|
|
1703
|
+
if current_status != status:
|
|
1704
|
+
fix['status'] = status
|
|
1511
1705
|
if fix:
|
|
1512
1706
|
try:
|
|
1513
1707
|
cached_vm.update(fix)
|
|
@@ -1515,6 +1709,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1515
1709
|
print(f" ✓ VM {original_vm_name} 叢集關聯已修正: → {cluster['name']}")
|
|
1516
1710
|
if 'device' in fix:
|
|
1517
1711
|
print(f" ✓ VM {original_vm_name} 設備關聯已修正: → {device.name}")
|
|
1712
|
+
if 'status' in fix:
|
|
1713
|
+
print(f" ✓ VM {original_vm_name} 電源狀態更新: {current_status} → {status}")
|
|
1518
1714
|
except Exception as e:
|
|
1519
1715
|
print(f" ✗ 更新 VM {original_vm_name} 關聯失敗: {e}")
|
|
1520
1716
|
print(f" ℹ️ VM {original_vm_name} 無配置變更,跳過同步")
|
|
@@ -1698,8 +1894,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1698
1894
|
seen_vmids.add(int(vm['vmid']))
|
|
1699
1895
|
if self.process_virtual_machine(vm, device, cluster, force=node_is_new):
|
|
1700
1896
|
success_count += 1
|
|
1701
|
-
#
|
|
1702
|
-
self.detect_deleted_vms(seen_vmids)
|
|
1897
|
+
# 偵測並清理本次同步消失的 VM(已從 PVE 刪除)
|
|
1898
|
+
self.detect_deleted_vms(seen_vmids, cluster_id=cluster['id'])
|
|
1703
1899
|
print(f"\n虛擬機同步完成: {success_count}/{total_count} 個虛擬機")
|
|
1704
1900
|
return success_count > 0, success_count, total_count
|
|
1705
1901
|
|
|
@@ -1733,6 +1929,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1733
1929
|
print("\n" + "=" * 50)
|
|
1734
1930
|
print("✓ 節點同步成功")
|
|
1735
1931
|
vms_success, success_count, total_count = self.sync_pve_virtual_machines(devices, cluster)
|
|
1932
|
+
print("\n同步 VM 操作記錄...")
|
|
1933
|
+
self.sync_vm_tasks()
|
|
1736
1934
|
elapsed = time.time() - start_time
|
|
1737
1935
|
self.stats['elapsed_time'] = elapsed
|
|
1738
1936
|
self.stats['total_vms'] = total_count
|
|
@@ -9,7 +9,9 @@ from pve_sync_plugin.models import (
|
|
|
9
9
|
PveClusterConfig,
|
|
10
10
|
PvePluginSettings,
|
|
11
11
|
PveSyncJob,
|
|
12
|
+
PveVmTaskLog,
|
|
12
13
|
PveWebhookEvent,
|
|
14
|
+
VmProvisioningLog,
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
|
|
@@ -159,3 +161,36 @@ class PvePluginSettingsSerializer(NetBoxModelSerializer):
|
|
|
159
161
|
"enable_backup_sync",
|
|
160
162
|
]
|
|
161
163
|
# Never expose secrets via API
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class VmProvisioningLogSerializer(NetBoxModelSerializer):
|
|
167
|
+
url = serializers.HyperlinkedIdentityField(
|
|
168
|
+
view_name="plugins-api:pve_sync_plugin-api:vmprovisioninglog-detail",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
class Meta:
|
|
172
|
+
model = VmProvisioningLog
|
|
173
|
+
fields = [
|
|
174
|
+
"id", "url", "display",
|
|
175
|
+
"vm_name", "vmid", "cluster_name", "node", "os_type",
|
|
176
|
+
"cpu", "ram_gb", "disk_gb",
|
|
177
|
+
"management_ip", "management_gw", "internet_ip", "internet_gw",
|
|
178
|
+
"status", "notes", "checklist",
|
|
179
|
+
"created", "last_updated",
|
|
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,6 +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 PveVmTaskLogViewSet, VmProvisioningLogViewSet
|
|
12
13
|
|
|
13
14
|
app_name = "pve_sync_plugin-api"
|
|
14
15
|
|
|
@@ -20,6 +21,8 @@ router.register("events", api_views.PveWebhookEventViewSet)
|
|
|
20
21
|
router.register("clusters", api_views.PveClusterConfigViewSet)
|
|
21
22
|
router.register("backup-status", api_views.PveBackupStatusViewSet)
|
|
22
23
|
router.register("settings", api_views.PvePluginSettingsViewSet)
|
|
24
|
+
router.register("provisioning", api_views.VmProvisioningLogViewSet)
|
|
25
|
+
router.register("task-logs", api_views.PveVmTaskLogViewSet)
|
|
23
26
|
|
|
24
27
|
urlpatterns = [
|
|
25
28
|
path("webhook/", plugin_views.webhook_receiver, name="webhook"),
|
|
@@ -17,7 +17,9 @@ from pve_sync_plugin.models import (
|
|
|
17
17
|
PveClusterConfig,
|
|
18
18
|
PvePluginSettings,
|
|
19
19
|
PveSyncJob,
|
|
20
|
+
PveVmTaskLog,
|
|
20
21
|
PveWebhookEvent,
|
|
22
|
+
VmProvisioningLog,
|
|
21
23
|
)
|
|
22
24
|
|
|
23
25
|
from .serializers import (
|
|
@@ -25,7 +27,9 @@ from .serializers import (
|
|
|
25
27
|
PveClusterConfigSerializer,
|
|
26
28
|
PvePluginSettingsSerializer,
|
|
27
29
|
PveSyncJobSerializer,
|
|
30
|
+
PveVmTaskLogSerializer,
|
|
28
31
|
PveWebhookEventSerializer,
|
|
32
|
+
VmProvisioningLogSerializer,
|
|
29
33
|
)
|
|
30
34
|
|
|
31
35
|
|
|
@@ -92,3 +96,13 @@ class PveBackupStatusViewSet(NetBoxModelViewSet):
|
|
|
92
96
|
class PvePluginSettingsViewSet(NetBoxModelViewSet):
|
|
93
97
|
queryset = PvePluginSettings.objects.all()
|
|
94
98
|
serializer_class = PvePluginSettingsSerializer
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class VmProvisioningLogViewSet(NetBoxModelViewSet):
|
|
102
|
+
queryset = VmProvisioningLog.objects.order_by("-created")
|
|
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 遷移"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from django.db import migrations, models
|
|
2
|
+
import django.db.models.deletion
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Migration(migrations.Migration):
|
|
6
|
+
|
|
7
|
+
dependencies = [
|
|
8
|
+
("pve_sync_plugin", "0013_pluginsettings_log_retention"),
|
|
9
|
+
("extras", "0138_customfieldchoiceset_choice_colors"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.CreateModel(
|
|
14
|
+
name="VmProvisioningLog",
|
|
15
|
+
fields=[
|
|
16
|
+
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
17
|
+
("created", models.DateTimeField(auto_now_add=True, null=True)),
|
|
18
|
+
("last_updated", models.DateTimeField(auto_now=True, null=True)),
|
|
19
|
+
("custom_field_data", models.JSONField(blank=True, default=dict, encoder=None)),
|
|
20
|
+
("vm_name", models.CharField(max_length=100, verbose_name="VM 名稱")),
|
|
21
|
+
("vmid", models.IntegerField(blank=True, null=True, verbose_name="VMID")),
|
|
22
|
+
("cluster_name", models.CharField(blank=True, max_length=100, verbose_name="叢集")),
|
|
23
|
+
("node", models.CharField(blank=True, max_length=100, verbose_name="節點")),
|
|
24
|
+
("os_type", models.CharField(blank=True, max_length=50, verbose_name="作業系統")),
|
|
25
|
+
("cpu", models.IntegerField(blank=True, null=True, verbose_name="CPU")),
|
|
26
|
+
("ram_gb", models.IntegerField(blank=True, null=True, verbose_name="記憶體 (GB)")),
|
|
27
|
+
("disk_gb", models.IntegerField(blank=True, null=True, verbose_name="磁碟 (GB)")),
|
|
28
|
+
("management_ip", models.CharField(blank=True, max_length=50, verbose_name="Management IP")),
|
|
29
|
+
("management_gw", models.CharField(blank=True, max_length=50, verbose_name="Management 閘道")),
|
|
30
|
+
("internet_ip", models.CharField(blank=True, max_length=50, verbose_name="Internet IP")),
|
|
31
|
+
("internet_gw", models.CharField(blank=True, max_length=50, verbose_name="Internet 閘道")),
|
|
32
|
+
("status", models.CharField(
|
|
33
|
+
choices=[("planning", "規劃中"), ("in_progress", "部署中"), ("completed", "完成")],
|
|
34
|
+
default="planning", max_length=20, verbose_name="狀態",
|
|
35
|
+
)),
|
|
36
|
+
("notes", models.TextField(blank=True, verbose_name="備註")),
|
|
37
|
+
("checklist", models.JSONField(blank=True, default=dict, verbose_name="清單狀態")),
|
|
38
|
+
],
|
|
39
|
+
options={"ordering": ["-created"], "verbose_name": "VM Provisioning Log"},
|
|
40
|
+
),
|
|
41
|
+
]
|
|
@@ -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
|
+
]
|