proxsync 2.0.3__tar.gz → 2.0.5__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.3/proxsync.egg-info → proxsync-2.0.5}/PKG-INFO +1 -1
  2. {proxsync-2.0.3 → proxsync-2.0.5/proxsync.egg-info}/PKG-INFO +1 -1
  3. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync.py +39 -8
  4. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/filtersets.py +25 -0
  5. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/views.py +8 -7
  6. {proxsync-2.0.3 → proxsync-2.0.5}/pyproject.toml +1 -1
  7. {proxsync-2.0.3 → proxsync-2.0.5}/state_db.py +2 -2
  8. {proxsync-2.0.3 → proxsync-2.0.5}/MANIFEST.in +0 -0
  9. {proxsync-2.0.3 → proxsync-2.0.5}/README.md +0 -0
  10. {proxsync-2.0.3 → proxsync-2.0.5}/config.py +0 -0
  11. {proxsync-2.0.3 → proxsync-2.0.5}/pbs_sync.py +0 -0
  12. {proxsync-2.0.3 → proxsync-2.0.5}/proxsync.egg-info/SOURCES.txt +0 -0
  13. {proxsync-2.0.3 → proxsync-2.0.5}/proxsync.egg-info/dependency_links.txt +0 -0
  14. {proxsync-2.0.3 → proxsync-2.0.5}/proxsync.egg-info/requires.txt +0 -0
  15. {proxsync-2.0.3 → proxsync-2.0.5}/proxsync.egg-info/top_level.txt +0 -0
  16. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/__init__.py +0 -0
  17. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/api/__init__.py +0 -0
  18. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/api/serializers.py +0 -0
  19. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/api/urls.py +0 -0
  20. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/api/views.py +0 -0
  21. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/apps.py +0 -0
  22. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/choices.py +0 -0
  23. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/forms.py +0 -0
  24. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/management/__init__.py +0 -0
  25. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/management/commands/__init__.py +0 -0
  26. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/management/commands/pve_sync.py +0 -0
  27. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/management/commands/run_scheduled_syncs.py +0 -0
  28. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0001_initial.py +0 -0
  29. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0002_pvepluginsettings.py +0 -0
  30. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0003_netboxmodel_timestamps.py +0 -0
  31. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0004_model_verbose_names.py +0 -0
  32. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py +0 -0
  33. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py +0 -0
  34. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0007_rename_verbose_names_to_proxmox_sync.py +0 -0
  35. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py +0 -0
  36. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0009_add_every_3h_schedule.py +0 -0
  37. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0010_pvedriftevent.py +0 -0
  38. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py +0 -0
  39. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py +0 -0
  40. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py +0 -0
  41. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0014_vmprovisioninglog.py +0 -0
  42. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0015_pvevmtasklog.py +0 -0
  43. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/0016_pvevmtasklog_status_textfield.py +0 -0
  44. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/migrations/__init__.py +0 -0
  45. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/models.py +0 -0
  46. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/navigation.py +0 -0
  47. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/signals.py +0 -0
  48. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/sync/__init__.py +0 -0
  49. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/sync/config_bridge.py +0 -0
  50. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/sync/engine.py +0 -0
  51. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/tables.py +0 -0
  52. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/tasks.py +0 -0
  53. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/template_content.py +0 -0
  54. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/dashboard.html +0 -0
  55. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html +0 -0
  56. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/inc/vm_task_log_panel.html +0 -0
  57. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/pbsserverconfig.html +0 -0
  58. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/pvebackupstatus.html +0 -0
  59. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/pveclusterconfig.html +0 -0
  60. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/pvedriftevent.html +0 -0
  61. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/pvesyncjob.html +0 -0
  62. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/pvevmtasklog_list.html +0 -0
  63. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/pvewebhookevent.html +0 -0
  64. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/settings.html +0 -0
  65. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/vm_provisioning_combined.html +0 -0
  66. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templates/pve_sync/vm_provisioning_log.html +0 -0
  67. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templatetags/__init__.py +0 -0
  68. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/templatetags/pve_sync_tags.py +0 -0
  69. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/urls.py +0 -0
  70. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/utils.py +0 -0
  71. {proxsync-2.0.3 → proxsync-2.0.5}/pve_sync_plugin/view_helpers.py +0 -0
  72. {proxsync-2.0.3 → proxsync-2.0.5}/setup.cfg +0 -0
  73. {proxsync-2.0.3 → proxsync-2.0.5}/tests/test_plugin_contracts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxsync
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: NetBox plugin for Proxmox VE inventory synchronization
5
5
  Author-email: lixiujie85 <lixiujie85@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxsync
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: NetBox plugin for Proxmox VE inventory synchronization
5
5
  Author-email: lixiujie85 <lixiujie85@gmail.com>
