Sync VMware virtual disks into NetBox

This commit is contained in:
2026-07-10 10:32:03 +02:00
parent 8ac7ae134b
commit 58af2bde5d
6 changed files with 132 additions and 10 deletions
+1
View File
@@ -9,3 +9,4 @@
| 0.1.4 | 4.4.0 | 4.6.x |
| 0.1.5 | 4.4.0 | 4.6.x |
| 0.1.6 | 4.4.0 | 4.6.x |
| 0.1.7 | 4.4.0 | 4.6.x |
+1
View File
@@ -9,6 +9,7 @@ NetBox plugin zum Synchronisieren von VMware vSphere/vCenter VMs nach NetBox.
- Manuelle Synchronisation per Button
- Optionale automatische Synchronisation ueber ein Minutenintervall
- Import von VM-Name, Status, vCPU, RAM, Disk, Plattform, Interfaces, MAC-Adressen und IPs
- Virtuelle VMware-Festplatten werden als NetBox Virtual Disks mit Groesse und Backing-Info dokumentiert
- Primaere IPv4/IPv6 wird anhand der ersten gefundenen Gast-IP gesetzt
- Multi-Tenant-sicherer VM-Abgleich ueber `name + cluster`
+1 -1
View File
@@ -5,7 +5,7 @@ class VMwareImporterConfig(PluginConfig):
name = "netbox_vmware_importer"
verbose_name = "VMware Importer"
description = "Synchronize VMware vSphere virtual machines into NetBox."
version = "0.1.6"
version = "0.1.7"
author = "Internal NetBox Team"
base_url = "vmware-importer"
min_version = "4.4.0"
+7 -1
View File
@@ -57,12 +57,18 @@ class SyncVCenterEndpointJob(JobRunner):
endpoint.mark_success(result)
self.logger.info(
"VMware sync finished for %s: %s VM(s), %s created, %s updated, %s skipped.",
(
"VMware sync finished for %s: %s VM(s), %s created, %s updated, %s skipped; "
"virtual disks: %s created, %s updated, %s deleted."
),
endpoint.name,
result.synced,
result.created,
result.updated,
result.skipped,
result.virtual_disks_created,
result.virtual_disks_updated,
result.virtual_disks_deleted,
)
+121 -7
View File
@@ -5,9 +5,10 @@ from dataclasses import dataclass, field
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import Sum
from dcim.models import Platform
from ipam.models import IPAddress
from virtualization.models import VMInterface, VirtualMachine
from virtualization.models import VMInterface, VirtualDisk, VirtualMachine
try:
from pyVim.connect import Disconnect, SmartConnect
@@ -25,6 +26,13 @@ class InterfaceData:
ip_addresses: list[str] = field(default_factory=list)
@dataclass
class DiskData:
name: str
size_mb: int
description: str = ""
@dataclass
class VMData:
name: str
@@ -33,6 +41,7 @@ class VMData:
memory_mb: int
disk_mb: int
guest_os: str = ""
disks: list[DiskData] = field(default_factory=list)
interfaces: list[InterfaceData] = field(default_factory=list)
@@ -49,6 +58,9 @@ class SyncResult:
ip_addresses_created: int = 0
ip_addresses_updated: int = 0
ip_conflicts: int = 0
virtual_disks_created: int = 0
virtual_disks_updated: int = 0
virtual_disks_deleted: int = 0
class VMwareConnectionError(RuntimeError):
@@ -101,7 +113,8 @@ class VMwareClient:
memory_mb = int(getattr(hardware, "memoryMB", 0) or 0)
vcpus = int(getattr(hardware, "numCPU", 0) or 0)
disk_mb = self._get_disk_size_mb(hardware)
disks = self._get_disks(hardware)
disk_mb = sum(disk.size_mb for disk in disks)
guest_os = getattr(config, "guestFullName", "") or ""
power_state = str(getattr(getattr(vm_obj, "runtime", None), "powerState", ""))
status = "active" if "poweredOn" in power_state else "offline"
@@ -113,16 +126,64 @@ class VMwareClient:
memory_mb=memory_mb,
disk_mb=disk_mb,
guest_os=guest_os,
disks=disks,
interfaces=self._get_interfaces(vm_obj),
)
@staticmethod
def _get_disk_size_mb(hardware):
disk_mb = 0
def _get_disks(hardware):
disks = []
used_names = set()
disk_index = 0
for device in getattr(hardware, "device", []) or []:
if isinstance(device, vim.vm.device.VirtualDisk):
disk_mb += round((device.capacityInKB or 0) / 1024)
return disk_mb
disk_index += 1
name = VMwareClient._unique_disk_name(device, disk_index, used_names)
disks.append(
DiskData(
name=name,
size_mb=round((device.capacityInKB or 0) / 1024),
description=VMwareClient._disk_description(device),
)
)
return disks
@staticmethod
def _unique_disk_name(device, disk_index, used_names):
device_info = getattr(device, "deviceInfo", None)
base_name = getattr(device_info, "label", None) or f"Hard disk {disk_index}"
base_name = VMwareClient._truncate_component_name(str(base_name).strip() or f"Hard disk {disk_index}")
name = base_name
suffix = 2
while name in used_names:
suffix_text = f" ({suffix})"
name = f"{base_name[:64 - len(suffix_text)]}{suffix_text}"
suffix += 1
used_names.add(name)
return name
@staticmethod
def _truncate_component_name(value):
return value[:64]
@staticmethod
def _disk_description(device):
backing = getattr(device, "backing", None)
filename = getattr(backing, "fileName", None)
thin_provisioned = getattr(backing, "thinProvisioned", None)
parts = []
if filename:
parts.append(str(filename))
if thin_provisioned is not None:
parts.append("thin" if thin_provisioned else "thick")
detail = ", ".join(parts) if parts else f"key={getattr(device, 'key', 'unknown')}"
return f"VMware: {detail}"[:200]
@staticmethod
def _get_interfaces(vm_obj):
@@ -205,10 +266,12 @@ class VMwareImporter:
nb_vm.status = vm_data.status
nb_vm.vcpus = vm_data.vcpus
nb_vm.memory = vm_data.memory_mb
nb_vm.disk = vm_data.disk_mb
nb_vm.tenant = self.endpoint.tenant
nb_vm.platform = self._match_platform(vm_data.guest_os, platforms)
if created or not nb_vm.virtualdisks.exists():
nb_vm.disk = vm_data.disk_mb
nb_vm.full_clean()
nb_vm.save()
@@ -219,6 +282,8 @@ class VMwareImporter:
result.updated += 1
self.logger.info("Updated VM %s", vm_data.name)
self._sync_virtual_disks(nb_vm, vm_data, result)
if self.endpoint.sync_interfaces:
primary_ipv4, primary_ipv6 = self._sync_interfaces(nb_vm, vm_data, result)
self._set_primary_ips(nb_vm, primary_ipv4, primary_ipv6)
@@ -235,6 +300,55 @@ class VMwareImporter:
return None
def _sync_virtual_disks(self, nb_vm, vm_data, result):
if not vm_data.disks:
return
synced_names = set()
for disk_data in vm_data.disks:
synced_names.add(disk_data.name)
virtual_disk = VirtualDisk.objects.filter(
virtual_machine=nb_vm,
name=disk_data.name,
).first()
created = virtual_disk is None
if created:
virtual_disk = VirtualDisk(
virtual_machine=nb_vm,
name=disk_data.name,
)
virtual_disk.size = disk_data.size_mb
virtual_disk.description = disk_data.description
virtual_disk.full_clean()
virtual_disk.save()
if created:
result.virtual_disks_created += 1
self.logger.info("Created virtual disk %s on %s", disk_data.name, nb_vm.name)
else:
result.virtual_disks_updated += 1
self.logger.info("Updated virtual disk %s on %s", disk_data.name, nb_vm.name)
stale_disks = VirtualDisk.objects.filter(
virtual_machine=nb_vm,
description__startswith="VMware:",
).exclude(name__in=synced_names)
stale_count = stale_disks.count()
if stale_count:
stale_disks.delete()
result.virtual_disks_deleted += stale_count
self.logger.info("Deleted %s stale VMware virtual disk(s) from %s", stale_count, nb_vm.name)
disk_total = nb_vm.virtualdisks.aggregate(total=Sum("size", default=0))["total"] or vm_data.disk_mb
if nb_vm.disk != disk_total:
nb_vm.disk = disk_total
nb_vm.full_clean()
nb_vm.save(update_fields=("disk",))
self.logger.info("Updated aggregate disk size for %s to %s MB", nb_vm.name, disk_total)
def _sync_interfaces(self, nb_vm, vm_data, result):
primary_ipv4 = None
primary_ipv6 = None
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "netbox-vmware-importer"
version = "0.1.6"
version = "0.1.7"
description = "NetBox plugin to synchronize VMware vSphere virtual machines into NetBox."
readme = "README.md"
requires-python = ">=3.12"