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,221 @@
1
+ """Config bridge: translates plugin DB settings into the format expected by
2
+ the standalone sync engine (OptimizedPVEToNetBoxSync).
3
+
4
+ This module allows the sync engine to be driven either from:
5
+ 1. Plugin DB models (PvePluginSettings / PveClusterConfig)
6
+ 2. A YAML config file + env vars (standalone mode)
7
+ """
8
+
9
+ import logging
10
+ import os
11
+ import tempfile
12
+
13
+ import yaml
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Environment variable keys managed by the config bridge.
18
+ # Used for setup and cleanup to avoid cross-worker pollution.
19
+ _ENV_KEYS = [
20
+ "PVE_SYNC_CONFIG_FILE",
21
+ "PVE_API_HOST",
22
+ "PVE_API_USER",
23
+ "PVE_API_TOKEN",
24
+ "PVE_API_SECRET",
25
+ "PVE_API_VERIFY_SSL",
26
+ "NB_API_URL",
27
+ "NB_API_TOKEN",
28
+ "TELEGRAM_BOT_TOKEN",
29
+ "TELEGRAM_CHAT_ID",
30
+ ]
31
+
32
+
33
+ class ConfigValidationError(Exception):
34
+ """Raised when required configuration values are missing."""
35
+
36
+
37
+ def validate_config(config_data):
38
+ """Validate that required fields are present and non-empty.
39
+
40
+ Raises:
41
+ ConfigValidationError: if critical connection details are missing.
42
+ """
43
+ cluster = config_data.get("clusters", [{}])[0]
44
+ pve = cluster.get("pve", {})
45
+ netbox = cluster.get("netbox", {})
46
+
47
+ missing = []
48
+ if not pve.get("host"):
49
+ missing.append("pve.host (PVE API host)")
50
+ if not pve.get("token"):
51
+ missing.append("pve.token (PVE API token)")
52
+ if not pve.get("secret"):
53
+ missing.append("pve.secret (PVE API secret)")
54
+ if not netbox.get("url"):
55
+ missing.append("netbox.url (NetBox API URL)")
56
+ if not netbox.get("token"):
57
+ missing.append("netbox.token (NetBox API token)")
58
+
59
+ if missing:
60
+ raise ConfigValidationError(
61
+ f"Missing required config fields: {', '.join(missing)}"
62
+ )
63
+
64
+
65
+ def build_runtime_config_from_db(cluster_name="default"):
66
+ """Read plugin DB models and produce a config dict compatible with
67
+ the standalone sync engine.
68
+
69
+ Returns:
70
+ dict: config in the same schema as config.yaml
71
+ """
72
+ from pve_sync_plugin.models import PveClusterConfig
73
+ from pve_sync_plugin.utils import get_plugin_config
74
+
75
+ cluster = PveClusterConfig.objects.filter(
76
+ name=cluster_name, enabled=True
77
+ ).first()
78
+
79
+ if cluster:
80
+ pve_host = cluster.pve_host
81
+ pve_user = cluster.pve_user
82
+ pve_token = cluster.pve_token
83
+ pve_secret = cluster.pve_secret
84
+ verify_ssl = cluster.pve_verify_ssl
85
+ site_name = (
86
+ cluster.netbox_site.name
87
+ if cluster.netbox_site
88
+ else get_plugin_config("default_site", "Main Datacenter")
89
+ )
90
+ cluster_type = (
91
+ cluster.netbox_cluster_type.name
92
+ if cluster.netbox_cluster_type
93
+ else get_plugin_config("default_cluster_type", "Proxmox")
94
+ )
95
+ netbox_cluster = (
96
+ cluster.netbox_cluster.name
97
+ if cluster.netbox_cluster
98
+ else get_plugin_config("default_netbox_cluster", "Proxmox Cluster")
99
+ )
100
+ logical_name = cluster.name
101
+ notify_on_sync = cluster.notify_on_sync
102
+ else:
103
+ raise ConfigValidationError(
104
+ f"No enabled PVE cluster config found for cluster '{cluster_name}'. "
105
+ "Please add a cluster in PVE Clusters → Add Cluster."
106
+ )
107
+
108
+ telegram_enabled = notify_on_sync and bool(
109
+ get_plugin_config("telegram_bot_token", "")
110
+ and get_plugin_config("telegram_chat_id", "")
111
+ )
112
+
113
+ return {
114
+ "clusters": [
115
+ {
116
+ "name": logical_name,
117
+ "pve": {
118
+ "host": pve_host,
119
+ "user": pve_user,
120
+ "token": pve_token,
121
+ "secret": pve_secret,
122
+ "verify_ssl": bool(verify_ssl),
123
+ },
124
+ "netbox": {
125
+ "url": get_plugin_config("netbox_url", ""),
126
+ "token": get_plugin_config("netbox_token", ""),
127
+ },
128
+ "settings": {
129
+ "cluster_name": netbox_cluster,
130
+ "site_name": site_name,
131
+ "cluster_type": cluster_type,
132
+ },
133
+ }
134
+ ],
135
+ "telegram": {
136
+ "enabled": telegram_enabled,
137
+ "bot_token": get_plugin_config("telegram_bot_token", ""),
138
+ "chat_id": get_plugin_config("telegram_chat_id", ""),
139
+ },
140
+ "monitoring": {
141
+ "node_offline_alert": True,
142
+ "config_drift_alert": True,
143
+ "tag_change_alert": True,
144
+ "resource_alert": {
145
+ "enabled": True,
146
+ "memory_threshold": 85,
147
+ "cpu_threshold": 90,
148
+ "disk_threshold": 10,
149
+ },
150
+ },
151
+ "sync": {
152
+ "incremental": True,
153
+ "force_full_sync": False,
154
+ "batch_size": 50,
155
+ "default_node_role": get_plugin_config(
156
+ "default_node_role", "PVE"
157
+ ),
158
+ "default_node_type": get_plugin_config(
159
+ "default_node_type", "Standard Server"
160
+ ),
161
+ },
162
+ "state_db": {
163
+ "path": get_plugin_config(
164
+ "state_db_path", "/var/lib/pve-sync/state.db"
165
+ ),
166
+ "cleanup_days": 90,
167
+ },
168
+ }
169
+
170
+
171
+ def write_runtime_config_file(cluster_name="default"):
172
+ """Write a temporary YAML config file and set env vars.
173
+
174
+ Returns:
175
+ str: path to the temporary config file (caller must clean up)
176
+
177
+ Raises:
178
+ ConfigValidationError: if required config values are missing.
179
+ """
180
+ config_data = build_runtime_config_from_db(cluster_name)
181
+ validate_config(config_data)
182
+
183
+ config_file = tempfile.NamedTemporaryFile(
184
+ "w", prefix="pve-sync-", suffix=".yaml", delete=False
185
+ )
186
+ with config_file:
187
+ yaml.safe_dump(config_data, config_file, allow_unicode=True, sort_keys=False)
188
+
189
+ _apply_runtime_env(config_file.name, config_data)
190
+ return config_file.name
191
+
192
+
193
+ def _apply_runtime_env(config_path, config_data):
194
+ """Set environment variables expected by the standalone sync engine."""
195
+ os.environ["PVE_SYNC_CONFIG_FILE"] = config_path
196
+ cluster = config_data["clusters"][0]
197
+ pve = cluster["pve"]
198
+ netbox = cluster["netbox"]
199
+ telegram = config_data.get("telegram", {})
200
+
201
+ os.environ["PVE_API_HOST"] = str(pve.get("host", ""))
202
+ os.environ["PVE_API_USER"] = str(pve.get("user", ""))
203
+ os.environ["PVE_API_TOKEN"] = str(pve.get("token", ""))
204
+ os.environ["PVE_API_SECRET"] = str(pve.get("secret", ""))
205
+ os.environ["PVE_API_VERIFY_SSL"] = (
206
+ "true" if pve.get("verify_ssl") else "false"
207
+ )
208
+ os.environ["NB_API_URL"] = str(netbox.get("url", ""))
209
+ os.environ["NB_API_TOKEN"] = str(netbox.get("token", ""))
210
+ os.environ["TELEGRAM_BOT_TOKEN"] = str(telegram.get("bot_token", ""))
211
+ os.environ["TELEGRAM_CHAT_ID"] = str(telegram.get("chat_id", ""))
212
+
213
+
214
+ def cleanup_runtime_env():
215
+ """Remove temporary environment variables set by _apply_runtime_env.
216
+
217
+ Must be called in a finally block after sync completes to prevent
218
+ env var pollution across RQ worker invocations.
219
+ """
220
+ for key in _ENV_KEYS:
221
+ os.environ.pop(key, None)
@@ -0,0 +1,50 @@
1
+ import importlib
2
+ import logging
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+
7
+ class PVESyncEngine:
8
+ """Plugin-facing wrapper around the standalone OptimizedPVEToNetBoxSync.
9
+
10
+ Usage in tasks.py::
11
+
12
+ from pve_sync_plugin.sync import PVESyncEngine
13
+
14
+ engine = PVESyncEngine(config_path="/tmp/pve-sync-xxx.yaml")
15
+ engine.run()
16
+ stats = engine.stats
17
+ """
18
+
19
+ def __init__(self, config_path=None, job_id=None):
20
+ self.config_path = config_path
21
+ self.job_id = job_id
22
+ self._sync_instance = None
23
+ self.stats = {}
24
+
25
+ def run(self):
26
+ """Execute a full sync cycle using the standalone engine."""
27
+ try:
28
+ config_module = importlib.import_module("config")
29
+ sync_module = importlib.import_module("pve_sync")
30
+ except ImportError as exc:
31
+ logger.error(
32
+ "Cannot import standalone sync modules. "
33
+ "Make sure the project root is accessible: %s",
34
+ exc,
35
+ )
36
+ raise
37
+
38
+ # Re-initialize config with our temp file
39
+ if self.config_path:
40
+ config_module._global_config = None
41
+ config_module.init_config(self.config_path)
42
+
43
+ # Run the sync
44
+ self._sync_instance = sync_module.OptimizedPVEToNetBoxSync(job_id=self.job_id)
45
+ self._sync_instance.sync()
46
+
47
+ # Capture stats
48
+ self.stats = dict(getattr(self._sync_instance, "stats", {}))
49
+
50
+ return self.stats
@@ -0,0 +1,278 @@
1
+ """NetBox tables for PVE Sync plugin list views."""
2
+
3
+ import django_tables2 as tables
4
+
5
+ from netbox.tables import NetBoxTable, columns
6
+
7
+ from .models import (
8
+ PbsServerConfig,
9
+ PveBackupStatus,
10
+ PveClusterConfig,
11
+ PveDriftEvent,
12
+ PveSyncJob,
13
+ PveWebhookEvent,
14
+ )
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # PveSyncJob
19
+ # ---------------------------------------------------------------------------
20
+
21
+ class PveSyncJobTable(NetBoxTable):
22
+ """Table for sync job history with status badges and timing information."""
23
+
24
+ id = tables.Column(linkify=True)
25
+ cluster_name = tables.Column(linkify=True)
26
+ status = columns.ChoiceFieldColumn()
27
+ trigger = columns.ChoiceFieldColumn()
28
+ start_time = columns.DateTimeColumn()
29
+ end_time = columns.DateTimeColumn()
30
+ success_rate = tables.Column(
31
+ accessor="success_rate",
32
+ verbose_name="Success %",
33
+ orderable=False,
34
+ )
35
+ triggered_by = tables.Column(linkify=True)
36
+ actions = columns.ActionsColumn(actions=("delete", "changelog"))
37
+
38
+ class Meta(NetBoxTable.Meta):
39
+ model = PveSyncJob
40
+ fields = (
41
+ "pk",
42
+ "id",
43
+ "cluster_name",
44
+ "status",
45
+ "trigger",
46
+ "start_time",
47
+ "end_time",
48
+ "total_vms",
49
+ "success_vms",
50
+ "failed_vms",
51
+ "success_rate",
52
+ "nodes_offline",
53
+ "config_drifts",
54
+ "resource_alerts",
55
+ "triggered_by",
56
+ )
57
+ default_columns = (
58
+ "id",
59
+ "cluster_name",
60
+ "status",
61
+ "trigger",
62
+ "start_time",
63
+ "end_time",
64
+ "total_vms",
65
+ "success_rate",
66
+ )
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # PveWebhookEvent
71
+ # ---------------------------------------------------------------------------
72
+
73
+ class PveWebhookEventTable(NetBoxTable):
74
+ """Table for webhook events received from PVE."""
75
+
76
+ id = tables.Column(linkify=True)
77
+ event_type = columns.ChoiceFieldColumn()
78
+ node = tables.Column()
79
+ vmid = tables.Column(verbose_name="VM ID")
80
+ vm_name = tables.Column()
81
+ processed = columns.BooleanColumn()
82
+ received_at = columns.DateTimeColumn()
83
+ sync_job = tables.Column(linkify=True)
84
+ actions = columns.ActionsColumn(actions=("delete", "changelog"))
85
+
86
+ class Meta(NetBoxTable.Meta):
87
+ model = PveWebhookEvent
88
+ fields = (
89
+ "pk",
90
+ "id",
91
+ "event_type",
92
+ "node",
93
+ "vmid",
94
+ "vm_name",
95
+ "processed",
96
+ "received_at",
97
+ "sync_job",
98
+ )
99
+ default_columns = (
100
+ "id",
101
+ "event_type",
102
+ "node",
103
+ "vmid",
104
+ "vm_name",
105
+ "processed",
106
+ "received_at",
107
+ )
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # PveClusterConfig
112
+ # ---------------------------------------------------------------------------
113
+
114
+ class PveClusterConfigTable(NetBoxTable):
115
+ """Table for PVE cluster connection profiles."""
116
+
117
+ name = tables.Column(linkify=True)
118
+ pve_host = tables.Column()
119
+ enabled = columns.BooleanColumn()
120
+ sync_schedule = columns.ChoiceFieldColumn()
121
+ netbox_site = tables.Column(linkify=True)
122
+ netbox_cluster = tables.Column(linkify=True)
123
+ last_sync = columns.DateTimeColumn()
124
+ last_sync_status = tables.Column()
125
+
126
+ class Meta(NetBoxTable.Meta):
127
+ model = PveClusterConfig
128
+ fields = (
129
+ "pk",
130
+ "name",
131
+ "pve_host",
132
+ "pve_user",
133
+ "enabled",
134
+ "sync_schedule",
135
+ "netbox_site",
136
+ "netbox_cluster",
137
+ "last_sync",
138
+ "last_sync_status",
139
+ )
140
+ default_columns = (
141
+ "name",
142
+ "pve_host",
143
+ "enabled",
144
+ "sync_schedule",
145
+ "netbox_site",
146
+ "netbox_cluster",
147
+ "last_sync",
148
+ "last_sync_status",
149
+ )
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # PbsServerConfig
154
+ # ---------------------------------------------------------------------------
155
+
156
+ class PbsServerConfigTable(NetBoxTable):
157
+ """Table for PBS server connection profiles."""
158
+
159
+ name = tables.Column(linkify=True)
160
+ pbs_host = tables.Column()
161
+ pbs_node_name = tables.Column()
162
+ enabled = columns.BooleanColumn()
163
+ netbox_site = tables.Column(linkify=True)
164
+ last_sync = columns.DateTimeColumn()
165
+ last_sync_status = tables.Column()
166
+ actions = columns.ActionsColumn(actions=("edit", "delete", "changelog"))
167
+
168
+ class Meta(NetBoxTable.Meta):
169
+ model = PbsServerConfig
170
+ fields = (
171
+ "pk",
172
+ "name",
173
+ "pbs_host",
174
+ "pbs_node_name",
175
+ "enabled",
176
+ "netbox_site",
177
+ "last_sync",
178
+ "last_sync_status",
179
+ )
180
+ default_columns = (
181
+ "name",
182
+ "pbs_host",
183
+ "pbs_node_name",
184
+ "enabled",
185
+ "netbox_site",
186
+ "last_sync",
187
+ "last_sync_status",
188
+ )
189
+
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # PveDriftEvent
193
+ # ---------------------------------------------------------------------------
194
+
195
+ class PveDriftEventTable(NetBoxTable):
196
+ """Table for VM configuration drift events."""
197
+
198
+ id = tables.Column(linkify=True)
199
+ vm_name = tables.Column(verbose_name="VM 名稱")
200
+ vmid = tables.Column(verbose_name="VM ID")
201
+ cluster_name = tables.Column(verbose_name="叢集")
202
+ drift_type = columns.ChoiceFieldColumn(verbose_name="變更類型")
203
+ field_name = tables.Column(verbose_name="欄位")
204
+ old_value = tables.Column(verbose_name="舊值")
205
+ new_value = tables.Column(verbose_name="新值")
206
+ notified_telegram = columns.BooleanColumn(verbose_name="已通知")
207
+ created = columns.DateTimeColumn(verbose_name="偵測時間")
208
+ sync_job = tables.Column(linkify=True, verbose_name="同步任務")
209
+ actions = columns.ActionsColumn(actions=("delete",))
210
+
211
+ class Meta(NetBoxTable.Meta):
212
+ model = PveDriftEvent
213
+ fields = (
214
+ "pk",
215
+ "id",
216
+ "vm_name",
217
+ "vmid",
218
+ "cluster_name",
219
+ "drift_type",
220
+ "field_name",
221
+ "old_value",
222
+ "new_value",
223
+ "notified_telegram",
224
+ "created",
225
+ "sync_job",
226
+ )
227
+ default_columns = (
228
+ "id",
229
+ "vm_name",
230
+ "vmid",
231
+ "cluster_name",
232
+ "drift_type",
233
+ "field_name",
234
+ "old_value",
235
+ "new_value",
236
+ "notified_telegram",
237
+ "created",
238
+ )
239
+
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # PveBackupStatus
243
+ # ---------------------------------------------------------------------------
244
+
245
+ class PveBackupStatusTable(NetBoxTable):
246
+ """Table for per-VM backup tracking."""
247
+
248
+ id = tables.Column(linkify=True)
249
+ vm = tables.Column(linkify=True, verbose_name="Virtual Machine")
250
+ backup_status = columns.ChoiceFieldColumn()
251
+ last_backup = columns.DateTimeColumn()
252
+ backup_age_days = tables.Column(
253
+ accessor="backup_age_days",
254
+ verbose_name="Age (days)",
255
+ orderable=False,
256
+ )
257
+ next_backup = columns.DateTimeColumn()
258
+ actions = columns.ActionsColumn(actions=("changelog",))
259
+
260
+ class Meta(NetBoxTable.Meta):
261
+ model = PveBackupStatus
262
+ fields = (
263
+ "pk",
264
+ "id",
265
+ "vm",
266
+ "backup_status",
267
+ "last_backup",
268
+ "backup_age_days",
269
+ "backup_size",
270
+ "next_backup",
271
+ )
272
+ default_columns = (
273
+ "id",
274
+ "vm",
275
+ "backup_status",
276
+ "last_backup",
277
+ "backup_age_days",
278
+ )