6
6
  License-Expression: MIT
@@ -1084,6 +1084,11 @@ class OptimizedPVEToNetBoxSync:
1084
1084
  disks[key] = m.group(1) if m else val_str[:80]
1085
1085
  return disks
1086
1086
 
1087
+ @staticmethod
1088
+ def _norm_desc(s) -> str:
1089
+ """Normalize description: unify line endings, strip whitespace."""
1090
+ return (s or '').replace('\r\n', '\n').replace('\r', '\n').strip()
1091
+
1087
1092
  def _write_drift_event(self, vm_name: str, vmid: int, drift_type: str,
1088
1093
  field_name: str, old_value: str, new_value: str,
1089
1094
  notified: bool = False):
@@ -1235,6 +1240,23 @@ class OptimizedPVEToNetBoxSync:
1235
1240
  print(f"⚠️ 清理孤兒 VM 失敗: {exc}")
1236
1241
  traceback.print_exc()
1237
1242
 
1243
+ def _apply_log_retention(self):
1244
+ """依 PvePluginSettings.log_retention_days 清理過期記錄。"""
1245
+ try:
1246
+ import datetime as _dt
1247
+ from django.utils import timezone
1248
+ from pve_sync_plugin.models import PveDriftEvent, PvePluginSettings, PveVmTaskLog
1249
+ days = PvePluginSettings.load().log_retention_days
1250
+ if not days:
1251
+ return
1252
+ cutoff = timezone.now() - _dt.timedelta(days=days)
1253
+ d_del, _ = PveDriftEvent.objects.filter(created__lt=cutoff).delete()
1254
+ t_del, _ = PveVmTaskLog.objects.filter(start_time__lt=cutoff).delete()
1255
+ if d_del or t_del:
1256
+ print(f" 🧹 Log 清理:刪除 {d_del} 筆漂移事件、{t_del} 筆操作記錄(>{days} 天)")
1257
+ except Exception as exc:
1258
+ print(f" ⚠️ Log 清理失敗: {exc}")
1259
+
1238
1260
  def sync_vm_tasks(self):
1239
1261
  """從 PVE 各節點抓取 VM 操作記錄(建立/刪除/Clone/遷移)並同步到 NetBox。"""
1240
1262
  try:
@@ -1271,7 +1293,15 @@ class OptimizedPVEToNetBoxSync:
1271
1293
  # 只通知最近 2 小時內的操作(避免首次匯入時大量通知)
1272
1294
  notify_cutoff = time.time() - 7200
1273
1295
 
1274
- since_ts = int(time.time()) - 30 * 86400 # 抓最近 30 天
1296
+ # 從上次已存記錄的最新時間起算(+1 秒避免邊界重複);首次則抓 30 天
1297
+ latest = PveVmTaskLog.objects.filter(
1298
+ cluster_name=self.cluster_name
1299
+ ).order_by("-start_time").values_list("start_time", flat=True).first()
1300
+ if latest:
1301
+ import calendar
1302
+ since_ts = int(calendar.timegm(latest.utctimetuple())) + 1
1303
+ else:
1304
+ since_ts = int(time.time()) - 30 * 86400
1275
1305
  new_count = 0
1276
1306
 
1277
1307
  for node_name in self.pve_cache.get("vms_by_node", {}):
@@ -1436,8 +1466,8 @@ class OptimizedPVEToNetBoxSync:
1436
1466
  self._write_drift_event(vm_name, vm_id, 'disk_change', k, old_v, new_v, notified=True)
1437
1467
 
1438
1468
  # VM 描述變更偵測
1439
- current_desc = (vm_config.get('description') or '').strip()[:500]
1440
- old_desc = (last_config.get('description') or '').strip()
1469
+ current_desc = self._norm_desc(vm_config.get('description'))[:500]
1470
+ old_desc = self._norm_desc(last_config.get('description'))
1441
1471
  if old_desc and old_desc != current_desc:
1442
1472
  self.stats['config_drifts_detected'] += 1
1443
1473
  print(f"📝 VM {vm_name} 描述變更")
