proxsync 2.0.0__tar.gz → 2.0.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.0.0/proxsync.egg-info → proxsync-2.0.2}/PKG-INFO +12 -14
- {proxsync-2.0.0 → proxsync-2.0.2}/README.md +11 -13
- {proxsync-2.0.0 → proxsync-2.0.2/proxsync.egg-info}/PKG-INFO +12 -14
- {proxsync-2.0.0 → proxsync-2.0.2}/proxsync.egg-info/SOURCES.txt +3 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync.py +110 -34
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/api/serializers.py +18 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/api/urls.py +2 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/api/views.py +7 -0
- proxsync-2.0.2/pve_sync_plugin/migrations/0014_vmprovisioninglog.py +41 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/models.py +55 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/navigation.py +9 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/tables.py +23 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/tasks.py +6 -4
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/templates/pve_sync/pvesyncjob.html +77 -48
- proxsync-2.0.2/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html +334 -0
- proxsync-2.0.2/pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html +114 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/templatetags/pve_sync_tags.py +14 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/urls.py +12 -1
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/views.py +262 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pyproject.toml +1 -1
- {proxsync-2.0.0 → proxsync-2.0.2}/MANIFEST.in +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/config.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pbs_sync.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/proxsync.egg-info/dependency_links.txt +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/proxsync.egg-info/requires.txt +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/proxsync.egg-info/top_level.txt +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/__init__.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/api/__init__.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/apps.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/choices.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/filtersets.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/forms.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/management/__init__.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/management/commands/__init__.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/management/commands/pve_sync.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/management/commands/run_scheduled_syncs.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0001_initial.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0002_pvepluginsettings.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0003_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0004_model_verbose_names.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0007_rename_verbose_names_to_proxmox_sync.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0009_add_every_3h_schedule.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0010_pvedriftevent.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/migrations/__init__.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/signals.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/sync/__init__.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/sync/config_bridge.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/sync/engine.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/template_content.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/templates/pve_sync/dashboard.html +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/templates/pve_sync/pbsserverconfig.html +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/templates/pve_sync/pvebackupstatus.html +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/templates/pve_sync/pveclusterconfig.html +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/templates/pve_sync/pvedriftevent.html +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/templates/pve_sync/pvewebhookevent.html +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/templates/pve_sync/settings.html +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/templatetags/__init__.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/utils.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/pve_sync_plugin/view_helpers.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/setup.cfg +0 -0
- {proxsync-2.0.0 → proxsync-2.0.2}/state_db.py +0 -0
- {proxsync-2.0.0 → proxsync-2.0.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.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: NetBox plugin for Proxmox VE inventory synchronization
|
|
5
5
|
Author-email: lixiujie85 <lixiujie85@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -114,7 +114,7 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
114
114
|
### 1. 安裝插件
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
|
-
/opt/netbox/venv/bin/pip install
|
|
117
|
+
/opt/netbox/venv/bin/pip install proxsync
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
### 2. 啟用插件
|
|
@@ -125,14 +125,6 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
125
125
|
PLUGINS = [
|
|
126
126
|
"pve_sync_plugin",
|
|
127
127
|
]
|
|
128
|
-
|
|
129
|
-
PLUGINS_CONFIG = {
|
|
130
|
-
"pve_sync_plugin": {
|
|
131
|
-
# 也可在 Web UI Settings 頁面設定,此處為預設值
|
|
132
|
-
"netbox_url": "http://localhost:8001",
|
|
133
|
-
"netbox_token": "your-netbox-api-token",
|
|
134
|
-
},
|
|
135
|
-
}
|
|
136
128
|
```
|
|
137
129
|
|
|
138
130
|
### 3. 建立資料表
|
|
@@ -149,12 +141,12 @@ cd /opt/netbox/netbox
|
|
|
149
141
|
sudo systemctl restart netbox netbox-rq
|
|
150
142
|
```
|
|
151
143
|
|
|
152
|
-
### 5.
|
|
144
|
+
### 5. 設定排程自動同步
|
|
153
145
|
|
|
154
|
-
在
|
|
146
|
+
在 crontab 加入以下排程,每 15 分鐘檢查一次是否有到期的同步任務:
|
|
155
147
|
|
|
156
148
|
```bash
|
|
157
|
-
|
|
149
|
+
crontab -e
|
|
158
150
|
```
|
|
159
151
|
|
|
160
152
|
加入:
|
|
@@ -163,7 +155,13 @@ sudo crontab -e
|
|
|
163
155
|
*/15 * * * * cd /opt/netbox/netbox && /opt/netbox/venv/bin/python manage.py run_scheduled_syncs >> /var/log/netbox-pve-scheduled-sync.log 2>&1
|
|
164
156
|
```
|
|
165
157
|
|
|
166
|
-
|
|
158
|
+
### 6. 在 Web UI 完成設定
|
|
159
|
+
|
|
160
|
+
前往 `Plugins → Proxmox Sync → Settings`,填入 NetBox URL、API Token,以及 Telegram 通知設定(可選)。
|
|
161
|
+
|
|
162
|
+
接著前往 `Plugins → Proxmox Sync → PVE Clusters → Add` 新增第一個 PVE 叢集連線。
|
|
163
|
+
|
|
164
|
+
> 設定好 cron 後,只需在各叢集設定中選擇同步週期,系統即會自動執行。
|
|
167
165
|
|
|
168
166
|
---
|
|
169
167
|
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
### 1. 安裝插件
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
/opt/netbox/venv/bin/pip install
|
|
81
|
+
/opt/netbox/venv/bin/pip install proxsync
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
### 2. 啟用插件
|
|
@@ -89,14 +89,6 @@
|
|
|
89
89
|
PLUGINS = [
|
|
90
90
|
"pve_sync_plugin",
|
|
91
91
|
]
|
|
92
|
-
|
|
93
|
-
PLUGINS_CONFIG = {
|
|
94
|
-
"pve_sync_plugin": {
|
|
95
|
-
# 也可在 Web UI Settings 頁面設定,此處為預設值
|
|
96
|
-
"netbox_url": "http://localhost:8001",
|
|
97
|
-
"netbox_token": "your-netbox-api-token",
|
|
98
|
-
},
|
|
99
|
-
}
|
|
100
92
|
```
|
|
101
93
|
|
|
102
94
|
### 3. 建立資料表
|
|
@@ -113,12 +105,12 @@ cd /opt/netbox/netbox
|
|
|
113
105
|
sudo systemctl restart netbox netbox-rq
|
|
114
106
|
```
|
|
115
107
|
|
|
116
|
-
### 5.
|
|
108
|
+
### 5. 設定排程自動同步
|
|
117
109
|
|
|
118
|
-
在
|
|
110
|
+
在 crontab 加入以下排程,每 15 分鐘檢查一次是否有到期的同步任務:
|
|
119
111
|
|
|
120
112
|
```bash
|
|
121
|
-
|
|
113
|
+
crontab -e
|
|
122
114
|
```
|
|
123
115
|
|
|
124
116
|
加入:
|
|
@@ -127,7 +119,13 @@ sudo crontab -e
|
|
|
127
119
|
*/15 * * * * cd /opt/netbox/netbox && /opt/netbox/venv/bin/python manage.py run_scheduled_syncs >> /var/log/netbox-pve-scheduled-sync.log 2>&1
|
|
128
120
|
```
|
|
129
121
|
|
|
130
|
-
|
|
122
|
+
### 6. 在 Web UI 完成設定
|
|
123
|
+
|
|
124
|
+
前往 `Plugins → Proxmox Sync → Settings`,填入 NetBox URL、API Token,以及 Telegram 通知設定(可選)。
|
|
125
|
+
|
|
126
|
+
接著前往 `Plugins → Proxmox Sync → PVE Clusters → Add` 新增第一個 PVE 叢集連線。
|
|
127
|
+
|
|
128
|
+
> 設定好 cron 後,只需在各叢集設定中選擇同步週期,系統即會自動執行。
|
|
131
129
|
|
|
132
130
|
---
|
|
133
131
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxsync
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: NetBox plugin for Proxmox VE inventory synchronization
|
|
5
5
|
Author-email: lixiujie85 <lixiujie85@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -114,7 +114,7 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
114
114
|
### 1. 安裝插件
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
|
-
/opt/netbox/venv/bin/pip install
|
|
117
|
+
/opt/netbox/venv/bin/pip install proxsync
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
### 2. 啟用插件
|
|
@@ -125,14 +125,6 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
125
125
|
PLUGINS = [
|
|
126
126
|
"pve_sync_plugin",
|
|
127
127
|
]
|
|
128
|
-
|
|
129
|
-
PLUGINS_CONFIG = {
|
|
130
|
-
"pve_sync_plugin": {
|
|
131
|
-
# 也可在 Web UI Settings 頁面設定,此處為預設值
|
|
132
|
-
"netbox_url": "http://localhost:8001",
|
|
133
|
-
"netbox_token": "your-netbox-api-token",
|
|
134
|
-
},
|
|
135
|
-
}
|
|
136
128
|
```
|
|
137
129
|
|
|
138
130
|
### 3. 建立資料表
|
|
@@ -149,12 +141,12 @@ cd /opt/netbox/netbox
|
|
|
149
141
|
sudo systemctl restart netbox netbox-rq
|
|
150
142
|
```
|
|
151
143
|
|
|
152
|
-
### 5.
|
|
144
|
+
### 5. 設定排程自動同步
|
|
153
145
|
|
|
154
|
-
在
|
|
146
|
+
在 crontab 加入以下排程,每 15 分鐘檢查一次是否有到期的同步任務:
|
|
155
147
|
|
|
156
148
|
```bash
|
|
157
|
-
|
|
149
|
+
crontab -e
|
|
158
150
|
```
|
|
159
151
|
|
|
160
152
|
加入:
|
|
@@ -163,7 +155,13 @@ sudo crontab -e
|
|
|
163
155
|
*/15 * * * * cd /opt/netbox/netbox && /opt/netbox/venv/bin/python manage.py run_scheduled_syncs >> /var/log/netbox-pve-scheduled-sync.log 2>&1
|
|
164
156
|
```
|
|
165
157
|
|
|
166
|
-
|
|
158
|
+
### 6. 在 Web UI 完成設定
|
|
159
|
+
|
|
160
|
+
前往 `Plugins → Proxmox Sync → Settings`,填入 NetBox URL、API Token,以及 Telegram 通知設定(可選)。
|
|
161
|
+
|
|
162
|
+
接著前往 `Plugins → Proxmox Sync → PVE Clusters → Add` 新增第一個 PVE 叢集連線。
|
|
163
|
+
|
|
164
|
+
> 設定好 cron 後,只需在各叢集設定中選擇同步週期,系統即會自動執行。
|
|
167
165
|
|
|
168
166
|
---
|
|
169
167
|
|
|
@@ -46,6 +46,7 @@ pve_sync_plugin/migrations/0010_pvedriftevent.py
|
|
|
46
46
|
pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py
|
|
47
47
|
pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py
|
|
48
48
|
pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py
|
|
49
|
+
pve_sync_plugin/migrations/0014_vmprovisioninglog.py
|
|
49
50
|
pve_sync_plugin/migrations/__init__.py
|
|
50
51
|
pve_sync_plugin/sync/__init__.py
|
|
51
52
|
pve_sync_plugin/sync/config_bridge.py
|
|
@@ -58,6 +59,8 @@ pve_sync_plugin/templates/pve_sync/pvedriftevent.html
|
|
|
58
59
|
pve_sync_plugin/templates/pve_sync/pvesyncjob.html
|
|
59
60
|
pve_sync_plugin/templates/pve_sync/pvewebhookevent.html
|
|
60
61
|
pve_sync_plugin/templates/pve_sync/settings.html
|
|
62
|
+
pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html
|
|
63
|
+
pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html
|
|
61
64
|
pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html
|
|
62
65
|
pve_sync_plugin/templatetags/__init__.py
|
|
63
66
|
pve_sync_plugin/templatetags/pve_sync_tags.py
|
|
@@ -119,6 +119,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
119
119
|
'vms_by_node': {},
|
|
120
120
|
'pools': {},
|
|
121
121
|
'node_networks': {},
|
|
122
|
+
'failed_nodes': set(), # nodes where VM API call failed — skip cleanup for these
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
self.error_log = []
|
|
@@ -361,6 +362,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
361
362
|
except Exception as e:
|
|
362
363
|
print(f" 加載節點 {node_name} 的虛擬機失敗: {e}")
|
|
363
364
|
self.pve_cache['vms_by_node'][node_name] = []
|
|
365
|
+
self.pve_cache['failed_nodes'].add(node_name.lower())
|
|
364
366
|
elapsed = time.time() - start_time
|
|
365
367
|
print(f"✓ PVE 數據加載完成,耗時 {elapsed:.2f} 秒")
|
|
366
368
|
print(f" 節點: {len(self.pve_cache['nodes'])} 個")
|
|
@@ -1124,40 +1126,114 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1124
1126
|
self.send_telegram_notification(new_msg)
|
|
1125
1127
|
self._write_drift_event(vm_name, vm_id, 'vm_created', 'vmid', '', str(vm_id), notified=True)
|
|
1126
1128
|
|
|
1127
|
-
def detect_deleted_vms(self, seen_vmids: set):
|
|
1128
|
-
"""
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1129
|
+
def detect_deleted_vms(self, seen_vmids: set, cluster_id: int = None):
|
|
1130
|
+
"""偵測並清理 NetBox 中已從 PVE 刪除的 VM(含 IP)。"""
|
|
1131
|
+
|
|
1132
|
+
# ── 1. 舊有:drift 通知(依賴 state_db)──
|
|
1133
|
+
if self.enhanced_mode and self.state_db and self._known_vmids:
|
|
1134
|
+
deleted_vmids = set(self._known_vmids.keys()) - seen_vmids
|
|
1135
|
+
for vmid in deleted_vmids:
|
|
1136
|
+
info = self._known_vmids[vmid]
|
|
1137
|
+
vm_name = info.get('vm_name') or f"VM-{vmid}"
|
|
1138
|
+
node_name = info.get('node') or 'unknown'
|
|
1139
|
+
try:
|
|
1140
|
+
from pve_sync_plugin.models import PveDriftEvent
|
|
1141
|
+
if PveDriftEvent.objects.filter(
|
|
1142
|
+
vmid=vmid, cluster_name=self.cluster_name, drift_type='vm_deleted'
|
|
1143
|
+
).exists():
|
|
1144
|
+
continue
|
|
1145
|
+
except Exception:
|
|
1146
|
+
pass
|
|
1147
|
+
print(f"🗑️ VM {vm_name} 刪除偵測 (ID: {vmid}, 上次節點: {node_name})")
|
|
1148
|
+
self.stats['config_drifts_detected'] += 1
|
|
1149
|
+
del_msg = (
|
|
1150
|
+
f"🗑️ <b>VM 刪除偵測</b>\n\n"
|
|
1151
|
+
f"🖥️ 名稱: <b>{vm_name}</b> (ID: {vmid})\n"
|
|
1152
|
+
f"🔀 叢集: {self.cluster_name}\n"
|
|
1153
|
+
f"📌 上次節點: <code>{node_name}</code>\n"
|
|
1154
|
+
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1155
|
+
f"⚠️ 此 VM 已從 Proxmox 中移除。"
|
|
1156
|
+
)
|
|
1157
|
+
self.send_telegram_notification(del_msg)
|
|
1158
|
+
self._write_drift_event(vm_name, vmid, 'vm_deleted', 'vmid', str(vmid), '', notified=True)
|
|
1159
|
+
|
|
1160
|
+
# ── 2. 主動清理:刪除 NetBox 中已不存在於 PVE 的 VM 及其 IP ──
|
|
1161
|
+
if not cluster_id:
|
|
1135
1162
|
return
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
f"🔀 叢集: {self.cluster_name}\n"
|
|
1155
|
-
f"📌 上次節點: <code>{node_name}</code>\n"
|
|
1156
|
-
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1157
|
-
f"⚠️ 此 VM 已從 Proxmox 中移除。"
|
|
1163
|
+
|
|
1164
|
+
failed_nodes = self.pve_cache.get('failed_nodes', set())
|
|
1165
|
+
if failed_nodes:
|
|
1166
|
+
print(f"⚠️ 節點 API 失敗,跳過其 VM 清理: {failed_nodes}")
|
|
1167
|
+
|
|
1168
|
+
try:
|
|
1169
|
+
from django.contrib.contenttypes.models import ContentType
|
|
1170
|
+
from virtualization.models import VirtualMachine, VMInterface
|
|
1171
|
+
from ipam.models import IPAddress
|
|
1172
|
+
|
|
1173
|
+
seen_serials = {str(v) for v in seen_vmids}
|
|
1174
|
+
stale_vms = list(
|
|
1175
|
+
VirtualMachine.objects
|
|
1176
|
+
.filter(cluster_id=cluster_id)
|
|
1177
|
+
.exclude(serial='')
|
|
1178
|
+
.exclude(serial__isnull=True)
|
|
1179
|
+
.exclude(serial__in=seen_serials)
|
|
1180
|
+
.select_related('device')
|
|
1158
1181
|
)
|
|
1159
|
-
|
|
1160
|
-
|
|
1182
|
+
|
|
1183
|
+
if not stale_vms:
|
|
1184
|
+
return
|
|
1185
|
+
|
|
1186
|
+
from pve_sync_plugin.models import PveDriftEvent
|
|
1187
|
+
|
|
1188
|
+
vm_ct = ContentType.objects.get_for_model(VMInterface)
|
|
1189
|
+
for vm in stale_vms:
|
|
1190
|
+
device_name = vm.device.name.lower() if vm.device else None
|
|
1191
|
+
if device_name and device_name in failed_nodes:
|
|
1192
|
+
print(f" ⏭️ 跳過 {vm.name} (VMID {vm.serial}):節點 {vm.device.name} API 失敗")
|
|
1193
|
+
continue
|
|
1194
|
+
|
|
1195
|
+
vm_name_str = vm.name
|
|
1196
|
+
vmid_int = int(vm.serial)
|
|
1197
|
+
|
|
1198
|
+
iface_ids = list(
|
|
1199
|
+
VMInterface.objects.filter(virtual_machine=vm).values_list('id', flat=True)
|
|
1200
|
+
)
|
|
1201
|
+
ip_deleted = 0
|
|
1202
|
+
if iface_ids:
|
|
1203
|
+
ip_deleted, _ = IPAddress.objects.filter(
|
|
1204
|
+
assigned_object_type=vm_ct,
|
|
1205
|
+
assigned_object_id__in=iface_ids,
|
|
1206
|
+
).delete()
|
|
1207
|
+
|
|
1208
|
+
vm.delete()
|
|
1209
|
+
print(f" 🗑️ 已刪除 VM {vm_name_str} (VMID {vmid_int}) 及 {ip_deleted} 個 IP")
|
|
1210
|
+
self.stats['config_drifts_detected'] += 1
|
|
1211
|
+
|
|
1212
|
+
# 寫入漂移事件(避免與 section 1 重複)
|
|
1213
|
+
already_logged = PveDriftEvent.objects.filter(
|
|
1214
|
+
vmid=vmid_int,
|
|
1215
|
+
cluster_name=self.cluster_name,
|
|
1216
|
+
drift_type='vm_deleted',
|
|
1217
|
+
).exists()
|
|
1218
|
+
if not already_logged:
|
|
1219
|
+
self._write_drift_event(
|
|
1220
|
+
vm_name_str, vmid_int, 'vm_deleted', 'vmid',
|
|
1221
|
+
str(vmid_int), '', notified=False,
|
|
1222
|
+
)
|
|
1223
|
+
del_msg = (
|
|
1224
|
+
f"🗑️ <b>VM 已從 NetBox 清除</b>\n\n"
|
|
1225
|
+
f"🖥️ 名稱: <b>{vm_name_str}</b> (VMID: {vmid_int})\n"
|
|
1226
|
+
f"🔀 叢集: {self.cluster_name}\n"
|
|
1227
|
+
f"🧹 已清除 IP 記錄: {ip_deleted} 個\n"
|
|
1228
|
+
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1229
|
+
f"⚠️ 此 VM 已從 Proxmox 刪除,NetBox 記錄已自動移除。"
|
|
1230
|
+
)
|
|
1231
|
+
self.send_telegram_notification(del_msg)
|
|
1232
|
+
|
|
1233
|
+
except Exception as exc:
|
|
1234
|
+
import traceback
|
|
1235
|
+
print(f"⚠️ 清理孤兒 VM 失敗: {exc}")
|
|
1236
|
+
traceback.print_exc()
|
|
1161
1237
|
|
|
1162
1238
|
def detect_config_drift(self, vm_id: int, vm_name: str, vm_config: Dict[str, Any],
|
|
1163
1239
|
current_tags: List[str], network_interfaces: List[Dict] = None,
|
|
@@ -1698,8 +1774,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1698
1774
|
seen_vmids.add(int(vm['vmid']))
|
|
1699
1775
|
if self.process_virtual_machine(vm, device, cluster, force=node_is_new):
|
|
1700
1776
|
success_count += 1
|
|
1701
|
-
#
|
|
1702
|
-
self.detect_deleted_vms(seen_vmids)
|
|
1777
|
+
# 偵測並清理本次同步消失的 VM(已從 PVE 刪除)
|
|
1778
|
+
self.detect_deleted_vms(seen_vmids, cluster_id=cluster['id'])
|
|
1703
1779
|
print(f"\n虛擬機同步完成: {success_count}/{total_count} 個虛擬機")
|
|
1704
1780
|
return success_count > 0, success_count, total_count
|
|
1705
1781
|
|
|
@@ -10,6 +10,7 @@ from pve_sync_plugin.models import (
|
|
|
10
10
|
PvePluginSettings,
|
|
11
11
|
PveSyncJob,
|
|
12
12
|
PveWebhookEvent,
|
|
13
|
+
VmProvisioningLog,
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
|
|
@@ -159,3 +160,20 @@ class PvePluginSettingsSerializer(NetBoxModelSerializer):
|
|
|
159
160
|
"enable_backup_sync",
|
|
160
161
|
]
|
|
161
162
|
# Never expose secrets via API
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class VmProvisioningLogSerializer(NetBoxModelSerializer):
|
|
166
|
+
url = serializers.HyperlinkedIdentityField(
|
|
167
|
+
view_name="plugins-api:pve_sync_plugin-api:vmprovisioninglog-detail",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
class Meta:
|
|
171
|
+
model = VmProvisioningLog
|
|
172
|
+
fields = [
|
|
173
|
+
"id", "url", "display",
|
|
174
|
+
"vm_name", "vmid", "cluster_name", "node", "os_type",
|
|
175
|
+
"cpu", "ram_gb", "disk_gb",
|
|
176
|
+
"management_ip", "management_gw", "internet_ip", "internet_gw",
|
|
177
|
+
"status", "notes", "checklist",
|
|
178
|
+
"created", "last_updated",
|
|
179
|
+
]
|
|
@@ -9,6 +9,7 @@ from netbox.api.routers import NetBoxRouter
|
|
|
9
9
|
from pve_sync_plugin import views as plugin_views
|
|
10
10
|
|
|
11
11
|
from . import views as api_views
|
|
12
|
+
from .views import VmProvisioningLogViewSet
|
|
12
13
|
|
|
13
14
|
app_name = "pve_sync_plugin-api"
|
|
14
15
|
|
|
@@ -20,6 +21,7 @@ router.register("events", api_views.PveWebhookEventViewSet)
|
|
|
20
21
|
router.register("clusters", api_views.PveClusterConfigViewSet)
|
|
21
22
|
router.register("backup-status", api_views.PveBackupStatusViewSet)
|
|
22
23
|
router.register("settings", api_views.PvePluginSettingsViewSet)
|
|
24
|
+
router.register("provisioning", api_views.VmProvisioningLogViewSet)
|
|
23
25
|
|
|
24
26
|
urlpatterns = [
|
|
25
27
|
path("webhook/", plugin_views.webhook_receiver, name="webhook"),
|
|
@@ -18,6 +18,7 @@ from pve_sync_plugin.models import (
|
|
|
18
18
|
PvePluginSettings,
|
|
19
19
|
PveSyncJob,
|
|
20
20
|
PveWebhookEvent,
|
|
21
|
+
VmProvisioningLog,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
from .serializers import (
|
|
@@ -26,6 +27,7 @@ from .serializers import (
|
|
|
26
27
|
PvePluginSettingsSerializer,
|
|
27
28
|
PveSyncJobSerializer,
|
|
28
29
|
PveWebhookEventSerializer,
|
|
30
|
+
VmProvisioningLogSerializer,
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
|
|
@@ -92,3 +94,8 @@ class PveBackupStatusViewSet(NetBoxModelViewSet):
|
|
|
92
94
|
class PvePluginSettingsViewSet(NetBoxModelViewSet):
|
|
93
95
|
queryset = PvePluginSettings.objects.all()
|
|
94
96
|
serializer_class = PvePluginSettingsSerializer
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class VmProvisioningLogViewSet(NetBoxModelViewSet):
|
|
100
|
+
queryset = VmProvisioningLog.objects.order_by("-created")
|
|
101
|
+
serializer_class = VmProvisioningLogSerializer
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from django.db import migrations, models
|
|
2
|
+
import django.db.models.deletion
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Migration(migrations.Migration):
|
|
6
|
+
|
|
7
|
+
dependencies = [
|
|
8
|
+
("pve_sync_plugin", "0013_pluginsettings_log_retention"),
|
|
9
|
+
("extras", "0138_customfieldchoiceset_choice_colors"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.CreateModel(
|
|
14
|
+
name="VmProvisioningLog",
|
|
15
|
+
fields=[
|
|
16
|
+
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
17
|
+
("created", models.DateTimeField(auto_now_add=True, null=True)),
|
|
18
|
+
("last_updated", models.DateTimeField(auto_now=True, null=True)),
|
|
19
|
+
("custom_field_data", models.JSONField(blank=True, default=dict, encoder=None)),
|
|
20
|
+
("vm_name", models.CharField(max_length=100, verbose_name="VM 名稱")),
|
|
21
|
+
("vmid", models.IntegerField(blank=True, null=True, verbose_name="VMID")),
|
|
22
|
+
("cluster_name", models.CharField(blank=True, max_length=100, verbose_name="叢集")),
|
|
23
|
+
("node", models.CharField(blank=True, max_length=100, verbose_name="節點")),
|
|
24
|
+
("os_type", models.CharField(blank=True, max_length=50, verbose_name="作業系統")),
|
|
25
|
+
("cpu", models.IntegerField(blank=True, null=True, verbose_name="CPU")),
|
|
26
|
+
("ram_gb", models.IntegerField(blank=True, null=True, verbose_name="記憶體 (GB)")),
|
|
27
|
+
("disk_gb", models.IntegerField(blank=True, null=True, verbose_name="磁碟 (GB)")),
|
|
28
|
+
("management_ip", models.CharField(blank=True, max_length=50, verbose_name="Management IP")),
|
|
29
|
+
("management_gw", models.CharField(blank=True, max_length=50, verbose_name="Management 閘道")),
|
|
30
|
+
("internet_ip", models.CharField(blank=True, max_length=50, verbose_name="Internet IP")),
|
|
31
|
+
("internet_gw", models.CharField(blank=True, max_length=50, verbose_name="Internet 閘道")),
|
|
32
|
+
("status", models.CharField(
|
|
33
|
+
choices=[("planning", "規劃中"), ("in_progress", "部署中"), ("completed", "完成")],
|
|
34
|
+
default="planning", max_length=20, verbose_name="狀態",
|
|
35
|
+
)),
|
|
36
|
+
("notes", models.TextField(blank=True, verbose_name="備註")),
|
|
37
|
+
("checklist", models.JSONField(blank=True, default=dict, verbose_name="清單狀態")),
|
|
38
|
+
],
|
|
39
|
+
options={"ordering": ["-created"], "verbose_name": "VM Provisioning Log"},
|
|
40
|
+
),
|
|
41
|
+
]
|
|
@@ -93,6 +93,10 @@ class PveSyncJob(NetBoxModel):
|
|
|
93
93
|
"partial": "warning",
|
|
94
94
|
}.get(self.status, "secondary")
|
|
95
95
|
|
|
96
|
+
@property
|
|
97
|
+
def is_pbs_job(self):
|
|
98
|
+
return self.cluster_name.startswith("pbs:")
|
|
99
|
+
|
|
96
100
|
@property
|
|
97
101
|
def error_message(self):
|
|
98
102
|
if not self.details:
|
|
@@ -459,3 +463,54 @@ class PbsServerConfig(NetBoxModel):
|
|
|
459
463
|
host = host.split('://', 1)[1]
|
|
460
464
|
host = host.split(':')[0]
|
|
461
465
|
return host
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class VmProvisioningLog(NetBoxModel):
|
|
470
|
+
"""Planning record created in the VM Provisioning Planner before PVE sync."""
|
|
471
|
+
|
|
472
|
+
STATUS_CHOICES = [
|
|
473
|
+
("planning", "規劃中"),
|
|
474
|
+
("in_progress", "部署中"),
|
|
475
|
+
("completed", "完成"),
|
|
476
|
+
]
|
|
477
|
+
|
|
478
|
+
vm_name = models.CharField(max_length=100, verbose_name="VM 名稱")
|
|
479
|
+
vmid = models.IntegerField(null=True, blank=True, verbose_name="VMID")
|
|
480
|
+
cluster_name = models.CharField(max_length=100, blank=True, verbose_name="叢集")
|
|
481
|
+
node = models.CharField(max_length=100, blank=True, verbose_name="節點")
|
|
482
|
+
os_type = models.CharField(max_length=50, blank=True, verbose_name="作業系統")
|
|
483
|
+
cpu = models.IntegerField(null=True, blank=True, verbose_name="CPU")
|
|
484
|
+
ram_gb = models.IntegerField(null=True, blank=True, verbose_name="記憶體 (GB)")
|
|
485
|
+
disk_gb = models.IntegerField(null=True, blank=True, verbose_name="磁碟 (GB)")
|
|
486
|
+
management_ip = models.CharField(max_length=50, blank=True, verbose_name="Management IP")
|
|
487
|
+
management_gw = models.CharField(max_length=50, blank=True, verbose_name="Management 閘道")
|
|
488
|
+
internet_ip = models.CharField(max_length=50, blank=True, verbose_name="Internet IP")
|
|
489
|
+
internet_gw = models.CharField(max_length=50, blank=True, verbose_name="Internet 閘道")
|
|
490
|
+
status = models.CharField(max_length=20, default="planning",
|
|
491
|
+
choices=STATUS_CHOICES, verbose_name="狀態")
|
|
492
|
+
notes = models.TextField(blank=True, verbose_name="備註")
|
|
493
|
+
checklist = models.JSONField(default=dict, blank=True, verbose_name="清單狀態")
|
|
494
|
+
|
|
495
|
+
class Meta:
|
|
496
|
+
ordering = ["-created"]
|
|
497
|
+
verbose_name = "VM Provisioning Log"
|
|
498
|
+
|
|
499
|
+
def __str__(self):
|
|
500
|
+
vmid_str = f" (VMID {self.vmid})" if self.vmid else ""
|
|
501
|
+
return f"{self.vm_name}{vmid_str}"
|
|
502
|
+
|
|
503
|
+
def get_absolute_url(self):
|
|
504
|
+
return reverse("plugins:pve_sync_plugin:vmprovisioninglog", args=[self.pk])
|
|
505
|
+
|
|
506
|
+
@property
|
|
507
|
+
def status_badge(self):
|
|
508
|
+
return {"planning": "info", "in_progress": "warning", "completed": "success"}.get(
|
|
509
|
+
self.status, "secondary"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
QEMU_GA_ITEMS = [
|
|
513
|
+
("chk_qemu_options", "在 VM Options 啟用 QEMU Guest Agent"),
|
|
514
|
+
("chk_qemu_install", "安裝 qemu-guest-agent 套件"),
|
|
515
|
+
("chk_qemu_enable", "啟用並啟動 qemu-guest-agent 服務"),
|
|
516
|
+
]
|
|
@@ -70,6 +70,11 @@ pbs_item = PluginMenuItem(
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
vm_provisioning_item = PluginMenuItem(
|
|
74
|
+
link="plugins:pve_sync_plugin:vmprovisioninglog_list",
|
|
75
|
+
link_text="VM 佈建",
|
|
76
|
+
)
|
|
77
|
+
|
|
73
78
|
menu = PluginMenu(
|
|
74
79
|
label="Proxmox Sync",
|
|
75
80
|
groups=(
|
|
@@ -85,6 +90,10 @@ menu = PluginMenu(
|
|
|
85
90
|
"Data Protection",
|
|
86
91
|
(backup_status_item,),
|
|
87
92
|
),
|
|
93
|
+
(
|
|
94
|
+
"VM Operations",
|
|
95
|
+
(vm_provisioning_item,),
|
|
96
|
+
),
|
|
88
97
|
(
|
|
89
98
|
"Configuration",
|
|
90
99
|
(clusters_item, pbs_item, settings_item),
|
|
@@ -11,6 +11,7 @@ from .models import (
|
|
|
11
11
|
PveDriftEvent,
|
|
12
12
|
PveSyncJob,
|
|
13
13
|
PveWebhookEvent,
|
|
14
|
+
VmProvisioningLog,
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
|
|
@@ -276,3 +277,25 @@ class PveBackupStatusTable(NetBoxTable):
|
|
|
276
277
|
"last_backup",
|
|
277
278
|
"backup_age_days",
|
|
278
279
|
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# VmProvisioningLog
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
class VmProvisioningLogTable(NetBoxTable):
|
|
287
|
+
vm_name = tables.Column(linkify=True, verbose_name="VM 名稱")
|
|
288
|
+
vmid = tables.Column(verbose_name="VMID")
|
|
289
|
+
cluster_name = tables.Column(verbose_name="叢集")
|
|
290
|
+
node = tables.Column(verbose_name="節點")
|
|
291
|
+
status = columns.ChoiceFieldColumn(verbose_name="狀態")
|
|
292
|
+
management_ip = tables.Column(verbose_name="Management IP")
|
|
293
|
+
internet_ip = tables.Column(verbose_name="Internet IP")
|
|
294
|
+
actions = columns.ActionsColumn(actions=("delete",))
|
|
295
|
+
|
|
296
|
+
class Meta(NetBoxTable.Meta):
|
|
297
|
+
model = VmProvisioningLog
|
|
298
|
+
fields = ("pk", "vm_name", "vmid", "cluster_name", "node", "status",
|
|
299
|
+
"management_ip", "internet_ip", "created")
|
|
300
|
+
default_columns = ("vm_name", "vmid", "cluster_name", "node", "status",
|
|
301
|
+
"management_ip", "internet_ip")
|
|
@@ -252,18 +252,20 @@ def run_pbs_sync_job(job_id, pbs_server_pk):
|
|
|
252
252
|
syncer.sync()
|
|
253
253
|
|
|
254
254
|
# Write backup records to PveBackupStatus
|
|
255
|
+
backup_updated, backup_skipped = None, None
|
|
255
256
|
try:
|
|
256
257
|
backup_records = _fetch_pbs_snapshots(pbs)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
job.details["backup_skipped"] = skipped
|
|
260
|
-
logger.info("PBS backup status: %d updated, %d no VM match", updated, skipped)
|
|
258
|
+
backup_updated, backup_skipped = _apply_pbs_backup_status(backup_records)
|
|
259
|
+
logger.info("PBS backup status: %d updated, %d no VM match", backup_updated, backup_skipped)
|
|
261
260
|
except Exception as exc:
|
|
262
261
|
logger.warning("PBS backup status sync failed (non-fatal): %s", exc)
|
|
263
262
|
|
|
264
263
|
job.refresh_from_db()
|
|
265
264
|
job.status = "success"
|
|
266
265
|
job.end_time = timezone.now()
|
|
266
|
+
if backup_updated is not None:
|
|
267
|
+
job.details["backup_updated"] = backup_updated
|
|
268
|
+
job.details["backup_skipped"] = backup_skipped
|
|
267
269
|
job.details.pop("heartbeat", None)
|
|
268
270
|
job.save()
|
|
269
271
|
|