mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Refactor DCIM forms
This commit is contained in:
5533
netbox/dcim/forms.py
5533
netbox/dcim/forms.py
File diff suppressed because it is too large
Load Diff
10
netbox/dcim/forms/__init__.py
Normal file
10
netbox/dcim/forms/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
from .fields import *
|
||||
from .models import *
|
||||
from .filtersets import *
|
||||
from .object_create import *
|
||||
from .object_import import *
|
||||
from .bulk_create import *
|
||||
from .bulk_edit import *
|
||||
from .bulk_import import *
|
||||
from .connections import *
|
||||
from .formsets import *
|
111
netbox/dcim/forms/bulk_create.py
Normal file
111
netbox/dcim/forms/bulk_create.py
Normal file
@ -0,0 +1,111 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, form_from_model
|
||||
from .object_create import ComponentForm
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortBulkCreateForm',
|
||||
'ConsoleServerPortBulkCreateForm',
|
||||
'DeviceBayBulkCreateForm',
|
||||
# 'FrontPortBulkCreateForm',
|
||||
'InterfaceBulkCreateForm',
|
||||
'InventoryItemBulkCreateForm',
|
||||
'PowerOutletBulkCreateForm',
|
||||
'PowerPortBulkCreateForm',
|
||||
'RearPortBulkCreateForm',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortBulkCreateForm(
|
||||
form_from_model(ConsolePort, ['type', 'speed', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = ConsolePort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class ConsoleServerPortBulkCreateForm(
|
||||
form_from_model(ConsoleServerPort, ['type', 'speed', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = ConsoleServerPort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'speed', 'description', 'tags')
|
||||
|
||||
|
||||
class PowerPortBulkCreateForm(
|
||||
form_from_model(PowerPort, ['type', 'maximum_draw', 'allocated_draw', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = PowerPort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags')
|
||||
|
||||
|
||||
class PowerOutletBulkCreateForm(
|
||||
form_from_model(PowerOutlet, ['type', 'feed_leg', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = PowerOutlet
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'feed_leg', 'description', 'tags')
|
||||
|
||||
|
||||
class InterfaceBulkCreateForm(
|
||||
form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = Interface
|
||||
field_order = (
|
||||
'name_pattern', 'label_pattern', 'type', 'enabled', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'tags',
|
||||
)
|
||||
|
||||
|
||||
# class FrontPortBulkCreateForm(
|
||||
# form_from_model(FrontPort, ['label', 'type', 'description', 'tags']),
|
||||
# DeviceBulkAddComponentForm
|
||||
# ):
|
||||
# pass
|
||||
|
||||
|
||||
class RearPortBulkCreateForm(
|
||||
form_from_model(RearPort, ['type', 'color', 'positions', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = RearPort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
|
||||
model = DeviceBay
|
||||
field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
|
||||
|
||||
|
||||
class InventoryItemBulkCreateForm(
|
||||
form_from_model(InventoryItem, ['manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = InventoryItem
|
||||
field_order = (
|
||||
'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
||||
'tags',
|
||||
)
|
1090
netbox/dcim/forms/bulk_edit.py
Normal file
1090
netbox/dcim/forms/bulk_edit.py
Normal file
File diff suppressed because it is too large
Load Diff
976
netbox/dcim/forms/bulk_import.py
Normal file
976
netbox/dcim/forms/bulk_import.py
Normal file
@ -0,0 +1,976 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelCSVForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
|
||||
from virtualization.models import Cluster
|
||||
|
||||
__all__ = (
|
||||
'CableCSVForm',
|
||||
'ChildDeviceCSVForm',
|
||||
'ConsolePortCSVForm',
|
||||
'ConsoleServerPortCSVForm',
|
||||
'DeviceBayCSVForm',
|
||||
'DeviceCSVForm',
|
||||
'DeviceRoleCSVForm',
|
||||
'FrontPortCSVForm',
|
||||
'InterfaceCSVForm',
|
||||
'InventoryItemCSVForm',
|
||||
'LocationCSVForm',
|
||||
'ManufacturerCSVForm',
|
||||
'PlatformCSVForm',
|
||||
'PowerFeedCSVForm',
|
||||
'PowerOutletCSVForm',
|
||||
'PowerPanelCSVForm',
|
||||
'PowerPortCSVForm',
|
||||
'RackCSVForm',
|
||||
'RackReservationCSVForm',
|
||||
'RackRoleCSVForm',
|
||||
'RearPortCSVForm',
|
||||
'RegionCSVForm',
|
||||
'SiteCSVForm',
|
||||
'SiteGroupCSVForm',
|
||||
'VirtualChassisCSVForm',
|
||||
)
|
||||
|
||||
|
||||
class RegionCSVForm(CustomFieldModelCSVForm):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of parent region'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ('name', 'slug', 'parent', 'description')
|
||||
|
||||
|
||||
class SiteGroupCSVForm(CustomFieldModelCSVForm):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site group'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = ('name', 'slug', 'parent', 'description')
|
||||
|
||||
|
||||
class SiteCSVForm(CustomFieldModelCSVForm):
|
||||
status = CSVChoiceField(
|
||||
choices=SiteStatusChoices,
|
||||
required=False,
|
||||
help_text='Operational status'
|
||||
)
|
||||
region = CSVModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned region'
|
||||
)
|
||||
group = CSVModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned group'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = (
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'comments',
|
||||
)
|
||||
help_texts = {
|
||||
'time_zone': mark_safe(
|
||||
'Time zone (<a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">available options</a>)'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class LocationCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned site'
|
||||
)
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Parent location',
|
||||
error_messages={
|
||||
'invalid_choice': 'Location not found.',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = ('site', 'parent', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
class RackRoleCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ('name', 'slug', 'color', 'description')
|
||||
help_texts = {
|
||||
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||
}
|
||||
|
||||
|
||||
class RackCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
location = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant'
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=RackStatusChoices,
|
||||
required=False,
|
||||
help_text='Operational status'
|
||||
)
|
||||
role = CSVModelChoiceField(
|
||||
queryset=RackRole.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned role'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=RackTypeChoices,
|
||||
required=False,
|
||||
help_text='Rack type'
|
||||
)
|
||||
width = forms.ChoiceField(
|
||||
choices=RackWidthChoices,
|
||||
help_text='Rail-to-rail width (in inches)'
|
||||
)
|
||||
outer_unit = CSVChoiceField(
|
||||
choices=RackDimensionUnitChoices,
|
||||
required=False,
|
||||
help_text='Unit for outer dimensions'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = (
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag',
|
||||
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
||||
)
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit location queryset by assigned site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
|
||||
class RackReservationCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Parent site'
|
||||
)
|
||||
location = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text="Rack's location (if any)"
|
||||
)
|
||||
rack = CSVModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Rack'
|
||||
)
|
||||
units = SimpleArrayField(
|
||||
base_field=forms.IntegerField(),
|
||||
required=True,
|
||||
help_text='Comma-separated list of individual unit numbers'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ('site', 'location', 'rack', 'units', 'tenant', 'description')
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit location queryset by assigned site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
# Limit rack queryset by assigned site and group
|
||||
params = {
|
||||
f"site__{self.fields['site'].to_field_name}": data.get('site'),
|
||||
f"location__{self.fields['location'].to_field_name}": data.get('location'),
|
||||
}
|
||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
||||
|
||||
|
||||
class ManufacturerCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ('name', 'slug', 'description')
|
||||
|
||||
|
||||
class DeviceRoleCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ('name', 'slug', 'color', 'vm_role', 'description')
|
||||
help_texts = {
|
||||
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||
}
|
||||
|
||||
|
||||
class PlatformCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
manufacturer = CSVModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Limit platform assignments to this manufacturer'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description')
|
||||
|
||||
|
||||
class BaseDeviceCSVForm(CustomFieldModelCSVForm):
|
||||
device_role = CSVModelChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned role'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
manufacturer = CSVModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Device type manufacturer'
|
||||
)
|
||||
device_type = CSVModelChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
to_field_name='model',
|
||||
help_text='Device type model'
|
||||
)
|
||||
platform = CSVModelChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned platform'
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=DeviceStatusChoices,
|
||||
help_text='Operational status'
|
||||
)
|
||||
virtual_chassis = CSVModelChoiceField(
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Virtual chassis'
|
||||
)
|
||||
cluster = CSVModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Virtualization cluster'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
fields = []
|
||||
model = Device
|
||||
help_texts = {
|
||||
'vc_position': 'Virtual chassis position',
|
||||
'vc_priority': 'Virtual chassis priority',
|
||||
}
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit device type queryset by manufacturer
|
||||
params = {f"manufacturer__{self.fields['manufacturer'].to_field_name}": data.get('manufacturer')}
|
||||
self.fields['device_type'].queryset = self.fields['device_type'].queryset.filter(**params)
|
||||
|
||||
|
||||
class DeviceCSVForm(BaseDeviceCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned site'
|
||||
)
|
||||
location = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text="Assigned location (if any)"
|
||||
)
|
||||
rack = CSVModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text="Assigned rack (if any)"
|
||||
)
|
||||
face = CSVChoiceField(
|
||||
choices=DeviceFaceChoices,
|
||||
required=False,
|
||||
help_text='Mounted rack face'
|
||||
)
|
||||
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'location', 'rack', 'position', 'face', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster',
|
||||
'comments',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit location queryset by assigned site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
# Limit rack queryset by assigned site and group
|
||||
params = {
|
||||
f"site__{self.fields['site'].to_field_name}": data.get('site'),
|
||||
f"location__{self.fields['location'].to_field_name}": data.get('location'),
|
||||
}
|
||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
||||
|
||||
|
||||
class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Parent device'
|
||||
)
|
||||
device_bay = CSVModelChoiceField(
|
||||
queryset=DeviceBay.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Device bay in which this device is installed'
|
||||
)
|
||||
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit device bay queryset by parent device
|
||||
params = {f"device__{self.fields['parent'].to_field_name}": data.get('parent')}
|
||||
self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Set parent_bay reverse relationship
|
||||
device_bay = self.cleaned_data.get('device_bay')
|
||||
if device_bay:
|
||||
self.instance.parent_bay = device_bay
|
||||
|
||||
# Inherit site and rack from parent device
|
||||
parent = self.cleaned_data.get('parent')
|
||||
if parent:
|
||||
self.instance.site = parent.site
|
||||
self.instance.rack = parent.rack
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False,
|
||||
help_text='Port type'
|
||||
)
|
||||
speed = CSVTypedChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
coerce=int,
|
||||
empty_value=None,
|
||||
required=False,
|
||||
help_text='Port speed in bps'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False,
|
||||
help_text='Port type'
|
||||
)
|
||||
speed = CSVTypedChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
coerce=int,
|
||||
empty_value=None,
|
||||
required=False,
|
||||
help_text='Port speed in bps'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
|
||||
|
||||
|
||||
class PowerPortCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
required=False,
|
||||
help_text='Port type'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = (
|
||||
'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description',
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
required=False,
|
||||
help_text='Outlet type'
|
||||
)
|
||||
power_port = CSVModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Local power port which feeds this outlet'
|
||||
)
|
||||
feed_leg = CSVChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
required=False,
|
||||
help_text='Electrical phase (for three-phase circuits)'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit PowerPort choices to those belonging to this device (or VC master)
|
||||
if self.is_bound:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
device = None
|
||||
else:
|
||||
try:
|
||||
device = self.instance.device
|
||||
except Device.DoesNotExist:
|
||||
device = None
|
||||
|
||||
if device:
|
||||
self.fields['power_port'].queryset = PowerPort.objects.filter(
|
||||
device__in=[device, device.get_vc_master()]
|
||||
)
|
||||
else:
|
||||
self.fields['power_port'].queryset = PowerPort.objects.none()
|
||||
|
||||
|
||||
class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Parent interface'
|
||||
)
|
||||
lag = CSVModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Parent LAG interface'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
help_text='Physical medium'
|
||||
)
|
||||
mode = CSVChoiceField(
|
||||
choices=InterfaceModeChoices,
|
||||
required=False,
|
||||
help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = (
|
||||
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu',
|
||||
'mgmt_only', 'description', 'mode',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit LAG choices to interfaces belonging to this device (or virtual chassis)
|
||||
device = None
|
||||
if self.is_bound and 'device' in self.data:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
pass
|
||||
if device and device.virtual_chassis:
|
||||
self.fields['lag'].queryset = Interface.objects.filter(
|
||||
Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis),
|
||||
type=InterfaceTypeChoices.TYPE_LAG
|
||||
)
|
||||
self.fields['parent'].queryset = Interface.objects.filter(
|
||||
Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis)
|
||||
)
|
||||
elif device:
|
||||
self.fields['lag'].queryset = Interface.objects.filter(
|
||||
device=device,
|
||||
type=InterfaceTypeChoices.TYPE_LAG
|
||||
)
|
||||
self.fields['parent'].queryset = Interface.objects.filter(device=device)
|
||||
else:
|
||||
self.fields['lag'].queryset = Interface.objects.none()
|
||||
self.fields['parent'].queryset = Interface.objects.none()
|
||||
|
||||
def clean_enabled(self):
|
||||
# Make sure enabled is True when it's not included in the uploaded data
|
||||
if 'enabled' not in self.data:
|
||||
return True
|
||||
else:
|
||||
return self.cleaned_data['enabled']
|
||||
|
||||
|
||||
class FrontPortCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
rear_port = CSVModelChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Corresponding rear port'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
help_text='Physical medium classification'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
|
||||
'description',
|
||||
)
|
||||
help_texts = {
|
||||
'rear_port_position': 'Mapped position on corresponding rear port',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit RearPort choices to those belonging to this device (or VC master)
|
||||
if self.is_bound:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
device = None
|
||||
else:
|
||||
try:
|
||||
device = self.instance.device
|
||||
except Device.DoesNotExist:
|
||||
device = None
|
||||
|
||||
if device:
|
||||
self.fields['rear_port'].queryset = RearPort.objects.filter(
|
||||
device__in=[device, device.get_vc_master()]
|
||||
)
|
||||
else:
|
||||
self.fields['rear_port'].queryset = RearPort.objects.none()
|
||||
|
||||
|
||||
class RearPortCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
help_text='Physical medium classification',
|
||||
choices=PortTypeChoices,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description')
|
||||
help_texts = {
|
||||
'positions': 'Number of front ports which may be mapped'
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
installed_device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Child device installed within this bay',
|
||||
error_messages={
|
||||
'invalid_choice': 'Child device not found.',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ('device', 'name', 'label', 'installed_device', 'description')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit installed device choices to devices of the correct type and location
|
||||
if self.is_bound:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
device = None
|
||||
else:
|
||||
try:
|
||||
device = self.instance.device
|
||||
except Device.DoesNotExist:
|
||||
device = None
|
||||
|
||||
if device:
|
||||
self.fields['installed_device'].queryset = Device.objects.filter(
|
||||
site=device.site,
|
||||
rack=device.rack,
|
||||
parent_bay__isnull=True,
|
||||
device_type__u_height=0,
|
||||
device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
|
||||
).exclude(pk=device.pk)
|
||||
else:
|
||||
self.fields['installed_device'].queryset = Interface.objects.none()
|
||||
|
||||
|
||||
class InventoryItemCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
manufacturer = CSVModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Parent inventory item'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit parent choices to inventory items belonging to this device
|
||||
device = None
|
||||
if self.is_bound and 'device' in self.data:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
pass
|
||||
if device:
|
||||
self.fields['parent'].queryset = InventoryItem.objects.filter(device=device)
|
||||
else:
|
||||
self.fields['parent'].queryset = InventoryItem.objects.none()
|
||||
|
||||
|
||||
class CableCSVForm(CustomFieldModelCSVForm):
|
||||
# Termination A
|
||||
side_a_device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Side A device'
|
||||
)
|
||||
side_a_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=CABLE_TERMINATION_MODELS,
|
||||
help_text='Side A type'
|
||||
)
|
||||
side_a_name = forms.CharField(
|
||||
help_text='Side A component name'
|
||||
)
|
||||
|
||||
# Termination B
|
||||
side_b_device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Side B device'
|
||||
)
|
||||
side_b_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=CABLE_TERMINATION_MODELS,
|
||||
help_text='Side B type'
|
||||
)
|
||||
side_b_name = forms.CharField(
|
||||
help_text='Side B component name'
|
||||
)
|
||||
|
||||
# Cable attributes
|
||||
status = CSVChoiceField(
|
||||
choices=CableStatusChoices,
|
||||
required=False,
|
||||
help_text='Connection status'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=CableTypeChoices,
|
||||
required=False,
|
||||
help_text='Physical medium classification'
|
||||
)
|
||||
length_unit = CSVChoiceField(
|
||||
choices=CableLengthUnitChoices,
|
||||
required=False,
|
||||
help_text='Length unit'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
|
||||
'status', 'label', 'color', 'length', 'length_unit',
|
||||
]
|
||||
help_texts = {
|
||||
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||
}
|
||||
|
||||
def _clean_side(self, side):
|
||||
"""
|
||||
Derive a Cable's A/B termination objects.
|
||||
|
||||
:param side: 'a' or 'b'
|
||||
"""
|
||||
assert side in 'ab', f"Invalid side designation: {side}"
|
||||
|
||||
device = self.cleaned_data.get(f'side_{side}_device')
|
||||
content_type = self.cleaned_data.get(f'side_{side}_type')
|
||||
name = self.cleaned_data.get(f'side_{side}_name')
|
||||
if not device or not content_type or not name:
|
||||
return None
|
||||
|
||||
model = content_type.model_class()
|
||||
try:
|
||||
termination_object = model.objects.get(device=device, name=name)
|
||||
if termination_object.cable is not None:
|
||||
raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
|
||||
except ObjectDoesNotExist:
|
||||
raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
|
||||
|
||||
setattr(self.instance, f'termination_{side}', termination_object)
|
||||
return termination_object
|
||||
|
||||
def clean_side_a_name(self):
|
||||
return self._clean_side('a')
|
||||
|
||||
def clean_side_b_name(self):
|
||||
return self._clean_side('b')
|
||||
|
||||
def clean_length_unit(self):
|
||||
# Avoid trying to save as NULL
|
||||
length_unit = self.cleaned_data.get('length_unit', None)
|
||||
return length_unit if length_unit is not None else ''
|
||||
|
||||
|
||||
class VirtualChassisCSVForm(CustomFieldModelCSVForm):
|
||||
master = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Master device'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ('name', 'domain', 'master')
|
||||
|
||||
|
||||
class PowerPanelCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site'
|
||||
)
|
||||
location = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ('site', 'location', 'name')
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit group queryset by assigned site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
|
||||
class PowerFeedCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned site'
|
||||
)
|
||||
power_panel = CSVModelChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Upstream power panel'
|
||||
)
|
||||
location = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text="Rack's location (if any)"
|
||||
)
|
||||
rack = CSVModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Rack'
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=PowerFeedStatusChoices,
|
||||
required=False,
|
||||
help_text='Operational status'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=PowerFeedTypeChoices,
|
||||
required=False,
|
||||
help_text='Primary or redundant'
|
||||
)
|
||||
supply = CSVChoiceField(
|
||||
choices=PowerFeedSupplyChoices,
|
||||
required=False,
|
||||
help_text='Supply type (AC/DC)'
|
||||
)
|
||||
phase = CSVChoiceField(
|
||||
choices=PowerFeedPhaseChoices,
|
||||
required=False,
|
||||
help_text='Single or three-phase'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = (
|
||||
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
|
||||
'voltage', 'amperage', 'max_utilization', 'comments',
|
||||
)
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit power_panel queryset by site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['power_panel'].queryset = self.fields['power_panel'].queryset.filter(**params)
|
||||
|
||||
# Limit location queryset by site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
# Limit rack queryset by site and group
|
||||
params = {
|
||||
f"site__{self.fields['site'].to_field_name}": data.get('site'),
|
||||
f"location__{self.fields['location'].to_field_name}": data.get('location'),
|
||||
}
|
||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
49
netbox/dcim/forms/common.py
Normal file
49
netbox/dcim/forms/common.py
Normal file
@ -0,0 +1,49 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
|
||||
__all__ = (
|
||||
'InterfaceCommonForm',
|
||||
)
|
||||
|
||||
|
||||
class InterfaceCommonForm(forms.Form):
|
||||
mac_address = forms.CharField(
|
||||
empty_value=None,
|
||||
required=False,
|
||||
label='MAC address'
|
||||
)
|
||||
mtu = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=INTERFACE_MTU_MIN,
|
||||
max_value=INTERFACE_MTU_MAX,
|
||||
label='MTU'
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine'
|
||||
tagged_vlans = self.cleaned_data.get('tagged_vlans')
|
||||
|
||||
# Untagged interfaces cannot be assigned tagged VLANs
|
||||
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
|
||||
raise forms.ValidationError({
|
||||
'mode': "An access interface cannot have tagged VLANs assigned."
|
||||
})
|
||||
|
||||
# Remove all tagged VLAN assignments from "tagged all" interfaces
|
||||
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
|
||||
self.cleaned_data['tagged_vlans'] = []
|
||||
|
||||
# Validate tagged VLANs; must be a global VLAN or in the same site
|
||||
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans:
|
||||
valid_sites = [None, self.cleaned_data[parent_field].site]
|
||||
invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]
|
||||
|
||||
if invalid_vlans:
|
||||
raise forms.ValidationError({
|
||||
'tagged_vlans': f"The tagged VLANs ({', '.join(invalid_vlans)}) must belong to the same site as "
|
||||
f"the interface's parent device/VM, or they must be global"
|
||||
})
|
289
netbox/dcim/forms/connections.py
Normal file
289
netbox/dcim/forms/connections.py
Normal file
@ -0,0 +1,289 @@
|
||||
from circuits.models import Circuit, CircuitTermination, Provider
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelForm
|
||||
from extras.models import Tag
|
||||
from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
|
||||
|
||||
__all__ = (
|
||||
'ConnectCableToCircuitTerminationForm',
|
||||
'ConnectCableToConsolePortForm',
|
||||
'ConnectCableToConsoleServerPortForm',
|
||||
'ConnectCableToFrontPortForm',
|
||||
'ConnectCableToInterfaceForm',
|
||||
'ConnectCableToPowerFeedForm',
|
||||
'ConnectCableToPowerPortForm',
|
||||
'ConnectCableToPowerOutletForm',
|
||||
'ConnectCableToRearPortForm',
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm):
|
||||
"""
|
||||
Base form for connecting a Cable to a Device component
|
||||
"""
|
||||
termination_b_region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
)
|
||||
termination_b_site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
}
|
||||
)
|
||||
termination_b_location = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
label='Location',
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$termination_b_site'
|
||||
}
|
||||
)
|
||||
termination_b_rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$termination_b_site',
|
||||
'location_id': '$termination_b_location',
|
||||
}
|
||||
)
|
||||
termination_b_device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device',
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$termination_b_site',
|
||||
'location_id': '$termination_b_location',
|
||||
'rack_id': '$termination_b_rack',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device',
|
||||
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'status': StaticSelect,
|
||||
'type': StaticSelect,
|
||||
'length_unit': StaticSelect,
|
||||
}
|
||||
|
||||
def clean_termination_b_id(self):
|
||||
# Return the PK rather than the object
|
||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
||||
|
||||
|
||||
class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=ConsolePort.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=ConsoleServerPort.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToPowerPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=PowerOutlet.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToInterfaceForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device',
|
||||
'kind': 'physical',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToFrontPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=FrontPort.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm):
|
||||
termination_b_provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider',
|
||||
required=False
|
||||
)
|
||||
termination_b_region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
)
|
||||
termination_b_site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
}
|
||||
)
|
||||
termination_b_circuit = DynamicModelChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
label='Circuit',
|
||||
query_params={
|
||||
'provider_id': '$termination_b_provider',
|
||||
'site_id': '$termination_b_site',
|
||||
}
|
||||
)
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=CircuitTermination.objects.all(),
|
||||
label='Side',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'circuit_id': '$termination_b_circuit'
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
|
||||
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
]
|
||||
|
||||
def clean_termination_b_id(self):
|
||||
# Return the PK rather than the object
|
||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
||||
|
||||
|
||||
class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||
termination_b_region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
)
|
||||
termination_b_site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
}
|
||||
)
|
||||
termination_b_location = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
label='Location',
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$termination_b_site'
|
||||
}
|
||||
)
|
||||
termination_b_powerpanel = DynamicModelChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
label='Power Panel',
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$termination_b_site',
|
||||
'location_id': '$termination_b_location',
|
||||
}
|
||||
)
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=PowerFeed.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'power_panel_id': '$termination_b_powerpanel'
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
|
||||
'color', 'length', 'length_unit', 'tags',
|
||||
]
|
||||
|
||||
def clean_termination_b_id(self):
|
||||
# Return the PK rather than the object
|
||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
25
netbox/dcim/forms/fields.py
Normal file
25
netbox/dcim/forms/fields.py
Normal file
@ -0,0 +1,25 @@
|
||||
from django import forms
|
||||
from netaddr import EUI
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
__all__ = (
|
||||
'MACAddressField',
|
||||
)
|
||||
|
||||
|
||||
class MACAddressField(forms.Field):
|
||||
widget = forms.CharField
|
||||
default_error_messages = {
|
||||
'invalid': 'MAC address must be in EUI-48 format',
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
value = super().to_python(value)
|
||||
|
||||
# Validate MAC address format
|
||||
try:
|
||||
value = EUI(value.strip())
|
||||
except AddrFormatError:
|
||||
raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
|
||||
|
||||
return value
|
1143
netbox/dcim/forms/filtersets.py
Normal file
1143
netbox/dcim/forms/filtersets.py
Normal file
File diff suppressed because it is too large
Load Diff
21
netbox/dcim/forms/formsets.py
Normal file
21
netbox/dcim/forms/formsets.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django import forms
|
||||
|
||||
__all__ = (
|
||||
'BaseVCMemberFormSet',
|
||||
)
|
||||
|
||||
|
||||
class BaseVCMemberFormSet(forms.BaseModelFormSet):
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check for duplicate VC position values
|
||||
vc_position_list = []
|
||||
for form in self.forms:
|
||||
vc_position = form.cleaned_data.get('vc_position')
|
||||
if vc_position:
|
||||
if vc_position in vc_position_list:
|
||||
error_msg = f"A virtual chassis member already exists in position {vc_position}."
|
||||
form.add_error('vc_position', error_msg)
|
||||
vc_position_list.append(vc_position)
|
1232
netbox/dcim/forms/models.py
Normal file
1232
netbox/dcim/forms/models.py
Normal file
File diff suppressed because it is too large
Load Diff
614
netbox/dcim/forms/object_create.py
Normal file
614
netbox/dcim/forms/object_create.py
Normal file
@ -0,0 +1,614 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelForm, CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from ipam.models import VLAN
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
ExpandableNameField, StaticSelect,
|
||||
)
|
||||
from .common import InterfaceCommonForm
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortCreateForm',
|
||||
'ConsolePortTemplateCreateForm',
|
||||
'ConsoleServerPortCreateForm',
|
||||
'ConsoleServerPortTemplateCreateForm',
|
||||
'DeviceBayCreateForm',
|
||||
'DeviceBayTemplateCreateForm',
|
||||
'FrontPortCreateForm',
|
||||
'FrontPortTemplateCreateForm',
|
||||
'InterfaceCreateForm',
|
||||
'InterfaceTemplateCreateForm',
|
||||
'InventoryItemCreateForm',
|
||||
'PowerOutletCreateForm',
|
||||
'PowerOutletTemplateCreateForm',
|
||||
'PowerPortCreateForm',
|
||||
'PowerPortTemplateCreateForm',
|
||||
'RearPortCreateForm',
|
||||
'RearPortTemplateCreateForm',
|
||||
'VirtualChassisCreateForm',
|
||||
)
|
||||
|
||||
|
||||
class ComponentForm(forms.Form):
|
||||
"""
|
||||
Subclass this form when facilitating the creation of one or more device component or component templates based on
|
||||
a name pattern.
|
||||
"""
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
label_pattern = ExpandableNameField(
|
||||
label='Label',
|
||||
required=False,
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate that the number of components being created from both the name_pattern and label_pattern are equal
|
||||
if self.cleaned_data['label_pattern']:
|
||||
name_pattern_count = len(self.cleaned_data['name_pattern'])
|
||||
label_pattern_count = len(self.cleaned_data['label_pattern'])
|
||||
if name_pattern_count != label_pattern_count:
|
||||
raise forms.ValidationError({
|
||||
'label_pattern': f'The provided name pattern will create {name_pattern_count} components, however '
|
||||
f'{label_pattern_count} labels will be generated. These counts must match.'
|
||||
}, code='label_pattern_mismatch')
|
||||
|
||||
|
||||
class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
}
|
||||
)
|
||||
members = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'rack_id': '$rack',
|
||||
}
|
||||
)
|
||||
initial_position = forms.IntegerField(
|
||||
initial=1,
|
||||
required=False,
|
||||
help_text='Position of the first member device. Increases by one for each additional member.'
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = [
|
||||
'name', 'domain', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
# Assign VC members
|
||||
if instance.pk:
|
||||
initial_position = self.cleaned_data.get('initial_position') or 1
|
||||
for i, member in enumerate(self.cleaned_data['members'], start=initial_position):
|
||||
member.virtual_chassis = instance
|
||||
member.vc_position = i
|
||||
member.save()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
#
|
||||
# Component templates
|
||||
#
|
||||
|
||||
class ComponentTemplateCreateForm(BootstrapMixin, ComponentForm):
|
||||
"""
|
||||
Base form for the creation of device component templates (subclassed from ComponentTemplateModel).
|
||||
"""
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'device_types': 'device_type'
|
||||
}
|
||||
)
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
query_params={
|
||||
'manufacturer_id': '$manufacturer'
|
||||
}
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description')
|
||||
|
||||
|
||||
class PowerPortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerPortTypeChoices),
|
||||
required=False
|
||||
)
|
||||
maximum_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Maximum power draw (watts)"
|
||||
)
|
||||
allocated_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Allocated power draw (watts)"
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw',
|
||||
'description',
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletTypeChoices),
|
||||
required=False
|
||||
)
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
required=False
|
||||
)
|
||||
feed_leg = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletFeedLegChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg',
|
||||
'description',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port choices to current DeviceType
|
||||
device_type = DeviceType.objects.get(
|
||||
pk=self.initial.get('device_type') or self.data.get('device_type')
|
||||
)
|
||||
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
|
||||
device_type=device_type
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
mgmt_only = forms.BooleanField(
|
||||
required=False,
|
||||
label='Management only'
|
||||
)
|
||||
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only', 'description')
|
||||
|
||||
|
||||
class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'description',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
device_type = DeviceType.objects.get(
|
||||
pk=self.initial.get('device_type') or self.data.get('device_type')
|
||||
)
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in device_type.frontporttemplates.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = RearPortTemplate.objects.filter(device_type=device_type)
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate that the number of ports being created equals the number of selected (rear port, position) tuples
|
||||
front_port_count = len(self.cleaned_data['name_pattern'])
|
||||
rear_port_count = len(self.cleaned_data['rear_port_set'])
|
||||
if front_port_count != rear_port_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
|
||||
'were selected. These counts must match.'.format(front_port_count, rear_port_count)
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
}
|
||||
|
||||
|
||||
class RearPortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
positions = forms.IntegerField(
|
||||
min_value=REARPORT_POSITIONS_MIN,
|
||||
max_value=REARPORT_POSITIONS_MAX,
|
||||
initial=1,
|
||||
help_text='The number of front ports which may be mapped to each rear port'
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'description',
|
||||
)
|
||||
|
||||
|
||||
class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ComponentCreateForm(BootstrapMixin, CustomFieldsMixin, ComponentForm):
|
||||
"""
|
||||
Base form for the creation of device components (models subclassed from ComponentModel).
|
||||
"""
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortCreateForm(ComponentCreateForm):
|
||||
model = ConsolePort
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
speed = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortSpeedChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class ConsoleServerPortCreateForm(ComponentCreateForm):
|
||||
model = ConsoleServerPort
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
speed = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortSpeedChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class PowerPortCreateForm(ComponentCreateForm):
|
||||
model = PowerPort
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerPortTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
maximum_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Maximum draw in watts"
|
||||
)
|
||||
allocated_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Allocated draw in watts"
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
|
||||
'description', 'tags',
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletCreateForm(ComponentCreateForm):
|
||||
model = PowerOutlet
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False
|
||||
)
|
||||
feed_leg = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletFeedLegChoices),
|
||||
required=False
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port queryset to PowerPorts which belong to the parent Device
|
||||
device = Device.objects.get(
|
||||
pk=self.initial.get('device') or self.data.get('device')
|
||||
)
|
||||
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
|
||||
|
||||
|
||||
class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
||||
model = Interface
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
enabled = forms.BooleanField(
|
||||
required=False,
|
||||
initial=True
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'type': 'lag',
|
||||
}
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label='MAC Address'
|
||||
)
|
||||
mgmt_only = forms.BooleanField(
|
||||
required=False,
|
||||
label='Management only',
|
||||
help_text='This interface is used only for out-of-band management'
|
||||
)
|
||||
mode = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfaceModeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
untagged_vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
||||
'description', 'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit VLAN choices by device
|
||||
device_id = self.initial.get('device') or self.data.get('device')
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device_id)
|
||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device_id)
|
||||
|
||||
|
||||
class FrontPortCreateForm(ComponentCreateForm):
|
||||
model = FrontPort
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
device = Device.objects.get(
|
||||
pk=self.initial.get('device') or self.data.get('device')
|
||||
)
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available
|
||||
# mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in device.frontports.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = RearPort.objects.filter(device=device)
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate that the number of ports being created equals the number of selected (rear port, position) tuples
|
||||
front_port_count = len(self.cleaned_data['name_pattern'])
|
||||
rear_port_count = len(self.cleaned_data['rear_port_set'])
|
||||
if front_port_count != rear_port_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
|
||||
'were selected. These counts must match.'.format(front_port_count, rear_port_count)
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
}
|
||||
|
||||
|
||||
class RearPortCreateForm(ComponentCreateForm):
|
||||
model = RearPort
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
positions = forms.IntegerField(
|
||||
min_value=REARPORT_POSITIONS_MIN,
|
||||
max_value=REARPORT_POSITIONS_MAX,
|
||||
initial=1,
|
||||
help_text='The number of front ports which may be mapped to each rear port'
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)
|
||||
|
||||
|
||||
class DeviceBayCreateForm(ComponentCreateForm):
|
||||
model = DeviceBay
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')
|
||||
|
||||
|
||||
class InventoryItemCreateForm(ComponentCreateForm):
|
||||
model = InventoryItem
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device'
|
||||
}
|
||||
)
|
||||
part_id = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
label='Part ID'
|
||||
)
|
||||
serial = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
)
|
||||
asset_tag = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
)
|
||||
field_order = (
|
||||
'device', 'parent', 'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'description', 'tags',
|
||||
)
|
148
netbox/dcim/forms/object_import.py
Normal file
148
netbox/dcim/forms/object_import.py
Normal file
@ -0,0 +1,148 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.choices import InterfaceTypeChoices, PortTypeChoices
|
||||
from dcim.models import *
|
||||
from utilities.forms import BootstrapMixin
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortTemplateImportForm',
|
||||
'ConsoleServerPortTemplateImportForm',
|
||||
'DeviceBayTemplateImportForm',
|
||||
'DeviceTypeImportForm',
|
||||
'FrontPortTemplateImportForm',
|
||||
'InterfaceTemplateImportForm',
|
||||
'PowerOutletTemplateImportForm',
|
||||
'PowerPortTemplateImportForm',
|
||||
'RearPortTemplateImportForm',
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'comments',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Component template import forms
|
||||
#
|
||||
|
||||
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
def __init__(self, device_type, data=None, *args, **kwargs):
|
||||
|
||||
# Must pass the parent DeviceType on form initialization
|
||||
data.update({
|
||||
'device_type': device_type.pk,
|
||||
})
|
||||
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
def clean_device_type(self):
|
||||
|
||||
data = self.cleaned_data['device_type']
|
||||
|
||||
# Limit fields referencing other components to the parent DeviceType
|
||||
for field_name, field in self.fields.items():
|
||||
if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type':
|
||||
field.queryset = field.queryset.filter(device_type=data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypeChoices.CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'mgmt_only', 'description',
|
||||
]
|
||||
|
||||
|
||||
class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices.CHOICES
|
||||
)
|
||||
rear_port = forms.ModelChoiceField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
|
||||
]
|
||||
|
||||
|
||||
class RearPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices.CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'positions', 'label', 'description',
|
||||
]
|
||||
|
||||
|
||||
class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'description',
|
||||
]
|
@ -1,5 +1,6 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices
|
||||
from dcim.forms import *
|
||||
from dcim.models import *
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
@ -1,11 +1,10 @@
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Prefetch
|
||||
from django.db.models import Prefetch
|
||||
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
|
@ -5,7 +5,8 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.choices import InterfaceModeChoices
|
||||
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
|
||||
from dcim.forms import InterfaceCommonForm, INTERFACE_MODE_HELP_TEXT
|
||||
from dcim.forms.models import INTERFACE_MODE_HELP_TEXT
|
||||
from dcim.forms.common import InterfaceCommonForm
|
||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm,
|
||||
@ -569,7 +570,12 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldM
|
||||
]
|
||||
|
||||
|
||||
class VirtualMachineFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class VirtualMachineFilterForm(
|
||||
BootstrapMixin,
|
||||
LocalConfigContextFilterForm,
|
||||
TenancyFilterForm,
|
||||
CustomFieldModelFilterForm
|
||||
):
|
||||
model = VirtualMachine
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
|
Reference in New Issue
Block a user