diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py deleted file mode 100644 index 233d45220..000000000 --- a/netbox/dcim/forms.py +++ /dev/null @@ -1,5533 +0,0 @@ -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, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelFilterForm, CustomFieldModelForm, - CustomFieldsMixin, LocalConfigContextFilterForm, -) -from extras.models import Tag -from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN -from ipam.models import IPAddress, VLAN, VLANGroup -from tenancy.forms import TenancyFilterForm, TenancyForm -from tenancy.models import Tenant -from utilities.forms import ( - APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, - ClearableFileInput, ColorField, CommentField, CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, - CSVTypedChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, - JSONField, NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect, StaticSelectMultiple, - 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, CustomFieldModelFilterForm): - field_order = [ - 'q', 'name', 'label', 'region_id', 'site_group_id', 'site_id', - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - name = forms.CharField( - required=False - ) - label = forms.CharField( - required=False - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_group_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site group'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id', - 'group_id': '$site_group_id', - }, - label=_('Site'), - fetch_trigger='open' - ) - location_id = DynamicModelMultipleChoiceField( - queryset=Location.objects.all(), - required=False, - query_params={ - 'site_id': '$site_id', - }, - label=_('Location'), - fetch_trigger='open' - ) - device_id = DynamicModelMultipleChoiceField( - queryset=Device.objects.all(), - required=False, - query_params={ - 'site_id': '$site_id', - 'location_id': '$location_id', - }, - label=_('Device'), - fetch_trigger='open' - ) - - -class InterfaceCommonForm(forms.Form): - mac_address = forms.CharField( - empty_value=None, - required=False, - label='MAC address' - ) - mtu = forms.IntegerField( - required=False, - min_value=INTERFACE_MTU_MIN, - max_value=INTERFACE_MTU_MAX, - label='MTU' - ) - - def clean(self): - super().clean() - - parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine' - tagged_vlans = self.cleaned_data.get('tagged_vlans') - - # Untagged interfaces cannot be assigned tagged VLANs - if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans: - raise forms.ValidationError({ - 'mode': "An access interface cannot have tagged VLANs assigned." - }) - - # Remove all tagged VLAN assignments from "tagged all" interfaces - elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL: - self.cleaned_data['tagged_vlans'] = [] - - # Validate tagged VLANs; must be a global VLAN or in the same site - elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans: - valid_sites = [None, self.cleaned_data[parent_field].site] - invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites] - - if invalid_vlans: - raise forms.ValidationError({ - 'tagged_vlans': f"The tagged VLANs ({', '.join(invalid_vlans)}) must belong to the same site as " - f"the interface's parent device/VM, or they must be global" - }) - - -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, CustomFieldModelBulkEditForm): - 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, CustomFieldModelFilterForm): - model = Region - field_groups = [ - ['q'], - ['parent_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - parent_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Parent region'), - fetch_trigger='open' - ) - - -# -# 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, CustomFieldModelBulkEditForm): - 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, CustomFieldModelFilterForm): - model = SiteGroup - field_groups = [ - ['q'], - ['parent_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - parent_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Parent group'), - fetch_trigger='open' - ) - - -# -# 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=StaticSelect() - ) - 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': StaticSelect(), - 'time_zone': StaticSelect(), - } - 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, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Site.objects.all(), - widget=forms.MultipleHiddenInput - ) - status = forms.ChoiceField( - choices=add_blank_choice(SiteStatusChoices), - required=False, - initial='', - widget=StaticSelect() - ) - 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=StaticSelect() - ) - - class Meta: - nullable_fields = [ - 'region', 'group', 'tenant', 'asn', 'description', 'time_zone', - ] - - -class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = Site - field_order = ['q', 'status', 'region_id', 'tenant_group_id', 'tenant_id'] - field_groups = [ - ['q', 'tag'], - ['status', 'region_id', 'group_id'], - ['tenant_group_id', 'tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - status = forms.MultipleChoiceField( - choices=SiteStatusChoices, - required=False, - widget=StaticSelectMultiple(), - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - group_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site group'), - fetch_trigger='open' - ) - 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, CustomFieldModelBulkEditForm): - 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, CustomFieldModelFilterForm): - model = Location - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_group_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site group'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id', - 'group_id': '$site_group_id', - }, - label=_('Site'), - fetch_trigger='open' - ) - parent_id = DynamicModelMultipleChoiceField( - queryset=Location.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id', - 'site_id': '$site_id', - }, - label=_('Parent'), - fetch_trigger='open' - ) - - -# -# 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, CustomFieldModelBulkEditForm): - 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'] - - -class RackRoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = RackRole - field_groups = [ - ['q'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - - -# -# 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': StaticSelect(), - 'type': StaticSelect(), - 'width': StaticSelect(), - 'outer_unit': StaticSelect(), - } - - -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, CustomFieldModelBulkEditForm): - 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=StaticSelect() - ) - 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=StaticSelect() - ) - width = forms.ChoiceField( - choices=add_blank_choice(RackWidthChoices), - required=False, - widget=StaticSelect() - ) - 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=StaticSelect() - ) - 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, CustomFieldModelFilterForm): - model = Rack - field_order = ['q', 'region_id', 'site_id', 'location_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id'] - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_id', 'location_id'], - ['status', 'role_id'], - ['type', 'width', 'serial', 'asset_tag'], - ['tenant_group_id', 'tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id' - }, - label=_('Site'), - fetch_trigger='open' - ) - location_id = DynamicModelMultipleChoiceField( - queryset=Location.objects.all(), - required=False, - null_option='None', - query_params={ - 'site_id': '$site_id' - }, - label=_('Location'), - fetch_trigger='open' - ) - status = forms.MultipleChoiceField( - choices=RackStatusChoices, - required=False, - widget=StaticSelectMultiple() - ) - type = forms.MultipleChoiceField( - choices=RackTypeChoices, - required=False, - widget=StaticSelectMultiple() - ) - width = forms.MultipleChoiceField( - choices=RackWidthChoices, - required=False, - widget=StaticSelectMultiple() - ) - role_id = DynamicModelMultipleChoiceField( - queryset=RackRole.objects.all(), - required=False, - null_option='None', - label=_('Role'), - fetch_trigger='open' - ) - serial = forms.CharField( - required=False - ) - asset_tag = forms.CharField( - required=False - ) - tag = TagFilterField(model) - - -# -# Rack elevations -# - -class RackElevationFilterForm(RackFilterForm): - field_order = [ - 'q', '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', - }, - fetch_trigger='open' - ) - - -# -# Rack reservations -# - -class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - region = DynamicModelChoiceField( - queryset=Region.objects.all(), - required=False, - initial_params={ - 'sites': '$site' - }, - fetch_trigger='open' - ) - site_group = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - initial_params={ - 'sites': '$site' - }, - fetch_trigger='open' - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region', - 'group_id': '$site_group', - }, - fetch_trigger='open' - ) - location = DynamicModelChoiceField( - queryset=Location.objects.all(), - required=False, - query_params={ - 'site_id': '$site' - }, - fetch_trigger='open' - ) - rack = DynamicModelChoiceField( - queryset=Rack.objects.all(), - query_params={ - 'site_id': '$site', - 'location_id': '$location', - }, - fetch_trigger='open' - ) - 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=StaticSelect() - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False, - fetch_trigger='open' - ) - - 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, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=RackReservation.objects.all(), - widget=forms.MultipleHiddenInput() - ) - user = forms.ModelChoiceField( - queryset=User.objects.order_by( - 'username' - ), - required=False, - widget=StaticSelect() - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - description = forms.CharField( - max_length=100, - required=False - ) - - class Meta: - nullable_fields = [] - - -class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = RackReservation - field_order = ['q', 'region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id'] - field_groups = [ - ['q', 'tag'], - ['user_id'], - ['region_id', 'site_id', 'location_id'], - ['tenant_group_id', 'tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id' - }, - label=_('Site'), - fetch_trigger='open' - ) - location_id = DynamicModelMultipleChoiceField( - queryset=Location.objects.prefetch_related('site'), - required=False, - label=_('Location'), - null_option='None', - fetch_trigger='open' - ) - user_id = DynamicModelMultipleChoiceField( - queryset=User.objects.all(), - required=False, - label=_('User'), - widget=APISelectMultiple( - api_url='/api/users/users/', - ), - fetch_trigger='open' - ) - 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, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Manufacturer.objects.all(), - widget=forms.MultipleHiddenInput - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['description'] - - -class ManufacturerFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = Manufacturer - field_groups = [ - ['q'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - - -# -# 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': StaticSelect(), - 'front_image': ClearableFileInput(attrs={ - 'accept': DEVICETYPE_IMAGE_FORMATS - }), - 'rear_image': ClearableFileInput(attrs={ - 'accept': DEVICETYPE_IMAGE_FORMATS - }) - } - - -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, CustomFieldModelBulkEditForm): - 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, CustomFieldModelFilterForm): - model = DeviceType - field_groups = [ - ['q', 'tag'], - ['manufacturer_id', 'subdevice_role'], - ['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - manufacturer_id = DynamicModelMultipleChoiceField( - queryset=Manufacturer.objects.all(), - required=False, - label=_('Manufacturer'), - fetch_trigger='open' - ) - subdevice_role = forms.MultipleChoiceField( - choices=add_blank_choice(SubdeviceRoleChoices), - required=False, - widget=StaticSelectMultiple() - ) - console_ports = forms.NullBooleanField( - required=False, - label='Has console ports', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - console_server_ports = forms.NullBooleanField( - required=False, - label='Has console server ports', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - power_ports = forms.NullBooleanField( - required=False, - label='Has power ports', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - power_outlets = forms.NullBooleanField( - required=False, - label='Has power outlets', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - interfaces = forms.NullBooleanField( - required=False, - label='Has interfaces', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - pass_through_ports = forms.NullBooleanField( - required=False, - label='Has pass-through ports', - widget=StaticSelect( - 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=StaticSelect() - ) - 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=StaticSelect() - ) - - 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=StaticSelect() - ) - 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=StaticSelect() - ) - 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=StaticSelect() - ) - 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=StaticSelect() - ) - field_order = ( - 'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg', - 'description', - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Limit power_port choices to current DeviceType - device_type = DeviceType.objects.get( - pk=self.initial.get('device_type') or self.data.get('device_type') - ) - self.fields['power_port'].queryset = PowerPortTemplate.objects.filter( - device_type=device_type - ) - - -class 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=StaticSelect() - ) - power_port = forms.ModelChoiceField( - queryset=PowerPortTemplate.objects.all(), - required=False - ) - feed_leg = forms.ChoiceField( - choices=add_blank_choice(PowerOutletFeedLegChoices), - required=False, - widget=StaticSelect() - ) - 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': StaticSelect(), - } - - -class InterfaceTemplateCreateForm(ComponentTemplateCreateForm): - type = forms.ChoiceField( - choices=InterfaceTypeChoices, - widget=StaticSelect() - ) - mgmt_only = forms.BooleanField( - required=False, - label='Management only' - ) - field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only', 'description') - - -class 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=StaticSelect() - ) - 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': StaticSelect(), - } - - 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=StaticSelect() - ) - color = ColorField( - required=False - ) - rear_port_set = forms.MultipleChoiceField( - choices=[], - label='Rear ports', - help_text='Select one rear port assignment for each front port being created.', - ) - field_order = ( - 'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'description', - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - device_type = DeviceType.objects.get( - pk=self.initial.get('device_type') or self.data.get('device_type') - ) - - # Determine which rear port positions are occupied. These will be excluded from the list of available mappings. - occupied_port_positions = [ - (front_port.rear_port_id, front_port.rear_port_position) - for front_port in device_type.frontporttemplates.all() - ] - - # Populate rear port choices - choices = [] - rear_ports = RearPortTemplate.objects.filter(device_type=device_type) - for rear_port in rear_ports: - for i in range(1, rear_port.positions + 1): - if (rear_port.pk, i) not in occupied_port_positions: - choices.append( - ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i)) - ) - self.fields['rear_port_set'].choices = choices - - def clean(self): - super().clean() - - # Validate that the number of ports being created equals the number of selected (rear port, position) tuples - front_port_count = len(self.cleaned_data['name_pattern']) - rear_port_count = len(self.cleaned_data['rear_port_set']) - if front_port_count != rear_port_count: - raise forms.ValidationError({ - 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments ' - 'were selected. These counts must match.'.format(front_port_count, rear_port_count) - }) - - def get_iterative_data(self, iteration): - - # Assign rear port and position from selected set - rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':') - - return { - 'rear_port': int(rear_port), - 'rear_port_position': int(position), - } - - -class 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=StaticSelect() - ) - 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': StaticSelect(), - } - - -class RearPortTemplateCreateForm(ComponentTemplateCreateForm): - type = forms.ChoiceField( - choices=PortTypeChoices, - widget=StaticSelect(), - ) - color = ColorField( - required=False - ) - positions = forms.IntegerField( - min_value=REARPORT_POSITIONS_MIN, - max_value=REARPORT_POSITIONS_MAX, - initial=1, - help_text='The number of front ports which may be mapped to each rear port' - ) - field_order = ( - 'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'description', - ) - - -class 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=StaticSelect() - ) - 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' - ) - - 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, CustomFieldModelBulkEditForm): - 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'] - - -class DeviceRoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = DeviceRole - field_groups = [ - ['q'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - - -# -# 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, CustomFieldModelBulkEditForm): - 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'] - - -class PlatformFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = Platform - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - manufacturer_id = DynamicModelMultipleChoiceField( - queryset=Manufacturer.objects.all(), - required=False, - label=_('Manufacturer'), - fetch_trigger='open' - ) - - -# -# 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': StaticSelect(), - 'status': StaticSelect(), - 'primary_ip4': StaticSelect(), - 'primary_ip6': StaticSelect(), - } - - 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' - ) - virtual_chassis = CSVModelChoiceField( - queryset=VirtualChassis.objects.all(), - to_field_name='name', - required=False, - help_text='Virtual chassis' - ) - cluster = CSVModelChoiceField( - queryset=Cluster.objects.all(), - to_field_name='name', - required=False, - help_text='Virtualization cluster' - ) - - class Meta: - fields = [] - model = Device - help_texts = { - 'vc_position': 'Virtual chassis position', - 'vc_priority': 'Virtual chassis priority', - } - - def __init__(self, data=None, *args, **kwargs): - super().__init__(data, *args, **kwargs) - - if data: - - # Limit device type queryset by manufacturer - params = {f"manufacturer__{self.fields['manufacturer'].to_field_name}": data.get('manufacturer')} - self.fields['device_type'].queryset = self.fields['device_type'].queryset.filter(**params) - - -class DeviceCSVForm(BaseDeviceCSVForm): - site = CSVModelChoiceField( - queryset=Site.objects.all(), - to_field_name='name', - help_text='Assigned site' - ) - location = CSVModelChoiceField( - queryset=Location.objects.all(), - to_field_name='name', - required=False, - help_text="Assigned location (if any)" - ) - rack = CSVModelChoiceField( - queryset=Rack.objects.all(), - to_field_name='name', - required=False, - help_text="Assigned rack (if any)" - ) - face = CSVChoiceField( - choices=DeviceFaceChoices, - required=False, - help_text='Mounted rack face' - ) - - class Meta(BaseDeviceCSVForm.Meta): - fields = [ - 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', - 'site', 'location', 'rack', 'position', 'face', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', - 'comments', - ] - - def __init__(self, data=None, *args, **kwargs): - super().__init__(data, *args, **kwargs) - - if data: - - # Limit location queryset by assigned site - params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} - self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) - - # Limit rack queryset by assigned site and group - params = { - f"site__{self.fields['site'].to_field_name}": data.get('site'), - f"location__{self.fields['location'].to_field_name}": data.get('location'), - } - self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) - - -class ChildDeviceCSVForm(BaseDeviceCSVForm): - parent = CSVModelChoiceField( - queryset=Device.objects.all(), - to_field_name='name', - help_text='Parent device' - ) - device_bay = CSVModelChoiceField( - queryset=DeviceBay.objects.all(), - to_field_name='name', - help_text='Device bay in which this device is installed' - ) - - class Meta(BaseDeviceCSVForm.Meta): - fields = [ - 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', - 'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments', - ] - - def __init__(self, data=None, *args, **kwargs): - super().__init__(data, *args, **kwargs) - - if data: - - # Limit device bay queryset by parent device - params = {f"device__{self.fields['parent'].to_field_name}": data.get('parent')} - self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params) - - def clean(self): - super().clean() - - # Set parent_bay reverse relationship - device_bay = self.cleaned_data.get('device_bay') - if device_bay: - self.instance.parent_bay = device_bay - - # Inherit site and rack from parent device - parent = self.cleaned_data.get('parent') - if parent: - self.instance.site = parent.site - self.instance.rack = parent.rack - - -class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - 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=StaticSelect() - ) - serial = forms.CharField( - max_length=50, - required=False, - label='Serial Number' - ) - - class Meta: - nullable_fields = [ - 'tenant', 'platform', 'serial', - ] - - -class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm): - model = Device - field_order = [ - 'q', 'region_id', 'site_group_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 = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id'], - ['status', 'role_id', 'serial', 'asset_tag', 'mac_address'], - ['manufacturer_id', 'device_type_id', 'platform_id'], - ['tenant_group_id', 'tenant_id'], - [ - 'has_primary_ip', 'virtual_chassis_member', 'console_ports', 'console_server_ports', 'power_ports', - 'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data', - ], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_group_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site group'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id', - 'group_id': '$site_group_id', - }, - label=_('Site'), - fetch_trigger='open' - ) - location_id = DynamicModelMultipleChoiceField( - queryset=Location.objects.all(), - required=False, - null_option='None', - query_params={ - 'site_id': '$site_id' - }, - label=_('Location'), - fetch_trigger='open' - ) - rack_id = DynamicModelMultipleChoiceField( - queryset=Rack.objects.all(), - required=False, - null_option='None', - query_params={ - 'site_id': '$site_id', - 'location_id': '$location_id', - }, - label=_('Rack'), - fetch_trigger='open' - ) - role_id = DynamicModelMultipleChoiceField( - queryset=DeviceRole.objects.all(), - required=False, - label=_('Role'), - fetch_trigger='open' - ) - manufacturer_id = DynamicModelMultipleChoiceField( - queryset=Manufacturer.objects.all(), - required=False, - label=_('Manufacturer'), - fetch_trigger='open' - ) - device_type_id = DynamicModelMultipleChoiceField( - queryset=DeviceType.objects.all(), - required=False, - query_params={ - 'manufacturer_id': '$manufacturer_id' - }, - label=_('Model'), - fetch_trigger='open' - ) - platform_id = DynamicModelMultipleChoiceField( - queryset=Platform.objects.all(), - required=False, - null_option='None', - label=_('Platform'), - fetch_trigger='open' - ) - status = forms.MultipleChoiceField( - choices=DeviceStatusChoices, - required=False, - widget=StaticSelectMultiple() - ) - serial = forms.CharField( - required=False - ) - 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=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - virtual_chassis_member = forms.NullBooleanField( - required=False, - label='Virtual chassis member', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - console_ports = forms.NullBooleanField( - required=False, - label='Has console ports', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - console_server_ports = forms.NullBooleanField( - required=False, - label='Has console server ports', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - power_ports = forms.NullBooleanField( - required=False, - label='Has power ports', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - power_outlets = forms.NullBooleanField( - required=False, - label='Has power outlets', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - interfaces = forms.NullBooleanField( - required=False, - label='Has interfaces', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - pass_through_ports = forms.NullBooleanField( - required=False, - label='Has pass-through ports', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - tag = TagFilterField(model) - - -# -# Device components -# - -class ComponentCreateForm(BootstrapMixin, CustomFieldsMixin, ComponentForm): - """ - Base form for the creation of device components (models subclassed from ComponentModel). - """ - device = DynamicModelChoiceField( - queryset=Device.objects.all() - ) - description = forms.CharField( - max_length=200, - required=False - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - -class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentForm): - pk = forms.ModelMultipleChoiceField( - queryset=Device.objects.all(), - widget=forms.MultipleHiddenInput() - ) - description = forms.CharField( - max_length=100, - required=False - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - -# -# Console ports -# - - -class ConsolePortFilterForm(DeviceComponentFilterForm): - model = ConsolePort - field_groups = [ - ['q', 'tag'], - ['name', 'label', 'type', 'speed'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], - ] - type = forms.MultipleChoiceField( - choices=ConsolePortTypeChoices, - required=False, - widget=StaticSelectMultiple() - ) - speed = forms.MultipleChoiceField( - choices=ConsolePortSpeedChoices, - required=False, - widget=StaticSelectMultiple() - ) - 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=StaticSelect() - ) - speed = forms.ChoiceField( - choices=add_blank_choice(ConsolePortSpeedChoices), - required=False, - widget=StaticSelect() - ) - field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags') - - -class 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, - CustomFieldModelBulkEditForm -): - 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 = [ - ['q', 'tag'], - ['name', 'label', 'type', 'speed'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], - ] - type = forms.MultipleChoiceField( - choices=ConsolePortTypeChoices, - required=False, - widget=StaticSelectMultiple() - ) - speed = forms.MultipleChoiceField( - choices=ConsolePortSpeedChoices, - required=False, - widget=StaticSelectMultiple() - ) - 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=StaticSelect() - ) - speed = forms.ChoiceField( - choices=add_blank_choice(ConsolePortSpeedChoices), - required=False, - widget=StaticSelect() - ) - field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags') - - -class 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, - CustomFieldModelBulkEditForm -): - 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 = [ - ['q', 'tag'], - ['name', 'label', 'type'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], - ] - type = forms.MultipleChoiceField( - choices=PowerPortTypeChoices, - required=False, - widget=StaticSelectMultiple() - ) - 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=StaticSelect() - ) - maximum_draw = forms.IntegerField( - min_value=1, - required=False, - help_text="Maximum draw in watts" - ) - allocated_draw = forms.IntegerField( - min_value=1, - required=False, - help_text="Allocated draw in watts" - ) - field_order = ( - 'device', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', - 'description', 'tags', - ) - - -class 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, - CustomFieldModelBulkEditForm -): - 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 = [ - ['q', 'tag'], - ['name', 'label', 'type'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], - ] - type = forms.MultipleChoiceField( - choices=PowerOutletTypeChoices, - required=False, - widget=StaticSelectMultiple() - ) - 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=StaticSelect() - ) - power_port = forms.ModelChoiceField( - queryset=PowerPort.objects.all(), - required=False - ) - feed_leg = forms.ChoiceField( - choices=add_blank_choice(PowerOutletFeedLegChoices), - required=False - ) - field_order = ( - 'device', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description', - 'tags', - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Limit power_port queryset to PowerPorts which belong to the parent Device - device = Device.objects.get( - pk=self.initial.get('device') or self.data.get('device') - ) - self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) - - -class 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, - CustomFieldModelBulkEditForm -): - 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 = [ - ['q', 'tag'], - ['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], - ] - type = forms.MultipleChoiceField( - choices=InterfaceTypeChoices, - required=False, - widget=StaticSelectMultiple() - ) - enabled = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - mgmt_only = forms.NullBooleanField( - required=False, - widget=StaticSelect( - 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', - } - ) - vlan_group = DynamicModelChoiceField( - queryset=VLANGroup.objects.all(), - required=False, - label='VLAN group' - ) - untagged_vlan = DynamicModelChoiceField( - queryset=VLAN.objects.all(), - required=False, - label='Untagged VLAN', - query_params={ - 'group_id': '$vlan_group', - } - ) - tagged_vlans = DynamicModelMultipleChoiceField( - queryset=VLAN.objects.all(), - required=False, - label='Tagged VLANs', - query_params={ - 'group_id': '$vlan_group', - } - ) - 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': StaticSelect(), - 'mode': StaticSelect(), - } - 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=StaticSelect(), - ) - enabled = forms.BooleanField( - required=False, - initial=True - ) - parent = DynamicModelChoiceField( - queryset=Interface.objects.all(), - required=False, - query_params={ - 'device_id': '$device', - } - ) - lag = DynamicModelChoiceField( - queryset=Interface.objects.all(), - required=False, - query_params={ - 'device_id': '$device', - 'type': 'lag', - } - ) - mac_address = forms.CharField( - required=False, - label='MAC Address' - ) - mgmt_only = forms.BooleanField( - required=False, - label='Management only', - help_text='This interface is used only for out-of-band management' - ) - mode = forms.ChoiceField( - choices=add_blank_choice(InterfaceModeChoices), - required=False, - widget=StaticSelect(), - ) - untagged_vlan = DynamicModelChoiceField( - queryset=VLAN.objects.all(), - required=False - ) - tagged_vlans = DynamicModelMultipleChoiceField( - queryset=VLAN.objects.all(), - required=False - ) - field_order = ( - 'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', - 'description', 'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags' - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Limit VLAN choices by device - device_id = self.initial.get('device') or self.data.get('device') - self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device_id) - self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device_id) - - -class 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, - CustomFieldModelBulkEditForm -): - 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 - ) - self.fields['parent'].queryset = Interface.objects.filter( - Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis) - ) - elif device: - self.fields['lag'].queryset = Interface.objects.filter( - device=device, - type=InterfaceTypeChoices.TYPE_LAG - ) - self.fields['parent'].queryset = Interface.objects.filter(device=device) - else: - self.fields['lag'].queryset = Interface.objects.none() - self.fields['parent'].queryset = Interface.objects.none() - - def clean_enabled(self): - # Make sure enabled is True when it's not included in the uploaded data - if 'enabled' not in self.data: - return True - else: - return self.cleaned_data['enabled'] - - -# -# Front pass-through ports -# - -class FrontPortFilterForm(DeviceComponentFilterForm): - field_groups = [ - ['q', 'tag'], - ['name', 'label', 'type', 'color'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], - ] - model = FrontPort - type = forms.MultipleChoiceField( - choices=PortTypeChoices, - required=False, - widget=StaticSelectMultiple() - ) - 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': StaticSelect(), - 'rear_port': StaticSelect(), - } - - 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=StaticSelect(), - ) - color = ColorField( - required=False - ) - rear_port_set = forms.MultipleChoiceField( - choices=[], - label='Rear ports', - help_text='Select one rear port assignment for each front port being created.', - ) - field_order = ( - 'device', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'mark_connected', 'description', - 'tags', - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - device = Device.objects.get( - pk=self.initial.get('device') or self.data.get('device') - ) - - # Determine which rear port positions are occupied. These will be excluded from the list of available - # mappings. - occupied_port_positions = [ - (front_port.rear_port_id, front_port.rear_port_position) - for front_port in device.frontports.all() - ] - - # Populate rear port choices - choices = [] - rear_ports = RearPort.objects.filter(device=device) - for rear_port in rear_ports: - for i in range(1, rear_port.positions + 1): - if (rear_port.pk, i) not in occupied_port_positions: - choices.append( - ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i)) - ) - self.fields['rear_port_set'].choices = choices - - def clean(self): - super().clean() - - # Validate that the number of ports being created equals the number of selected (rear port, position) tuples - front_port_count = len(self.cleaned_data['name_pattern']) - rear_port_count = len(self.cleaned_data['rear_port_set']) - if front_port_count != rear_port_count: - raise forms.ValidationError({ - 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments ' - 'were selected. These counts must match.'.format(front_port_count, rear_port_count) - }) - - def get_iterative_data(self, iteration): - - # Assign rear port and position from selected set - rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':') - - return { - 'rear_port': int(rear_port), - 'rear_port_position': int(position), - } - - -# class 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, - CustomFieldModelBulkEditForm -): - 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 = [ - ['q', 'tag'], - ['name', 'label', 'type', 'color'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], - ] - type = forms.MultipleChoiceField( - choices=PortTypeChoices, - required=False, - widget=StaticSelectMultiple() - ) - 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': StaticSelect(), - } - - -class RearPortCreateForm(ComponentCreateForm): - model = RearPort - type = forms.ChoiceField( - choices=PortTypeChoices, - widget=StaticSelect(), - ) - color = ColorField( - required=False - ) - positions = forms.IntegerField( - min_value=REARPORT_POSITIONS_MIN, - max_value=REARPORT_POSITIONS_MAX, - initial=1, - help_text='The number of front ports which may be mapped to each rear port' - ) - field_order = ( - 'device', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'mark_connected', 'description', - 'tags', - ) - - -class 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, - CustomFieldModelBulkEditForm -): - 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 = [ - ['q', 'tag'], - ['name', 'label'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], - ] - 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=StaticSelect(), - ) - - 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, - CustomFieldModelBulkEditForm -): - 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 - ) - parent = CSVModelChoiceField( - queryset=Device.objects.all(), - to_field_name='name', - required=False, - help_text='Parent inventory item' - ) - - class Meta: - model = InventoryItem - fields = ( - 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Limit parent choices to inventory items belonging to this device - device = None - if self.is_bound and 'device' in self.data: - try: - device = self.fields['device'].to_python(self.data['device']) - except forms.ValidationError: - pass - if device: - self.fields['parent'].queryset = InventoryItem.objects.filter(device=device) - else: - self.fields['parent'].queryset = InventoryItem.objects.none() - - -class 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, - CustomFieldModelBulkEditForm -): - 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 = [ - ['q', 'tag'], - ['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], - ] - manufacturer_id = DynamicModelMultipleChoiceField( - queryset=Manufacturer.objects.all(), - required=False, - label=_('Manufacturer'), - fetch_trigger='open' - ) - serial = forms.CharField( - required=False - ) - asset_tag = forms.CharField( - required=False - ) - discovered = forms.NullBooleanField( - required=False, - widget=StaticSelect( - 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', - 'location_id': '$termination_b_location', - 'rack_id': '$termination_b_rack', - } - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = Cable - fields = [ - 'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device', - 'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', - ] - widgets = { - 'status': StaticSelect, - 'type': StaticSelect, - 'length_unit': StaticSelect, - } - - def clean_termination_b_id(self): - # Return the PK rather than the object - return getattr(self.cleaned_data['termination_b_id'], 'pk', None) - - -class ConnectCableToConsolePortForm(ConnectCableToDeviceForm): - termination_b_id = DynamicModelChoiceField( - queryset=ConsolePort.objects.all(), - label='Name', - disabled_indicator='_occupied', - query_params={ - 'device_id': '$termination_b_device' - } - ) - - -class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm): - termination_b_id = DynamicModelChoiceField( - queryset=ConsoleServerPort.objects.all(), - label='Name', - disabled_indicator='_occupied', - query_params={ - 'device_id': '$termination_b_device' - } - ) - - -class ConnectCableToPowerPortForm(ConnectCableToDeviceForm): - termination_b_id = DynamicModelChoiceField( - queryset=PowerPort.objects.all(), - label='Name', - disabled_indicator='_occupied', - query_params={ - 'device_id': '$termination_b_device' - } - ) - - -class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm): - termination_b_id = DynamicModelChoiceField( - queryset=PowerOutlet.objects.all(), - label='Name', - disabled_indicator='_occupied', - query_params={ - 'device_id': '$termination_b_device' - } - ) - - -class ConnectCableToInterfaceForm(ConnectCableToDeviceForm): - termination_b_id = DynamicModelChoiceField( - queryset=Interface.objects.all(), - label='Name', - disabled_indicator='_occupied', - query_params={ - 'device_id': '$termination_b_device', - 'kind': 'physical', - } - ) - - -class ConnectCableToFrontPortForm(ConnectCableToDeviceForm): - termination_b_id = DynamicModelChoiceField( - queryset=FrontPort.objects.all(), - label='Name', - disabled_indicator='_occupied', - query_params={ - 'device_id': '$termination_b_device' - } - ) - - -class ConnectCableToRearPortForm(ConnectCableToDeviceForm): - termination_b_id = DynamicModelChoiceField( - queryset=RearPort.objects.all(), - label='Name', - disabled_indicator='_occupied', - query_params={ - 'device_id': '$termination_b_device' - } - ) - - -class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm): - termination_b_provider = DynamicModelChoiceField( - queryset=Provider.objects.all(), - label='Provider', - required=False - ) - termination_b_region = DynamicModelChoiceField( - queryset=Region.objects.all(), - label='Region', - required=False - ) - termination_b_site_group = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - label='Site group', - required=False - ) - termination_b_site = DynamicModelChoiceField( - queryset=Site.objects.all(), - label='Site', - required=False, - query_params={ - 'region_id': '$termination_b_region', - 'group_id': '$termination_b_site_group', - } - ) - termination_b_circuit = DynamicModelChoiceField( - queryset=Circuit.objects.all(), - label='Circuit', - query_params={ - 'provider_id': '$termination_b_provider', - 'site_id': '$termination_b_site', - } - ) - termination_b_id = DynamicModelChoiceField( - queryset=CircuitTermination.objects.all(), - label='Side', - disabled_indicator='_occupied', - query_params={ - 'circuit_id': '$termination_b_circuit' - } - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = Cable - fields = [ - 'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit', - 'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', - ] - - def clean_termination_b_id(self): - # Return the PK rather than the object - return getattr(self.cleaned_data['termination_b_id'], 'pk', None) - - -class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm): - termination_b_region = DynamicModelChoiceField( - queryset=Region.objects.all(), - label='Region', - required=False - ) - termination_b_site_group = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - label='Site group', - required=False - ) - termination_b_site = DynamicModelChoiceField( - queryset=Site.objects.all(), - label='Site', - required=False, - query_params={ - 'region_id': '$termination_b_region', - 'group_id': '$termination_b_site_group', - } - ) - termination_b_location = DynamicModelChoiceField( - queryset=Location.objects.all(), - label='Location', - required=False, - query_params={ - 'site_id': '$termination_b_site' - } - ) - termination_b_powerpanel = DynamicModelChoiceField( - queryset=PowerPanel.objects.all(), - label='Power Panel', - required=False, - query_params={ - 'site_id': '$termination_b_site', - 'location_id': '$termination_b_location', - } - ) - termination_b_id = DynamicModelChoiceField( - queryset=PowerFeed.objects.all(), - label='Name', - disabled_indicator='_occupied', - query_params={ - 'power_panel_id': '$termination_b_powerpanel' - } - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = Cable - fields = [ - 'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label', - 'color', 'length', 'length_unit', 'tags', - ] - - def clean_termination_b_id(self): - # Return the PK rather than the object - return getattr(self.cleaned_data['termination_b_id'], 'pk', None) - - -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': StaticSelect, - 'type': StaticSelect, - 'length_unit': StaticSelect, - } - 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, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Cable.objects.all(), - widget=forms.MultipleHiddenInput - ) - type = forms.ChoiceField( - choices=add_blank_choice(CableTypeChoices), - required=False, - initial='', - widget=StaticSelect() - ) - status = forms.ChoiceField( - choices=add_blank_choice(CableStatusChoices), - required=False, - widget=StaticSelect(), - initial='' - ) - label = forms.CharField( - max_length=100, - required=False - ) - color = ColorField( - required=False - ) - length = forms.DecimalField( - min_value=0, - required=False - ) - length_unit = forms.ChoiceField( - choices=add_blank_choice(CableLengthUnitChoices), - required=False, - initial='', - widget=StaticSelect() - ) - - 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, CustomFieldModelFilterForm): - model = Cable - field_groups = [ - ['q', 'tag'], - ['site_id', 'rack_id', 'device_id'], - ['type', 'status', 'color'], - ['tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id' - }, - label=_('Site'), - fetch_trigger='open' - ) - tenant_id = DynamicModelMultipleChoiceField( - queryset=Tenant.objects.all(), - required=False, - label=_('Tenant'), - fetch_trigger='open' - ) - rack_id = DynamicModelMultipleChoiceField( - queryset=Rack.objects.all(), - required=False, - label=_('Rack'), - null_option='None', - query_params={ - 'site_id': '$site_id' - }, - fetch_trigger='open' - ) - type = forms.MultipleChoiceField( - choices=add_blank_choice(CableTypeChoices), - required=False, - widget=StaticSelect() - ) - status = forms.ChoiceField( - required=False, - choices=add_blank_choice(CableStatusChoices), - widget=StaticSelect() - ) - 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'), - fetch_trigger='open' - ) - tag = TagFilterField(model) - - -# -# Connections -# - -class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id' - }, - label=_('Site'), - fetch_trigger='open' - ) - device_id = DynamicModelMultipleChoiceField( - queryset=Device.objects.all(), - required=False, - query_params={ - 'site_id': '$site_id' - }, - label=_('Device'), - fetch_trigger='open' - ) - - -class PowerConnectionFilterForm(BootstrapMixin, forms.Form): - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id' - }, - label=_('Site'), - fetch_trigger='open' - ) - device_id = DynamicModelMultipleChoiceField( - queryset=Device.objects.all(), - required=False, - query_params={ - 'site_id': '$site_id' - }, - label=_('Device'), - fetch_trigger='open' - ) - - -class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id' - }, - label=_('Site'), - fetch_trigger='open' - ) - device_id = DynamicModelMultipleChoiceField( - queryset=Device.objects.all(), - required=False, - query_params={ - 'site_id': '$site_id' - }, - label=_('Device'), - fetch_trigger='open' - ) - - -# -# 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, CustomFieldModelBulkEditForm): - 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, CustomFieldModelFilterForm): - model = VirtualChassis - field_order = ['q', 'region_id', 'site_group_id', 'site_id', 'tenant_group_id', 'tenant_id'] - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id'], - ['tenant_group_id', 'tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_group_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site group'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id', - 'group_id': '$site_group_id', - }, - label=_('Site'), - fetch_trigger='open' - ) - 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, CustomFieldModelBulkEditForm): - 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, CustomFieldModelFilterForm): - model = PowerPanel - field_groups = ( - ('q', 'tag'), - ('region_id', 'site_group_id', 'site_id', 'location_id') - ) - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_group_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site group'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id', - 'group_id': '$site_group_id', - }, - label=_('Site'), - fetch_trigger='open' - ) - location_id = DynamicModelMultipleChoiceField( - queryset=Location.objects.all(), - required=False, - null_option='None', - query_params={ - 'site_id': '$site_id' - }, - label=_('Location'), - fetch_trigger='open' - ) - 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': StaticSelect(), - 'type': StaticSelect(), - 'supply': StaticSelect(), - 'phase': StaticSelect(), - } - - -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, CustomFieldModelBulkEditForm): - 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=StaticSelect() - ) - type = forms.ChoiceField( - choices=add_blank_choice(PowerFeedTypeChoices), - required=False, - initial='', - widget=StaticSelect() - ) - supply = forms.ChoiceField( - choices=add_blank_choice(PowerFeedSupplyChoices), - required=False, - initial='', - widget=StaticSelect() - ) - phase = forms.ChoiceField( - choices=add_blank_choice(PowerFeedPhaseChoices), - required=False, - initial='', - widget=StaticSelect() - ) - 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, CustomFieldModelFilterForm): - model = PowerFeed - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id'], - ['power_panel_id', 'rack_id'], - ['status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_group_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site group'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id' - }, - label=_('Site'), - fetch_trigger='open' - ) - power_panel_id = DynamicModelMultipleChoiceField( - queryset=PowerPanel.objects.all(), - required=False, - null_option='None', - query_params={ - 'site_id': '$site_id' - }, - label=_('Power panel'), - fetch_trigger='open' - ) - rack_id = DynamicModelMultipleChoiceField( - queryset=Rack.objects.all(), - required=False, - null_option='None', - query_params={ - 'site_id': '$site_id' - }, - label=_('Rack'), - fetch_trigger='open' - ) - status = forms.MultipleChoiceField( - choices=PowerFeedStatusChoices, - required=False, - widget=StaticSelectMultiple() - ) - type = forms.ChoiceField( - choices=add_blank_choice(PowerFeedTypeChoices), - required=False, - widget=StaticSelect() - ) - supply = forms.ChoiceField( - choices=add_blank_choice(PowerFeedSupplyChoices), - required=False, - widget=StaticSelect() - ) - phase = forms.ChoiceField( - choices=add_blank_choice(PowerFeedPhaseChoices), - required=False, - widget=StaticSelect() - ) - voltage = forms.IntegerField( - required=False - ) - amperage = forms.IntegerField( - required=False - ) - max_utilization = forms.IntegerField( - required=False - ) - tag = TagFilterField(model) diff --git a/netbox/dcim/forms/__init__.py b/netbox/dcim/forms/__init__.py new file mode 100644 index 000000000..322abff9a --- /dev/null +++ b/netbox/dcim/forms/__init__.py @@ -0,0 +1,10 @@ +from .fields import * +from .models import * +from .filtersets import * +from .object_create import * +from .object_import import * +from .bulk_create import * +from .bulk_edit import * +from .bulk_import import * +from .connections import * +from .formsets import * diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py new file mode 100644 index 000000000..3464280f1 --- /dev/null +++ b/netbox/dcim/forms/bulk_create.py @@ -0,0 +1,111 @@ +from django import forms + +from dcim.models import * +from extras.forms import CustomFieldsMixin +from extras.models import Tag +from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, form_from_model +from .object_create import ComponentForm + +__all__ = ( + 'ConsolePortBulkCreateForm', + 'ConsoleServerPortBulkCreateForm', + 'DeviceBayBulkCreateForm', + # 'FrontPortBulkCreateForm', + 'InterfaceBulkCreateForm', + 'InventoryItemBulkCreateForm', + 'PowerOutletBulkCreateForm', + 'PowerPortBulkCreateForm', + 'RearPortBulkCreateForm', +) + + +# +# Device components +# + +class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentForm): + pk = forms.ModelMultipleChoiceField( + queryset=Device.objects.all(), + widget=forms.MultipleHiddenInput() + ) + description = forms.CharField( + max_length=100, + required=False + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + +class ConsolePortBulkCreateForm( + form_from_model(ConsolePort, ['type', 'speed', 'mark_connected']), + DeviceBulkAddComponentForm +): + model = ConsolePort + field_order = ('name_pattern', 'label_pattern', 'type', 'mark_connected', 'description', 'tags') + + +class ConsoleServerPortBulkCreateForm( + form_from_model(ConsoleServerPort, ['type', 'speed', 'mark_connected']), + DeviceBulkAddComponentForm +): + model = ConsoleServerPort + field_order = ('name_pattern', 'label_pattern', 'type', 'speed', 'description', 'tags') + + +class PowerPortBulkCreateForm( + form_from_model(PowerPort, ['type', 'maximum_draw', 'allocated_draw', 'mark_connected']), + DeviceBulkAddComponentForm +): + model = PowerPort + field_order = ('name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags') + + +class PowerOutletBulkCreateForm( + form_from_model(PowerOutlet, ['type', 'feed_leg', 'mark_connected']), + DeviceBulkAddComponentForm +): + model = PowerOutlet + field_order = ('name_pattern', 'label_pattern', 'type', 'feed_leg', 'description', 'tags') + + +class InterfaceBulkCreateForm( + form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'mark_connected']), + DeviceBulkAddComponentForm +): + model = Interface + field_order = ( + 'name_pattern', 'label_pattern', 'type', 'enabled', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'tags', + ) + + +# class FrontPortBulkCreateForm( +# form_from_model(FrontPort, ['label', 'type', 'description', 'tags']), +# DeviceBulkAddComponentForm +# ): +# pass + + +class RearPortBulkCreateForm( + form_from_model(RearPort, ['type', 'color', 'positions', 'mark_connected']), + DeviceBulkAddComponentForm +): + model = RearPort + field_order = ('name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags') + + +class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm): + model = DeviceBay + field_order = ('name_pattern', 'label_pattern', 'description', 'tags') + + +class InventoryItemBulkCreateForm( + form_from_model(InventoryItem, ['manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']), + DeviceBulkAddComponentForm +): + model = InventoryItem + field_order = ( + 'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', + 'tags', + ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py new file mode 100644 index 000000000..c1b1bcb3a --- /dev/null +++ b/netbox/dcim/forms/bulk_edit.py @@ -0,0 +1,1090 @@ +from django import forms +from django.contrib.auth.models import User +from timezone_field import TimeZoneFormField + +from dcim.choices import * +from dcim.constants import * +from dcim.models import * +from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm +from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN +from ipam.models import VLAN +from tenancy.models import Tenant +from utilities.forms import ( + add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, + DynamicModelChoiceField, DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect, +) + +__all__ = ( + 'CableBulkEditForm', + 'ConsolePortBulkEditForm', + 'ConsolePortTemplateBulkEditForm', + 'ConsoleServerPortBulkEditForm', + 'ConsoleServerPortTemplateBulkEditForm', + 'DeviceBayBulkEditForm', + 'DeviceBayTemplateBulkEditForm', + 'DeviceBulkEditForm', + 'DeviceRoleBulkEditForm', + 'DeviceTypeBulkEditForm', + 'FrontPortBulkEditForm', + 'FrontPortTemplateBulkEditForm', + 'InterfaceBulkEditForm', + 'InterfaceTemplateBulkEditForm', + 'InventoryItemBulkEditForm', + 'LocationBulkEditForm', + 'ManufacturerBulkEditForm', + 'PlatformBulkEditForm', + 'PowerFeedBulkEditForm', + 'PowerOutletBulkEditForm', + 'PowerOutletTemplateBulkEditForm', + 'PowerPanelBulkEditForm', + 'PowerPortBulkEditForm', + 'PowerPortTemplateBulkEditForm', + 'RackBulkEditForm', + 'RackReservationBulkEditForm', + 'RackRoleBulkEditForm', + 'RearPortBulkEditForm', + 'RearPortTemplateBulkEditForm', + 'RegionBulkEditForm', + 'SiteBulkEditForm', + 'SiteGroupBulkEditForm', + 'VirtualChassisBulkEditForm', +) + + +class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + 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 SiteGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + 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 SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Site.objects.all(), + widget=forms.MultipleHiddenInput + ) + status = forms.ChoiceField( + choices=add_blank_choice(SiteStatusChoices), + required=False, + initial='', + widget=StaticSelect() + ) + 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=StaticSelect() + ) + + class Meta: + nullable_fields = [ + 'region', 'group', 'tenant', 'asn', 'description', 'time_zone', + ] + + +class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + 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 RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + 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'] + + +class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + 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=StaticSelect() + ) + 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=StaticSelect() + ) + width = forms.ChoiceField( + choices=add_blank_choice(RackWidthChoices), + required=False, + widget=StaticSelect() + ) + 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=StaticSelect() + ) + comments = CommentField( + widget=SmallTextarea, + label='Comments' + ) + + class Meta: + nullable_fields = [ + 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments', + ] + + +class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=RackReservation.objects.all(), + widget=forms.MultipleHiddenInput() + ) + user = forms.ModelChoiceField( + queryset=User.objects.order_by( + 'username' + ), + required=False, + widget=StaticSelect() + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [] + + +class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Manufacturer.objects.all(), + widget=forms.MultipleHiddenInput + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['description'] + + +class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + 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 DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + 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'] + + +class PlatformBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + 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'] + + +class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + 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=StaticSelect() + ) + serial = forms.CharField( + max_length=50, + required=False, + label='Serial Number' + ) + + class Meta: + nullable_fields = [ + 'tenant', 'platform', 'serial', + ] + + +class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Cable.objects.all(), + widget=forms.MultipleHiddenInput + ) + type = forms.ChoiceField( + choices=add_blank_choice(CableTypeChoices), + required=False, + initial='', + widget=StaticSelect() + ) + status = forms.ChoiceField( + choices=add_blank_choice(CableStatusChoices), + required=False, + widget=StaticSelect(), + initial='' + ) + label = forms.CharField( + max_length=100, + required=False + ) + color = ColorField( + required=False + ) + length = forms.DecimalField( + min_value=0, + required=False + ) + length_unit = forms.ChoiceField( + choices=add_blank_choice(CableLengthUnitChoices), + required=False, + initial='', + widget=StaticSelect() + ) + + 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 VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=VirtualChassis.objects.all(), + widget=forms.MultipleHiddenInput() + ) + domain = forms.CharField( + max_length=30, + required=False + ) + + class Meta: + nullable_fields = ['domain'] + + +class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + 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 PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + 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=StaticSelect() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerFeedTypeChoices), + required=False, + initial='', + widget=StaticSelect() + ) + supply = forms.ChoiceField( + choices=add_blank_choice(PowerFeedSupplyChoices), + required=False, + initial='', + widget=StaticSelect() + ) + phase = forms.ChoiceField( + choices=add_blank_choice(PowerFeedPhaseChoices), + required=False, + initial='', + widget=StaticSelect() + ) + 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', + ] + + +# +# Device component templates +# + +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=StaticSelect() + ) + + class Meta: + nullable_fields = ('label', '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=StaticSelect() + ) + description = forms.CharField( + required=False + ) + + class Meta: + nullable_fields = ('label', 'type', '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=StaticSelect() + ) + 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 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=StaticSelect() + ) + power_port = forms.ModelChoiceField( + queryset=PowerPortTemplate.objects.all(), + required=False + ) + feed_leg = forms.ChoiceField( + choices=add_blank_choice(PowerOutletFeedLegChoices), + required=False, + widget=StaticSelect() + ) + 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 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=StaticSelect() + ) + mgmt_only = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect, + label='Management only' + ) + description = forms.CharField( + required=False + ) + + class Meta: + nullable_fields = ('label', 'description') + + +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=StaticSelect() + ) + color = ColorField( + required=False + ) + description = forms.CharField( + required=False + ) + + class Meta: + nullable_fields = ('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=StaticSelect() + ) + color = ColorField( + required=False + ) + description = forms.CharField( + required=False + ) + + class Meta: + nullable_fields = ('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') + + +# +# Device components +# + +class ConsolePortBulkEditForm( + form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + CustomFieldModelBulkEditForm +): + 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 ConsoleServerPortBulkEditForm( + form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + CustomFieldModelBulkEditForm +): + 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 PowerPortBulkEditForm( + form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + CustomFieldModelBulkEditForm +): + 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 PowerOutletBulkEditForm( + form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + CustomFieldModelBulkEditForm +): + 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 InterfaceBulkEditForm( + form_from_model(Interface, [ + 'label', 'type', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode', + ]), + BootstrapMixin, + AddRemoveTagsForm, + CustomFieldModelBulkEditForm +): + 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 FrontPortBulkEditForm( + form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + CustomFieldModelBulkEditForm +): + pk = forms.ModelMultipleChoiceField( + queryset=FrontPort.objects.all(), + widget=forms.MultipleHiddenInput() + ) + + class Meta: + nullable_fields = ['label', 'description'] + + +class RearPortBulkEditForm( + form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + CustomFieldModelBulkEditForm +): + pk = forms.ModelMultipleChoiceField( + queryset=RearPort.objects.all(), + widget=forms.MultipleHiddenInput() + ) + + class Meta: + nullable_fields = ['label', 'description'] + + +class DeviceBayBulkEditForm( + form_from_model(DeviceBay, ['label', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + CustomFieldModelBulkEditForm +): + pk = forms.ModelMultipleChoiceField( + queryset=DeviceBay.objects.all(), + widget=forms.MultipleHiddenInput() + ) + + class Meta: + nullable_fields = ['label', 'description'] + + +class InventoryItemBulkEditForm( + form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + CustomFieldModelBulkEditForm +): + 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'] diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py new file mode 100644 index 000000000..93f17e839 --- /dev/null +++ b/netbox/dcim/forms/bulk_import.py @@ -0,0 +1,976 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.forms.array import SimpleArrayField +from django.core.exceptions import ObjectDoesNotExist +from django.utils.safestring import mark_safe + +from dcim.choices import * +from dcim.constants import * +from dcim.models import * +from extras.forms import CustomFieldModelCSVForm +from tenancy.models import Tenant +from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField +from virtualization.models import Cluster + +__all__ = ( + 'CableCSVForm', + 'ChildDeviceCSVForm', + 'ConsolePortCSVForm', + 'ConsoleServerPortCSVForm', + 'DeviceBayCSVForm', + 'DeviceCSVForm', + 'DeviceRoleCSVForm', + 'FrontPortCSVForm', + 'InterfaceCSVForm', + 'InventoryItemCSVForm', + 'LocationCSVForm', + 'ManufacturerCSVForm', + 'PlatformCSVForm', + 'PowerFeedCSVForm', + 'PowerOutletCSVForm', + 'PowerPanelCSVForm', + 'PowerPortCSVForm', + 'RackCSVForm', + 'RackReservationCSVForm', + 'RackRoleCSVForm', + 'RearPortCSVForm', + 'RegionCSVForm', + 'SiteCSVForm', + 'SiteGroupCSVForm', + 'VirtualChassisCSVForm', +) + + +class RegionCSVForm(CustomFieldModelCSVForm): + parent = CSVModelChoiceField( + queryset=Region.objects.all(), + required=False, + to_field_name='name', + help_text='Name of parent region' + ) + + class Meta: + model = Region + fields = ('name', 'slug', 'parent', 'description') + + +class SiteGroupCSVForm(CustomFieldModelCSVForm): + parent = CSVModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + to_field_name='name', + help_text='Name of parent site group' + ) + + class Meta: + model = SiteGroup + fields = ('name', 'slug', 'parent', 'description') + + +class SiteCSVForm(CustomFieldModelCSVForm): + status = CSVChoiceField( + choices=SiteStatusChoices, + required=False, + help_text='Operational status' + ) + region = CSVModelChoiceField( + queryset=Region.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned region' + ) + group = CSVModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned group' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) + + class Meta: + model = Site + fields = ( + 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description', + 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', + 'contact_email', 'comments', + ) + help_texts = { + 'time_zone': mark_safe( + 'Time zone (available options)' + ) + } + + +class LocationCSVForm(CustomFieldModelCSVForm): + site = CSVModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name', + help_text='Assigned site' + ) + parent = CSVModelChoiceField( + queryset=Location.objects.all(), + required=False, + to_field_name='name', + help_text='Parent location', + error_messages={ + 'invalid_choice': 'Location not found.', + } + ) + + class Meta: + model = Location + fields = ('site', 'parent', 'name', 'slug', 'description') + + +class RackRoleCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + + class Meta: + model = RackRole + fields = ('name', 'slug', 'color', 'description') + help_texts = { + 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), + } + + +class RackCSVForm(CustomFieldModelCSVForm): + site = CSVModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name' + ) + location = CSVModelChoiceField( + queryset=Location.objects.all(), + required=False, + to_field_name='name' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Name of assigned tenant' + ) + status = CSVChoiceField( + choices=RackStatusChoices, + required=False, + help_text='Operational status' + ) + role = CSVModelChoiceField( + queryset=RackRole.objects.all(), + required=False, + to_field_name='name', + help_text='Name of assigned role' + ) + type = CSVChoiceField( + choices=RackTypeChoices, + required=False, + help_text='Rack type' + ) + width = forms.ChoiceField( + choices=RackWidthChoices, + help_text='Rail-to-rail width (in inches)' + ) + outer_unit = CSVChoiceField( + choices=RackDimensionUnitChoices, + required=False, + help_text='Unit for outer dimensions' + ) + + class Meta: + model = Rack + fields = ( + 'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', + 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', + ) + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit location queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) + + +class RackReservationCSVForm(CustomFieldModelCSVForm): + site = CSVModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name', + help_text='Parent site' + ) + location = CSVModelChoiceField( + queryset=Location.objects.all(), + to_field_name='name', + required=False, + help_text="Rack's location (if any)" + ) + rack = CSVModelChoiceField( + queryset=Rack.objects.all(), + to_field_name='name', + help_text='Rack' + ) + units = SimpleArrayField( + base_field=forms.IntegerField(), + required=True, + help_text='Comma-separated list of individual unit numbers' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) + + class Meta: + model = RackReservation + fields = ('site', 'location', 'rack', 'units', 'tenant', 'description') + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit location queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) + + # Limit rack queryset by assigned site and group + params = { + f"site__{self.fields['site'].to_field_name}": data.get('site'), + f"location__{self.fields['location'].to_field_name}": data.get('location'), + } + self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) + + +class ManufacturerCSVForm(CustomFieldModelCSVForm): + + class Meta: + model = Manufacturer + fields = ('name', 'slug', 'description') + + +class DeviceRoleCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + + class Meta: + model = DeviceRole + fields = ('name', 'slug', 'color', 'vm_role', 'description') + help_texts = { + 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), + } + + +class PlatformCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + manufacturer = CSVModelChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + to_field_name='name', + help_text='Limit platform assignments to this manufacturer' + ) + + class Meta: + model = Platform + fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description') + + +class BaseDeviceCSVForm(CustomFieldModelCSVForm): + device_role = CSVModelChoiceField( + queryset=DeviceRole.objects.all(), + to_field_name='name', + help_text='Assigned role' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) + manufacturer = CSVModelChoiceField( + queryset=Manufacturer.objects.all(), + to_field_name='name', + help_text='Device type manufacturer' + ) + device_type = CSVModelChoiceField( + queryset=DeviceType.objects.all(), + to_field_name='model', + help_text='Device type model' + ) + platform = CSVModelChoiceField( + queryset=Platform.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned platform' + ) + status = CSVChoiceField( + choices=DeviceStatusChoices, + help_text='Operational status' + ) + virtual_chassis = CSVModelChoiceField( + queryset=VirtualChassis.objects.all(), + to_field_name='name', + required=False, + help_text='Virtual chassis' + ) + cluster = CSVModelChoiceField( + queryset=Cluster.objects.all(), + to_field_name='name', + required=False, + help_text='Virtualization cluster' + ) + + class Meta: + fields = [] + model = Device + help_texts = { + 'vc_position': 'Virtual chassis position', + 'vc_priority': 'Virtual chassis priority', + } + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit device type queryset by manufacturer + params = {f"manufacturer__{self.fields['manufacturer'].to_field_name}": data.get('manufacturer')} + self.fields['device_type'].queryset = self.fields['device_type'].queryset.filter(**params) + + +class DeviceCSVForm(BaseDeviceCSVForm): + site = CSVModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name', + help_text='Assigned site' + ) + location = CSVModelChoiceField( + queryset=Location.objects.all(), + to_field_name='name', + required=False, + help_text="Assigned location (if any)" + ) + rack = CSVModelChoiceField( + queryset=Rack.objects.all(), + to_field_name='name', + required=False, + help_text="Assigned rack (if any)" + ) + face = CSVChoiceField( + choices=DeviceFaceChoices, + required=False, + help_text='Mounted rack face' + ) + + class Meta(BaseDeviceCSVForm.Meta): + fields = [ + 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', + 'site', 'location', 'rack', 'position', 'face', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', + 'comments', + ] + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit location queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) + + # Limit rack queryset by assigned site and group + params = { + f"site__{self.fields['site'].to_field_name}": data.get('site'), + f"location__{self.fields['location'].to_field_name}": data.get('location'), + } + self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) + + +class ChildDeviceCSVForm(BaseDeviceCSVForm): + parent = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Parent device' + ) + device_bay = CSVModelChoiceField( + queryset=DeviceBay.objects.all(), + to_field_name='name', + help_text='Device bay in which this device is installed' + ) + + class Meta(BaseDeviceCSVForm.Meta): + fields = [ + 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', + 'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments', + ] + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit device bay queryset by parent device + params = {f"device__{self.fields['parent'].to_field_name}": data.get('parent')} + self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params) + + def clean(self): + super().clean() + + # Set parent_bay reverse relationship + device_bay = self.cleaned_data.get('device_bay') + if device_bay: + self.instance.parent_bay = device_bay + + # Inherit site and rack from parent device + parent = self.cleaned_data.get('parent') + if parent: + self.instance.site = parent.site + self.instance.rack = parent.rack + + +# +# Device components +# + +class ConsolePortCSVForm(CustomFieldModelCSVForm): + device = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name' + ) + type = CSVChoiceField( + choices=ConsolePortTypeChoices, + required=False, + help_text='Port type' + ) + speed = CSVTypedChoiceField( + choices=ConsolePortSpeedChoices, + coerce=int, + empty_value=None, + required=False, + help_text='Port speed in bps' + ) + + class Meta: + model = ConsolePort + fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description') + + +class ConsoleServerPortCSVForm(CustomFieldModelCSVForm): + device = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name' + ) + type = CSVChoiceField( + choices=ConsolePortTypeChoices, + required=False, + help_text='Port type' + ) + speed = CSVTypedChoiceField( + choices=ConsolePortSpeedChoices, + coerce=int, + empty_value=None, + required=False, + help_text='Port speed in bps' + ) + + class Meta: + model = ConsoleServerPort + fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description') + + +class PowerPortCSVForm(CustomFieldModelCSVForm): + device = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name' + ) + type = CSVChoiceField( + choices=PowerPortTypeChoices, + required=False, + help_text='Port type' + ) + + class Meta: + model = PowerPort + fields = ( + 'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description', + ) + + +class PowerOutletCSVForm(CustomFieldModelCSVForm): + device = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name' + ) + type = CSVChoiceField( + choices=PowerOutletTypeChoices, + required=False, + help_text='Outlet type' + ) + power_port = CSVModelChoiceField( + queryset=PowerPort.objects.all(), + required=False, + to_field_name='name', + help_text='Local power port which feeds this outlet' + ) + feed_leg = CSVChoiceField( + choices=PowerOutletFeedLegChoices, + required=False, + help_text='Electrical phase (for three-phase circuits)' + ) + + class Meta: + model = PowerOutlet + fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit PowerPort choices to those belonging to this device (or VC master) + if self.is_bound: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + try: + device = self.instance.device + except Device.DoesNotExist: + device = None + + if device: + self.fields['power_port'].queryset = PowerPort.objects.filter( + device__in=[device, device.get_vc_master()] + ) + else: + self.fields['power_port'].queryset = PowerPort.objects.none() + + +class InterfaceCSVForm(CustomFieldModelCSVForm): + device = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name' + ) + parent = CSVModelChoiceField( + queryset=Interface.objects.all(), + required=False, + to_field_name='name', + help_text='Parent interface' + ) + lag = CSVModelChoiceField( + queryset=Interface.objects.all(), + required=False, + to_field_name='name', + help_text='Parent LAG interface' + ) + type = CSVChoiceField( + choices=InterfaceTypeChoices, + help_text='Physical medium' + ) + mode = CSVChoiceField( + choices=InterfaceModeChoices, + required=False, + help_text='IEEE 802.1Q operational mode (for L2 interfaces)' + ) + + class Meta: + model = Interface + fields = ( + 'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu', + 'mgmt_only', 'description', 'mode', + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit LAG choices to interfaces belonging to this device (or virtual chassis) + device = None + if self.is_bound and 'device' in self.data: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + pass + if device and device.virtual_chassis: + self.fields['lag'].queryset = Interface.objects.filter( + Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis), + type=InterfaceTypeChoices.TYPE_LAG + ) + self.fields['parent'].queryset = Interface.objects.filter( + Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis) + ) + elif device: + self.fields['lag'].queryset = Interface.objects.filter( + device=device, + type=InterfaceTypeChoices.TYPE_LAG + ) + self.fields['parent'].queryset = Interface.objects.filter(device=device) + else: + self.fields['lag'].queryset = Interface.objects.none() + self.fields['parent'].queryset = Interface.objects.none() + + def clean_enabled(self): + # Make sure enabled is True when it's not included in the uploaded data + if 'enabled' not in self.data: + return True + else: + return self.cleaned_data['enabled'] + + +class FrontPortCSVForm(CustomFieldModelCSVForm): + device = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name' + ) + rear_port = CSVModelChoiceField( + queryset=RearPort.objects.all(), + to_field_name='name', + help_text='Corresponding rear port' + ) + type = CSVChoiceField( + choices=PortTypeChoices, + help_text='Physical medium classification' + ) + + class Meta: + model = FrontPort + fields = ( + 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position', + 'description', + ) + help_texts = { + 'rear_port_position': 'Mapped position on corresponding rear port', + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit RearPort choices to those belonging to this device (or VC master) + if self.is_bound: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + try: + device = self.instance.device + except Device.DoesNotExist: + device = None + + if device: + self.fields['rear_port'].queryset = RearPort.objects.filter( + device__in=[device, device.get_vc_master()] + ) + else: + self.fields['rear_port'].queryset = RearPort.objects.none() + + +class RearPortCSVForm(CustomFieldModelCSVForm): + device = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name' + ) + type = CSVChoiceField( + help_text='Physical medium classification', + choices=PortTypeChoices, + ) + + class Meta: + model = RearPort + fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description') + help_texts = { + 'positions': 'Number of front ports which may be mapped' + } + + +class DeviceBayCSVForm(CustomFieldModelCSVForm): + device = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name' + ) + installed_device = CSVModelChoiceField( + queryset=Device.objects.all(), + required=False, + to_field_name='name', + help_text='Child device installed within this bay', + error_messages={ + 'invalid_choice': 'Child device not found.', + } + ) + + class Meta: + model = DeviceBay + fields = ('device', 'name', 'label', 'installed_device', 'description') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit installed device choices to devices of the correct type and location + if self.is_bound: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + try: + device = self.instance.device + except Device.DoesNotExist: + device = None + + if device: + self.fields['installed_device'].queryset = Device.objects.filter( + site=device.site, + rack=device.rack, + parent_bay__isnull=True, + device_type__u_height=0, + device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD + ).exclude(pk=device.pk) + else: + self.fields['installed_device'].queryset = Interface.objects.none() + + +class InventoryItemCSVForm(CustomFieldModelCSVForm): + device = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name' + ) + manufacturer = CSVModelChoiceField( + queryset=Manufacturer.objects.all(), + to_field_name='name', + required=False + ) + parent = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + required=False, + help_text='Parent inventory item' + ) + + class Meta: + model = InventoryItem + fields = ( + 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit parent choices to inventory items belonging to this device + device = None + if self.is_bound and 'device' in self.data: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + pass + if device: + self.fields['parent'].queryset = InventoryItem.objects.filter(device=device) + else: + self.fields['parent'].queryset = InventoryItem.objects.none() + + +class CableCSVForm(CustomFieldModelCSVForm): + # Termination A + side_a_device = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Side A device' + ) + side_a_type = CSVContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=CABLE_TERMINATION_MODELS, + help_text='Side A type' + ) + side_a_name = forms.CharField( + help_text='Side A component name' + ) + + # Termination B + side_b_device = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Side B device' + ) + side_b_type = CSVContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=CABLE_TERMINATION_MODELS, + help_text='Side B type' + ) + side_b_name = forms.CharField( + help_text='Side B component name' + ) + + # Cable attributes + status = CSVChoiceField( + choices=CableStatusChoices, + required=False, + help_text='Connection status' + ) + type = CSVChoiceField( + choices=CableTypeChoices, + required=False, + help_text='Physical medium classification' + ) + length_unit = CSVChoiceField( + choices=CableLengthUnitChoices, + required=False, + help_text='Length unit' + ) + + class Meta: + model = Cable + fields = [ + 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type', + 'status', 'label', 'color', 'length', 'length_unit', + ] + help_texts = { + 'color': mark_safe('RGB color in hexadecimal (e.g. 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 VirtualChassisCSVForm(CustomFieldModelCSVForm): + master = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + required=False, + help_text='Master device' + ) + + class Meta: + model = VirtualChassis + fields = ('name', 'domain', 'master') + + +class PowerPanelCSVForm(CustomFieldModelCSVForm): + site = CSVModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name', + help_text='Name of parent site' + ) + location = CSVModelChoiceField( + queryset=Location.objects.all(), + required=False, + to_field_name='name' + ) + + class Meta: + model = PowerPanel + fields = ('site', 'location', 'name') + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit group queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) + + +class PowerFeedCSVForm(CustomFieldModelCSVForm): + site = CSVModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name', + help_text='Assigned site' + ) + power_panel = CSVModelChoiceField( + queryset=PowerPanel.objects.all(), + to_field_name='name', + help_text='Upstream power panel' + ) + location = CSVModelChoiceField( + queryset=Location.objects.all(), + to_field_name='name', + required=False, + help_text="Rack's location (if any)" + ) + rack = CSVModelChoiceField( + queryset=Rack.objects.all(), + to_field_name='name', + required=False, + help_text='Rack' + ) + status = CSVChoiceField( + choices=PowerFeedStatusChoices, + required=False, + help_text='Operational status' + ) + type = CSVChoiceField( + choices=PowerFeedTypeChoices, + required=False, + help_text='Primary or redundant' + ) + supply = CSVChoiceField( + choices=PowerFeedSupplyChoices, + required=False, + help_text='Supply type (AC/DC)' + ) + phase = CSVChoiceField( + choices=PowerFeedPhaseChoices, + required=False, + help_text='Single or three-phase' + ) + + class Meta: + model = PowerFeed + fields = ( + 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', + 'voltage', 'amperage', 'max_utilization', 'comments', + ) + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit power_panel queryset by site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['power_panel'].queryset = self.fields['power_panel'].queryset.filter(**params) + + # Limit location queryset by site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) + + # Limit rack queryset by site and group + params = { + f"site__{self.fields['site'].to_field_name}": data.get('site'), + f"location__{self.fields['location'].to_field_name}": data.get('location'), + } + self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) diff --git a/netbox/dcim/forms/common.py b/netbox/dcim/forms/common.py new file mode 100644 index 000000000..f484b48e1 --- /dev/null +++ b/netbox/dcim/forms/common.py @@ -0,0 +1,49 @@ +from django import forms + +from dcim.choices import * +from dcim.constants import * + +__all__ = ( + 'InterfaceCommonForm', +) + + +class InterfaceCommonForm(forms.Form): + mac_address = forms.CharField( + empty_value=None, + required=False, + label='MAC address' + ) + mtu = forms.IntegerField( + required=False, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, + label='MTU' + ) + + def clean(self): + super().clean() + + parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine' + tagged_vlans = self.cleaned_data.get('tagged_vlans') + + # Untagged interfaces cannot be assigned tagged VLANs + if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans: + raise forms.ValidationError({ + 'mode': "An access interface cannot have tagged VLANs assigned." + }) + + # Remove all tagged VLAN assignments from "tagged all" interfaces + elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL: + self.cleaned_data['tagged_vlans'] = [] + + # Validate tagged VLANs; must be a global VLAN or in the same site + elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans: + valid_sites = [None, self.cleaned_data[parent_field].site] + invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites] + + if invalid_vlans: + raise forms.ValidationError({ + 'tagged_vlans': f"The tagged VLANs ({', '.join(invalid_vlans)}) must belong to the same site as " + f"the interface's parent device/VM, or they must be global" + }) diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py new file mode 100644 index 000000000..a2ceea6cf --- /dev/null +++ b/netbox/dcim/forms/connections.py @@ -0,0 +1,289 @@ +from circuits.models import Circuit, CircuitTermination, Provider +from dcim.models import * +from extras.forms import CustomFieldModelForm +from extras.models import Tag +from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect + +__all__ = ( + 'ConnectCableToCircuitTerminationForm', + 'ConnectCableToConsolePortForm', + 'ConnectCableToConsoleServerPortForm', + 'ConnectCableToFrontPortForm', + 'ConnectCableToInterfaceForm', + 'ConnectCableToPowerFeedForm', + 'ConnectCableToPowerPortForm', + 'ConnectCableToPowerOutletForm', + 'ConnectCableToRearPortForm', +) + + +class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm): + """ + Base form for connecting a Cable to a Device component + """ + termination_b_region = DynamicModelChoiceField( + queryset=Region.objects.all(), + label='Region', + required=False + ) + termination_b_site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + label='Site group', + required=False + ) + termination_b_site = DynamicModelChoiceField( + queryset=Site.objects.all(), + label='Site', + required=False, + query_params={ + 'region_id': '$termination_b_region', + 'group_id': '$termination_b_site_group', + } + ) + termination_b_location = DynamicModelChoiceField( + queryset=Location.objects.all(), + label='Location', + required=False, + null_option='None', + query_params={ + 'site_id': '$termination_b_site' + } + ) + termination_b_rack = DynamicModelChoiceField( + queryset=Rack.objects.all(), + label='Rack', + required=False, + null_option='None', + query_params={ + 'site_id': '$termination_b_site', + 'location_id': '$termination_b_location', + } + ) + termination_b_device = DynamicModelChoiceField( + queryset=Device.objects.all(), + label='Device', + required=False, + query_params={ + 'site_id': '$termination_b_site', + 'location_id': '$termination_b_location', + 'rack_id': '$termination_b_rack', + } + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = Cable + fields = [ + 'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device', + 'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', + ] + widgets = { + 'status': StaticSelect, + 'type': StaticSelect, + 'length_unit': StaticSelect, + } + + def clean_termination_b_id(self): + # Return the PK rather than the object + return getattr(self.cleaned_data['termination_b_id'], 'pk', None) + + +class ConnectCableToConsolePortForm(ConnectCableToDeviceForm): + termination_b_id = DynamicModelChoiceField( + queryset=ConsolePort.objects.all(), + label='Name', + disabled_indicator='_occupied', + query_params={ + 'device_id': '$termination_b_device' + } + ) + + +class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm): + termination_b_id = DynamicModelChoiceField( + queryset=ConsoleServerPort.objects.all(), + label='Name', + disabled_indicator='_occupied', + query_params={ + 'device_id': '$termination_b_device' + } + ) + + +class ConnectCableToPowerPortForm(ConnectCableToDeviceForm): + termination_b_id = DynamicModelChoiceField( + queryset=PowerPort.objects.all(), + label='Name', + disabled_indicator='_occupied', + query_params={ + 'device_id': '$termination_b_device' + } + ) + + +class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm): + termination_b_id = DynamicModelChoiceField( + queryset=PowerOutlet.objects.all(), + label='Name', + disabled_indicator='_occupied', + query_params={ + 'device_id': '$termination_b_device' + } + ) + + +class ConnectCableToInterfaceForm(ConnectCableToDeviceForm): + termination_b_id = DynamicModelChoiceField( + queryset=Interface.objects.all(), + label='Name', + disabled_indicator='_occupied', + query_params={ + 'device_id': '$termination_b_device', + 'kind': 'physical', + } + ) + + +class ConnectCableToFrontPortForm(ConnectCableToDeviceForm): + termination_b_id = DynamicModelChoiceField( + queryset=FrontPort.objects.all(), + label='Name', + disabled_indicator='_occupied', + query_params={ + 'device_id': '$termination_b_device' + } + ) + + +class ConnectCableToRearPortForm(ConnectCableToDeviceForm): + termination_b_id = DynamicModelChoiceField( + queryset=RearPort.objects.all(), + label='Name', + disabled_indicator='_occupied', + query_params={ + 'device_id': '$termination_b_device' + } + ) + + +class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm): + termination_b_provider = DynamicModelChoiceField( + queryset=Provider.objects.all(), + label='Provider', + required=False + ) + termination_b_region = DynamicModelChoiceField( + queryset=Region.objects.all(), + label='Region', + required=False + ) + termination_b_site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + label='Site group', + required=False + ) + termination_b_site = DynamicModelChoiceField( + queryset=Site.objects.all(), + label='Site', + required=False, + query_params={ + 'region_id': '$termination_b_region', + 'group_id': '$termination_b_site_group', + } + ) + termination_b_circuit = DynamicModelChoiceField( + queryset=Circuit.objects.all(), + label='Circuit', + query_params={ + 'provider_id': '$termination_b_provider', + 'site_id': '$termination_b_site', + } + ) + termination_b_id = DynamicModelChoiceField( + queryset=CircuitTermination.objects.all(), + label='Side', + disabled_indicator='_occupied', + query_params={ + 'circuit_id': '$termination_b_circuit' + } + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = Cable + fields = [ + 'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit', + 'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', + ] + + def clean_termination_b_id(self): + # Return the PK rather than the object + return getattr(self.cleaned_data['termination_b_id'], 'pk', None) + + +class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm): + termination_b_region = DynamicModelChoiceField( + queryset=Region.objects.all(), + label='Region', + required=False + ) + termination_b_site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + label='Site group', + required=False + ) + termination_b_site = DynamicModelChoiceField( + queryset=Site.objects.all(), + label='Site', + required=False, + query_params={ + 'region_id': '$termination_b_region', + 'group_id': '$termination_b_site_group', + } + ) + termination_b_location = DynamicModelChoiceField( + queryset=Location.objects.all(), + label='Location', + required=False, + query_params={ + 'site_id': '$termination_b_site' + } + ) + termination_b_powerpanel = DynamicModelChoiceField( + queryset=PowerPanel.objects.all(), + label='Power Panel', + required=False, + query_params={ + 'site_id': '$termination_b_site', + 'location_id': '$termination_b_location', + } + ) + termination_b_id = DynamicModelChoiceField( + queryset=PowerFeed.objects.all(), + label='Name', + disabled_indicator='_occupied', + query_params={ + 'power_panel_id': '$termination_b_powerpanel' + } + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = Cable + fields = [ + 'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label', + 'color', 'length', 'length_unit', 'tags', + ] + + def clean_termination_b_id(self): + # Return the PK rather than the object + return getattr(self.cleaned_data['termination_b_id'], 'pk', None) diff --git a/netbox/dcim/forms/fields.py b/netbox/dcim/forms/fields.py new file mode 100644 index 000000000..25a20667b --- /dev/null +++ b/netbox/dcim/forms/fields.py @@ -0,0 +1,25 @@ +from django import forms +from netaddr import EUI +from netaddr.core import AddrFormatError + +__all__ = ( + 'MACAddressField', +) + + +class MACAddressField(forms.Field): + widget = forms.CharField + default_error_messages = { + 'invalid': 'MAC address must be in EUI-48 format', + } + + def to_python(self, value): + value = super().to_python(value) + + # Validate MAC address format + try: + value = EUI(value.strip()) + except AddrFormatError: + raise forms.ValidationError(self.error_messages['invalid'], code='invalid') + + return value diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py new file mode 100644 index 000000000..95ff9aa3d --- /dev/null +++ b/netbox/dcim/forms/filtersets.py @@ -0,0 +1,1143 @@ +from django import forms +from django.contrib.auth.models import User +from django.utils.translation import gettext as _ + +from dcim.choices import * +from dcim.constants import * +from dcim.models import * +from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm +from tenancy.forms import TenancyFilterForm +from tenancy.models import Tenant +from utilities.forms import ( + APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect, + StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, +) + +__all__ = ( + 'CableFilterForm', + 'ConsoleConnectionFilterForm', + 'ConsolePortFilterForm', + 'ConsoleServerPortFilterForm', + 'DeviceBayFilterForm', + 'DeviceFilterForm', + 'DeviceRoleFilterForm', + 'DeviceTypeFilterForm', + 'FrontPortFilterForm', + 'InterfaceConnectionFilterForm', + 'InterfaceFilterForm', + 'InventoryItemFilterForm', + 'LocationFilterForm', + 'ManufacturerFilterForm', + 'PlatformFilterForm', + 'PowerConnectionFilterForm', + 'PowerFeedFilterForm', + 'PowerOutletFilterForm', + 'PowerPanelFilterForm', + 'PowerPortFilterForm', + 'RackFilterForm', + 'RackElevationFilterForm', + 'RackReservationFilterForm', + 'RackRoleFilterForm', + 'RearPortFilterForm', + 'RegionFilterForm', + 'SiteFilterForm', + 'SiteGroupFilterForm', + 'VirtualChassisFilterForm', +) + + +class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + field_order = [ + 'q', 'name', 'label', 'region_id', 'site_group_id', 'site_id', + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + name = forms.CharField( + required=False + ) + label = forms.CharField( + required=False + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id', + 'group_id': '$site_group_id', + }, + label=_('Site'), + fetch_trigger='open' + ) + location_id = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + query_params={ + 'site_id': '$site_id', + }, + label=_('Location'), + fetch_trigger='open' + ) + device_id = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + required=False, + query_params={ + 'site_id': '$site_id', + 'location_id': '$location_id', + }, + label=_('Device'), + fetch_trigger='open' + ) + + +class RegionFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = Region + field_groups = [ + ['q'], + ['parent_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + parent_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Parent region'), + fetch_trigger='open' + ) + + +class SiteGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = SiteGroup + field_groups = [ + ['q'], + ['parent_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + parent_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Parent group'), + fetch_trigger='open' + ) + + +class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = Site + field_order = ['q', 'status', 'region_id', 'tenant_group_id', 'tenant_id'] + field_groups = [ + ['q', 'tag'], + ['status', 'region_id', 'group_id'], + ['tenant_group_id', 'tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + status = forms.MultipleChoiceField( + choices=SiteStatusChoices, + required=False, + widget=StaticSelectMultiple(), + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = Location + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id', + 'group_id': '$site_group_id', + }, + label=_('Site'), + fetch_trigger='open' + ) + parent_id = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id', + 'site_id': '$site_id', + }, + label=_('Parent'), + fetch_trigger='open' + ) + + +class RackRoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = RackRole + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + +class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = Rack + field_order = ['q', 'region_id', 'site_id', 'location_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id'] + field_groups = [ + ['q', 'tag'], + ['region_id', 'site_id', 'location_id'], + ['status', 'role_id'], + ['type', 'width', 'serial', 'asset_tag'], + ['tenant_group_id', 'tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id' + }, + label=_('Site'), + fetch_trigger='open' + ) + location_id = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + label=_('Location'), + fetch_trigger='open' + ) + status = forms.MultipleChoiceField( + choices=RackStatusChoices, + required=False, + widget=StaticSelectMultiple() + ) + type = forms.MultipleChoiceField( + choices=RackTypeChoices, + required=False, + widget=StaticSelectMultiple() + ) + width = forms.MultipleChoiceField( + choices=RackWidthChoices, + required=False, + widget=StaticSelectMultiple() + ) + role_id = DynamicModelMultipleChoiceField( + queryset=RackRole.objects.all(), + required=False, + null_option='None', + label=_('Role'), + fetch_trigger='open' + ) + serial = forms.CharField( + required=False + ) + asset_tag = forms.CharField( + required=False + ) + tag = TagFilterField(model) + + +class RackElevationFilterForm(RackFilterForm): + field_order = [ + 'q', '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', + }, + fetch_trigger='open' + ) + + +class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = RackReservation + field_order = ['q', 'region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id'] + field_groups = [ + ['q', 'tag'], + ['user_id'], + ['region_id', 'site_id', 'location_id'], + ['tenant_group_id', 'tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id' + }, + label=_('Site'), + fetch_trigger='open' + ) + location_id = DynamicModelMultipleChoiceField( + queryset=Location.objects.prefetch_related('site'), + required=False, + label=_('Location'), + null_option='None', + fetch_trigger='open' + ) + user_id = DynamicModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, + label=_('User'), + widget=APISelectMultiple( + api_url='/api/users/users/', + ), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class ManufacturerFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = Manufacturer + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + +class DeviceTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = DeviceType + field_groups = [ + ['q', 'tag'], + ['manufacturer_id', 'subdevice_role'], + ['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + manufacturer_id = DynamicModelMultipleChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + label=_('Manufacturer'), + fetch_trigger='open' + ) + subdevice_role = forms.MultipleChoiceField( + choices=add_blank_choice(SubdeviceRoleChoices), + required=False, + widget=StaticSelectMultiple() + ) + console_ports = forms.NullBooleanField( + required=False, + label='Has console ports', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + console_server_ports = forms.NullBooleanField( + required=False, + label='Has console server ports', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + power_ports = forms.NullBooleanField( + required=False, + label='Has power ports', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + power_outlets = forms.NullBooleanField( + required=False, + label='Has power outlets', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + interfaces = forms.NullBooleanField( + required=False, + label='Has interfaces', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + pass_through_ports = forms.NullBooleanField( + required=False, + label='Has pass-through ports', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + tag = TagFilterField(model) + + +class DeviceRoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = DeviceRole + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + +class PlatformFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = Platform + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + manufacturer_id = DynamicModelMultipleChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + label=_('Manufacturer'), + fetch_trigger='open' + ) + + +class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm): + model = Device + field_order = [ + 'q', 'region_id', 'site_group_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 = [ + ['q', 'tag'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id'], + ['status', 'role_id', 'serial', 'asset_tag', 'mac_address'], + ['manufacturer_id', 'device_type_id', 'platform_id'], + ['tenant_group_id', 'tenant_id'], + [ + 'has_primary_ip', 'virtual_chassis_member', 'console_ports', 'console_server_ports', 'power_ports', + 'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data', + ], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id', + 'group_id': '$site_group_id', + }, + label=_('Site'), + fetch_trigger='open' + ) + location_id = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + label=_('Location'), + fetch_trigger='open' + ) + rack_id = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id', + 'location_id': '$location_id', + }, + label=_('Rack'), + fetch_trigger='open' + ) + role_id = DynamicModelMultipleChoiceField( + queryset=DeviceRole.objects.all(), + required=False, + label=_('Role'), + fetch_trigger='open' + ) + manufacturer_id = DynamicModelMultipleChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + label=_('Manufacturer'), + fetch_trigger='open' + ) + device_type_id = DynamicModelMultipleChoiceField( + queryset=DeviceType.objects.all(), + required=False, + query_params={ + 'manufacturer_id': '$manufacturer_id' + }, + label=_('Model'), + fetch_trigger='open' + ) + platform_id = DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), + required=False, + null_option='None', + label=_('Platform'), + fetch_trigger='open' + ) + status = forms.MultipleChoiceField( + choices=DeviceStatusChoices, + required=False, + widget=StaticSelectMultiple() + ) + serial = forms.CharField( + required=False + ) + 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=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + virtual_chassis_member = forms.NullBooleanField( + required=False, + label='Virtual chassis member', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + console_ports = forms.NullBooleanField( + required=False, + label='Has console ports', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + console_server_ports = forms.NullBooleanField( + required=False, + label='Has console server ports', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + power_ports = forms.NullBooleanField( + required=False, + label='Has power ports', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + power_outlets = forms.NullBooleanField( + required=False, + label='Has power outlets', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + interfaces = forms.NullBooleanField( + required=False, + label='Has interfaces', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + pass_through_ports = forms.NullBooleanField( + required=False, + label='Has pass-through ports', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + tag = TagFilterField(model) + + +class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = VirtualChassis + field_order = ['q', 'region_id', 'site_group_id', 'site_id', 'tenant_group_id', 'tenant_id'] + field_groups = [ + ['q', 'tag'], + ['region_id', 'site_group_id', 'site_id'], + ['tenant_group_id', 'tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id', + 'group_id': '$site_group_id', + }, + label=_('Site'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = Cable + field_groups = [ + ['q', 'tag'], + ['site_id', 'rack_id', 'device_id'], + ['type', 'status', 'color'], + ['tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id' + }, + label=_('Site'), + fetch_trigger='open' + ) + tenant_id = DynamicModelMultipleChoiceField( + queryset=Tenant.objects.all(), + required=False, + label=_('Tenant'), + fetch_trigger='open' + ) + rack_id = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + label=_('Rack'), + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + fetch_trigger='open' + ) + type = forms.MultipleChoiceField( + choices=add_blank_choice(CableTypeChoices), + required=False, + widget=StaticSelect() + ) + status = forms.ChoiceField( + required=False, + choices=add_blank_choice(CableStatusChoices), + widget=StaticSelect() + ) + 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'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = PowerPanel + field_groups = ( + ('q', 'tag'), + ('region_id', 'site_group_id', 'site_id', 'location_id') + ) + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id', + 'group_id': '$site_group_id', + }, + label=_('Site'), + fetch_trigger='open' + ) + location_id = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + label=_('Location'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = PowerFeed + field_groups = [ + ['q', 'tag'], + ['region_id', 'site_group_id', 'site_id'], + ['power_panel_id', 'rack_id'], + ['status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id' + }, + label=_('Site'), + fetch_trigger='open' + ) + power_panel_id = DynamicModelMultipleChoiceField( + queryset=PowerPanel.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + label=_('Power panel'), + fetch_trigger='open' + ) + rack_id = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + label=_('Rack'), + fetch_trigger='open' + ) + status = forms.MultipleChoiceField( + choices=PowerFeedStatusChoices, + required=False, + widget=StaticSelectMultiple() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerFeedTypeChoices), + required=False, + widget=StaticSelect() + ) + supply = forms.ChoiceField( + choices=add_blank_choice(PowerFeedSupplyChoices), + required=False, + widget=StaticSelect() + ) + phase = forms.ChoiceField( + choices=add_blank_choice(PowerFeedPhaseChoices), + required=False, + widget=StaticSelect() + ) + voltage = forms.IntegerField( + required=False + ) + amperage = forms.IntegerField( + required=False + ) + max_utilization = forms.IntegerField( + required=False + ) + tag = TagFilterField(model) + + +# +# Device components +# + +class ConsolePortFilterForm(DeviceComponentFilterForm): + model = ConsolePort + field_groups = [ + ['q', 'tag'], + ['name', 'label', 'type', 'speed'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ] + type = forms.MultipleChoiceField( + choices=ConsolePortTypeChoices, + required=False, + widget=StaticSelectMultiple() + ) + speed = forms.MultipleChoiceField( + choices=ConsolePortSpeedChoices, + required=False, + widget=StaticSelectMultiple() + ) + tag = TagFilterField(model) + + +class ConsoleServerPortFilterForm(DeviceComponentFilterForm): + model = ConsoleServerPort + field_groups = [ + ['q', 'tag'], + ['name', 'label', 'type', 'speed'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ] + type = forms.MultipleChoiceField( + choices=ConsolePortTypeChoices, + required=False, + widget=StaticSelectMultiple() + ) + speed = forms.MultipleChoiceField( + choices=ConsolePortSpeedChoices, + required=False, + widget=StaticSelectMultiple() + ) + tag = TagFilterField(model) + + +class PowerPortFilterForm(DeviceComponentFilterForm): + model = PowerPort + field_groups = [ + ['q', 'tag'], + ['name', 'label', 'type'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ] + type = forms.MultipleChoiceField( + choices=PowerPortTypeChoices, + required=False, + widget=StaticSelectMultiple() + ) + tag = TagFilterField(model) + + +class PowerOutletFilterForm(DeviceComponentFilterForm): + model = PowerOutlet + field_groups = [ + ['q', 'tag'], + ['name', 'label', 'type'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ] + type = forms.MultipleChoiceField( + choices=PowerOutletTypeChoices, + required=False, + widget=StaticSelectMultiple() + ) + tag = TagFilterField(model) + + +class InterfaceFilterForm(DeviceComponentFilterForm): + model = Interface + field_groups = [ + ['q', 'tag'], + ['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ] + type = forms.MultipleChoiceField( + choices=InterfaceTypeChoices, + required=False, + widget=StaticSelectMultiple() + ) + enabled = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + mgmt_only = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + mac_address = forms.CharField( + required=False, + label='MAC address' + ) + tag = TagFilterField(model) + + +class FrontPortFilterForm(DeviceComponentFilterForm): + field_groups = [ + ['q', 'tag'], + ['name', 'label', 'type', 'color'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ] + model = FrontPort + type = forms.MultipleChoiceField( + choices=PortTypeChoices, + required=False, + widget=StaticSelectMultiple() + ) + color = ColorField( + required=False + ) + tag = TagFilterField(model) + + +class RearPortFilterForm(DeviceComponentFilterForm): + model = RearPort + field_groups = [ + ['q', 'tag'], + ['name', 'label', 'type', 'color'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ] + type = forms.MultipleChoiceField( + choices=PortTypeChoices, + required=False, + widget=StaticSelectMultiple() + ) + color = ColorField( + required=False + ) + tag = TagFilterField(model) + + +class DeviceBayFilterForm(DeviceComponentFilterForm): + model = DeviceBay + field_groups = [ + ['q', 'tag'], + ['name', 'label'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ] + tag = TagFilterField(model) + + +class InventoryItemFilterForm(DeviceComponentFilterForm): + model = InventoryItem + field_groups = [ + ['q', 'tag'], + ['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ] + manufacturer_id = DynamicModelMultipleChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + label=_('Manufacturer'), + fetch_trigger='open' + ) + serial = forms.CharField( + required=False + ) + asset_tag = forms.CharField( + required=False + ) + discovered = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + tag = TagFilterField(model) + + +# +# Connections +# + +class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id' + }, + label=_('Site'), + fetch_trigger='open' + ) + device_id = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + required=False, + query_params={ + 'site_id': '$site_id' + }, + label=_('Device'), + fetch_trigger='open' + ) + + +class PowerConnectionFilterForm(BootstrapMixin, forms.Form): + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id' + }, + label=_('Site'), + fetch_trigger='open' + ) + device_id = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + required=False, + query_params={ + 'site_id': '$site_id' + }, + label=_('Device'), + fetch_trigger='open' + ) + + +class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region_id' + }, + label=_('Site'), + fetch_trigger='open' + ) + device_id = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + required=False, + query_params={ + 'site_id': '$site_id' + }, + label=_('Device'), + fetch_trigger='open' + ) diff --git a/netbox/dcim/forms/formsets.py b/netbox/dcim/forms/formsets.py new file mode 100644 index 000000000..6109a1575 --- /dev/null +++ b/netbox/dcim/forms/formsets.py @@ -0,0 +1,21 @@ +from django import forms + +__all__ = ( + 'BaseVCMemberFormSet', +) + + +class BaseVCMemberFormSet(forms.BaseModelFormSet): + + def clean(self): + super().clean() + + # Check for duplicate VC position values + vc_position_list = [] + for form in self.forms: + vc_position = form.cleaned_data.get('vc_position') + if vc_position: + if vc_position in vc_position_list: + error_msg = f"A virtual chassis member already exists in position {vc_position}." + form.add_error('vc_position', error_msg) + vc_position_list.append(vc_position) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py new file mode 100644 index 000000000..83fa00e33 --- /dev/null +++ b/netbox/dcim/forms/models.py @@ -0,0 +1,1232 @@ +from django import forms +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from timezone_field import TimeZoneFormField + +from dcim.choices import * +from dcim.constants import * +from dcim.models import * +from extras.forms import CustomFieldModelForm +from extras.models import Tag +from ipam.models import IPAddress, VLAN, VLANGroup +from tenancy.forms import TenancyForm +from utilities.forms import ( + APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, DynamicModelChoiceField, + DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea, + SlugField, StaticSelect, +) +from virtualization.models import Cluster, ClusterGroup +from .common import InterfaceCommonForm + +__all__ = ( + 'CableForm', + 'ConsolePortForm', + 'ConsolePortTemplateForm', + 'ConsoleServerPortForm', + 'ConsoleServerPortTemplateForm', + 'DeviceBayForm', + 'DeviceBayTemplateForm', + 'DeviceForm', + 'DeviceRoleForm', + 'DeviceTypeForm', + 'DeviceVCMembershipForm', + 'FrontPortForm', + 'FrontPortTemplateForm', + 'InterfaceForm', + 'InterfaceTemplateForm', + 'InventoryItemForm', + 'LocationForm', + 'ManufacturerForm', + 'PlatformForm', + 'PowerFeedForm', + 'PowerOutletForm', + 'PowerOutletTemplateForm', + 'PowerPanelForm', + 'PowerPortForm', + 'PowerPortTemplateForm', + 'RackForm', + 'RackReservationForm', + 'RackRoleForm', + 'RearPortForm', + 'RearPortTemplateForm', + 'RegionForm', + 'SiteForm', + 'SiteGroupForm', + 'VirtualChassisForm', +) + +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) +""" + + +class RegionForm(BootstrapMixin, CustomFieldModelForm): + parent = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False + ) + slug = SlugField() + + class Meta: + model = Region + fields = ( + 'parent', 'name', 'slug', 'description', + ) + + +class SiteGroupForm(BootstrapMixin, CustomFieldModelForm): + parent = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False + ) + slug = SlugField() + + class Meta: + model = SiteGroup + fields = ( + 'parent', 'name', 'slug', 'description', + ) + + +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=StaticSelect() + ) + 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': StaticSelect(), + 'time_zone': StaticSelect(), + } + 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 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 RackRoleForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + + class Meta: + model = RackRole + fields = [ + 'name', 'slug', 'color', 'description', + ] + + +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': StaticSelect(), + 'type': StaticSelect(), + 'width': StaticSelect(), + 'outer_unit': StaticSelect(), + } + + +class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + initial_params={ + 'sites': '$site' + }, + fetch_trigger='open' + ) + site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + initial_params={ + 'sites': '$site' + }, + fetch_trigger='open' + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region', + 'group_id': '$site_group', + }, + fetch_trigger='open' + ) + location = DynamicModelChoiceField( + queryset=Location.objects.all(), + required=False, + query_params={ + 'site_id': '$site' + }, + fetch_trigger='open' + ) + rack = DynamicModelChoiceField( + queryset=Rack.objects.all(), + query_params={ + 'site_id': '$site', + 'location_id': '$location', + }, + fetch_trigger='open' + ) + 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=StaticSelect() + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False, + fetch_trigger='open' + ) + + 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 ManufacturerForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + + class Meta: + model = Manufacturer + fields = [ + 'name', 'slug', 'description', + ] + + +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': StaticSelect(), + 'front_image': ClearableFileInput(attrs={ + 'accept': DEVICETYPE_IMAGE_FORMATS + }), + 'rear_image': ClearableFileInput(attrs={ + 'accept': DEVICETYPE_IMAGE_FORMATS + }) + } + + +class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + + class Meta: + model = DeviceRole + fields = [ + 'name', 'slug', 'color', 'vm_role', 'description', + ] + + +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 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': StaticSelect(), + 'status': StaticSelect(), + 'primary_ip4': StaticSelect(), + 'primary_ip6': StaticSelect(), + } + + 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 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': StaticSelect, + 'type': StaticSelect, + 'length_unit': StaticSelect, + } + error_messages = { + 'length': { + 'max_value': 'Maximum length is 32767 (any unit)' + } + } + + +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 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': StaticSelect(), + 'type': StaticSelect(), + 'supply': StaticSelect(), + 'phase': StaticSelect(), + } + + +# +# Virtual chassis +# + +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 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 + + +# +# Device component templates +# + + +class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): + + class Meta: + model = ConsolePortTemplate + fields = [ + 'device_type', 'name', 'label', 'type', 'description', + ] + widgets = { + 'device_type': forms.HiddenInput(), + } + + +class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): + + class Meta: + model = ConsoleServerPortTemplate + fields = [ + 'device_type', 'name', 'label', 'type', 'description', + ] + widgets = { + 'device_type': forms.HiddenInput(), + } + + +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 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 InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): + + class Meta: + model = InterfaceTemplate + fields = [ + 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', + ] + widgets = { + 'device_type': forms.HiddenInput(), + 'type': StaticSelect(), + } + + +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': StaticSelect(), + } + + 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 RearPortTemplateForm(BootstrapMixin, forms.ModelForm): + + class Meta: + model = RearPortTemplate + fields = [ + 'device_type', 'name', 'label', 'type', 'color', 'positions', 'description', + ] + widgets = { + 'device_type': forms.HiddenInput(), + 'type': StaticSelect(), + } + + +class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): + + class Meta: + model = DeviceBayTemplate + fields = [ + 'device_type', 'name', 'label', 'description', + ] + widgets = { + 'device_type': forms.HiddenInput(), + } + + +# +# Device components +# + +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 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 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 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 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', + } + ) + vlan_group = DynamicModelChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + label='VLAN group' + ) + untagged_vlan = DynamicModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + label='Untagged VLAN', + query_params={ + 'group_id': '$vlan_group', + } + ) + tagged_vlans = DynamicModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False, + label='Tagged VLANs', + query_params={ + 'group_id': '$vlan_group', + } + ) + 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': StaticSelect(), + 'mode': StaticSelect(), + } + 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 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': StaticSelect(), + 'rear_port': StaticSelect(), + } + + 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 + ) + + +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': StaticSelect(), + } + + +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 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=StaticSelect(), + ) + + def __init__(self, device_bay, *args, **kwargs): + + super().__init__(*args, **kwargs) + + self.fields['installed_device'].queryset = Device.objects.filter( + site=device_bay.device.site, + rack=device_bay.device.rack, + parent_bay__isnull=True, + device_type__u_height=0, + device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD + ).exclude(pk=device_bay.device.pk) + + +class InventoryItemForm(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', + ] diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py new file mode 100644 index 000000000..7577ad355 --- /dev/null +++ b/netbox/dcim/forms/object_create.py @@ -0,0 +1,614 @@ +from django import forms + +from dcim.choices import * +from dcim.constants import * +from dcim.models import * +from extras.forms import CustomFieldModelForm, CustomFieldsMixin +from extras.models import Tag +from ipam.models import VLAN +from utilities.forms import ( + add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + ExpandableNameField, StaticSelect, +) +from .common import InterfaceCommonForm + +__all__ = ( + 'ConsolePortCreateForm', + 'ConsolePortTemplateCreateForm', + 'ConsoleServerPortCreateForm', + 'ConsoleServerPortTemplateCreateForm', + 'DeviceBayCreateForm', + 'DeviceBayTemplateCreateForm', + 'FrontPortCreateForm', + 'FrontPortTemplateCreateForm', + 'InterfaceCreateForm', + 'InterfaceTemplateCreateForm', + 'InventoryItemCreateForm', + 'PowerOutletCreateForm', + 'PowerOutletTemplateCreateForm', + 'PowerPortCreateForm', + 'PowerPortTemplateCreateForm', + 'RearPortCreateForm', + 'RearPortTemplateCreateForm', + 'VirtualChassisCreateForm', +) + + +class ComponentForm(forms.Form): + """ + Subclass this form when facilitating the creation of one or more device component or component templates based on + a name pattern. + """ + name_pattern = ExpandableNameField( + label='Name' + ) + label_pattern = ExpandableNameField( + label='Label', + required=False, + help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)' + ) + + def clean(self): + super().clean() + + # Validate that the number of components being created from both the name_pattern and label_pattern are equal + if self.cleaned_data['label_pattern']: + name_pattern_count = len(self.cleaned_data['name_pattern']) + label_pattern_count = len(self.cleaned_data['label_pattern']) + if name_pattern_count != label_pattern_count: + raise forms.ValidationError({ + 'label_pattern': f'The provided name pattern will create {name_pattern_count} components, however ' + f'{label_pattern_count} labels will be generated. These counts must match.' + }, code='label_pattern_mismatch') + + +class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm): + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + initial_params={ + 'sites': '$site' + } + ) + site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + initial_params={ + 'sites': '$site' + } + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region', + 'group_id': '$site_group', + } + ) + rack = DynamicModelChoiceField( + queryset=Rack.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site' + } + ) + members = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + required=False, + query_params={ + 'site_id': '$site', + 'rack_id': '$rack', + } + ) + initial_position = forms.IntegerField( + initial=1, + required=False, + help_text='Position of the first member device. Increases by one for each additional member.' + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = VirtualChassis + fields = [ + 'name', 'domain', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags', + ] + + def save(self, *args, **kwargs): + instance = super().save(*args, **kwargs) + + # Assign VC members + if instance.pk: + initial_position = self.cleaned_data.get('initial_position') or 1 + for i, member in enumerate(self.cleaned_data['members'], start=initial_position): + member.virtual_chassis = instance + member.vc_position = i + member.save() + + return instance + + +# +# Component templates +# + +class ComponentTemplateCreateForm(BootstrapMixin, ComponentForm): + """ + Base form for the creation of device component templates (subclassed from ComponentTemplateModel). + """ + manufacturer = DynamicModelChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + initial_params={ + 'device_types': 'device_type' + } + ) + device_type = DynamicModelChoiceField( + queryset=DeviceType.objects.all(), + query_params={ + 'manufacturer_id': '$manufacturer' + } + ) + description = forms.CharField( + required=False + ) + + +class ConsolePortTemplateCreateForm(ComponentTemplateCreateForm): + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + widget=StaticSelect() + ) + field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description') + + +class ConsoleServerPortTemplateCreateForm(ComponentTemplateCreateForm): + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + widget=StaticSelect() + ) + field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description') + + +class PowerPortTemplateCreateForm(ComponentTemplateCreateForm): + type = forms.ChoiceField( + choices=add_blank_choice(PowerPortTypeChoices), + required=False + ) + maximum_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Maximum power draw (watts)" + ) + allocated_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Allocated power draw (watts)" + ) + field_order = ( + 'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', + 'description', + ) + + +class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm): + type = forms.ChoiceField( + choices=add_blank_choice(PowerOutletTypeChoices), + required=False + ) + power_port = forms.ModelChoiceField( + queryset=PowerPortTemplate.objects.all(), + required=False + ) + feed_leg = forms.ChoiceField( + choices=add_blank_choice(PowerOutletFeedLegChoices), + required=False, + widget=StaticSelect() + ) + field_order = ( + 'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg', + 'description', + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit power_port choices to current DeviceType + device_type = DeviceType.objects.get( + pk=self.initial.get('device_type') or self.data.get('device_type') + ) + self.fields['power_port'].queryset = PowerPortTemplate.objects.filter( + device_type=device_type + ) + + +class InterfaceTemplateCreateForm(ComponentTemplateCreateForm): + type = forms.ChoiceField( + choices=InterfaceTypeChoices, + widget=StaticSelect() + ) + mgmt_only = forms.BooleanField( + required=False, + label='Management only' + ) + field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only', 'description') + + +class FrontPortTemplateCreateForm(ComponentTemplateCreateForm): + type = forms.ChoiceField( + choices=PortTypeChoices, + widget=StaticSelect() + ) + color = ColorField( + required=False + ) + rear_port_set = forms.MultipleChoiceField( + choices=[], + label='Rear ports', + help_text='Select one rear port assignment for each front port being created.', + ) + field_order = ( + 'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'description', + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + device_type = DeviceType.objects.get( + pk=self.initial.get('device_type') or self.data.get('device_type') + ) + + # Determine which rear port positions are occupied. These will be excluded from the list of available mappings. + occupied_port_positions = [ + (front_port.rear_port_id, front_port.rear_port_position) + for front_port in device_type.frontporttemplates.all() + ] + + # Populate rear port choices + choices = [] + rear_ports = RearPortTemplate.objects.filter(device_type=device_type) + for rear_port in rear_ports: + for i in range(1, rear_port.positions + 1): + if (rear_port.pk, i) not in occupied_port_positions: + choices.append( + ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i)) + ) + self.fields['rear_port_set'].choices = choices + + def clean(self): + super().clean() + + # Validate that the number of ports being created equals the number of selected (rear port, position) tuples + front_port_count = len(self.cleaned_data['name_pattern']) + rear_port_count = len(self.cleaned_data['rear_port_set']) + if front_port_count != rear_port_count: + raise forms.ValidationError({ + 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments ' + 'were selected. These counts must match.'.format(front_port_count, rear_port_count) + }) + + def get_iterative_data(self, iteration): + + # Assign rear port and position from selected set + rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':') + + return { + 'rear_port': int(rear_port), + 'rear_port_position': int(position), + } + + +class RearPortTemplateCreateForm(ComponentTemplateCreateForm): + type = forms.ChoiceField( + choices=PortTypeChoices, + widget=StaticSelect(), + ) + color = ColorField( + required=False + ) + positions = forms.IntegerField( + min_value=REARPORT_POSITIONS_MIN, + max_value=REARPORT_POSITIONS_MAX, + initial=1, + help_text='The number of front ports which may be mapped to each rear port' + ) + field_order = ( + 'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'description', + ) + + +class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm): + field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description') + + +# +# Device components +# + +class ComponentCreateForm(BootstrapMixin, CustomFieldsMixin, ComponentForm): + """ + Base form for the creation of device components (models subclassed from ComponentModel). + """ + device = DynamicModelChoiceField( + queryset=Device.objects.all() + ) + description = forms.CharField( + max_length=200, + required=False + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + +class ConsolePortCreateForm(ComponentCreateForm): + model = ConsolePort + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect() + ) + speed = forms.ChoiceField( + choices=add_blank_choice(ConsolePortSpeedChoices), + required=False, + widget=StaticSelect() + ) + field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags') + + +class ConsoleServerPortCreateForm(ComponentCreateForm): + model = ConsoleServerPort + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect() + ) + speed = forms.ChoiceField( + choices=add_blank_choice(ConsolePortSpeedChoices), + required=False, + widget=StaticSelect() + ) + field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags') + + +class PowerPortCreateForm(ComponentCreateForm): + model = PowerPort + type = forms.ChoiceField( + choices=add_blank_choice(PowerPortTypeChoices), + required=False, + widget=StaticSelect() + ) + maximum_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Maximum draw in watts" + ) + allocated_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Allocated draw in watts" + ) + field_order = ( + 'device', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', + 'description', 'tags', + ) + + +class PowerOutletCreateForm(ComponentCreateForm): + model = PowerOutlet + type = forms.ChoiceField( + choices=add_blank_choice(PowerOutletTypeChoices), + required=False, + widget=StaticSelect() + ) + power_port = forms.ModelChoiceField( + queryset=PowerPort.objects.all(), + required=False + ) + feed_leg = forms.ChoiceField( + choices=add_blank_choice(PowerOutletFeedLegChoices), + required=False + ) + field_order = ( + 'device', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description', + 'tags', + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit power_port queryset to PowerPorts which belong to the parent Device + device = Device.objects.get( + pk=self.initial.get('device') or self.data.get('device') + ) + self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) + + +class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): + model = Interface + type = forms.ChoiceField( + choices=InterfaceTypeChoices, + widget=StaticSelect(), + ) + enabled = forms.BooleanField( + required=False, + initial=True + ) + parent = DynamicModelChoiceField( + queryset=Interface.objects.all(), + required=False, + query_params={ + 'device_id': '$device', + } + ) + lag = DynamicModelChoiceField( + queryset=Interface.objects.all(), + required=False, + query_params={ + 'device_id': '$device', + 'type': 'lag', + } + ) + mac_address = forms.CharField( + required=False, + label='MAC Address' + ) + mgmt_only = forms.BooleanField( + required=False, + label='Management only', + help_text='This interface is used only for out-of-band management' + ) + mode = forms.ChoiceField( + choices=add_blank_choice(InterfaceModeChoices), + required=False, + widget=StaticSelect(), + ) + untagged_vlan = DynamicModelChoiceField( + queryset=VLAN.objects.all(), + required=False + ) + tagged_vlans = DynamicModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False + ) + field_order = ( + 'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', + 'description', 'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags' + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit VLAN choices by device + device_id = self.initial.get('device') or self.data.get('device') + self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device_id) + self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device_id) + + +class FrontPortCreateForm(ComponentCreateForm): + model = FrontPort + type = forms.ChoiceField( + choices=PortTypeChoices, + widget=StaticSelect(), + ) + color = ColorField( + required=False + ) + rear_port_set = forms.MultipleChoiceField( + choices=[], + label='Rear ports', + help_text='Select one rear port assignment for each front port being created.', + ) + field_order = ( + 'device', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'mark_connected', 'description', + 'tags', + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + device = Device.objects.get( + pk=self.initial.get('device') or self.data.get('device') + ) + + # Determine which rear port positions are occupied. These will be excluded from the list of available + # mappings. + occupied_port_positions = [ + (front_port.rear_port_id, front_port.rear_port_position) + for front_port in device.frontports.all() + ] + + # Populate rear port choices + choices = [] + rear_ports = RearPort.objects.filter(device=device) + for rear_port in rear_ports: + for i in range(1, rear_port.positions + 1): + if (rear_port.pk, i) not in occupied_port_positions: + choices.append( + ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i)) + ) + self.fields['rear_port_set'].choices = choices + + def clean(self): + super().clean() + + # Validate that the number of ports being created equals the number of selected (rear port, position) tuples + front_port_count = len(self.cleaned_data['name_pattern']) + rear_port_count = len(self.cleaned_data['rear_port_set']) + if front_port_count != rear_port_count: + raise forms.ValidationError({ + 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments ' + 'were selected. These counts must match.'.format(front_port_count, rear_port_count) + }) + + def get_iterative_data(self, iteration): + + # Assign rear port and position from selected set + rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':') + + return { + 'rear_port': int(rear_port), + 'rear_port_position': int(position), + } + + +class RearPortCreateForm(ComponentCreateForm): + model = RearPort + type = forms.ChoiceField( + choices=PortTypeChoices, + widget=StaticSelect(), + ) + color = ColorField( + required=False + ) + positions = forms.IntegerField( + min_value=REARPORT_POSITIONS_MIN, + max_value=REARPORT_POSITIONS_MAX, + initial=1, + help_text='The number of front ports which may be mapped to each rear port' + ) + field_order = ( + 'device', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'mark_connected', 'description', + 'tags', + ) + + +class DeviceBayCreateForm(ComponentCreateForm): + model = DeviceBay + field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags') + + +class InventoryItemCreateForm(ComponentCreateForm): + model = InventoryItem + manufacturer = DynamicModelChoiceField( + queryset=Manufacturer.objects.all(), + required=False + ) + parent = DynamicModelChoiceField( + queryset=InventoryItem.objects.all(), + required=False, + query_params={ + 'device_id': '$device' + } + ) + part_id = forms.CharField( + max_length=50, + required=False, + label='Part ID' + ) + serial = forms.CharField( + max_length=50, + required=False, + ) + asset_tag = forms.CharField( + max_length=50, + required=False, + ) + field_order = ( + 'device', 'parent', 'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag', + 'description', 'tags', + ) diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py new file mode 100644 index 000000000..0596261a6 --- /dev/null +++ b/netbox/dcim/forms/object_import.py @@ -0,0 +1,148 @@ +from django import forms + +from dcim.choices import InterfaceTypeChoices, PortTypeChoices +from dcim.models import * +from utilities.forms import BootstrapMixin + +__all__ = ( + 'ConsolePortTemplateImportForm', + 'ConsoleServerPortTemplateImportForm', + 'DeviceBayTemplateImportForm', + 'DeviceTypeImportForm', + 'FrontPortTemplateImportForm', + 'InterfaceTemplateImportForm', + 'PowerOutletTemplateImportForm', + 'PowerPortTemplateImportForm', + 'RearPortTemplateImportForm', +) + + +class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm): + manufacturer = forms.ModelChoiceField( + queryset=Manufacturer.objects.all(), + to_field_name='name' + ) + + class Meta: + model = DeviceType + fields = [ + 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', + 'comments', + ] + + +# +# Component template import forms +# + +class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm): + + def __init__(self, device_type, data=None, *args, **kwargs): + + # Must pass the parent DeviceType on form initialization + data.update({ + 'device_type': device_type.pk, + }) + + super().__init__(data, *args, **kwargs) + + def clean_device_type(self): + + data = self.cleaned_data['device_type'] + + # Limit fields referencing other components to the parent DeviceType + for field_name, field in self.fields.items(): + if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type': + field.queryset = field.queryset.filter(device_type=data) + + return data + + +class ConsolePortTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = ConsolePortTemplate + fields = [ + 'device_type', 'name', 'label', 'type', 'description', + ] + + +class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = ConsoleServerPortTemplate + fields = [ + 'device_type', 'name', 'label', 'type', 'description', + ] + + +class PowerPortTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = PowerPortTemplate + fields = [ + 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', + ] + + +class PowerOutletTemplateImportForm(ComponentTemplateImportForm): + power_port = forms.ModelChoiceField( + queryset=PowerPortTemplate.objects.all(), + to_field_name='name', + required=False + ) + + class Meta: + model = PowerOutletTemplate + fields = [ + 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', + ] + + +class InterfaceTemplateImportForm(ComponentTemplateImportForm): + type = forms.ChoiceField( + choices=InterfaceTypeChoices.CHOICES + ) + + class Meta: + model = InterfaceTemplate + fields = [ + 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', + ] + + +class FrontPortTemplateImportForm(ComponentTemplateImportForm): + type = forms.ChoiceField( + choices=PortTypeChoices.CHOICES + ) + rear_port = forms.ModelChoiceField( + queryset=RearPortTemplate.objects.all(), + to_field_name='name' + ) + + class Meta: + model = FrontPortTemplate + fields = [ + 'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description', + ] + + +class RearPortTemplateImportForm(ComponentTemplateImportForm): + type = forms.ChoiceField( + choices=PortTypeChoices.CHOICES + ) + + class Meta: + model = RearPortTemplate + fields = [ + 'device_type', 'name', 'type', 'positions', 'label', 'description', + ] + + +class DeviceBayTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = DeviceBayTemplate + fields = [ + 'device_type', 'name', 'label', 'description', + ] diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 6eeffbc96..3b2a9eff0 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -1,5 +1,6 @@ from django.test import TestCase +from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices from dcim.forms import * from dcim.models import * from virtualization.models import Cluster, ClusterGroup, ClusterType diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index acdbfba65..a82d7dadf 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,11 +1,10 @@ -import logging from collections import OrderedDict from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.paginator import EmptyPage, PageNotAnInteger from django.db import transaction -from django.db.models import F, Prefetch +from django.db.models import Prefetch from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index bf5dec00c..74bf32e54 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -5,7 +5,8 @@ from django.utils.translation import gettext as _ from dcim.choices import InterfaceModeChoices from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN -from dcim.forms import InterfaceCommonForm, INTERFACE_MODE_HELP_TEXT +from dcim.forms.models import INTERFACE_MODE_HELP_TEXT +from dcim.forms.common import InterfaceCommonForm from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup from extras.forms import ( AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, @@ -569,7 +570,12 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldM ] -class VirtualMachineFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm): +class VirtualMachineFilterForm( + BootstrapMixin, + LocalConfigContextFilterForm, + TenancyFilterForm, + CustomFieldModelFilterForm +): model = VirtualMachine field_groups = [ ['q', 'tag'],