proxsync 2.0.0__py3-none-any.whl

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 (59) hide show
  1. config.py +337 -0
  2. pbs_sync.py +244 -0
  3. proxsync-2.0.0.dist-info/METADATA +468 -0
  4. proxsync-2.0.0.dist-info/RECORD +59 -0
  5. proxsync-2.0.0.dist-info/WHEEL +5 -0
  6. proxsync-2.0.0.dist-info/top_level.txt +5 -0
  7. pve_sync.py +1804 -0
  8. pve_sync_plugin/__init__.py +46 -0
  9. pve_sync_plugin/api/__init__.py +1 -0
  10. pve_sync_plugin/api/serializers.py +161 -0
  11. pve_sync_plugin/api/urls.py +27 -0
  12. pve_sync_plugin/api/views.py +94 -0
  13. pve_sync_plugin/apps.py +5 -0
  14. pve_sync_plugin/choices.py +57 -0
  15. pve_sync_plugin/filtersets.py +137 -0
  16. pve_sync_plugin/forms.py +219 -0
  17. pve_sync_plugin/management/__init__.py +1 -0
  18. pve_sync_plugin/management/commands/__init__.py +1 -0
  19. pve_sync_plugin/management/commands/pve_sync.py +182 -0
  20. pve_sync_plugin/management/commands/run_scheduled_syncs.py +109 -0
  21. pve_sync_plugin/migrations/0001_initial.py +234 -0
  22. pve_sync_plugin/migrations/0002_pvepluginsettings.py +42 -0
  23. pve_sync_plugin/migrations/0003_netboxmodel_timestamps.py +148 -0
  24. pve_sync_plugin/migrations/0004_model_verbose_names.py +34 -0
  25. pve_sync_plugin/migrations/0005_align_netboxmodel_timestamps.py +111 -0
  26. pve_sync_plugin/migrations/0006_pbsserverconfig_and_more.py +124 -0
  27. pve_sync_plugin/migrations/0007_rename_verbose_names_to_proxmox_sync.py +26 -0
  28. pve_sync_plugin/migrations/0008_pbsserverconfig_sync_schedule.py +26 -0
  29. pve_sync_plugin/migrations/0009_add_every_3h_schedule.py +39 -0
  30. pve_sync_plugin/migrations/0010_pvedriftevent.py +62 -0
  31. pve_sync_plugin/migrations/0011_pveclusterconfig_notify_on_sync.py +19 -0
  32. pve_sync_plugin/migrations/0012_pvedriftevent_tags_and_index_renames.py +39 -0
  33. pve_sync_plugin/migrations/0013_pluginsettings_log_retention.py +19 -0
  34. pve_sync_plugin/migrations/__init__.py +1 -0
  35. pve_sync_plugin/models.py +461 -0
  36. pve_sync_plugin/navigation.py +94 -0
  37. pve_sync_plugin/signals.py +24 -0
  38. pve_sync_plugin/sync/__init__.py +8 -0
  39. pve_sync_plugin/sync/config_bridge.py +221 -0
  40. pve_sync_plugin/sync/engine.py +50 -0
  41. pve_sync_plugin/tables.py +278 -0
  42. pve_sync_plugin/tasks.py +440 -0
  43. pve_sync_plugin/template_content.py +44 -0
  44. pve_sync_plugin/templates/pve_sync/dashboard.html +400 -0
  45. pve_sync_plugin/templates/pve_sync/inc/vm_sync_button.html +7 -0
  46. pve_sync_plugin/templates/pve_sync/pbsserverconfig.html +69 -0
  47. pve_sync_plugin/templates/pve_sync/pvebackupstatus.html +27 -0
  48. pve_sync_plugin/templates/pve_sync/pveclusterconfig.html +52 -0
  49. pve_sync_plugin/templates/pve_sync/pvedriftevent.html +89 -0
  50. pve_sync_plugin/templates/pve_sync/pvesyncjob.html +197 -0
  51. pve_sync_plugin/templates/pve_sync/pvewebhookevent.html +42 -0
  52. pve_sync_plugin/templates/pve_sync/settings.html +79 -0
  53. pve_sync_plugin/templatetags/__init__.py +1 -0
  54. pve_sync_plugin/templatetags/pve_sync_tags.py +86 -0
  55. pve_sync_plugin/urls.py +186 -0
  56. pve_sync_plugin/utils.py +84 -0
  57. pve_sync_plugin/view_helpers.py +98 -0
  58. pve_sync_plugin/views.py +535 -0
  59. state_db.py +543 -0
