proxsync 2.0.7__tar.gz → 2.0.9__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.9}/PKG-INFO +4 -4
- {proxsync-2.0.7 → proxsync-2.0.9/proxsync.egg-info}/PKG-INFO +4 -4
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync.py +93 -129
- {proxsync-2.0.7 → proxsync-2.0.9}/pyproject.toml +4 -4
- {proxsync-2.0.7 → proxsync-2.0.9}/state_db.py +2 -24
- {proxsync-2.0.7 → proxsync-2.0.9}/MANIFEST.in +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/README.md +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/config.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pbs_sync.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/proxsync.egg-info/SOURCES.txt +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/proxsync.egg-info/dependency_links.txt +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/proxsync.egg-info/requires.txt +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/proxsync.egg-info/top_level.txt +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/api/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/api/serializers.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/api/urls.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/api/views.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/apps.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/choices.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/filtersets.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/forms.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/management/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/management/commands/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/management/commands/pve_sync.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/management/commands/run_scheduled_syncs.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0001_initial.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0002_pvepluginsettings.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0003_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0004_model_verbose_names.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0007_rename_verbose_names_to_proxmox_sync.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0009_add_every_3h_schedule.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0010_pvedriftevent.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0014_vmprovisioninglog.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0015_pvevmtasklog.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0016_pvevmtasklog_status_textfield.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/models.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/navigation.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/signals.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/sync/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/sync/config_bridge.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/sync/engine.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/tables.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/tasks.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/template_content.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/dashboard.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/inc/vm_task_log_panel.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pbsserverconfig.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pvebackupstatus.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pveclusterconfig.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pvedriftevent.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pvesyncjob.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pvevmtasklog_list.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pvewebhookevent.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/settings.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templatetags/__init__.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templatetags/pve_sync_tags.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/urls.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/utils.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/view_helpers.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/views.py +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/setup.cfg +0 -0
- {proxsync-2.0.7 → proxsync-2.0.9}/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.9
|
|
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.9
|
|
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,27 @@ 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
|
+
"""
|
|
182
|
+
self.send_telegram_notification(message)
|
|
183
|
+
print(f"📧 已發送 MAC 衝突通知: {vm_name} - {mac_address}")
|
|
184
|
+
|
|
177
185
|
def send_enhanced_summary(self):
|
|
178
186
|
if not hasattr(self, 'stats'):
|
|
179
187
|
return
|
|
@@ -212,9 +220,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
212
220
|
message += f"• {error['vm_name']} - {error['ip_address']}\n"
|
|
213
221
|
if len(self.error_log) > 5:
|
|
214
222
|
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
223
|
self.send_telegram_notification(message)
|
|
219
224
|
|
|
220
225
|
# ---------- API 連接 ----------
|
|
@@ -301,7 +306,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
301
306
|
self.nb_cache['platforms'][platform.name.lower()] = platform
|
|
302
307
|
for role in self.nb_api.dcim.device_roles.all():
|
|
303
308
|
self.nb_cache['roles'][role.name.lower()] = role
|
|
304
|
-
self.nb_cache['device_roles'][role.name.lower()] = role
|
|
305
309
|
for cluster in self.nb_api.virtualization.clusters.all():
|
|
306
310
|
self.nb_cache['clusters'][str(cluster.id)] = cluster
|
|
307
311
|
for site in self.nb_api.dcim.sites.all():
|
|
@@ -310,6 +314,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
310
314
|
self.nb_cache['manufacturers'][manufacturer.name.lower()] = manufacturer
|
|
311
315
|
for device_type in self.nb_api.dcim.device_types.all():
|
|
312
316
|
self.nb_cache['device_types'][device_type.model.lower()] = device_type
|
|
317
|
+
for device_role in self.nb_api.dcim.device_roles.all():
|
|
318
|
+
self.nb_cache['device_roles'][device_role.name.lower()] = device_role
|
|
313
319
|
for cluster_type in self.nb_api.virtualization.cluster_types.all():
|
|
314
320
|
self.nb_cache['cluster_types'][cluster_type.name.lower()] = cluster_type
|
|
315
321
|
elapsed = time.time() - start_time
|
|
@@ -366,13 +372,13 @@ class OptimizedPVEToNetBoxSync:
|
|
|
366
372
|
vm['node'] = node_name
|
|
367
373
|
all_vms.append(vm)
|
|
368
374
|
self.pve_cache['vms_by_node'][node_name] = all_vms
|
|
369
|
-
for vm in all_vms:
|
|
375
|
+
for vm in all_vms[:10]:
|
|
370
376
|
try:
|
|
371
377
|
if vm['type'] == 'qemu':
|
|
372
378
|
vm['config'] = self.pve_api.nodes(node_name).qemu(vm['vmid']).config.get()
|
|
373
379
|
else:
|
|
374
380
|
vm['config'] = self.pve_api.nodes(node_name).lxc(vm['vmid']).config.get()
|
|
375
|
-
except
|
|
381
|
+
except:
|
|
376
382
|
pass
|
|
377
383
|
except Exception as e:
|
|
378
384
|
print(f" 加載節點 {node_name} 的虛擬機失敗: {e}")
|
|
@@ -939,33 +945,43 @@ class OptimizedPVEToNetBoxSync:
|
|
|
939
945
|
return {}, {}
|
|
940
946
|
|
|
941
947
|
# ---------- 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
|
-
|
|
948
|
+
def set_vm_interface_mac(self, vm_interface, mac_address: str, vm_name: str = None):
|
|
949
|
+
"""Create/update a dcim.MACAddress for the VMInterface and set it as primary.
|
|
950
|
+
|
|
951
|
+
Two different PVE VMs can legitimately report the same hardware-address
|
|
952
|
+
(e.g. a clone whose NIC MAC was never regenerated). NetBox refuses to
|
|
953
|
+
move a MACAddress that is another object's primary MAC, so failures here
|
|
954
|
+
are caught and reported instead of raised — a MAC conflict must never
|
|
955
|
+
block syncing the interface's IP address.
|
|
956
|
+
"""
|
|
957
|
+
try:
|
|
958
|
+
mac_lower = mac_address.lower()
|
|
959
|
+
existing_mac = self.nb_cache['mac_addresses'].get(mac_lower)
|
|
960
|
+
if existing_mac:
|
|
961
|
+
# Already exists — ensure it's assigned to this interface and set as primary
|
|
962
|
+
if getattr(existing_mac, 'assigned_object_id', None) != vm_interface.id:
|
|
963
|
+
existing_mac.assigned_object_type = 'virtualization.vminterface'
|
|
964
|
+
existing_mac.assigned_object_id = vm_interface.id
|
|
965
|
+
existing_mac.save()
|
|
966
|
+
# Set primary_mac_address on the interface if not already set
|
|
967
|
+
current_primary = getattr(vm_interface, 'primary_mac_address', None)
|
|
968
|
+
if not current_primary or (hasattr(current_primary, 'id') and current_primary.id != existing_mac.id):
|
|
969
|
+
vm_interface.primary_mac_address = existing_mac.id
|
|
970
|
+
vm_interface.save()
|
|
971
|
+
else:
|
|
972
|
+
# Create new MACAddress assigned to this VMInterface
|
|
973
|
+
mac_obj = self.nb_api.dcim.mac_addresses.create(
|
|
974
|
+
mac_address=mac_address,
|
|
975
|
+
assigned_object_type='virtualization.vminterface',
|
|
976
|
+
assigned_object_id=vm_interface.id,
|
|
977
|
+
)
|
|
978
|
+
self.nb_cache['mac_addresses'][mac_lower] = mac_obj
|
|
979
|
+
# Set it as primary
|
|
980
|
+
vm_interface.primary_mac_address = mac_obj.id
|
|
957
981
|
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()
|
|
982
|
+
except Exception as e:
|
|
983
|
+
print(f" 設定 MAC 位址失敗 {mac_address}: {e}")
|
|
984
|
+
self.log_mac_conflict_error(vm_name or "Unknown", mac_address, str(e))
|
|
969
985
|
|
|
970
986
|
# ---------- VM 介面處理 ----------
|
|
971
987
|
def process_vm_interfaces(self, vm, vm_config: Dict, agent_interfaces: Dict, mac_to_interface: Dict,
|
|
@@ -1001,7 +1017,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1001
1017
|
if enabled_changed:
|
|
1002
1018
|
vm_interface.enabled = enabled
|
|
1003
1019
|
if mac_address:
|
|
1004
|
-
self.set_vm_interface_mac(vm_interface, mac_address)
|
|
1020
|
+
self.set_vm_interface_mac(vm_interface, mac_address, vm_name=vm.name)
|
|
1005
1021
|
# set_vm_interface_mac only saves the interface in some branches;
|
|
1006
1022
|
# ensure the enabled change is always persisted
|
|
1007
1023
|
if enabled_changed:
|
|
@@ -1015,7 +1031,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1015
1031
|
vm_interface = self.nb_api.virtualization.interfaces.create(**iface_create_data)
|
|
1016
1032
|
self.nb_cache['vm_interfaces'].setdefault(vm.id, {})[config_key] = vm_interface
|
|
1017
1033
|
if mac_address:
|
|
1018
|
-
self.set_vm_interface_mac(vm_interface, mac_address)
|
|
1034
|
+
self.set_vm_interface_mac(vm_interface, mac_address, vm_name=vm.name)
|
|
1019
1035
|
iface_data = {
|
|
1020
1036
|
'name': config_key, 'mac_address': mac_address or '', 'bridge': bridge_name
|
|
1021
1037
|
}
|
|
@@ -1099,11 +1115,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1099
1115
|
disks[key] = m.group(1) if m else val_str[:80]
|
|
1100
1116
|
return disks
|
|
1101
1117
|
|
|
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
1118
|
def _write_drift_event(self, vm_name: str, vmid: int, drift_type: str,
|
|
1108
1119
|
field_name: str, old_value: str, new_value: str,
|
|
1109
1120
|
notified: bool = False):
|
|
@@ -1126,13 +1137,13 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1126
1137
|
except Exception as e:
|
|
1127
1138
|
print(f" ⚠ 無法寫入漂移事件記錄: {e}")
|
|
1128
1139
|
|
|
1129
|
-
def detect_new_vm(self, vm_id: int, vm_name: str, node_name: str
|
|
1140
|
+
def detect_new_vm(self, vm_id: int, vm_name: str, node_name: str):
|
|
1130
1141
|
"""若此 vmid 在先前同步中不存在(但叢集有先前記錄),視為新增 VM。"""
|
|
1131
1142
|
if not self.enhanced_mode or not self.state_db:
|
|
1132
1143
|
return
|
|
1133
1144
|
if not self._known_vmids:
|
|
1134
1145
|
return # 首次同步,無先前記錄,不觸發
|
|
1135
|
-
if
|
|
1146
|
+
if self.state_db.get_last_vm_config(vm_id, self.cluster_name):
|
|
1136
1147
|
return # 已有記錄,不是新增
|
|
1137
1148
|
print(f"🆕 VM {vm_name} 新增偵測 (ID: {vm_id}, 節點: {node_name})")
|
|
1138
1149
|
self.stats['config_drifts_detected'] += 1
|
|
@@ -1271,29 +1282,15 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1271
1282
|
print(f" 🧹 Log 清理:刪除 {d_del} 筆漂移事件、{t_del} 筆操作記錄(>{days} 天)")
|
|
1272
1283
|
except Exception as exc:
|
|
1273
1284
|
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
1285
|
|
|
1286
1286
|
def sync_vm_tasks(self):
|
|
1287
1287
|
"""從 PVE 各節點抓取 VM 操作記錄(建立/刪除/Clone/遷移)並同步到 NetBox。"""
|
|
1288
|
-
import calendar
|
|
1289
|
-
import datetime as _dt
|
|
1290
1288
|
try:
|
|
1291
1289
|
from django.utils import timezone
|
|
1292
1290
|
from pve_sync_plugin.models import PveVmTaskLog
|
|
1293
1291
|
except ImportError:
|
|
1294
1292
|
return
|
|
1295
1293
|
|
|
1296
|
-
_utc = _dt.timezone.utc
|
|
1297
1294
|
VM_TASK_TYPES = {
|
|
1298
1295
|
"qmcreate", "qmdestroy", "qmclone", "qmmigrate", "qmrestore",
|
|
1299
1296
|
"qmstart", "qmstop", "qmshutdown", "qmreboot",
|
|
@@ -1327,18 +1324,10 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1327
1324
|
cluster_name=self.cluster_name
|
|
1328
1325
|
).order_by("-start_time").values_list("start_time", flat=True).first()
|
|
1329
1326
|
if latest:
|
|
1327
|
+
import calendar
|
|
1330
1328
|
since_ts = int(calendar.timegm(latest.utctimetuple())) + 1
|
|
1331
1329
|
else:
|
|
1332
1330
|
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
1331
|
new_count = 0
|
|
1343
1332
|
|
|
1344
1333
|
for node_name in self.pve_cache.get("vms_by_node", {}):
|
|
@@ -1362,8 +1351,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1362
1351
|
except (ValueError, TypeError):
|
|
1363
1352
|
continue
|
|
1364
1353
|
|
|
1365
|
-
# UPID
|
|
1366
|
-
if upid
|
|
1354
|
+
# UPID 去重
|
|
1355
|
+
if PveVmTaskLog.objects.filter(upid=upid).exists():
|
|
1367
1356
|
continue
|
|
1368
1357
|
|
|
1369
1358
|
task_type = task.get("type")
|
|
@@ -1373,6 +1362,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1373
1362
|
status = task.get("status", "") or ""
|
|
1374
1363
|
vm_name = vmid_to_name.get(vmid, f"VM-{vmid}")
|
|
1375
1364
|
|
|
1365
|
+
import datetime as _dt
|
|
1366
|
+
_utc = _dt.timezone.utc
|
|
1376
1367
|
start_dt = _dt.datetime.fromtimestamp(start_ts, tz=_utc) if start_ts else timezone.now()
|
|
1377
1368
|
end_dt = _dt.datetime.fromtimestamp(end_ts, tz=_utc) if end_ts else None
|
|
1378
1369
|
|
|
@@ -1389,7 +1380,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1389
1380
|
status=status,
|
|
1390
1381
|
notified_telegram=False,
|
|
1391
1382
|
)
|
|
1392
|
-
new_upids.add(upid)
|
|
1393
1383
|
new_count += 1
|
|
1394
1384
|
|
|
1395
1385
|
# Telegram 通知(僅近期且已完成的操作)
|
|
@@ -1417,12 +1407,11 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1417
1407
|
|
|
1418
1408
|
def detect_config_drift(self, vm_id: int, vm_name: str, vm_config: Dict[str, Any],
|
|
1419
1409
|
current_tags: List[str], network_interfaces: List[Dict] = None,
|
|
1420
|
-
current_node: str = None
|
|
1410
|
+
current_node: str = None) -> List[Dict]:
|
|
1421
1411
|
if not self.enhanced_mode or not self.state_db:
|
|
1422
1412
|
return []
|
|
1423
1413
|
drifts = []
|
|
1424
|
-
|
|
1425
|
-
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1414
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1426
1415
|
if not last_config:
|
|
1427
1416
|
return drifts
|
|
1428
1417
|
current_memory = int(vm_config.get('memory', 0))
|
|
@@ -1503,8 +1492,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1503
1492
|
self._write_drift_event(vm_name, vm_id, 'disk_change', k, old_v, new_v, notified=True)
|
|
1504
1493
|
|
|
1505
1494
|
# VM 描述變更偵測
|
|
1506
|
-
current_desc =
|
|
1507
|
-
old_desc =
|
|
1495
|
+
current_desc = (vm_config.get('description') or '').strip()[:500]
|
|
1496
|
+
old_desc = (last_config.get('description') or '').strip()
|
|
1508
1497
|
if old_desc and old_desc != current_desc:
|
|
1509
1498
|
self.stats['config_drifts_detected'] += 1
|
|
1510
1499
|
print(f"📝 VM {vm_name} 描述變更")
|
|
@@ -1544,7 +1533,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1544
1533
|
return drifts
|
|
1545
1534
|
|
|
1546
1535
|
def should_sync_vm(self, vm_id: int, vm_config: Dict[str, Any], tags: List[str],
|
|
1547
|
-
network_interfaces: List[Dict] = None
|
|
1536
|
+
network_interfaces: List[Dict] = None) -> bool:
|
|
1548
1537
|
if not self.sync_config.get('incremental', True):
|
|
1549
1538
|
return True
|
|
1550
1539
|
if self.sync_config.get('force_full_sync', False):
|
|
@@ -1552,35 +1541,34 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1552
1541
|
current_hash = compute_config_hash(vm_config, tags, network_interfaces)
|
|
1553
1542
|
if not self.state_db:
|
|
1554
1543
|
return True
|
|
1555
|
-
|
|
1556
|
-
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1544
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1557
1545
|
if not last_config:
|
|
1558
1546
|
return True
|
|
1559
1547
|
if current_hash != last_config.get('config_hash'):
|
|
1560
1548
|
return True
|
|
1561
1549
|
return False
|
|
1562
1550
|
|
|
1563
|
-
def detect_tag_changes(self, vm_id: int, vm_name: str, current_tags: List[str]
|
|
1564
|
-
last_config=None) -> List[Dict]:
|
|
1551
|
+
def detect_tag_changes(self, vm_id: int, vm_name: str, current_tags: List[str]) -> List[Dict]:
|
|
1565
1552
|
if not self.enhanced_mode or not self.state_db:
|
|
1566
1553
|
return []
|
|
1567
|
-
|
|
1568
|
-
|
|
1554
|
+
changes = []
|
|
1555
|
+
last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
|
|
1569
1556
|
if not last_config:
|
|
1570
|
-
return
|
|
1557
|
+
return changes
|
|
1571
1558
|
old_tags = set(last_config.get('tags', []))
|
|
1572
1559
|
new_tags = set(current_tags)
|
|
1573
1560
|
added = new_tags - old_tags
|
|
1574
1561
|
removed = old_tags - new_tags
|
|
1575
|
-
changes = []
|
|
1576
1562
|
for tag in added:
|
|
1577
1563
|
changes.append({'type': 'added', 'tag': tag})
|
|
1578
1564
|
self.stats['tag_changes'] += 1
|
|
1579
1565
|
print(f"🏷️ VM {vm_name} 新增標籤: {tag}")
|
|
1566
|
+
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', '', tag, notified=False)
|
|
1580
1567
|
for tag in removed:
|
|
1581
1568
|
changes.append({'type': 'removed', 'tag': tag})
|
|
1582
1569
|
self.stats['tag_changes'] += 1
|
|
1583
1570
|
print(f"🏷️ VM {vm_name} 移除標籤: {tag}")
|
|
1571
|
+
self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', tag, '', notified=False)
|
|
1584
1572
|
if changes:
|
|
1585
1573
|
added_list = [c['tag'] for c in changes if c['type'] == 'added']
|
|
1586
1574
|
removed_list = [c['tag'] for c in changes if c['type'] == 'removed']
|
|
@@ -1596,12 +1584,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1596
1584
|
f"📅 時間: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
1597
1585
|
f"{detail}\n請確認是否為預期變更。"
|
|
1598
1586
|
)
|
|
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)
|
|
1587
|
+
self.send_telegram_notification(tag_msg)
|
|
1605
1588
|
return changes
|
|
1606
1589
|
|
|
1607
1590
|
def detect_ip_change(self, vm_id: int, vm_name: str, current_ip: str):
|
|
@@ -1669,16 +1652,10 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1669
1652
|
tag_ids.append(tag.id)
|
|
1670
1653
|
except Exception as e:
|
|
1671
1654
|
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
1655
|
if self.enhanced_mode:
|
|
1679
|
-
self.detect_new_vm(int(vm_id), original_vm_name, node_name
|
|
1656
|
+
self.detect_new_vm(int(vm_id), original_vm_name, node_name)
|
|
1680
1657
|
if self.enhanced_mode and tag_names:
|
|
1681
|
-
self.detect_tag_changes(int(vm_id), original_vm_name, tag_names
|
|
1658
|
+
self.detect_tag_changes(int(vm_id), original_vm_name, tag_names)
|
|
1682
1659
|
vm_status = vm_data.get('status', '')
|
|
1683
1660
|
if tag_names and vm_status in ('running', 'stopped'):
|
|
1684
1661
|
first_tag = tag_names[0].lower()
|
|
@@ -1761,9 +1738,8 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1761
1738
|
})
|
|
1762
1739
|
if self.enhanced_mode:
|
|
1763
1740
|
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):
|
|
1741
|
+
network_interfaces, current_node=node_name)
|
|
1742
|
+
if not force and not self.should_sync_vm(int(vm_id), vm_config, tag_names, network_interfaces):
|
|
1767
1743
|
cached_vm = self.nb_cache['virtual_machines_by_serial'].get(f"{vm_id}::{cluster['id']}")
|
|
1768
1744
|
if cached_vm is None:
|
|
1769
1745
|
# VM 在 NetBox 中不存在(例如隨節點被連帶刪除),
|
|
@@ -1804,7 +1780,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1804
1780
|
vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
|
|
1805
1781
|
memory=memory, vcpus=vcpus_save, tags=tag_names,
|
|
1806
1782
|
node=node_name, primary_ip=cur_ip, vm_name=original_vm_name,
|
|
1807
|
-
description=
|
|
1783
|
+
description=(vm_config.get('description') or '').strip()[:500],
|
|
1808
1784
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1809
1785
|
)
|
|
1810
1786
|
except Exception as _snap_err:
|
|
@@ -1861,7 +1837,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1861
1837
|
update_data = {
|
|
1862
1838
|
'name': vm_name, 'cluster': cluster['id'], 'device': device.id, 'role': role_id,
|
|
1863
1839
|
'vcpus': vcpus, 'memory': int(vm_config.get('memory', 0)), 'status': status,
|
|
1864
|
-
'description':
|
|
1840
|
+
'description': (vm_config.get('description') or '')[:200], 'platform': platform_id,
|
|
1865
1841
|
'start_on_boot': boot_choice
|
|
1866
1842
|
}
|
|
1867
1843
|
if tag_ids:
|
|
@@ -1880,7 +1856,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1880
1856
|
vm_data_dict = {
|
|
1881
1857
|
'serial': vm_id, 'name': vm_name, 'cluster': cluster['id'], 'device': device.id,
|
|
1882
1858
|
'role': role_id, 'vcpus': vcpus, 'memory': int(vm_config.get('memory', 0)),
|
|
1883
|
-
'status': status, 'description':
|
|
1859
|
+
'status': status, 'description': (vm_config.get('description') or '')[:200],
|
|
1884
1860
|
'platform': platform_id, 'start_on_boot': boot_choice
|
|
1885
1861
|
}
|
|
1886
1862
|
if tag_ids:
|
|
@@ -1917,7 +1893,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1917
1893
|
vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
|
|
1918
1894
|
memory=memory, vcpus=vcpus_save, tags=tag_names,
|
|
1919
1895
|
node=node_name, primary_ip=primary_ip_str, vm_name=original_vm_name,
|
|
1920
|
-
description=
|
|
1896
|
+
description=(vm_config.get('description') or '').strip()[:500],
|
|
1921
1897
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1922
1898
|
)
|
|
1923
1899
|
return True
|
|
@@ -1942,7 +1918,7 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1942
1918
|
node=node_name,
|
|
1943
1919
|
primary_ip=_ip,
|
|
1944
1920
|
vm_name=original_vm_name,
|
|
1945
|
-
description=
|
|
1921
|
+
description=(vm_config.get('description') or '').strip()[:500],
|
|
1946
1922
|
disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
|
|
1947
1923
|
)
|
|
1948
1924
|
except Exception:
|
|
@@ -1985,18 +1961,6 @@ class OptimizedPVEToNetBoxSync:
|
|
|
1985
1961
|
print("=" * 50)
|
|
1986
1962
|
if self.enhanced_mode and self.state_db:
|
|
1987
1963
|
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
1964
|
sync_mode = "增量" if self.sync_config.get('incremental', True) else "全量"
|
|
2001
1965
|
start_message = f"""
|
|
2002
1966
|
🔄 <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.9"
|
|
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.9}/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.9}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py
RENAMED
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.9}/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.9}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py
RENAMED
|
File without changes
|
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.9}/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.9}/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.9}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html
RENAMED
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.9}/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.9}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html
RENAMED
|
File without changes
|
{proxsync-2.0.7 → proxsync-2.0.9}/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
|