proxsync 2.0.5__tar.gz → 2.0.7__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.5/proxsync.egg-info → proxsync-2.0.7}/PKG-INFO +1 -1
- {proxsync-2.0.5 → proxsync-2.0.7/proxsync.egg-info}/PKG-INFO +1 -1
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync.py +94 -32
- {proxsync-2.0.5 → proxsync-2.0.7}/pyproject.toml +1 -1
- {proxsync-2.0.5 → proxsync-2.0.7}/state_db.py +23 -1
- {proxsync-2.0.5 → proxsync-2.0.7}/MANIFEST.in +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/README.md +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/config.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pbs_sync.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/proxsync.egg-info/SOURCES.txt +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/proxsync.egg-info/dependency_links.txt +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/proxsync.egg-info/requires.txt +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/proxsync.egg-info/top_level.txt +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/__init__.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/api/__init__.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/api/serializers.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/api/urls.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/api/views.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/apps.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/choices.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/filtersets.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/forms.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/management/__init__.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/management/commands/__init__.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/management/commands/pve_sync.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/management/commands/run_scheduled_syncs.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0001_initial.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0002_pvepluginsettings.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0003_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0004_model_verbose_names.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0007_rename_verbose_names_to_proxmox_sync.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0009_add_every_3h_schedule.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0010_pvedriftevent.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0014_vmprovisioninglog.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0015_pvevmtasklog.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0016_pvevmtasklog_status_textfield.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/__init__.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/models.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/navigation.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/signals.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/sync/__init__.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/sync/config_bridge.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/sync/engine.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/tables.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/tasks.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/template_content.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/dashboard.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/inc/vm_task_log_panel.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/pbsserverconfig.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/pvebackupstatus.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/pveclusterconfig.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/pvedriftevent.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/pvesyncjob.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/pvevmtasklog_list.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/pvewebhookevent.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/settings.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templatetags/__init__.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/templatetags/pve_sync_tags.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/urls.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/utils.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/view_helpers.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/views.py +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/setup.cfg +0 -0
- {proxsync-2.0.5 → proxsync-2.0.7}/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}")
|
|
@@ -1111,13 +1126,13 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1111
1126
|
except Exception as e:
|
|
1112
1127
|
print(f" ⚠ 無法寫入漂移事件記錄: {e}")
|
|
1113
1128
|
|
|
1114
|
-
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):
|
|
1115
1130
|
"""若此 vmid 在先前同步中不存在(但叢集有先前記錄),視為新增 VM。"""
|
|
1116
1131
|
if not self.enhanced_mode or not self.state_db:
|
|
1117
1132
|
return
|
|
1118
1133
|
if not self._known_vmids:
|
|
1119
1134
|
return # 首次同步,無先前記錄,不觸發
|
|
1120
|
-
if
|
|
1135
|
+
if last_config:
|
|
1121
1136
|
return # 已有記錄,不是新增
|
|
1122
1137
|
print(f"🆕 VM {vm_name} 新增偵測 (ID: {vm_id}, 節點: {node_name})")
|
|
1123
1138
|
self.stats['config_drifts_detected'] += 1
|
|
@@ -1256,15 +1271,29 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1256
1271
|
print(f" 🧹 Log 清理:刪除 {d_del} 筆漂移事件、{t_del} 筆操作記錄(>{days} 天)")
|
|
1257
1272
|
except Exception as exc:
|
|
1258
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}")
|
|
1259
1285
|
|
|
1260
1286
|
def sync_vm_tasks(self):
|
|
1261
1287
|
"""從 PVE 各節點抓取 VM 操作記錄(建立/刪除/Clone/遷移)並同步到 NetBox。"""
|
|
1288
|
+
import calendar
|
|
1289
|
+
import datetime as _dt
|
|
1262
1290
|
try:
|
|
1263
1291
|
from django.utils import timezone
|
|
1264
1292
|
from pve_sync_plugin.models import PveVmTaskLog
|
|
1265
1293
|
except ImportError:
|
|
1266
1294
|
return
|
|
1267
1295
|
|
|
1296
|
+
_utc = _dt.timezone.utc
|
|
1268
1297
|
VM_TASK_TYPES = {
|
|
1269
1298
|
"qmcreate", "qmdestroy", "qmclone", "qmmigrate", "qmrestore",
|
|
1270
1299
|
"qmstart", "qmstop", "qmshutdown", "qmreboot",
|
|
@@ -1298,10 +1327,18 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1298
1327
|
cluster_name=self.cluster_name
|
|
1299
1328
|
).order_by("-start_time").values_list("start_time", flat=True).first()
|
|
1300
1329
|
if latest:
|
|
1301
|
-
import calendar
|
|
1302
1330
|
since_ts = int(calendar.timegm(latest.utctimetuple())) + 1
|
|
1303
1331
|
else:
|
|
1304
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)
|
|
1305
1342
|
new_count = 0
|
|
1306
1343
|
|
|
1307
1344
|
for node_name in self.pve_cache.get("vms_by_node", {}):
|
|
@@ -1325,8 +1362,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1325
1362
|
except (ValueError, TypeError):
|
|
1326
1363
|
continue
|
|
1327
1364
|
|
|
1328
|
-
# UPID
|
|
1329
|
-
if
|
|
1365
|
+
# UPID 去重(使用預先撈取的集合,避免 N+1 查詢)
|
|
1366
|
+
if upid in existing_upids or upid in new_upids:
|
|
1330
1367
|
continue
|
|
1331
1368
|
|
|
1332
1369
|
task_type = task.get("type")
|
|
@@ -1336,8 +1373,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1336
1373
|
status = task.get("status", "") or ""
|
|
1337
1374
|
vm_name = vmid_to_name.get(vmid, f"VM-{vmid}")
|
|
1338
1375
|
|
|
1339
|
-
import datetime as _dt
|
|
1340
|
-
_utc = _dt.timezone.utc
|
|
1341
1376
|
start_dt = _dt.datetime.fromtimestamp(start_ts, tz=_utc) if start_ts else timezone.now()
|
|
1342
1377
|
end_dt = _dt.datetime.fromtimestamp(end_ts, tz=_utc) if end_ts else None
|
|
1343
1378
|
|
|
@@ -1354,6 +1389,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1354
1389
|
status=status,
|
|
1355
1390
|
notified_telegram=False,
|
|
1356
1391
|
)
|
|
1392
|
+
new_upids.add(upid)
|
|
1357
1393
|
new_count += 1
|
|
1358
1394
|
|
|
1359
1395
|
# Telegram 通知(僅近期且已完成的操作)
|
|
@@ -1381,11 +1417,12 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1381
1417
|
|
|
1382
1418
|
def detect_config_drift(self, vm_id: int, vm_name: str, vm_config: Dict[str, Any],
|
|
1383
1419
|
current_tags: List[str], network_interfaces: List[Dict] = None,
|
|
1384
|
-
current_node: str = None) -> List[Dict]:
|
|
1420
|
+
current_node: str = None, last_config=None) -> List[Dict]:
|
|
1385
1421
|
if not self.enhanced_mode or not self.state_db:
|
|
1386
1422
|
return []
|
|
1387
1423
|
drifts = []
|
|
1388
|
-
last_config
|
|
1424
|
+
if last_config is None:
|
|
1425
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1389
1426
|
if not last_config:
|
|
1390
1427
|
return drifts
|
|
1391
1428
|
current_memory = int(vm_config.get('memory', 0))
|
|
@@ -1466,8 +1503,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1466
1503
|
self._write_drift_event(vm_name, vm_id, 'disk_change', k, old_v, new_v, notified=True)
|
|
1467
1504
|
|
|
1468
1505
|
# VM 描述變更偵測
|
|
1469
|
-
current_desc = self._norm_desc(vm_config.get('description'))[:500]
|
|
1470
|
-
old_desc = self._norm_desc(last_config.get('description'))
|
|
1506
|
+
current_desc = self._norm_desc(vm_config.get('description'))[:500].strip()
|
|
1507
|
+
old_desc = self._norm_desc(last_config.get('description'))[:500].strip()
|
|
1471
1508
|
if old_desc and old_desc != current_desc:
|
|
1472
1509
|
self.stats['config_drifts_detected'] += 1
|
|
1473
1510
|
print(f"📝 VM {vm_name} 描述變更")
|
|
@@ -1507,7 +1544,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1507
1544
|
return drifts
|
|
1508
1545
|
|
|
1509
1546
|
def should_sync_vm(self, vm_id: int, vm_config: Dict[str, Any], tags: List[str],
|
|
1510
|
-
network_interfaces: List[Dict] = None) -> bool:
|
|
1547
|
+
network_interfaces: List[Dict] = None, last_config=None) -> bool:
|
|
1511
1548
|
if not self.sync_config.get('incremental', True):
|
|
1512
1549
|
return True
|
|
1513
1550
|
if self.sync_config.get('force_full_sync', False):
|
|
@@ -1515,34 +1552,35 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1515
1552
|
current_hash = compute_config_hash(vm_config, tags, network_interfaces)
|
|
1516
1553
|
if not self.state_db:
|
|
1517
1554
|
return True
|
|
1518
|
-
last_config
|
|
1555
|
+
if last_config is None:
|
|
1556
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1519
1557
|
if not last_config:
|
|
1520
1558
|
return True
|
|
1521
1559
|
if current_hash != last_config.get('config_hash'):
|
|
1522
1560
|
return True
|
|
1523
1561
|
return False
|
|
1524
1562
|
|
|
1525
|
-
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]:
|
|
1526
1565
|
if not self.enhanced_mode or not self.state_db:
|
|
1527
1566
|
return []
|
|
1528
|
-
|
|
1529
|
-
|
|
1567
|
+
if last_config is None:
|
|
1568
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1530
1569
|
if not last_config:
|
|
1531
|
-
return
|
|
1570
|
+
return []
|
|
1532
1571
|
old_tags = set(last_config.get('tags', []))
|
|
1533
1572
|
new_tags = set(current_tags)
|
|
1534
1573
|
added = new_tags - old_tags
|
|
1535
1574
|
removed = old_tags - new_tags
|
|
1575
|
+
changes = []
|
|
1536
1576
|
for tag in added:
|
|
1537
1577
|
changes.append({'type': 'added', 'tag': tag})
|
|
1538
1578
|
self.stats['tag_changes'] += 1
|
|
1539
1579
|
print(f"🏷️ VM {vm_name} 新增標籤: {tag}")
|
|
1540
|
-
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', '', tag, notified=False)
|
|
1541
1580
|
for tag in removed:
|
|
1542
1581
|
changes.append({'type': 'removed', 'tag': tag})
|
|
1543
1582
|
self.stats['tag_changes'] += 1
|
|
1544
1583
|
print(f"🏷️ VM {vm_name} 移除標籤: {tag}")
|
|
1545
|
-
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', tag, '', notified=False)
|
|
1546
1584
|
if changes:
|
|
1547
1585
|
added_list = [c['tag'] for c in changes if c['type'] == 'added']
|
|
1548
1586
|
removed_list = [c['tag'] for c in changes if c['type'] == 'removed']
|
|
@@ -1558,7 +1596,12 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1558
1596
|
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1559
1597
|
f"{detail}\n請確認是否為預期變更。"
|
|
1560
1598
|
)
|
|
1561
|
-
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)
|
|
1562
1605
|
return changes
|
|
1563
1606
|
|
|
1564
1607
|
def detect_ip_change(self, vm_id: int, vm_name: str, current_ip: str):
|
|
@@ -1626,10 +1669,16 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1626
1669
|
tag_ids.append(tag.id)
|
|
1627
1670
|
except Exception as e:
|
|
1628
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
|
+
)
|
|
1629
1678
|
if self.enhanced_mode:
|
|
1630
|
-
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)
|
|
1631
1680
|
if self.enhanced_mode and tag_names:
|
|
1632
|
-
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)
|
|
1633
1682
|
vm_status = vm_data.get('status', '')
|
|
1634
1683
|
if tag_names and vm_status in ('running', 'stopped'):
|
|
1635
1684
|
first_tag = tag_names[0].lower()
|
|
@@ -1712,8 +1761,9 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1712
1761
|
})
|
|
1713
1762
|
if self.enhanced_mode:
|
|
1714
1763
|
self.detect_config_drift(int(vm_id), original_vm_name, vm_config, tag_names,
|
|
1715
|
-
network_interfaces, current_node=node_name)
|
|
1716
|
-
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):
|
|
1717
1767
|
cached_vm = self.nb_cache['virtual_machines_by_serial'].get(f"{vm_id}::{cluster['id']}")
|
|
1718
1768
|
if cached_vm is None:
|
|
1719
1769
|
# VM 在 NetBox 中不存在(例如隨節點被連帶刪除),
|
|
@@ -1754,7 +1804,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1754
1804
|
vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
|
|
1755
1805
|
memory=memory, vcpus=vcpus_save, tags=tag_names,
|
|
1756
1806
|
node=node_name, primary_ip=cur_ip, vm_name=original_vm_name,
|
|
1757
|
-
description=self._norm_desc(vm_config.get('description'))[:500],
|
|
1807
|
+
description=self._norm_desc(vm_config.get('description'))[:500].strip(),
|
|
1758
1808
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1759
1809
|
)
|
|
1760
1810
|
except Exception as _snap_err:
|
|
@@ -1867,7 +1917,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1867
1917
|
vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
|
|
1868
1918
|
memory=memory, vcpus=vcpus_save, tags=tag_names,
|
|
1869
1919
|
node=node_name, primary_ip=primary_ip_str, vm_name=original_vm_name,
|
|
1870
|
-
description=self._norm_desc(vm_config.get('description'))[:500],
|
|
1920
|
+
description=self._norm_desc(vm_config.get('description'))[:500].strip(),
|
|
1871
1921
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1872
1922
|
)
|
|
1873
1923
|
return True
|
|
@@ -1892,7 +1942,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1892
1942
|
node=node_name,
|
|
1893
1943
|
primary_ip=_ip,
|
|
1894
1944
|
vm_name=original_vm_name,
|
|
1895
|
-
description=self._norm_desc(vm_config.get('description'))[:500],
|
|
1945
|
+
description=self._norm_desc(vm_config.get('description'))[:500].strip(),
|
|
1896
1946
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1897
1947
|
)
|
|
1898
1948
|
except Exception:
|
|
@@ -1935,6 +1985,18 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1935
1985
|
print("=" * 50)
|
|
1936
1986
|
if self.enhanced_mode and self.state_db:
|
|
1937
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
|
|
1938
2000
|
sync_mode = "增量" if self.sync_config.get('incremental', True) else "全量"
|
|
1939
2001
|
start_message = f"""
|
|
1940
2002
|
🔄 <b>PVE-NetBox 同步開始</b>
|
|
@@ -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') or '').replace('\r\n', '\n').replace('\r', '\n').strip()[:200],
|
|
532
|
+
'description': (vm_config.get('description') or '').replace('\r\n', '\n').replace('\r', '\n').strip()[:200].strip(),
|
|
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.5 → proxsync-2.0.7}/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.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py
RENAMED
|
File without changes
|
{proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.0.5 → proxsync-2.0.7}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.5 → proxsync-2.0.7}/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.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html
RENAMED
|
File without changes
|
{proxsync-2.0.5 → proxsync-2.0.7}/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.5 → proxsync-2.0.7}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html
RENAMED
|
File without changes
|
{proxsync-2.0.5 → proxsync-2.0.7}/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
|