first commit
This commit is contained in:
257
netbox_librenms_plugin/import_utils/vm_operations.py
Normal file
257
netbox_librenms_plugin/import_utils/vm_operations.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user