Files
netbox/netbox/virtualization/forms/bulk_edit.py
2026-04-10 14:58:07 -04:00

385 lines
12 KiB
Python

from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfaceModeChoices
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
from dcim.forms.mixins import ScopedBulkEditForm
from dcim.models import Device, DeviceRole, Platform, Site
from extras.models import ConfigTemplate
from ipam.models import VLAN, VRF, VLANGroup, VLANTranslationPolicy
from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm
from netbox.forms.mixins import OwnerMixin
from tenancy.models import Tenant
from utilities.forms import BulkRenameForm, add_blank_choice
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet
from utilities.forms.utils import get_capacity_unit_label
from utilities.forms.widgets import BulkEditNullBooleanSelect
from ..choices import *
from ..models import *
__all__ = (
'ClusterBulkEditForm',
'ClusterGroupBulkEditForm',
'ClusterTypeBulkEditForm',
'VMInterfaceBulkEditForm',
'VMInterfaceBulkRenameForm',
'VirtualDiskBulkEditForm',
'VirtualDiskBulkRenameForm',
'VirtualMachineBulkEditForm',
'VirtualMachineTypeBulkEditForm',
)
class ClusterTypeBulkEditForm(OrganizationalModelBulkEditForm):
model = ClusterType
fieldsets = (
FieldSet('description'),
)
nullable_fields = ('description', 'comments')
class ClusterGroupBulkEditForm(OrganizationalModelBulkEditForm):
model = ClusterGroup
fieldsets = (
FieldSet('description'),
)
nullable_fields = ('description', 'comments')
class ClusterBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
type = DynamicModelChoiceField(
label=_('Type'),
queryset=ClusterType.objects.all(),
required=False
)
group = DynamicModelChoiceField(
label=_('Group'),
queryset=ClusterGroup.objects.all(),
required=False
)
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(ClusterStatusChoices),
required=False,
initial=''
)
tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False
)
model = Cluster
fieldsets = (
FieldSet('type', 'group', 'status', 'tenant', 'description'),
FieldSet('scope_type', 'scope', name=_('Scope')),
)
nullable_fields = (
'group', 'scope', 'tenant', 'description', 'comments',
)
class VirtualMachineTypeBulkEditForm(PrimaryModelBulkEditForm):
default_platform = DynamicModelChoiceField(
label=_('Default platform'),
queryset=Platform.objects.all(),
required=False
)
default_vcpus = forms.IntegerField(
label=_('Default vCPUs'),
required=False,
)
default_memory = forms.IntegerField(
label=_('Default Memory (MB)'),
required=False,
)
model = VirtualMachineType
fieldsets = (
FieldSet('description', name=_('Virtual Machine Type')),
FieldSet('default_platform', 'default_vcpus', 'default_memory', name=_('Defaults')),
)
nullable_fields = (
'default_platform', 'default_vcpus', 'default_memory', 'description', 'comments',
)
class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm):
virtual_machine_type = DynamicModelChoiceField(
label=_('Virtual machine type'),
queryset=VirtualMachineType.objects.all(),
required=False
)
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(VirtualMachineStatusChoices),
required=False,
initial='',
)
start_on_boot = forms.ChoiceField(
label=_('Start on boot'),
choices=add_blank_choice(VirtualMachineStartOnBootChoices),
required=False,
initial='',
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
required=False
)
cluster = DynamicModelChoiceField(
label=_('Cluster'),
queryset=Cluster.objects.all(),
required=False,
query_params={
'site_id': '$site'
}
)
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
required=False,
query_params={
'cluster_id': '$cluster',
'site_id': '$site'
}
)
role = DynamicModelChoiceField(
label=_('Role'),
queryset=DeviceRole.objects.filter(
vm_role=True
),
required=False,
query_params={
"vm_role": "True"
}
)
tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False
)
platform = DynamicModelChoiceField(
label=_('Platform'),
queryset=Platform.objects.all(),
required=False
)
vcpus = forms.IntegerField(
required=False,
label=_('vCPUs')
)
memory = forms.IntegerField(
required=False,
label=_('Memory')
)
disk = forms.IntegerField(
required=False,
label=_('Disk')
)
config_template = DynamicModelChoiceField(
queryset=ConfigTemplate.objects.all(),
required=False
)
model = VirtualMachine
fieldsets = (
FieldSet('virtual_machine_type', 'status', 'start_on_boot', 'role', 'tenant', 'platform', 'description'),
FieldSet('site', 'cluster', 'device', name=_('Placement')),
FieldSet('vcpus', 'memory', 'disk', name=_('Resources')),
FieldSet('config_template', name=_('Configuration')),
)
nullable_fields = (
'virtual_machine_type', 'role', 'site', 'cluster', 'device', 'platform', 'vcpus', 'memory', 'disk', 'tenant',
'description', 'comments',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set unit labels based on configured RAM_BASE_UNIT / DISK_BASE_UNIT (MB vs MiB)
self.fields['memory'].label = _('Memory ({unit})').format(unit=get_capacity_unit_label(settings.RAM_BASE_UNIT))
self.fields['disk'].label = _('Disk ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT))
class VMInterfaceBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
virtual_machine = forms.ModelChoiceField(
label=_('Virtual machine'),
queryset=VirtualMachine.objects.all(),
required=False,
disabled=True,
widget=forms.HiddenInput()
)
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=VMInterface.objects.all(),
required=False
)
bridge = DynamicModelChoiceField(
label=_('Bridge'),
queryset=VMInterface.objects.all(),
required=False
)
enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False,
widget=BulkEditNullBooleanSelect()
)
mtu = forms.IntegerField(
required=False,
min_value=INTERFACE_MTU_MIN,
max_value=INTERFACE_MTU_MAX,
label=_('MTU')
)
description = forms.CharField(
label=_('Description'),
max_length=100,
required=False
)
mode = forms.ChoiceField(
label=_('Mode'),
choices=add_blank_choice(InterfaceModeChoices),
required=False
)
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
label=_('VLAN group')
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
query_params={
'group_id': '$vlan_group',
},
label=_('Untagged VLAN')
)
tagged_vlans = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
query_params={
'group_id': '$vlan_group',
},
label=_('Tagged VLANs')
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('VRF')
)
vlan_translation_policy = DynamicModelChoiceField(
queryset=VLANTranslationPolicy.objects.all(),
required=False,
label=_('VLAN Translation Policy')
)
model = VMInterface
fieldsets = (
FieldSet('mtu', 'enabled', 'vrf', 'description'),
FieldSet('parent', 'bridge', name=_('Related Interfaces')),
FieldSet(
'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vlan_translation_policy',
name=_('802.1Q Switching')
),
)
nullable_fields = (
'parent', 'bridge', 'mtu', 'vrf', 'description', 'vlan_translation_policy',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'virtual_machine' in self.initial:
vm_id = self.initial.get('virtual_machine')
# Restrict parent/bridge interface assignment by VM
self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id)
self.fields['bridge'].widget.add_query_param('virtual_machine_id', vm_id)
# Limit VLAN choices by virtual machine
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
else:
# See 5643
if 'pk' in self.initial:
site = None
interfaces = VMInterface.objects.filter(
pk__in=self.initial['pk']
).prefetch_related(
'virtual_machine__site',
'virtual_machine__cluster',
'virtual_machine__device',
)
# Determine the effective site for each interface's VM (from its site,
# cluster, or device). If all selected interfaces share the same site,
# use it to filter VLAN choices; otherwise leave unfiltered.
for interface in interfaces:
vm = interface.virtual_machine
vm_site = (
vm.site
or (vm.cluster and vm.cluster._site)
or (vm.device and vm.device.site)
)
if site is None:
site = vm_site
elif vm_site is not site:
site = None
break
if site is not None:
self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
self.fields['parent'].choices = ()
self.fields['parent'].widget.attrs['disabled'] = True
self.fields['bridge'].choices = ()
self.fields['bridge'].widget.attrs['disabled'] = True
class VMInterfaceBulkRenameForm(BulkRenameForm):
pk = forms.ModelMultipleChoiceField(
queryset=VMInterface.objects.all(),
widget=forms.MultipleHiddenInput()
)
class VirtualDiskBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
virtual_machine = forms.ModelChoiceField(
label=_('Virtual machine'),
queryset=VirtualMachine.objects.all(),
required=False,
disabled=True,
widget=forms.HiddenInput()
)
size = forms.IntegerField(
required=False,
label=_('Size')
)
description = forms.CharField(
label=_('Description'),
max_length=100,
required=False
)
model = VirtualDisk
fieldsets = (
FieldSet('size', 'description'),
)
nullable_fields = ('description',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set unit label based on configured DISK_BASE_UNIT (MB vs MiB)
self.fields['size'].label = _('Size ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT))
class VirtualDiskBulkRenameForm(BulkRenameForm):
pk = forms.ModelMultipleChoiceField(
queryset=VirtualDisk.objects.all(),
widget=forms.MultipleHiddenInput()
)