import ssl import atexit import ipaddress import tkinter as tk from tkinter import ttk, messagebox from pyVim.connect import SmartConnect, Disconnect from pyVmomi import vim import pynetbox ssl._create_default_https_context = ssl._create_unverified_context class ESXiNetBoxImporter: def __init__(self, root): self.root = root self.root.title("ESXi -> NetBox VM Synchronizer") self.root.geometry("950x750") self.si = None self.nb = None self.vms = [] self.site_map = {} self.cluster_map = {} self.tenant_map = {} self.platforms = [] self.create_gui() # ========================================================= # GUI # ========================================================= def create_gui(self): main = ttk.Frame(self.root, padding=10) main.pack(fill="both", expand=True) # ============================================ # ESXi / vCenter # ============================================ ttk.Label(main, text="ESXi / vCenter Host").grid( row=0, column=0, sticky="w" ) self.esxi_host = ttk.Entry(main, width=45) self.esxi_host.grid(row=0, column=1, sticky="ew") ttk.Label(main, text="Username").grid( row=1, column=0, sticky="w" ) self.esxi_user = ttk.Entry(main, width=45) self.esxi_user.grid(row=1, column=1, sticky="ew") ttk.Label(main, text="Password").grid( row=2, column=0, sticky="w" ) self.esxi_pass = ttk.Entry(main, width=45, show="*") self.esxi_pass.grid(row=2, column=1, sticky="ew") # ============================================ # NetBox # ============================================ ttk.Label(main, text="NetBox URL").grid( row=3, column=0, sticky="w" ) self.nb_url = ttk.Entry(main, width=45) self.nb_url.grid(row=3, column=1, sticky="ew") ttk.Label(main, text="NetBox Token").grid( row=4, column=0, sticky="w" ) self.nb_token = ttk.Entry(main, width=45, show="*") self.nb_token.grid(row=4, column=1, sticky="ew") ttk.Button( main, text="Verbinden & Daten laden", command=self.connect_all ).grid( row=5, column=1, sticky="e", pady=10 ) # ============================================ # Zielauswahl # ============================================ ttk.Label(main, text="Site").grid( row=6, column=0, sticky="w" ) self.site_combo = ttk.Combobox(main, width=42) self.site_combo.grid(row=6, column=1, sticky="ew") ttk.Label(main, text="Cluster").grid( row=7, column=0, sticky="w" ) self.cluster_combo = ttk.Combobox(main, width=42) self.cluster_combo.grid(row=7, column=1, sticky="ew") ttk.Label(main, text="Tenant").grid( row=8, column=0, sticky="w" ) self.tenant_combo = ttk.Combobox(main, width=42) self.tenant_combo.grid(row=8, column=1, sticky="ew") # ============================================ # VM Liste # ============================================ ttk.Label(main, text="Gefundene VMs").grid( row=9, column=0, sticky="w", pady=(15, 5) ) self.vm_list = tk.Listbox( main, selectmode=tk.MULTIPLE, width=100, height=25 ) self.vm_list.grid( row=10, column=0, columnspan=2, sticky="nsew" ) scrollbar = ttk.Scrollbar( main, orient="vertical", command=self.vm_list.yview ) scrollbar.grid(row=10, column=2, sticky="ns") self.vm_list.config(yscrollcommand=scrollbar.set) # ============================================ # Buttons # ============================================ btn_frame = ttk.Frame(main) btn_frame.grid( row=11, column=0, columnspan=2, pady=10, sticky="e" ) ttk.Button( btn_frame, text="Alle auswählen", command=self.select_all ).pack(side="left", padx=5) ttk.Button( btn_frame, text="Import / Sync starten", command=self.import_vms ).pack(side="left", padx=5) # ============================================ # Logfeld # ============================================ ttk.Label(main, text="Log").grid( row=12, column=0, sticky="w" ) self.log_text = tk.Text(main, height=10) self.log_text.grid( row=13, column=0, columnspan=2, sticky="nsew" ) main.columnconfigure(1, weight=1) main.rowconfigure(10, weight=1) # ========================================================= # Logging # ========================================================= def log(self, text): self.log_text.insert(tk.END, f"{text}\n") self.log_text.see(tk.END) self.root.update() # ========================================================= # Select All # ========================================================= def select_all(self): self.vm_list.select_set(0, tk.END) # ========================================================= # Verbindungen # ========================================================= def connect_all(self): try: self.connect_esxi() self.connect_netbox() self.load_netbox_data() self.load_vms() messagebox.showinfo( "Erfolg", "Verbindung erfolgreich hergestellt" ) except Exception as e: messagebox.showerror( "Fehler", str(e) ) def connect_esxi(self): self.log("Verbinde zu ESXi/vCenter...") context = ssl._create_unverified_context() self.si = SmartConnect( host=self.esxi_host.get(), user=self.esxi_user.get(), pwd=self.esxi_pass.get(), sslContext=context ) atexit.register(Disconnect, self.si) self.log("ESXi Verbindung erfolgreich") def connect_netbox(self): self.log("Verbinde zu NetBox...") self.nb = pynetbox.api( self.nb_url.get(), token=self.nb_token.get() ) self.log("NetBox Verbindung erfolgreich") # ========================================================= # NetBox Daten # ========================================================= def load_netbox_data(self): self.log("Lade NetBox Daten...") # Sites sites = list(self.nb.dcim.sites.all()) self.site_map = {x.name: x.id for x in sites} self.site_combo["values"] = list(self.site_map.keys()) # Cluster clusters = list(self.nb.virtualization.clusters.all()) self.cluster_map = {x.name: x.id for x in clusters} self.cluster_combo["values"] = list(self.cluster_map.keys()) # Tenant tenants = list(self.nb.tenancy.tenants.all()) self.tenant_map = {x.name: x.id for x in tenants} self.tenant_combo["values"] = list(self.tenant_map.keys()) # Plattformen self.platforms = list(self.nb.dcim.platforms.all()) self.log("NetBox Daten geladen") # ========================================================= # VMware VMs laden # ========================================================= def load_vms(self): self.log("Lade VMs aus VMware...") content = self.si.RetrieveContent() container = content.rootFolder view_type = [vim.VirtualMachine] recursive = True container_view = content.viewManager.CreateContainerView( container, view_type, recursive ) self.vms = container_view.view self.vm_list.delete(0, tk.END) for vm in self.vms: self.vm_list.insert(tk.END, vm.name) self.log(f"{len(self.vms)} VMs gefunden") # ========================================================= # Plattform Mapping # ========================================================= def get_platform_id(self, guest_os): if not guest_os: return None guest_os = guest_os.lower() for platform in self.platforms: if platform.name.lower() in guest_os: return platform.id return None # ========================================================= # Prefix automatisch ergänzen # ========================================================= def get_ip_with_prefix(self, ip): try: addr = ipaddress.ip_address(ip) if addr.version == 4: return f"{ip}/24" return f"{ip}/64" except: return None # ========================================================= # VM Import / Sync # ========================================================= def import_vms(self): selected = self.vm_list.curselection() if not selected: messagebox.showwarning( "Hinweis", "Keine VMs ausgewählt" ) return cluster_name = self.cluster_combo.get() tenant_name = self.tenant_combo.get() cluster_id = self.cluster_map.get(cluster_name) tenant_id = self.tenant_map.get(tenant_name) if not cluster_id: messagebox.showerror( "Fehler", "Bitte Cluster auswählen" ) return for idx in selected: vm = self.vms[idx] self.log(f"Synchronisiere VM: {vm.name}") # ============================================ # Hardwaredaten # ============================================ memory = int(vm.config.hardware.memoryMB) vcpus = int(vm.config.hardware.numCPU) # ============================================ # Disk in MB (NetBox >= 4.x) # ============================================ disk_size = 0 for dev in vm.config.hardware.device: if isinstance(dev, vim.vm.device.VirtualDisk): # VMware liefert KB # NetBox erwartet MB size_mb = dev.capacityInKB / 1024 disk_size += round(size_mb) # ============================================ # Betriebssystem # ============================================ guest_os = "" try: guest_os = vm.config.guestFullName except: pass platform_id = self.get_platform_id(guest_os) # ============================================ # Status # ============================================ power_state = str(vm.runtime.powerState) if "poweredOn" in power_state: status = "active" else: status = "offline" # ============================================ # VM Daten # ============================================ vm_data = { "name": vm.name, "cluster": cluster_id, "status": status, "vcpus": vcpus, "memory": memory, "disk": disk_size, "tenant": tenant_id, "platform": platform_id } # ============================================ # VM erstellen / aktualisieren # ============================================ nb_vm = self.nb.virtualization.virtual_machines.get( name=vm.name ) if nb_vm: nb_vm.update(vm_data) self.log(f"VM aktualisiert: {vm.name}") else: nb_vm = self.nb.virtualization.virtual_machines.create( vm_data ) self.log(f"VM erstellt: {vm.name}") # ============================================ # Interfaces + IPs # ============================================ primary_ipv4 = None primary_ipv6 = None if vm.guest and hasattr(vm.guest, "net"): for net in vm.guest.net: mac = getattr(net, "macAddress", None) if not mac: continue interface_name = getattr(net, "device", None) if not interface_name: interface_name = f"NIC-{mac[-5:]}" iface_data = { "virtual_machine": nb_vm.id, "name": interface_name, "mac_address": mac, "enabled": True } # Interface zuerst über Namen suchen existing_iface = self.nb.virtualization.interfaces.get( virtual_machine_id=nb_vm.id, name=interface_name ) if existing_iface: # Falls MAC geändert wurde -> aktualisieren existing_iface.update(iface_data) vm_iface = existing_iface self.log( f"Interface aktualisiert: {interface_name}" ) else: vm_iface = self.nb.virtualization.interfaces.create( iface_data ) self.log( f"Interface erstellt: {interface_name}" ) self.log(f"Interface synchronisiert: {mac}") # ============================================ # IPs # ============================================ if hasattr(net, "ipAddress"): for ip in net.ipAddress: if ip.startswith("127."): continue if ip == "::1": continue address = self.get_ip_with_prefix(ip) if not address: continue existing_ip = self.nb.ipam.ip_addresses.get( address=address ) ip_data = { "address": address, "status": "active", "assigned_object_type": "virtualization.vminterface", "assigned_object_id": vm_iface.id } if existing_ip: existing_ip.update(ip_data) ip_obj = existing_ip self.log(f"IP aktualisiert: {address}") else: ip_obj = self.nb.ipam.ip_addresses.create( ip_data ) self.log(f"IP erstellt: {address}") # ============================================ # Primary IP setzen # ============================================ try: parsed_ip = ipaddress.ip_address(ip) if parsed_ip.version == 4: if not primary_ipv4: primary_ipv4 = ip_obj.id else: if not primary_ipv6: primary_ipv6 = ip_obj.id except: pass # ============================================ # Primäre IPs setzen # ============================================ update_data = {} if primary_ipv4: update_data["primary_ip4"] = primary_ipv4 if primary_ipv6: update_data["primary_ip6"] = primary_ipv6 if update_data: nb_vm.update(update_data) self.log( f"Primary IPs gesetzt für {vm.name}" ) messagebox.showinfo( "Fertig", "VM Import / Synchronisierung abgeschlossen" ) self.log("Synchronisierung abgeschlossen") # ========================================================= # Main # ========================================================= if __name__ == "__main__": root = tk.Tk() app = ESXiNetBoxImporter(root) root.mainloop()