@@ -1724,7 +1754,7 @@ class OptimizedPVEToNetBoxSync:
1724
1754
  vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
1725
1755
  memory=memory, vcpus=vcpus_save, tags=tag_names,
1726
1756
  node=node_name, primary_ip=cur_ip, vm_name=original_vm_name,
1727
- description=(vm_config.get('description') or '').strip()[:500],
1757
+ description=self._norm_desc(vm_config.get('description'))[:500],
1728
1758
  disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
1729
1759
  )
1730
1760
  except Exception as _snap_err:
@@ -1781,7 +1811,7 @@ class OptimizedPVEToNetBoxSync:
1781
1811
  update_data = {
1782
1812
  'name': vm_name, 'cluster': cluster['id'], 'device': device.id, 'role': role_id,
1783
1813
  'vcpus': vcpus, 'memory': int(vm_config.get('memory', 0)), 'status': status,
1784
- 'description': (vm_config.get('description') or '')[:200], 'platform': platform_id,
1814
+ 'description': self._norm_desc(vm_config.get('description'))[:200], 'platform': platform_id,
1785
1815
  'start_on_boot': boot_choice
1786
1816
  }
1787
1817
  if tag_ids:
@@ -1800,7 +1830,7 @@ class OptimizedPVEToNetBoxSync:
1800
1830
  vm_data_dict = {
1801
1831
  'serial': vm_id, 'name': vm_name, 'cluster': cluster['id'], 'device': device.id,
1802
1832
  'role': role_id, 'vcpus': vcpus, 'memory': int(vm_config.get('memory', 0)),
1803
- 'status': status, 'description': (vm_config.get('description') or '')[:200],
1833
+ 'status': status, 'description': self._norm_desc(vm_config.get('description'))[:200],
1804
1834
  'platform': platform_id, 'start_on_boot': boot_choice
1805
1835
  }
1806
1836
  if tag_ids:
@@ -1837,7 +1867,7 @@ class OptimizedPVEToNetBoxSync:
1837
1867
  vm_id=int(vm_id), cluster_name=self.cluster_name, config_hash=config_hash,
1838
1868
  memory=memory, vcpus=vcpus_save, tags=tag_names,
1839
1869
  node=node_name, primary_ip=primary_ip_str, vm_name=original_vm_name,
1840
- description=(vm_config.get('description') or '').strip()[:500],
1870
+ description=self._norm_desc(vm_config.get('description'))[:500],
1841
1871
  disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
1842
1872
  )
1843
1873
  return True
@@ -1862,7 +1892,7 @@ class OptimizedPVEToNetBoxSync:
1862
1892
  node=node_name,
1863
1893
  primary_ip=_ip,
1864
1894
  vm_name=original_vm_name,
1865
- description=(vm_config.get('description') or '').strip()[:500],
1895
+ description=self._norm_desc(vm_config.get('description'))[:500],
1866
1896
  disk_summary=json.dumps(self._parse_disk_summary(vm_config)),
1867
1897
  )
1868
1898
  except Exception:
@@ -1931,6 +1961,7 @@ class OptimizedPVEToNetBoxSync:
1931
1961
  vms_success, success_count, total_count = self.sync_pve_virtual_machines(devices, cluster)
1932
1962
  print("\n同步 VM 操作記錄...")
1933
1963
  self.sync_vm_tasks()
1964
+ self._apply_log_retention()
1934
1965
  elapsed = time.time() - start_time
1935
1966
  self.stats['elapsed_time'] = elapsed
1936
1967
  self.stats['total_vms'] = total_count
@@ -10,6 +10,7 @@ from .choices import (
10
10
  SyncJobStatusChoices,
11
11
  SyncJobTriggerChoices,
12
12
  SyncScheduleChoices,
13
+ VmTaskTypeChoices,
13
14
  WebhookEventChoices,
14
15
  )
