Files
netbox-librenms-plugin/netbox_librenms_plugin/import_utils/vm_operations.py
Vlastislav Svatek 673e67106e
Some checks failed
ci / deploy (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
first commit
2026-06-05 10:39:05 +02:00

258 lines
9.6 KiB
Python

"""Virtual machine creation and import operations."""
import logging
from dcim.models import DeviceRole
from django.db import transaction
from django.utils import timezone
from virtualization.models import Cluster
from ..librenms_api import LibreNMSAPI
from .bulk_import import _is_job_cancelled
from .device_operations import _determine_device_name, fetch_device_with_cache, validate_device_for_import
from .permissions import require_permissions
logger = logging.getLogger(__name__)
def create_vm_from_librenms(
libre_device: dict,
validation: dict,
server_key: str = "default",
use_sysname: bool = True,
strip_domain: bool = False,
role=None,
):
"""
Create a NetBox VirtualMachine from LibreNMS device data.
Args:
libre_device: Device data from LibreNMS
validation: Validation result from validate_device_for_import with import_as_vm=True
use_sysname: If True, prefer sysName; if False, use hostname
server_key: LibreNMS server key used to store the librenms_id custom field
Returns:
Created VirtualMachine instance
Raises:
Exception if VM cannot be created
"""
from virtualization.models import VirtualMachine
if not validation["can_import"]:
raise ValueError(f"VM cannot be imported: {', '.join(validation['issues'])}")
# Extract matched objects from validation
cluster = validation["cluster"]["cluster"]
platform = validation["platform"].get("platform")
role = role if role is not None else validation.get("device_role", {}).get("role")
# Determine VM name - use pre-computed name if available (handles strip_domain),
# falling back to the validated resolved_name before recomputing from raw fields.
vm_name = libre_device.get("_computed_name") or validation.get("resolved_name")
if not vm_name:
vm_name = _determine_device_name(
libre_device,
use_sysname=use_sysname,
strip_domain=strip_domain,
device_id=libre_device.get("device_id"),
)
# Generate import timestamp comment
import_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S %Z")
# Validate device_id before creating the VM so a missing/invalid value
# never leaves a VM without a librenms_id (partial persistence).
raw_device_id = libre_device["device_id"]
if isinstance(raw_device_id, bool):
raise ValueError(f"device_id is a boolean ({raw_device_id!r}); expected an integer")
librenms_device_id = int(raw_device_id)
from ..utils import set_librenms_device_id
# Create the VM and assign its LibreNMS ID atomically so a failure in
# set_librenms_device_id never leaves a VM without a mapping.
with transaction.atomic():
vm = VirtualMachine.objects.create(
name=vm_name,
cluster=cluster,
role=role,
platform=platform,
comments=f"Imported from LibreNMS (device_id={librenms_device_id}) by netbox-librenms-plugin on {import_time}",
)
set_librenms_device_id(vm, librenms_device_id, server_key)
vm.save()
logger.info(f"Created VM {vm.name} (ID: {vm.pk}) from LibreNMS device {libre_device['device_id']}")
return vm
def bulk_import_vms(
vm_imports: dict[int, dict[str, int]],
api: LibreNMSAPI,
sync_options: dict = None,
libre_devices_cache: dict = None,
job=None,
user=None,
) -> dict:
"""
Import multiple LibreNMS devices as VMs in NetBox.
Handles validation, cluster/role assignment, name determination,
and VM creation. Supports both synchronous and background job execution.
This function consolidates VM import logic that was previously duplicated
in BulkImportDevicesView and ImportDevicesJob, ensuring consistent behavior
across synchronous and background import paths.
Args:
vm_imports: Dict mapping device_id to {"cluster_id": int, "device_role_id": int}
api: LibreNMSAPI instance for device fetching
sync_options: Optional dict with use_sysname, strip_domain settings
libre_devices_cache: Optional pre-fetched device data cache
job: Optional JobRunner instance for background job logging/cancellation
user: User performing the import (for permission checks). If job is provided,
user is extracted from job.job.user if not explicitly passed.
Returns:
Dict with keys:
- success: List of {"device_id": int, "device": VM, "message": str}
- failed: List of {"device_id": int, "error": str}
- skipped: List of {"device_id": int, "reason": str}
Raises:
PermissionDenied: If user lacks required permissions
Example:
>>> # Synchronous import from view
>>> vm_imports = {123: {"cluster_id": 5, "device_role_id": 2}}
>>> result = bulk_import_vms(vm_imports, api, sync_options, user=request.user)
>>> print(f"Created {len(result['success'])} VMs")
>>>
>>> # Background job import
>>> result = bulk_import_vms(vm_imports, api, sync_options, cache, job=self)
"""
from netbox_librenms_plugin.import_validation_helpers import (
apply_cluster_to_validation,
apply_role_to_validation,
)
# Extract user from job if not explicitly provided
if user is None and job is not None:
user = getattr(job.job, "user", None)
# Check permissions at start of bulk operation
require_permissions(user, ["virtualization.add_virtualmachine"], "import VMs")
result = {"success": [], "failed": [], "skipped": []}
vm_ids = list(vm_imports.keys())
# Use job logger if available, otherwise standard logger
log = job.logger if job else logger
for idx, vm_id in enumerate(vm_ids, start=1):
# Check for job cancellation before first VM and every 5 thereafter
if job and (idx == 1 or idx % 5 == 0) and _is_job_cancelled(job):
log.warning(f"Job cancelled at VM {idx} of {len(vm_ids)}")
break
log.info(f"Processing VM {idx} of {len(vm_ids)}")
try:
# Fetch device data (uses cache helper)
libre_device = fetch_device_with_cache(vm_id, api, api.server_key, libre_devices_cache)
if not libre_device:
result["failed"].append(
{
"device_id": vm_id,
"error": f"Device {vm_id} not found in LibreNMS",
}
)
log.error(f"Device {vm_id} not found in LibreNMS")
continue
# Validate as VM
use_sysname_opt = sync_options.get("use_sysname", True) if sync_options else True
strip_domain_opt = sync_options.get("strip_domain", False) if sync_options else False
validation = validate_device_for_import(
libre_device,
import_as_vm=True,
api=api,
use_sysname=use_sysname_opt,
strip_domain=strip_domain_opt,
server_key=api.server_key,
)
# Check if VM already exists
if validation.get("existing_device"):
result["skipped"].append(
{
"device_id": vm_id,
"reason": f"VM already exists: {validation['existing_device'].name}",
}
)
log.info(f"VM already exists: {validation['existing_device'].name}")
continue
# Apply manual cluster and role selections
vm_mappings = vm_imports[vm_id]
cluster_id = vm_mappings.get("cluster_id")
role_id = vm_mappings.get("device_role_id")
if cluster_id:
cluster = Cluster.objects.filter(id=cluster_id).first()
if cluster:
apply_cluster_to_validation(validation, cluster)
else:
result["failed"].append(
{"device_id": vm_id, "error": f"Selected cluster (id={cluster_id}) no longer exists"}
)
continue
role = None
if role_id:
role = DeviceRole.objects.filter(id=role_id).first()
if role:
apply_role_to_validation(validation, role, is_vm=True)
else:
result["failed"].append(
{"device_id": vm_id, "error": f"Selected role (id={role_id}) no longer exists"}
)
continue
# Determine VM name
vm_name = _determine_device_name(
libre_device,
use_sysname=use_sysname_opt,
strip_domain=strip_domain_opt,
device_id=vm_id,
)
# Update validation with computed name
libre_device["_computed_name"] = vm_name
# Create VM
vm = create_vm_from_librenms(
libre_device,
validation,
use_sysname=use_sysname_opt,
strip_domain=strip_domain_opt,
server_key=api.server_key,
)
result["success"].append(
{
"device_id": vm_id,
"device": vm,
"message": f"VM {vm.name} created successfully",
}
)
log.info(f"Successfully imported VM {vm.name} (ID: {vm_id})")
except Exception as vm_error:
log.error(f"Failed to import VM {vm_id}: {vm_error}", exc_info=True)
result["failed"].append({"device_id": vm_id, "error": str(vm_error)})
return result