proxsync 2.1.0__tar.gz → 2.1.2__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.1.0/proxsync.egg-info → proxsync-2.1.2}/PKG-INFO +47 -2
- {proxsync-2.1.0 → proxsync-2.1.2}/README.md +46 -1
- {proxsync-2.1.0 → proxsync-2.1.2/proxsync.egg-info}/PKG-INFO +47 -2
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync.py +180 -37
- {proxsync-2.1.0 → proxsync-2.1.2}/pyproject.toml +1 -1
- {proxsync-2.1.0 → proxsync-2.1.2}/state_db.py +24 -2
- {proxsync-2.1.0 → proxsync-2.1.2}/MANIFEST.in +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/config.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pbs_sync.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/proxsync.egg-info/SOURCES.txt +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/proxsync.egg-info/dependency_links.txt +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/proxsync.egg-info/requires.txt +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/proxsync.egg-info/top_level.txt +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/__init__.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/api/__init__.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/api/serializers.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/api/urls.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/api/views.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/apps.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/choices.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/filtersets.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/forms.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/management/__init__.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/management/commands/__init__.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/management/commands/pve_sync.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/management/commands/run_scheduled_syncs.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0001_initial.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0002_pvepluginsettings.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0003_netboxmodel_timestamps.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0004_model_verbose_names.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0007_rename_verbose_names_to_proxmox_sync.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0009_add_every_3h_schedule.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0010_pvedriftevent.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0014_vmprovisioninglog.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0015_pvevmtasklog.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0016_pvevmtasklog_status_textfield.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/__init__.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/models.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/navigation.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/signals.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/sync/__init__.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/sync/config_bridge.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/sync/engine.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/tables.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/tasks.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/template_content.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/dashboard.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/inc/vm_task_log_panel.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/pbsserverconfig.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/pvebackupstatus.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/pveclusterconfig.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/pvedriftevent.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/pvesyncjob.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/pvevmtasklog_list.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/pvewebhookevent.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/settings.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templatetags/__init__.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/templatetags/pve_sync_tags.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/urls.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/utils.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/view_helpers.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/views.py +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/setup.cfg +0 -0
- {proxsync-2.1.0 → proxsync-2.1.2}/tests/test_plugin_contracts.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxsync
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: NetBox plugin for Proxmox VE inventory synchronization
|
|
5
5
|
Author-email: lixiujie85 <lixiujie85@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -36,7 +36,7 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
36
36
|
|
|
37
37
|
# proxsync
|
|
38
38
|
|
|
39
|
-
[](https://github.com/XiuJie2/proxsync)
|
|
40
40
|
[](https://www.python.org/)
|
|
41
41
|
[](https://netbox.dev/)
|
|
42
42
|
[](https://www.proxmox.com/)
|
|
@@ -46,6 +46,49 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
49
|
+
## 目錄
|
|
50
|
+
|
|
51
|
+
- [專案截圖](#專案截圖)
|
|
52
|
+
- [功能特色](#功能特色)
|
|
53
|
+
- [系統需求](#系統需求)
|
|
54
|
+
- [安裝](#安裝)
|
|
55
|
+
- [設定](#設定)
|
|
56
|
+
- [使用方式](#使用方式)
|
|
57
|
+
- [資料對應](#資料對應)
|
|
58
|
+
- [專案結構](#專案結構)
|
|
59
|
+
- [設定優先級](#設定優先級)
|
|
60
|
+
- [常見問題](#常見問題)
|
|
61
|
+
- [開發](#開發)
|
|
62
|
+
- [授權](#授權)
|
|
63
|
+
- [致謝](#致謝)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 專案截圖
|
|
68
|
+
|
|
69
|
+
<!--
|
|
70
|
+
將截圖放進 docs/screenshots/ 目錄,檔名對應下方即可自動顯示。
|
|
71
|
+
建議尺寸:1600x900 或以上,PNG 格式。
|
|
72
|
+
-->
|
|
73
|
+
|
|
74
|
+
### Dashboard
|
|
75
|
+
|
|
76
|
+

|
|
77
|
+
|
|
78
|
+
### PVE Cluster 詳情
|
|
79
|
+
|
|
80
|
+

|
|
81
|
+
|
|
82
|
+
### Sync Jobs 同步歷史
|
|
83
|
+
|
|
84
|
+

|
|
85
|
+
|
|
86
|
+
### Telegram 通知
|
|
87
|
+
|
|
88
|
+

|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
49
92
|
## 功能特色
|
|
50
93
|
|
|
51
94
|
### Proxmox VE 同步
|
|
@@ -54,6 +97,8 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
54
97
|
- VM / CT → NetBox **Virtual Machine**(狀態、規格、磁碟、標籤)
|
|
55
98
|
- QEMU Guest Agent 整合,從執行中 VM 取得實際 IP 位址
|
|
56
99
|
- 配置漂移偵測:自動發現 VM 的 CPU / 記憶體設定變更
|
|
100
|
+
- 節點放置檢查:VM 描述 (Notes) 內若標記 `node: <預期節點>`,當實際所在節點與標記不符時透過 Telegram 提醒
|
|
101
|
+
- Guest Agent 異常提醒:VM 執行中卻取不到 IP(QEMU Guest Agent 未啟用或未安裝)時透過 Telegram 提醒
|
|
57
102
|
- 增量同步:透過 SQLite state_db 追蹤變更,避免全量更新
|
|
58
103
|
|
|
59
104
|
### 多叢集支援
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# proxsync
|
|
2
2
|
|
|
3
|
-
[](https://github.com/XiuJie2/proxsync)
|
|
4
4
|
[](https://www.python.org/)
|
|
5
5
|
[](https://netbox.dev/)
|
|
6
6
|
[](https://www.proxmox.com/)
|
|
@@ -10,6 +10,49 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## 目錄
|
|
14
|
+
|
|
15
|
+
- [專案截圖](#專案截圖)
|
|
16
|
+
- [功能特色](#功能特色)
|
|
17
|
+
- [系統需求](#系統需求)
|
|
18
|
+
- [安裝](#安裝)
|
|
19
|
+
- [設定](#設定)
|
|
20
|
+
- [使用方式](#使用方式)
|
|
21
|
+
- [資料對應](#資料對應)
|
|
22
|
+
- [專案結構](#專案結構)
|
|
23
|
+
- [設定優先級](#設定優先級)
|
|
24
|
+
- [常見問題](#常見問題)
|
|
25
|
+
- [開發](#開發)
|
|
26
|
+
- [授權](#授權)
|
|
27
|
+
- [致謝](#致謝)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 專案截圖
|
|
32
|
+
|
|
33
|
+
<!--
|
|
34
|
+
將截圖放進 docs/screenshots/ 目錄,檔名對應下方即可自動顯示。
|
|
35
|
+
建議尺寸:1600x900 或以上,PNG 格式。
|
|
36
|
+
-->
|
|
37
|
+
|
|
38
|
+
### Dashboard
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
### PVE Cluster 詳情
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
### Sync Jobs 同步歷史
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
### Telegram 通知
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
13
56
|
## 功能特色
|
|
14
57
|
|
|
15
58
|
### Proxmox VE 同步
|
|
@@ -18,6 +61,8 @@
|
|
|
18
61
|
- VM / CT → NetBox **Virtual Machine**(狀態、規格、磁碟、標籤)
|
|
19
62
|
- QEMU Guest Agent 整合,從執行中 VM 取得實際 IP 位址
|
|
20
63
|
- 配置漂移偵測:自動發現 VM 的 CPU / 記憶體設定變更
|
|
64
|
+
- 節點放置檢查:VM 描述 (Notes) 內若標記 `node: <預期節點>`,當實際所在節點與標記不符時透過 Telegram 提醒
|
|
65
|
+
- Guest Agent 異常提醒:VM 執行中卻取不到 IP(QEMU Guest Agent 未啟用或未安裝)時透過 Telegram 提醒
|
|
21
66
|
- 增量同步:透過 SQLite state_db 追蹤變更,避免全量更新
|
|
22
67
|
|
|
23
68
|
### 多叢集支援
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxsync
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: NetBox plugin for Proxmox VE inventory synchronization
|
|
5
5
|
Author-email: lixiujie85 <lixiujie85@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -36,7 +36,7 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
36
36
|
|
|
37
37
|
# proxsync
|
|
38
38
|
|
|
39
|
-
[](https://github.com/XiuJie2/proxsync)
|
|
40
40
|
[](https://www.python.org/)
|
|
41
41
|
[](https://netbox.dev/)
|
|
42
42
|
[](https://www.proxmox.com/)
|
|
@@ -46,6 +46,49 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
49
|
+
## 目錄
|
|
50
|
+
|
|
51
|
+
- [專案截圖](#專案截圖)
|
|
52
|
+
- [功能特色](#功能特色)
|
|
53
|
+
- [系統需求](#系統需求)
|
|
54
|
+
- [安裝](#安裝)
|
|
55
|
+
- [設定](#設定)
|
|
56
|
+
- [使用方式](#使用方式)
|
|
57
|
+
- [資料對應](#資料對應)
|
|
58
|
+
- [專案結構](#專案結構)
|
|
59
|
+
- [設定優先級](#設定優先級)
|
|
60
|
+
- [常見問題](#常見問題)
|
|
61
|
+
- [開發](#開發)
|
|
62
|
+
- [授權](#授權)
|
|
63
|
+
- [致謝](#致謝)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 專案截圖
|
|
68
|
+
|
|
69
|
+
<!--
|
|
70
|
+
將截圖放進 docs/screenshots/ 目錄,檔名對應下方即可自動顯示。
|
|
71
|
+
建議尺寸:1600x900 或以上,PNG 格式。
|
|
72
|
+
-->
|
|
73
|
+
|
|
74
|
+
### Dashboard
|
|
75
|
+
|
|
76
|
+

|
|
77
|
+
|
|
78
|
+
### PVE Cluster 詳情
|
|
79
|
+
|
|
80
|
+

|
|
81
|
+
|
|
82
|
+
### Sync Jobs 同步歷史
|
|
83
|
+
|
|
84
|
+

|
|
85
|
+
|
|
86
|
+
### Telegram 通知
|
|
87
|
+
|
|
88
|
+

|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
49
92
|
## 功能特色
|
|
50
93
|
|
|
51
94
|
### Proxmox VE 同步
|
|
@@ -54,6 +97,8 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
54
97
|
- VM / CT → NetBox **Virtual Machine**(狀態、規格、磁碟、標籤)
|
|
55
98
|
- QEMU Guest Agent 整合,從執行中 VM 取得實際 IP 位址
|
|
56
99
|
- 配置漂移偵測:自動發現 VM 的 CPU / 記憶體設定變更
|
|
100
|
+
- 節點放置檢查:VM 描述 (Notes) 內若標記 `node: <預期節點>`,當實際所在節點與標記不符時透過 Telegram 提醒
|
|
101
|
+
- Guest Agent 異常提醒:VM 執行中卻取不到 IP(QEMU Guest Agent 未啟用或未安裝)時透過 Telegram 提醒
|
|
57
102
|
- 增量同步:透過 SQLite state_db 追蹤變更,避免全量更新
|
|
58
103
|
|
|
59
104
|
### 多叢集支援
|
|
@@ -44,6 +44,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
44
44
|
'config_drifts_detected': 0,
|
|
45
45
|
'tag_changes': 0,
|
|
46
46
|
'resources_alert': 0,
|
|
47
|
+
'node_mismatch_alerts': 0,
|
|
48
|
+
'no_ip_alerts': 0,
|
|
47
49
|
'total_vms': 0,
|
|
48
50
|
'success_vms': 0,
|
|
49
51
|
'elapsed_time': 0
|
|
@@ -125,12 +127,25 @@ class OptimizedPVEToNetBoxSync:
|
|
|
125
127
|
self.error_log = []
|
|
126
128
|
|
|
127
129
|
# ---------- Telegram ----------
|
|
128
|
-
|
|
130
|
+
# Max Telegram messages per sync run; excess are counted and reported in the summary.
|
|
131
|
+
_MAX_TELEGRAM_PER_SYNC = 20
|
|
132
|
+
|
|
133
|
+
def send_telegram_notification(self, message: str) -> bool:
|
|
134
|
+
if not getattr(self, 'notify_on_sync', True):
|
|
135
|
+
return False
|
|
136
|
+
if not self.telegram_bot_token or not self.telegram_chat_id:
|
|
137
|
+
return False
|
|
138
|
+
# Throttle: cap messages per sync run to avoid flooding on bulk changes.
|
|
139
|
+
sent = getattr(self, '_telegram_sent', 0)
|
|
140
|
+
if sent >= self._MAX_TELEGRAM_PER_SYNC:
|
|
141
|
+
self._telegram_suppressed = getattr(self, '_telegram_suppressed', 0) + 1
|
|
142
|
+
return False
|
|
129
143
|
try:
|
|
130
144
|
url = f"https://api.telegram.org/bot{self.telegram_bot_token}/sendMessage"
|
|
131
145
|
payload = {'chat_id': self.telegram_chat_id, 'text': message, 'parse_mode': 'HTML'}
|
|
132
146
|
response = requests.post(url, data=payload, timeout=10)
|
|
133
147
|
if response.status_code == 200:
|
|
148
|
+
self._telegram_sent = sent + 1
|
|
134
149
|
print(f"✓ Telegram 通知已發送")
|
|
135
150
|
return True
|
|
136
151
|
else:
|
|
@@ -233,6 +248,10 @@ class OptimizedPVEToNetBoxSync:
|
|
|
233
248
|
extras.append(f"🏷️ 標籤變更: {self.stats['tag_changes']}")
|
|
234
249
|
if self.stats.get('resources_alert', 0) > 0:
|
|
235
250
|
extras.append(f"⚠️ 資源告警: {self.stats['resources_alert']}")
|
|
251
|
+
if self.stats.get('node_mismatch_alerts', 0) > 0:
|
|
252
|
+
extras.append(f"📍 節點放置異常: {self.stats['node_mismatch_alerts']}")
|
|
253
|
+
if self.stats.get('no_ip_alerts', 0) > 0:
|
|
254
|
+
extras.append(f"🌐 運行中但無 IP: {self.stats['no_ip_alerts']}")
|
|
236
255
|
if extras:
|
|
237
256
|
message += "\n🔍 檢測發現:\n" + "\n".join(extras)
|
|
238
257
|
if self.error_log:
|
|
@@ -241,6 +260,9 @@ class OptimizedPVEToNetBoxSync:
|
|
|
241
260
|
message += f"• {error['vm_name']} - {error['ip_address']}\n"
|
|
242
261
|
if len(self.error_log) > 5:
|
|
243
262
|
message += f"• ... 還有 {len(self.error_log) - 5} 個錯誤\n"
|
|
263
|
+
suppressed = getattr(self, '_telegram_suppressed', 0)
|
|
264
|
+
if suppressed:
|
|
265
|
+
message += f"\n📵 本輪通知已達上限,{suppressed} 則訊息被壓制(詳見 NetBox 漂移事件記錄)"
|
|
244
266
|
self.send_telegram_notification(message)
|
|
245
267
|
|
|
246
268
|
# ---------- API 連接 ----------
|
|
@@ -327,6 +349,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
327
349
|
self.nb_cache['platforms'][platform.name.lower()] = platform
|
|
328
350
|
for role in self.nb_api.dcim.device_roles.all():
|
|
329
351
|
self.nb_cache['roles'][role.name.lower()] = role
|
|
352
|
+
self.nb_cache['device_roles'][role.name.lower()] = role
|
|
330
353
|
for cluster in self.nb_api.virtualization.clusters.all():
|
|
331
354
|
self.nb_cache['clusters'][str(cluster.id)] = cluster
|
|
332
355
|
for site in self.nb_api.dcim.sites.all():
|
|
@@ -335,8 +358,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
335
358
|
self.nb_cache['manufacturers'][manufacturer.name.lower()] = manufacturer
|
|
336
359
|
for device_type in self.nb_api.dcim.device_types.all():
|
|
337
360
|
self.nb_cache['device_types'][device_type.model.lower()] = device_type
|
|
338
|
-
for device_role in self.nb_api.dcim.device_roles.all():
|
|
339
|
-
self.nb_cache['device_roles'][device_role.name.lower()] = device_role
|
|
340
361
|
for cluster_type in self.nb_api.virtualization.cluster_types.all():
|
|
341
362
|
self.nb_cache['cluster_types'][cluster_type.name.lower()] = cluster_type
|
|
342
363
|
elapsed = time.time() - start_time
|
|
@@ -393,13 +414,13 @@ class OptimizedPVEToNetBoxSync:
|
|
|
393
414
|
vm['node'] = node_name
|
|
394
415
|
all_vms.append(vm)
|
|
395
416
|
self.pve_cache['vms_by_node'][node_name] = all_vms
|
|
396
|
-
for vm in all_vms
|
|
417
|
+
for vm in all_vms:
|
|
397
418
|
try:
|
|
398
419
|
if vm['type'] == 'qemu':
|
|
399
420
|
vm['config'] = self.pve_api.nodes(node_name).qemu(vm['vmid']).config.get()
|
|
400
421
|
else:
|
|
401
422
|
vm['config'] = self.pve_api.nodes(node_name).lxc(vm['vmid']).config.get()
|
|
402
|
-
except:
|
|
423
|
+
except Exception:
|
|
403
424
|
pass
|
|
404
425
|
except Exception as e:
|
|
405
426
|
print(f" 加載節點 {node_name} 的虛擬機失敗: {e}")
|
|
@@ -1147,6 +1168,25 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1147
1168
|
disks[key] = m.group(1) if m else val_str[:80]
|
|
1148
1169
|
return disks
|
|
1149
1170
|
|
|
1171
|
+
@staticmethod
|
|
1172
|
+
def _norm_desc(s) -> str:
|
|
1173
|
+
"""Normalize description: unify line endings, strip whitespace."""
|
|
1174
|
+
return (s or '').replace('\r\n', '\n').replace('\r', '\n').strip()
|
|
1175
|
+
|
|
1176
|
+
@staticmethod
|
|
1177
|
+
def _parse_expected_node(description: str) -> Optional[str]:
|
|
1178
|
+
"""從 VM 描述 (Notes) 解析使用者標記的預期節點。
|
|
1179
|
+
|
|
1180
|
+
支援格式如 "node: 181" 或 "node:181"(半形/全形冒號皆可,
|
|
1181
|
+
且不區分大小寫,Node/NODE/node 皆可),必須獨佔一行。
|
|
1182
|
+
找不到則回傳 None。
|
|
1183
|
+
"""
|
|
1184
|
+
import re
|
|
1185
|
+
if not description:
|
|
1186
|
+
return None
|
|
1187
|
+
m = re.search(r'(?im)^\s*node\s*[::]\s*(\S+)\s*$', description)
|
|
1188
|
+
return m.group(1).strip() if m else None
|
|
1189
|
+
|
|
1150
1190
|
def _write_drift_event(self, vm_name: str, vmid: int, drift_type: str,
|
|
1151
1191
|
field_name: str, old_value: str, new_value: str,
|
|
1152
1192
|
notified: bool = False):
|
|
@@ -1169,13 +1209,13 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1169
1209
|
except Exception as e:
|
|
1170
1210
|
print(f" ⚠ 無法寫入漂移事件記錄: {e}")
|
|
1171
1211
|
|
|
1172
|
-
def detect_new_vm(self, vm_id: int, vm_name: str, node_name: str):
|
|
1212
|
+
def detect_new_vm(self, vm_id: int, vm_name: str, node_name: str, last_config=None):
|
|
1173
1213
|
"""若此 vmid 在先前同步中不存在(但叢集有先前記錄),視為新增 VM。"""
|
|
1174
1214
|
if not self.enhanced_mode or not self.state_db:
|
|
1175
1215
|
return
|
|
1176
1216
|
if not self._known_vmids:
|
|
1177
1217
|
return # 首次同步,無先前記錄,不觸發
|
|
1178
|
-
if
|
|
1218
|
+
if last_config:
|
|
1179
1219
|
return # 已有記錄,不是新增
|
|
1180
1220
|
print(f"🆕 VM {vm_name} 新增偵測 (ID: {vm_id}, 節點: {node_name})")
|
|
1181
1221
|
self.stats['config_drifts_detected'] += 1
|
|
@@ -1314,15 +1354,29 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1314
1354
|
print(f" 🧹 Log 清理:刪除 {d_del} 筆漂移事件、{t_del} 筆操作記錄(>{days} 天)")
|
|
1315
1355
|
except Exception as exc:
|
|
1316
1356
|
print(f" ⚠️ Log 清理失敗: {exc}")
|
|
1357
|
+
# Also clean up state_db vm_config_history (keep latest row per VM, remove old history)
|
|
1358
|
+
if self.state_db:
|
|
1359
|
+
try:
|
|
1360
|
+
from pve_sync_plugin.models import PvePluginSettings
|
|
1361
|
+
days = PvePluginSettings.load().log_retention_days
|
|
1362
|
+
if days:
|
|
1363
|
+
h_del = self.state_db.cleanup_old_history(self.cluster_name, days)
|
|
1364
|
+
if h_del:
|
|
1365
|
+
print(f" 🧹 state_db 清理:刪除 {h_del} 筆舊配置歷史(>{days} 天)")
|
|
1366
|
+
except Exception as exc:
|
|
1367
|
+
print(f" ⚠️ state_db 歷史清理失敗: {exc}")
|
|
1317
1368
|
|
|
1318
1369
|
def sync_vm_tasks(self):
|
|
1319
1370
|
"""從 PVE 各節點抓取 VM 操作記錄(建立/刪除/Clone/遷移)並同步到 NetBox。"""
|
|
1371
|
+
import calendar
|
|
1372
|
+
import datetime as _dt
|
|
1320
1373
|
try:
|
|
1321
1374
|
from django.utils import timezone
|
|
1322
1375
|
from pve_sync_plugin.models import PveVmTaskLog
|
|
1323
1376
|
except ImportError:
|
|
1324
1377
|
return
|
|
1325
1378
|
|
|
1379
|
+
_utc = _dt.timezone.utc
|
|
1326
1380
|
VM_TASK_TYPES = {
|
|
1327
1381
|
"qmcreate", "qmdestroy", "qmclone", "qmmigrate", "qmrestore",
|
|
1328
1382
|
"qmstart", "qmstop", "qmshutdown", "qmreboot",
|
|
@@ -1356,10 +1410,18 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1356
1410
|
cluster_name=self.cluster_name
|
|
1357
1411
|
).order_by("-start_time").values_list("start_time", flat=True).first()
|
|
1358
1412
|
if latest:
|
|
1359
|
-
import calendar
|
|
1360
1413
|
since_ts = int(calendar.timegm(latest.utctimetuple())) + 1
|
|
1361
1414
|
else:
|
|
1362
1415
|
since_ts = int(time.time()) - 30 * 86400
|
|
1416
|
+
|
|
1417
|
+
# Pre-fetch existing UPIDs in the window → O(1) lookup instead of N+1 queries
|
|
1418
|
+
since_dt = _dt.datetime.fromtimestamp(since_ts, tz=_utc)
|
|
1419
|
+
existing_upids: set = set(
|
|
1420
|
+
PveVmTaskLog.objects
|
|
1421
|
+
.filter(cluster_name=self.cluster_name, start_time__gte=since_dt)
|
|
1422
|
+
.values_list("upid", flat=True)
|
|
1423
|
+
)
|
|
1424
|
+
new_upids: set = set() # UPIDs inserted this run (avoid duplicates within same run)
|
|
1363
1425
|
new_count = 0
|
|
1364
1426
|
|
|
1365
1427
|
for node_name in self.pve_cache.get("vms_by_node", {}):
|
|
@@ -1383,8 +1445,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1383
1445
|
except (ValueError, TypeError):
|
|
1384
1446
|
continue
|
|
1385
1447
|
|
|
1386
|
-
# UPID
|
|
1387
|
-
if
|
|
1448
|
+
# UPID 去重(使用預先撈取的集合,避免 N+1 查詢)
|
|
1449
|
+
if upid in existing_upids or upid in new_upids:
|
|
1388
1450
|
continue
|
|
1389
1451
|
|
|
1390
1452
|
task_type = task.get("type")
|
|
@@ -1394,8 +1456,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1394
1456
|
status = task.get("status", "") or ""
|
|
1395
1457
|
vm_name = vmid_to_name.get(vmid, f"VM-{vmid}")
|
|
1396
1458
|
|
|
1397
|
-
import datetime as _dt
|
|
1398
|
-
_utc = _dt.timezone.utc
|
|
1399
1459
|
start_dt = _dt.datetime.fromtimestamp(start_ts, tz=_utc) if start_ts else timezone.now()
|
|
1400
1460
|
end_dt = _dt.datetime.fromtimestamp(end_ts, tz=_utc) if end_ts else None
|
|
1401
1461
|
|
|
@@ -1412,6 +1472,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1412
1472
|
status=status,
|
|
1413
1473
|
notified_telegram=False,
|
|
1414
1474
|
)
|
|
1475
|
+
new_upids.add(upid)
|
|
1415
1476
|
new_count += 1
|
|
1416
1477
|
|
|
1417
1478
|
# Telegram 通知(僅近期且已完成的操作)
|
|
@@ -1439,11 +1500,12 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1439
1500
|
|
|
1440
1501
|
def detect_config_drift(self, vm_id: int, vm_name: str, vm_config: Dict[str, Any],
|
|
1441
1502
|
current_tags: List[str], network_interfaces: List[Dict] = None,
|
|
1442
|
-
current_node: str = None) -> List[Dict]:
|
|
1503
|
+
current_node: str = None, last_config=None) -> List[Dict]:
|
|
1443
1504
|
if not self.enhanced_mode or not self.state_db:
|
|
1444
1505
|
return []
|
|
1445
1506
|
drifts = []
|
|
1446
|
-
last_config
|
|
1507
|
+
if last_config is None:
|
|
1508
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1447
1509
|
if not last_config:
|
|
1448
1510
|
return drifts
|
|
1449
1511
|
current_memory = int(vm_config.get('memory', 0))
|
|
@@ -1524,8 +1586,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1524
1586
|
self._write_drift_event(vm_name, vm_id, 'disk_change', k, old_v, new_v, notified=True)
|
|
1525
1587
|
|
|
1526
1588
|
# VM 描述變更偵測
|
|
1527
|
-
current_desc = (vm_config.get('description')
|
|
1528
|
-
old_desc = (last_config.get('description')
|
|
1589
|
+
current_desc = self._norm_desc(vm_config.get('description'))[:500].strip()
|
|
1590
|
+
old_desc = self._norm_desc(last_config.get('description'))[:500].strip()
|
|
1529
1591
|
if old_desc and old_desc != current_desc:
|
|
1530
1592
|
self.stats['config_drifts_detected'] += 1
|
|
1531
1593
|
print(f"📝 VM {vm_name} 描述變更")
|
|
@@ -1565,7 +1627,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1565
1627
|
return drifts
|
|
1566
1628
|
|
|
1567
1629
|
def should_sync_vm(self, vm_id: int, vm_config: Dict[str, Any], tags: List[str],
|
|
1568
|
-
network_interfaces: List[Dict] = None) -> bool:
|
|
1630
|
+
network_interfaces: List[Dict] = None, last_config=None) -> bool:
|
|
1569
1631
|
if not self.sync_config.get('incremental', True):
|
|
1570
1632
|
return True
|
|
1571
1633
|
if self.sync_config.get('force_full_sync', False):
|
|
@@ -1573,34 +1635,35 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1573
1635
|
current_hash = compute_config_hash(vm_config, tags, network_interfaces)
|
|
1574
1636
|
if not self.state_db:
|
|
1575
1637
|
return True
|
|
1576
|
-
last_config
|
|
1638
|
+
if last_config is None:
|
|
1639
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1577
1640
|
if not last_config:
|
|
1578
1641
|
return True
|
|
1579
1642
|
if current_hash != last_config.get('config_hash'):
|
|
1580
1643
|
return True
|
|
1581
1644
|
return False
|
|
1582
1645
|
|
|
1583
|
-
def detect_tag_changes(self, vm_id: int, vm_name: str, current_tags: List[str]
|
|
1646
|
+
def detect_tag_changes(self, vm_id: int, vm_name: str, current_tags: List[str],
|
|
1647
|
+
last_config=None) -> List[Dict]:
|
|
1584
1648
|
if not self.enhanced_mode or not self.state_db:
|
|
1585
1649
|
return []
|
|
1586
|
-
|
|
1587
|
-
|
|
1650
|
+
if last_config is None:
|
|
1651
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1588
1652
|
if not last_config:
|
|
1589
|
-
return
|
|
1653
|
+
return []
|
|
1590
1654
|
old_tags = set(last_config.get('tags', []))
|
|
1591
1655
|
new_tags = set(current_tags)
|
|
1592
1656
|
added = new_tags - old_tags
|
|
1593
1657
|
removed = old_tags - new_tags
|
|
1658
|
+
changes = []
|
|
1594
1659
|
for tag in added:
|
|
1595
1660
|
changes.append({'type': 'added', 'tag': tag})
|
|
1596
1661
|
self.stats['tag_changes'] += 1
|
|
1597
1662
|
print(f"🏷️ VM {vm_name} 新增標籤: {tag}")
|
|
1598
|
-
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', '', tag, notified=False)
|
|
1599
1663
|
for tag in removed:
|
|
1600
1664
|
changes.append({'type': 'removed', 'tag': tag})
|
|
1601
1665
|
self.stats['tag_changes'] += 1
|
|
1602
1666
|
print(f"🏷️ VM {vm_name} 移除標籤: {tag}")
|
|
1603
|
-
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', tag, '', notified=False)
|
|
1604
1667
|
if changes:
|
|
1605
1668
|
added_list = [c['tag'] for c in changes if c['type'] == 'added']
|
|
1606
1669
|
removed_list = [c['tag'] for c in changes if c['type'] == 'removed']
|
|
@@ -1616,7 +1679,12 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1616
1679
|
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1617
1680
|
f"{detail}\n請確認是否為預期變更。"
|
|
1618
1681
|
)
|
|
1619
|
-
self.send_telegram_notification(tag_msg)
|
|
1682
|
+
notified = self.send_telegram_notification(tag_msg)
|
|
1683
|
+
for c in changes:
|
|
1684
|
+
if c['type'] == 'added':
|
|
1685
|
+
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', '', c['tag'], notified=notified)
|
|
1686
|
+
else:
|
|
1687
|
+
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', c['tag'], '', notified=notified)
|
|
1620
1688
|
return changes
|
|
1621
1689
|
|
|
1622
1690
|
def detect_ip_change(self, vm_id: int, vm_name: str, current_ip: str):
|
|
@@ -1643,6 +1711,36 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1643
1711
|
self.send_telegram_notification(ip_msg)
|
|
1644
1712
|
self._write_drift_event(vm_name, vm_id, 'ip_change', 'primary_ip', old_ip, current_ip, notified=True)
|
|
1645
1713
|
|
|
1714
|
+
def check_agent_ip_missing(self, vm_id: str, vm_name: str, vm_type: str, is_template: bool,
|
|
1715
|
+
vm_status: str, qemu_agent_enabled: bool, ip_str: str):
|
|
1716
|
+
"""VM 已在執行中,卻完全取不到 IP —— 多半是 QEMU Guest Agent 未啟用/未安裝。
|
|
1717
|
+
|
|
1718
|
+
每次同步皆檢查(非一次性事件),只要問題未解決就會持續提醒。
|
|
1719
|
+
"""
|
|
1720
|
+
if vm_type != 'qemu' or is_template or vm_status != 'running' or ip_str:
|
|
1721
|
+
return
|
|
1722
|
+
print(f"🌐 VM {vm_name} 運行中但無法取得 IP")
|
|
1723
|
+
self.stats['no_ip_alerts'] = self.stats.get('no_ip_alerts', 0) + 1
|
|
1724
|
+
if qemu_agent_enabled:
|
|
1725
|
+
detail = (
|
|
1726
|
+
"⚠️ QEMU Guest Agent 已在設定中啟用,但仍無法取得 IP,"
|
|
1727
|
+
"請確認 VM 內部的 qemu-guest-agent 服務是否已安裝並執行中。"
|
|
1728
|
+
)
|
|
1729
|
+
else:
|
|
1730
|
+
detail = (
|
|
1731
|
+
"⚠️ 此 VM 尚未啟用 QEMU Guest Agent(Options → QEMU Guest Agent),"
|
|
1732
|
+
"請啟用後於 VM 內安裝對應套件(qemu-guest-agent)。"
|
|
1733
|
+
)
|
|
1734
|
+
msg = (
|
|
1735
|
+
f"🌐 <b>VM 運行中但無法取得 IP</b>\n\n"
|
|
1736
|
+
f"🖥️ 名稱: <b>{vm_name}</b> (ID: {vm_id})\n"
|
|
1737
|
+
f"🔀 叢集: {self.cluster_name}\n"
|
|
1738
|
+
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1739
|
+
f"{detail}"
|
|
1740
|
+
)
|
|
1741
|
+
self.send_telegram_notification(msg)
|
|
1742
|
+
self._write_drift_event(vm_name, int(vm_id), 'agent_ip_missing', 'primary_ip', '', '', notified=True)
|
|
1743
|
+
|
|
1646
1744
|
# ---------- 虛擬機處理主邏輯 ----------
|
|
1647
1745
|
def process_virtual_machine(self, vm_data: Dict, device, cluster: Dict, force: bool = False) -> bool:
|
|
1648
1746
|
vm_id = str(vm_data['vmid'])
|
|
@@ -1660,7 +1758,9 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1660
1758
|
else:
|
|
1661
1759
|
vm_config = self.pve_api.nodes(node_name).lxc(vm_data['vmid']).config.get()
|
|
1662
1760
|
except Exception as e:
|
|
1663
|
-
|
|
1761
|
+
error_msg = str(e)
|
|
1762
|
+
print(f" 獲取 VM 配置失敗: {error_msg}")
|
|
1763
|
+
self.log_vm_sync_error(original_vm_name, int(vm_id), f"無法取得 PVE 配置: {error_msg}")
|
|
1664
1764
|
return False
|
|
1665
1765
|
is_template = vm_data.get('is_template', False) or vm_config.get('template', 0) == 1
|
|
1666
1766
|
boot_choice = 'on' if vm_config.get('onboot', 0) == 1 else 'off'
|
|
@@ -1684,10 +1784,16 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1684
1784
|
tag_ids.append(tag.id)
|
|
1685
1785
|
except Exception as e:
|
|
1686
1786
|
print(f" 建立標籤失敗 {tag_name}: {e}")
|
|
1787
|
+
# Read last_config once; share with detect_new_vm / detect_tag_changes /
|
|
1788
|
+
# detect_config_drift / should_sync_vm to avoid repeated SQLite round-trips.
|
|
1789
|
+
last_config = (
|
|
1790
|
+
self.state_db.get_last_vm_config(int(vm_id), self.cluster_name)
|
|
1791
|
+
if (self.enhanced_mode and self.state_db) else None
|
|
1792
|
+
)
|
|
1687
1793
|
if self.enhanced_mode:
|
|
1688
|
-
self.detect_new_vm(int(vm_id), original_vm_name, node_name)
|
|
1794
|
+
self.detect_new_vm(int(vm_id), original_vm_name, node_name, last_config=last_config)
|
|
1689
1795
|
if self.enhanced_mode and tag_names:
|
|
1690
|
-
self.detect_tag_changes(int(vm_id), original_vm_name, tag_names)
|
|
1796
|
+
self.detect_tag_changes(int(vm_id), original_vm_name, tag_names, last_config=last_config)
|
|
1691
1797
|
vm_status = vm_data.get('status', '')
|
|
1692
1798
|
if tag_names and vm_status in ('running', 'stopped'):
|
|
1693
1799
|
first_tag = tag_names[0].lower()
|
|
@@ -1716,6 +1822,26 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1716
1822
|
⚠️ 虛擬機應處於關機狀態,請確認開機原因。
|
|
1717
1823
|
"""
|
|
1718
1824
|
self.send_telegram_notification(message)
|
|
1825
|
+
|
|
1826
|
+
# 節點放置檢查:VM 描述 (Notes) 內若標記 "node: <預期節點>",
|
|
1827
|
+
# 而目前實際所在節點與其不符,則提醒(每次同步皆檢查,非一次性事件)。
|
|
1828
|
+
expected_node = self._parse_expected_node(vm_config.get('description', ''))
|
|
1829
|
+
if expected_node and expected_node != node_name:
|
|
1830
|
+
print(f"📍 VM {original_vm_name} 節點不符期望: 目前={node_name}, 期望={expected_node}")
|
|
1831
|
+
self.stats['node_mismatch_alerts'] += 1
|
|
1832
|
+
node_mismatch_msg = (
|
|
1833
|
+
f"📍 <b>VM 節點與 Notes 標記不符</b>\n\n"
|
|
1834
|
+
f"🖥️ 名稱: <b>{original_vm_name}</b> (ID: {vm_id})\n"
|
|
1835
|
+
f"🔀 叢集: {self.cluster_name}\n"
|
|
1836
|
+
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1837
|
+
f"📌 目前節點: <code>{node_name}</code>\n"
|
|
1838
|
+
f"📋 Notes 標記節點: <code>{expected_node}</code>\n\n"
|
|
1839
|
+
f"⚠️ 請確認此 VM 是否應遷回指定節點。"
|
|
1840
|
+
)
|
|
1841
|
+
self.send_telegram_notification(node_mismatch_msg)
|
|
1842
|
+
self._write_drift_event(original_vm_name, int(vm_id), 'node_mismatch', 'node',
|
|
1843
|
+
node_name, expected_node, notified=True)
|
|
1844
|
+
|
|
1719
1845
|
role_id = None
|
|
1720
1846
|
vm_pool = self.get_vm_pool(vm_data['vmid'], vm_type)
|
|
1721
1847
|
if vm_pool:
|
|
@@ -1770,8 +1896,9 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1770
1896
|
})
|
|
1771
1897
|
if self.enhanced_mode:
|
|
1772
1898
|
self.detect_config_drift(int(vm_id), original_vm_name, vm_config, tag_names,
|
|
1773
|
-
network_interfaces, current_node=node_name)
|
|
1774
|
-
if not force and not self.should_sync_vm(int(vm_id), vm_config, tag_names, network_interfaces
|
|
1899
|
+
network_interfaces, current_node=node_name, last_config=last_config)
|
|
1900
|
+
if not force and not self.should_sync_vm(int(vm_id), vm_config, tag_names, network_interfaces,
|
|
1901
|
+
last_config=last_config):
|
|
1775
1902
|
cached_vm = self.nb_cache['virtual_machines_by_serial'].get(f"{vm_id}::{cluster['id']}")
|
|
1776
1903
|
if cached_vm is None:
|
|
1777
1904
|
# VM 在 NetBox 中不存在(例如隨節點被連帶刪除),
|
|
@@ -1817,18 +1944,20 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1817
1944
|
except Exception as e:
|
|
1818
1945
|
print(f" 設定 VM 主 IP 失敗: {e}")
|
|
1819
1946
|
print(f" ℹ️ VM {original_vm_name} 無配置變更,跳過同步")
|
|
1947
|
+
cur_ip = getattr(skip_primary_ip, 'address', None) \
|
|
1948
|
+
or getattr(getattr(cached_vm, 'primary_ip4', None), 'address', None) or ''
|
|
1949
|
+
self.check_agent_ip_missing(vm_id, original_vm_name, vm_type, is_template,
|
|
1950
|
+
vm_status, qemu_agent_enabled, cur_ip)
|
|
1820
1951
|
if self.state_db:
|
|
1821
1952
|
try:
|
|
1822
1953
|
config_hash = compute_config_hash(vm_config, tag_names, network_interfaces)
|
|
1823
1954
|
memory = int(vm_config.get('memory', 0))
|
|
1824
1955
|
vcpus_save = int(vm_config.get('vcpus', vcpus))
|
|
1825
|
-
cur_ip = getattr(skip_primary_ip, 'address', None) \
|
|
1826
|
-
or getattr(getattr(cached_vm, 'primary_ip4', None), 'address', None) or ''
|
|
1827
1956
|
self.state_db.save_vm_config_snapshot(
|
|
1828
1957
|
vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
|
|
1829
1958
|
memory=memory, vcpus=vcpus_save, tags=tag_names,
|
|
1830
1959
|
node=node_name, primary_ip=cur_ip, vm_name=original_vm_name,
|
|
1831
|
-
description=(vm_config.get('description')
|
|
1960
|
+
description=self._norm_desc(vm_config.get('description'))[:500].strip(),
|
|
1832
1961
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1833
1962
|
)
|
|
1834
1963
|
except Exception as _snap_err:
|
|
@@ -1885,7 +2014,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1885
2014
|
update_data = {
|
|
1886
2015
|
'name': vm_name, 'cluster': cluster['id'], 'device': device.id, 'role': role_id,
|
|
1887
2016
|
'vcpus': vcpus, 'memory': int(vm_config.get('memory', 0)), 'status': status,
|
|
1888
|
-
'description': (vm_config.get('description')
|
|
2017
|
+
'description': self._norm_desc(vm_config.get('description'))[:200], 'platform': platform_id,
|
|
1889
2018
|
'start_on_boot': boot_choice
|
|
1890
2019
|
}
|
|
1891
2020
|
if tag_ids:
|
|
@@ -1904,7 +2033,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1904
2033
|
vm_data_dict = {
|
|
1905
2034
|
'serial': vm_id, 'name': vm_name, 'cluster': cluster['id'], 'device': device.id,
|
|
1906
2035
|
'role': role_id, 'vcpus': vcpus, 'memory': int(vm_config.get('memory', 0)),
|
|
1907
|
-
'status': status, 'description': (vm_config.get('description')
|
|
2036
|
+
'status': status, 'description': self._norm_desc(vm_config.get('description'))[:200],
|
|
1908
2037
|
'platform': platform_id, 'start_on_boot': boot_choice
|
|
1909
2038
|
}
|
|
1910
2039
|
if tag_ids:
|
|
@@ -1932,6 +2061,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1932
2061
|
print(f" 設定 VM 主 IP 失敗: {e}")
|
|
1933
2062
|
if self.enhanced_mode and primary_ip_str:
|
|
1934
2063
|
self.detect_ip_change(int(vm_id), original_vm_name, primary_ip_str)
|
|
2064
|
+
self.check_agent_ip_missing(vm_id, original_vm_name, vm_type, is_template,
|
|
2065
|
+
vm_status, qemu_agent_enabled, primary_ip_str)
|
|
1935
2066
|
print(f" 標籤: {len(tag_ids)}個, 介面: {interface_count}個, 磁碟: {disk_count}個, 大小: {disk_size}MB")
|
|
1936
2067
|
if self.enhanced_mode and self.state_db:
|
|
1937
2068
|
config_hash = compute_config_hash(vm_config, tag_names, network_interfaces)
|
|
@@ -1941,7 +2072,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1941
2072
|
vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
|
|
1942
2073
|
memory=memory, vcpus=vcpus_save, tags=tag_names,
|
|
1943
2074
|
node=node_name, primary_ip=primary_ip_str, vm_name=original_vm_name,
|
|
1944
|
-
description=(vm_config.get('description')
|
|
2075
|
+
description=self._norm_desc(vm_config.get('description'))[:500].strip(),
|
|
1945
2076
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1946
2077
|
)
|
|
1947
2078
|
return True
|
|
@@ -1967,7 +2098,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1967
2098
|
node=node_name,
|
|
1968
2099
|
primary_ip=_ip,
|
|
1969
2100
|
vm_name=original_vm_name,
|
|
1970
|
-
description=(vm_config.get('description')
|
|
2101
|
+
description=self._norm_desc(vm_config.get('description'))[:500].strip(),
|
|
1971
2102
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1972
2103
|
)
|
|
1973
2104
|
except Exception:
|
|
@@ -2010,6 +2141,18 @@ class OptimizedPVEToNetBoxSync:
|
|
|
2010
2141
|
print("=" * 50)
|
|
2011
2142
|
if self.enhanced_mode and self.state_db:
|
|
2012
2143
|
self.sync_log_id = self.state_db.start_sync_log(self.cluster_name)
|
|
2144
|
+
# Load notify_on_sync from PveClusterConfig (DB setting takes precedence over default True)
|
|
2145
|
+
self.notify_on_sync = True
|
|
2146
|
+
try:
|
|
2147
|
+
from pve_sync_plugin.models import PveClusterConfig
|
|
2148
|
+
cfg = PveClusterConfig.objects.filter(name=self.cluster_name).first()
|
|
2149
|
+
if cfg is not None:
|
|
2150
|
+
self.notify_on_sync = cfg.notify_on_sync
|
|
2151
|
+
except Exception:
|
|
2152
|
+
pass
|
|
2153
|
+
# Reset per-sync Telegram counters
|
|
2154
|
+
self._telegram_sent = 0
|
|
2155
|
+
self._telegram_suppressed = 0
|
|
2013
2156
|
sync_mode = "增量" if self.sync_config.get('incremental', True) else "全量"
|
|
2014
2157
|
start_message = f"""
|
|
2015
2158
|
🔄 <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].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
|
{proxsync-2.1.0 → proxsync-2.1.2}/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.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py
RENAMED
|
File without changes
|
{proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.1.0 → proxsync-2.1.2}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.1.0 → proxsync-2.1.2}/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.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html
RENAMED
|
File without changes
|
{proxsync-2.1.0 → proxsync-2.1.2}/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.1.0 → proxsync-2.1.2}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html
RENAMED
|
File without changes
|
{proxsync-2.1.0 → proxsync-2.1.2}/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
|