import re from django import forms from django.contrib.auth.models import User 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 django.utils.translation import gettext as _ from netaddr import EUI from netaddr.core import AddrFormatError from timezone_field import TimeZoneFormField from circuits.models import Circuit, CircuitTermination, Provider from extras.forms import ( AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldModelCSVForm, CustomFieldFilterForm, CustomFieldModelForm, LocalConfigContextFilterForm, ) from extras.models import Tag from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN from ipam.models import IPAddress, VLAN from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup from .choices import * from .constants import * from .models import * DEVICE_BY_PK_RE = r'{\d+\}' INTERFACE_MODE_HELP_TEXT = """ Access: One untagged VLAN
Tagged: One untagged VLAN and/or one or more tagged VLANs
Tagged (All): Implies all VLANs are available (w/optional untagged VLAN) """ def get_device_by_name_or_pk(name): """ Attempt to retrieve a device by either its name or primary key ('{pk}'). """ if re.match(DEVICE_BY_PK_RE, name): pk = name.strip('{}') device = Device.objects.get(pk=pk) else: device = Device.objects.get(name=name) return device class DeviceComponentFilterForm(BootstrapMixin, CustomFieldFilterForm): field_order = [ 'name', 'label', 'region_id', 'site_group_id', 'site_id', ] field_groups = [ ['name', 'label'], ['region_id', 'site_group_id', 'site_id'], ] name = forms.CharField( required=False ) label = forms.CharField( required=False ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Site') ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, query_params={ 'site_id': '$site_id' }, label=_('Device') ) class InterfaceCommonForm(forms.Form): mac_address = forms.CharField( empty_value=None, required=False, label='MAC address' ) def clean(self): super().clean() parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine' tagged_vlans = self.cleaned_data['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: 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" }) 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') # # Fields # 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 # # Regions # class RegionForm(BootstrapMixin, CustomFieldModelForm): parent = DynamicModelChoiceField( queryset=Region.objects.all(), required=False ) slug = SlugField() class Meta: model = Region fields = ( 'parent', 'name', 'slug', 'description', ) 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 RegionBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Region.objects.all(), widget=forms.MultipleHiddenInput ) parent = DynamicModelChoiceField( queryset=Region.objects.all(), required=False ) description = forms.CharField( max_length=200, required=False ) class Meta: nullable_fields = ['parent', 'description'] class RegionFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Site # # Site groups # class SiteGroupForm(BootstrapMixin, CustomFieldModelForm): parent = DynamicModelChoiceField( queryset=SiteGroup.objects.all(), required=False ) slug = SlugField() class Meta: model = SiteGroup fields = ( 'parent', 'name', 'slug', '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 SiteGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=SiteGroup.objects.all(), widget=forms.MultipleHiddenInput ) parent = DynamicModelChoiceField( queryset=SiteGroup.objects.all(), required=False ) description = forms.CharField( max_length=200, required=False ) class Meta: nullable_fields = ['parent', 'description'] class SiteGroupFilterForm(BootstrapMixin, CustomFieldFilterForm): model = SiteGroup # # Sites # class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False ) group = DynamicModelChoiceField( queryset=SiteGroup.objects.all(), required=False ) slug = SlugField() time_zone = TimeZoneFormField( choices=add_blank_choice(TimeZoneFormField().choices), required=False, widget=StaticSelect2() ) comments = CommentField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = Site fields = [ 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments', 'tags', ] fieldsets = ( ('Site', ( 'name', 'slug', 'status', 'region', 'group', 'facility', 'asn', 'time_zone', 'description', 'tags', )), ('Tenancy', ('tenant_group', 'tenant')), ('Contact Info', ( 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', )), ) widgets = { 'physical_address': SmallTextarea( attrs={ 'rows': 3, } ), 'shipping_address': SmallTextarea( attrs={ 'rows': 3, } ), 'status': StaticSelect2(), 'time_zone': StaticSelect2(), } help_texts = { 'name': "Full name of the site", 'facility': "Data center provider and facility (e.g. Equinix NY7)", 'asn': "BGP autonomous system number", 'time_zone': "Local time zone", 'description': "Short description (will appear in sites list)", 'physical_address': "Physical location of the building (e.g. for GPS)", 'shipping_address': "If different from the physical address", 'latitude': "Latitude in decimal format (xx.yyyyyy)", 'longitude': "Longitude in decimal format (xx.yyyyyy)" } 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 (available options)' ) } class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Site.objects.all(), widget=forms.MultipleHiddenInput ) status = forms.ChoiceField( choices=add_blank_choice(SiteStatusChoices), required=False, initial='', widget=StaticSelect2() ) region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False ) group = DynamicModelChoiceField( queryset=SiteGroup.objects.all(), required=False ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), required=False ) asn = forms.IntegerField( min_value=BGP_ASN_MIN, max_value=BGP_ASN_MAX, required=False, label='ASN' ) description = forms.CharField( max_length=100, required=False ) time_zone = TimeZoneFormField( choices=add_blank_choice(TimeZoneFormField().choices), required=False, widget=StaticSelect2() ) class Meta: nullable_fields = [ 'region', 'group', 'tenant', 'asn', 'description', 'time_zone', ] class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Site field_order = ['status', 'region_id', 'tenant_group_id', 'tenant_id'] field_groups = [ ['status', 'region_id'], ['tenant_group_id', 'tenant_id'], ['tag'] ] status = forms.MultipleChoiceField( choices=SiteStatusChoices, required=False, widget=StaticSelect2Multiple() ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, label=_('Group') ) tag = TagFilterField(model) # # Locations # class LocationForm(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(), query_params={ 'region_id': '$region', 'group_id': '$site_group', } ) parent = DynamicModelChoiceField( queryset=Location.objects.all(), required=False, query_params={ 'site_id': '$site' } ) slug = SlugField() class Meta: model = Location fields = ( 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', ) 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 LocationBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Location.objects.all(), widget=forms.MultipleHiddenInput ) site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False ) parent = DynamicModelChoiceField( queryset=Location.objects.all(), required=False, query_params={ 'site_id': '$site' } ) description = forms.CharField( max_length=200, required=False ) class Meta: nullable_fields = ['parent', 'description'] class LocationFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Location region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Site') ) parent_id = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), required=False, query_params={ 'region_id': '$region_id', 'site_id': '$site_id', }, label=_('Parent') ) # # Rack roles # class RackRoleForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() class Meta: model = RackRole fields = [ 'name', 'slug', 'color', '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. 00ff00)'), } class RackRoleBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RackRole.objects.all(), widget=forms.MultipleHiddenInput ) color = ColorField( required=False ) description = forms.CharField( max_length=200, required=False ) class Meta: nullable_fields = ['color', 'description'] # # Racks # class RackForm(BootstrapMixin, TenancyForm, 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(), query_params={ 'region_id': '$region', 'group_id': '$site_group', } ) location = DynamicModelChoiceField( queryset=Location.objects.all(), required=False, query_params={ 'site_id': '$site' } ) role = DynamicModelChoiceField( queryset=RackRole.objects.all(), required=False ) comments = CommentField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = Rack fields = [ 'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'tags', ] help_texts = { 'site': "The site at which the rack exists", 'name': "Organizational rack name", 'facility_id': "The unique rack ID assigned by the facility", 'u_height': "Height in rack units", } widgets = { 'status': StaticSelect2(), 'type': StaticSelect2(), 'width': StaticSelect2(), 'outer_unit': StaticSelect2(), } 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 RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput ) 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', } ) location = DynamicModelChoiceField( queryset=Location.objects.all(), required=False, query_params={ 'site_id': '$site' } ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), required=False ) status = forms.ChoiceField( choices=add_blank_choice(RackStatusChoices), required=False, initial='', widget=StaticSelect2() ) role = DynamicModelChoiceField( queryset=RackRole.objects.all(), required=False ) serial = forms.CharField( max_length=50, required=False, label='Serial Number' ) asset_tag = forms.CharField( max_length=50, required=False ) type = forms.ChoiceField( choices=add_blank_choice(RackTypeChoices), required=False, widget=StaticSelect2() ) width = forms.ChoiceField( choices=add_blank_choice(RackWidthChoices), required=False, widget=StaticSelect2() ) u_height = forms.IntegerField( required=False, label='Height (U)' ) desc_units = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect, label='Descending units' ) outer_width = forms.IntegerField( required=False, min_value=1 ) outer_depth = forms.IntegerField( required=False, min_value=1 ) outer_unit = forms.ChoiceField( choices=add_blank_choice(RackDimensionUnitChoices), required=False, widget=StaticSelect2() ) comments = CommentField( widget=SmallTextarea, label='Comments' ) class Meta: nullable_fields = [ 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments', ] class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Rack field_order = ['region_id', 'site_id', 'location_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id'] field_groups = [ ['status', 'role_id'], ['region_id', 'site_id', 'location_id'], ['tenant_group_id', 'tenant_id'], ] region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Site') ) location_id = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), required=False, null_option='None', query_params={ 'site_id': '$site_id' }, label=_('Location') ) status = forms.MultipleChoiceField( choices=RackStatusChoices, required=False, widget=StaticSelect2Multiple() ) type = forms.MultipleChoiceField( choices=RackTypeChoices, required=False, widget=StaticSelect2Multiple() ) width = forms.MultipleChoiceField( choices=RackWidthChoices, required=False, widget=StaticSelect2Multiple() ) role_id = DynamicModelMultipleChoiceField( queryset=RackRole.objects.all(), required=False, null_option='None', label=_('Role') ) asset_tag = forms.CharField( required=False ) tag = TagFilterField(model) # # Rack elevations # class RackElevationFilterForm(RackFilterForm): field_order = [ 'region_id', 'site_id', 'location_id', 'id', 'status', 'role_id', 'tenant_group_id', 'tenant_id', ] id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), label=_('Rack'), required=False, query_params={ 'site_id': '$site_id', 'location_id': '$location_id', } ) # # Rack reservations # class RackReservationForm(BootstrapMixin, TenancyForm, 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', } ) location = DynamicModelChoiceField( queryset=Location.objects.all(), required=False, query_params={ 'site_id': '$site' } ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), query_params={ 'site_id': '$site', 'location_id': '$location', } ) units = NumericArrayField( base_field=forms.IntegerField(), help_text="Comma-separated list of numeric unit IDs. A range may be specified using a hyphen." ) user = forms.ModelChoiceField( queryset=User.objects.order_by( 'username' ), widget=StaticSelect2() ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = RackReservation fields = [ 'region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'tags', ] fieldsets = ( ('Reservation', ('region', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')), ('Tenancy', ('tenant_group', 'tenant')), ) 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 RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput() ) user = forms.ModelChoiceField( queryset=User.objects.order_by( 'username' ), required=False, widget=StaticSelect2() ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), required=False ) description = forms.CharField( max_length=100, required=False ) class Meta: nullable_fields = [] class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = RackReservation field_order = ['region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id'] field_groups = [ ['region_id', 'site_id', 'location_id'], ['user_id', 'tenant_group_id', 'tenant_id'], ] region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Region') ) location_id = DynamicModelMultipleChoiceField( queryset=Location.objects.prefetch_related('site'), required=False, label=_('Location'), null_option='None' ) user_id = DynamicModelMultipleChoiceField( queryset=User.objects.all(), required=False, label=_('User'), widget=APISelectMultiple( api_url='/api/users/users/', ) ) tag = TagFilterField(model) # # Manufacturers # class ManufacturerForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() class Meta: model = Manufacturer fields = [ 'name', 'slug', 'description', ] class ManufacturerCSVForm(CustomFieldModelCSVForm): class Meta: model = Manufacturer fields = ('name', 'slug', 'description') class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Manufacturer.objects.all(), widget=forms.MultipleHiddenInput ) description = forms.CharField( max_length=200, required=False ) class Meta: nullable_fields = ['description'] # # Device types # class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm): manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all() ) slug = SlugField( slug_source='model' ) comments = CommentField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = DeviceType fields = [ 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', ] fieldsets = ( ('Device Type', ( 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'tags', )), ('Images', ('front_image', 'rear_image')), ) widgets = { 'subdevice_role': StaticSelect2(), # Exclude SVG images (unsupported by PIL) 'front_image': forms.ClearableFileInput(attrs={ 'accept': 'image/bmp,image/gif,image/jpeg,image/png,image/tiff' }), 'rear_image': forms.ClearableFileInput(attrs={ 'accept': 'image/bmp,image/gif,image/jpeg,image/png,image/tiff' }) } 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', ] class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput() ) manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), required=False ) u_height = forms.IntegerField( min_value=1, required=False ) is_full_depth = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect(), label='Is full depth' ) class Meta: nullable_fields = [] class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): model = DeviceType field_groups = [ ['manufacturer_id', 'subdevice_role'], ['console_ports', 'console_server_ports'], ['power_ports', 'power_outlets'], ['interfaces', 'pass_through_ports'], ['tag'] ] manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, label=_('Manufacturer') ) subdevice_role = forms.MultipleChoiceField( choices=add_blank_choice(SubdeviceRoleChoices), required=False, widget=StaticSelect2Multiple() ) console_ports = forms.NullBooleanField( required=False, label='Has console ports', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) console_server_ports = forms.NullBooleanField( required=False, label='Has console server ports', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) power_ports = forms.NullBooleanField( required=False, label='Has power ports', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) power_outlets = forms.NullBooleanField( required=False, label='Has power outlets', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) interfaces = forms.NullBooleanField( required=False, label='Has interfaces', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) pass_through_ports = forms.NullBooleanField( required=False, label='Has pass-through ports', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) tag = TagFilterField(model) # # Device 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 ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsolePortTemplate fields = [ 'device_type', 'name', 'label', 'type', 'description', ] widgets = { 'device_type': forms.HiddenInput(), } class ConsolePortTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() ) field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description') class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ConsolePortTemplate.objects.all(), widget=forms.MultipleHiddenInput() ) label = forms.CharField( max_length=64, required=False ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, widget=StaticSelect2() ) class Meta: nullable_fields = ('label', 'type', 'description') class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsoleServerPortTemplate fields = [ 'device_type', 'name', 'label', 'type', 'description', ] widgets = { 'device_type': forms.HiddenInput(), } class ConsoleServerPortTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() ) field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description') class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ConsoleServerPortTemplate.objects.all(), widget=forms.MultipleHiddenInput() ) label = forms.CharField( max_length=64, required=False ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, widget=StaticSelect2() ) description = forms.CharField( required=False ) class Meta: nullable_fields = ('label', 'type', 'description') class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerPortTemplate fields = [ 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', ] widgets = { 'device_type': forms.HiddenInput(), } 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 PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=PowerPortTemplate.objects.all(), widget=forms.MultipleHiddenInput() ) label = forms.CharField( max_length=64, required=False ) type = forms.ChoiceField( choices=add_blank_choice(PowerPortTypeChoices), required=False, widget=StaticSelect2() ) 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)" ) description = forms.CharField( required=False ) class Meta: nullable_fields = ('label', 'type', 'maximum_draw', 'allocated_draw', 'description') class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerOutletTemplate fields = [ 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', ] widgets = { 'device_type': forms.HiddenInput(), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Limit power_port choices to current DeviceType if hasattr(self.instance, 'device_type'): self.fields['power_port'].queryset = PowerPortTemplate.objects.filter( device_type=self.instance.device_type ) 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=StaticSelect2() ) 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 PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=PowerOutletTemplate.objects.all(), widget=forms.MultipleHiddenInput() ) device_type = forms.ModelChoiceField( queryset=DeviceType.objects.all(), required=False, disabled=True, widget=forms.HiddenInput() ) label = forms.CharField( max_length=64, required=False ) type = forms.ChoiceField( choices=add_blank_choice(PowerOutletTypeChoices), required=False, widget=StaticSelect2() ) power_port = forms.ModelChoiceField( queryset=PowerPortTemplate.objects.all(), required=False ) feed_leg = forms.ChoiceField( choices=add_blank_choice(PowerOutletFeedLegChoices), required=False, widget=StaticSelect2() ) description = forms.CharField( required=False ) class Meta: nullable_fields = ('label', 'type', 'power_port', 'feed_leg', 'description') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Limit power_port queryset to PowerPortTemplates which belong to the parent DeviceType if 'device_type' in self.initial: device_type = DeviceType.objects.filter(pk=self.initial['device_type']).first() self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(device_type=device_type) else: self.fields['power_port'].choices = () self.fields['power_port'].widget.attrs['disabled'] = True class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = InterfaceTemplate fields = [ 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', ] widgets = { 'device_type': forms.HiddenInput(), 'type': StaticSelect2(), } class InterfaceTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=InterfaceTypeChoices, widget=StaticSelect2() ) mgmt_only = forms.BooleanField( required=False, label='Management only' ) field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only', 'description') class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput() ) label = forms.CharField( max_length=64, required=False ) type = forms.ChoiceField( choices=add_blank_choice(InterfaceTypeChoices), required=False, widget=StaticSelect2() ) mgmt_only = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect, label='Management only' ) description = forms.CharField( required=False ) class Meta: nullable_fields = ('label', 'description') class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = FrontPortTemplate fields = [ 'device_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', ] widgets = { 'device_type': forms.HiddenInput(), 'rear_port': StaticSelect2(), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Limit rear_port choices to current DeviceType if hasattr(self.instance, 'device_type'): self.fields['rear_port'].queryset = RearPortTemplate.objects.filter( device_type=self.instance.device_type ) class FrontPortTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=PortTypeChoices, widget=StaticSelect2() ) 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 FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=FrontPortTemplate.objects.all(), widget=forms.MultipleHiddenInput() ) label = forms.CharField( max_length=64, required=False ) type = forms.ChoiceField( choices=add_blank_choice(PortTypeChoices), required=False, widget=StaticSelect2() ) color = ColorField( required=False ) description = forms.CharField( required=False ) class Meta: nullable_fields = ('description',) class RearPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = RearPortTemplate fields = [ 'device_type', 'name', 'label', 'type', 'color', 'positions', 'description', ] widgets = { 'device_type': forms.HiddenInput(), 'type': StaticSelect2(), } class RearPortTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=PortTypeChoices, widget=StaticSelect2(), ) 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 RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RearPortTemplate.objects.all(), widget=forms.MultipleHiddenInput() ) label = forms.CharField( max_length=64, required=False ) type = forms.ChoiceField( choices=add_blank_choice(PortTypeChoices), required=False, widget=StaticSelect2() ) color = ColorField( required=False ) description = forms.CharField( required=False ) class Meta: nullable_fields = ('description',) class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = DeviceBayTemplate fields = [ 'device_type', 'name', 'label', 'description', ] widgets = { 'device_type': forms.HiddenInput(), } class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm): field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description') class DeviceBayTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=DeviceBayTemplate.objects.all(), widget=forms.MultipleHiddenInput() ) label = forms.CharField( max_length=64, required=False ) description = forms.CharField( required=False ) class Meta: nullable_fields = ('label', 'description') # # 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', required=False ) 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', ] # # Device roles # class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() class Meta: model = DeviceRole fields = [ 'name', 'slug', 'color', 'vm_role', '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. 00ff00)'), } class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=DeviceRole.objects.all(), widget=forms.MultipleHiddenInput ) color = ColorField( required=False ) vm_role = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect, label='VM role' ) description = forms.CharField( max_length=200, required=False ) class Meta: nullable_fields = ['color', 'description'] # # Platforms # class PlatformForm(BootstrapMixin, CustomFieldModelForm): manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), required=False ) slug = SlugField( max_length=64 ) class Meta: model = Platform fields = [ 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', ] widgets = { 'napalm_args': SmallTextarea(), } 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 PlatformBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Platform.objects.all(), widget=forms.MultipleHiddenInput ) manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), required=False ) napalm_driver = forms.CharField( max_length=50, required=False ) # TODO: Bulk edit support for napalm_args description = forms.CharField( max_length=200, required=False ) class Meta: nullable_fields = ['manufacturer', 'napalm_driver', 'description'] # # Devices # class DeviceForm(BootstrapMixin, TenancyForm, 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(), query_params={ 'region_id': '$region', 'group_id': '$site_group', } ) location = DynamicModelChoiceField( queryset=Location.objects.all(), required=False, query_params={ 'site_id': '$site' }, initial_params={ 'racks': '$rack' } ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, query_params={ 'site_id': '$site', 'location_id': '$location', } ) position = forms.IntegerField( required=False, help_text="The lowest-numbered unit occupied by the device", widget=APISelect( api_url='/api/dcim/racks/{{rack}}/elevation/', attrs={ 'disabled-indicator': 'device', 'data-query-param-face': "[\"$face\"]", } ) ) 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' } ) device_role = DynamicModelChoiceField( queryset=DeviceRole.objects.all() ) platform = DynamicModelChoiceField( queryset=Platform.objects.all(), required=False, query_params={ 'manufacturer_id': ['$manufacturer', 'null'] } ) cluster_group = DynamicModelChoiceField( queryset=ClusterGroup.objects.all(), required=False, null_option='None', initial_params={ 'clusters': '$cluster' } ) cluster = DynamicModelChoiceField( queryset=Cluster.objects.all(), required=False, query_params={ 'group_id': '$cluster_group' } ) comments = CommentField() local_context_data = JSONField( required=False, label='' ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = Device fields = [ 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack', 'location', 'position', 'face', 'status', 'platform', 'primary_ip4', 'primary_ip6', 'cluster_group', 'cluster', 'tenant_group', 'tenant', 'comments', 'tags', 'local_context_data' ] help_texts = { 'device_role': "The function this device serves", 'serial': "Chassis serial number", 'local_context_data': "Local config context data overwrites all source contexts in the final rendered " "config context", } widgets = { 'face': StaticSelect2(), 'status': StaticSelect2(), 'primary_ip4': StaticSelect2(), 'primary_ip6': StaticSelect2(), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance.pk: # Compile list of choices for primary IPv4 and IPv6 addresses 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)) # 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 # 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) # Limit platform by manufacturer self.fields['platform'].queryset = Platform.objects.filter( Q(manufacturer__isnull=True) | Q(manufacturer=self.instance.device_type.manufacturer) ) # 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 # Rack position position = self.data.get('position') or self.initial.get('position') if position: self.fields['position'].widget.choices = [(position, f'U{position}')] 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' ) cluster = CSVModelChoiceField( queryset=Cluster.objects.all(), to_field_name='name', required=False, help_text='Virtualization cluster' ) class Meta: fields = [] model = Device 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', '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', '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 class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Device.objects.all(), widget=forms.MultipleHiddenInput() ) manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), required=False ) device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all(), required=False, query_params={ 'manufacturer_id': '$manufacturer' } ) device_role = DynamicModelChoiceField( queryset=DeviceRole.objects.all(), required=False ) site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False ) location = DynamicModelChoiceField( queryset=Location.objects.all(), required=False, query_params={ 'site_id': '$site' } ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), required=False ) platform = DynamicModelChoiceField( queryset=Platform.objects.all(), required=False ) status = forms.ChoiceField( choices=add_blank_choice(DeviceStatusChoices), required=False, widget=StaticSelect2() ) serial = forms.CharField( max_length=50, required=False, label='Serial Number' ) class Meta: nullable_fields = [ 'tenant', 'platform', 'serial', ] class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm): model = Device field_order = [ 'region_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id', 'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip', ] field_groups = [ ['region_id', 'site_id', 'location_id', 'rack_id'], ['status', 'role_id', 'asset_tag'], ['tenant_group_id', 'tenant_id'], ['manufacturer_id', 'device_type_id'], ['mac_address', 'has_primary_ip'], ] region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Site') ) location_id = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), required=False, null_option='None', query_params={ 'site_id': '$site_id' }, label=_('Location') ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), required=False, null_option='None', query_params={ 'site_id': '$site_id', 'location_id': '$location_id', }, label=_('Rack') ) role_id = DynamicModelMultipleChoiceField( queryset=DeviceRole.objects.all(), required=False, label=_('Role') ) manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, label=_('Manufacturer') ) device_type_id = DynamicModelMultipleChoiceField( queryset=DeviceType.objects.all(), required=False, query_params={ 'manufacturer_id': '$manufacturer_id' }, label=_('Model') ) platform_id = DynamicModelMultipleChoiceField( queryset=Platform.objects.all(), required=False, null_option='None', label=_('Platform') ) status = forms.MultipleChoiceField( choices=DeviceStatusChoices, required=False, widget=StaticSelect2Multiple() ) asset_tag = forms.CharField( required=False ) mac_address = forms.CharField( required=False, label='MAC address' ) has_primary_ip = forms.NullBooleanField( required=False, label='Has a primary IP', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) virtual_chassis_member = forms.NullBooleanField( required=False, label='Virtual chassis member', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) console_ports = forms.NullBooleanField( required=False, label='Has console ports', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) console_server_ports = forms.NullBooleanField( required=False, label='Has console server ports', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) power_ports = forms.NullBooleanField( required=False, label='Has power ports', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) power_outlets = forms.NullBooleanField( required=False, label='Has power outlets', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) interfaces = forms.NullBooleanField( required=False, label='Has interfaces', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) pass_through_ports = forms.NullBooleanField( required=False, label='Has pass-through ports', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) tag = TagFilterField(model) # # Device components # class ComponentCreateForm(BootstrapMixin, CustomFieldForm, 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 DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldForm, 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 ) # # Console ports # class ConsolePortFilterForm(DeviceComponentFilterForm): model = ConsolePort field_groups = [ ['name', 'label'], ['type', 'speed'], ['region_id', 'site_group_id', 'site_id'], ['tag'] ] type = forms.MultipleChoiceField( choices=ConsolePortTypeChoices, required=False, widget=StaticSelect2Multiple() ) speed = forms.MultipleChoiceField( choices=ConsolePortSpeedChoices, required=False, widget=StaticSelect2Multiple() ) tag = TagFilterField(model) class ConsolePortForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = ConsolePort fields = [ 'device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), } class ConsolePortCreateForm(ComponentCreateForm): model = ConsolePort type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, widget=StaticSelect2() ) speed = forms.ChoiceField( choices=add_blank_choice(ConsolePortSpeedChoices), required=False, widget=StaticSelect2() ) field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags') 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 ConsolePortBulkEditForm( form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']), BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=ConsolePort.objects.all(), widget=forms.MultipleHiddenInput() ) mark_connected = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect ) class Meta: nullable_fields = ['label', 'description'] 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') # # Console server ports # class ConsoleServerPortFilterForm(DeviceComponentFilterForm): model = ConsoleServerPort field_groups = [ ['name', 'label'], ['type', 'speed'], ['region_id', 'site_group_id', 'site_id'], ['tag'] ] type = forms.MultipleChoiceField( choices=ConsolePortTypeChoices, required=False, widget=StaticSelect2Multiple() ) speed = forms.MultipleChoiceField( choices=ConsolePortSpeedChoices, required=False, widget=StaticSelect2Multiple() ) tag = TagFilterField(model) class ConsoleServerPortForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = ConsoleServerPort fields = [ 'device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), } class ConsoleServerPortCreateForm(ComponentCreateForm): model = ConsoleServerPort type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, widget=StaticSelect2() ) speed = forms.ChoiceField( choices=add_blank_choice(ConsolePortSpeedChoices), required=False, widget=StaticSelect2() ) field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', '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 ConsoleServerPortBulkEditForm( form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']), BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput() ) mark_connected = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect ) class Meta: nullable_fields = ['label', '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') # # Power ports # class PowerPortFilterForm(DeviceComponentFilterForm): model = PowerPort field_groups = [ ['name', 'label', 'type'], ['region_id', 'site_group_id', 'site_id'], ['tag'], ] type = forms.MultipleChoiceField( choices=PowerPortTypeChoices, required=False, widget=StaticSelect2Multiple() ) tag = TagFilterField(model) class PowerPortForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = PowerPort fields = [ 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), } class PowerPortCreateForm(ComponentCreateForm): model = PowerPort type = forms.ChoiceField( choices=add_blank_choice(PowerPortTypeChoices), required=False, widget=StaticSelect2() ) 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 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 PowerPortBulkEditForm( form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']), BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=PowerPort.objects.all(), widget=forms.MultipleHiddenInput() ) mark_connected = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect ) class Meta: nullable_fields = ['label', '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', ) # # Power outlets # class PowerOutletFilterForm(DeviceComponentFilterForm): model = PowerOutlet field_groups = [ ['name', 'label', 'type'], ['region_id', 'site_group_id', 'site_id'], ['tag'], ] type = forms.MultipleChoiceField( choices=PowerOutletTypeChoices, required=False, widget=StaticSelect2Multiple() ) tag = TagFilterField(model) class PowerOutletForm(BootstrapMixin, CustomFieldModelForm): power_port = forms.ModelChoiceField( queryset=PowerPort.objects.all(), required=False ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = PowerOutlet fields = [ 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Limit power_port choices to the local device if hasattr(self.instance, 'device'): self.fields['power_port'].queryset = PowerPort.objects.filter( device=self.instance.device ) class PowerOutletCreateForm(ComponentCreateForm): model = PowerOutlet type = forms.ChoiceField( choices=add_blank_choice(PowerOutletTypeChoices), required=False, widget=StaticSelect2() ) 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 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 PowerOutletBulkEditForm( form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']), BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput() ) device = forms.ModelChoiceField( queryset=Device.objects.all(), required=False, disabled=True, widget=forms.HiddenInput() ) mark_connected = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect ) class Meta: nullable_fields = ['label', 'type', 'feed_leg', 'power_port', 'description'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Limit power_port queryset to PowerPorts which belong to the parent Device if 'device' in self.initial: device = Device.objects.filter(pk=self.initial['device']).first() self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) else: self.fields['power_port'].choices = () self.fields['power_port'].widget.attrs['disabled'] = True 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() # # Interfaces # class InterfaceFilterForm(DeviceComponentFilterForm): model = Interface field_groups = [ ['name', 'label', 'type', 'enabled'], ['mgmt_only', 'mac_address'], ['region_id', 'site_group_id', 'site_id'], ['tag'], ] type = forms.MultipleChoiceField( choices=InterfaceTypeChoices, required=False, widget=StaticSelect2Multiple() ) enabled = forms.NullBooleanField( required=False, widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) mgmt_only = forms.NullBooleanField( required=False, widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) mac_address = forms.CharField( required=False, label='MAC address' ) tag = TagFilterField(model) class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): parent = DynamicModelChoiceField( queryset=Interface.objects.all(), required=False, label='Parent interface' ) lag = DynamicModelChoiceField( queryset=Interface.objects.all(), required=False, label='LAG interface', query_params={ 'type': 'lag', } ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, label='Untagged VLAN' ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, label='Tagged VLANs' ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = Interface fields = [ 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', ] widgets = { 'device': forms.HiddenInput(), 'type': StaticSelect2(), 'mode': StaticSelect2(), } labels = { 'mode': '802.1Q Mode', } help_texts = { 'mode': INTERFACE_MODE_HELP_TEXT, } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device # Restrict parent/LAG interface assignment by device/VC self.fields['parent'].widget.add_query_param('device_id', device.pk) if device.virtual_chassis and device.virtual_chassis.master: # Get available LAG interfaces by VirtualChassis master self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk) else: self.fields['lag'].widget.add_query_param('device_id', device.pk) # Limit VLAN choices by device self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk) self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk) class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): model = Interface type = forms.ChoiceField( choices=InterfaceTypeChoices, widget=StaticSelect2(), ) 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', } ) mtu = forms.IntegerField( required=False, min_value=INTERFACE_MTU_MIN, max_value=INTERFACE_MTU_MAX, label='MTU' ) 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=StaticSelect2(), ) 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 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 InterfaceBulkEditForm( form_from_model(Interface, [ 'label', 'type', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode', ]), BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput() ) device = forms.ModelChoiceField( queryset=Device.objects.all(), required=False, disabled=True, widget=forms.HiddenInput() ) enabled = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect ) parent = DynamicModelChoiceField( queryset=Interface.objects.all(), required=False ) lag = DynamicModelChoiceField( queryset=Interface.objects.all(), required=False, query_params={ 'type': 'lag', } ) mgmt_only = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect, label='Management only' ) mark_connected = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False ) class Meta: nullable_fields = [ 'label', 'parent', 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans' ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if 'device' in self.initial: device = Device.objects.filter(pk=self.initial['device']).first() # Restrict parent/LAG interface assignment by device self.fields['parent'].widget.add_query_param('device_id', device.pk) self.fields['lag'].widget.add_query_param('device_id', device.pk) # Limit VLAN choices by device self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk) self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk) else: # See #4523 if 'pk' in self.initial: site = None interfaces = Interface.objects.filter(pk__in=self.initial['pk']).prefetch_related('device__site') # Check interface sites. First interface should set site, further interfaces will either continue the # loop or reset back to no site and break the loop. for interface in interfaces: if site is None: site = interface.device.site elif interface.device.site is not site: site = None break if site is not None: self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk) self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk) self.fields['parent'].choices = () self.fields['parent'].widget.attrs['disabled'] = True self.fields['lag'].choices = () self.fields['lag'].widget.attrs['disabled'] = True def clean(self): super().clean() # Untagged interfaces cannot be assigned tagged VLANs if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['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'] = [] 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 ) elif device: self.fields['lag'].queryset = Interface.objects.filter( device=device, type=InterfaceTypeChoices.TYPE_LAG ) else: self.fields['lag'].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'] # # Front pass-through ports # class FrontPortFilterForm(DeviceComponentFilterForm): field_groups = [ ['name', 'label', 'type', 'color'], ['region_id', 'site_group_id', 'site_id'], ['tag'] ] model = FrontPort type = forms.MultipleChoiceField( choices=PortTypeChoices, required=False, widget=StaticSelect2Multiple() ) color = ColorField( required=False ) tag = TagFilterField(model) class FrontPortForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = FrontPort fields = [ 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), 'type': StaticSelect2(), 'rear_port': StaticSelect2(), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Limit RearPort choices to the local device if hasattr(self.instance, 'device'): self.fields['rear_port'].queryset = self.fields['rear_port'].queryset.filter( device=self.instance.device ) # TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic class FrontPortCreateForm(ComponentCreateForm): model = FrontPort type = forms.ChoiceField( choices=PortTypeChoices, widget=StaticSelect2(), ) 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 FrontPortBulkCreateForm( # form_from_model(FrontPort, ['label', 'type', 'description', 'tags']), # DeviceBulkAddComponentForm # ): # pass class FrontPortBulkEditForm( form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']), BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=FrontPort.objects.all(), widget=forms.MultipleHiddenInput() ) class Meta: nullable_fields = ['label', 'description'] 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() # # Rear pass-through ports # class RearPortFilterForm(DeviceComponentFilterForm): model = RearPort field_groups = [ ['name', 'label', 'type', 'color'], ['region_id', 'site_group_id', 'site_id'], ['tag'] ] type = forms.MultipleChoiceField( choices=PortTypeChoices, required=False, widget=StaticSelect2Multiple() ) color = ColorField( required=False ) tag = TagFilterField(model) class RearPortForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = RearPort fields = [ 'device', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), 'type': StaticSelect2(), } class RearPortCreateForm(ComponentCreateForm): model = RearPort type = forms.ChoiceField( choices=PortTypeChoices, widget=StaticSelect2(), ) 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 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 RearPortBulkEditForm( form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']), BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=RearPort.objects.all(), widget=forms.MultipleHiddenInput() ) class Meta: nullable_fields = ['label', 'description'] 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' } # # Device bays # class DeviceBayFilterForm(DeviceComponentFilterForm): model = DeviceBay field_groups = [ ['name', 'label'], ['region_id', 'site_group_id', 'site_id'], ['tag'] ] tag = TagFilterField(model) class DeviceBayForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = DeviceBay fields = [ 'device', 'name', 'label', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), } class DeviceBayCreateForm(ComponentCreateForm): model = DeviceBay field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags') class PopulateDeviceBayForm(BootstrapMixin, 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/rack of the parent device.", widget=StaticSelect2(), ) 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 DeviceBayBulkCreateForm(DeviceBulkAddComponentForm): model = DeviceBay field_order = ('name_pattern', 'label_pattern', 'description', 'tags') class DeviceBayBulkEditForm( form_from_model(DeviceBay, ['label', 'description']), BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=DeviceBay.objects.all(), widget=forms.MultipleHiddenInput() ) class Meta: nullable_fields = ['label', 'description'] 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() # # Inventory items # class InventoryItemForm(BootstrapMixin, CustomFieldModelForm): device = DynamicModelChoiceField( queryset=Device.objects.all() ) parent = DynamicModelChoiceField( queryset=InventoryItem.objects.all(), required=False, query_params={ 'device_id': '$device' } ) manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), required=False ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = InventoryItem fields = [ 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', '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', ) 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 ) class Meta: model = InventoryItem fields = ( 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', ) 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', ) class InventoryItemBulkEditForm( form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']), BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=InventoryItem.objects.all(), widget=forms.MultipleHiddenInput() ) manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), required=False ) class Meta: nullable_fields = ['label', 'manufacturer', 'part_id', 'description'] class InventoryItemFilterForm(DeviceComponentFilterForm): model = InventoryItem field_groups = [ ['name', 'label', 'manufacturer_id'], ['serial', 'asset_tag', 'discovered'], ['region_id', 'site_group_id', 'site_id'], ['tag'] ] manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, label=_('Manufacturer') ) serial = forms.CharField( required=False ) asset_tag = forms.CharField( required=False ) discovered = forms.NullBooleanField( required=False, widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) tag = TagFilterField(model) # # Cables # 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', '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': StaticSelect2, 'type': StaticSelect2, 'length_unit': StaticSelect2, } 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) class CableForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = Cable fields = [ 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', ] widgets = { 'status': StaticSelect2, 'type': StaticSelect2, 'length_unit': StaticSelect2, } error_messages = { 'length': { 'max_value': 'Maximum length is 32767 (any unit)' } } 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. 00ff00)'), } 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 CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Cable.objects.all(), widget=forms.MultipleHiddenInput ) type = forms.ChoiceField( choices=add_blank_choice(CableTypeChoices), required=False, initial='', widget=StaticSelect2() ) status = forms.ChoiceField( choices=add_blank_choice(CableStatusChoices), required=False, widget=StaticSelect2(), initial='' ) label = forms.CharField( max_length=100, required=False ) color = ColorField( required=False ) length = forms.IntegerField( min_value=1, required=False ) length_unit = forms.ChoiceField( choices=add_blank_choice(CableLengthUnitChoices), required=False, initial='', widget=StaticSelect2() ) class Meta: nullable_fields = [ 'type', 'status', 'label', 'color', 'length', ] def clean(self): super().clean() # Validate length/unit length = self.cleaned_data.get('length') length_unit = self.cleaned_data.get('length_unit') if length and not length_unit: raise forms.ValidationError({ 'length_unit': "Must specify a unit when setting length" }) class CableFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Cable field_groups = [ ['type', 'status', 'color'], ['device_id', 'rack_id'], ['region_id', 'site_id', 'tenant_id'], ['tag'] ] region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Site') ) tenant_id = DynamicModelMultipleChoiceField( queryset=Tenant.objects.all(), required=False, label=_('Tenant') ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), required=False, label=_('Rack'), null_option='None', query_params={ 'site_id': '$site_id' } ) type = forms.MultipleChoiceField( choices=add_blank_choice(CableTypeChoices), required=False, widget=StaticSelect2() ) status = forms.ChoiceField( required=False, choices=add_blank_choice(CableStatusChoices), widget=StaticSelect2() ) color = ColorField( required=False ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, query_params={ 'site_id': '$site_id', 'tenant_id': '$tenant_id', 'rack_id': '$rack_id', }, label=_('Device') ) tag = TagFilterField(model) # # Connections # class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Site') ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, query_params={ 'site_id': '$site_id' }, label=_('Device') ) class PowerConnectionFilterForm(BootstrapMixin, forms.Form): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Site') ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, query_params={ 'site_id': '$site_id' }, label=_('Device') ) class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Site') ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, query_params={ 'site_id': '$site_id' }, label=_('Device') ) # # Virtual chassis # class DeviceSelectionForm(forms.Form): pk = forms.ModelMultipleChoiceField( queryset=Device.objects.all(), widget=forms.MultipleHiddenInput() ) 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 class VirtualChassisForm(BootstrapMixin, CustomFieldModelForm): master = forms.ModelChoiceField( queryset=Device.objects.all(), required=False, ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = VirtualChassis fields = [ 'name', 'domain', 'master', '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 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 = 'A virtual chassis member already exists in position {}.'.format(vc_position) form.add_error('vc_position', error_msg) vc_position_list.append(vc_position) 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(BootstrapMixin, forms.Form): 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' } ) device = DynamicModelChoiceField( queryset=Device.objects.all(), query_params={ 'site_id': '$site', 'rack_id': '$rack', 'virtual_chassis_id': 'null', } ) 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 class VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=VirtualChassis.objects.all(), widget=forms.MultipleHiddenInput() ) domain = forms.CharField( max_length=30, required=False ) class Meta: nullable_fields = ['domain'] 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 VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VirtualChassis field_order = ['region_id', 'site_group_id', 'site_id', 'tenant_group_id', 'tenant_id'] field_groups = [ ['region_id', 'site_group_id', 'site_id'], ['tenant_group_id', 'tenant_id'], ['tag'] ] region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Site') ) tag = TagFilterField(model) # # Power panels # class PowerPanelForm(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(), query_params={ 'region_id': '$region', 'group_id': '$site_group', } ) location = DynamicModelChoiceField( queryset=Location.objects.all(), required=False, query_params={ 'site_id': '$site' } ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = PowerPanel fields = [ 'region', 'site_group', 'site', 'location', 'name', 'tags', ] fieldsets = ( ('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'tags')), ) 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 PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=PowerPanel.objects.all(), widget=forms.MultipleHiddenInput ) 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', } ) location = DynamicModelChoiceField( queryset=Location.objects.all(), required=False, query_params={ 'site_id': '$site' } ) class Meta: nullable_fields = ['location'] class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): model = PowerPanel region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Site') ) location_id = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), required=False, null_option='None', query_params={ 'site_id': '$site_id' }, label=_('Location') ) tag = TagFilterField(model) # # Power feeds # class PowerFeedForm(BootstrapMixin, CustomFieldModelForm): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, initial_params={ 'sites__powerpanel': '$power_panel' } ) site_group = DynamicModelChoiceField( queryset=SiteGroup.objects.all(), required=False, initial_params={ 'sites': '$site' } ) site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, initial_params={ 'powerpanel': '$power_panel' }, query_params={ 'region_id': '$region', 'group_id': '$site_group', } ) power_panel = DynamicModelChoiceField( queryset=PowerPanel.objects.all(), query_params={ 'site_id': '$site' } ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, query_params={ 'site_id': '$site' } ) comments = CommentField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = PowerFeed fields = [ 'region', 'site_group', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'comments', 'tags', ] fieldsets = ( ('Power Panel', ('region', 'site', 'power_panel')), ('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')), ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')), ) widgets = { 'status': StaticSelect2(), 'type': StaticSelect2(), 'supply': StaticSelect2(), 'phase': StaticSelect2(), } 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) class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=PowerFeed.objects.all(), widget=forms.MultipleHiddenInput ) power_panel = DynamicModelChoiceField( queryset=PowerPanel.objects.all(), required=False ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, ) status = forms.ChoiceField( choices=add_blank_choice(PowerFeedStatusChoices), required=False, initial='', widget=StaticSelect2() ) type = forms.ChoiceField( choices=add_blank_choice(PowerFeedTypeChoices), required=False, initial='', widget=StaticSelect2() ) supply = forms.ChoiceField( choices=add_blank_choice(PowerFeedSupplyChoices), required=False, initial='', widget=StaticSelect2() ) phase = forms.ChoiceField( choices=add_blank_choice(PowerFeedPhaseChoices), required=False, initial='', widget=StaticSelect2() ) voltage = forms.IntegerField( required=False ) amperage = forms.IntegerField( required=False ) max_utilization = forms.IntegerField( required=False ) mark_connected = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect ) comments = CommentField( widget=SmallTextarea, label='Comments' ) class Meta: nullable_fields = [ 'location', 'comments', ] class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): model = PowerFeed field_groups = [ ['region_id', 'site_group_id', 'site_id'], ['power_panel_id', 'rack_id'], ['type', 'supply', 'max_utilization'], ['phase', 'voltage', 'amperage'], ['status', 'tag'] ] region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ 'region_id': '$region_id' }, label=_('Site') ) power_panel_id = DynamicModelMultipleChoiceField( queryset=PowerPanel.objects.all(), required=False, null_option='None', query_params={ 'site_id': '$site_id' }, label=_('Power panel') ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), required=False, null_option='None', query_params={ 'site_id': '$site_id' }, label=_('Rack') ) status = forms.MultipleChoiceField( choices=PowerFeedStatusChoices, required=False, widget=StaticSelect2Multiple() ) type = forms.ChoiceField( choices=add_blank_choice(PowerFeedTypeChoices), required=False, widget=StaticSelect2() ) supply = forms.ChoiceField( choices=add_blank_choice(PowerFeedSupplyChoices), required=False, widget=StaticSelect2() ) phase = forms.ChoiceField( choices=add_blank_choice(PowerFeedPhaseChoices), required=False, widget=StaticSelect2() ) voltage = forms.IntegerField( required=False ) amperage = forms.IntegerField( required=False ) max_utilization = forms.IntegerField( required=False ) tag = TagFilterField(model)