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.
Files changed (73) hide show
  1. {proxsync-2.0.7/proxsync.egg-info → proxsync-2.0.9}/PKG-INFO +4 -4
  2. {proxsync-2.0.7 → proxsync-2.0.9/proxsync.egg-info}/PKG-INFO +4 -4
  3. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync.py +93 -129
  4. {proxsync-2.0.7 → proxsync-2.0.9}/pyproject.toml +4 -4
  5. {proxsync-2.0.7 → proxsync-2.0.9}/state_db.py +2 -24
  6. {proxsync-2.0.7 → proxsync-2.0.9}/MANIFEST.in +0 -0
  7. {proxsync-2.0.7 → proxsync-2.0.9}/README.md +0 -0
  8. {proxsync-2.0.7 → proxsync-2.0.9}/config.py +0 -0
  9. {proxsync-2.0.7 → proxsync-2.0.9}/pbs_sync.py +0 -0
  10. {proxsync-2.0.7 → proxsync-2.0.9}/proxsync.egg-info/SOURCES.txt +0 -0
  11. {proxsync-2.0.7 → proxsync-2.0.9}/proxsync.egg-info/dependency_links.txt +0 -0
  12. {proxsync-2.0.7 → proxsync-2.0.9}/proxsync.egg-info/requires.txt +0 -0
  13. {proxsync-2.0.7 → proxsync-2.0.9}/proxsync.egg-info/top_level.txt +0 -0
  14. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/__init__.py +0 -0
  15. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/api/__init__.py +0 -0
  16. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/api/serializers.py +0 -0
  17. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/api/urls.py +0 -0
  18. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/api/views.py +0 -0
  19. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/apps.py +0 -0
  20. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/choices.py +0 -0
  21. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/filtersets.py +0 -0
  22. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/forms.py +0 -0
  23. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/management/__init__.py +0 -0
  24. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/management/commands/__init__.py +0 -0
  25. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/management/commands/pve_sync.py +0 -0
  26. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/management/commands/run_scheduled_syncs.py +0 -0
  27. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0001_initial.py +0 -0
  28. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0002_pvepluginsettings.py +0 -0
  29. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0003_netboxmodel_timestamps.py +0 -0
  30. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0004_model_verbose_names.py +0 -0
  31. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py +0 -0
  32. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py +0 -0
  33. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0007_rename_verbose_names_to_proxmox_sync.py +0 -0
  34. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py +0 -0
  35. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0009_add_every_3h_schedule.py +0 -0
  36. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0010_pvedriftevent.py +0 -0
  37. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py +0 -0
  38. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py +0 -0
  39. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py +0 -0
  40. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0014_vmprovisioninglog.py +0 -0
  41. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0015_pvevmtasklog.py +0 -0
  42. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/0016_pvevmtasklog_status_textfield.py +0 -0
  43. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/migrations/__init__.py +0 -0
  44. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/models.py +0 -0
  45. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/navigation.py +0 -0
  46. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/signals.py +0 -0
  47. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/sync/__init__.py +0 -0
  48. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/sync/config_bridge.py +0 -0
  49. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/sync/engine.py +0 -0
  50. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/tables.py +0 -0
  51. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/tasks.py +0 -0
  52. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/template_content.py +0 -0
  53. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/dashboard.html +0 -0
  54. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html +0 -0
  55. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/inc/vm_task_log_panel.html +0 -0
  56. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pbsserverconfig.html +0 -0
  57. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pvebackupstatus.html +0 -0
  58. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pveclusterconfig.html +0 -0
  59. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pvedriftevent.html +0 -0
  60. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pvesyncjob.html +0 -0
  61. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pvevmtasklog_list.html +0 -0
  62. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/pvewebhookevent.html +0 -0
  63. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/settings.html +0 -0
  64. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html +0 -0
  65. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html +0 -0
  66. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templatetags/__init__.py +0 -0
  67. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/templatetags/pve_sync_tags.py +0 -0
  68. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/urls.py +0 -0
  69. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/utils.py +0 -0
  70. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/view_helpers.py +0 -0
  71. {proxsync-2.0.7 → proxsync-2.0.9}/pve_sync_plugin/views.py +0 -0
  72. {proxsync-2.0.7 → proxsync-2.0.9}/setup.cfg +0 -0
  73. {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.7
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/lixiujie85/proxsync
8
- Project-URL: Source, https://github.com/lixiujie85/proxsync
9
- Project-URL: Issues, https://github.com/lixiujie85/proxsync/issues
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.7
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/lixiujie85/proxsync
8
- Project-URL: Source, https://github.com/lixiujie85/proxsync
9
- Project-URL: Issues, https://github.com/lixiujie85/proxsync/issues
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
- # Max Telegram messages per sync run; excess are counted and reported in the summary.
129
- _MAX_TELEGRAM_PER_SYNC = 20
130
-
131
- def send_telegram_notification(self, message: str) -> bool:
132
- if not getattr(self, 'notify_on_sync', True):
133
- return False
134
- if not self.telegram_bot_token or not self.telegram_chat_id:
135
- return False
136
- # Throttle: cap messages per sync run to avoid flooding on bulk changes.
137
- sent = getattr(self, '_telegram_sent', 0)
138
- if sent >= self._MAX_TELEGRAM_PER_SYNC:
139
- self._telegram_suppressed = getattr(self, '_telegram_suppressed', 0) + 1
140
- return False
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 Exception:
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
- mac_lower = mac_address.lower()
945
- existing_mac = self.nb_cache['mac_addresses'].get(mac_lower)
946
- if existing_mac:
947
- # Already exists — ensure it's assigned to this interface and set as primary
948
- needs_save = False
949
- if getattr(existing_mac, 'assigned_object_id', None) != vm_interface.id:
950
- existing_mac.assigned_object_type = 'virtualization.vminterface'
951
- existing_mac.assigned_object_id = vm_interface.id
952
- existing_mac.save()
953
- # Set primary_mac_address on the interface if not already set
954
- current_primary = getattr(vm_interface, 'primary_mac_address', None)
955
- if not current_primary or (hasattr(current_primary, 'id') and current_primary.id != existing_mac.id):
956
- vm_interface.primary_mac_address = existing_mac.id
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
- else:
959
- # Create new MACAddress assigned to this VMInterface
960
- mac_obj = self.nb_api.dcim.mac_addresses.create(
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, last_config=None):
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 last_config:
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 去重(使用預先撈取的集合,避免 N+1 查詢)
1366
- if upid in existing_upids or upid in new_upids:
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, last_config=None) -> List[Dict]:
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
- if last_config is None:
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 = self._norm_desc(vm_config.get('description'))[:500].strip()
1507
- old_desc = self._norm_desc(last_config.get('description'))[:500].strip()
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, last_config=None) -> bool:
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
- if last_config is None:
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
- if last_config is None:
1568
- last_config = self.state_db.get_last_vm_config(vm_id, self.cluster_name)
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
- notified = self.send_telegram_notification(tag_msg)
1600
- for c in changes:
1601
- if c['type'] == 'added':
1602
- self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', '', c['tag'], notified=notified)
1603
- else:
1604
- self._write_drift_event(vm_name, vm_id, 'tag_change', 'tags', c['tag'], '', notified=notified)
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, last_config=last_config)
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, last_config=last_config)
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, last_config=last_config)
1765
- if not force and not self.should_sync_vm(int(vm_id), vm_config, tag_names, network_interfaces,
1766
- last_config=last_config):
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=self._norm_desc(vm_config.get('description'))[:500].strip(),
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': self._norm_desc(vm_config.get('description'))[:200], 'platform': platform_id,
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': self._norm_desc(vm_config.get('description'))[:200],
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=self._norm_desc(vm_config.get('description'))[:500].strip(),
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=self._norm_desc(vm_config.get('description'))[:500].strip(),
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"
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/lixiujie85/proxsync"
40
- Source = "https://github.com/lixiujie85/proxsync"
41
- Issues = "https://github.com/lixiujie85/proxsync/issues"
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 OR REPLACE INTO vm_config_history
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': (vm_config.get('description') or '').replace('\r\n', '\n').replace('\r', '\n').strip()[:200].strip(),
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