netbox/netbox/dcim/forms/model_forms.py
Jeremy Stretch 8d7889e2c0
Closes #19002: Module type profiles (#19014)
* Move Module & ModuleType models to a separate file

* Add ModuleTypeProfile & related fields

* Initial work on JSON schema validation

* Add attributes property on ModuleType

* Introduce MultipleOfValidator

* Introduce JSONSchemaProperty

* Enable dynamic form field rendering

* Misc cleanup

* Fix migration conflict

* Ensure deterministic ordering of attriubte fields

* Support choices & default values

* Include module type attributes on module view

* Enable modifying individual attributes via REST API

* Enable filtering by attribute values

* Add documentation & tests

* Schema should be optional

* Include attributes column for profiles

* Profile is nullable

* Include some initial profiles to be installed via migration

* Fix migrations conflict

* Fix filterset test

* Misc cleanup

* Fixes #19023: get_field_value() should respect null values in bound forms (#19024)

* Skip filters which do not specify a JSON-serializable value

* Fix handling of array item types

* Fix initial data in schema field during bulk edit

* Implement sanity checking for JSON schema definitions

* Fall back to filtering by string value
2025-04-01 12:05:06 -05:00

1927 lines
60 KiB
Python

from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.validators import EMPTY_VALUES
from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneFormField
from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from extras.models import ConfigTemplate
from ipam.choices import VLANQinQRoleChoices
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from users.models import User
from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
)
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
from utilities.jsonschema import JSONSchemaProperty
from virtualization.models import Cluster, VMInterface
from wireless.models import WirelessLAN, WirelessLANGroup
from .common import InterfaceCommonForm, ModuleCommonForm
__all__ = (
'CableForm',
'ConsolePortForm',
'ConsolePortTemplateForm',
'ConsoleServerPortForm',
'ConsoleServerPortTemplateForm',
'DeviceBayForm',
'DeviceBayTemplateForm',
'DeviceForm',
'DeviceRoleForm',
'DeviceTypeForm',
'DeviceVCMembershipForm',
'FrontPortForm',
'FrontPortTemplateForm',
'InterfaceForm',
'InterfaceTemplateForm',
'InventoryItemForm',
'InventoryItemRoleForm',
'InventoryItemTemplateForm',
'LocationForm',
'MACAddressForm',
'ManufacturerForm',
'ModuleForm',
'ModuleBayForm',
'ModuleBayTemplateForm',
'ModuleTypeForm',
'ModuleTypeProfileForm',
'PlatformForm',
'PopulateDeviceBayForm',
'PowerFeedForm',
'PowerOutletForm',
'PowerOutletTemplateForm',
'PowerPanelForm',
'PowerPortForm',
'PowerPortTemplateForm',
'RackForm',
'RackReservationForm',
'RackRoleForm',
'RackTypeForm',
'RearPortForm',
'RearPortTemplateForm',
'RegionForm',
'SiteForm',
'SiteGroupForm',
'VCMemberSelectForm',
'VirtualChassisForm',
'VirtualDeviceContextForm'
)
class RegionForm(NetBoxModelForm):
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=Region.objects.all(),
required=False
)
slug = SlugField()
comments = CommentField()
fieldsets = (
FieldSet('parent', 'name', 'slug', 'description', 'tags'),
)
class Meta:
model = Region
fields = (
'parent', 'name', 'slug', 'description', 'tags', 'comments',
)
class SiteGroupForm(NetBoxModelForm):
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=SiteGroup.objects.all(),
required=False
)
slug = SlugField()
comments = CommentField()
fieldsets = (
FieldSet('parent', 'name', 'slug', 'description', 'tags'),
)
class Meta:
model = SiteGroup
fields = (
'parent', 'name', 'slug', 'description', 'comments', 'tags',
)
class SiteForm(TenancyForm, NetBoxModelForm):
region = DynamicModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(),
required=False,
quick_add=True
)
group = DynamicModelChoiceField(
label=_('Group'),
queryset=SiteGroup.objects.all(),
required=False,
quick_add=True
)
asns = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(),
label=_('ASNs'),
required=False
)
slug = SlugField()
time_zone = TimeZoneFormField(
label=_('Time zone'),
choices=add_blank_choice(TimeZoneFormField().choices),
required=False
)
comments = CommentField()
fieldsets = (
FieldSet(
'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
name=_('Site')
),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
FieldSet('physical_address', 'shipping_address', 'latitude', 'longitude', name=_('Contact Info')),
)
class Meta:
model = Site
fields = (
'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone',
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags',
)
widgets = {
'physical_address': forms.Textarea(
attrs={
'rows': 3,
}
),
'shipping_address': forms.Textarea(
attrs={
'rows': 3,
}
),
}
class LocationForm(TenancyForm, NetBoxModelForm):
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
selector=True
)
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site'
}
)
slug = SlugField()
comments = CommentField()
fieldsets = (
FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
model = Location
fields = (
'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant',
'facility', 'tags', 'comments',
)
class RackRoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')),
)
class Meta:
model = RackRole
fields = [
'name', 'slug', 'color', 'description', 'tags',
]
class RackTypeForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
quick_add=True
)
comments = CommentField()
slug = SlugField(
label=_('Slug'),
slug_source='model'
)
fieldsets = (
FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')),
FieldSet(
'width', 'u_height',
InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
'mounting_depth', name=_('Dimensions')
),
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
)
class Meta:
model = RackType
fields = [
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
'weight_unit', 'description', 'comments', 'tags',
]
class RackForm(TenancyForm, NetBoxModelForm):
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
selector=True
)
location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site'
}
)
role = DynamicModelChoiceField(
label=_('Role'),
queryset=RackRole.objects.all(),
required=False
)
rack_type = DynamicModelChoiceField(
label=_('Rack Type'),
queryset=RackType.objects.all(),
required=False,
help_text=_("Select a pre-defined rack type, or set physical characteristics below.")
)
comments = CommentField()
fieldsets = (
FieldSet(
'site', 'location', 'name', 'status', 'role', 'rack_type', 'description', 'airflow', 'tags',
name=_('Rack')
),
FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
model = Rack
fields = [
'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
'weight_unit', 'description', 'comments', 'tags',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Mimic HTMXSelect()
self.fields['rack_type'].widget.attrs.update({
'hx-get': '.',
'hx-include': '#form_fields',
'hx-target': '#form_fields',
})
# Omit RackType-defined fields if rack_type is set
if get_field_value(self, 'rack_type'):
for field_name in Rack.RACKTYPE_FIELDS:
del self.fields[field_name]
else:
self.fieldsets = (
*self.fieldsets,
FieldSet(
'form_factor', 'width', 'starting_unit', 'u_height',
InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit',
label=_('Outer Dimensions')),
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
'mounting_depth', 'desc_units', name=_('Dimensions')
),
)
class RackReservationForm(TenancyForm, NetBoxModelForm):
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
selector=True
)
units = NumericArrayField(
label=_('Units'),
base_field=forms.IntegerField(),
help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
)
user = forms.ModelChoiceField(
label=_('User'),
queryset=User.objects.order_by('username')
)
comments = CommentField()
fieldsets = (
FieldSet('rack', 'units', 'user', 'description', 'tags', name=_('Reservation')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
model = RackReservation
fields = [
'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
]
class ManufacturerForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')),
)
class Meta:
model = Manufacturer
fields = [
'name', 'slug', 'description', 'tags',
]
class DeviceTypeForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
quick_add=True
)
default_platform = DynamicModelChoiceField(
label=_('Default platform'),
queryset=Platform.objects.all(),
required=False,
selector=True,
query_params={
'manufacturer_id': ['$manufacturer', 'null'],
}
)
slug = SlugField(
label=_('Slug'),
slug_source='model'
)
comments = CommentField()
fieldsets = (
FieldSet('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags', name=_('Device Type')),
FieldSet(
'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
'weight', 'weight_unit', name=_('Chassis')
),
FieldSet('front_image', 'rear_image', name=_('Images')),
)
class Meta:
model = DeviceType
fields = [
'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization',
'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image',
'description', 'comments', 'tags',
]
widgets = {
'front_image': ClearableFileInput(attrs={
'accept': DEVICETYPE_IMAGE_FORMATS
}),
'rear_image': ClearableFileInput(attrs={
'accept': DEVICETYPE_IMAGE_FORMATS
}),
}
class ModuleTypeProfileForm(NetBoxModelForm):
schema = JSONField(
label=_('Schema'),
required=False,
help_text=_("Enter a valid JSON schema to define supported attributes.")
)
comments = CommentField()
fieldsets = (
FieldSet('name', 'description', 'schema', 'tags', name=_('Profile')),
)
class Meta:
model = ModuleTypeProfile
fields = [
'name', 'description', 'schema', 'comments', 'tags',
]
class ModuleTypeForm(NetBoxModelForm):
profile = forms.ModelChoiceField(
queryset=ModuleTypeProfile.objects.all(),
label=_('Profile'),
required=False,
widget=HTMXSelect()
)
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all()
)
comments = CommentField()
@property
def fieldsets(self):
return [
FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')),
FieldSet('profile', *self.attr_fields, name=_('Profile & Attributes'))
]
class Meta:
model = ModuleType
fields = [
'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit',
'comments', 'tags',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Track profile-specific attribute fields
self.attr_fields = []
# Retrieve assigned ModuleTypeProfile, if any
if not (profile_id := get_field_value(self, 'profile')):
return
if not (profile := ModuleTypeProfile.objects.filter(pk=profile_id).first()):
return
# Extend form with fields for profile attributes
for attr, form_field in self._get_attr_form_fields(profile).items():
field_name = f'attr_{attr}'
self.attr_fields.append(field_name)
self.fields[field_name] = form_field
if self.instance.attribute_data:
self.fields[field_name].initial = self.instance.attribute_data.get(attr)
@staticmethod
def _get_attr_form_fields(profile):
"""
Return a dictionary mapping of attribute names to form fields, suitable for extending
the form per the selected ModuleTypeProfile.
"""
if not profile.schema:
return {}
properties = profile.schema.get('properties', {})
required_fields = profile.schema.get('required', [])
attr_fields = {}
for name, options in properties.items():
prop = JSONSchemaProperty(**options)
attr_fields[name] = prop.to_form_field(name, required=name in required_fields)
return dict(sorted(attr_fields.items()))
def _post_clean(self):
# Compile attribute data from the individual form fields
if self.cleaned_data.get('profile'):
self.instance.attribute_data = {
name[5:]: self.cleaned_data[name] # Remove the attr_ prefix
for name in self.attr_fields
if self.cleaned_data.get(name) not in EMPTY_VALUES
}
return super()._post_clean()
class DeviceRoleForm(NetBoxModelForm):
config_template = DynamicModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(),
required=False
)
slug = SlugField()
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=DeviceRole.objects.all(),
required=False,
)
comments = CommentField()
fieldsets = (
FieldSet(
'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description',
'tags', name=_('Device Role')
),
)
class Meta:
model = DeviceRole
fields = [
'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'comments', 'tags',
]
class PlatformForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
required=False,
quick_add=True
)
config_template = DynamicModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(),
required=False
)
slug = SlugField(
label=_('Slug'),
max_length=64
)
fieldsets = (
FieldSet('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform')),
)
class Meta:
model = Platform
fields = [
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
]
class DeviceForm(TenancyForm, NetBoxModelForm):
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
selector=True
)
location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site'
},
initial_params={
'racks': '$rack'
}
)
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
required=False,
query_params={
'site_id': '$site',
'location_id': '$location',
}
)
position = forms.DecimalField(
label=_('Position'),
required=False,
help_text=_("The lowest-numbered unit occupied by the device"),
localize=True,
widget=APISelect(
api_url='/api/dcim/racks/{{rack}}/elevation/',
attrs={
'ts-disabled-field': 'device',
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
},
)
)
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),
context={
'parent': 'manufacturer',
},
selector=True
)
role = DynamicModelChoiceField(
label=_('Device role'),
queryset=DeviceRole.objects.all(),
quick_add=True
)
platform = DynamicModelChoiceField(
label=_('Platform'),
queryset=Platform.objects.all(),
required=False,
selector=True,
query_params={
'available_for_device_type': '$device_type',
}
)
cluster = DynamicModelChoiceField(
label=_('Cluster'),
queryset=Cluster.objects.all(),
required=False,
selector=True,
query_params={
'site_id': ['$site', 'null']
},
)
comments = CommentField()
local_context_data = JSONField(
required=False,
label=''
)
virtual_chassis = DynamicModelChoiceField(
label=_('Virtual chassis'),
queryset=VirtualChassis.objects.all(),
required=False,
context={
'parent': 'master',
},
selector=True
)
vc_position = forms.IntegerField(
required=False,
label=_('Position'),
help_text=_("The position in the virtual chassis this device is identified by")
)
vc_priority = forms.IntegerField(
required=False,
label=_('Priority'),
help_text=_("The priority of the device in the virtual chassis")
)
config_template = DynamicModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(),
required=False
)
class Meta:
model = Device
fields = [
'name', 'role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster',
'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
'comments', 'tags', 'local_context_data',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
# Compile list of choices for primary IPv4 and IPv6 addresses
oob_ip_choices = [(None, '---------')]
for family in [4, 6]:
ip_choices = [(None, '---------')]
# Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True)
# Collect interface IPs
interface_ips = IPAddress.objects.filter(
address__family=family,
assigned_object_type=ContentType.objects.get_for_model(Interface),
assigned_object_id__in=interface_ids
).prefetch_related('assigned_object')
if interface_ips:
ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
ip_choices.append(('Interface IPs', ip_list))
oob_ip_choices.extend(ip_list)
# Collect NAT IPs
nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
address__family=family,
nat_inside__assigned_object_type=ContentType.objects.get_for_model(Interface),
nat_inside__assigned_object_id__in=interface_ids
).prefetch_related('assigned_object')
if nat_ips:
ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
ip_choices.append(('NAT IPs', ip_list))
self.fields['primary_ip{}'.format(family)].choices = ip_choices
self.fields['oob_ip'].choices = oob_ip_choices
# If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
# can be flipped from one face to another.
self.fields['position'].widget.add_query_param('exclude', self.instance.pk)
# Disable rack assignment if this is a child device installed in a parent device
if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
self.fields['site'].disabled = True
self.fields['rack'].disabled = True
self.initial['site'] = self.instance.parent_bay.device.site_id
self.initial['rack'] = self.instance.parent_bay.device.rack_id
else:
# An object that doesn't exist yet can't have any IPs assigned to it
self.fields['primary_ip4'].choices = []
self.fields['primary_ip4'].widget.attrs['readonly'] = True
self.fields['primary_ip6'].choices = []
self.fields['primary_ip6'].widget.attrs['readonly'] = True
self.fields['oob_ip'].choices = []
self.fields['oob_ip'].widget.attrs['readonly'] = True
# Rack position
position = self.data.get('position') or self.initial.get('position')
if position:
self.fields['position'].widget.choices = [(position, f'U{position}')]
class ModuleForm(ModuleCommonForm, NetBoxModelForm):
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
initial_params={
'modulebays': '$module_bay'
}
)
module_bay = DynamicModelChoiceField(
label=_('Module bay'),
queryset=ModuleBay.objects.all(),
query_params={
'device_id': '$device'
}
)
module_type = DynamicModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(),
context={
'parent': 'manufacturer',
},
selector=True
)
comments = CommentField()
replicate_components = forms.BooleanField(
label=_('Replicate components'),
required=False,
initial=True,
help_text=_("Automatically populate components associated with this module type")
)
adopt_components = forms.BooleanField(
label=_('Adopt components'),
required=False,
initial=False,
help_text=_("Adopt already existing components")
)
fieldsets = (
FieldSet('device', 'module_bay', 'module_type', 'status', 'description', 'tags', name=_('Module')),
FieldSet('serial', 'asset_tag', 'replicate_components', 'adopt_components', name=_('Hardware')),
)
class Meta:
model = Module
fields = [
'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components',
'adopt_components', 'description', 'comments',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
self.fields['device'].disabled = True
self.fields['replicate_components'].initial = False
self.fields['replicate_components'].disabled = True
self.fields['adopt_components'].initial = False
self.fields['adopt_components'].disabled = True
def get_termination_type_choices():
return add_blank_choice([
(f'{ct.app_label}.{ct.model}', ct.model_class()._meta.verbose_name.title())
for ct in ContentType.objects.filter(CABLE_TERMINATION_MODELS)
])
class CableForm(TenancyForm, NetBoxModelForm):
a_terminations_type = forms.ChoiceField(
choices=get_termination_type_choices,
required=False,
widget=HTMXSelect(),
label=_('Type')
)
b_terminations_type = forms.ChoiceField(
choices=get_termination_type_choices,
required=False,
widget=HTMXSelect(),
label=_('Type')
)
comments = CommentField()
class Meta:
model = Cable
fields = [
'a_terminations_type', 'b_terminations_type', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
'length', 'length_unit', 'description', 'comments', 'tags',
]
class PowerPanelForm(NetBoxModelForm):
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
selector=True
)
location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site'
}
)
comments = CommentField()
fieldsets = (
FieldSet('site', 'location', 'name', 'description', 'tags', name=_('Power Panel')),
)
class Meta:
model = PowerPanel
fields = [
'site', 'location', 'name', 'description', 'comments', 'tags',
]
class PowerFeedForm(TenancyForm, NetBoxModelForm):
power_panel = DynamicModelChoiceField(
label=_('Power panel'),
queryset=PowerPanel.objects.all(),
selector=True,
quick_add=True
)
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
required=False,
selector=True
)
comments = CommentField()
fieldsets = (
FieldSet(
'power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags',
name=_('Power Feed')
),
FieldSet('supply', 'voltage', 'amperage', 'phase', 'max_utilization', name=_('Characteristics')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
model = PowerFeed
fields = [
'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
]
#
# Virtual chassis
#
class VirtualChassisForm(NetBoxModelForm):
master = forms.ModelChoiceField(
label=_('Master'),
queryset=Device.objects.all(),
required=False,
)
comments = CommentField()
class Meta:
model = VirtualChassis
fields = [
'name', 'domain', 'master', 'description', 'comments', 'tags',
]
widgets = {
'master': SelectWithPK(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['master'].queryset = Device.objects.filter(virtual_chassis=self.instance)
class DeviceVCMembershipForm(forms.ModelForm):
class Meta:
model = Device
fields = [
'vc_position', 'vc_priority',
]
labels = {
'vc_position': 'Position',
'vc_priority': 'Priority',
}
def __init__(self, validate_vc_position=False, *args, **kwargs):
super().__init__(*args, **kwargs)
# Require VC position (only required when the Device is a VirtualChassis member)
self.fields['vc_position'].required = True
# Add bootstrap classes to form elements.
self.fields['vc_position'].widget.attrs = {'class': 'form-control'}
self.fields['vc_priority'].widget.attrs = {'class': 'form-control'}
# Validation of vc_position is optional. This is only required when adding a new member to an existing
# VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
self.validate_vc_position = validate_vc_position
def clean_vc_position(self):
vc_position = self.cleaned_data['vc_position']
if self.validate_vc_position:
conflicting_members = Device.objects.filter(
virtual_chassis=self.instance.virtual_chassis,
vc_position=vc_position
)
if conflicting_members.exists():
raise forms.ValidationError(
'A virtual chassis member already exists in position {}.'.format(vc_position)
)
return vc_position
class VCMemberSelectForm(forms.Form):
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
query_params={
'virtual_chassis_id': 'null',
},
selector=True
)
def clean_device(self):
device = self.cleaned_data['device']
if device.virtual_chassis is not None:
raise forms.ValidationError(
f"Device {device} is already assigned to a virtual chassis."
)
return device
#
# Device component templates
#
class ComponentTemplateForm(forms.ModelForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),
context={
'parent': 'manufacturer',
}
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Disable reassignment of DeviceType when editing an existing instance
if self.instance.pk:
self.fields['device_type'].disabled = True
class ModularComponentTemplateForm(ComponentTemplateForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all().all(),
required=False,
context={
'parent': 'manufacturer',
}
)
module_type = DynamicModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(),
required=False,
context={
'parent': 'manufacturer',
}
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Disable reassignment of ModuleType when editing an existing instance
if self.instance.pk:
self.fields['module_type'].disabled = True
# Components attached to a module need to present this standardized substitution help text.
self.fields['name'].help_text = _(
"Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range are not "
"supported (example: <code>[ge,xe]-0/0/[0-9]</code>). The token <code>{module}</code>, if present, will be "
"automatically replaced with the position value when creating a new module."
)
class ConsolePortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
)
class Meta:
model = ConsolePortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'description',
]
class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
)
class Meta:
model = ConsoleServerPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'description',
]
class PowerPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet(
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
),
)
class Meta:
model = PowerPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
]
class PowerOutletTemplateForm(ModularComponentTemplateForm):
power_port = DynamicModelChoiceField(
label=_('Power port'),
queryset=PowerPortTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type',
}
)
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description'),
)
class Meta:
model = PowerOutletTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
]
class InterfaceTemplateForm(ModularComponentTemplateForm):
bridge = DynamicModelChoiceField(
label=_('Bridge'),
queryset=InterfaceTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type',
'module_type_id': '$module_type',
}
)
fieldsets = (
FieldSet(
'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge',
),
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
FieldSet('rf_role', name=_('Wireless')),
)
class Meta:
model = InterfaceTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode',
'poe_type', 'bridge', 'rf_role',
]
class FrontPortTemplateForm(ModularComponentTemplateForm):
rear_port = DynamicModelChoiceField(
label=_('Rear port'),
queryset=RearPortTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type',
'module_type_id': '$module_type',
}
)
fieldsets = (
FieldSet(
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
'description',
),
)
class Meta:
model = FrontPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
'description',
]
class RearPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description'),
)
class Meta:
model = RearPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
]
class ModuleBayTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'position', 'description'),
)
class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'position', 'description',
]
class DeviceBayTemplateForm(ComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'name', 'label', 'description'),
)
class Meta:
model = DeviceBayTemplate
fields = [
'device_type', 'name', 'label', 'description',
]
class InventoryItemTemplateForm(ComponentTemplateForm):
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=InventoryItemTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type'
}
)
role = DynamicModelChoiceField(
label=_('Role'),
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
required=False
)
# Assigned component selectors
consoleporttemplate = DynamicModelChoiceField(
queryset=ConsolePortTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type'
},
label=_('Console port template')
)
consoleserverporttemplate = DynamicModelChoiceField(
queryset=ConsoleServerPortTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type'
},
label=_('Console server port template')
)
frontporttemplate = DynamicModelChoiceField(
queryset=FrontPortTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type'
},
label=_('Front port template')
)
interfacetemplate = DynamicModelChoiceField(
queryset=InterfaceTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type'
},
label=_('Interface template')
)
poweroutlettemplate = DynamicModelChoiceField(
queryset=PowerOutletTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type'
},
label=_('Power outlet template')
)
powerporttemplate = DynamicModelChoiceField(
queryset=PowerPortTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type'
},
label=_('Power port template')
)
rearporttemplate = DynamicModelChoiceField(
queryset=RearPortTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type'
},
label=_('Rear port template')
)
fieldsets = (
FieldSet(
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
),
FieldSet(
TabbedGroups(
FieldSet('interfacetemplate', name=_('Interface')),
FieldSet('consoleporttemplate', name=_('Console Port')),
FieldSet('consoleserverporttemplate', name=_('Console Server Port')),
FieldSet('frontporttemplate', name=_('Front Port')),
FieldSet('rearporttemplate', name=_('Rear Port')),
FieldSet('powerporttemplate', name=_('Power Port')),
FieldSet('poweroutlettemplate', name=_('Power Outlet')),
),
name=_('Component Assignment')
)
)
class Meta:
model = InventoryItemTemplate
fields = [
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
]
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
component_type = initial.get('component_type')
component_id = initial.get('component_id')
if instance:
# When editing set the initial value for component selection
for component_model in ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS):
if type(instance.component) is component_model.model_class():
initial[component_model.model] = instance.component
break
elif component_type and component_id:
# When adding the InventoryItem from a component page
content_type = ContentType.objects.filter(
MODULAR_COMPONENT_TEMPLATE_MODELS
).filter(pk=component_type).first()
if content_type:
if component := content_type.model_class().objects.filter(pk=component_id).first():
initial[content_type.model] = component
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
def clean(self):
super().clean()
# Handle object assignment
selected_objects = [
field for field in (
'consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate',
'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate'
) if self.cleaned_data[field]
]
if len(selected_objects) > 1:
raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
elif selected_objects:
self.instance.component = self.cleaned_data[selected_objects[0]]
else:
self.instance.component = None
#
# Device components
#
class DeviceComponentForm(NetBoxModelForm):
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
selector=True
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Disable reassignment of Device when editing an existing instance
if self.instance.pk:
self.fields['device'].disabled = True
class ModularDeviceComponentForm(DeviceComponentForm):
module = DynamicModelChoiceField(
label=_('Module'),
queryset=Module.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
class ConsolePortForm(ModularDeviceComponentForm):
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
),
)
class Meta:
model = ConsolePort
fields = [
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
]
class ConsoleServerPortForm(ModularDeviceComponentForm):
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
),
)
class Meta:
model = ConsoleServerPort
fields = [
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
]
class PowerPortForm(ModularDeviceComponentForm):
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
'description', 'tags',
),
)
class Meta:
model = PowerPort
fields = [
'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
'description', 'tags',
]
class PowerOutletForm(ModularDeviceComponentForm):
power_port = DynamicModelChoiceField(
label=_('Power port'),
queryset=PowerPort.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
'description', 'tags',
),
)
class Meta:
model = PowerOutlet
fields = [
'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
'description', 'tags',
]
class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
vdcs = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(),
required=False,
label=_('Virtual device contexts'),
initial_params={
'interfaces': '$parent',
},
query_params={
'device_id': '$device',
}
)
parent = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label=_('Parent interface'),
query_params={
'virtual_chassis_member_id': '$device',
}
)
bridge = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label=_('Bridged interface'),
query_params={
'virtual_chassis_member_id': '$device',
}
)
lag = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label=_('LAG interface'),
query_params={
'virtual_chassis_member_id': '$device',
'type': 'lag',
}
)
wireless_lan_group = DynamicModelChoiceField(
queryset=WirelessLANGroup.objects.all(),
required=False,
label=_('Wireless LAN group')
)
wireless_lans = DynamicModelMultipleChoiceField(
queryset=WirelessLAN.objects.all(),
required=False,
label=_('Wireless LANs'),
query_params={
'group_id': '$wireless_lan_group',
}
)
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
label=_('VLAN group'),
help_text=_("Filter VLANs available for assignment by group.")
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
label=_('Untagged VLAN'),
query_params={
'group_id': '$vlan_group',
'available_on_device': '$device',
}
)
tagged_vlans = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
label=_('Tagged VLANs'),
query_params={
'group_id': '$vlan_group',
'available_on_device': '$device',
}
)
qinq_svlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
label=_('Q-in-Q Service VLAN'),
query_params={
'group_id': '$vlan_group',
'available_on_device': '$device',
'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
}
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('VRF')
)
primary_mac_address = DynamicModelChoiceField(
queryset=MACAddress.objects.all(),
label=_('Primary MAC address'),
required=False,
quick_add=True,
quick_add_params={'interface': '$pk'}
)
wwn = forms.CharField(
empty_value=None,
required=False,
label=_('WWN')
)
vlan_translation_policy = DynamicModelChoiceField(
queryset=VLANTranslationPolicy.objects.all(),
required=False,
label=_('VLAN Translation Policy')
)
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags', name=_('Interface')
),
FieldSet('vrf', 'primary_mac_address', 'wwn', name=_('Addressing')),
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
FieldSet(
'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy',
name=_('802.1Q Switching')
),
FieldSet(
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
name=_('Wireless')
),
)
class Meta:
model = Interface
fields = [
'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge',
'lag', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'primary_mac_address',
'tags',
]
widgets = {
'speed': NumberWithOptions(
options=InterfaceSpeedChoices
),
'mode': HTMXSelect(),
}
labels = {
'mode': '802.1Q Mode',
}
class FrontPortForm(ModularDeviceComponentForm):
rear_port = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
query_params={
'device_id': '$device',
}
)
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
'description', 'tags',
),
)
class Meta:
model = FrontPort
fields = [
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
'description', 'tags',
]
class RearPortForm(ModularDeviceComponentForm):
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
),
)
class Meta:
model = RearPort
fields = [
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
]
class ModuleBayForm(ModularDeviceComponentForm):
fieldsets = (
FieldSet('device', 'module', 'name', 'label', 'position', 'description', 'tags',),
)
class Meta:
model = ModuleBay
fields = [
'device', 'module', 'name', 'label', 'position', 'description', 'tags',
]
class DeviceBayForm(DeviceComponentForm):
fieldsets = (
FieldSet('device', 'name', 'label', 'description', 'tags',),
)
class Meta:
model = DeviceBay
fields = [
'device', 'name', 'label', 'description', 'tags',
]
class PopulateDeviceBayForm(forms.Form):
installed_device = forms.ModelChoiceField(
queryset=Device.objects.all(),
label=_('Child Device'),
help_text=_("Child devices must first be created and assigned to the site and rack of the parent device.")
)
def __init__(self, device_bay, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['installed_device'].queryset = Device.objects.filter(
site=device_bay.device.site,
rack=device_bay.device.rack,
parent_bay__isnull=True,
device_type__u_height=0,
device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
).exclude(pk=device_bay.device.pk)
class InventoryItemForm(DeviceComponentForm):
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=InventoryItem.objects.all(),
required=False,
query_params={
'device_id': '$device'
}
)
role = DynamicModelChoiceField(
label=_('Role'),
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
required=False
)
# Assigned component selectors
consoleport = DynamicModelChoiceField(
queryset=ConsolePort.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Console port')
)
consoleserverport = DynamicModelChoiceField(
queryset=ConsoleServerPort.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Console server port')
)
frontport = DynamicModelChoiceField(
queryset=FrontPort.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Front port')
)
interface = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Interface')
)
poweroutlet = DynamicModelChoiceField(
queryset=PowerOutlet.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Power outlet')
)
powerport = DynamicModelChoiceField(
queryset=PowerPort.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Power port')
)
rearport = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Rear port')
)
fieldsets = (
FieldSet(
'device', 'parent', 'name', 'label', 'status', 'role', 'description', 'tags',
name=_('Inventory Item')
),
FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')),
FieldSet(
TabbedGroups(
FieldSet('interface', name=_('Interface')),
FieldSet('consoleport', name=_('Console Port')),
FieldSet('consoleserverport', name=_('Console Server Port')),
FieldSet('frontport', name=_('Front Port')),
FieldSet('rearport', name=_('Rear Port')),
FieldSet('powerport', name=_('Power Port')),
FieldSet('poweroutlet', name=_('Power Outlet')),
),
name=_('Component Assignment')
)
)
class Meta:
model = InventoryItem
fields = [
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'status', 'description', 'tags',
]
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
component_type = initial.get('component_type')
component_id = initial.get('component_id')
if instance:
# When editing set the initial value for component selection
for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
if type(instance.component) is component_model.model_class():
initial[component_model.model] = instance.component
break
elif component_type and component_id:
# When adding the InventoryItem from a component page
if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
if component := content_type.model_class().objects.filter(pk=component_id).first():
initial[content_type.model] = component
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
# Specifically allow editing the device of IntentoryItems
if self.instance.pk:
self.fields['device'].disabled = False
def clean(self):
super().clean()
# Handle object assignment
selected_objects = [
field for field in (
'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'
) if self.cleaned_data[field]
]
if len(selected_objects) > 1:
raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
elif selected_objects:
self.instance.component = self.cleaned_data[selected_objects[0]]
else:
self.instance.component = None
# Device component roles
#
class InventoryItemRoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Inventory Item Role')),
)
class Meta:
model = InventoryItemRole
fields = [
'name', 'slug', 'color', 'description', 'tags',
]
class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
selector=True
)
primary_ip4 = DynamicModelChoiceField(
queryset=IPAddress.objects.all(),
label=_('Primary IPv4'),
required=False,
query_params={
'device_id': '$device',
'family': '4',
}
)
primary_ip6 = DynamicModelChoiceField(
queryset=IPAddress.objects.all(),
label=_('Primary IPv6'),
required=False,
query_params={
'device_id': '$device',
'family': '6',
}
)
fieldsets = (
FieldSet(
'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags',
name=_('Virtual Device Context')
),
FieldSet('tenant_group', 'tenant', name=_('Tenancy'))
)
class Meta:
model = VirtualDeviceContext
fields = [
'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant',
'comments', 'tags'
]
#
# Addressing
#
class MACAddressForm(NetBoxModelForm):
mac_address = forms.CharField(
required=True,
label=_('MAC address')
)
interface = DynamicModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(),
required=False,
context={
'parent': 'device',
},
)
vminterface = DynamicModelChoiceField(
label=_('VM Interface'),
queryset=VMInterface.objects.all(),
required=False,
context={
'parent': 'virtual_machine',
},
)
fieldsets = (
FieldSet(
'mac_address', 'description', 'tags',
),
FieldSet(
TabbedGroups(
FieldSet('interface', name=_('Device')),
FieldSet('vminterface', name=_('Virtual Machine')),
),
),
)
class Meta:
model = MACAddress
fields = [
'mac_address', 'interface', 'vminterface', 'description', 'tags',
]
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
if instance:
if type(instance.assigned_object) is Interface:
initial['interface'] = instance.assigned_object
elif type(instance.assigned_object) is VMInterface:
initial['vminterface'] = instance.assigned_object
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if instance and instance.assigned_object and instance.assigned_object.primary_mac_address:
if instance.assigned_object.primary_mac_address.pk == instance.pk:
self.fields['interface'].disabled = True
self.fields['vminterface'].disabled = True
def clean(self):
super().clean()
# Handle object assignment
selected_objects = [
field for field in ('interface', 'vminterface') if self.cleaned_data[field]
]
if len(selected_objects) > 1:
raise forms.ValidationError({
selected_objects[1]: _("A MAC address can only be assigned to a single object.")
})
elif selected_objects:
self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
else:
self.instance.assigned_object = None