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.
- netbox_vcenter/__init__.py +59 -0
- netbox_vcenter/api/__init__.py +1 -0
- netbox_vcenter/api/urls.py +5 -0
- netbox_vcenter/client.py +247 -0
- netbox_vcenter/forms.py +77 -0
- netbox_vcenter/navigation.py +25 -0
- netbox_vcenter/templates/netbox_vcenter/compare.html +219 -0
- netbox_vcenter/templates/netbox_vcenter/dashboard.html +311 -0
- netbox_vcenter/templates/netbox_vcenter/import.html +182 -0
- netbox_vcenter/templatetags/__init__.py +0 -0
- netbox_vcenter/templatetags/vcenter_tags.py +16 -0
- netbox_vcenter/urls.py +13 -0
- netbox_vcenter/views.py +748 -0
- netbox_vcenter_server-0.2.0.dist-info/METADATA +200 -0
- netbox_vcenter_server-0.2.0.dist-info/RECORD +18 -0
- netbox_vcenter_server-0.2.0.dist-info/WHEEL +5 -0
- netbox_vcenter_server-0.2.0.dist-info/licenses/LICENSE +190 -0
- netbox_vcenter_server-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NetBox vCenter Plugin
|
|
3
|
+
|
|
4
|
+
Provides a dashboard for viewing and importing VMs from VMware vCenter servers.
|
|
5
|
+
Supports multiple vCenter servers with per-server caching and VM import to NetBox.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from netbox.plugins import PluginConfig
|
|
9
|
+
|
|
10
|
+
__version__ = "0.2.0"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VcenterConfig(PluginConfig):
|
|
14
|
+
"""Plugin configuration for NetBox vCenter integration."""
|
|
15
|
+
|
|
16
|
+
name = "netbox_vcenter"
|
|
17
|
+
verbose_name = "vCenter"
|
|
18
|
+
description = "View and import VMs from VMware vCenter servers"
|
|
19
|
+
version = __version__
|
|
20
|
+
author = "sieteunoseis"
|
|
21
|
+
author_email = "jeremy.worden@gmail.com"
|
|
22
|
+
base_url = "vcenter"
|
|
23
|
+
min_version = "4.0.0"
|
|
24
|
+
|
|
25
|
+
# Required settings - plugin won't load without these
|
|
26
|
+
required_settings = []
|
|
27
|
+
|
|
28
|
+
# Default configuration values
|
|
29
|
+
default_settings = {
|
|
30
|
+
# List of vCenter servers to choose from
|
|
31
|
+
"vcenter_servers": [],
|
|
32
|
+
# Connection settings
|
|
33
|
+
"timeout": 60, # Connection timeout in seconds (longer for MFA)
|
|
34
|
+
"verify_ssl": False, # SSL verification (False for self-signed certs)
|
|
35
|
+
# MFA/2FA settings
|
|
36
|
+
"mfa_enabled": False, # Whether to show MFA warning
|
|
37
|
+
"mfa_label": "MFA", # Short label: "Duo", "2FA", "MFA", etc.
|
|
38
|
+
"mfa_message": "Check your authenticator after clicking Connect & Sync.",
|
|
39
|
+
# Name matching for duplicate detection
|
|
40
|
+
# Options: "exact" (case-insensitive full name), "hostname" (strip domain), "regex" (custom pattern)
|
|
41
|
+
"name_match_mode": "exact",
|
|
42
|
+
# Regex pattern to extract the match portion from VM name (used when name_match_mode is "regex")
|
|
43
|
+
# Example: r"^([^.]+)" extracts hostname (same as "hostname" mode)
|
|
44
|
+
# Example: r"^(\w+\d+)" extracts letters followed by numbers
|
|
45
|
+
"name_match_pattern": r"^([^.]+)",
|
|
46
|
+
# Import/sync settings
|
|
47
|
+
# Whether to normalize VM names on import (strip domain, lowercase)
|
|
48
|
+
# e.g., "WebServer01.example.com" -> "webserver01"
|
|
49
|
+
"normalize_imported_name": True,
|
|
50
|
+
# Tag slug to apply to imported/synced VMs (must exist in NetBox, or leave empty)
|
|
51
|
+
"default_tag": "",
|
|
52
|
+
# Optional default role slug for imported VMs (must exist in NetBox, or leave empty)
|
|
53
|
+
"default_role": "",
|
|
54
|
+
# Optional default platform slug for imported VMs (must exist in NetBox, or leave empty)
|
|
55
|
+
"default_platform": "",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
config = VcenterConfig
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API module for NetBox Vcenter plugin."""
|
netbox_vcenter/client.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""vCenter API client using pyvmomi."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import ssl
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pyVim.connect import Disconnect, SmartConnect
|
|
8
|
+
from pyVmomi import vim
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VCenterClient:
|
|
14
|
+
"""Client for connecting to VMware vCenter and fetching VM data."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, server: str, username: str, password: str, verify_ssl: bool = False):
|
|
17
|
+
"""
|
|
18
|
+
Initialize vCenter client.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
server: vCenter server hostname
|
|
22
|
+
username: vCenter username (e.g., domain\\user)
|
|
23
|
+
password: vCenter password
|
|
24
|
+
verify_ssl: Whether to verify SSL certificates
|
|
25
|
+
"""
|
|
26
|
+
self.server = server
|
|
27
|
+
self.username = username
|
|
28
|
+
self.password = password
|
|
29
|
+
self.verify_ssl = verify_ssl
|
|
30
|
+
self.service_instance = None
|
|
31
|
+
self.content = None
|
|
32
|
+
|
|
33
|
+
def connect(self):
|
|
34
|
+
"""
|
|
35
|
+
Connect to vCenter server.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
ServiceInstance object
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
Exception: If connection fails
|
|
42
|
+
"""
|
|
43
|
+
logger.info(f"Connecting to vCenter: {self.server}")
|
|
44
|
+
|
|
45
|
+
ssl_context = None
|
|
46
|
+
if not self.verify_ssl:
|
|
47
|
+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
48
|
+
ssl_context.check_hostname = False
|
|
49
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
50
|
+
|
|
51
|
+
self.service_instance = SmartConnect(
|
|
52
|
+
host=self.server,
|
|
53
|
+
user=self.username,
|
|
54
|
+
pwd=self.password,
|
|
55
|
+
sslContext=ssl_context,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.content = self.service_instance.RetrieveContent()
|
|
59
|
+
logger.info(f"Connected to vCenter: {self.content.about.fullName}")
|
|
60
|
+
|
|
61
|
+
return self.service_instance
|
|
62
|
+
|
|
63
|
+
def disconnect(self):
|
|
64
|
+
"""Disconnect from vCenter server."""
|
|
65
|
+
if self.service_instance:
|
|
66
|
+
logger.info(f"Disconnecting from vCenter: {self.server}")
|
|
67
|
+
Disconnect(self.service_instance)
|
|
68
|
+
self.service_instance = None
|
|
69
|
+
self.content = None
|
|
70
|
+
|
|
71
|
+
def _get_objects_of_type(self, obj_type):
|
|
72
|
+
"""Get all objects of a specific type from vCenter."""
|
|
73
|
+
view_mgr = self.content.viewManager.CreateContainerView(
|
|
74
|
+
self.content.rootFolder, [obj_type], True
|
|
75
|
+
)
|
|
76
|
+
try:
|
|
77
|
+
return list(view_mgr.view)
|
|
78
|
+
finally:
|
|
79
|
+
view_mgr.Destroy()
|
|
80
|
+
|
|
81
|
+
def get_vcenter_info(self) -> dict:
|
|
82
|
+
"""Get vCenter server information."""
|
|
83
|
+
about = self.content.about
|
|
84
|
+
return {
|
|
85
|
+
"name": about.name,
|
|
86
|
+
"full_name": about.fullName,
|
|
87
|
+
"version": about.version,
|
|
88
|
+
"build": about.build,
|
|
89
|
+
"os_type": about.osType,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def fetch_all_vms(self) -> list:
|
|
93
|
+
"""
|
|
94
|
+
Fetch all virtual machines from vCenter.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of VM dictionaries with details
|
|
98
|
+
"""
|
|
99
|
+
logger.info(f"Fetching VMs from {self.server}")
|
|
100
|
+
|
|
101
|
+
vms = self._get_objects_of_type(vim.VirtualMachine)
|
|
102
|
+
vm_list = []
|
|
103
|
+
|
|
104
|
+
for vm in vms:
|
|
105
|
+
try:
|
|
106
|
+
vm_data = {
|
|
107
|
+
"name": vm.name,
|
|
108
|
+
"power_state": "on" if vm.runtime.powerState == "poweredOn" else "off",
|
|
109
|
+
"vcpus": None,
|
|
110
|
+
"memory_mb": None,
|
|
111
|
+
"disk_gb": None,
|
|
112
|
+
"cluster": None,
|
|
113
|
+
"datacenter": None,
|
|
114
|
+
"guest_os": None,
|
|
115
|
+
"uuid": None,
|
|
116
|
+
"ip_addresses": [],
|
|
117
|
+
"primary_ip": None,
|
|
118
|
+
"interfaces": [],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Get hardware config
|
|
122
|
+
if vm.config:
|
|
123
|
+
vm_data["vcpus"] = vm.config.hardware.numCPU
|
|
124
|
+
vm_data["memory_mb"] = vm.config.hardware.memoryMB
|
|
125
|
+
vm_data["guest_os"] = vm.config.guestFullName
|
|
126
|
+
vm_data["uuid"] = vm.config.uuid
|
|
127
|
+
|
|
128
|
+
# Calculate total disk capacity
|
|
129
|
+
disk_devices = [
|
|
130
|
+
device
|
|
131
|
+
for device in vm.config.hardware.device
|
|
132
|
+
if isinstance(device, vim.vm.device.VirtualDisk)
|
|
133
|
+
]
|
|
134
|
+
if disk_devices:
|
|
135
|
+
total_kb = sum(d.capacityInKB for d in disk_devices)
|
|
136
|
+
vm_data["disk_gb"] = round(total_kb / 1048576) # KB to GB
|
|
137
|
+
|
|
138
|
+
# Get network interfaces and IP addresses from VMware Tools
|
|
139
|
+
if vm.guest:
|
|
140
|
+
# Primary IP from guest info
|
|
141
|
+
if vm.guest.ipAddress:
|
|
142
|
+
vm_data["primary_ip"] = vm.guest.ipAddress
|
|
143
|
+
vm_data["ip_addresses"].append(vm.guest.ipAddress)
|
|
144
|
+
|
|
145
|
+
# Get all network interfaces
|
|
146
|
+
if vm.guest.net:
|
|
147
|
+
for nic in vm.guest.net:
|
|
148
|
+
interface = {
|
|
149
|
+
"name": nic.network or "Unknown",
|
|
150
|
+
"mac": nic.macAddress,
|
|
151
|
+
"connected": nic.connected,
|
|
152
|
+
"ip_addresses": [],
|
|
153
|
+
}
|
|
154
|
+
if nic.ipConfig and nic.ipConfig.ipAddress:
|
|
155
|
+
for ip_info in nic.ipConfig.ipAddress:
|
|
156
|
+
ip = ip_info.ipAddress
|
|
157
|
+
interface["ip_addresses"].append(ip)
|
|
158
|
+
if ip not in vm_data["ip_addresses"]:
|
|
159
|
+
vm_data["ip_addresses"].append(ip)
|
|
160
|
+
vm_data["interfaces"].append(interface)
|
|
161
|
+
|
|
162
|
+
# Get cluster and datacenter
|
|
163
|
+
if vm.runtime.host:
|
|
164
|
+
host = vm.runtime.host
|
|
165
|
+
if host.parent and isinstance(host.parent, vim.ClusterComputeResource):
|
|
166
|
+
vm_data["cluster"] = host.parent.name
|
|
167
|
+
# Walk up to find datacenter
|
|
168
|
+
parent = host.parent
|
|
169
|
+
while parent:
|
|
170
|
+
if isinstance(parent, vim.Datacenter):
|
|
171
|
+
vm_data["datacenter"] = parent.name
|
|
172
|
+
break
|
|
173
|
+
parent = getattr(parent, "parent", None)
|
|
174
|
+
|
|
175
|
+
vm_list.append(vm_data)
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.warning(f"Error processing VM {vm.name}: {e}")
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
logger.info(f"Fetched {len(vm_list)} VMs from {self.server}")
|
|
182
|
+
return vm_list
|
|
183
|
+
|
|
184
|
+
def fetch_clusters(self) -> list:
|
|
185
|
+
"""
|
|
186
|
+
Fetch all clusters from vCenter.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
List of cluster dictionaries
|
|
190
|
+
"""
|
|
191
|
+
clusters = self._get_objects_of_type(vim.ClusterComputeResource)
|
|
192
|
+
cluster_list = []
|
|
193
|
+
|
|
194
|
+
for cluster in clusters:
|
|
195
|
+
cluster_list.append(
|
|
196
|
+
{
|
|
197
|
+
"name": cluster.name,
|
|
198
|
+
"host_count": len(cluster.host) if cluster.host else 0,
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return cluster_list
|
|
203
|
+
|
|
204
|
+
def fetch_datacenters(self) -> list:
|
|
205
|
+
"""
|
|
206
|
+
Fetch all datacenters from vCenter.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of datacenter dictionaries
|
|
210
|
+
"""
|
|
211
|
+
datacenters = self._get_objects_of_type(vim.Datacenter)
|
|
212
|
+
return [{"name": dc.name} for dc in datacenters]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def connect_and_fetch(
|
|
216
|
+
server: str, username: str, password: str, verify_ssl: bool = False
|
|
217
|
+
) -> tuple[Optional[list], Optional[str]]:
|
|
218
|
+
"""
|
|
219
|
+
Connect to vCenter and fetch all VMs.
|
|
220
|
+
|
|
221
|
+
This is a convenience function that handles connection/disconnection.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
server: vCenter server hostname
|
|
225
|
+
username: vCenter username
|
|
226
|
+
password: vCenter password
|
|
227
|
+
verify_ssl: Whether to verify SSL certificates
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Tuple of (vm_list, error_message)
|
|
231
|
+
- On success: (list of VMs, None)
|
|
232
|
+
- On failure: (None, error message)
|
|
233
|
+
"""
|
|
234
|
+
client = VCenterClient(server, username, password, verify_ssl)
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
client.connect()
|
|
238
|
+
vms = client.fetch_all_vms()
|
|
239
|
+
return vms, None
|
|
240
|
+
except vim.fault.InvalidLogin as e:
|
|
241
|
+
logger.error(f"vCenter authentication failed: {e.msg}")
|
|
242
|
+
return None, f"Authentication failed: Invalid username or password"
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"vCenter connection failed: {e}")
|
|
245
|
+
return None, f"Connection failed: {str(e)}"
|
|
246
|
+
finally:
|
|
247
|
+
client.disconnect()
|
netbox_vcenter/forms.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Forms for NetBox vCenter plugin."""
|
|
2
|
+
|
|
3
|
+
from django import forms
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from virtualization.models import Cluster
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VCenterConnectForm(forms.Form):
|
|
9
|
+
"""Form for connecting to a vCenter server."""
|
|
10
|
+
|
|
11
|
+
server = forms.ChoiceField(
|
|
12
|
+
label="vCenter Server",
|
|
13
|
+
help_text="Select the vCenter server to connect to",
|
|
14
|
+
widget=forms.Select(attrs={"class": "form-select"}),
|
|
15
|
+
)
|
|
16
|
+
username = forms.CharField(
|
|
17
|
+
label="Username",
|
|
18
|
+
help_text="Enter your username (e.g., domain\\user or user@domain)",
|
|
19
|
+
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "domain\\username"}),
|
|
20
|
+
)
|
|
21
|
+
password = forms.CharField(
|
|
22
|
+
label="Password",
|
|
23
|
+
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
|
24
|
+
)
|
|
25
|
+
verify_ssl = forms.BooleanField(
|
|
26
|
+
label="Verify SSL Certificate",
|
|
27
|
+
required=False,
|
|
28
|
+
initial=False,
|
|
29
|
+
help_text="Enable for trusted certificates (disable for self-signed)",
|
|
30
|
+
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def __init__(self, *args, **kwargs):
|
|
34
|
+
super().__init__(*args, **kwargs)
|
|
35
|
+
# Populate server choices from plugin config
|
|
36
|
+
config = settings.PLUGINS_CONFIG.get("netbox_vcenter", {})
|
|
37
|
+
servers = config.get("vcenter_servers", [])
|
|
38
|
+
self.fields["server"].choices = [(s, s) for s in servers]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VMImportForm(forms.Form):
|
|
42
|
+
"""Form for importing VMs from vCenter to NetBox."""
|
|
43
|
+
|
|
44
|
+
cluster = forms.ModelChoiceField(
|
|
45
|
+
queryset=Cluster.objects.all(),
|
|
46
|
+
label="Target Cluster",
|
|
47
|
+
help_text="NetBox cluster to assign the imported VMs to",
|
|
48
|
+
widget=forms.Select(attrs={"class": "form-select"}),
|
|
49
|
+
)
|
|
50
|
+
selected_vms = forms.CharField(
|
|
51
|
+
widget=forms.HiddenInput(),
|
|
52
|
+
help_text="JSON list of selected VM names",
|
|
53
|
+
)
|
|
54
|
+
vcenter_server = forms.CharField(
|
|
55
|
+
widget=forms.HiddenInput(),
|
|
56
|
+
help_text="Source vCenter server",
|
|
57
|
+
)
|
|
58
|
+
update_existing = forms.BooleanField(
|
|
59
|
+
label="Update existing VMs",
|
|
60
|
+
required=False,
|
|
61
|
+
initial=False,
|
|
62
|
+
help_text="Update vCPUs, memory, disk, status, and IP address for VMs that already exist in NetBox",
|
|
63
|
+
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def clean_selected_vms(self):
|
|
67
|
+
"""Parse the selected VMs JSON string."""
|
|
68
|
+
import json
|
|
69
|
+
|
|
70
|
+
data = self.cleaned_data.get("selected_vms", "[]")
|
|
71
|
+
try:
|
|
72
|
+
vms = json.loads(data)
|
|
73
|
+
if not isinstance(vms, list):
|
|
74
|
+
raise forms.ValidationError("Invalid VM selection format")
|
|
75
|
+
return vms
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
raise forms.ValidationError("Invalid VM selection data")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Navigation menu items for NetBox vCenter plugin."""
|
|
2
|
+
|
|
3
|
+
from netbox.plugins import PluginMenu, PluginMenuItem
|
|
4
|
+
|
|
5
|
+
menu = PluginMenu(
|
|
6
|
+
label="vCenter",
|
|
7
|
+
groups=(
|
|
8
|
+
(
|
|
9
|
+
"Virtual Machines",
|
|
10
|
+
(
|
|
11
|
+
PluginMenuItem(
|
|
12
|
+
link="plugins:netbox_vcenter:dashboard",
|
|
13
|
+
link_text="Import Dashboard",
|
|
14
|
+
permissions=["virtualization.view_virtualmachine"],
|
|
15
|
+
),
|
|
16
|
+
PluginMenuItem(
|
|
17
|
+
link="plugins:netbox_vcenter:compare",
|
|
18
|
+
link_text="Compare with NetBox",
|
|
19
|
+
permissions=["virtualization.view_virtualmachine"],
|
|
20
|
+
),
|
|
21
|
+
),
|
|
22
|
+
),
|
|
23
|
+
),
|
|
24
|
+
icon_class="mdi mdi-server",
|
|
25
|
+
)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
{% extends 'base/layout.html' %}
|
|
2
|
+
{% load helpers %}
|
|
3
|
+
|
|
4
|
+
{% block title %}vCenter vs NetBox Comparison{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<div class="row mb-4">
|
|
8
|
+
<div class="col-md-12">
|
|
9
|
+
<div class="card">
|
|
10
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
11
|
+
<h5 class="card-title mb-0">
|
|
12
|
+
<i class="mdi mdi-compare"></i> Compare vCenter with NetBox
|
|
13
|
+
</h5>
|
|
14
|
+
<div>
|
|
15
|
+
<select class="form-select form-select-sm d-inline-block w-auto" id="server-select">
|
|
16
|
+
<option value="">Select vCenter...</option>
|
|
17
|
+
{% for s in servers %}
|
|
18
|
+
<option value="{{ s }}" {% if s == server %}selected{% endif %}>{{ s }}</option>
|
|
19
|
+
{% endfor %}
|
|
20
|
+
</select>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="card-body">
|
|
24
|
+
{% if not server %}
|
|
25
|
+
<div class="alert alert-info">
|
|
26
|
+
<i class="mdi mdi-information"></i>
|
|
27
|
+
Select a vCenter server to compare with NetBox VMs.
|
|
28
|
+
</div>
|
|
29
|
+
{% elif not cached_data %}
|
|
30
|
+
<div class="alert alert-warning">
|
|
31
|
+
<i class="mdi mdi-alert"></i>
|
|
32
|
+
No cached data for <strong>{{ server }}</strong>.
|
|
33
|
+
<a href="{% url 'plugins:netbox_vcenter:dashboard' %}?server={{ server }}">Sync first</a>.
|
|
34
|
+
</div>
|
|
35
|
+
{% else %}
|
|
36
|
+
<!-- Summary Stats -->
|
|
37
|
+
<div class="row mb-4">
|
|
38
|
+
<div class="col-md-3">
|
|
39
|
+
<div class="card bg-primary text-white">
|
|
40
|
+
<div class="card-body text-center">
|
|
41
|
+
<h3 class="mb-0">{{ in_both_count }}</h3>
|
|
42
|
+
<small>In Both</small>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="col-md-3">
|
|
47
|
+
<div class="card bg-success text-white">
|
|
48
|
+
<div class="card-body text-center">
|
|
49
|
+
<h3 class="mb-0">{{ only_vcenter_count }}</h3>
|
|
50
|
+
<small>Only in vCenter</small>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="col-md-3">
|
|
55
|
+
<div class="card bg-secondary text-white">
|
|
56
|
+
<div class="card-body text-center">
|
|
57
|
+
<h3 class="mb-0">{{ only_netbox_count }}</h3>
|
|
58
|
+
<small>Only in NetBox</small>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="col-md-3">
|
|
63
|
+
<div class="card bg-danger text-white">
|
|
64
|
+
<div class="card-body text-center">
|
|
65
|
+
<h3 class="mb-0">{{ diff_count }}</h3>
|
|
66
|
+
<small>Spec Differences</small>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<!-- Only in vCenter (Can Import) -->
|
|
73
|
+
{% if comparison.only_in_vcenter %}
|
|
74
|
+
<h5 class="mt-4 mb-3">
|
|
75
|
+
<span class="badge bg-success text-white">{{ only_vcenter_count }}</span>
|
|
76
|
+
Only in vCenter (Can Import)
|
|
77
|
+
</h5>
|
|
78
|
+
<div class="table-responsive mb-4">
|
|
79
|
+
<table class="table table-sm table-hover table-striped">
|
|
80
|
+
<thead class="table-success">
|
|
81
|
+
<tr>
|
|
82
|
+
<th>VM Name</th>
|
|
83
|
+
<th>Power</th>
|
|
84
|
+
<th>vCPUs</th>
|
|
85
|
+
<th>Memory</th>
|
|
86
|
+
<th>Disk</th>
|
|
87
|
+
<th>Cluster</th>
|
|
88
|
+
</tr>
|
|
89
|
+
</thead>
|
|
90
|
+
<tbody>
|
|
91
|
+
{% for vm in comparison.only_in_vcenter %}
|
|
92
|
+
<tr>
|
|
93
|
+
<td><strong>{{ vm.name }}</strong></td>
|
|
94
|
+
<td>
|
|
95
|
+
{% if vm.power_state == 'on' %}
|
|
96
|
+
<span class="badge bg-success text-white">ON</span>
|
|
97
|
+
{% else %}
|
|
98
|
+
<span class="badge bg-secondary text-white">OFF</span>
|
|
99
|
+
{% endif %}
|
|
100
|
+
</td>
|
|
101
|
+
<td>{{ vm.vcpus|default:'-' }}</td>
|
|
102
|
+
<td>{{ vm.memory_mb|default:'-' }}</td>
|
|
103
|
+
<td>{{ vm.disk_gb|default:'-' }}</td>
|
|
104
|
+
<td>{{ vm.cluster|default:'-' }}</td>
|
|
105
|
+
</tr>
|
|
106
|
+
{% endfor %}
|
|
107
|
+
</tbody>
|
|
108
|
+
</table>
|
|
109
|
+
</div>
|
|
110
|
+
{% endif %}
|
|
111
|
+
|
|
112
|
+
<!-- VMs with Differences -->
|
|
113
|
+
{% if diff_count > 0 %}
|
|
114
|
+
<div class="d-flex justify-content-between align-items-center mt-4 mb-3">
|
|
115
|
+
<h5 class="mb-0">
|
|
116
|
+
<span class="badge bg-danger text-white">{{ diff_count }}</span>
|
|
117
|
+
VMs with Spec Differences
|
|
118
|
+
</h5>
|
|
119
|
+
<form method="post" action="{% url 'plugins:netbox_vcenter:sync_differences' %}" class="d-inline">
|
|
120
|
+
{% csrf_token %}
|
|
121
|
+
<input type="hidden" name="server" value="{{ server }}">
|
|
122
|
+
<button type="submit" class="btn btn-sm btn-primary"
|
|
123
|
+
onclick="return confirm('Sync {{ diff_count }} VM(s) with spec differences from vCenter to NetBox?');">
|
|
124
|
+
<i class="mdi mdi-sync"></i> Sync All Differences
|
|
125
|
+
</button>
|
|
126
|
+
</form>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="table-responsive mb-4">
|
|
129
|
+
<table class="table table-sm table-hover table-striped">
|
|
130
|
+
<thead class="table-danger">
|
|
131
|
+
<tr>
|
|
132
|
+
<th>VM Name</th>
|
|
133
|
+
<th>Field</th>
|
|
134
|
+
<th>vCenter</th>
|
|
135
|
+
<th>NetBox</th>
|
|
136
|
+
</tr>
|
|
137
|
+
</thead>
|
|
138
|
+
<tbody>
|
|
139
|
+
{% for vm in comparison.in_both %}
|
|
140
|
+
{% if vm.has_differences %}
|
|
141
|
+
{% if vm.vcenter.vcpus != vm.netbox.vcpus %}
|
|
142
|
+
<tr>
|
|
143
|
+
<td><strong>{{ vm.name }}</strong></td>
|
|
144
|
+
<td>vCPUs</td>
|
|
145
|
+
<td>{{ vm.vcenter.vcpus|default:'-' }}</td>
|
|
146
|
+
<td>{{ vm.netbox.vcpus|default:'-' }}</td>
|
|
147
|
+
</tr>
|
|
148
|
+
{% endif %}
|
|
149
|
+
{% if vm.vcenter.memory_mb != vm.netbox.memory_mb %}
|
|
150
|
+
<tr>
|
|
151
|
+
<td><strong>{{ vm.name }}</strong></td>
|
|
152
|
+
<td>Memory (MB)</td>
|
|
153
|
+
<td>{{ vm.vcenter.memory_mb|default:'-' }}</td>
|
|
154
|
+
<td>{{ vm.netbox.memory_mb|default:'-' }}</td>
|
|
155
|
+
</tr>
|
|
156
|
+
{% endif %}
|
|
157
|
+
{% if vm.vcenter.disk_gb != vm.netbox.disk_gb %}
|
|
158
|
+
<tr>
|
|
159
|
+
<td><strong>{{ vm.name }}</strong></td>
|
|
160
|
+
<td>Disk (GB)</td>
|
|
161
|
+
<td>{{ vm.vcenter.disk_gb|default:'-' }}</td>
|
|
162
|
+
<td>{{ vm.netbox.disk_gb|default:'-' }}</td>
|
|
163
|
+
</tr>
|
|
164
|
+
{% endif %}
|
|
165
|
+
{% endif %}
|
|
166
|
+
{% endfor %}
|
|
167
|
+
</tbody>
|
|
168
|
+
</table>
|
|
169
|
+
</div>
|
|
170
|
+
{% endif %}
|
|
171
|
+
|
|
172
|
+
<!-- Only in NetBox (Orphaned?) -->
|
|
173
|
+
{% if comparison.only_in_netbox %}
|
|
174
|
+
<h5 class="mt-4 mb-3">
|
|
175
|
+
<span class="badge bg-secondary text-white">{{ only_netbox_count }}</span>
|
|
176
|
+
Only in NetBox (Not in {{ server }})
|
|
177
|
+
</h5>
|
|
178
|
+
<div class="table-responsive mb-4">
|
|
179
|
+
<table class="table table-sm table-hover table-striped">
|
|
180
|
+
<thead class="table-secondary">
|
|
181
|
+
<tr>
|
|
182
|
+
<th>VM Name</th>
|
|
183
|
+
<th>vCPUs</th>
|
|
184
|
+
<th>Memory</th>
|
|
185
|
+
<th>NetBox Cluster</th>
|
|
186
|
+
</tr>
|
|
187
|
+
</thead>
|
|
188
|
+
<tbody>
|
|
189
|
+
{% for vm in comparison.only_in_netbox %}
|
|
190
|
+
<tr>
|
|
191
|
+
<td><strong>{{ vm.name }}</strong></td>
|
|
192
|
+
<td>{{ vm.vcpus|default:'-' }}</td>
|
|
193
|
+
<td>{{ vm.memory_mb|default:'-' }}</td>
|
|
194
|
+
<td>{{ vm.cluster|default:'-' }}</td>
|
|
195
|
+
</tr>
|
|
196
|
+
{% endfor %}
|
|
197
|
+
</tbody>
|
|
198
|
+
</table>
|
|
199
|
+
</div>
|
|
200
|
+
<p class="text-muted">
|
|
201
|
+
<i class="mdi mdi-information"></i>
|
|
202
|
+
These VMs exist in NetBox but were not found in <strong>{{ server }}</strong>.
|
|
203
|
+
They may be on a different vCenter or have been deleted.
|
|
204
|
+
</p>
|
|
205
|
+
{% endif %}
|
|
206
|
+
{% endif %}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<script>
|
|
213
|
+
document.getElementById('server-select').addEventListener('change', function() {
|
|
214
|
+
if (this.value) {
|
|
215
|
+
window.location.href = '?server=' + this.value;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
</script>
|
|
219
|
+
{% endblock %}
|