proxsync 2.0.4__tar.gz → 2.0.6__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.4/proxsync.egg-info → proxsync-2.0.6}/PKG-INFO +1 -1
- {proxsync-2.0.4 → proxsync-2.0.6/proxsync.egg-info}/PKG-INFO +1 -1
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync.py +101 -34
- {proxsync-2.0.4 → proxsync-2.0.6}/pyproject.toml +1 -1
- {proxsync-2.0.4 → proxsync-2.0.6}/state_db.py +24 -2
- {proxsync-2.0.4 → proxsync-2.0.6}/MANIFEST.in +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/README.md +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/config.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pbs_sync.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/proxsync.egg-info/SOURCES.txt +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/proxsync.egg-info/dependency_links.txt +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/proxsync.egg-info/requires.txt +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/proxsync.egg-info/top_level.txt +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/__init__.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/api/__init__.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/api/serializers.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/api/urls.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/api/views.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/apps.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/choices.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/filtersets.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/forms.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/management/__init__.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/management/commands/__init__.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/management/commands/pve_sync.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/management/commands/run_scheduled_syncs.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0001_initial.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0002_pvepluginsettings.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0003_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0004_model_verbose_names.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0007_rename_verbose_names_to_proxmox_sync.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0009_add_every_3h_schedule.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0010_pvedriftevent.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0014_vmprovisioninglog.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0015_pvevmtasklog.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0016_pvevmtasklog_status_textfield.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/__init__.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/models.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/navigation.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/signals.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/sync/__init__.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/sync/config_bridge.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/sync/engine.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/tables.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/tasks.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/template_content.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/dashboard.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/inc/vm_task_log_panel.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/pbsserverconfig.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/pvebackupstatus.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/pveclusterconfig.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/pvedriftevent.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/pvesyncjob.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/pvevmtasklog_list.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/pvewebhookevent.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/settings.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templatetags/__init__.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templatetags/pve_sync_tags.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/urls.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/utils.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/view_helpers.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/views.py +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/setup.cfg +0 -0
- {proxsync-2.0.4 → proxsync-2.0.6}/tests/test_plugin_contracts.py +0 -0
|
@@ -125,12 +125,25 @@ class OptimizedPVEToNetBoxSync:
|
|
|
125
125
|
self.error_log = []
|
|
126
126
|
|
|
127
127
|
# ---------- Telegram ----------
|
|
128
|
-
|
|
128
|
+
# Max Telegram messages per sync run; excess are counted and reported in the summary.
|
|
129
|
+
_MAX_TELEGRAM_PER_SYNC = 20
|
|
130
|
+
|
|
131
|
+
def send_telegram_notification(self, message: str) -> bool:
|
|
132
|
+
if not getattr(self, 'notify_on_sync', True):
|
|
133
|
+
return False
|
|
134
|
+
if not self.telegram_bot_token or not self.telegram_chat_id:
|
|
135
|
+
return False
|
|
136
|
+
# Throttle: cap messages per sync run to avoid flooding on bulk changes.
|
|
137
|
+
sent = getattr(self, '_telegram_sent', 0)
|
|
138
|
+
if sent >= self._MAX_TELEGRAM_PER_SYNC:
|
|
139
|
+
self._telegram_suppressed = getattr(self, '_telegram_suppressed', 0) + 1
|
|
140
|
+
return False
|
|
129
141
|
try:
|
|
130
142
|
url = f"https://api.telegram.org/bot{self.telegram_bot_token}/sendMessage"
|
|
131
143
|
payload = {'chat_id': self.telegram_chat_id, 'text': message, 'parse_mode': 'HTML'}
|
|
132
144
|
response = requests.post(url, data=payload, timeout=10)
|
|
133
145
|
if response.status_code == 200:
|
|
146
|
+
self._telegram_sent = sent + 1
|
|
134
147
|
print(f"✓ Telegram 通知已發送")
|
|
135
148
|
return True
|
|
136
149
|
else:
|
|
@@ -199,6 +212,9 @@ class OptimizedPVEToNetBoxSync:
|
|
|
199
212
|
message += f"• {error['vm_name']} - {error['ip_address']}\n"
|
|
200
213
|
if len(self.error_log) > 5:
|
|
201
214
|
message += f"• ... 還有 {len(self.error_log) - 5} 個錯誤\n"
|
|
215
|
+
suppressed = getattr(self, '_telegram_suppressed', 0)
|
|
216
|
+
if suppressed:
|
|
217
|
+
message += f"\n📵 本輪通知已達上限,{suppressed} 則訊息被壓制(詳見 NetBox 漂移事件記錄)"
|
|
202
218
|
self.send_telegram_notification(message)
|
|
203
219
|
|
|
204
220
|
# ---------- API 連接 ----------
|
|
@@ -285,6 +301,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
285
301
|
self.nb_cache['platforms'][platform.name.lower()] = platform
|
|
286
302
|
for role in self.nb_api.dcim.device_roles.all():
|
|
287
303
|
self.nb_cache['roles'][role.name.lower()] = role
|
|
304
|
+
self.nb_cache['device_roles'][role.name.lower()] = role
|
|
288
305
|
for cluster in self.nb_api.virtualization.clusters.all():
|
|
289
306
|
self.nb_cache['clusters'][str(cluster.id)] = cluster
|
|
290
307
|
for site in self.nb_api.dcim.sites.all():
|
|
@@ -293,8 +310,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
293
310
|
self.nb_cache['manufacturers'][manufacturer.name.lower()] = manufacturer
|
|
294
311
|
for device_type in self.nb_api.dcim.device_types.all():
|
|
295
312
|
self.nb_cache['device_types'][device_type.model.lower()] = device_type
|
|
296
|
-
for device_role in self.nb_api.dcim.device_roles.all():
|
|
297
|
-
self.nb_cache['device_roles'][device_role.name.lower()] = device_role
|
|
298
313
|
for cluster_type in self.nb_api.virtualization.cluster_types.all():
|
|
299
314
|
self.nb_cache['cluster_types'][cluster_type.name.lower()] = cluster_type
|
|
300
315
|
elapsed = time.time() - start_time
|
|
@@ -351,13 +366,13 @@ class OptimizedPVEToNetBoxSync:
|
|
|
351
366
|
vm['node'] = node_name
|
|
352
367
|
all_vms.append(vm)
|
|
353
368
|
self.pve_cache['vms_by_node'][node_name] = all_vms
|
|
354
|
-
for vm in all_vms
|
|
369
|
+
for vm in all_vms:
|
|
355
370
|
try:
|
|
356
371
|
if vm['type'] == 'qemu':
|
|
357
372
|
vm['config'] = self.pve_api.nodes(node_name).qemu(vm['vmid']).config.get()
|
|
358
373
|
else:
|
|
359
374
|
vm['config'] = self.pve_api.nodes(node_name).lxc(vm['vmid']).config.get()
|
|
360
|
-
except:
|
|
375
|
+
except Exception:
|
|
361
376
|
pass
|
|
362
377
|
except Exception as e:
|
|
363
378
|
print(f" 加載節點 {node_name} 的虛擬機失敗: {e}")
|
|
@@ -1084,6 +1099,11 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1084
1099
|
disks[key] = m.group(1) if m else val_str[:80]
|
|
1085
1100
|
return disks
|
|
1086
1101
|
|
|
1102
|
+
@staticmethod
|
|
1103
|
+
def _norm_desc(s) -> str:
|
|
1104
|
+
"""Normalize description: unify line endings, strip whitespace."""
|
|
1105
|
+
return (s or '').replace('\r\n', '\n').replace('\r', '\n').strip()
|
|
1106
|
+
|
|
1087
1107
|
def _write_drift_event(self, vm_name: str, vmid: int, drift_type: str,
|
|
1088
1108
|
field_name: str, old_value: str, new_value: str,
|
|
1089
1109
|
notified: bool = False):
|
|
@@ -1106,13 +1126,13 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1106
1126
|
except Exception as e:
|
|
1107
1127
|
print(f" ⚠ 無法寫入漂移事件記錄: {e}")
|
|
1108
1128
|
|
|
1109
|
-
def detect_new_vm(self, vm_id: int, vm_name: str, node_name: str):
|
|
1129
|
+
def detect_new_vm(self, vm_id: int, vm_name: str, node_name: str, last_config=None):
|
|
1110
1130
|
"""若此 vmid 在先前同步中不存在(但叢集有先前記錄),視為新增 VM。"""
|
|
1111
1131
|
if not self.enhanced_mode or not self.state_db:
|
|
1112
1132
|
return
|
|
1113
1133
|
if not self._known_vmids:
|
|
1114
1134
|
return # 首次同步,無先前記錄,不觸發
|
|
1115
|
-
if
|
|
1135
|
+
if last_config:
|
|
1116
1136
|
return # 已有記錄,不是新增
|
|
1117
1137
|
print(f"🆕 VM {vm_name} 新增偵測 (ID: {vm_id}, 節點: {node_name})")
|
|
1118
1138
|
self.stats['config_drifts_detected'] += 1
|
|
@@ -1251,15 +1271,29 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1251
1271
|
print(f" 🧹 Log 清理:刪除 {d_del} 筆漂移事件、{t_del} 筆操作記錄(>{days} 天)")
|
|
1252
1272
|
except Exception as exc:
|
|
1253
1273
|
print(f" ⚠️ Log 清理失敗: {exc}")
|
|
1274
|
+
# Also clean up state_db vm_config_history (keep latest row per VM, remove old history)
|
|
1275
|
+
if self.state_db:
|
|
1276
|
+
try:
|
|
1277
|
+
from pve_sync_plugin.models import PvePluginSettings
|
|
1278
|
+
days = PvePluginSettings.load().log_retention_days
|
|
1279
|
+
if days:
|
|
1280
|
+
h_del = self.state_db.cleanup_old_history(self.cluster_name, days)
|
|
1281
|
+
if h_del:
|
|
1282
|
+
print(f" 🧹 state_db 清理:刪除 {h_del} 筆舊配置歷史(>{days} 天)")
|
|
1283
|
+
except Exception as exc:
|
|
1284
|
+
print(f" ⚠️ state_db 歷史清理失敗: {exc}")
|
|
1254
1285
|
|
|
1255
1286
|
def sync_vm_tasks(self):
|
|
1256
1287
|
"""從 PVE 各節點抓取 VM 操作記錄(建立/刪除/Clone/遷移)並同步到 NetBox。"""
|
|
1288
|
+
import calendar
|
|
1289
|
+
import datetime as _dt
|
|
1257
1290
|
try:
|
|
1258
1291
|
from django.utils import timezone
|
|
1259
1292
|
from pve_sync_plugin.models import PveVmTaskLog
|
|
1260
1293
|
except ImportError:
|
|
1261
1294
|
return
|
|
1262
1295
|
|
|
1296
|
+
_utc = _dt.timezone.utc
|
|
1263
1297
|
VM_TASK_TYPES = {
|
|
1264
1298
|
"qmcreate", "qmdestroy", "qmclone", "qmmigrate", "qmrestore",
|
|
1265
1299
|
"qmstart", "qmstop", "qmshutdown", "qmreboot",
|
|
@@ -1293,10 +1327,18 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1293
1327
|
cluster_name=self.cluster_name
|
|
1294
1328
|
).order_by("-start_time").values_list("start_time", flat=True).first()
|
|
1295
1329
|
if latest:
|
|
1296
|
-
import calendar
|
|
1297
1330
|
since_ts = int(calendar.timegm(latest.utctimetuple())) + 1
|
|
1298
1331
|
else:
|
|
1299
1332
|
since_ts = int(time.time()) - 30 * 86400
|
|
1333
|
+
|
|
1334
|
+
# Pre-fetch existing UPIDs in the window → O(1) lookup instead of N+1 queries
|
|
1335
|
+
since_dt = _dt.datetime.fromtimestamp(since_ts, tz=_utc)
|
|
1336
|
+
existing_upids: set = set(
|
|
1337
|
+
PveVmTaskLog.objects
|
|
1338
|
+
.filter(cluster_name=self.cluster_name, start_time__gte=since_dt)
|
|
1339
|
+
.values_list("upid", flat=True)
|
|
1340
|
+
)
|
|
1341
|
+
new_upids: set = set() # UPIDs inserted this run (avoid duplicates within same run)
|
|
1300
1342
|
new_count = 0
|
|
1301
1343
|
|
|
1302
1344
|
for node_name in self.pve_cache.get("vms_by_node", {}):
|
|
@@ -1320,8 +1362,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1320
1362
|
except (ValueError, TypeError):
|
|
1321
1363
|
continue
|
|
1322
1364
|
|
|
1323
|
-
# UPID
|
|
1324
|
-
if
|
|
1365
|
+
# UPID 去重(使用預先撈取的集合,避免 N+1 查詢)
|
|
1366
|
+
if upid in existing_upids or upid in new_upids:
|
|
1325
1367
|
continue
|
|
1326
1368
|
|
|
1327
1369
|
task_type = task.get("type")
|
|
@@ -1331,8 +1373,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1331
1373
|
status = task.get("status", "") or ""
|
|
1332
1374
|
vm_name = vmid_to_name.get(vmid, f"VM-{vmid}")
|
|
1333
1375
|
|
|
1334
|
-
import datetime as _dt
|
|
1335
|
-
_utc = _dt.timezone.utc
|
|
1336
1376
|
start_dt = _dt.datetime.fromtimestamp(start_ts, tz=_utc) if start_ts else timezone.now()
|
|
1337
1377
|
end_dt = _dt.datetime.fromtimestamp(end_ts, tz=_utc) if end_ts else None
|
|
1338
1378
|
|
|
@@ -1349,6 +1389,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1349
1389
|
status=status,
|
|
1350
1390
|
notified_telegram=False,
|
|
1351
1391
|
)
|
|
1392
|
+
new_upids.add(upid)
|
|
1352
1393
|
new_count += 1
|
|
1353
1394
|
|
|
1354
1395
|
# Telegram 通知(僅近期且已完成的操作)
|
|
@@ -1376,11 +1417,12 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1376
1417
|
|
|
1377
1418
|
def detect_config_drift(self, vm_id: int, vm_name: str, vm_config: Dict[str, Any],
|
|
1378
1419
|
current_tags: List[str], network_interfaces: List[Dict] = None,
|
|
1379
|
-
current_node: str = None) -> List[Dict]:
|
|
1420
|
+
current_node: str = None, last_config=None) -> List[Dict]:
|
|
1380
1421
|
if not self.enhanced_mode or not self.state_db:
|
|
1381
1422
|
return []
|
|
1382
1423
|
drifts = []
|
|
1383
|
-
last_config
|
|
1424
|
+
if last_config is None:
|
|
1425
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1384
1426
|
if not last_config:
|
|
1385
1427
|
return drifts
|
|
1386
1428
|
current_memory = int(vm_config.get('memory', 0))
|
|
@@ -1461,8 +1503,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1461
1503
|
self._write_drift_event(vm_name, vm_id, 'disk_change', k, old_v, new_v, notified=True)
|
|
1462
1504
|
|
|
1463
1505
|
# VM 描述變更偵測
|
|
1464
|
-
current_desc = (vm_config.get('description')
|
|
1465
|
-
old_desc = (last_config.get('description')
|
|
1506
|
+
current_desc = self._norm_desc(vm_config.get('description'))[:500]
|
|
1507
|
+
old_desc = self._norm_desc(last_config.get('description'))
|
|
1466
1508
|
if old_desc and old_desc != current_desc:
|
|
1467
1509
|
self.stats['config_drifts_detected'] += 1
|
|
1468
1510
|
print(f"📝 VM {vm_name} 描述變更")
|
|
@@ -1502,7 +1544,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1502
1544
|
return drifts
|
|
1503
1545
|
|
|
1504
1546
|
def should_sync_vm(self, vm_id: int, vm_config: Dict[str, Any], tags: List[str],
|
|
1505
|
-
network_interfaces: List[Dict] = None) -> bool:
|
|
1547
|
+
network_interfaces: List[Dict] = None, last_config=None) -> bool:
|
|
1506
1548
|
if not self.sync_config.get('incremental', True):
|
|
1507
1549
|
return True
|
|
1508
1550
|
if self.sync_config.get('force_full_sync', False):
|
|
@@ -1510,34 +1552,35 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1510
1552
|
current_hash = compute_config_hash(vm_config, tags, network_interfaces)
|
|
1511
1553
|
if not self.state_db:
|
|
1512
1554
|
return True
|
|
1513
|
-
last_config
|
|
1555
|
+
if last_config is None:
|
|
1556
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1514
1557
|
if not last_config:
|
|
1515
1558
|
return True
|
|
1516
1559
|
if current_hash != last_config.get('config_hash'):
|
|
1517
1560
|
return True
|
|
1518
1561
|
return False
|
|
1519
1562
|
|
|
1520
|
-
def detect_tag_changes(self, vm_id: int, vm_name: str, current_tags: List[str]
|
|
1563
|
+
def detect_tag_changes(self, vm_id: int, vm_name: str, current_tags: List[str],
|
|
1564
|
+
last_config=None) -> List[Dict]:
|
|
1521
1565
|
if not self.enhanced_mode or not self.state_db:
|
|
1522
1566
|
return []
|
|
1523
|
-
|
|
1524
|
-
|
|
1567
|
+
if last_config is None:
|
|
1568
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1525
1569
|
if not last_config:
|
|
1526
|
-
return
|
|
1570
|
+
return []
|
|
1527
1571
|
old_tags = set(last_config.get('tags', []))
|
|
1528
1572
|
new_tags = set(current_tags)
|
|
1529
1573
|
added = new_tags - old_tags
|
|
1530
1574
|
removed = old_tags - new_tags
|
|
1575
|
+
changes = []
|
|
1531
1576
|
for tag in added:
|
|
1532
1577
|
changes.append({'type': 'added', 'tag': tag})
|
|
1533
1578
|
self.stats['tag_changes'] += 1
|
|
1534
1579
|
print(f"🏷️ VM {vm_name} 新增標籤: {tag}")
|
|
1535
|
-
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', '', tag, notified=False)
|
|
1536
1580
|
for tag in removed:
|
|
1537
1581
|
changes.append({'type': 'removed', 'tag': tag})
|
|
1538
1582
|
self.stats['tag_changes'] += 1
|
|
1539
1583
|
print(f"🏷️ VM {vm_name} 移除標籤: {tag}")
|
|
1540
|
-
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', tag, '', notified=False)
|
|
1541
1584
|
if changes:
|
|
1542
1585
|
added_list = [c['tag'] for c in changes if c['type'] == 'added']
|
|
1543
1586
|
removed_list = [c['tag'] for c in changes if c['type'] == 'removed']
|
|
@@ -1553,7 +1596,12 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1553
1596
|
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1554
1597
|
f"{detail}\n請確認是否為預期變更。"
|
|
1555
1598
|
)
|
|
1556
|
-
self.send_telegram_notification(tag_msg)
|
|
1599
|
+
notified = self.send_telegram_notification(tag_msg)
|
|
1600
|
+
for c in changes:
|
|
1601
|
+
if c['type'] == 'added':
|
|
1602
|
+
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', '', c['tag'], notified=notified)
|
|
1603
|
+
else:
|
|
1604
|
+
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', c['tag'], '', notified=notified)
|
|
1557
1605
|
return changes
|
|
1558
1606
|
|
|
1559
1607
|
def detect_ip_change(self, vm_id: int, vm_name: str, current_ip: str):
|
|
@@ -1621,10 +1669,16 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1621
1669
|
tag_ids.append(tag.id)
|
|
1622
1670
|
except Exception as e:
|
|
1623
1671
|
print(f" 建立標籤失敗 {tag_name}: {e}")
|
|
1672
|
+
# Read last_config once; share with detect_new_vm / detect_tag_changes /
|
|
1673
|
+
# detect_config_drift / should_sync_vm to avoid repeated SQLite round-trips.
|
|
1674
|
+
last_config = (
|
|
1675
|
+
self.state_db.get_last_vm_config(int(vm_id), self.cluster_name)
|
|
1676
|
+
if (self.enhanced_mode and self.state_db) else None
|
|
1677
|
+
)
|
|
1624
1678
|
if self.enhanced_mode:
|
|
1625
|
-
self.detect_new_vm(int(vm_id), original_vm_name, node_name)
|
|
1679
|
+
self.detect_new_vm(int(vm_id), original_vm_name, node_name, last_config=last_config)
|
|
1626
1680
|
if self.enhanced_mode and tag_names:
|
|
1627
|
-
self.detect_tag_changes(int(vm_id), original_vm_name, tag_names)
|
|
1681
|
+
self.detect_tag_changes(int(vm_id), original_vm_name, tag_names, last_config=last_config)
|
|
1628
1682
|
vm_status = vm_data.get('status', '')
|
|
1629
1683
|
if tag_names and vm_status in ('running', 'stopped'):
|
|
1630
1684
|
first_tag = tag_names[0].lower()
|
|
@@ -1707,8 +1761,9 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1707
1761
|
})
|
|
1708
1762
|
if self.enhanced_mode:
|
|
1709
1763
|
self.detect_config_drift(int(vm_id), original_vm_name, vm_config, tag_names,
|
|
1710
|
-
network_interfaces, current_node=node_name)
|
|
1711
|
-
if not force and not self.should_sync_vm(int(vm_id), vm_config, tag_names, network_interfaces
|
|
1764
|
+
network_interfaces, current_node=node_name, last_config=last_config)
|
|
1765
|
+
if not force and not self.should_sync_vm(int(vm_id), vm_config, tag_names, network_interfaces,
|
|
1766
|
+
last_config=last_config):
|
|
1712
1767
|
cached_vm = self.nb_cache['virtual_machines_by_serial'].get(f"{vm_id}::{cluster['id']}")
|
|
1713
1768
|
if cached_vm is None:
|
|
1714
1769
|
# VM 在 NetBox 中不存在(例如隨節點被連帶刪除),
|
|
@@ -1749,7 +1804,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1749
1804
|
vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
|
|
1750
1805
|
memory=memory, vcpus=vcpus_save, tags=tag_names,
|
|
1751
1806
|
node=node_name, primary_ip=cur_ip, vm_name=original_vm_name,
|
|
1752
|
-
description=(vm_config.get('description')
|
|
1807
|
+
description=self._norm_desc(vm_config.get('description'))[:500],
|
|
1753
1808
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1754
1809
|
)
|
|
1755
1810
|
except Exception as _snap_err:
|
|
@@ -1806,7 +1861,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1806
1861
|
update_data = {
|
|
1807
1862
|
'name': vm_name, 'cluster': cluster['id'], 'device': device.id, 'role': role_id,
|
|
1808
1863
|
'vcpus': vcpus, 'memory': int(vm_config.get('memory', 0)), 'status': status,
|
|
1809
|
-
'description': (vm_config.get('description')
|
|
1864
|
+
'description': self._norm_desc(vm_config.get('description'))[:200], 'platform': platform_id,
|
|
1810
1865
|
'start_on_boot': boot_choice
|
|
1811
1866
|
}
|
|
1812
1867
|
if tag_ids:
|
|
@@ -1825,7 +1880,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1825
1880
|
vm_data_dict = {
|
|
1826
1881
|
'serial': vm_id, 'name': vm_name, 'cluster': cluster['id'], 'device': device.id,
|
|
1827
1882
|
'role': role_id, 'vcpus': vcpus, 'memory': int(vm_config.get('memory', 0)),
|
|
1828
|
-
'status': status, 'description': (vm_config.get('description')
|
|
1883
|
+
'status': status, 'description': self._norm_desc(vm_config.get('description'))[:200],
|
|
1829
1884
|
'platform': platform_id, 'start_on_boot': boot_choice
|
|
1830
1885
|
}
|
|
1831
1886
|
if tag_ids:
|
|
@@ -1862,7 +1917,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1862
1917
|
vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
|
|
1863
1918
|
memory=memory, vcpus=vcpus_save, tags=tag_names,
|
|
1864
1919
|
node=node_name, primary_ip=primary_ip_str, vm_name=original_vm_name,
|
|
1865
|
-
description=(vm_config.get('description')
|
|
1920
|
+
description=self._norm_desc(vm_config.get('description'))[:500],
|
|
1866
1921
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1867
1922
|
)
|
|
1868
1923
|
return True
|
|
@@ -1887,7 +1942,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1887
1942
|
node=node_name,
|
|
1888
1943
|
primary_ip=_ip,
|
|
1889
1944
|
vm_name=original_vm_name,
|
|
1890
|
-
description=(vm_config.get('description')
|
|
1945
|
+
description=self._norm_desc(vm_config.get('description'))[:500],
|
|
1891
1946
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1892
1947
|
)
|
|
1893
1948
|
except Exception:
|
|
@@ -1930,6 +1985,18 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1930
1985
|
print("=" * 50)
|
|
1931
1986
|
if self.enhanced_mode and self.state_db:
|
|
1932
1987
|
self.sync_log_id = self.state_db.start_sync_log(self.cluster_name)
|
|
1988
|
+
# Load notify_on_sync from PveClusterConfig (DB setting takes precedence over default True)
|
|
1989
|
+
self.notify_on_sync = True
|
|
1990
|
+
try:
|
|
1991
|
+
from pve_sync_plugin.models import PveClusterConfig
|
|
1992
|
+
cfg = PveClusterConfig.objects.filter(name=self.cluster_name).first()
|
|
1993
|
+
if cfg is not None:
|
|
1994
|
+
self.notify_on_sync = cfg.notify_on_sync
|
|
1995
|
+
except Exception:
|
|
1996
|
+
pass
|
|
1997
|
+
# Reset per-sync Telegram counters
|
|
1998
|
+
self._telegram_sent = 0
|
|
1999
|
+
self._telegram_suppressed = 0
|
|
1933
2000
|
sync_mode = "增量" if self.sync_config.get('incremental', True) else "全量"
|
|
1934
2001
|
start_message = f"""
|
|
1935
2002
|
🔄 <b>PVE-NetBox 同步開始</b>
|
|
@@ -154,7 +154,7 @@ class StateDB:
|
|
|
154
154
|
with sqlite3.connect(self.db_path) as conn:
|
|
155
155
|
conn.execute(
|
|
156
156
|
"""
|
|
157
|
-
INSERT INTO vm_config_history
|
|
157
|
+
INSERT OR REPLACE INTO vm_config_history
|
|
158
158
|
(vm_id, cluster_name, config_hash, memory, vcpus, tags_json,
|
|
159
159
|
node, primary_ip, vm_name, description, disk_summary)
|
|
160
160
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -256,6 +256,28 @@ class StateDB:
|
|
|
256
256
|
})
|
|
257
257
|
return changes
|
|
258
258
|
|
|
259
|
+
def cleanup_old_history(self, cluster_name: str, days: int) -> int:
|
|
260
|
+
"""Delete vm_config_history rows older than `days`, keeping the latest row per VM."""
|
|
261
|
+
import datetime as _dt
|
|
262
|
+
cutoff = (_dt.datetime.utcnow() - _dt.timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S')
|
|
263
|
+
with self._lock:
|
|
264
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
265
|
+
result = conn.execute(
|
|
266
|
+
"""
|
|
267
|
+
DELETE FROM vm_config_history
|
|
268
|
+
WHERE cluster_name = ?
|
|
269
|
+
AND sync_time < ?
|
|
270
|
+
AND sync_time NOT IN (
|
|
271
|
+
SELECT MAX(sync_time) FROM vm_config_history
|
|
272
|
+
WHERE cluster_name = ?
|
|
273
|
+
GROUP BY vm_id
|
|
274
|
+
)
|
|
275
|
+
""",
|
|
276
|
+
(cluster_name, cutoff, cluster_name),
|
|
277
|
+
)
|
|
278
|
+
conn.commit()
|
|
279
|
+
return result.rowcount
|
|
280
|
+
|
|
259
281
|
# ========== 节点状态历史 ==========
|
|
260
282
|
|
|
261
283
|
def save_node_status(self, node_name: str, cluster_name: str, status: str):
|
|
@@ -507,7 +529,7 @@ def compute_config_hash(vm_config: Dict[str, Any], tags: List[str], network_inte
|
|
|
507
529
|
'ostype': vm_config.get('ostype', ''),
|
|
508
530
|
'cores': vm_config.get('cores', 1),
|
|
509
531
|
'sockets': vm_config.get('sockets', 1),
|
|
510
|
-
'description': vm_config.get('description', '')[:200],
|
|
532
|
+
'description': (vm_config.get('description') or '').replace('\r\n', '\n').replace('\r', '\n').strip()[:200],
|
|
511
533
|
'tags': sorted(tags), # 排序以确保一致性
|
|
512
534
|
'networks': net_config # 添加网络接口配置
|
|
513
535
|
}
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.4 → proxsync-2.0.6}/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.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py
RENAMED
|
File without changes
|
{proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/migrations/0016_pvevmtasklog_status_textfield.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html
RENAMED
|
File without changes
|
{proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/inc/vm_task_log_panel.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
|
|
File without changes
|
{proxsync-2.0.4 → proxsync-2.0.6}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html
RENAMED
|
File without changes
|
{proxsync-2.0.4 → proxsync-2.0.6}/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
|
|
File without changes
|