Files
netbox/netbox/dcim/forms/object_create.py
2025-12-01 12:25:34 -05:00

359 lines
11 KiB
Python

from django import forms
from django.utils.translation import gettext_lazy as _
from dcim.models import *
from netbox.forms import NetBoxModelForm
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
from utilities.forms.rendering import FieldSet, TabbedGroups
from utilities.forms.widgets import APISelect
from . import model_forms
__all__ = (
'ComponentCreateForm',
'ConsolePortCreateForm',
'ConsolePortTemplateCreateForm',
'ConsoleServerPortCreateForm',
'ConsoleServerPortTemplateCreateForm',
'DeviceBayCreateForm',
'DeviceBayTemplateCreateForm',
'FrontPortCreateForm',
'FrontPortTemplateCreateForm',
'InterfaceCreateForm',
'InterfaceTemplateCreateForm',
'InventoryItemCreateForm',
'InventoryItemTemplateCreateForm',
'ModuleBayCreateForm',
'ModuleBayTemplateCreateForm',
'PowerOutletCreateForm',
'PowerOutletTemplateCreateForm',
'PowerPortCreateForm',
'PowerPortTemplateCreateForm',
'RearPortCreateForm',
'RearPortTemplateCreateForm',
'VirtualChassisCreateForm',
)
class ComponentCreateForm(forms.Form):
"""
Subclass this form when facilitating the creation of one or more component or component template objects based on
a name pattern.
"""
name = ExpandableNameField(
label=_('Name'),
)
label = ExpandableNameField(
label=_('Label'),
required=False,
help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
)
# Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by
# ComponentCreateView when creating objects.
replication_fields = ('name', 'label')
def clean(self):
super().clean()
# Validate that all replication fields generate an equal number of values (or a single value)
if not (patterns := self.cleaned_data.get(self.replication_fields[0])):
return
pattern_count = len(patterns)
for field_name in self.replication_fields:
value_count = len(self.cleaned_data[field_name])
if self.cleaned_data[field_name]:
if value_count == 1:
# If the field resolves to a single value (because no pattern was used), multiply it by the number
# of expected values. This allows us to reuse the same label when creating multiple components.
self.cleaned_data[field_name] = self.cleaned_data[field_name] * pattern_count
elif value_count != pattern_count:
raise forms.ValidationError({
field_name: _(
"The provided pattern specifies {value_count} values, but {pattern_count} are expected."
).format(value_count=value_count, pattern_count=pattern_count)
}, code='label_pattern_mismatch')
#
# Device component templates
#
class ConsolePortTemplateCreateForm(ComponentCreateForm, model_forms.ConsolePortTemplateForm):
class Meta(model_forms.ConsolePortTemplateForm.Meta):
exclude = ('name', 'label')
class ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm):
class Meta(model_forms.ConsoleServerPortTemplateForm.Meta):
exclude = ('name', 'label')
class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm):
class Meta(model_forms.PowerPortTemplateForm.Meta):
exclude = ('name', 'label')
class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm):
class Meta(model_forms.PowerOutletTemplateForm.Meta):
exclude = ('name', 'label')
class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemplateForm):
class Meta(model_forms.InterfaceTemplateForm.Meta):
exclude = ('name', 'label')
class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
# Override fieldsets from FrontPortTemplateForm
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description',
),
)
class Meta:
model = FrontPortTemplate
fields = (
'device_type', 'module_type', 'type', 'color', 'positions', 'description',
)
def get_iterative_data(self, iteration):
positions = self.cleaned_data['positions']
offset = positions * iteration
return {
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
}
class RearPortTemplateCreateForm(ComponentCreateForm, model_forms.RearPortTemplateForm):
class Meta(model_forms.RearPortTemplateForm.Meta):
exclude = ('name', 'label')
class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemplateForm):
class Meta(model_forms.DeviceBayTemplateForm.Meta):
exclude = ('name', 'label')
class ModuleBayTemplateCreateForm(ComponentCreateForm, model_forms.ModuleBayTemplateForm):
position = ExpandableNameField(
label=_('Position'),
required=False,
help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
)
replication_fields = ('name', 'label', 'position')
class Meta(model_forms.ModuleBayTemplateForm.Meta):
exclude = ('name', 'label', 'position')
class InventoryItemTemplateCreateForm(ComponentCreateForm, model_forms.InventoryItemTemplateForm):
class Meta(model_forms.InventoryItemTemplateForm.Meta):
exclude = ('name', 'label')
#
# Device components
#
class ConsolePortCreateForm(ComponentCreateForm, model_forms.ConsolePortForm):
class Meta(model_forms.ConsolePortForm.Meta):
exclude = ('name', 'label')
class ConsoleServerPortCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortForm):
class Meta(model_forms.ConsoleServerPortForm.Meta):
exclude = ('name', 'label')
class PowerPortCreateForm(ComponentCreateForm, model_forms.PowerPortForm):
class Meta(model_forms.PowerPortForm.Meta):
exclude = ('name', 'label')
class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
class Meta(model_forms.PowerOutletForm.Meta):
exclude = ('name', 'label')
class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
class Meta(model_forms.InterfaceForm.Meta):
exclude = ('name', 'label')
class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
selector=True,
widget=APISelect(
# TODO: Clean up the application of HTMXSelect attributes
attrs={
'hx-get': '.',
'hx-include': '#form_fields',
'hx-target': '#form_fields',
}
)
)
# Override fieldsets from FrontPortForm to omit rear_port_position
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
'description', 'tags',
),
)
class Meta:
model = FrontPort
fields = [
'device', 'module', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', 'tags',
]
def get_iterative_data(self, iteration):
positions = self.cleaned_data['positions']
offset = positions * iteration
return {
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
}
class RearPortCreateForm(ComponentCreateForm, model_forms.RearPortForm):
class Meta(model_forms.RearPortForm.Meta):
exclude = ('name', 'label')
class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm):
class Meta(model_forms.DeviceBayForm.Meta):
exclude = ('name', 'label')
class ModuleBayCreateForm(ComponentCreateForm, model_forms.ModuleBayForm):
position = ExpandableNameField(
label=_('Position'),
required=False,
help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
)
replication_fields = ('name', 'label', 'position')
class Meta(model_forms.ModuleBayForm.Meta):
exclude = ('name', 'label', 'position')
class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm):
class Meta(model_forms.InventoryItemForm.Meta):
exclude = ('name', 'label')
#
# Virtual chassis
#
class VirtualChassisCreateForm(NetBoxModelForm):
region = DynamicModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site_group = DynamicModelChoiceField(
label=_('Site group'),
queryset=SiteGroup.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region',
'group_id': '$site_group',
}
)
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
required=False,
null_option='None',
query_params={
'site_id': '$site'
}
)
members = DynamicModelMultipleChoiceField(
label=_('Members'),
queryset=Device.objects.all(),
required=False,
query_params={
'virtual_chassis_id': 'null',
'site_id': '$site',
'rack_id': '$rack',
}
)
initial_position = forms.IntegerField(
label=_('Initial position'),
initial=1,
required=False,
help_text=_('Position of the first member device. Increases by one for each additional member.')
)
fieldsets = (
FieldSet('name', 'domain', 'description', 'tags', name=_('Virtual Chassis')),
FieldSet('region', 'site_group', 'site', 'rack', 'members', 'initial_position', name=_('Member Devices')),
)
class Meta:
model = VirtualChassis
fields = [
'name', 'domain', 'description', 'region', 'site_group', 'site', 'rack', 'owner', 'members',
'initial_position', 'tags',
]
def clean(self):
super().clean()
if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
raise forms.ValidationError({
'initial_position': _("A position must be specified for the first VC member.")
})
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
# Assign VC members
if instance.pk and self.cleaned_data['members']:
initial_position = self.cleaned_data.get('initial_position', 1)
for i, member in enumerate(self.cleaned_data['members'], start=initial_position):
member.snapshot()
member.virtual_chassis = instance
member.vc_position = i
member.save()
return instance