@@ -0,0 +1,46 @@
1
+ """NetBox plugin entrypoint for Proxmox Sync."""
2
+
3
+ from netbox.plugins import PluginConfig
4
+
5
+ __version__ = "2.1.0"
6
+
7
+
8
+ class PveSyncPluginConfig(PluginConfig):
9
+
10
+ name = "pve_sync_plugin"
11
+ verbose_name = "Proxmox Sync"
12
+ description = "Synchronize Proxmox VE and Proxmox Backup Server inventory into NetBox."
13
+ author = "Proxmox Sync Maintainers"
14
+ author_email = ""
15
+ version = __version__
16
+ base_url = "pve-sync"
17
+ min_version = "4.5.0"
18
+ max_version = "4.6.99"
19
+
20
+ middleware = []
21
+ queues = ["default"]
22
+
23
+ required_settings = []
24
+ default_settings = {
25
+ "netbox_url": "",
26
+ "netbox_token": "",
27
+ "telegram_bot_token": "",
28
+ "telegram_chat_id": "",
29
+ "webhook_secret": "",
30
+ "default_cluster_name": "default",
31
+ "default_netbox_cluster": "Proxmox Cluster",
32
+ "default_site": "Main Datacenter",
33
+ "default_cluster_type": "Proxmox",
34
+ "default_node_role": "PVE",
35
+ "default_node_type": "Standard Server",
36
+ "state_db_path": "/var/lib/netbox/pve-sync-state.db",
37
+ "enable_backup_sync": True,
38
+ }
39
+
40
+ def ready(self):
41
+ super().ready()
42
+ from pve_sync_plugin.signals import register_signals
43
+ register_signals()
44
+
45
+
46
+ config = PveSyncPluginConfig
@@ -0,0 +1 @@
1
+ """PVE Sync Plugin API package."""
@@ -0,0 +1,161 @@
1
+ """API serializers for PVE Sync plugin models."""
2
+
3
+ from rest_framework import serializers
4
+
5
+ from netbox.api.serializers import NetBoxModelSerializer
6
+
7
+ from pve_sync_plugin.models import (
8
+ PveBackupStatus,
9
+ PveClusterConfig,
10
+ PvePluginSettings,
11
+ PveSyncJob,
12
+ PveWebhookEvent,
13
+ )
14
+
15
+
16
+ class PveSyncJobSerializer(NetBoxModelSerializer):
17
+ url = serializers.HyperlinkedIdentityField(
18
+ view_name="plugins-api:pve_sync_plugin-api:pvesyncjob-detail",
19
+ )
20
+ duration = serializers.FloatField(read_only=True)
21
+ success_rate = serializers.FloatField(read_only=True)
22
+
23
+ class Meta:
24
+ model = PveSyncJob
25
+ fields = [
26
+ "id",
27
+ "url",
28
+ "display",
29
+ "cluster_name",
30
+ "status",
31
+ "trigger",
32
+ "start_time",
33
+ "end_time",
34
+ "duration",
35
+ "total_vms",
36
+ "success_vms",
37
+ "failed_vms",
38
+ "success_rate",
39
+ "nodes_offline",
40
+ "config_drifts",
41
+ "tag_changes",
42
+ "resource_alerts",
43
+ "details",
44
+ "triggered_by",
45
+ "created",
46
+ "last_updated",
47
+ ]
48
+
49
+
50
+ class PveWebhookEventSerializer(NetBoxModelSerializer):
51
+ url = serializers.HyperlinkedIdentityField(
52
+ view_name="plugins-api:pve_sync_plugin-api:pvewebhookevent-detail",
53
+ )
54
+
55
+ class Meta:
56
+ model = PveWebhookEvent
57
+ fields = [
58
+ "id",
59
+ "url",
60
+ "display",
61
+ "event_type",
62
+ "node",
63
+ "vmid",
64
+ "vm_name",
65
+ "raw_data",
66
+ "processed",
67
+ "processed_at",
68
+ "processing_error",
69
+ "sync_job",
70
+ "received_at",
71
+ "created",
72
+ "last_updated",
73
+ ]
74
+
75
+
76
+ class PveClusterConfigSerializer(NetBoxModelSerializer):
77
+ url = serializers.HyperlinkedIdentityField(
78
+ view_name="plugins-api:pve_sync_plugin-api:pveclusterconfig-detail",
79
+ )
80
+
81
+ class Meta:
82
+ model = PveClusterConfig
83
+ fields = [
84
+ "id",
85
+ "url",
86
+ "display",
87
+ "name",
88
+ "description",
89
+ "pve_host",
90
+ "pve_user",
91
+ "pve_token",
92
+ "pve_secret",
93
+ "pve_verify_ssl",
94
+ "netbox_site",
95
+ "netbox_cluster_type",
96
+ "netbox_cluster",
97
+ "enabled",
98
+ "sync_schedule",
99
+ "last_sync",
100
+ "last_sync_status",
101
+ "created",
102
+ "last_updated",
103
+ ]
104
+ # Never expose credentials in API responses
105
+ extra_kwargs = {
106
+ "pve_token": {"write_only": True},
107
+ "pve_secret": {"write_only": True},
108
+ }
109
+
110
+
111
+ class PveBackupStatusSerializer(NetBoxModelSerializer):
112
+ url = serializers.HyperlinkedIdentityField(
113
+ view_name="plugins-api:pve_sync_plugin-api:pvebackupstatus-detail",
114
+ )
115
+ backup_age_days = serializers.IntegerField(read_only=True)
116
+ is_stale = serializers.BooleanField(read_only=True)
117
+
118
+ class Meta:
119
+ model = PveBackupStatus
120
+ fields = [
121
+ "id",
122
+ "url",
123
+ "display",
124
+ "vm",
125
+ "last_backup",
126
+ "backup_size",
127
+ "backup_status",
128
+ "backup_path",
129
+ "next_backup",
130
+ "pve_backup_id",
131
+ "backup_age_days",
132
+ "is_stale",
133
+ "created",
134
+ "last_updated",
135
+ ]
136
+
137
+
138
+ class PvePluginSettingsSerializer(NetBoxModelSerializer):
139
+ url = serializers.HyperlinkedIdentityField(
140
+ view_name="plugins-api:pve_sync_plugin-api:pvepluginsettings-detail",
141
+ )
142
+
143
+ class Meta:
144
+ model = PvePluginSettings
145
+ fields = [
146
+ "id",
147
+ "url",
148
+ "pve_api_host",
149
+ "pve_api_user",
150
+ "pve_api_verify_ssl",
151
+ "netbox_url",
152
+ "default_cluster_name",
153
+ "default_netbox_cluster",
154
+ "default_site",
155
+ "default_cluster_type",
156
+ "default_node_role",
157
+ "default_node_type",
158
+ "state_db_path",
159
+ "enable_backup_sync",
160
+ ]
161
+ # Never expose secrets via API
@@ -0,0 +1,27 @@
1
+ """NetBox REST API routes for the PVE Sync plugin.
2
+
3
+ Mounted by NetBox under /api/plugins/pve-sync/.
4
+ """
5
+
6
+ from django.urls import path
7
+ from netbox.api.routers import NetBoxRouter
8
+
9
+ from pve_sync_plugin import views as plugin_views
10
+
11
+ from . import views as api_views
12
+
13
+ app_name = "pve_sync_plugin-api"
14
+
15
+ router = NetBoxRouter()
16
+ router.APIRootView.cls_name = "PveSyncPluginRootView"
17
+
18
+ router.register("jobs", api_views.PveSyncJobViewSet)
19
+ router.register("events", api_views.PveWebhookEventViewSet)
20
+ router.register("clusters", api_views.PveClusterConfigViewSet)
21
+ router.register("backup-status", api_views.PveBackupStatusViewSet)
22
+ router.register("settings", api_views.PvePluginSettingsViewSet)
23
+
24
+ urlpatterns = [
25
+ path("webhook/", plugin_views.webhook_receiver, name="webhook"),
26
+ *router.urls,
27
+ ]
@@ -0,0 +1,94 @@
1
+ """API ViewSets for PVE Sync plugin models."""
2
+
3
+ from rest_framework.decorators import action
4
+ from rest_framework.response import Response
5
+ from rest_framework import status as drf_status
6
+
7
+ from netbox.api.viewsets import NetBoxModelViewSet
8
+
9
+ from pve_sync_plugin.filtersets import (
10
+ PveBackupStatusFilterSet,
11
+ PveClusterConfigFilterSet,
12
+ PveSyncJobFilterSet,
13
+ PveWebhookEventFilterSet,
14
+ )
15
+ from pve_sync_plugin.models import (
16
+ PveBackupStatus,
17
+ PveClusterConfig,
18
+ PvePluginSettings,
19
+ PveSyncJob,
20
+ PveWebhookEvent,
21
+ )
22
+
23
+ from .serializers import (
24
+ PveBackupStatusSerializer,
25
+ PveClusterConfigSerializer,
26
+ PvePluginSettingsSerializer,
27
+ PveSyncJobSerializer,
28
+ PveWebhookEventSerializer,
29
+ )
30
+
31
+
32
+ class PveSyncJobViewSet(NetBoxModelViewSet):
33
+ queryset = PveSyncJob.objects.all()
34
+ serializer_class = PveSyncJobSerializer
35
+ filterset_class = PveSyncJobFilterSet
36
+
37
+ @action(detail=False, methods=["post"], url_path="trigger")
38
+ def trigger_sync(self, request):
39
+ """POST /api/plugins/pve-sync/jobs/trigger/ — start a new sync."""
40
+ from pve_sync_plugin.tasks import enqueue_sync
41
+
42
+ cluster_name = request.data.get("cluster", "default")
43
+
44
+ if cluster_name != "default":
45
+ if not PveClusterConfig.objects.filter(
46
+ name=cluster_name, enabled=True
47
+ ).exists():
48
+ return Response(
49
+ {"detail": f"Cluster '{cluster_name}' not found or disabled."},
50
+ status=drf_status.HTTP_404_NOT_FOUND,
51
+ )
52
+
53
+ job = PveSyncJob.objects.create(
54
+ cluster_name=cluster_name,
55
+ status="pending",
56
+ trigger="api",
57
+ triggered_by=request.user if request.user.is_authenticated else None,
58
+ details={},
59
+ )
60
+
61
+ try:
62
+ enqueue_sync(job)
63
+ except Exception as exc:
64
+ return Response(
65
+ {"detail": f"Failed to enqueue: {exc}"},
66
+ status=drf_status.HTTP_500_INTERNAL_SERVER_ERROR,
67
+ )
68
+
69
+ job.refresh_from_db()
70
+ serializer = self.get_serializer(job)
71
+ return Response(serializer.data, status=drf_status.HTTP_202_ACCEPTED)
72
+
73
+
74
+ class PveWebhookEventViewSet(NetBoxModelViewSet):
75
+ queryset = PveWebhookEvent.objects.all()
76
+ serializer_class = PveWebhookEventSerializer
77
+ filterset_class = PveWebhookEventFilterSet
78
+
79
+
80
+ class PveClusterConfigViewSet(NetBoxModelViewSet):
81
+ queryset = PveClusterConfig.objects.all()
82
+ serializer_class = PveClusterConfigSerializer
83
+ filterset_class = PveClusterConfigFilterSet
84
+
85
+
86
+ class PveBackupStatusViewSet(NetBoxModelViewSet):
87
+ queryset = PveBackupStatus.objects.select_related("vm")
88
+ serializer_class = PveBackupStatusSerializer
89
+ filterset_class = PveBackupStatusFilterSet
90
+
91
+
92
+ class PvePluginSettingsViewSet(NetBoxModelViewSet):
93
+ queryset = PvePluginSettings.objects.all()
94
+ serializer_class = PvePluginSettingsSerializer
@@ -0,0 +1,5 @@
1
+ """
2
+ Compatibility shim — NetBox uses the PluginConfig defined in __init__.py.
3
+ This file is kept so Django does not auto-discover a conflicting AppConfig.
4
+ """
5
+ from pve_sync_plugin import PveSyncPluginConfig # noqa: F401
@@ -0,0 +1,57 @@
1
+ """Enumerated choices for PVE Sync plugin models."""
2
+
3
+ from django.db import models
4
+
5
+
6
+ class SyncJobStatusChoices(models.TextChoices):
7
+ STATUS_PENDING = "pending", "Pending"
8
+ STATUS_RUNNING = "running", "Running"
9
+ STATUS_SUCCESS = "success", "Success"
10
+ STATUS_FAILED = "failed", "Failed"
11
+ STATUS_PARTIAL = "partial", "Partial Success"
12
+
13
+
14
+ class SyncJobTriggerChoices(models.TextChoices):
15
+ TRIGGER_MANUAL = "manual", "Manual"
16
+ TRIGGER_SCHEDULED = "scheduled", "Scheduled"
17
+ TRIGGER_WEBHOOK = "webhook", "Webhook"
18
+ TRIGGER_API = "api", "API Call"
19
+
20
+
21
+ class WebhookEventChoices(models.TextChoices):
22
+ VM_STARTED = "vm-started", "VM Started"
23
+ VM_STOPPED = "vm-stopped", "VM Stopped"
24
+ VM_MIGRATED = "vm-migrated", "VM Migrated"
25
+ NODE_ONLINE = "node-online", "Node Online"
26
+ NODE_OFFLINE = "node-offline", "Node Offline"
27
+ BACKUP_DONE = "backup-done", "Backup Completed"
28
+ BACKUP_FAILED = "backup-failed", "Backup Failed"
29
+ CONFIG_CHANGE = "configuration-change", "Configuration Change"
30
+
31
+
32
+ class BackupStatusChoices(models.TextChoices):
33
+ STATUS_SUCCESS = "success", "Success"
34
+ STATUS_FAILED = "failed", "Failed"
35
+ STATUS_RUNNING = "running", "Running"
36
+ STATUS_UNKNOWN = "unknown", "Unknown"
37
+
38
+
39
+ class SyncScheduleChoices(models.TextChoices):
40
+ DISABLED = "disabled", "Disabled"
41
+ HOURLY = "hourly", "Hourly"
42
+ EVERY_3H = "every_3h", "Every 3 hours"
43
+ EVERY_6H = "every_6h", "Every 6 hours"
44
+ DAILY = "daily", "Daily"
45
+ WEEKLY = "weekly", "Weekly"
46
+
47
+
48
+ class DriftTypeChoices(models.TextChoices):
49
+ HARDWARE = "hardware", "硬體配置變更"
50
+ MIGRATION = "migration", "VM 遷移"
51
+ IP_CHANGE = "ip_change", "IP 位址變更"
52
+ TAG_CHANGE = "tag_change", "標籤變更"
53
+ VM_CREATED = "vm_created", "VM 新增"
54
+ VM_DELETED = "vm_deleted", "VM 刪除"
55
+ VM_RENAMED = "vm_renamed", "VM 更名"
56
+ DISK_CHANGE = "disk_change", "磁碟配置變更"
57
+ DESCRIPTION_CHANGE = "description_change", "描述變更"
@@ -0,0 +1,137 @@
1
+ """FilterSets for PVE Sync plugin models (used by list views and API)."""
2
+
3
+ import django_filters
4
+
5
+ from netbox.filtersets import NetBoxModelFilterSet
6
+
7
+ from .choices import (
8
+ BackupStatusChoices,
9
+ DriftTypeChoices,
10
+ SyncJobStatusChoices,
11
+ SyncJobTriggerChoices,
12
+ SyncScheduleChoices,
13
+ WebhookEventChoices,
14
+ )
15
+ from .models import (
16
+ PbsServerConfig,
17
+ PveBackupStatus,
18
+ PveClusterConfig,
19
+ PveDriftEvent,
20
+ PveSyncJob,
21
+ PveWebhookEvent,
22
+ )
23
+
24
+
25
+ class PveSyncJobFilterSet(NetBoxModelFilterSet):
26
+ """Filter sync jobs by status, trigger, cluster, and time range."""
27
+
28
+ status = django_filters.MultipleChoiceFilter(choices=SyncJobStatusChoices.choices)
29
+ trigger = django_filters.MultipleChoiceFilter(choices=SyncJobTriggerChoices.choices)
30
+ cluster_name = django_filters.CharFilter(lookup_expr="icontains")
31
+ start_time_after = django_filters.DateTimeFilter(
32
+ field_name="start_time", lookup_expr="gte"
33
+ )
34
+ start_time_before = django_filters.DateTimeFilter(
35
+ field_name="start_time", lookup_expr="lte"
36
+ )
37
+
38
+ class Meta:
39
+ model = PveSyncJob
40
+ fields = ["id", "status", "trigger", "cluster_name"]
41
+
42
+ def search(self, queryset, name, value):
43
+ if not value.strip():
44
+ return queryset
45
+ return queryset.filter(cluster_name__icontains=value)
46
+
47
+
48
+ class PveWebhookEventFilterSet(NetBoxModelFilterSet):
49
+ """Filter webhook events by type, processing state, and VM."""
50
+
51
+ event_type = django_filters.MultipleChoiceFilter(
52
+ choices=WebhookEventChoices.choices
53
+ )
54
+ processed = django_filters.BooleanFilter()
55
+ vmid = django_filters.NumberFilter()
56
+ node = django_filters.CharFilter(lookup_expr="icontains")
57
+
58
+ class Meta:
59
+ model = PveWebhookEvent
60
+ fields = ["id", "event_type", "processed", "vmid", "node"]
61
+
62
+ def search(self, queryset, name, value):
63
+ if not value.strip():
64
+ return queryset
65
+ return queryset.filter(vm_name__icontains=value)
66
+
67
+
68
+ class PveClusterConfigFilterSet(NetBoxModelFilterSet):
69
+ """Filter cluster configs by name, enabled state, and schedule."""
70
+
71
+ enabled = django_filters.BooleanFilter()
72
+ sync_schedule = django_filters.MultipleChoiceFilter(
73
+ choices=SyncScheduleChoices.choices
74
+ )
75
+
76
+ class Meta:
77
+ model = PveClusterConfig
78
+ fields = ["id", "name", "enabled", "sync_schedule"]
79
+
80
+ def search(self, queryset, name, value):
81
+ if not value.strip():
82
+ return queryset
83
+ return queryset.filter(name__icontains=value) | queryset.filter(
84
+ description__icontains=value
85
+ )
86
+
87
+
88
+ class PbsServerConfigFilterSet(NetBoxModelFilterSet):
89
+ """Filter PBS server configs by name and enabled state."""
90
+
91
+ enabled = django_filters.BooleanFilter()
92
+
93
+ class Meta:
94
+ model = PbsServerConfig
95
+ fields = ["id", "name", "enabled"]
96
+
97
+ def search(self, queryset, name, value):
98
+ if not value.strip():
99
+ return queryset
100
+ return queryset.filter(name__icontains=value) | queryset.filter(
101
+ description__icontains=value
102
+ )
103
+
104
+
105
+ class PveDriftEventFilterSet(NetBoxModelFilterSet):
106
+ """Filter drift events by type, VM name, cluster, and notification state."""
107
+
108
+ drift_type = django_filters.MultipleChoiceFilter(choices=DriftTypeChoices.choices)
109
+ vm_name = django_filters.CharFilter(lookup_expr="icontains")
110
+ cluster_name = django_filters.CharFilter(lookup_expr="icontains")
111
+ notified_telegram = django_filters.BooleanFilter()
112
+
113
+ class Meta:
114
+ model = PveDriftEvent
115
+ fields = ["id", "drift_type", "vmid", "cluster_name", "notified_telegram"]
116
+
117
+ def search(self, queryset, name, value):
118
+ if not value.strip():
119
+ return queryset
120
+ return queryset.filter(vm_name__icontains=value) | queryset.filter(cluster_name__icontains=value)
121
+
122
+
123
+ class PveBackupStatusFilterSet(NetBoxModelFilterSet):
124
+ """Filter backup statuses by state."""
125
+
126
+ backup_status = django_filters.MultipleChoiceFilter(
127
+ choices=BackupStatusChoices.choices
128
+ )
129
+
130
+ class Meta:
131
+ model = PveBackupStatus
132
+ fields = ["id", "backup_status"]
133
+
134
+ def search(self, queryset, name, value):
135
+ if not value.strip():
136
+ return queryset
137
+ return queryset.filter(vm__name__icontains=value)