Sync MAC addresses and schedule endpoint sync jobs
This commit is contained in:
@@ -10,3 +10,4 @@
|
||||
| 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 |
|
||||
| 0.1.8 | 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
|
||||
- Optionale automatische Synchronisation ueber ein Minutenintervall
|
||||
- Import von VM-Name, Status, vCPU, RAM, Disk, Plattform, Interfaces, MAC-Adressen und IPs
|
||||
- MAC-Adressen werden als NetBox MAC Address Objekte am VM-Interface angelegt und als primaere MAC gesetzt
|
||||
- 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`
|
||||
@@ -50,6 +51,8 @@ sudo systemctl restart netbox netbox-rq
|
||||
|
||||
Wenn `Sync interval minutes` gesetzt ist, prueft ein Systemjob alle fuenf Minuten, welche Endpoints faellig sind, und stellt die eigentlichen Sync-Jobs in die Queue.
|
||||
|
||||
Ab Version `0.1.8` wird beim Speichern eines Endpoints zusaetzlich ein wiederkehrender NetBox-Job fuer genau diesen Endpoint geplant. Nach einem Update vorhandene Endpoints einmal speichern oder `netbox-rq` neu starten und bis zum naechsten Systemjob-Lauf warten.
|
||||
|
||||
## Sicherheit
|
||||
|
||||
Das vCenter-Passwort wird verschluesselt in der Plugin-Tabelle gespeichert. Der Schluessel wird aus `SECRET_KEY` abgeleitet. Wenn `SECRET_KEY` rotiert wird, muessen die vCenter-Passwoerter in den Endpoints erneut gesetzt werden.
|
||||
|
||||
@@ -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.7"
|
||||
version = "0.1.8"
|
||||
author = "Internal NetBox Team"
|
||||
base_url = "vmware-importer"
|
||||
min_version = "4.4.0"
|
||||
@@ -15,6 +15,7 @@ class VMwareImporterConfig(PluginConfig):
|
||||
|
||||
# Importing registers the scheduler system job with NetBox.
|
||||
from . import jobs # noqa: F401
|
||||
from . import signals # noqa: F401
|
||||
|
||||
|
||||
config = VMwareImporterConfig
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from core.choices import JobStatusChoices
|
||||
from core.exceptions import JobFailed
|
||||
from django.utils import timezone
|
||||
from netbox.jobs import JobRunner, system_job
|
||||
@@ -7,6 +8,29 @@ from .models import VCenterEndpoint
|
||||
from .sync import VMwareImporter
|
||||
|
||||
|
||||
def clear_endpoint_sync_schedule(endpoint):
|
||||
return SyncVCenterEndpointJob.get_jobs(endpoint).filter(
|
||||
status__in=JobStatusChoices.ENQUEUED_STATE_CHOICES,
|
||||
interval__isnull=False,
|
||||
).delete()
|
||||
|
||||
|
||||
def schedule_endpoint_sync(endpoint):
|
||||
if not endpoint.enabled or not endpoint.sync_interval_minutes:
|
||||
clear_endpoint_sync_schedule(endpoint)
|
||||
return None
|
||||
|
||||
if not endpoint.next_sync_at:
|
||||
endpoint.next_sync_at = timezone.now()
|
||||
VCenterEndpoint.objects.filter(pk=endpoint.pk).update(next_sync_at=endpoint.next_sync_at)
|
||||
|
||||
return SyncVCenterEndpointJob.enqueue_once(
|
||||
instance=endpoint,
|
||||
schedule_at=endpoint.next_sync_at,
|
||||
interval=endpoint.sync_interval_minutes,
|
||||
)
|
||||
|
||||
|
||||
class SyncVCenterEndpointJob(JobRunner):
|
||||
class Meta:
|
||||
name = "VMware VM synchronization"
|
||||
@@ -59,7 +83,8 @@ class SyncVCenterEndpointJob(JobRunner):
|
||||
self.logger.info(
|
||||
(
|
||||
"VMware sync finished for %s: %s VM(s), %s created, %s updated, %s skipped; "
|
||||
"virtual disks: %s created, %s updated, %s deleted."
|
||||
"virtual disks: %s created, %s updated, %s deleted; "
|
||||
"MAC addresses: %s created, %s updated, %s deleted."
|
||||
),
|
||||
endpoint.name,
|
||||
result.synced,
|
||||
@@ -69,6 +94,9 @@ class SyncVCenterEndpointJob(JobRunner):
|
||||
result.virtual_disks_created,
|
||||
result.virtual_disks_updated,
|
||||
result.virtual_disks_deleted,
|
||||
result.mac_addresses_created,
|
||||
result.mac_addresses_updated,
|
||||
result.mac_addresses_deleted,
|
||||
)
|
||||
|
||||
|
||||
@@ -79,18 +107,17 @@ class ScheduleDueVCenterSyncsJob(JobRunner):
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
now = timezone.now()
|
||||
due_endpoints = VCenterEndpoint.objects.filter(
|
||||
enabled_endpoints = VCenterEndpoint.objects.filter(
|
||||
enabled=True,
|
||||
sync_interval_minutes__isnull=False,
|
||||
).filter(next_sync_at__lte=now)
|
||||
)
|
||||
|
||||
queued = 0
|
||||
for endpoint in due_endpoints:
|
||||
SyncVCenterEndpointJob.enqueue_once(instance=endpoint)
|
||||
endpoint.last_status = SyncStatusChoices.STATUS_QUEUED
|
||||
endpoint.last_message = "Automatic sync job queued."
|
||||
endpoint.set_next_sync(now)
|
||||
endpoint.save(update_fields=("last_status", "last_message", "next_sync_at", "last_updated"))
|
||||
queued += 1
|
||||
scheduled = 0
|
||||
for endpoint in enabled_endpoints:
|
||||
if endpoint.next_sync_at is None:
|
||||
endpoint.next_sync_at = now
|
||||
endpoint.save(update_fields=("next_sync_at", "last_updated"))
|
||||
if schedule_endpoint_sync(endpoint):
|
||||
scheduled += 1
|
||||
|
||||
self.logger.info("Queued %s VMware sync job(s).", queued)
|
||||
self.logger.info("Ensured %s VMware endpoint sync schedule(s).", scheduled)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from django.db import transaction
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .jobs import clear_endpoint_sync_schedule, schedule_endpoint_sync
|
||||
from .models import VCenterEndpoint
|
||||
|
||||
|
||||
AUTOMATIC_SYNC_FIELDS = {"enabled", "sync_interval_minutes"}
|
||||
|
||||
|
||||
@receiver(post_save, sender=VCenterEndpoint)
|
||||
def schedule_vcenter_endpoint_sync(sender, instance, raw=False, update_fields=None, **kwargs):
|
||||
if raw:
|
||||
return
|
||||
|
||||
if update_fields is not None and not AUTOMATIC_SYNC_FIELDS.intersection(update_fields):
|
||||
return
|
||||
|
||||
transaction.on_commit(lambda: schedule_endpoint_sync(instance))
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=VCenterEndpoint)
|
||||
def clear_vcenter_endpoint_sync(sender, instance, **kwargs):
|
||||
clear_endpoint_sync_schedule(instance)
|
||||
@@ -6,7 +6,7 @@ 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 dcim.models import MACAddress, Platform
|
||||
from ipam.models import IPAddress
|
||||
from virtualization.models import VMInterface, VirtualDisk, VirtualMachine
|
||||
|
||||
@@ -58,6 +58,9 @@ class SyncResult:
|
||||
ip_addresses_created: int = 0
|
||||
ip_addresses_updated: int = 0
|
||||
ip_conflicts: int = 0
|
||||
mac_addresses_created: int = 0
|
||||
mac_addresses_updated: int = 0
|
||||
mac_addresses_deleted: int = 0
|
||||
virtual_disks_created: int = 0
|
||||
virtual_disks_updated: int = 0
|
||||
virtual_disks_deleted: int = 0
|
||||
@@ -214,11 +217,13 @@ class VMwareImporter:
|
||||
self.include_pattern = re.compile(endpoint.include_name_regex) if endpoint.include_name_regex else None
|
||||
self.exclude_pattern = re.compile(endpoint.exclude_name_regex) if endpoint.exclude_name_regex else None
|
||||
self.interface_content_type = None
|
||||
self.mac_address_content_type = None
|
||||
|
||||
def sync(self):
|
||||
result = SyncResult()
|
||||
platforms = list(Platform.objects.all())
|
||||
self.interface_content_type = ContentType.objects.get_for_model(VMInterface)
|
||||
self.mac_address_content_type = ContentType.objects.get_for_model(VMInterface)
|
||||
|
||||
with VMwareClient(self.endpoint) as client:
|
||||
for vm_data in client.iter_virtual_machines():
|
||||
@@ -366,7 +371,6 @@ class VMwareImporter:
|
||||
name=interface_data.name,
|
||||
)
|
||||
|
||||
vm_interface.mac_address = interface_data.mac_address
|
||||
vm_interface.enabled = True
|
||||
vm_interface.full_clean()
|
||||
vm_interface.save()
|
||||
@@ -378,6 +382,8 @@ class VMwareImporter:
|
||||
result.interfaces_updated += 1
|
||||
self.logger.info("Updated interface %s on %s", interface_data.name, nb_vm.name)
|
||||
|
||||
self._sync_mac_address(vm_interface, interface_data.mac_address, result)
|
||||
|
||||
if not self.endpoint.sync_ip_addresses:
|
||||
continue
|
||||
|
||||
@@ -398,6 +404,69 @@ class VMwareImporter:
|
||||
|
||||
return primary_ipv4, primary_ipv6
|
||||
|
||||
def _sync_mac_address(self, vm_interface, mac_address, result):
|
||||
normalized_mac = self._normalize_mac_address(mac_address)
|
||||
if normalized_mac is None:
|
||||
return None
|
||||
|
||||
mac_obj = MACAddress.objects.filter(
|
||||
assigned_object_type=self.mac_address_content_type,
|
||||
assigned_object_id=vm_interface.pk,
|
||||
mac_address=normalized_mac,
|
||||
).first()
|
||||
created = mac_obj is None
|
||||
|
||||
if created:
|
||||
mac_obj = MACAddress(
|
||||
mac_address=normalized_mac,
|
||||
assigned_object=vm_interface,
|
||||
)
|
||||
|
||||
mac_obj.description = "VMware reported MAC address"
|
||||
mac_obj.full_clean()
|
||||
mac_obj.save()
|
||||
|
||||
if created:
|
||||
result.mac_addresses_created += 1
|
||||
self.logger.info("Created MAC address %s on interface %s", normalized_mac, vm_interface.name)
|
||||
else:
|
||||
result.mac_addresses_updated += 1
|
||||
self.logger.info("Updated MAC address %s on interface %s", normalized_mac, vm_interface.name)
|
||||
|
||||
if vm_interface.primary_mac_address_id != mac_obj.pk:
|
||||
vm_interface.primary_mac_address = mac_obj
|
||||
vm_interface.full_clean()
|
||||
vm_interface.save(update_fields=("primary_mac_address",))
|
||||
self.logger.info("Set primary MAC address %s on interface %s", normalized_mac, vm_interface.name)
|
||||
|
||||
stale_macs = MACAddress.objects.filter(
|
||||
assigned_object_type=self.mac_address_content_type,
|
||||
assigned_object_id=vm_interface.pk,
|
||||
description="VMware reported MAC address",
|
||||
).exclude(pk=mac_obj.pk)
|
||||
stale_count = stale_macs.count()
|
||||
if stale_count:
|
||||
stale_macs.delete()
|
||||
result.mac_addresses_deleted += stale_count
|
||||
self.logger.info("Deleted %s stale VMware MAC address(es) from interface %s", stale_count, vm_interface.name)
|
||||
|
||||
return mac_obj
|
||||
|
||||
@staticmethod
|
||||
def _normalize_mac_address(mac_address):
|
||||
if not mac_address:
|
||||
return None
|
||||
|
||||
value = str(mac_address).strip().replace("-", ":").lower()
|
||||
parts = value.split(":")
|
||||
if len(parts) != 6:
|
||||
return None
|
||||
|
||||
try:
|
||||
return ":".join(f"{int(part, 16):02x}" for part in parts)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _normalize_ip_address(self, raw_ip):
|
||||
if not raw_ip:
|
||||
return None
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "netbox-vmware-importer"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
description = "NetBox plugin to synchronize VMware vSphere virtual machines into NetBox."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
Reference in New Issue
Block a user