Sync VMware virtual disks into NetBox
This commit is contained in:
@@ -9,3 +9,4 @@
|
|||||||
| 0.1.4 | 4.4.0 | 4.6.x |
|
| 0.1.4 | 4.4.0 | 4.6.x |
|
||||||
| 0.1.5 | 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.6 | 4.4.0 | 4.6.x |
|
||||||
|
| 0.1.7 | 4.4.0 | 4.6.x |
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ NetBox plugin zum Synchronisieren von VMware vSphere/vCenter VMs nach NetBox.
|
|||||||
- Manuelle Synchronisation per Button
|
- Manuelle Synchronisation per Button
|
||||||
- Optionale automatische Synchronisation ueber ein Minutenintervall
|
- Optionale automatische Synchronisation ueber ein Minutenintervall
|
||||||
- Import von VM-Name, Status, vCPU, RAM, Disk, Plattform, Interfaces, MAC-Adressen und IPs
|
- 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
|
- Primaere IPv4/IPv6 wird anhand der ersten gefundenen Gast-IP gesetzt
|
||||||
- Multi-Tenant-sicherer VM-Abgleich ueber `name + cluster`
|
- Multi-Tenant-sicherer VM-Abgleich ueber `name + cluster`
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class VMwareImporterConfig(PluginConfig):
|
|||||||
name = "netbox_vmware_importer"
|
name = "netbox_vmware_importer"
|
||||||
verbose_name = "VMware Importer"
|
verbose_name = "VMware Importer"
|
||||||
description = "Synchronize VMware vSphere virtual machines into NetBox."
|
description = "Synchronize VMware vSphere virtual machines into NetBox."
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
author = "Internal NetBox Team"
|
author = "Internal NetBox Team"
|
||||||
base_url = "vmware-importer"
|
base_url = "vmware-importer"
|
||||||
min_version = "4.4.0"
|
min_version = "4.4.0"
|
||||||
|
|||||||
@@ -57,12 +57,18 @@ class SyncVCenterEndpointJob(JobRunner):
|
|||||||
|
|
||||||
endpoint.mark_success(result)
|
endpoint.mark_success(result)
|
||||||
self.logger.info(
|
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,
|
endpoint.name,
|
||||||
result.synced,
|
result.synced,
|
||||||
result.created,
|
result.created,
|
||||||
result.updated,
|
result.updated,
|
||||||
result.skipped,
|
result.skipped,
|
||||||
|
result.virtual_disks_created,
|
||||||
|
result.virtual_disks_updated,
|
||||||
|
result.virtual_disks_deleted,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ from dataclasses import dataclass, field
|
|||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import Sum
|
||||||
from dcim.models import Platform
|
from dcim.models import Platform
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
from virtualization.models import VMInterface, VirtualMachine
|
from virtualization.models import VMInterface, VirtualDisk, VirtualMachine
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pyVim.connect import Disconnect, SmartConnect
|
from pyVim.connect import Disconnect, SmartConnect
|
||||||
@@ -25,6 +26,13 @@ class InterfaceData:
|
|||||||
ip_addresses: list[str] = field(default_factory=list)
|
ip_addresses: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DiskData:
|
||||||
|
name: str
|
||||||
|
size_mb: int
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class VMData:
|
class VMData:
|
||||||
name: str
|
name: str
|
||||||
@@ -33,6 +41,7 @@ class VMData:
|
|||||||
memory_mb: int
|
memory_mb: int
|
||||||
disk_mb: int
|
disk_mb: int
|
||||||
guest_os: str = ""
|
guest_os: str = ""
|
||||||
|
disks: list[DiskData] = field(default_factory=list)
|
||||||
interfaces: list[InterfaceData] = field(default_factory=list)
|
interfaces: list[InterfaceData] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +58,9 @@ class SyncResult:
|
|||||||
ip_addresses_created: int = 0
|
ip_addresses_created: int = 0
|
||||||
ip_addresses_updated: int = 0
|
ip_addresses_updated: int = 0
|
||||||
ip_conflicts: int = 0
|
ip_conflicts: int = 0
|
||||||
|
virtual_disks_created: int = 0
|
||||||
|
virtual_disks_updated: int = 0
|
||||||
|
virtual_disks_deleted: int = 0
|
||||||
|
|
||||||
|
|
||||||
class VMwareConnectionError(RuntimeError):
|
class VMwareConnectionError(RuntimeError):
|
||||||
@@ -101,7 +113,8 @@ class VMwareClient:
|
|||||||
|
|
||||||
memory_mb = int(getattr(hardware, "memoryMB", 0) or 0)
|
memory_mb = int(getattr(hardware, "memoryMB", 0) or 0)
|
||||||
vcpus = int(getattr(hardware, "numCPU", 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 ""
|
guest_os = getattr(config, "guestFullName", "") or ""
|
||||||
power_state = str(getattr(getattr(vm_obj, "runtime", None), "powerState", ""))
|
power_state = str(getattr(getattr(vm_obj, "runtime", None), "powerState", ""))
|
||||||
status = "active" if "poweredOn" in power_state else "offline"
|
status = "active" if "poweredOn" in power_state else "offline"
|
||||||
@@ -113,16 +126,64 @@ class VMwareClient:
|
|||||||
memory_mb=memory_mb,
|
memory_mb=memory_mb,
|
||||||
disk_mb=disk_mb,
|
disk_mb=disk_mb,
|
||||||
guest_os=guest_os,
|
guest_os=guest_os,
|
||||||
|
disks=disks,
|
||||||
interfaces=self._get_interfaces(vm_obj),
|
interfaces=self._get_interfaces(vm_obj),
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_disk_size_mb(hardware):
|
def _get_disks(hardware):
|
||||||
disk_mb = 0
|
disks = []
|
||||||
|
used_names = set()
|
||||||
|
disk_index = 0
|
||||||
|
|
||||||
for device in getattr(hardware, "device", []) or []:
|
for device in getattr(hardware, "device", []) or []:
|
||||||
if isinstance(device, vim.vm.device.VirtualDisk):
|
if isinstance(device, vim.vm.device.VirtualDisk):
|
||||||
disk_mb += round((device.capacityInKB or 0) / 1024)
|
disk_index += 1
|
||||||
return disk_mb
|
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
|
@staticmethod
|
||||||
def _get_interfaces(vm_obj):
|
def _get_interfaces(vm_obj):
|
||||||
@@ -205,10 +266,12 @@ class VMwareImporter:
|
|||||||
nb_vm.status = vm_data.status
|
nb_vm.status = vm_data.status
|
||||||
nb_vm.vcpus = vm_data.vcpus
|
nb_vm.vcpus = vm_data.vcpus
|
||||||
nb_vm.memory = vm_data.memory_mb
|
nb_vm.memory = vm_data.memory_mb
|
||||||
nb_vm.disk = vm_data.disk_mb
|
|
||||||
nb_vm.tenant = self.endpoint.tenant
|
nb_vm.tenant = self.endpoint.tenant
|
||||||
nb_vm.platform = self._match_platform(vm_data.guest_os, platforms)
|
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.full_clean()
|
||||||
nb_vm.save()
|
nb_vm.save()
|
||||||
|
|
||||||
@@ -219,6 +282,8 @@ class VMwareImporter:
|
|||||||
result.updated += 1
|
result.updated += 1
|
||||||
self.logger.info("Updated VM %s", vm_data.name)
|
self.logger.info("Updated VM %s", vm_data.name)
|
||||||
|
|
||||||
|
self._sync_virtual_disks(nb_vm, vm_data, result)
|
||||||
|
|
||||||
if self.endpoint.sync_interfaces:
|
if self.endpoint.sync_interfaces:
|
||||||
primary_ipv4, primary_ipv6 = self._sync_interfaces(nb_vm, vm_data, result)
|
primary_ipv4, primary_ipv6 = self._sync_interfaces(nb_vm, vm_data, result)
|
||||||
self._set_primary_ips(nb_vm, primary_ipv4, primary_ipv6)
|
self._set_primary_ips(nb_vm, primary_ipv4, primary_ipv6)
|
||||||
@@ -235,6 +300,55 @@ class VMwareImporter:
|
|||||||
|
|
||||||
return None
|
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):
|
def _sync_interfaces(self, nb_vm, vm_data, result):
|
||||||
primary_ipv4 = None
|
primary_ipv4 = None
|
||||||
primary_ipv6 = None
|
primary_ipv6 = None
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-vmware-importer"
|
name = "netbox-vmware-importer"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
description = "NetBox plugin to synchronize VMware vSphere virtual machines into NetBox."
|
description = "NetBox plugin to synchronize VMware vSphere virtual machines into NetBox."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
Reference in New Issue
Block a user