15
16
  from .models import (
@@ -18,6 +19,7 @@ from .models import (
18
19
  PveClusterConfig,
19
20
  PveDriftEvent,
20
21
  PveSyncJob,
22
+ PveVmTaskLog,
21
23
  PveWebhookEvent,
22
24
  )
23
25
 
@@ -120,6 +122,29 @@ class PveDriftEventFilterSet(NetBoxModelFilterSet):
120
122
  return queryset.filter(vm_name__icontains=value) | queryset.filter(cluster_name__icontains=value)
121
123
 
122
124
 
125
+ class PveVmTaskLogFilterSet(NetBoxModelFilterSet):
126
+ """Filter VM task logs by type, operator, cluster, and VM."""
127
+
128
+ task_type = django_filters.MultipleChoiceFilter(choices=VmTaskTypeChoices.choices)
129
+ vmid = django_filters.NumberFilter()
130
+ cluster_name = django_filters.CharFilter(lookup_expr="icontains")
131
+ operator = django_filters.CharFilter(lookup_expr="icontains")
132
+ node = django_filters.CharFilter(lookup_expr="icontains")
133
+
134
+ class Meta:
135
+ model = PveVmTaskLog
136
+ fields = ["id", "vmid", "task_type", "cluster_name", "operator", "node"]
137
+
138
+ def search(self, queryset, name, value):
139
+ if not value.strip():
140
+ return queryset
141
+ return (
142
+ queryset.filter(vm_name__icontains=value)
143
+ | queryset.filter(operator__icontains=value)
144
+ | queryset.filter(cluster_name__icontains=value)
145
+ )
146
+
147
+
123
148
  class PveBackupStatusFilterSet(NetBoxModelFilterSet):
124
149
  """Filter backup statuses by state."""
125
150
 
@@ -30,6 +30,7 @@ from .filtersets import (
30
30
  PveClusterConfigFilterSet,
31
31
  PveDriftEventFilterSet,
32
32
  PveSyncJobFilterSet,
33
+ PveVmTaskLogFilterSet,
33
34
  PveWebhookEventFilterSet,
34
35
  )
35
36
  from .forms import (
@@ -351,6 +352,7 @@ class PveDriftEventBulkDeleteView(generic.BulkDeleteView):
351
352
  class PveVmTaskLogListView(generic.ObjectListView):
352
353
  queryset = PveVmTaskLog.objects.order_by("-start_time")
353
354
  table = PveVmTaskLogTable
355
+ filterset = PveVmTaskLogFilterSet
354
356
  template_name = "pve_sync/pvevmtasklog_list.html"
355
357
 
356
358
 
@@ -666,13 +668,12 @@ class VmPlannerCheckIpApi(PermissionRequiredMixin, View):
666
668
  ip_str = str(ip_obj)
667
669
 
668
670
  # ── 1. IPAM check (authoritative) ──
669
- # An IP can be stored as any prefix length (e.g. /16, /24, /32).
670
- # Compare only the host part to find any existing allocation.
671
- ipam_entry = None
672
- for addr in NetBoxIPAddress.objects.all().values_list("address", flat=True):
673
- if str(addr.ip) == ip_str:
674
- ipam_entry = str(addr) # e.g. "172.17.202.11/16"
675
- break
671
+ # Filter by host bits using __net_host_contained: finds any /N record
672
+ # whose host address matches ip_str, without a full table scan.
673
+ ipam_qs = NetBoxIPAddress.objects.filter(
674
+ address__net_host_contained=ip_str
675
+ ).values_list("address", flat=True).first()
676
+ ipam_entry = str(ipam_qs) if ipam_qs else None
676
677
 
677
678
  if ipam_entry:
678
679
  return JsonResponse({
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proxsync"
7
- version = "2.0.3"
7
+ version = "2.0.5"
8
8
  description = "NetBox plugin for Proxmox VE inventory synchronization"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -154,7 +154,7 @@ class StateDB:
154
154
  with sqlite3.connect(self.db_path) as conn:
155
155
  conn.execute(
156
156
  """
157
- INSERT INTO vm_config_history
157
+ INSERT OR REPLACE INTO vm_config_history
158
158
  (vm_id, cluster_name, config_hash, memory, vcpus, tags_json,
159
159
  node, primary_ip, vm_name, description, disk_summary)
160
160
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -507,7 +507,7 @@ def compute_config_hash(vm_config: Dict[str, Any], tags: List[str], network_inte
507
507
  'ostype': vm_config.get('ostype', ''),
508
508
  'cores': vm_config.get('cores', 1),
509
509
  'sockets': vm_config.get('sockets', 1),
510
- 'description': vm_config.get('description', '')[:200], # 限制长度
510
+ 'description': (vm_config.get('description') or '').replace('\r\n', '\n').replace('\r', '\n').strip()[:200],
511
511
  'tags': sorted(tags), # 排序以确保一致性
512
512
  'networks': net_config # 添加网络接口配置
513
513
  }
File without changes
File without changes
File without changes
File without changes
File without changes