proxsync 2.0.7__tar.gz → 2.0.8__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.7/proxsync.egg-info → proxsync-2.0.8}/PKG-INFO +4 -4
- {proxsync-2.0.7 → proxsync-2.0.8/proxsync.egg-info}/PKG-INFO +4 -4
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync.py +94 -129
- {proxsync-2.0.7 → proxsync-2.0.8}/pyproject.toml +4 -4
- {proxsync-2.0.7 → proxsync-2.0.8}/state_db.py +2 -24
- {proxsync-2.0.7 → proxsync-2.0.8}/MANIFEST.in +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/README.md +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/config.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pbs_sync.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/proxsync.egg-info/SOURCES.txt +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/proxsync.egg-info/dependency_links.txt +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/proxsync.egg-info/requires.txt +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/proxsync.egg-info/top_level.txt +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/api/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/api/serializers.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/api/urls.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/api/views.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/apps.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/choices.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/filtersets.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/forms.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/management/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/management/commands/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/management/commands/pve_sync.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/management/commands/run_scheduled_syncs.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0001_initial.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0002_pvepluginsettings.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0003_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0004_model_verbose_names.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0007_rename_verbose_names_to_proxmox_sync.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0009_add_every_3h_schedule.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0010_pvedriftevent.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0014_vmprovisioninglog.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0015_pvevmtasklog.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0016_pvevmtasklog_status_textfield.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/models.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/navigation.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/signals.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/sync/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/sync/config_bridge.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/sync/engine.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/tables.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/tasks.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/template_content.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/dashboard.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/inc/vm_task_log_panel.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/pbsserverconfig.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/pvebackupstatus.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/pveclusterconfig.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/pvedriftevent.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/pvesyncjob.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/pvevmtasklog_list.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/pvewebhookevent.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/settings.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templatetags/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/templatetags/pve_sync_tags.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/urls.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/utils.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/view_helpers.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/views.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/setup.cfg +0 -0
- {proxsync-2.0.7 → proxsync-2.0.8}/tests/test_plugin_contracts.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxsync
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.8
|
|
4
4
|
Summary: NetBox plugin for Proxmox VE inventory synchronization
|
|
5
5
|
Author-email: lixiujie85 <lixiujie85@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Source, https://github.com/
|
|
9
|
-
Project-URL: Issues, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/xiujie2/proxsync
|
|
8
|
+
Project-URL: Source, https://github.com/xiujie2/proxsync
|
|
9
|
+
Project-URL: Issues, https://github.com/xiujie2/proxsync/issues
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Framework :: Django
|
|
12
12
|
Classifier: Intended Audience :: System Administrators
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxsync
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.8
|
|
4
4
|
Summary: NetBox plugin for Proxmox VE inventory synchronization
|
|
5
5
|
Author-email: lixiujie85 <lixiujie85@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Source, https://github.com/
|
|
9
|
-
Project-URL: Issues, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/xiujie2/proxsync
|
|
8
|
+
Project-URL: Source, https://github.com/xiujie2/proxsync
|
|
9
|
+
Project-URL: Issues, https://github.com/xiujie2/proxsync/issues
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Framework :: Django
|
|
12
12
|
Classifier: Intended Audience :: System Administrators
|
|
@@ -125,25 +125,12 @@ class OptimizedPVEToNetBoxSync:
|
|
|
125
125
|
self.error_log = []
|
|
126
126
|
|
|
127
127
|
# ---------- Telegram ----------
|
|
128
|
-
|
|
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
|
|
128
|
+
def send_telegram_notification(self, message: str):
|
|
141
129
|
try:
|
|
142
130
|
url = f"https://api.telegram.org/bot{self.telegram_bot_token}/sendMessage"
|
|
143
131
|
payload = {'chat_id': self.telegram_chat_id, 'text': message, 'parse_mode': 'HTML'}
|
|
144
132
|
response = requests.post(url, data=payload, timeout=10)
|
|
145
133
|
if response.status_code == 200:
|
|
146
|
-
self._telegram_sent = sent + 1
|
|
147
134
|
print(f"✓ Telegram 通知已發送")
|
|
148
135
|
return True
|
|
149
136
|
else:
|
|
@@ -174,6 +161,28 @@ class OptimizedPVEToNetBoxSync:
|
|
|
174
161
|
self.send_telegram_notification(message)
|
|
175
162
|
print(f"📧 已發送 IP 衝突通知: {vm_name} - {ip_address}")
|
|
176
163
|
|
|
164
|
+
def log_mac_conflict_error(self, vm_name: str, mac_address: str, error_message: str):
|
|
165
|
+
error_info = {
|
|
166
|
+
'timestamp': time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
167
|
+
'vm_name': vm_name,
|
|
168
|
+
'ip_address': mac_address,
|
|
169
|
+
'error': error_message
|
|
170
|
+
}
|
|
171
|
+
self.error_log.append(error_info)
|
|
172
|
+
message = f"""
|
|
173
|
+
🚨 <b>PVE-NetBox 同步 MAC 位址衝突警告</b>
|
|
174
|
+
|
|
175
|
+
📅 時間: {error_info['timestamp']}
|
|
176
|
+
🖥️ 虛擬機: {vm_name}
|
|
177
|
+
🔌 MAC 位址: {mac_address}
|
|
178
|
+
❌ 錯誤: {error_message}
|
|
179
|
+
|
|
180
|
+
⚠️ 該網卡的 MAC 位址與另一台已同步的 VM 重複(通常是複製 VM 時未重新產生 MAC),
|
|
181
|
+
IP 位址仍會照常同步,但 MAC 關聯需要手動處理
|
|
182
|
+
"""
|
|
183
|
+
self.send_telegram_notification(message)
|
|
184
|
+
print(f"📧 已發送 MAC 衝突通知: {vm_name} - {mac_address}")
|
|
185
|
+
|
|
177
186
|
def send_enhanced_summary(self):
|
|
178
187
|
if not hasattr(self, 'stats'):
|
|
179
188
|
return
|
|
@@ -212,9 +221,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
212
221
|
message += f"• {error['vm_name']} - {error['ip_address']}\n"
|
|
213
222
|
if len(self.error_log) > 5:
|
|
214
223
|
message += f"• ... 還有 {len(self.error_log) - 5} 個錯誤\n"
|
|
215
|
-
suppressed = getattr(self, '_telegram_suppressed', 0)
|
|
216
|
-
if suppressed:
|
|
217
|
-
message += f"\n📵 本輪通知已達上限,{suppressed} 則訊息被壓制(詳見 NetBox 漂移事件記錄)"
|
|
218
224
|
self.send_telegram_notification(message)
|
|
219
225
|
|
|
220
226
|
# ---------- API 連接 ----------
|
|
@@ -301,7 +307,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
301
307
|
self.nb_cache['platforms'][platform.name.lower()] = platform
|
|
302
308
|
for role in self.nb_api.dcim.device_roles.all():
|
|
303
309
|
self.nb_cache['roles'][role.name.lower()] = role
|
|
304
|
-
self.nb_cache['device_roles'][role.name.lower()] = role
|
|
305
310
|
for cluster in self.nb_api.virtualization.clusters.all():
|
|
306
311
|
self.nb_cache['clusters'][str(cluster.id)] = cluster
|
|
307
312
|
for site in self.nb_api.dcim.sites.all():
|
|
@@ -310,6 +315,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
310
315
|
self.nb_cache['manufacturers'][manufacturer.name.lower()] = manufacturer
|
|
311
316
|
for device_type in self.nb_api.dcim.device_types.all():
|
|
312
317
|
self.nb_cache['device_types'][device_type.model.lower()] = device_type
|
|
318
|
+
for device_role in self.nb_api.dcim.device_roles.all():
|
|
319
|
+
self.nb_cache['device_roles'][device_role.name.lower()] = device_role
|
|
313
320
|
for cluster_type in self.nb_api.virtualization.cluster_types.all():
|
|
314
321
|
self.nb_cache['cluster_types'][cluster_type.name.lower()] = cluster_type
|
|
315
322
|
elapsed = time.time() - start_time
|
|
@@ -366,13 +373,13 @@ class OptimizedPVEToNetBoxSync:
|
|
|
366
373
|
vm['node'] = node_name
|
|
367
374
|
all_vms.append(vm)
|
|
368
375
|
self.pve_cache['vms_by_node'][node_name] = all_vms
|
|
369
|
-
for vm in all_vms:
|
|
376
|
+
for vm in all_vms[:10]:
|
|
370
377
|
try:
|
|
371
378
|
if vm['type'] == 'qemu':
|
|
372
379
|
vm['config'] = self.pve_api.nodes(node_name).qemu(vm['vmid']).config.get()
|
|
373
380
|
else:
|
|
374
381
|
vm['config'] = self.pve_api.nodes(node_name).lxc(vm['vmid']).config.get()
|
|
375
|
-
except
|
|
382
|
+
except:
|
|
376
383
|
pass
|
|
377
384
|
except Exception as e:
|
|
378
385
|
print(f" 加載節點 {node_name} 的虛擬機失敗: {e}")
|
|
@@ -939,33 +946,43 @@ class OptimizedPVEToNetBoxSync:
|
|
|
939
946
|
return {}, {}
|
|
940
947
|
|
|
941
948
|
# ---------- MAC 位址設定(NetBox 4.6+ 使用獨立 MACAddress 物件) ----------
|
|
942
|
-
def set_vm_interface_mac(self, vm_interface, mac_address: str):
|
|
943
|
-
"""Create/update a dcim.MACAddress for the VMInterface and set it as primary.
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
949
|
+
def set_vm_interface_mac(self, vm_interface, mac_address: str, vm_name: str = None):
|
|
950
|
+
"""Create/update a dcim.MACAddress for the VMInterface and set it as primary.
|
|
951
|
+
|
|
952
|
+
Two different PVE VMs can legitimately report the same hardware-address
|
|
953
|
+
(e.g. a clone whose NIC MAC was never regenerated). NetBox refuses to
|
|
954
|
+
move a MACAddress that is another object's primary MAC, so failures here
|
|
955
|
+
are caught and reported instead of raised — a MAC conflict must never
|
|
956
|
+
block syncing the interface's IP address.
|
|
957
|
+
"""
|
|
958
|
+
try:
|
|
959
|
+
mac_lower = mac_address.lower()
|
|
960
|
+
existing_mac = self.nb_cache['mac_addresses'].get(mac_lower)
|
|
961
|
+
if existing_mac:
|
|
962
|
+
# Already exists — ensure it's assigned to this interface and set as primary
|
|
963
|
+
if getattr(existing_mac, 'assigned_object_id', None) != vm_interface.id:
|
|
964
|
+
existing_mac.assigned_object_type = 'virtualization.vminterface'
|
|
965
|
+
existing_mac.assigned_object_id = vm_interface.id
|
|
966
|
+
existing_mac.save()
|
|
967
|
+
# Set primary_mac_address on the interface if not already set
|
|
968
|
+
current_primary = getattr(vm_interface, 'primary_mac_address', None)
|
|
969
|
+
if not current_primary or (hasattr(current_primary, 'id') and current_primary.id != existing_mac.id):
|
|
970
|
+
vm_interface.primary_mac_address = existing_mac.id
|
|
971
|
+
vm_interface.save()
|
|
972
|
+
else:
|
|
973
|
+
# Create new MACAddress assigned to this VMInterface
|
|
974
|
+
mac_obj = self.nb_api.dcim.mac_addresses.create(
|
|
975
|
+
mac_address=mac_address,
|
|
976
|
+
assigned_object_type='virtualization.vminterface',
|
|
977
|
+
assigned_object_id=vm_interface.id,
|
|
978
|
+
)
|
|
979
|
+
self.nb_cache['mac_addresses'][mac_lower] = mac_obj
|
|
980
|
+
# Set it as primary
|
|
981
|
+
vm_interface.primary_mac_address = mac_obj.id
|
|
957
982
|
vm_interface.save()
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
mac_address=mac_address,
|
|
962
|
-
assigned_object_type='virtualization.vminterface',
|
|
963
|
-
assigned_object_id=vm_interface.id,
|
|
964
|
-
)
|
|
965
|
-
self.nb_cache['mac_addresses'][mac_lower] = mac_obj
|
|
966
|
-
# Set it as primary
|
|
967
|
-
vm_interface.primary_mac_address = mac_obj.id
|
|
968
|
-
vm_interface.save()
|
|
983
|
+
except Exception as e:
|
|
984
|
+
print(f" 設定 MAC 位址失敗 {mac_address}: {e}")
|
|
985
|
+
self.log_mac_conflict_error(vm_name or "Unknown", mac_address, str(e))
|
|
969
986
|
|
|
970
987
|
# ---------- VM 介面處理 ----------
|
|
971
988
|
def process_vm_interfaces(self, vm, vm_config: Dict, agent_interfaces: Dict, mac_to_interface: Dict,
|
|
@@ -1001,7 +1018,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1001
1018
|
if enabled_changed:
|
|
1002
1019
|
vm_interface.enabled = enabled
|
|
1003
1020
|
if mac_address:
|
|
1004
|
-
self.set_vm_interface_mac(vm_interface, mac_address)
|
|
1021
|
+
self.set_vm_interface_mac(vm_interface, mac_address, vm_name=vm.name)
|
|
1005
1022
|
# set_vm_interface_mac only saves the interface in some branches;
|
|
1006
1023
|
# ensure the enabled change is always persisted
|
|
1007
1024
|
if enabled_changed:
|
|
@@ -1015,7 +1032,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1015
1032
|
vm_interface = self.nb_api.virtualization.interfaces.create(**iface_create_data)
|
|
1016
1033
|
self.nb_cache['vm_interfaces'].setdefault(vm.id, {})[config_key] = vm_interface
|
|
1017
1034
|
if mac_address:
|
|
1018
|
-
self.set_vm_interface_mac(vm_interface, mac_address)
|
|
1035
|
+
self.set_vm_interface_mac(vm_interface, mac_address, vm_name=vm.name)
|
|
1019
1036
|
iface_data = {
|
|
1020
1037
|
'name': config_key, 'mac_address': mac_address or '', 'bridge': bridge_name
|
|
1021
1038
|
}
|
|
@@ -1099,11 +1116,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1099
1116
|
disks[key] = m.group(1) if m else val_str[:80]
|
|
1100
1117
|
return disks
|
|
1101
1118
|
|
|
1102
|
-
@staticmethod
|
|
1103
|
-
def _norm_desc(s) -> str:
|
|
1104
|
-
"""Normalize description: unify line endings, strip whitespace."""
|
|
1105
|
-
return (s or '').replace('\r\n', '\n').replace('\r', '\n').strip()
|
|
1106
|
-
|
|
1107
1119
|
def _write_drift_event(self, vm_name: str, vmid: int, drift_type: str,
|
|
1108
1120
|
field_name: str, old_value: str, new_value: str,
|
|
1109
1121
|
notified: bool = False):
|
|
@@ -1126,13 +1138,13 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1126
1138
|
except Exception as e:
|
|
1127
1139
|
print(f" ⚠ 無法寫入漂移事件記錄: {e}")
|
|
1128
1140
|
|
|
1129
|
-
def detect_new_vm(self, vm_id: int, vm_name: str, node_name: str
|
|
1141
|
+
def detect_new_vm(self, vm_id: int, vm_name: str, node_name: str):
|
|
1130
1142
|
"""若此 vmid 在先前同步中不存在(但叢集有先前記錄),視為新增 VM。"""
|
|
1131
1143
|
if not self.enhanced_mode or not self.state_db:
|
|
1132
1144
|
return
|
|
1133
1145
|
if not self._known_vmids:
|
|
1134
1146
|
return # 首次同步,無先前記錄,不觸發
|
|
1135
|
-
if
|
|
1147
|
+
if self.state_db.get_last_vm_config(vm_id, self.cluster_name):
|
|
1136
1148
|
return # 已有記錄,不是新增
|
|
1137
1149
|
print(f"🆕 VM {vm_name} 新增偵測 (ID: {vm_id}, 節點: {node_name})")
|
|
1138
1150
|
self.stats['config_drifts_detected'] += 1
|
|
@@ -1271,29 +1283,15 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1271
1283
|
print(f" 🧹 Log 清理:刪除 {d_del} 筆漂移事件、{t_del} 筆操作記錄(>{days} 天)")
|
|
1272
1284
|
except Exception as exc:
|
|
1273
1285
|
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}")
|
|
1285
1286
|
|
|
1286
1287
|
def sync_vm_tasks(self):
|
|
1287
1288
|
"""從 PVE 各節點抓取 VM 操作記錄(建立/刪除/Clone/遷移)並同步到 NetBox。"""
|
|
1288
|
-
import calendar
|
|
1289
|
-
import datetime as _dt
|
|
1290
1289
|
try:
|
|
1291
1290
|
from django.utils import timezone
|
|
1292
1291
|
from pve_sync_plugin.models import PveVmTaskLog
|
|
1293
1292
|
except ImportError:
|
|
1294
1293
|
return
|
|
1295
1294
|
|
|
1296
|
-
_utc = _dt.timezone.utc
|
|
1297
1295
|
VM_TASK_TYPES = {
|
|
1298
1296
|
"qmcreate", "qmdestroy", "qmclone", "qmmigrate", "qmrestore",
|
|
1299
1297
|
"qmstart", "qmstop", "qmshutdown", "qmreboot",
|
|
@@ -1327,18 +1325,10 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1327
1325
|
cluster_name=self.cluster_name
|
|
1328
1326
|
).order_by("-start_time").values_list("start_time", flat=True).first()
|
|
1329
1327
|
if latest:
|
|
1328
|
+
import calendar
|
|
1330
1329
|
since_ts = int(calendar.timegm(latest.utctimetuple())) + 1
|
|
1331
1330
|
else:
|
|
1332
1331
|
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)
|
|
1342
1332
|
new_count = 0
|
|
1343
1333
|
|
|
1344
1334
|
for node_name in self.pve_cache.get("vms_by_node", {}):
|
|
@@ -1362,8 +1352,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1362
1352
|
except (ValueError, TypeError):
|
|
1363
1353
|
continue
|
|
1364
1354
|
|
|
1365
|
-
# UPID
|
|
1366
|
-
if upid
|
|
1355
|
+
# UPID 去重
|
|
1356
|
+
if PveVmTaskLog.objects.filter(upid=upid).exists():
|
|
1367
1357
|
continue
|
|
1368
1358
|
|
|
1369
1359
|
task_type = task.get("type")
|
|
@@ -1373,6 +1363,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1373
1363
|
status = task.get("status", "") or ""
|
|
1374
1364
|
vm_name = vmid_to_name.get(vmid, f"VM-{vmid}")
|
|
1375
1365
|
|
|
1366
|
+
import datetime as _dt
|
|
1367
|
+
_utc = _dt.timezone.utc
|
|
1376
1368
|
start_dt = _dt.datetime.fromtimestamp(start_ts, tz=_utc) if start_ts else timezone.now()
|
|
1377
1369
|
end_dt = _dt.datetime.fromtimestamp(end_ts, tz=_utc) if end_ts else None
|
|
1378
1370
|
|
|
@@ -1389,7 +1381,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1389
1381
|
status=status,
|
|
1390
1382
|
notified_telegram=False,
|
|
1391
1383
|
)
|
|
1392
|
-
new_upids.add(upid)
|
|
1393
1384
|
new_count += 1
|
|
1394
1385
|
|
|
1395
1386
|
# Telegram 通知(僅近期且已完成的操作)
|
|
@@ -1417,12 +1408,11 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1417
1408
|
|
|
1418
1409
|
def detect_config_drift(self, vm_id: int, vm_name: str, vm_config: Dict[str, Any],
|
|
1419
1410
|
current_tags: List[str], network_interfaces: List[Dict] = None,
|
|
1420
|
-
current_node: str = None
|
|
1411
|
+
current_node: str = None) -> List[Dict]:
|
|
1421
1412
|
if not self.enhanced_mode or not self.state_db:
|
|
1422
1413
|
return []
|
|
1423
1414
|
drifts = []
|
|
1424
|
-
|
|
1425
|
-
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1415
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1426
1416
|
if not last_config:
|
|
1427
1417
|
return drifts
|
|
1428
1418
|
current_memory = int(vm_config.get('memory', 0))
|
|
@@ -1503,8 +1493,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1503
1493
|
self._write_drift_event(vm_name, vm_id, 'disk_change', k, old_v, new_v, notified=True)
|
|
1504
1494
|
|
|
1505
1495
|
# VM 描述變更偵測
|
|
1506
|
-
current_desc =
|
|
1507
|
-
old_desc =
|
|
1496
|
+
current_desc = (vm_config.get('description') or '').strip()[:500]
|
|
1497
|
+
old_desc = (last_config.get('description') or '').strip()
|
|
1508
1498
|
if old_desc and old_desc != current_desc:
|
|
1509
1499
|
self.stats['config_drifts_detected'] += 1
|
|
1510
1500
|
print(f"📝 VM {vm_name} 描述變更")
|
|
@@ -1544,7 +1534,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1544
1534
|
return drifts
|
|
1545
1535
|
|
|
1546
1536
|
def should_sync_vm(self, vm_id: int, vm_config: Dict[str, Any], tags: List[str],
|
|
1547
|
-
network_interfaces: List[Dict] = None
|
|
1537
|
+
network_interfaces: List[Dict] = None) -> bool:
|
|
1548
1538
|
if not self.sync_config.get('incremental', True):
|
|
1549
1539
|
return True
|
|
1550
1540
|
if self.sync_config.get('force_full_sync', False):
|
|
@@ -1552,35 +1542,34 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1552
1542
|
current_hash = compute_config_hash(vm_config, tags, network_interfaces)
|
|
1553
1543
|
if not self.state_db:
|
|
1554
1544
|
return True
|
|
1555
|
-
|
|
1556
|
-
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1545
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1557
1546
|
if not last_config:
|
|
1558
1547
|
return True
|
|
1559
1548
|
if current_hash != last_config.get('config_hash'):
|
|
1560
1549
|
return True
|
|
1561
1550
|
return False
|
|
1562
1551
|
|
|
1563
|
-
def detect_tag_changes(self, vm_id: int, vm_name: str, current_tags: List[str]
|
|
1564
|
-
last_config=None) -> List[Dict]:
|
|
1552
|
+
def detect_tag_changes(self, vm_id: int, vm_name: str, current_tags: List[str]) -> List[Dict]:
|
|
1565
1553
|
if not self.enhanced_mode or not self.state_db:
|
|
1566
1554
|
return []
|
|
1567
|
-
|
|
1568
|
-
|
|
1555
|
+
changes = []
|
|
1556
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1569
1557
|
if not last_config:
|
|
1570
|
-
return
|
|
1558
|
+
return changes
|
|
1571
1559
|
old_tags = set(last_config.get('tags', []))
|
|
1572
1560
|
new_tags = set(current_tags)
|
|
1573
1561
|
added = new_tags - old_tags
|
|
1574
1562
|
removed = old_tags - new_tags
|
|
1575
|
-
changes = []
|
|
1576
1563
|
for tag in added:
|
|
1577
1564
|
changes.append({'type': 'added', 'tag': tag})
|
|
1578
1565
|
self.stats['tag_changes'] += 1
|
|
1579
1566
|
print(f"🏷️ VM {vm_name} 新增標籤: {tag}")
|
|
1567
|
+
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', '', tag, notified=False)
|
|
1580
1568
|
for tag in removed:
|
|
1581
1569
|
changes.append({'type': 'removed', 'tag': tag})
|
|
1582
1570
|
self.stats['tag_changes'] += 1
|
|
1583
1571
|
print(f"🏷️ VM {vm_name} 移除標籤: {tag}")
|
|
1572
|
+
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', tag, '', notified=False)
|
|
1584
1573
|
if changes:
|
|
1585
1574
|
added_list = [c['tag'] for c in changes if c['type'] == 'added']
|
|
1586
1575
|
removed_list = [c['tag'] for c in changes if c['type'] == 'removed']
|
|
@@ -1596,12 +1585,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1596
1585
|
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1597
1586
|
f"{detail}\n請確認是否為預期變更。"
|
|
1598
1587
|
)
|
|
1599
|
-
|
|
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)
|
|
1588
|
+
self.send_telegram_notification(tag_msg)
|
|
1605
1589
|
return changes
|
|
1606
1590
|
|
|
1607
1591
|
def detect_ip_change(self, vm_id: int, vm_name: str, current_ip: str):
|
|
@@ -1669,16 +1653,10 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1669
1653
|
tag_ids.append(tag.id)
|
|
1670
1654
|
except Exception as e:
|
|
1671
1655
|
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
|
-
)
|
|
1678
1656
|
if self.enhanced_mode:
|
|
1679
|
-
self.detect_new_vm(int(vm_id), original_vm_name, node_name
|
|
1657
|
+
self.detect_new_vm(int(vm_id), original_vm_name, node_name)
|
|
1680
1658
|
if self.enhanced_mode and tag_names:
|
|
1681
|
-
self.detect_tag_changes(int(vm_id), original_vm_name, tag_names
|
|
1659
|
+
self.detect_tag_changes(int(vm_id), original_vm_name, tag_names)
|
|
1682
1660
|
vm_status = vm_data.get('status', '')
|
|
1683
1661
|
if tag_names and vm_status in ('running', 'stopped'):
|
|
1684
1662
|
first_tag = tag_names[0].lower()
|
|
@@ -1761,9 +1739,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1761
1739
|
})
|
|
1762
1740
|
if self.enhanced_mode:
|
|
1763
1741
|
self.detect_config_drift(int(vm_id), original_vm_name, vm_config, tag_names,
|
|
1764
|
-
network_interfaces, current_node=node_name
|
|
1765
|
-
if not force and not self.should_sync_vm(int(vm_id), vm_config, tag_names, network_interfaces
|
|
1766
|
-
last_config=last_config):
|
|
1742
|
+
network_interfaces, current_node=node_name)
|
|
1743
|
+
if not force and not self.should_sync_vm(int(vm_id), vm_config, tag_names, network_interfaces):
|
|
1767
1744
|
cached_vm = self.nb_cache['virtual_machines_by_serial'].get(f"{vm_id}::{cluster['id']}")
|
|
1768
1745
|
if cached_vm is None:
|
|
1769
1746
|
# VM 在 NetBox 中不存在(例如隨節點被連帶刪除),
|
|
@@ -1804,7 +1781,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1804
1781
|
vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
|
|
1805
1782
|
memory=memory, vcpus=vcpus_save, tags=tag_names,
|
|
1806
1783
|
node=node_name, primary_ip=cur_ip, vm_name=original_vm_name,
|
|
1807
|
-
description=
|
|
1784
|
+
description=(vm_config.get('description') or '').strip()[:500],
|
|
1808
1785
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1809
1786
|
)
|
|
1810
1787
|
except Exception as _snap_err:
|
|
@@ -1861,7 +1838,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1861
1838
|
update_data = {
|
|
1862
1839
|
'name': vm_name, 'cluster': cluster['id'], 'device': device.id, 'role': role_id,
|
|
1863
1840
|
'vcpus': vcpus, 'memory': int(vm_config.get('memory', 0)), 'status': status,
|
|
1864
|
-
'description':
|
|
1841
|
+
'description': (vm_config.get('description') or '')[:200], 'platform': platform_id,
|
|
1865
1842
|
'start_on_boot': boot_choice
|
|
1866
1843
|
}
|
|
1867
1844
|
if tag_ids:
|
|
@@ -1880,7 +1857,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1880
1857
|
vm_data_dict = {
|
|
1881
1858
|
'serial': vm_id, 'name': vm_name, 'cluster': cluster['id'], 'device': device.id,
|
|
1882
1859
|
'role': role_id, 'vcpus': vcpus, 'memory': int(vm_config.get('memory', 0)),
|
|
1883
|
-
'status': status, 'description':
|
|
1860
|
+
'status': status, 'description': (vm_config.get('description') or '')[:200],
|
|
1884
1861
|
'platform': platform_id, 'start_on_boot': boot_choice
|
|
1885
1862
|
}
|
|
1886
1863
|
if tag_ids:
|
|
@@ -1917,7 +1894,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1917
1894
|
vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
|
|
1918
1895
|
memory=memory, vcpus=vcpus_save, tags=tag_names,
|
|
1919
1896
|
node=node_name, primary_ip=primary_ip_str, vm_name=original_vm_name,
|
|
1920
|
-
description=
|
|
1897
|
+
description=(vm_config.get('description') or '').strip()[:500],
|
|
1921
1898
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1922
1899
|
)
|
|
1923
1900
|
return True
|
|
@@ -1942,7 +1919,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1942
1919
|
node=node_name,
|
|
1943
1920
|
primary_ip=_ip,
|
|
1944
1921
|
vm_name=original_vm_name,
|
|
1945
|
-
description=
|
|
1922
|
+
description=(vm_config.get('description') or '').strip()[:500],
|
|
1946
1923
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1947
1924
|
)
|
|
1948
1925
|
except Exception:
|
|
@@ -1985,18 +1962,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1985
1962
|
print("=" * 50)
|
|
1986
1963
|
if self.enhanced_mode and self.state_db:
|
|
1987
1964
|
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
|
|
2000
1965
|
sync_mode = "增量" if self.sync_config.get('incremental', True) else "全量"
|
|
2001
1966
|
start_message = f"""
|
|
2002
1967
|
🔄 <b>PVE-NetBox 同步開始</b>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proxsync"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.8"
|
|
8
8
|
description = "NetBox plugin for Proxmox VE inventory synchronization"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -36,9 +36,9 @@ dependencies = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[project.urls]
|
|
39
|
-
Homepage = "https://github.com/
|
|
40
|
-
Source = "https://github.com/
|
|
41
|
-
Issues = "https://github.com/
|
|
39
|
+
Homepage = "https://github.com/xiujie2/proxsync"
|
|
40
|
+
Source = "https://github.com/xiujie2/proxsync"
|
|
41
|
+
Issues = "https://github.com/xiujie2/proxsync/issues"
|
|
42
42
|
|
|
43
43
|
[project.optional-dependencies]
|
|
44
44
|
standalone = [
|
|
@@ -154,7 +154,7 @@ class StateDB:
|
|
|
154
154
|
with sqlite3.connect(self.db_path) as conn:
|
|
155
155
|
conn.execute(
|
|
156
156
|
"""
|
|
157
|
-
INSERT
|
|
157
|
+
INSERT 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,28 +256,6 @@ 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
|
-
|
|
281
259
|
# ========== 节点状态历史 ==========
|
|
282
260
|
|
|
283
261
|
def save_node_status(self, node_name: str, cluster_name: str, status: str):
|
|
@@ -529,7 +507,7 @@ def compute_config_hash(vm_config: Dict[str, Any], tags: List[str], network_inte
|
|
|
529
507
|
'ostype': vm_config.get('ostype', ''),
|
|
530
508
|
'cores': vm_config.get('cores', 1),
|
|
531
509
|
'sockets': vm_config.get('sockets', 1),
|
|
532
|
-
'description':
|
|
510
|
+
'description': vm_config.get('description', '')[:200], # 限制长度
|
|
533
511
|
'tags': sorted(tags), # 排序以确保一致性
|
|
534
512
|
'networks': net_config # 添加网络接口配置
|
|
535
513
|
}
|
|
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.7 → proxsync-2.0.8}/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.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py
RENAMED
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.8}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.8}/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.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html
RENAMED
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.8}/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.7 → proxsync-2.0.8}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html
RENAMED
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.8}/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
|