netbox-vcenter-server 0.2.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.
@@ -0,0 +1,311 @@
1
+ {% extends 'base/layout.html' %}
2
+ {% load helpers %}
3
+ {% load vcenter_tags %}
4
+
5
+ {% block title %}vCenter Import Dashboard{% endblock %}
6
+
7
+ {% block tabs %}
8
+ <ul class="nav nav-tabs">
9
+ <li class="nav-item">
10
+ <a class="nav-link {% if selected_server == 'all' %}active{% endif %}"
11
+ href="?server=all">
12
+ <i class="mdi mdi-view-list"></i> All
13
+ <span class="badge bg-primary text-white ms-1">{{ vm_count }}</span>
14
+ </a>
15
+ </li>
16
+ {% for server in servers %}
17
+ <li class="nav-item">
18
+ <a class="nav-link {% if server == selected_server %}active{% endif %}"
19
+ href="?server={{ server }}">
20
+ {{ server }}
21
+ {% if cached_data|get_key:server %}
22
+ <span class="badge bg-success text-white ms-1">{{ cached_data|get_key:server|get_key:'count' }}</span>
23
+ {% else %}
24
+ <span class="badge bg-secondary text-white ms-1">-</span>
25
+ {% endif %}
26
+ </a>
27
+ </li>
28
+ {% endfor %}
29
+ <li class="nav-item">
30
+ <a class="nav-link" href="{% url 'plugins:netbox_vcenter:compare' %}{% if selected_server and selected_server != 'all' %}?server={{ selected_server }}{% endif %}">
31
+ <i class="mdi mdi-compare"></i> Compare
32
+ </a>
33
+ </li>
34
+ </ul>
35
+ {% endblock %}
36
+
37
+ {% block content %}
38
+ <div class="row mb-4">
39
+ <!-- Connection Form -->
40
+ <div class="col-md-5">
41
+ <div class="card">
42
+ <div class="card-header">
43
+ <h5 class="card-title mb-0">
44
+ <i class="mdi mdi-connection"></i> Connect to vCenter
45
+ </h5>
46
+ </div>
47
+ <div class="card-body">
48
+ {% if mfa_enabled %}
49
+ <div class="alert alert-warning mb-3 py-2">
50
+ <div><i class="mdi mdi-shield-alert"></i> <strong>{{ mfa_label }} Required</strong></div>
51
+ <div class="small">{{ mfa_message }}</div>
52
+ </div>
53
+ {% endif %}
54
+
55
+ <form method="post">
56
+ {% csrf_token %}
57
+
58
+ <div class="mb-3">
59
+ <label class="form-label">{{ form.server.label }}</label>
60
+ {{ form.server }}
61
+ <small class="text-muted">{{ form.server.help_text }}</small>
62
+ </div>
63
+
64
+ <div class="mb-3">
65
+ <label class="form-label">{{ form.username.label }}</label>
66
+ {{ form.username }}
67
+ <small class="text-muted">{{ form.username.help_text }}</small>
68
+ </div>
69
+
70
+ <div class="mb-3">
71
+ <label class="form-label">{{ form.password.label }}</label>
72
+ {{ form.password }}
73
+ </div>
74
+
75
+ <div class="mb-3 form-check">
76
+ {{ form.verify_ssl }}
77
+ <label class="form-check-label">{{ form.verify_ssl.label }}</label>
78
+ <br><small class="text-muted">{{ form.verify_ssl.help_text }}</small>
79
+ </div>
80
+
81
+ <button type="submit" class="btn btn-primary">
82
+ <i class="mdi mdi-sync"></i> Connect & Sync
83
+ </button>
84
+ </form>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- Cache Status -->
90
+ <div class="col-md-7">
91
+ <div class="card">
92
+ <div class="card-header">
93
+ <h5 class="card-title mb-0">
94
+ <i class="mdi mdi-database"></i> Cache Status
95
+ </h5>
96
+ </div>
97
+ <div class="card-body">
98
+ <table class="table table-hover mb-0">
99
+ <thead>
100
+ <tr>
101
+ <th>vCenter Server</th>
102
+ <th>VMs</th>
103
+ <th>Last Sync</th>
104
+ <th>Actions</th>
105
+ </tr>
106
+ </thead>
107
+ <tbody>
108
+ {% for server in servers %}
109
+ {% with data=cached_data|get_key:server %}
110
+ <tr{% if server == selected_server %} class="table-active"{% endif %}>
111
+ <td>
112
+ <a href="?server={{ server }}">{{ server }}</a>
113
+ </td>
114
+ <td>
115
+ {% if data %}
116
+ <span class="badge bg-primary text-white">{{ data.count }} VMs</span>
117
+ {% else %}
118
+ <span class="text-muted">Not synced</span>
119
+ {% endif %}
120
+ </td>
121
+ <td>
122
+ {% if data.timestamp %}
123
+ {{ data.timestamp|slice:":19" }}
124
+ {% else %}
125
+ <span class="text-muted">Never</span>
126
+ {% endif %}
127
+ </td>
128
+ <td>
129
+ {% if data %}
130
+ <a href="{% url 'plugins:netbox_vcenter:refresh' server %}"
131
+ class="btn btn-sm btn-outline-secondary"
132
+ onclick="return confirm('Clear cache for {{ server }}? You will need to re-enter credentials.');">
133
+ <i class="mdi mdi-refresh"></i> Clear Cache
134
+ </a>
135
+ {% endif %}
136
+ </td>
137
+ </tr>
138
+ {% endwith %}
139
+ {% endfor %}
140
+ </tbody>
141
+ </table>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ <!-- VM List -->
148
+ {% if vms %}
149
+ <div class="card">
150
+ <div class="card-header d-flex justify-content-between align-items-center">
151
+ <h5 class="card-title mb-0">
152
+ <i class="mdi mdi-server"></i>
153
+ {% if selected_server == 'all' %}All Virtual Machines{% else %}Virtual Machines from {{ selected_server }}{% endif %}
154
+ <span class="badge bg-info text-white ms-2">{{ vm_count }} VMs</span>
155
+ </h5>
156
+ <button type="button" class="btn btn-success" id="import-selected-btn" disabled>
157
+ <i class="mdi mdi-import"></i> Import Selected to NetBox
158
+ </button>
159
+ </div>
160
+ <div class="card-body p-0">
161
+ <div class="table-responsive">
162
+ <table class="table table-hover table-striped mb-0" id="vm-table">
163
+ <thead class="table-light">
164
+ <tr>
165
+ <th style="width: 40px;">
166
+ <input type="checkbox" id="select-all" class="form-check-input">
167
+ </th>
168
+ <th>Name</th>
169
+ {% if selected_server == 'all' %}<th>Server</th>{% endif %}
170
+ <th>Power</th>
171
+ <th>IP Address</th>
172
+ <th>vCPUs</th>
173
+ <th>Memory</th>
174
+ <th>Disk</th>
175
+ <th>Cluster</th>
176
+ <th>NetBox</th>
177
+ </tr>
178
+ </thead>
179
+ <tbody>
180
+ {% for vm in vms %}
181
+ <tr>
182
+ <td>
183
+ <input type="checkbox" class="form-check-input vm-checkbox"
184
+ value="{{ vm.name }}" data-vm-name="{{ vm.name }}" data-server="{{ vm.source_server }}">
185
+ </td>
186
+ <td>
187
+ <strong>{{ vm.name }}</strong>
188
+ {% if vm.guest_os %}
189
+ <br><small class="text-muted">{{ vm.guest_os }}</small>
190
+ {% endif %}
191
+ </td>
192
+ {% if selected_server == 'all' %}
193
+ <td><small>{{ vm.source_server }}</small></td>
194
+ {% endif %}
195
+ <td>
196
+ {% if vm.power_state == 'on' %}
197
+ <span class="badge bg-success text-white">ON</span>
198
+ {% else %}
199
+ <span class="badge bg-secondary text-white">OFF</span>
200
+ {% endif %}
201
+ </td>
202
+ <td>
203
+ {% if vm.primary_ip %}
204
+ <code>{{ vm.primary_ip }}</code>
205
+ {% else %}
206
+ <span class="text-muted">-</span>
207
+ {% endif %}
208
+ </td>
209
+ <td>{{ vm.vcpus|default:'-' }}</td>
210
+ <td>
211
+ {% if vm.memory_mb %}
212
+ {{ vm.memory_mb|floatformat:0 }} MB
213
+ {% else %}
214
+ -
215
+ {% endif %}
216
+ </td>
217
+ <td>
218
+ {% if vm.disk_gb %}
219
+ {{ vm.disk_gb }} GB
220
+ {% else %}
221
+ -
222
+ {% endif %}
223
+ </td>
224
+ <td>{{ vm.cluster|default:'-' }}</td>
225
+ <td>
226
+ {% if vm.exists_in_netbox %}
227
+ <span class="badge bg-info text-white" title="Already in NetBox">
228
+ <i class="mdi mdi-check"></i> Exists
229
+ </span>
230
+ {% else %}
231
+ <span class="badge bg-warning text-dark" title="Not in NetBox - can import">
232
+ <i class="mdi mdi-alert"></i> Missing
233
+ </span>
234
+ {% endif %}
235
+ </td>
236
+ </tr>
237
+ {% endfor %}
238
+ </tbody>
239
+ </table>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ {% elif selected_server == 'all' and vm_count == 0 %}
244
+ <div class="alert alert-info">
245
+ <i class="mdi mdi-information"></i>
246
+ No cached data from any server. Select a server tab and enter credentials to sync.
247
+ </div>
248
+ {% elif selected_server and selected_server != 'all' and not cached_data|get_key:selected_server %}
249
+ <div class="alert alert-info">
250
+ <i class="mdi mdi-information"></i>
251
+ No cached data for <strong>{{ selected_server }}</strong>.
252
+ Enter your credentials and click "Connect & Sync" to fetch VMs.
253
+ </div>
254
+ {% endif %}
255
+
256
+ <script>
257
+ document.addEventListener('DOMContentLoaded', function() {
258
+ const selectAll = document.getElementById('select-all');
259
+ const checkboxes = document.querySelectorAll('.vm-checkbox');
260
+ const importBtn = document.getElementById('import-selected-btn');
261
+
262
+ function updateImportButton() {
263
+ const selected = document.querySelectorAll('.vm-checkbox:checked');
264
+ importBtn.disabled = selected.length === 0;
265
+ importBtn.textContent = selected.length > 0
266
+ ? `Import ${selected.length} VM(s) to NetBox`
267
+ : 'Import Selected to NetBox';
268
+ }
269
+
270
+ if (selectAll) {
271
+ selectAll.addEventListener('change', function() {
272
+ checkboxes.forEach(cb => cb.checked = this.checked);
273
+ updateImportButton();
274
+ });
275
+ }
276
+
277
+ checkboxes.forEach(cb => {
278
+ cb.addEventListener('change', updateImportButton);
279
+ });
280
+
281
+ if (importBtn) {
282
+ importBtn.addEventListener('click', function() {
283
+ const selectedCheckboxes = Array.from(document.querySelectorAll('.vm-checkbox:checked'));
284
+
285
+ if (selectedCheckboxes.length === 0) {
286
+ alert('Please select at least one VM to import.');
287
+ return;
288
+ }
289
+
290
+ // Group VMs by server
291
+ const vmsByServer = {};
292
+ selectedCheckboxes.forEach(cb => {
293
+ const server = cb.dataset.server;
294
+ if (!vmsByServer[server]) vmsByServer[server] = [];
295
+ vmsByServer[server].push(cb.value);
296
+ });
297
+
298
+ const servers = Object.keys(vmsByServer);
299
+ if (servers.length > 1) {
300
+ alert('Selected VMs are from multiple servers. Please select VMs from one server at a time, or use individual server tabs.');
301
+ return;
302
+ }
303
+
304
+ const server = servers[0];
305
+ const vmsJson = encodeURIComponent(JSON.stringify(vmsByServer[server]));
306
+ window.location.href = `{% url 'plugins:netbox_vcenter:import' %}?vms=${vmsJson}&server=${server}`;
307
+ });
308
+ }
309
+ });
310
+ </script>
311
+ {% endblock %}
@@ -0,0 +1,182 @@
1
+ {% extends 'base/layout.html' %}
2
+ {% load helpers %}
3
+
4
+ {% block title %}Import VMs from vCenter{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="row">
8
+ <div class="col-md-8">
9
+ <div class="card">
10
+ <div class="card-header">
11
+ <h5 class="card-title mb-0">
12
+ <i class="mdi mdi-import"></i> Import VMs from {{ server }}
13
+ </h5>
14
+ </div>
15
+ <div class="card-body">
16
+ {% if existing_count > 0 %}
17
+ <div class="alert alert-info">
18
+ <i class="mdi mdi-information"></i>
19
+ <strong>{{ existing_count }} VM(s)</strong> already exist in NetBox.
20
+ Check "Update existing VMs" below to sync their specs.
21
+ </div>
22
+ {% endif %}
23
+
24
+ {% if new_count > 0 %}
25
+ <p class="mb-3">
26
+ The following <strong>{{ new_count }} VM(s)</strong> will be imported to NetBox:
27
+ </p>
28
+
29
+ <div class="table-responsive mb-4">
30
+ <table class="table table-sm table-hover">
31
+ <thead class="table-light">
32
+ <tr>
33
+ <th>VM Name</th>
34
+ <th>Power</th>
35
+ <th>vCPUs</th>
36
+ <th>Memory</th>
37
+ <th>Disk</th>
38
+ <th>Status</th>
39
+ </tr>
40
+ </thead>
41
+ <tbody>
42
+ {% for vm in vms %}
43
+ <tr{% if vm.exists_in_netbox %} class="table-secondary"{% endif %}>
44
+ <td>
45
+ {{ vm.name }}
46
+ {% if vm.exists_in_netbox %}
47
+ <span class="badge bg-warning text-dark">Exists</span>
48
+ {% endif %}
49
+ </td>
50
+ <td>
51
+ {% if vm.power_state == 'on' %}
52
+ <span class="badge bg-success text-white">ON</span>
53
+ {% else %}
54
+ <span class="badge bg-secondary text-white">OFF</span>
55
+ {% endif %}
56
+ </td>
57
+ <td>{{ vm.vcpus|default:'-' }}</td>
58
+ <td>{{ vm.memory_mb|default:'-' }} MB</td>
59
+ <td>{{ vm.disk_gb|default:'-' }} GB</td>
60
+ <td>
61
+ {% if vm.exists_in_netbox %}
62
+ <span class="text-muted">Skip</span>
63
+ {% else %}
64
+ <span class="text-success">Import</span>
65
+ {% endif %}
66
+ </td>
67
+ </tr>
68
+ {% endfor %}
69
+ </tbody>
70
+ </table>
71
+ </div>
72
+
73
+ <form method="post">
74
+ {% csrf_token %}
75
+ {{ form.selected_vms }}
76
+ {{ form.vcenter_server }}
77
+
78
+ <div class="mb-3">
79
+ <label class="form-label">
80
+ <strong>Target Cluster</strong> <span class="text-danger">*</span>
81
+ </label>
82
+ {{ form.cluster }}
83
+ <small class="text-muted">
84
+ Select the NetBox cluster to assign these VMs to.
85
+ </small>
86
+ </div>
87
+
88
+ {% if existing_count > 0 %}
89
+ <div class="mb-3 form-check">
90
+ {{ form.update_existing }}
91
+ <label class="form-check-label" for="{{ form.update_existing.id_for_label }}">
92
+ <strong>{{ form.update_existing.label }}</strong>
93
+ </label>
94
+ <br><small class="text-muted">{{ form.update_existing.help_text }}</small>
95
+ </div>
96
+ {% endif %}
97
+
98
+ <hr>
99
+
100
+ <div class="d-flex justify-content-between">
101
+ <a href="{% url 'plugins:netbox_vcenter:dashboard' %}?server={{ server }}"
102
+ class="btn btn-outline-secondary">
103
+ <i class="mdi mdi-arrow-left"></i> Cancel
104
+ </a>
105
+ <button type="submit" class="btn btn-success">
106
+ <i class="mdi mdi-import"></i>
107
+ {% if new_count > 0 %}Import {{ new_count }} VM(s){% else %}Process{% endif %}
108
+ </button>
109
+ </div>
110
+ </form>
111
+ {% else %}
112
+ <!-- All VMs exist - show update-only form -->
113
+ <div class="alert alert-info mb-3">
114
+ <i class="mdi mdi-information"></i>
115
+ All {{ existing_count }} selected VM(s) already exist in NetBox.
116
+ </div>
117
+
118
+ <form method="post">
119
+ {% csrf_token %}
120
+ {{ form.selected_vms }}
121
+ {{ form.vcenter_server }}
122
+
123
+ <div class="mb-3">
124
+ <label class="form-label">
125
+ <strong>Target Cluster</strong> <span class="text-danger">*</span>
126
+ </label>
127
+ {{ form.cluster }}
128
+ <small class="text-muted">
129
+ Select the NetBox cluster (used for any new VMs).
130
+ </small>
131
+ </div>
132
+
133
+ <div class="mb-3 form-check">
134
+ {{ form.update_existing }}
135
+ <label class="form-check-label" for="{{ form.update_existing.id_for_label }}">
136
+ <strong>{{ form.update_existing.label }}</strong>
137
+ </label>
138
+ <br><small class="text-muted">{{ form.update_existing.help_text }}</small>
139
+ </div>
140
+
141
+ <hr>
142
+
143
+ <div class="d-flex justify-content-between">
144
+ <a href="{% url 'plugins:netbox_vcenter:dashboard' %}?server={{ server }}"
145
+ class="btn btn-outline-secondary">
146
+ <i class="mdi mdi-arrow-left"></i> Cancel
147
+ </a>
148
+ <button type="submit" class="btn btn-primary">
149
+ <i class="mdi mdi-sync"></i> Update Existing VMs
150
+ </button>
151
+ </div>
152
+ </form>
153
+ {% endif %}
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <div class="col-md-4">
159
+ <div class="card">
160
+ <div class="card-header">
161
+ <h5 class="card-title mb-0">
162
+ <i class="mdi mdi-information"></i> Import Notes
163
+ </h5>
164
+ </div>
165
+ <div class="card-body">
166
+ <ul class="mb-0">
167
+ <li><strong>New VMs</strong> are created in the selected cluster.</li>
168
+ <li><strong>Update existing</strong> syncs:
169
+ <ul>
170
+ <li>vCPUs, Memory, Disk</li>
171
+ <li>Power state → Status</li>
172
+ <li>Primary IP address</li>
173
+ </ul>
174
+ </li>
175
+ <li>Name matching uses configured mode (hostname, exact, or regex).</li>
176
+ <li>IP addresses create interfaces as needed.</li>
177
+ </ul>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ {% endblock %}
File without changes
@@ -0,0 +1,16 @@
1
+ """Template tags for NetBox vCenter plugin."""
2
+
3
+ from django import template
4
+
5
+ register = template.Library()
6
+
7
+
8
+ @register.filter
9
+ def get_key(dictionary, key):
10
+ """Get a value from a dictionary by key.
11
+
12
+ Usage: {{ mydict|get_key:keyvar }}
13
+ """
14
+ if dictionary is None:
15
+ return None
16
+ return dictionary.get(key)
netbox_vcenter/urls.py ADDED
@@ -0,0 +1,13 @@
1
+ """URL configuration for NetBox vCenter plugin."""
2
+
3
+ from django.urls import path
4
+
5
+ from . import views
6
+
7
+ urlpatterns = [
8
+ path("", views.VCenterDashboardView.as_view(), name="dashboard"),
9
+ path("refresh/<str:server>/", views.VCenterRefreshView.as_view(), name="refresh"),
10
+ path("import/", views.VMImportView.as_view(), name="import"),
11
+ path("compare/", views.VMComparisonView.as_view(), name="compare"),
12
+ path("sync-differences/", views.SyncDifferencesView.as_view(), name="sync_differences"),
13
+ ]