diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py deleted file mode 100644 index f43a3cfff..000000000 --- a/netbox/circuits/forms.py +++ /dev/null @@ -1,513 +0,0 @@ -from django import forms -from django.utils.translation import gettext as _ - -from dcim.models import Region, Site, SiteGroup -from extras.forms import ( - AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm, -) -from extras.models import Tag -from tenancy.forms import TenancyFilterForm, TenancyForm -from tenancy.models import Tenant -from utilities.forms import ( - add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, DatePicker, - DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SmallTextarea, SlugField, - StaticSelect, StaticSelectMultiple, TagFilterField, -) -from .choices import CircuitStatusChoices -from .models import * - - -# -# Providers -# - -class ProviderForm(BootstrapMixin, CustomFieldModelForm): - slug = SlugField() - comments = CommentField() - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = Provider - fields = [ - 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags', - ] - fieldsets = ( - ('Provider', ('name', 'slug', 'asn', 'tags')), - ('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')), - ) - widgets = { - 'noc_contact': SmallTextarea( - attrs={'rows': 5} - ), - 'admin_contact': SmallTextarea( - attrs={'rows': 5} - ), - } - help_texts = { - 'name': "Full name of the provider", - 'asn': "BGP autonomous system number (if applicable)", - 'portal_url': "URL of the provider's customer support portal", - 'noc_contact': "NOC email address and phone number", - 'admin_contact': "Administrative contact email address and phone number", - } - - -class ProviderCSVForm(CustomFieldModelCSVForm): - slug = SlugField() - - class Meta: - model = Provider - fields = ( - 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', - ) - - -class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Provider.objects.all(), - widget=forms.MultipleHiddenInput - ) - asn = forms.IntegerField( - required=False, - label='ASN' - ) - account = forms.CharField( - max_length=30, - required=False, - label='Account number' - ) - portal_url = forms.URLField( - required=False, - label='Portal' - ) - noc_contact = forms.CharField( - required=False, - widget=SmallTextarea, - label='NOC contact' - ) - admin_contact = forms.CharField( - required=False, - widget=SmallTextarea, - label='Admin contact' - ) - comments = CommentField( - widget=SmallTextarea, - label='Comments' - ) - - class Meta: - nullable_fields = [ - 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', - ] - - -class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = Provider - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id'], - ['asn'], - ] - 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', - 'site_group_id': '$site_group_id', - }, - label=_('Site'), - fetch_trigger='open' - ) - asn = forms.IntegerField( - required=False, - label=_('ASN') - ) - tag = TagFilterField(model) - - -# -# Provider networks -# - -class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm): - provider = DynamicModelChoiceField( - queryset=Provider.objects.all() - ) - comments = CommentField() - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = ProviderNetwork - fields = [ - 'provider', 'name', 'description', 'comments', 'tags', - ] - fieldsets = ( - ('Provider Network', ('provider', 'name', 'description', 'tags')), - ) - - -class ProviderNetworkCSVForm(CustomFieldModelCSVForm): - provider = CSVModelChoiceField( - queryset=Provider.objects.all(), - to_field_name='name', - help_text='Assigned provider' - ) - - class Meta: - model = ProviderNetwork - fields = [ - 'provider', 'name', 'description', 'comments', - ] - - -class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=ProviderNetwork.objects.all(), - widget=forms.MultipleHiddenInput - ) - provider = DynamicModelChoiceField( - queryset=Provider.objects.all(), - required=False - ) - description = forms.CharField( - max_length=100, - required=False - ) - comments = CommentField( - widget=SmallTextarea, - label='Comments' - ) - - class Meta: - nullable_fields = [ - 'description', 'comments', - ] - - -class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = ProviderNetwork - field_groups = ( - ('q', 'tag'), - ('provider_id',), - ) - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - provider_id = DynamicModelMultipleChoiceField( - queryset=Provider.objects.all(), - required=False, - label=_('Provider'), - fetch_trigger='open' - ) - tag = TagFilterField(model) - - -# -# Circuit types -# - -class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm): - slug = SlugField() - - class Meta: - model = CircuitType - fields = [ - 'name', 'slug', 'description', - ] - - -class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=CircuitType.objects.all(), - widget=forms.MultipleHiddenInput - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['description'] - - -class CircuitTypeCSVForm(CustomFieldModelCSVForm): - slug = SlugField() - - class Meta: - model = CircuitType - fields = ('name', 'slug', 'description') - help_texts = { - 'name': 'Name of circuit type', - } - - -class CircuitTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = CircuitType - field_groups = [ - ['q'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - - -# -# Circuits -# - -class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - provider = DynamicModelChoiceField( - queryset=Provider.objects.all() - ) - type = DynamicModelChoiceField( - queryset=CircuitType.objects.all() - ) - comments = CommentField() - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = Circuit - fields = [ - 'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant', - 'comments', 'tags', - ] - fieldsets = ( - ('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) - help_texts = { - 'cid': "Unique circuit ID", - 'commit_rate': "Committed rate", - } - widgets = { - 'status': StaticSelect(), - 'install_date': DatePicker(), - 'commit_rate': SelectSpeedWidget(), - } - - -class CircuitCSVForm(CustomFieldModelCSVForm): - provider = CSVModelChoiceField( - queryset=Provider.objects.all(), - to_field_name='name', - help_text='Assigned provider' - ) - type = CSVModelChoiceField( - queryset=CircuitType.objects.all(), - to_field_name='name', - help_text='Type of circuit' - ) - status = CSVChoiceField( - choices=CircuitStatusChoices, - required=False, - help_text='Operational status' - ) - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned tenant' - ) - - class Meta: - model = Circuit - fields = [ - 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', - ] - - -class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Circuit.objects.all(), - widget=forms.MultipleHiddenInput - ) - type = DynamicModelChoiceField( - queryset=CircuitType.objects.all(), - required=False - ) - provider = DynamicModelChoiceField( - queryset=Provider.objects.all(), - required=False - ) - status = forms.ChoiceField( - choices=add_blank_choice(CircuitStatusChoices), - required=False, - initial='', - widget=StaticSelect() - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - commit_rate = forms.IntegerField( - required=False, - label='Commit rate (Kbps)' - ) - description = forms.CharField( - max_length=100, - required=False - ) - comments = CommentField( - widget=SmallTextarea, - label='Comments' - ) - - class Meta: - nullable_fields = [ - 'tenant', 'commit_rate', 'description', 'comments', - ] - - -class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = Circuit - field_groups = [ - ['q', 'tag'], - ['provider_id', 'provider_network_id'], - ['type_id', 'status', 'commit_rate'], - ['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') - ) - type_id = DynamicModelMultipleChoiceField( - queryset=CircuitType.objects.all(), - required=False, - label=_('Type'), - fetch_trigger='open' - ) - provider_id = DynamicModelMultipleChoiceField( - queryset=Provider.objects.all(), - required=False, - label=_('Provider'), - fetch_trigger='open' - ) - provider_network_id = DynamicModelMultipleChoiceField( - queryset=ProviderNetwork.objects.all(), - required=False, - query_params={ - 'provider_id': '$provider_id' - }, - label=_('Provider network'), - fetch_trigger='open' - ) - status = forms.MultipleChoiceField( - choices=CircuitStatusChoices, - required=False, - widget=StaticSelectMultiple() - ) - 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', - 'site_group_id': '$site_group_id', - }, - label=_('Site'), - fetch_trigger='open' - ) - commit_rate = forms.IntegerField( - required=False, - min_value=0, - label=_('Commit rate (Kbps)') - ) - tag = TagFilterField(model) - - -# -# Circuit terminations -# - -class CircuitTerminationForm(BootstrapMixin, forms.ModelForm): - 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', - }, - required=False - ) - provider_network = DynamicModelChoiceField( - queryset=ProviderNetwork.objects.all(), - required=False - ) - - class Meta: - model = CircuitTermination - fields = [ - 'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected', 'port_speed', - 'upstream_speed', 'xconnect_id', 'pp_info', 'description', - ] - help_texts = { - 'port_speed': "Physical circuit speed", - 'xconnect_id': "ID of the local cross-connect", - 'pp_info': "Patch panel ID and port number(s)" - } - widgets = { - 'term_side': forms.HiddenInput(), - 'port_speed': SelectSpeedWidget(), - 'upstream_speed': SelectSpeedWidget(), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.fields['provider_network'].widget.add_query_param('provider_id', self.instance.circuit.provider_id) diff --git a/netbox/circuits/forms/__init__.py b/netbox/circuits/forms/__init__.py new file mode 100644 index 000000000..5c23f833a --- /dev/null +++ b/netbox/circuits/forms/__init__.py @@ -0,0 +1,4 @@ +from .bulk_edit import * +from .bulk_import import * +from .filtersets import * +from .models import * diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py new file mode 100644 index 000000000..638426a5e --- /dev/null +++ b/netbox/circuits/forms/bulk_edit.py @@ -0,0 +1,135 @@ +from django import forms + +from circuits.choices import CircuitStatusChoices +from circuits.models import * +from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm +from tenancy.models import Tenant +from utilities.forms import ( + add_blank_choice, BootstrapMixin, CommentField, DynamicModelChoiceField, SmallTextarea, StaticSelect, +) + +__all__ = ( + 'CircuitBulkEditForm', + 'CircuitTypeBulkEditForm', + 'ProviderBulkEditForm', + 'ProviderNetworkBulkEditForm', +) + + +class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Provider.objects.all(), + widget=forms.MultipleHiddenInput + ) + asn = forms.IntegerField( + required=False, + label='ASN' + ) + account = forms.CharField( + max_length=30, + required=False, + label='Account number' + ) + portal_url = forms.URLField( + required=False, + label='Portal' + ) + noc_contact = forms.CharField( + required=False, + widget=SmallTextarea, + label='NOC contact' + ) + admin_contact = forms.CharField( + required=False, + widget=SmallTextarea, + label='Admin contact' + ) + comments = CommentField( + widget=SmallTextarea, + label='Comments' + ) + + class Meta: + nullable_fields = [ + 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', + ] + + +class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ProviderNetwork.objects.all(), + widget=forms.MultipleHiddenInput + ) + provider = DynamicModelChoiceField( + queryset=Provider.objects.all(), + required=False + ) + description = forms.CharField( + max_length=100, + required=False + ) + comments = CommentField( + widget=SmallTextarea, + label='Comments' + ) + + class Meta: + nullable_fields = [ + 'description', 'comments', + ] + + +class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=CircuitType.objects.all(), + widget=forms.MultipleHiddenInput + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['description'] + + +class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Circuit.objects.all(), + widget=forms.MultipleHiddenInput + ) + type = DynamicModelChoiceField( + queryset=CircuitType.objects.all(), + required=False + ) + provider = DynamicModelChoiceField( + queryset=Provider.objects.all(), + required=False + ) + status = forms.ChoiceField( + choices=add_blank_choice(CircuitStatusChoices), + required=False, + initial='', + widget=StaticSelect() + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + commit_rate = forms.IntegerField( + required=False, + label='Commit rate (Kbps)' + ) + description = forms.CharField( + max_length=100, + required=False + ) + comments = CommentField( + widget=SmallTextarea, + label='Comments' + ) + + class Meta: + nullable_fields = [ + 'tenant', 'commit_rate', 'description', 'comments', + ] diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py new file mode 100644 index 000000000..41ee7281a --- /dev/null +++ b/netbox/circuits/forms/bulk_import.py @@ -0,0 +1,77 @@ +from circuits.choices import CircuitStatusChoices +from circuits.models import * +from extras.forms import CustomFieldModelCSVForm +from tenancy.models import Tenant +from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField + +__all__ = ( + 'CircuitCSVForm', + 'CircuitTypeCSVForm', + 'ProviderCSVForm', + 'ProviderNetworkCSVForm', +) + + +class ProviderCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + + class Meta: + model = Provider + fields = ( + 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', + ) + + +class ProviderNetworkCSVForm(CustomFieldModelCSVForm): + provider = CSVModelChoiceField( + queryset=Provider.objects.all(), + to_field_name='name', + help_text='Assigned provider' + ) + + class Meta: + model = ProviderNetwork + fields = [ + 'provider', 'name', 'description', 'comments', + ] + + +class CircuitTypeCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + + class Meta: + model = CircuitType + fields = ('name', 'slug', 'description') + help_texts = { + 'name': 'Name of circuit type', + } + + +class CircuitCSVForm(CustomFieldModelCSVForm): + provider = CSVModelChoiceField( + queryset=Provider.objects.all(), + to_field_name='name', + help_text='Assigned provider' + ) + type = CSVModelChoiceField( + queryset=CircuitType.objects.all(), + to_field_name='name', + help_text='Type of circuit' + ) + status = CSVChoiceField( + choices=CircuitStatusChoices, + required=False, + help_text='Operational status' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) + + class Meta: + model = Circuit + fields = [ + 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', + ] diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py new file mode 100644 index 000000000..63b654148 --- /dev/null +++ b/netbox/circuits/forms/filtersets.py @@ -0,0 +1,159 @@ +from django import forms +from django.utils.translation import gettext as _ + +from circuits.choices import CircuitStatusChoices +from circuits.models import * +from dcim.models import Region, Site, SiteGroup +from extras.forms import CustomFieldModelFilterForm +from tenancy.forms import TenancyFilterForm +from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField + +__all__ = ( + 'CircuitFilterForm', + 'CircuitTypeFilterForm', + 'ProviderFilterForm', + 'ProviderNetworkFilterForm', +) + + +class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = Provider + field_groups = [ + ['q', 'tag'], + ['region_id', 'site_group_id', 'site_id'], + ['asn'], + ] + 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', + 'site_group_id': '$site_group_id', + }, + label=_('Site'), + fetch_trigger='open' + ) + asn = forms.IntegerField( + required=False, + label=_('ASN') + ) + tag = TagFilterField(model) + + +class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = ProviderNetwork + field_groups = ( + ('q', 'tag'), + ('provider_id',), + ) + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + provider_id = DynamicModelMultipleChoiceField( + queryset=Provider.objects.all(), + required=False, + label=_('Provider'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class CircuitTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = CircuitType + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + +class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = Circuit + field_groups = [ + ['q', 'tag'], + ['provider_id', 'provider_network_id'], + ['type_id', 'status', 'commit_rate'], + ['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') + ) + type_id = DynamicModelMultipleChoiceField( + queryset=CircuitType.objects.all(), + required=False, + label=_('Type'), + fetch_trigger='open' + ) + provider_id = DynamicModelMultipleChoiceField( + queryset=Provider.objects.all(), + required=False, + label=_('Provider'), + fetch_trigger='open' + ) + provider_network_id = DynamicModelMultipleChoiceField( + queryset=ProviderNetwork.objects.all(), + required=False, + query_params={ + 'provider_id': '$provider_id' + }, + label=_('Provider network'), + fetch_trigger='open' + ) + status = forms.MultipleChoiceField( + choices=CircuitStatusChoices, + required=False, + widget=StaticSelectMultiple() + ) + 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', + 'site_group_id': '$site_group_id', + }, + label=_('Site'), + fetch_trigger='open' + ) + commit_rate = forms.IntegerField( + required=False, + min_value=0, + label=_('Commit rate (Kbps)') + ) + tag = TagFilterField(model) diff --git a/netbox/circuits/forms/models.py b/netbox/circuits/forms/models.py new file mode 100644 index 000000000..659939293 --- /dev/null +++ b/netbox/circuits/forms/models.py @@ -0,0 +1,168 @@ +from django import forms + +from circuits.models import * +from dcim.models import Region, Site, SiteGroup +from extras.forms import CustomFieldModelForm +from extras.models import Tag +from tenancy.forms import TenancyForm +from utilities.forms import ( + BootstrapMixin, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + SelectSpeedWidget, SmallTextarea, SlugField, StaticSelect, +) + +__all__ = ( + 'CircuitForm', + 'CircuitTerminationForm', + 'CircuitTypeForm', + 'ProviderForm', + 'ProviderNetworkForm', +) + + +class ProviderForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + comments = CommentField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = Provider + fields = [ + 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags', + ] + fieldsets = ( + ('Provider', ('name', 'slug', 'asn', 'tags')), + ('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')), + ) + widgets = { + 'noc_contact': SmallTextarea( + attrs={'rows': 5} + ), + 'admin_contact': SmallTextarea( + attrs={'rows': 5} + ), + } + help_texts = { + 'name': "Full name of the provider", + 'asn': "BGP autonomous system number (if applicable)", + 'portal_url': "URL of the provider's customer support portal", + 'noc_contact': "NOC email address and phone number", + 'admin_contact': "Administrative contact email address and phone number", + } + + +class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm): + provider = DynamicModelChoiceField( + queryset=Provider.objects.all() + ) + comments = CommentField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = ProviderNetwork + fields = [ + 'provider', 'name', 'description', 'comments', 'tags', + ] + fieldsets = ( + ('Provider Network', ('provider', 'name', 'description', 'tags')), + ) + + +class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + + class Meta: + model = CircuitType + fields = [ + 'name', 'slug', 'description', + ] + + +class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + provider = DynamicModelChoiceField( + queryset=Provider.objects.all() + ) + type = DynamicModelChoiceField( + queryset=CircuitType.objects.all() + ) + comments = CommentField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = Circuit + fields = [ + 'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant', + 'comments', 'tags', + ] + fieldsets = ( + ('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + help_texts = { + 'cid': "Unique circuit ID", + 'commit_rate': "Committed rate", + } + widgets = { + 'status': StaticSelect(), + 'install_date': DatePicker(), + 'commit_rate': SelectSpeedWidget(), + } + + +class CircuitTerminationForm(BootstrapMixin, forms.ModelForm): + 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', + }, + required=False + ) + provider_network = DynamicModelChoiceField( + queryset=ProviderNetwork.objects.all(), + required=False + ) + + class Meta: + model = CircuitTermination + fields = [ + 'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected', 'port_speed', + 'upstream_speed', 'xconnect_id', 'pp_info', 'description', + ] + help_texts = { + 'port_speed': "Physical circuit speed", + 'xconnect_id': "ID of the local cross-connect", + 'pp_info': "Patch panel ID and port number(s)" + } + widgets = { + 'term_side': forms.HiddenInput(), + 'port_speed': SelectSpeedWidget(), + 'upstream_speed': SelectSpeedWidget(), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields['provider_network'].widget.add_query_param('provider_id', self.instance.circuit.provider_id) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py deleted file mode 100644 index 50b960e9d..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-dynamic-params': '[{"fieldName":"face","queryParam":"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..8331cbb10 --- /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-dynamic-params': '[{"fieldName":"face","queryParam":"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/extras/forms.py b/netbox/extras/forms.py deleted file mode 100644 index fe98d9ca3..000000000 --- a/netbox/extras/forms.py +++ /dev/null @@ -1,988 +0,0 @@ -from django import forms -from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType -from django.contrib.postgres.forms import SimpleArrayField -from django.utils.safestring import mark_safe -from django.utils.translation import gettext as _ - -from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup -from tenancy.models import Tenant, TenantGroup -from utilities.forms import ( - add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, - CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, CSVContentTypeField, CSVModelForm, - CSVMultipleContentTypeField, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect, - StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, -) -from virtualization.models import Cluster, ClusterGroup -from .choices import * -from .models import * -from .utils import FeatureQuery - - -# -# Custom fields -# - -class CustomFieldForm(BootstrapMixin, forms.ModelForm): - content_types = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields') - ) - - class Meta: - model = CustomField - fields = '__all__' - fieldsets = ( - ('Custom Field', ('name', 'label', 'type', 'weight', 'required', 'description')), - ('Assigned Models', ('content_types',)), - ('Behavior', ('filter_logic',)), - ('Values', ('default', 'choices')), - ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')), - ) - - -class CustomFieldCSVForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - help_text="One or more assigned object types" - ) - choices = SimpleArrayField( - base_field=forms.CharField(), - required=False, - help_text='Comma-separated list of field choices' - ) - - class Meta: - model = CustomField - fields = ( - 'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default', - 'choices', 'weight', - ) - - -class CustomFieldBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=CustomField.objects.all(), - widget=forms.MultipleHiddenInput - ) - description = forms.CharField( - required=False - ) - required = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - weight = forms.IntegerField( - required=False - ) - - class Meta: - nullable_fields = [] - - -class CustomFieldFilterForm(BootstrapMixin, forms.Form): - field_groups = [ - ['q'], - ['type', 'content_types'], - ['weight', 'required'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - content_types = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - type = forms.MultipleChoiceField( - choices=CustomFieldTypeChoices, - required=False, - widget=StaticSelectMultiple(), - label=_('Field type') - ) - weight = forms.IntegerField( - required=False - ) - required = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - - -# -# Custom links -# - -class CustomLinkForm(BootstrapMixin, forms.ModelForm): - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_links') - ) - - class Meta: - model = CustomLink - fields = '__all__' - fieldsets = ( - ('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window')), - ('Templates', ('link_text', 'link_url')), - ) - widgets = { - 'link_text': forms.Textarea(attrs={'class': 'font-monospace'}), - 'link_url': forms.Textarea(attrs={'class': 'font-monospace'}), - } - help_texts = { - 'link_text': 'Jinja2 template code for the link text. Reference the object as {{ obj }}. ' - 'Links which render as empty text will not be displayed.', - 'link_url': 'Jinja2 template code for the link URL. Reference the object as {{ obj }}.', - } - - -class CustomLinkCSVForm(CSVModelForm): - content_type = CSVContentTypeField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_links'), - help_text="Assigned object type" - ) - - class Meta: - model = CustomLink - fields = ( - 'name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url', - ) - - -class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=CustomLink.objects.all(), - widget=forms.MultipleHiddenInput - ) - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - new_window = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - weight = forms.IntegerField( - required=False - ) - button_class = forms.ChoiceField( - choices=CustomLinkButtonClassChoices, - required=False, - widget=StaticSelect() - ) - - class Meta: - nullable_fields = [] - - -class CustomLinkFilterForm(BootstrapMixin, forms.Form): - field_groups = [ - ['q'], - ['content_type', 'weight', 'new_window'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - weight = forms.IntegerField( - required=False - ) - new_window = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - - -# -# Export templates -# - -class ExportTemplateForm(BootstrapMixin, forms.ModelForm): - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_links') - ) - - class Meta: - model = ExportTemplate - fields = '__all__' - fieldsets = ( - ('Custom Link', ('name', 'content_type', 'description')), - ('Template', ('template_code',)), - ('Rendering', ('mime_type', 'file_extension', 'as_attachment')), - ) - widgets = { - 'template_code': forms.Textarea(attrs={'class': 'font-monospace'}), - } - - -class ExportTemplateCSVForm(CSVModelForm): - content_type = CSVContentTypeField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('export_templates'), - help_text="Assigned object type" - ) - - class Meta: - model = ExportTemplate - fields = ( - 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code', - ) - - -class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=ExportTemplate.objects.all(), - widget=forms.MultipleHiddenInput - ) - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - description = forms.CharField( - max_length=200, - required=False - ) - mime_type = forms.CharField( - max_length=50, - required=False - ) - file_extension = forms.CharField( - max_length=15, - required=False - ) - as_attachment = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - - class Meta: - nullable_fields = ['description', 'mime_type', 'file_extension'] - - -class ExportTemplateFilterForm(BootstrapMixin, forms.Form): - field_groups = [ - ['q'], - ['content_type', 'mime_type', 'file_extension', 'as_attachment'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - mime_type = forms.CharField( - required=False, - label=_('MIME type') - ) - file_extension = forms.CharField( - required=False - ) - as_attachment = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - - -# -# Webhooks -# - -class WebhookForm(BootstrapMixin, forms.ModelForm): - content_types = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('webhooks') - ) - - class Meta: - model = Webhook - fields = '__all__' - fieldsets = ( - ('Webhook', ('name', 'enabled')), - ('Assigned Models', ('content_types',)), - ('Events', ('type_create', 'type_update', 'type_delete')), - ('HTTP Request', ( - 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', - )), - ('SSL', ('ssl_verification', 'ca_file_path')), - ) - widgets = { - 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}), - 'body_template': forms.Textarea(attrs={'class': 'font-monospace'}), - } - - -class WebhookCSVForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('webhooks'), - help_text="One or more assigned object types" - ) - - class Meta: - model = Webhook - fields = ( - 'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'payload_url', - 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification', - 'ca_file_path' - ) - - -class WebhookBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Webhook.objects.all(), - widget=forms.MultipleHiddenInput - ) - enabled = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - type_create = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - type_update = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - type_delete = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - http_method = forms.ChoiceField( - choices=WebhookHttpMethodChoices, - required=False - ) - payload_url = forms.CharField( - required=False - ) - ssl_verification = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - secret = forms.CharField( - required=False - ) - ca_file_path = forms.CharField( - required=False - ) - - class Meta: - nullable_fields = ['secret', 'ca_file_path'] - - -class WebhookFilterForm(BootstrapMixin, forms.Form): - field_groups = [ - ['q'], - ['content_types', 'http_method', 'enabled'], - ['type_create', 'type_update', 'type_delete'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - content_types = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - http_method = forms.MultipleChoiceField( - choices=WebhookHttpMethodChoices, - required=False, - widget=StaticSelectMultiple(), - label=_('HTTP method') - ) - enabled = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - type_create = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - type_update = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - type_delete = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - - -# -# Custom field models -# - -class CustomFieldsMixin: - """ - Extend a Form to include custom field support. - """ - def __init__(self, *args, **kwargs): - self.custom_fields = [] - - super().__init__(*args, **kwargs) - - self._append_customfield_fields() - - def _get_content_type(self): - """ - Return the ContentType of the form's model. - """ - if not hasattr(self, 'model'): - raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.") - return ContentType.objects.get_for_model(self.model) - - def _get_form_field(self, customfield): - return customfield.to_form_field() - - def _append_customfield_fields(self): - """ - Append form fields for all CustomFields assigned to this object type. - """ - content_type = self._get_content_type() - - # Append form fields; assign initial values if modifying and existing object - for customfield in CustomField.objects.filter(content_types=content_type): - field_name = f'cf_{customfield.name}' - self.fields[field_name] = self._get_form_field(customfield) - - # Annotate the field in the list of CustomField form fields - self.custom_fields.append(field_name) - - -class CustomFieldModelForm(CustomFieldsMixin, forms.ModelForm): - """ - Extend ModelForm to include custom field support. - """ - def _get_content_type(self): - return ContentType.objects.get_for_model(self._meta.model) - - def _get_form_field(self, customfield): - if self.instance.pk: - form_field = customfield.to_form_field(set_initial=False) - form_field.initial = self.instance.custom_field_data.get(customfield.name, None) - return form_field - - return customfield.to_form_field() - - def clean(self): - - # Save custom field data on instance - for cf_name in self.custom_fields: - key = cf_name[3:] # Strip "cf_" from field name - value = self.cleaned_data.get(cf_name) - empty_values = self.fields[cf_name].empty_values - # Convert "empty" values to null - self.instance.custom_field_data[key] = value if value not in empty_values else None - - return super().clean() - - -class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm): - - def _get_form_field(self, customfield): - return customfield.to_form_field(for_csv_import=True) - - -class CustomFieldModelBulkEditForm(BulkEditForm): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.custom_fields = [] - self.obj_type = ContentType.objects.get_for_model(self.model) - - # Add all applicable CustomFields to the form - custom_fields = CustomField.objects.filter(content_types=self.obj_type) - for cf in custom_fields: - # Annotate non-required custom fields as nullable - if not cf.required: - self.nullable_fields.append(cf.name) - self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False) - # Annotate this as a custom field - self.custom_fields.append(cf.name) - - -class CustomFieldModelFilterForm(forms.Form): - - def __init__(self, *args, **kwargs): - - self.obj_type = ContentType.objects.get_for_model(self.model) - - super().__init__(*args, **kwargs) - - # Add all applicable CustomFields to the form - self.custom_field_filters = [] - custom_fields = CustomField.objects.filter(content_types=self.obj_type).exclude( - filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED - ) - for cf in custom_fields: - field_name = 'cf_{}'.format(cf.name) - self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False) - self.custom_field_filters.append(field_name) - - -# -# Tags -# - -class TagForm(BootstrapMixin, forms.ModelForm): - slug = SlugField() - - class Meta: - model = Tag - fields = [ - 'name', 'slug', 'color', 'description' - ] - fieldsets = ( - ('Tag', ('name', 'slug', 'color', 'description')), - ) - - -class TagCSVForm(CSVModelForm): - slug = SlugField() - - class Meta: - model = Tag - fields = ('name', 'slug', 'color', 'description') - help_texts = { - 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), - } - - -class AddRemoveTagsForm(forms.Form): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Add add/remove tags fields - self.fields['add_tags'] = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - self.fields['remove_tags'] = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - -class TagFilterForm(BootstrapMixin, forms.Form): - model = Tag - q = forms.CharField( - required=False, - label=_('Search') - ) - content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()), - required=False, - label=_('Tagged object type') - ) - - -class TagBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Tag.objects.all(), - widget=forms.MultipleHiddenInput - ) - color = ColorField( - required=False - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['description'] - - -# -# Config contexts -# - -class ConfigContextForm(BootstrapMixin, forms.ModelForm): - regions = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False - ) - site_groups = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False - ) - sites = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False - ) - device_types = DynamicModelMultipleChoiceField( - queryset=DeviceType.objects.all(), - required=False - ) - roles = DynamicModelMultipleChoiceField( - queryset=DeviceRole.objects.all(), - required=False - ) - platforms = DynamicModelMultipleChoiceField( - queryset=Platform.objects.all(), - required=False - ) - cluster_groups = DynamicModelMultipleChoiceField( - queryset=ClusterGroup.objects.all(), - required=False - ) - clusters = DynamicModelMultipleChoiceField( - queryset=Cluster.objects.all(), - required=False - ) - tenant_groups = DynamicModelMultipleChoiceField( - queryset=TenantGroup.objects.all(), - required=False - ) - tenants = DynamicModelMultipleChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - data = JSONField( - label='' - ) - - class Meta: - model = ConfigContext - fields = ( - 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types', - 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', - ) - - -class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=ConfigContext.objects.all(), - widget=forms.MultipleHiddenInput - ) - weight = forms.IntegerField( - required=False, - min_value=0 - ) - is_active = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - description = forms.CharField( - required=False, - max_length=100 - ) - - class Meta: - nullable_fields = [ - 'description', - ] - - -class ConfigContextFilterForm(BootstrapMixin, forms.Form): - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id'], - ['device_type_id', 'platform_id', 'role_id'], - ['cluster_group_id', 'cluster_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=_('Regions'), - fetch_trigger='open' - ) - site_group_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site groups'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - label=_('Sites'), - fetch_trigger='open' - ) - device_type_id = DynamicModelMultipleChoiceField( - queryset=DeviceType.objects.all(), - required=False, - label=_('Device types'), - fetch_trigger='open' - ) - role_id = DynamicModelMultipleChoiceField( - queryset=DeviceRole.objects.all(), - required=False, - label=_('Roles'), - fetch_trigger='open' - ) - platform_id = DynamicModelMultipleChoiceField( - queryset=Platform.objects.all(), - required=False, - label=_('Platforms'), - fetch_trigger='open' - ) - cluster_group_id = DynamicModelMultipleChoiceField( - queryset=ClusterGroup.objects.all(), - required=False, - label=_('Cluster groups'), - fetch_trigger='open' - ) - cluster_id = DynamicModelMultipleChoiceField( - queryset=Cluster.objects.all(), - required=False, - label=_('Clusters'), - fetch_trigger='open' - ) - tenant_group_id = DynamicModelMultipleChoiceField( - queryset=TenantGroup.objects.all(), - required=False, - label=_('Tenant groups'), - fetch_trigger='open' - ) - tenant_id = DynamicModelMultipleChoiceField( - queryset=Tenant.objects.all(), - required=False, - label=_('Tenant'), - fetch_trigger='open' - ) - tag = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - to_field_name='slug', - required=False, - label=_('Tags'), - fetch_trigger='open' - ) - - -# -# Filter form for local config context data -# - -class LocalConfigContextFilterForm(forms.Form): - local_context_data = forms.NullBooleanField( - required=False, - label=_('Has local config context data'), - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - - -# -# Image attachments -# - -class ImageAttachmentForm(BootstrapMixin, forms.ModelForm): - - class Meta: - model = ImageAttachment - fields = [ - 'name', 'image', - ] - - -# -# Journal entries -# - -class JournalEntryForm(BootstrapMixin, forms.ModelForm): - comments = CommentField() - - kind = forms.ChoiceField( - choices=add_blank_choice(JournalEntryKindChoices), - required=False, - widget=StaticSelect() - ) - - class Meta: - model = JournalEntry - fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'comments'] - widgets = { - 'assigned_object_type': forms.HiddenInput, - 'assigned_object_id': forms.HiddenInput, - } - - -class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=JournalEntry.objects.all(), - widget=forms.MultipleHiddenInput - ) - kind = forms.ChoiceField( - choices=JournalEntryKindChoices, - required=False - ) - comments = forms.CharField( - required=False, - widget=forms.Textarea() - ) - - class Meta: - nullable_fields = [] - - -class JournalEntryFilterForm(BootstrapMixin, forms.Form): - model = JournalEntry - field_groups = [ - ['q'], - ['created_before', 'created_after', 'created_by_id'], - ['assigned_object_type_id', 'kind'] - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - created_after = forms.DateTimeField( - required=False, - label=_('After'), - widget=DateTimePicker() - ) - created_before = forms.DateTimeField( - required=False, - label=_('Before'), - widget=DateTimePicker() - ) - created_by_id = DynamicModelMultipleChoiceField( - queryset=User.objects.all(), - required=False, - label=_('User'), - widget=APISelectMultiple( - api_url='/api/users/users/', - ), - fetch_trigger='open' - ) - assigned_object_type_id = DynamicModelMultipleChoiceField( - queryset=ContentType.objects.all(), - required=False, - label=_('Object Type'), - widget=APISelectMultiple( - api_url='/api/extras/content-types/', - ), - fetch_trigger='open' - ) - kind = forms.ChoiceField( - choices=add_blank_choice(JournalEntryKindChoices), - required=False, - widget=StaticSelect() - ) - - -# -# Change logging -# - -class ObjectChangeFilterForm(BootstrapMixin, forms.Form): - model = ObjectChange - field_groups = [ - ['q'], - ['time_before', 'time_after', 'action'], - ['user_id', 'changed_object_type_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - time_after = forms.DateTimeField( - required=False, - label=_('After'), - widget=DateTimePicker() - ) - time_before = forms.DateTimeField( - required=False, - label=_('Before'), - widget=DateTimePicker() - ) - action = forms.ChoiceField( - choices=add_blank_choice(ObjectChangeActionChoices), - required=False, - widget=StaticSelect() - ) - user_id = DynamicModelMultipleChoiceField( - queryset=User.objects.all(), - required=False, - label=_('User'), - widget=APISelectMultiple( - api_url='/api/users/users/', - ), - fetch_trigger='open' - ) - changed_object_type_id = DynamicModelMultipleChoiceField( - queryset=ContentType.objects.all(), - required=False, - label=_('Object Type'), - widget=APISelectMultiple( - api_url='/api/extras/content-types/', - ), - fetch_trigger='open' - ) - - -# -# Scripts -# - -class ScriptForm(BootstrapMixin, forms.Form): - _commit = forms.BooleanField( - required=False, - initial=True, - label="Commit changes", - help_text="Commit changes to the database (uncheck for a dry-run)" - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Move _commit to the end of the form - commit = self.fields.pop('_commit') - self.fields['_commit'] = commit - - @property - def requires_input(self): - """ - A boolean indicating whether the form requires user input (ignore the _commit field). - """ - return bool(len(self.fields) > 1) diff --git a/netbox/extras/forms/__init__.py b/netbox/extras/forms/__init__.py new file mode 100644 index 000000000..1584e2f51 --- /dev/null +++ b/netbox/extras/forms/__init__.py @@ -0,0 +1,6 @@ +from .models import * +from .filtersets import * +from .bulk_edit import * +from .bulk_import import * +from .customfields import * +from .scripts import * diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py new file mode 100644 index 000000000..b85a74a5b --- /dev/null +++ b/netbox/extras/forms/bulk_edit.py @@ -0,0 +1,199 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType + +from extras.choices import * +from extras.models import * +from extras.utils import FeatureQuery +from utilities.forms import ( + BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect, +) + +__all__ = ( + 'ConfigContextBulkEditForm', + 'CustomFieldBulkEditForm', + 'CustomLinkBulkEditForm', + 'ExportTemplateBulkEditForm', + 'JournalEntryBulkEditForm', + 'TagBulkEditForm', + 'WebhookBulkEditForm', +) + + +class CustomFieldBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=CustomField.objects.all(), + widget=forms.MultipleHiddenInput + ) + description = forms.CharField( + required=False + ) + required = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + weight = forms.IntegerField( + required=False + ) + + class Meta: + nullable_fields = [] + + +class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=CustomLink.objects.all(), + widget=forms.MultipleHiddenInput + ) + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + new_window = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + weight = forms.IntegerField( + required=False + ) + button_class = forms.ChoiceField( + choices=CustomLinkButtonClassChoices, + required=False, + widget=StaticSelect() + ) + + class Meta: + nullable_fields = [] + + +class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ExportTemplate.objects.all(), + widget=forms.MultipleHiddenInput + ) + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + mime_type = forms.CharField( + max_length=50, + required=False + ) + file_extension = forms.CharField( + max_length=15, + required=False + ) + as_attachment = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + + class Meta: + nullable_fields = ['description', 'mime_type', 'file_extension'] + + +class WebhookBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Webhook.objects.all(), + widget=forms.MultipleHiddenInput + ) + enabled = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + type_create = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + type_update = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + type_delete = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + http_method = forms.ChoiceField( + choices=WebhookHttpMethodChoices, + required=False + ) + payload_url = forms.CharField( + required=False + ) + ssl_verification = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + secret = forms.CharField( + required=False + ) + ca_file_path = forms.CharField( + required=False + ) + + class Meta: + nullable_fields = ['secret', 'ca_file_path'] + + +class TagBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Tag.objects.all(), + widget=forms.MultipleHiddenInput + ) + color = ColorField( + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['description'] + + +class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ConfigContext.objects.all(), + widget=forms.MultipleHiddenInput + ) + weight = forms.IntegerField( + required=False, + min_value=0 + ) + is_active = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + description = forms.CharField( + required=False, + max_length=100 + ) + + class Meta: + nullable_fields = [ + 'description', + ] + + +class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=JournalEntry.objects.all(), + widget=forms.MultipleHiddenInput + ) + kind = forms.ChoiceField( + choices=JournalEntryKindChoices, + required=False + ) + comments = forms.CharField( + required=False, + widget=forms.Textarea() + ) + + class Meta: + nullable_fields = [] diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py new file mode 100644 index 000000000..fb8cf53e8 --- /dev/null +++ b/netbox/extras/forms/bulk_import.py @@ -0,0 +1,91 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.forms import SimpleArrayField +from django.utils.safestring import mark_safe + +from extras.models import * +from extras.utils import FeatureQuery +from utilities.forms import CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField + +__all__ = ( + 'CustomFieldCSVForm', + 'CustomLinkCSVForm', + 'ExportTemplateCSVForm', + 'TagCSVForm', + 'WebhookCSVForm', +) + + +class CustomFieldCSVForm(CSVModelForm): + content_types = CSVMultipleContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + help_text="One or more assigned object types" + ) + choices = SimpleArrayField( + base_field=forms.CharField(), + required=False, + help_text='Comma-separated list of field choices' + ) + + class Meta: + model = CustomField + fields = ( + 'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default', + 'choices', 'weight', + ) + + +class CustomLinkCSVForm(CSVModelForm): + content_type = CSVContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_links'), + help_text="Assigned object type" + ) + + class Meta: + model = CustomLink + fields = ( + 'name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url', + ) + + +class ExportTemplateCSVForm(CSVModelForm): + content_type = CSVContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('export_templates'), + help_text="Assigned object type" + ) + + class Meta: + model = ExportTemplate + fields = ( + 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code', + ) + + +class WebhookCSVForm(CSVModelForm): + content_types = CSVMultipleContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('webhooks'), + help_text="One or more assigned object types" + ) + + class Meta: + model = Webhook + fields = ( + 'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'payload_url', + 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification', + 'ca_file_path' + ) + + +class TagCSVForm(CSVModelForm): + slug = SlugField() + + class Meta: + model = Tag + fields = ('name', 'slug', 'color', 'description') + help_texts = { + 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), + } diff --git a/netbox/extras/forms/customfields.py b/netbox/extras/forms/customfields.py new file mode 100644 index 000000000..9f68467fa --- /dev/null +++ b/netbox/extras/forms/customfields.py @@ -0,0 +1,123 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType + +from extras.choices import * +from extras.models import * +from utilities.forms import BulkEditForm, CSVModelForm + +__all__ = ( + 'CustomFieldModelCSVForm', + 'CustomFieldModelBulkEditForm', + 'CustomFieldModelFilterForm', + 'CustomFieldModelForm', + 'CustomFieldsMixin', +) + + +class CustomFieldsMixin: + """ + Extend a Form to include custom field support. + """ + def __init__(self, *args, **kwargs): + self.custom_fields = [] + + super().__init__(*args, **kwargs) + + self._append_customfield_fields() + + def _get_content_type(self): + """ + Return the ContentType of the form's model. + """ + if not hasattr(self, 'model'): + raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.") + return ContentType.objects.get_for_model(self.model) + + def _get_form_field(self, customfield): + return customfield.to_form_field() + + def _append_customfield_fields(self): + """ + Append form fields for all CustomFields assigned to this object type. + """ + content_type = self._get_content_type() + + # Append form fields; assign initial values if modifying and existing object + for customfield in CustomField.objects.filter(content_types=content_type): + field_name = f'cf_{customfield.name}' + self.fields[field_name] = self._get_form_field(customfield) + + # Annotate the field in the list of CustomField form fields + self.custom_fields.append(field_name) + + +class CustomFieldModelForm(CustomFieldsMixin, forms.ModelForm): + """ + Extend ModelForm to include custom field support. + """ + def _get_content_type(self): + return ContentType.objects.get_for_model(self._meta.model) + + def _get_form_field(self, customfield): + if self.instance.pk: + form_field = customfield.to_form_field(set_initial=False) + form_field.initial = self.instance.custom_field_data.get(customfield.name, None) + return form_field + + return customfield.to_form_field() + + def clean(self): + + # Save custom field data on instance + for cf_name in self.custom_fields: + key = cf_name[3:] # Strip "cf_" from field name + value = self.cleaned_data.get(cf_name) + empty_values = self.fields[cf_name].empty_values + # Convert "empty" values to null + self.instance.custom_field_data[key] = value if value not in empty_values else None + + return super().clean() + + +class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm): + + def _get_form_field(self, customfield): + return customfield.to_form_field(for_csv_import=True) + + +class CustomFieldModelBulkEditForm(BulkEditForm): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.custom_fields = [] + self.obj_type = ContentType.objects.get_for_model(self.model) + + # Add all applicable CustomFields to the form + custom_fields = CustomField.objects.filter(content_types=self.obj_type) + for cf in custom_fields: + # Annotate non-required custom fields as nullable + if not cf.required: + self.nullable_fields.append(cf.name) + self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False) + # Annotate this as a custom field + self.custom_fields.append(cf.name) + + +class CustomFieldModelFilterForm(forms.Form): + + def __init__(self, *args, **kwargs): + + self.obj_type = ContentType.objects.get_for_model(self.model) + + super().__init__(*args, **kwargs) + + # Add all applicable CustomFields to the form + self.custom_field_filters = [] + custom_fields = CustomField.objects.filter(content_types=self.obj_type).exclude( + filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED + ) + for cf in custom_fields: + field_name = 'cf_{}'.format(cf.name) + self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False) + self.custom_field_filters.append(field_name) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py new file mode 100644 index 000000000..6196ba8da --- /dev/null +++ b/netbox/extras/forms/filtersets.py @@ -0,0 +1,364 @@ +from django import forms +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext as _ + +from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup +from extras.choices import * +from extras.models import * +from extras.utils import FeatureQuery +from tenancy.models import Tenant, TenantGroup +from utilities.forms import ( + add_blank_choice, APISelectMultiple, BootstrapMixin, ContentTypeChoiceField, + ContentTypeMultipleChoiceField, DateTimePicker, DynamicModelMultipleChoiceField, StaticSelect, + StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, +) +from virtualization.models import Cluster, ClusterGroup + +__all__ = ( + 'ConfigContextFilterForm', + 'CustomFieldFilterForm', + 'CustomLinkFilterForm', + 'ExportTemplateFilterForm', + 'JournalEntryFilterForm', + 'LocalConfigContextFilterForm', + 'ObjectChangeFilterForm', + 'TagFilterForm', + 'WebhookFilterForm', +) + + +class CustomFieldFilterForm(BootstrapMixin, forms.Form): + field_groups = [ + ['q'], + ['type', 'content_types'], + ['weight', 'required'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + content_types = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + type = forms.MultipleChoiceField( + choices=CustomFieldTypeChoices, + required=False, + widget=StaticSelectMultiple(), + label=_('Field type') + ) + weight = forms.IntegerField( + required=False + ) + required = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class CustomLinkFilterForm(BootstrapMixin, forms.Form): + field_groups = [ + ['q'], + ['content_type', 'weight', 'new_window'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + weight = forms.IntegerField( + required=False + ) + new_window = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class ExportTemplateFilterForm(BootstrapMixin, forms.Form): + field_groups = [ + ['q'], + ['content_type', 'mime_type', 'file_extension', 'as_attachment'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + mime_type = forms.CharField( + required=False, + label=_('MIME type') + ) + file_extension = forms.CharField( + required=False + ) + as_attachment = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class WebhookFilterForm(BootstrapMixin, forms.Form): + field_groups = [ + ['q'], + ['content_types', 'http_method', 'enabled'], + ['type_create', 'type_update', 'type_delete'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + content_types = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + http_method = forms.MultipleChoiceField( + choices=WebhookHttpMethodChoices, + required=False, + widget=StaticSelectMultiple(), + label=_('HTTP method') + ) + enabled = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + type_create = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + type_update = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + type_delete = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class TagFilterForm(BootstrapMixin, forms.Form): + model = Tag + q = forms.CharField( + required=False, + label=_('Search') + ) + content_type_id = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()), + required=False, + label=_('Tagged object type') + ) + + +class ConfigContextFilterForm(BootstrapMixin, forms.Form): + field_groups = [ + ['q', 'tag'], + ['region_id', 'site_group_id', 'site_id'], + ['device_type_id', 'platform_id', 'role_id'], + ['cluster_group_id', 'cluster_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=_('Regions'), + fetch_trigger='open' + ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site groups'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + label=_('Sites'), + fetch_trigger='open' + ) + device_type_id = DynamicModelMultipleChoiceField( + queryset=DeviceType.objects.all(), + required=False, + label=_('Device types'), + fetch_trigger='open' + ) + role_id = DynamicModelMultipleChoiceField( + queryset=DeviceRole.objects.all(), + required=False, + label=_('Roles'), + fetch_trigger='open' + ) + platform_id = DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), + required=False, + label=_('Platforms'), + fetch_trigger='open' + ) + cluster_group_id = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + label=_('Cluster groups'), + fetch_trigger='open' + ) + cluster_id = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_('Clusters'), + fetch_trigger='open' + ) + tenant_group_id = DynamicModelMultipleChoiceField( + queryset=TenantGroup.objects.all(), + required=False, + label=_('Tenant groups'), + fetch_trigger='open' + ) + tenant_id = DynamicModelMultipleChoiceField( + queryset=Tenant.objects.all(), + required=False, + label=_('Tenant'), + fetch_trigger='open' + ) + tag = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + to_field_name='slug', + required=False, + label=_('Tags'), + fetch_trigger='open' + ) + + +class LocalConfigContextFilterForm(forms.Form): + local_context_data = forms.NullBooleanField( + required=False, + label=_('Has local config context data'), + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class JournalEntryFilterForm(BootstrapMixin, forms.Form): + model = JournalEntry + field_groups = [ + ['q'], + ['created_before', 'created_after', 'created_by_id'], + ['assigned_object_type_id', 'kind'] + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + created_after = forms.DateTimeField( + required=False, + label=_('After'), + widget=DateTimePicker() + ) + created_before = forms.DateTimeField( + required=False, + label=_('Before'), + widget=DateTimePicker() + ) + created_by_id = DynamicModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, + label=_('User'), + widget=APISelectMultiple( + api_url='/api/users/users/', + ), + fetch_trigger='open' + ) + assigned_object_type_id = DynamicModelMultipleChoiceField( + queryset=ContentType.objects.all(), + required=False, + label=_('Object Type'), + widget=APISelectMultiple( + api_url='/api/extras/content-types/', + ), + fetch_trigger='open' + ) + kind = forms.ChoiceField( + choices=add_blank_choice(JournalEntryKindChoices), + required=False, + widget=StaticSelect() + ) + + +class ObjectChangeFilterForm(BootstrapMixin, forms.Form): + model = ObjectChange + field_groups = [ + ['q'], + ['time_before', 'time_after', 'action'], + ['user_id', 'changed_object_type_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + time_after = forms.DateTimeField( + required=False, + label=_('After'), + widget=DateTimePicker() + ) + time_before = forms.DateTimeField( + required=False, + label=_('Before'), + widget=DateTimePicker() + ) + action = forms.ChoiceField( + choices=add_blank_choice(ObjectChangeActionChoices), + required=False, + widget=StaticSelect() + ) + user_id = DynamicModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, + label=_('User'), + widget=APISelectMultiple( + api_url='/api/users/users/', + ), + fetch_trigger='open' + ) + changed_object_type_id = DynamicModelMultipleChoiceField( + queryset=ContentType.objects.all(), + required=False, + label=_('Object Type'), + widget=APISelectMultiple( + api_url='/api/extras/content-types/', + ), + fetch_trigger='open' + ) diff --git a/netbox/extras/forms/models.py b/netbox/extras/forms/models.py new file mode 100644 index 000000000..7e462e62b --- /dev/null +++ b/netbox/extras/forms/models.py @@ -0,0 +1,223 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType + +from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup +from extras.choices import * +from extras.models import * +from extras.utils import FeatureQuery +from tenancy.models import Tenant, TenantGroup +from utilities.forms import ( + add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, + ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect, +) +from virtualization.models import Cluster, ClusterGroup + +__all__ = ( + 'AddRemoveTagsForm', + 'ConfigContextForm', + 'CustomFieldForm', + 'CustomLinkForm', + 'ExportTemplateForm', + 'ImageAttachmentForm', + 'JournalEntryForm', + 'TagForm', + 'WebhookForm', +) + + +class CustomFieldForm(BootstrapMixin, forms.ModelForm): + content_types = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields') + ) + + class Meta: + model = CustomField + fields = '__all__' + fieldsets = ( + ('Custom Field', ('name', 'label', 'type', 'weight', 'required', 'description')), + ('Assigned Models', ('content_types',)), + ('Behavior', ('filter_logic',)), + ('Values', ('default', 'choices')), + ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')), + ) + + +class CustomLinkForm(BootstrapMixin, forms.ModelForm): + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_links') + ) + + class Meta: + model = CustomLink + fields = '__all__' + fieldsets = ( + ('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window')), + ('Templates', ('link_text', 'link_url')), + ) + widgets = { + 'link_text': forms.Textarea(attrs={'class': 'font-monospace'}), + 'link_url': forms.Textarea(attrs={'class': 'font-monospace'}), + } + help_texts = { + 'link_text': 'Jinja2 template code for the link text. Reference the object as {{ obj }}. ' + 'Links which render as empty text will not be displayed.', + 'link_url': 'Jinja2 template code for the link URL. Reference the object as {{ obj }}.', + } + + +class ExportTemplateForm(BootstrapMixin, forms.ModelForm): + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_links') + ) + + class Meta: + model = ExportTemplate + fields = '__all__' + fieldsets = ( + ('Custom Link', ('name', 'content_type', 'description')), + ('Template', ('template_code',)), + ('Rendering', ('mime_type', 'file_extension', 'as_attachment')), + ) + widgets = { + 'template_code': forms.Textarea(attrs={'class': 'font-monospace'}), + } + + +class WebhookForm(BootstrapMixin, forms.ModelForm): + content_types = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('webhooks') + ) + + class Meta: + model = Webhook + fields = '__all__' + fieldsets = ( + ('Webhook', ('name', 'enabled')), + ('Assigned Models', ('content_types',)), + ('Events', ('type_create', 'type_update', 'type_delete')), + ('HTTP Request', ( + 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', + )), + ('SSL', ('ssl_verification', 'ca_file_path')), + ) + widgets = { + 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}), + 'body_template': forms.Textarea(attrs={'class': 'font-monospace'}), + } + + +class TagForm(BootstrapMixin, forms.ModelForm): + slug = SlugField() + + class Meta: + model = Tag + fields = [ + 'name', 'slug', 'color', 'description' + ] + fieldsets = ( + ('Tag', ('name', 'slug', 'color', 'description')), + ) + + +class AddRemoveTagsForm(forms.Form): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Add add/remove tags fields + self.fields['add_tags'] = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + self.fields['remove_tags'] = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + +class ConfigContextForm(BootstrapMixin, forms.ModelForm): + regions = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False + ) + site_groups = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False + ) + sites = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False + ) + device_types = DynamicModelMultipleChoiceField( + queryset=DeviceType.objects.all(), + required=False + ) + roles = DynamicModelMultipleChoiceField( + queryset=DeviceRole.objects.all(), + required=False + ) + platforms = DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), + required=False + ) + cluster_groups = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False + ) + clusters = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False + ) + tenant_groups = DynamicModelMultipleChoiceField( + queryset=TenantGroup.objects.all(), + required=False + ) + tenants = DynamicModelMultipleChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + data = JSONField( + label='' + ) + + class Meta: + model = ConfigContext + fields = ( + 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types', + 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', + ) + + +class ImageAttachmentForm(BootstrapMixin, forms.ModelForm): + + class Meta: + model = ImageAttachment + fields = [ + 'name', 'image', + ] + + +class JournalEntryForm(BootstrapMixin, forms.ModelForm): + comments = CommentField() + + kind = forms.ChoiceField( + choices=add_blank_choice(JournalEntryKindChoices), + required=False, + widget=StaticSelect() + ) + + class Meta: + model = JournalEntry + fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'comments'] + widgets = { + 'assigned_object_type': forms.HiddenInput, + 'assigned_object_id': forms.HiddenInput, + } diff --git a/netbox/extras/forms/scripts.py b/netbox/extras/forms/scripts.py new file mode 100644 index 000000000..380b4364c --- /dev/null +++ b/netbox/extras/forms/scripts.py @@ -0,0 +1,30 @@ +from django import forms + +from utilities.forms import BootstrapMixin + +__all__ = ( + 'ScriptForm', +) + + +class ScriptForm(BootstrapMixin, forms.Form): + _commit = forms.BooleanField( + required=False, + initial=True, + label="Commit changes", + help_text="Commit changes to the database (uncheck for a dry-run)" + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Move _commit to the end of the form + commit = self.fields.pop('_commit') + self.fields['_commit'] = commit + + @property + def requires_input(self): + """ + A boolean indicating whether the form requires user input (ignore the _commit field). + """ + return bool(len(self.fields) > 1) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py deleted file mode 100644 index c72884b3c..000000000 --- a/netbox/ipam/forms.py +++ /dev/null @@ -1,1881 +0,0 @@ -from django import forms -from django.contrib.contenttypes.models import ContentType -from django.utils.translation import gettext as _ - -from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup -from extras.forms import ( - AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, - CustomFieldModelFilterForm, -) -from extras.models import Tag -from tenancy.forms import TenancyFilterForm, TenancyForm -from tenancy.models import Tenant -from utilities.forms import ( - add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, ContentTypeChoiceField, CSVChoiceField, - CSVContentTypeField, CSVModelChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - ExpandableIPAddressField, NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple, TagFilterField, - BOOLEAN_WITH_BLANK_CHOICES, -) -from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface -from .choices import * -from .constants import * -from .models import * - -PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([ - (i, i) for i in range(PREFIX_LENGTH_MIN, PREFIX_LENGTH_MAX + 1) -]) - -IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([ - (i, i) for i in range(IPADDRESS_MASK_LENGTH_MIN, IPADDRESS_MASK_LENGTH_MAX + 1) -]) - - -# -# VRFs -# - -class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - import_targets = DynamicModelMultipleChoiceField( - queryset=RouteTarget.objects.all(), - required=False - ) - export_targets = DynamicModelMultipleChoiceField( - queryset=RouteTarget.objects.all(), - required=False - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = VRF - fields = [ - 'name', 'rd', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tenant_group', 'tenant', - 'tags', - ] - fieldsets = ( - ('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')), - ('Route Targets', ('import_targets', 'export_targets')), - ('Tenancy', ('tenant_group', 'tenant')), - ) - labels = { - 'rd': "RD", - } - help_texts = { - 'rd': "Route distinguisher in any format", - } - - -class VRFCSVForm(CustomFieldModelCSVForm): - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned tenant' - ) - - class Meta: - model = VRF - fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description') - - -class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=VRF.objects.all(), - widget=forms.MultipleHiddenInput() - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - enforce_unique = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect(), - label='Enforce unique space' - ) - description = forms.CharField( - max_length=100, - required=False - ) - - class Meta: - nullable_fields = [ - 'tenant', 'description', - ] - - -class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = VRF - field_groups = [ - ['q', 'tag'], - ['import_target_id', 'export_target_id'], - ['tenant_group_id', 'tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - import_target_id = DynamicModelMultipleChoiceField( - queryset=RouteTarget.objects.all(), - required=False, - label=_('Import targets'), - fetch_trigger='open' - ) - export_target_id = DynamicModelMultipleChoiceField( - queryset=RouteTarget.objects.all(), - required=False, - label=_('Export targets'), - fetch_trigger='open' - ) - tag = TagFilterField(model) - - -# -# Route targets -# - -class RouteTargetForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = RouteTarget - fields = [ - 'name', 'description', 'tenant_group', 'tenant', 'tags', - ] - fieldsets = ( - ('Route Target', ('name', 'description', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) - - -class RouteTargetCSVForm(CustomFieldModelCSVForm): - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned tenant' - ) - - class Meta: - model = RouteTarget - fields = ('name', 'description', 'tenant') - - -class RouteTargetBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=RouteTarget.objects.all(), - widget=forms.MultipleHiddenInput() - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = [ - 'tenant', 'description', - ] - - -class RouteTargetFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = RouteTarget - field_groups = [ - ['q', 'tag'], - ['importing_vrf_id', 'exporting_vrf_id'], - ['tenant_group_id', 'tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - importing_vrf_id = DynamicModelMultipleChoiceField( - queryset=VRF.objects.all(), - required=False, - label=_('Imported by VRF'), - fetch_trigger='open' - ) - exporting_vrf_id = DynamicModelMultipleChoiceField( - queryset=VRF.objects.all(), - required=False, - label=_('Exported by VRF'), - fetch_trigger='open' - ) - tag = TagFilterField(model) - - -# -# RIRs -# - -class RIRForm(BootstrapMixin, CustomFieldModelForm): - slug = SlugField() - - class Meta: - model = RIR - fields = [ - 'name', 'slug', 'is_private', 'description', - ] - - -class RIRCSVForm(CustomFieldModelCSVForm): - slug = SlugField() - - class Meta: - model = RIR - fields = ('name', 'slug', 'is_private', 'description') - help_texts = { - 'name': 'RIR name', - } - - -class RIRBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=RIR.objects.all(), - widget=forms.MultipleHiddenInput - ) - is_private = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['is_private', 'description'] - - -class RIRFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = RIR - field_groups = [ - ['q'], - ['is_private'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - is_private = forms.NullBooleanField( - required=False, - label=_('Private'), - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - - -# -# Aggregates -# - -class AggregateForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - rir = DynamicModelChoiceField( - queryset=RIR.objects.all(), - label='RIR' - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = Aggregate - fields = [ - 'prefix', 'rir', 'date_added', 'description', 'tenant_group', 'tenant', 'tags', - ] - fieldsets = ( - ('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) - help_texts = { - 'prefix': "IPv4 or IPv6 network", - 'rir': "Regional Internet Registry responsible for this prefix", - } - widgets = { - 'date_added': DatePicker(), - } - - -class AggregateCSVForm(CustomFieldModelCSVForm): - rir = CSVModelChoiceField( - queryset=RIR.objects.all(), - to_field_name='name', - help_text='Assigned RIR' - ) - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned tenant' - ) - - class Meta: - model = Aggregate - fields = ('prefix', 'rir', 'tenant', 'date_added', 'description') - - -class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Aggregate.objects.all(), - widget=forms.MultipleHiddenInput() - ) - rir = DynamicModelChoiceField( - queryset=RIR.objects.all(), - required=False, - label='RIR' - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - date_added = forms.DateField( - required=False - ) - description = forms.CharField( - max_length=100, - required=False - ) - - class Meta: - nullable_fields = [ - 'date_added', 'description', - ] - widgets = { - 'date_added': DatePicker(), - } - - -class AggregateFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = Aggregate - field_groups = [ - ['q', 'tag'], - ['family', 'rir_id'], - ['tenant_group_id', 'tenant_id'] - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - family = forms.ChoiceField( - required=False, - choices=add_blank_choice(IPAddressFamilyChoices), - label=_('Address family'), - widget=StaticSelect() - ) - rir_id = DynamicModelMultipleChoiceField( - queryset=RIR.objects.all(), - required=False, - label=_('RIR'), - fetch_trigger='open' - ) - tag = TagFilterField(model) - - -# -# Roles -# - -class RoleForm(BootstrapMixin, CustomFieldModelForm): - slug = SlugField() - - class Meta: - model = Role - fields = [ - 'name', 'slug', 'weight', 'description', - ] - - -class RoleCSVForm(CustomFieldModelCSVForm): - slug = SlugField() - - class Meta: - model = Role - fields = ('name', 'slug', 'weight', 'description') - - -class RoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Role.objects.all(), - widget=forms.MultipleHiddenInput - ) - weight = forms.IntegerField( - required=False - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['description'] - - -class RoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = Role - field_groups = [ - ['q'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - - -# -# Prefixes -# - -class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - vrf = DynamicModelChoiceField( - queryset=VRF.objects.all(), - required=False, - label='VRF' - ) - 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, - null_option='None', - query_params={ - 'region_id': '$region', - 'group_id': '$site_group', - } - ) - vlan_group = DynamicModelChoiceField( - queryset=VLANGroup.objects.all(), - required=False, - label='VLAN group', - null_option='None', - query_params={ - 'site_id': '$site' - }, - initial_params={ - 'vlans': '$vlan' - } - ) - vlan = DynamicModelChoiceField( - queryset=VLAN.objects.all(), - required=False, - label='VLAN', - query_params={ - 'site_id': '$site', - 'group_id': '$vlan_group', - } - ) - role = DynamicModelChoiceField( - queryset=Role.objects.all(), - required=False - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = Prefix - fields = [ - 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', - 'tenant_group', 'tenant', 'tags', - ] - fieldsets = ( - ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')), - ('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')), - ('Tenancy', ('tenant_group', 'tenant')), - ) - widgets = { - 'status': StaticSelect(), - } - - -class PrefixCSVForm(CustomFieldModelCSVForm): - vrf = CSVModelChoiceField( - queryset=VRF.objects.all(), - to_field_name='name', - required=False, - help_text='Assigned VRF' - ) - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned tenant' - ) - site = CSVModelChoiceField( - queryset=Site.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned site' - ) - vlan_group = CSVModelChoiceField( - queryset=VLANGroup.objects.all(), - required=False, - to_field_name='name', - help_text="VLAN's group (if any)" - ) - vlan = CSVModelChoiceField( - queryset=VLAN.objects.all(), - required=False, - to_field_name='vid', - help_text="Assigned VLAN" - ) - status = CSVChoiceField( - choices=PrefixStatusChoices, - help_text='Operational status' - ) - role = CSVModelChoiceField( - queryset=Role.objects.all(), - required=False, - to_field_name='name', - help_text='Functional role' - ) - - class Meta: - model = Prefix - fields = ( - 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', - 'description', - ) - - def __init__(self, data=None, *args, **kwargs): - super().__init__(data, *args, **kwargs) - - if data: - - # Limit VLAN queryset by assigned site and/or group (if specified) - params = {} - if data.get('site'): - params[f"site__{self.fields['site'].to_field_name}"] = data.get('site') - if data.get('vlan_group'): - params[f"group__{self.fields['vlan_group'].to_field_name}"] = data.get('vlan_group') - if params: - self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params) - - -class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Prefix.objects.all(), - widget=forms.MultipleHiddenInput() - ) - region = DynamicModelChoiceField( - queryset=Region.objects.all(), - required=False - ) - site_group = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region', - 'group_id': '$site_group', - } - ) - vrf = DynamicModelChoiceField( - queryset=VRF.objects.all(), - required=False, - label='VRF' - ) - prefix_length = forms.IntegerField( - min_value=PREFIX_LENGTH_MIN, - max_value=PREFIX_LENGTH_MAX, - required=False - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - status = forms.ChoiceField( - choices=add_blank_choice(PrefixStatusChoices), - required=False, - widget=StaticSelect() - ) - role = DynamicModelChoiceField( - queryset=Role.objects.all(), - required=False - ) - is_pool = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect(), - label='Is a pool' - ) - mark_utilized = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect(), - label='Treat as 100% utilized' - ) - description = forms.CharField( - max_length=100, - required=False - ) - - class Meta: - nullable_fields = [ - 'site', 'vrf', 'tenant', 'role', 'description', - ] - - -class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = Prefix - field_groups = [ - ['q', 'tag'], - ['within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized'], - ['vrf_id', 'present_in_vrf_id'], - ['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') - ) - mask_length__lte = forms.IntegerField( - widget=forms.HiddenInput() - ) - within_include = forms.CharField( - required=False, - widget=forms.TextInput( - attrs={ - 'placeholder': 'Prefix', - } - ), - label=_('Search within') - ) - family = forms.ChoiceField( - required=False, - choices=add_blank_choice(IPAddressFamilyChoices), - label=_('Address family'), - widget=StaticSelect() - ) - mask_length = forms.MultipleChoiceField( - required=False, - choices=PREFIX_MASK_LENGTH_CHOICES, - label=_('Mask length'), - widget=StaticSelectMultiple() - ) - vrf_id = DynamicModelMultipleChoiceField( - queryset=VRF.objects.all(), - required=False, - label=_('Assigned VRF'), - null_option='Global', - fetch_trigger='open' - ) - present_in_vrf_id = DynamicModelChoiceField( - queryset=VRF.objects.all(), - required=False, - label=_('Present in VRF'), - fetch_trigger='open' - ) - status = forms.MultipleChoiceField( - choices=PrefixStatusChoices, - required=False, - widget=StaticSelectMultiple() - ) - 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, - null_option='None', - query_params={ - 'region_id': '$region_id' - }, - label=_('Site'), - fetch_trigger='open' - ) - role_id = DynamicModelMultipleChoiceField( - queryset=Role.objects.all(), - required=False, - null_option='None', - label=_('Role'), - fetch_trigger='open' - ) - is_pool = forms.NullBooleanField( - required=False, - label=_('Is a pool'), - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - mark_utilized = forms.NullBooleanField( - required=False, - label=_('Marked as 100% utilized'), - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - tag = TagFilterField(model) - - -# -# IP ranges -# - -class IPRangeForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - vrf = DynamicModelChoiceField( - queryset=VRF.objects.all(), - required=False, - label='VRF' - ) - role = DynamicModelChoiceField( - queryset=Role.objects.all(), - required=False - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = IPRange - fields = [ - 'vrf', 'start_address', 'end_address', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags', - ] - fieldsets = ( - ('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'description', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) - widgets = { - 'status': StaticSelect(), - } - - -class IPRangeCSVForm(CustomFieldModelCSVForm): - vrf = CSVModelChoiceField( - queryset=VRF.objects.all(), - to_field_name='name', - required=False, - help_text='Assigned VRF' - ) - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned tenant' - ) - status = CSVChoiceField( - choices=IPRangeStatusChoices, - help_text='Operational status' - ) - role = CSVModelChoiceField( - queryset=Role.objects.all(), - required=False, - to_field_name='name', - help_text='Functional role' - ) - - class Meta: - model = IPRange - fields = ( - 'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'description', - ) - - -class IPRangeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=IPRange.objects.all(), - widget=forms.MultipleHiddenInput() - ) - vrf = DynamicModelChoiceField( - queryset=VRF.objects.all(), - required=False, - label='VRF' - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - status = forms.ChoiceField( - choices=add_blank_choice(IPRangeStatusChoices), - required=False, - widget=StaticSelect() - ) - role = DynamicModelChoiceField( - queryset=Role.objects.all(), - required=False - ) - description = forms.CharField( - max_length=100, - required=False - ) - - class Meta: - nullable_fields = [ - 'vrf', 'tenant', 'role', 'description', - ] - - -class IPRangeFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = IPRange - field_groups = [ - ['q', 'tag'], - ['family', 'vrf_id', 'status', 'role_id'], - ['tenant_group_id', 'tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - family = forms.ChoiceField( - required=False, - choices=add_blank_choice(IPAddressFamilyChoices), - label=_('Address family'), - widget=StaticSelect() - ) - vrf_id = DynamicModelMultipleChoiceField( - queryset=VRF.objects.all(), - required=False, - label=_('Assigned VRF'), - null_option='Global', - fetch_trigger='open' - ) - status = forms.MultipleChoiceField( - choices=PrefixStatusChoices, - required=False, - widget=StaticSelectMultiple() - ) - role_id = DynamicModelMultipleChoiceField( - queryset=Role.objects.all(), - required=False, - null_option='None', - label=_('Role'), - fetch_trigger='open' - ) - tag = TagFilterField(model) - - -# -# IP addresses -# - -class IPAddressForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - device = DynamicModelChoiceField( - queryset=Device.objects.all(), - required=False, - initial_params={ - 'interfaces': '$interface' - } - ) - interface = DynamicModelChoiceField( - queryset=Interface.objects.all(), - required=False, - query_params={ - 'device_id': '$device' - } - ) - virtual_machine = DynamicModelChoiceField( - queryset=VirtualMachine.objects.all(), - required=False, - initial_params={ - 'interfaces': '$vminterface' - } - ) - vminterface = DynamicModelChoiceField( - queryset=VMInterface.objects.all(), - required=False, - label='Interface', - query_params={ - 'virtual_machine_id': '$virtual_machine' - } - ) - vrf = DynamicModelChoiceField( - queryset=VRF.objects.all(), - required=False, - label='VRF' - ) - nat_region = DynamicModelChoiceField( - queryset=Region.objects.all(), - required=False, - label='Region', - initial_params={ - 'sites': '$nat_site' - } - ) - nat_site_group = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label='Site group', - initial_params={ - 'sites': '$nat_site' - } - ) - nat_site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False, - label='Site', - query_params={ - 'region_id': '$nat_region', - 'group_id': '$nat_site_group', - } - ) - nat_rack = DynamicModelChoiceField( - queryset=Rack.objects.all(), - required=False, - label='Rack', - null_option='None', - query_params={ - 'site_id': '$site' - } - ) - nat_device = DynamicModelChoiceField( - queryset=Device.objects.all(), - required=False, - label='Device', - query_params={ - 'site_id': '$site', - 'rack_id': '$nat_rack', - } - ) - nat_cluster = DynamicModelChoiceField( - queryset=Cluster.objects.all(), - required=False, - label='Cluster' - ) - nat_virtual_machine = DynamicModelChoiceField( - queryset=VirtualMachine.objects.all(), - required=False, - label='Virtual Machine', - query_params={ - 'cluster_id': '$nat_cluster', - } - ) - nat_vrf = DynamicModelChoiceField( - queryset=VRF.objects.all(), - required=False, - label='VRF' - ) - nat_inside = DynamicModelChoiceField( - queryset=IPAddress.objects.all(), - required=False, - label='IP Address', - query_params={ - 'device_id': '$nat_device', - 'virtual_machine_id': '$nat_virtual_machine', - 'vrf_id': '$nat_vrf', - } - ) - primary_for_parent = forms.BooleanField( - required=False, - label='Make this the primary IP for the device/VM' - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = IPAddress - fields = [ - 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack', - 'nat_device', 'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', - 'tags', - ] - widgets = { - 'status': StaticSelect(), - 'role': StaticSelect(), - } - - def __init__(self, *args, **kwargs): - - # Initialize helper selectors - instance = kwargs.get('instance') - initial = kwargs.get('initial', {}).copy() - if instance: - if type(instance.assigned_object) is Interface: - initial['interface'] = instance.assigned_object - elif type(instance.assigned_object) is VMInterface: - initial['vminterface'] = instance.assigned_object - if instance.nat_inside: - nat_inside_parent = instance.nat_inside.assigned_object - if type(nat_inside_parent) is Interface: - initial['nat_site'] = nat_inside_parent.device.site.pk - if nat_inside_parent.device.rack: - initial['nat_rack'] = nat_inside_parent.device.rack.pk - initial['nat_device'] = nat_inside_parent.device.pk - elif type(nat_inside_parent) is VMInterface: - initial['nat_cluster'] = nat_inside_parent.virtual_machine.cluster.pk - initial['nat_virtual_machine'] = nat_inside_parent.virtual_machine.pk - kwargs['initial'] = initial - - super().__init__(*args, **kwargs) - - # Initialize primary_for_parent if IP address is already assigned - if self.instance.pk and self.instance.assigned_object: - parent = self.instance.assigned_object.parent_object - if ( - self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or - self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk - ): - self.initial['primary_for_parent'] = True - - def clean(self): - super().clean() - - # Cannot select both a device interface and a VM interface - if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'): - raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface") - self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface') - - # Primary IP assignment is only available if an interface has been assigned. - interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface') - if self.cleaned_data.get('primary_for_parent') and not interface: - self.add_error( - 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs." - ) - - def save(self, *args, **kwargs): - ipaddress = super().save(*args, **kwargs) - - # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine. - interface = self.instance.assigned_object - if interface: - parent = interface.parent_object - if self.cleaned_data['primary_for_parent']: - if ipaddress.address.version == 4: - parent.primary_ip4 = ipaddress - else: - parent.primary_ip6 = ipaddress - parent.save() - elif ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress: - parent.primary_ip4 = None - parent.save() - elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress: - parent.primary_ip6 = None - parent.save() - - return ipaddress - - -class IPAddressBulkCreateForm(BootstrapMixin, forms.Form): - pattern = ExpandableIPAddressField( - label='Address pattern' - ) - - -class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - vrf = DynamicModelChoiceField( - queryset=VRF.objects.all(), - required=False, - label='VRF' - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = IPAddress - fields = [ - 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags', - ] - widgets = { - 'status': StaticSelect(), - 'role': StaticSelect(), - } - - -class IPAddressCSVForm(CustomFieldModelCSVForm): - vrf = CSVModelChoiceField( - queryset=VRF.objects.all(), - to_field_name='name', - required=False, - help_text='Assigned VRF' - ) - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - to_field_name='name', - required=False, - help_text='Assigned tenant' - ) - status = CSVChoiceField( - choices=IPAddressStatusChoices, - required=False, - help_text='Operational status' - ) - role = CSVChoiceField( - choices=IPAddressRoleChoices, - required=False, - help_text='Functional role' - ) - device = CSVModelChoiceField( - queryset=Device.objects.all(), - required=False, - to_field_name='name', - help_text='Parent device of assigned interface (if any)' - ) - virtual_machine = CSVModelChoiceField( - queryset=VirtualMachine.objects.all(), - required=False, - to_field_name='name', - help_text='Parent VM of assigned interface (if any)' - ) - interface = CSVModelChoiceField( - queryset=Interface.objects.none(), # Can also refer to VMInterface - required=False, - to_field_name='name', - help_text='Assigned interface' - ) - is_primary = forms.BooleanField( - help_text='Make this the primary IP for the assigned device', - required=False - ) - - class Meta: - model = IPAddress - fields = [ - 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary', - 'dns_name', 'description', - ] - - def __init__(self, data=None, *args, **kwargs): - super().__init__(data, *args, **kwargs) - - if data: - - # Limit interface queryset by assigned device - if data.get('device'): - self.fields['interface'].queryset = Interface.objects.filter( - **{f"device__{self.fields['device'].to_field_name}": data['device']} - ) - - # Limit interface queryset by assigned device - elif data.get('virtual_machine'): - self.fields['interface'].queryset = VMInterface.objects.filter( - **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']} - ) - - def clean(self): - super().clean() - - device = self.cleaned_data.get('device') - virtual_machine = self.cleaned_data.get('virtual_machine') - is_primary = self.cleaned_data.get('is_primary') - - # Validate is_primary - if is_primary and not device and not virtual_machine: - raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP") - - def save(self, *args, **kwargs): - - # Set interface assignment - if self.cleaned_data['interface']: - self.instance.assigned_object = self.cleaned_data['interface'] - - ipaddress = super().save(*args, **kwargs) - - # Set as primary for device/VM - if self.cleaned_data['is_primary']: - parent = self.cleaned_data['device'] or self.cleaned_data['virtual_machine'] - if self.instance.address.version == 4: - parent.primary_ip4 = ipaddress - elif self.instance.address.version == 6: - parent.primary_ip6 = ipaddress - parent.save() - - return ipaddress - - -class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=IPAddress.objects.all(), - widget=forms.MultipleHiddenInput() - ) - vrf = DynamicModelChoiceField( - queryset=VRF.objects.all(), - required=False, - label='VRF' - ) - mask_length = forms.IntegerField( - min_value=IPADDRESS_MASK_LENGTH_MIN, - max_value=IPADDRESS_MASK_LENGTH_MAX, - required=False - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - status = forms.ChoiceField( - choices=add_blank_choice(IPAddressStatusChoices), - required=False, - widget=StaticSelect() - ) - role = forms.ChoiceField( - choices=add_blank_choice(IPAddressRoleChoices), - required=False, - widget=StaticSelect() - ) - dns_name = forms.CharField( - max_length=255, - required=False - ) - description = forms.CharField( - max_length=100, - required=False - ) - - class Meta: - nullable_fields = [ - 'vrf', 'role', 'tenant', 'dns_name', 'description', - ] - - -class IPAddressAssignForm(BootstrapMixin, forms.Form): - vrf_id = DynamicModelChoiceField( - queryset=VRF.objects.all(), - required=False, - label='VRF' - ) - q = forms.CharField( - required=False, - label='Search', - ) - - -class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = IPAddress - field_order = [ - 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'role', - 'assigned_to_interface', 'tenant_group_id', 'tenant_id', - ] - field_groups = [ - ['q', 'tag'], - ['parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface'], - ['vrf_id', 'present_in_vrf_id'], - ['tenant_group_id', 'tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - parent = forms.CharField( - required=False, - widget=forms.TextInput( - attrs={ - 'placeholder': 'Prefix', - } - ), - label='Parent Prefix' - ) - family = forms.ChoiceField( - required=False, - choices=add_blank_choice(IPAddressFamilyChoices), - label=_('Address family'), - widget=StaticSelect() - ) - mask_length = forms.ChoiceField( - required=False, - choices=IPADDRESS_MASK_LENGTH_CHOICES, - label=_('Mask length'), - widget=StaticSelect() - ) - vrf_id = DynamicModelMultipleChoiceField( - queryset=VRF.objects.all(), - required=False, - label=_('Assigned VRF'), - null_option='Global', - fetch_trigger='open' - ) - present_in_vrf_id = DynamicModelChoiceField( - queryset=VRF.objects.all(), - required=False, - label=_('Present in VRF'), - fetch_trigger='open' - ) - status = forms.MultipleChoiceField( - choices=IPAddressStatusChoices, - required=False, - widget=StaticSelectMultiple() - ) - role = forms.MultipleChoiceField( - choices=IPAddressRoleChoices, - required=False, - widget=StaticSelectMultiple() - ) - assigned_to_interface = forms.NullBooleanField( - required=False, - label=_('Assigned to an interface'), - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - tag = TagFilterField(model) - - -# -# VLAN groups -# - -class VLANGroupForm(BootstrapMixin, CustomFieldModelForm): - scope_type = ContentTypeChoiceField( - queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), - required=False, - widget=StaticSelect - ) - region = DynamicModelChoiceField( - queryset=Region.objects.all(), - required=False, - initial_params={ - 'sites': '$site' - } - ) - sitegroup = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - initial_params={ - 'sites': '$site' - }, - label='Site group' - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False, - initial_params={ - 'locations': '$location' - }, - query_params={ - 'region_id': '$region', - 'group_id': '$sitegroup', - } - ) - location = DynamicModelChoiceField( - queryset=Location.objects.all(), - required=False, - initial_params={ - 'racks': '$rack' - }, - query_params={ - 'site_id': '$site', - } - ) - rack = DynamicModelChoiceField( - queryset=Rack.objects.all(), - required=False, - query_params={ - 'site_id': '$site', - 'location_id': '$location', - } - ) - clustergroup = DynamicModelChoiceField( - queryset=ClusterGroup.objects.all(), - required=False, - initial_params={ - 'clusters': '$cluster' - }, - label='Cluster group' - ) - cluster = DynamicModelChoiceField( - queryset=Cluster.objects.all(), - required=False, - query_params={ - 'group_id': '$clustergroup', - } - ) - slug = SlugField() - - class Meta: - model = VLANGroup - fields = [ - 'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', - 'clustergroup', 'cluster', - ] - fieldsets = ( - ('VLAN Group', ('name', 'slug', 'description')), - ('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')), - ) - widgets = { - 'scope_type': StaticSelect, - } - - def __init__(self, *args, **kwargs): - instance = kwargs.get('instance') - initial = kwargs.get('initial', {}) - - if instance is not None and instance.scope: - initial[instance.scope_type.model] = instance.scope - - kwargs['initial'] = initial - - super().__init__(*args, **kwargs) - - def clean(self): - super().clean() - - # Assign scope based on scope_type - if self.cleaned_data.get('scope_type'): - scope_field = self.cleaned_data['scope_type'].model - self.instance.scope = self.cleaned_data.get(scope_field) - else: - self.instance.scope_id = None - - -class VLANGroupCSVForm(CustomFieldModelCSVForm): - slug = SlugField() - scope_type = CSVContentTypeField( - queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), - required=False, - label='Scope type (app & model)' - ) - - class Meta: - model = VLANGroup - fields = ('name', 'slug', 'scope_type', 'scope_id', 'description') - labels = { - 'scope_id': 'Scope ID', - } - - -class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=VLANGroup.objects.all(), - widget=forms.MultipleHiddenInput - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['site', 'description'] - - -class VLANGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - field_groups = [ - ['q'], - ['region', 'sitegroup', 'site', 'location', 'rack'] - ] - model = VLANGroup - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - region = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - sitegroup = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site group'), - fetch_trigger='open' - ) - site = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - label=_('Site'), - fetch_trigger='open' - ) - location = DynamicModelMultipleChoiceField( - queryset=Location.objects.all(), - required=False, - label=_('Location'), - fetch_trigger='open' - ) - rack = DynamicModelMultipleChoiceField( - queryset=Rack.objects.all(), - required=False, - label=_('Rack'), - fetch_trigger='open' - ) - - -# -# VLANs -# - -class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - # VLANGroup assignment fields - scope_type = forms.ChoiceField( - choices=( - ('', ''), - ('dcim.region', 'Region'), - ('dcim.sitegroup', 'Site group'), - ('dcim.site', 'Site'), - ('dcim.location', 'Location'), - ('dcim.rack', 'Rack'), - ('virtualization.clustergroup', 'Cluster group'), - ('virtualization.cluster', 'Cluster'), - ), - required=False, - widget=StaticSelect, - label='Group scope' - ) - group = DynamicModelChoiceField( - queryset=VLANGroup.objects.all(), - required=False, - query_params={ - 'scope_type': '$scope_type', - }, - label='VLAN Group' - ) - - # Site assignment fields - region = DynamicModelChoiceField( - queryset=Region.objects.all(), - required=False, - initial_params={ - 'sites': '$site' - }, - label='Region' - ) - sitegroup = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - initial_params={ - 'sites': '$site' - }, - label='Site group' - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False, - null_option='None', - query_params={ - 'region_id': '$region', - 'group_id': '$sitegroup', - } - ) - - # Other fields - role = DynamicModelChoiceField( - queryset=Role.objects.all(), - required=False - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = VLAN - fields = [ - 'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags', - ] - help_texts = { - 'site': "Leave blank if this VLAN spans multiple sites", - 'group': "VLAN group (optional)", - 'vid': "Configured VLAN ID", - 'name': "Configured VLAN name", - 'status': "Operational status of this VLAN", - 'role': "The primary function of this VLAN", - } - widgets = { - 'status': StaticSelect(), - } - - -class VLANCSVForm(CustomFieldModelCSVForm): - site = CSVModelChoiceField( - queryset=Site.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned site' - ) - group = CSVModelChoiceField( - queryset=VLANGroup.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned VLAN group' - ) - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - to_field_name='name', - required=False, - help_text='Assigned tenant' - ) - status = CSVChoiceField( - choices=VLANStatusChoices, - help_text='Operational status' - ) - role = CSVModelChoiceField( - queryset=Role.objects.all(), - required=False, - to_field_name='name', - help_text='Functional role' - ) - - class Meta: - model = VLAN - fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description') - help_texts = { - 'vid': 'Numeric VLAN ID (1-4095)', - 'name': 'VLAN name', - } - - -class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=VLAN.objects.all(), - widget=forms.MultipleHiddenInput() - ) - region = DynamicModelChoiceField( - queryset=Region.objects.all(), - required=False - ) - site_group = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region', - 'group_id': '$site_group', - } - ) - group = DynamicModelChoiceField( - queryset=VLANGroup.objects.all(), - required=False, - query_params={ - 'site_id': '$site' - } - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - status = forms.ChoiceField( - choices=add_blank_choice(VLANStatusChoices), - required=False, - widget=StaticSelect() - ) - role = DynamicModelChoiceField( - queryset=Role.objects.all(), - required=False - ) - description = forms.CharField( - max_length=100, - required=False - ) - - class Meta: - nullable_fields = [ - 'site', 'group', 'tenant', 'role', 'description', - ] - - -class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = VLAN - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id'], - ['group_id', 'status', 'role_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, - null_option='None', - query_params={ - 'region': '$region' - }, - label=_('Site'), - fetch_trigger='open' - ) - group_id = DynamicModelMultipleChoiceField( - queryset=VLANGroup.objects.all(), - required=False, - null_option='None', - query_params={ - 'region': '$region' - }, - label=_('VLAN group'), - fetch_trigger='open' - ) - status = forms.MultipleChoiceField( - choices=VLANStatusChoices, - required=False, - widget=StaticSelectMultiple() - ) - role_id = DynamicModelMultipleChoiceField( - queryset=Role.objects.all(), - required=False, - null_option='None', - label=_('Role'), - fetch_trigger='open' - ) - tag = TagFilterField(model) - - -# -# Services -# - -class ServiceForm(BootstrapMixin, CustomFieldModelForm): - ports = NumericArrayField( - base_field=forms.IntegerField( - min_value=SERVICE_PORT_MIN, - max_value=SERVICE_PORT_MAX - ), - help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen." - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = Service - fields = [ - 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'tags', - ] - help_texts = { - 'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be " - "reachable via all IPs assigned to the device.", - } - widgets = { - 'protocol': StaticSelect(), - 'ipaddresses': StaticSelectMultiple(), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Limit IP address choices to those assigned to interfaces of the parent device/VM - if self.instance.device: - self.fields['ipaddresses'].queryset = IPAddress.objects.filter( - interface__in=self.instance.device.vc_interfaces().values_list('id', flat=True) - ) - elif self.instance.virtual_machine: - self.fields['ipaddresses'].queryset = IPAddress.objects.filter( - vminterface__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True) - ) - else: - self.fields['ipaddresses'].choices = [] - - -class ServiceFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = Service - field_groups = ( - ('q', 'tag'), - ('protocol', 'port'), - ) - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - protocol = forms.ChoiceField( - choices=add_blank_choice(ServiceProtocolChoices), - required=False, - widget=StaticSelectMultiple() - ) - port = forms.IntegerField( - required=False, - ) - tag = TagFilterField(model) - - -class ServiceCSVForm(CustomFieldModelCSVForm): - device = CSVModelChoiceField( - queryset=Device.objects.all(), - required=False, - to_field_name='name', - help_text='Required if not assigned to a VM' - ) - virtual_machine = CSVModelChoiceField( - queryset=VirtualMachine.objects.all(), - required=False, - to_field_name='name', - help_text='Required if not assigned to a device' - ) - protocol = CSVChoiceField( - choices=ServiceProtocolChoices, - help_text='IP protocol' - ) - - class Meta: - model = Service - fields = ('device', 'virtual_machine', 'name', 'protocol', 'ports', 'description') - - -class ServiceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Service.objects.all(), - widget=forms.MultipleHiddenInput() - ) - protocol = forms.ChoiceField( - choices=add_blank_choice(ServiceProtocolChoices), - required=False, - widget=StaticSelect() - ) - ports = NumericArrayField( - base_field=forms.IntegerField( - min_value=SERVICE_PORT_MIN, - max_value=SERVICE_PORT_MAX - ), - required=False - ) - description = forms.CharField( - max_length=100, - required=False - ) - - class Meta: - nullable_fields = [ - 'description', - ] diff --git a/netbox/ipam/forms/__init__.py b/netbox/ipam/forms/__init__.py new file mode 100644 index 000000000..fc3352358 --- /dev/null +++ b/netbox/ipam/forms/__init__.py @@ -0,0 +1,5 @@ +from .models import * +from .filtersets import * +from .bulk_create import * +from .bulk_edit import * +from .bulk_import import * diff --git a/netbox/ipam/forms/bulk_create.py b/netbox/ipam/forms/bulk_create.py new file mode 100644 index 000000000..790474c6e --- /dev/null +++ b/netbox/ipam/forms/bulk_create.py @@ -0,0 +1,13 @@ +from django import forms + +from utilities.forms import BootstrapMixin, ExpandableIPAddressField + +__all__ = ( + 'IPAddressBulkCreateForm', +) + + +class IPAddressBulkCreateForm(BootstrapMixin, forms.Form): + pattern = ExpandableIPAddressField( + label='Address pattern' + ) diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py new file mode 100644 index 000000000..895dbe200 --- /dev/null +++ b/netbox/ipam/forms/bulk_edit.py @@ -0,0 +1,378 @@ +from django import forms + +from dcim.models import Region, Site, SiteGroup +from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm +from ipam.choices import * +from ipam.constants import * +from ipam.models import * +from tenancy.models import Tenant +from utilities.forms import ( + add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, DatePicker, DynamicModelChoiceField, NumericArrayField, + StaticSelect, +) + +__all__ = ( + 'AggregateBulkEditForm', + 'IPAddressBulkEditForm', + 'IPRangeBulkEditForm', + 'PrefixBulkEditForm', + 'RIRBulkEditForm', + 'RoleBulkEditForm', + 'RouteTargetBulkEditForm', + 'ServiceBulkEditForm', + 'VLANBulkEditForm', + 'VLANGroupBulkEditForm', + 'VRFBulkEditForm', +) + + +class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=VRF.objects.all(), + widget=forms.MultipleHiddenInput() + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + enforce_unique = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect(), + label='Enforce unique space' + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'tenant', 'description', + ] + + +class RouteTargetBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=RouteTarget.objects.all(), + widget=forms.MultipleHiddenInput() + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = [ + 'tenant', 'description', + ] + + +class RIRBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=RIR.objects.all(), + widget=forms.MultipleHiddenInput + ) + is_private = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['is_private', 'description'] + + +class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Aggregate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + rir = DynamicModelChoiceField( + queryset=RIR.objects.all(), + required=False, + label='RIR' + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + date_added = forms.DateField( + required=False + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'date_added', 'description', + ] + widgets = { + 'date_added': DatePicker(), + } + + +class RoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Role.objects.all(), + widget=forms.MultipleHiddenInput + ) + weight = forms.IntegerField( + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['description'] + + +class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Prefix.objects.all(), + widget=forms.MultipleHiddenInput() + ) + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False + ) + site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region', + 'group_id': '$site_group', + } + ) + vrf = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) + prefix_length = forms.IntegerField( + min_value=PREFIX_LENGTH_MIN, + max_value=PREFIX_LENGTH_MAX, + required=False + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + status = forms.ChoiceField( + choices=add_blank_choice(PrefixStatusChoices), + required=False, + widget=StaticSelect() + ) + role = DynamicModelChoiceField( + queryset=Role.objects.all(), + required=False + ) + is_pool = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect(), + label='Is a pool' + ) + mark_utilized = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect(), + label='Treat as 100% utilized' + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'site', 'vrf', 'tenant', 'role', 'description', + ] + + +class IPRangeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=IPRange.objects.all(), + widget=forms.MultipleHiddenInput() + ) + vrf = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + status = forms.ChoiceField( + choices=add_blank_choice(IPRangeStatusChoices), + required=False, + widget=StaticSelect() + ) + role = DynamicModelChoiceField( + queryset=Role.objects.all(), + required=False + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'vrf', 'tenant', 'role', 'description', + ] + + +class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=IPAddress.objects.all(), + widget=forms.MultipleHiddenInput() + ) + vrf = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) + mask_length = forms.IntegerField( + min_value=IPADDRESS_MASK_LENGTH_MIN, + max_value=IPADDRESS_MASK_LENGTH_MAX, + required=False + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + status = forms.ChoiceField( + choices=add_blank_choice(IPAddressStatusChoices), + required=False, + widget=StaticSelect() + ) + role = forms.ChoiceField( + choices=add_blank_choice(IPAddressRoleChoices), + required=False, + widget=StaticSelect() + ) + dns_name = forms.CharField( + max_length=255, + required=False + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'vrf', 'role', 'tenant', 'dns_name', 'description', + ] + + +class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=VLANGroup.objects.all(), + widget=forms.MultipleHiddenInput + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['site', 'description'] + + +class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=VLAN.objects.all(), + widget=forms.MultipleHiddenInput() + ) + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False + ) + site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region', + 'group_id': '$site_group', + } + ) + group = DynamicModelChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + query_params={ + 'site_id': '$site' + } + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + status = forms.ChoiceField( + choices=add_blank_choice(VLANStatusChoices), + required=False, + widget=StaticSelect() + ) + role = DynamicModelChoiceField( + queryset=Role.objects.all(), + required=False + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'site', 'group', 'tenant', 'role', 'description', + ] + + +class ServiceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Service.objects.all(), + widget=forms.MultipleHiddenInput() + ) + protocol = forms.ChoiceField( + choices=add_blank_choice(ServiceProtocolChoices), + required=False, + widget=StaticSelect() + ) + ports = NumericArrayField( + base_field=forms.IntegerField( + min_value=SERVICE_PORT_MIN, + max_value=SERVICE_PORT_MAX + ), + required=False + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'description', + ] diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py new file mode 100644 index 000000000..ef5759748 --- /dev/null +++ b/netbox/ipam/forms/bulk_import.py @@ -0,0 +1,362 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType + +from dcim.models import Device, Interface, Site +from extras.forms import CustomFieldModelCSVForm +from ipam.choices import * +from ipam.constants import * +from ipam.models import * +from tenancy.models import Tenant +from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField +from virtualization.models import VirtualMachine, VMInterface + +__all__ = ( + 'AggregateCSVForm', + 'IPAddressCSVForm', + 'IPRangeCSVForm', + 'PrefixCSVForm', + 'RIRCSVForm', + 'RoleCSVForm', + 'RouteTargetCSVForm', + 'ServiceCSVForm', + 'VLANCSVForm', + 'VLANGroupCSVForm', + 'VRFCSVForm', +) + + +class VRFCSVForm(CustomFieldModelCSVForm): + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) + + class Meta: + model = VRF + fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description') + + +class RouteTargetCSVForm(CustomFieldModelCSVForm): + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) + + class Meta: + model = RouteTarget + fields = ('name', 'description', 'tenant') + + +class RIRCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + + class Meta: + model = RIR + fields = ('name', 'slug', 'is_private', 'description') + help_texts = { + 'name': 'RIR name', + } + + +class AggregateCSVForm(CustomFieldModelCSVForm): + rir = CSVModelChoiceField( + queryset=RIR.objects.all(), + to_field_name='name', + help_text='Assigned RIR' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) + + class Meta: + model = Aggregate + fields = ('prefix', 'rir', 'tenant', 'date_added', 'description') + + +class RoleCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + + class Meta: + model = Role + fields = ('name', 'slug', 'weight', 'description') + + +class PrefixCSVForm(CustomFieldModelCSVForm): + vrf = CSVModelChoiceField( + queryset=VRF.objects.all(), + to_field_name='name', + required=False, + help_text='Assigned VRF' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) + site = CSVModelChoiceField( + queryset=Site.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned site' + ) + vlan_group = CSVModelChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + to_field_name='name', + help_text="VLAN's group (if any)" + ) + vlan = CSVModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + to_field_name='vid', + help_text="Assigned VLAN" + ) + status = CSVChoiceField( + choices=PrefixStatusChoices, + help_text='Operational status' + ) + role = CSVModelChoiceField( + queryset=Role.objects.all(), + required=False, + to_field_name='name', + help_text='Functional role' + ) + + class Meta: + model = Prefix + fields = ( + 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', + 'description', + ) + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit VLAN queryset by assigned site and/or group (if specified) + params = {} + if data.get('site'): + params[f"site__{self.fields['site'].to_field_name}"] = data.get('site') + if data.get('vlan_group'): + params[f"group__{self.fields['vlan_group'].to_field_name}"] = data.get('vlan_group') + if params: + self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params) + + +class IPRangeCSVForm(CustomFieldModelCSVForm): + vrf = CSVModelChoiceField( + queryset=VRF.objects.all(), + to_field_name='name', + required=False, + help_text='Assigned VRF' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) + status = CSVChoiceField( + choices=IPRangeStatusChoices, + help_text='Operational status' + ) + role = CSVModelChoiceField( + queryset=Role.objects.all(), + required=False, + to_field_name='name', + help_text='Functional role' + ) + + class Meta: + model = IPRange + fields = ( + 'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'description', + ) + + +class IPAddressCSVForm(CustomFieldModelCSVForm): + vrf = CSVModelChoiceField( + queryset=VRF.objects.all(), + to_field_name='name', + required=False, + help_text='Assigned VRF' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + to_field_name='name', + required=False, + help_text='Assigned tenant' + ) + status = CSVChoiceField( + choices=IPAddressStatusChoices, + required=False, + help_text='Operational status' + ) + role = CSVChoiceField( + choices=IPAddressRoleChoices, + required=False, + help_text='Functional role' + ) + device = CSVModelChoiceField( + queryset=Device.objects.all(), + required=False, + to_field_name='name', + help_text='Parent device of assigned interface (if any)' + ) + virtual_machine = CSVModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + to_field_name='name', + help_text='Parent VM of assigned interface (if any)' + ) + interface = CSVModelChoiceField( + queryset=Interface.objects.none(), # Can also refer to VMInterface + required=False, + to_field_name='name', + help_text='Assigned interface' + ) + is_primary = forms.BooleanField( + help_text='Make this the primary IP for the assigned device', + required=False + ) + + class Meta: + model = IPAddress + fields = [ + 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary', + 'dns_name', 'description', + ] + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit interface queryset by assigned device + if data.get('device'): + self.fields['interface'].queryset = Interface.objects.filter( + **{f"device__{self.fields['device'].to_field_name}": data['device']} + ) + + # Limit interface queryset by assigned device + elif data.get('virtual_machine'): + self.fields['interface'].queryset = VMInterface.objects.filter( + **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']} + ) + + def clean(self): + super().clean() + + device = self.cleaned_data.get('device') + virtual_machine = self.cleaned_data.get('virtual_machine') + is_primary = self.cleaned_data.get('is_primary') + + # Validate is_primary + if is_primary and not device and not virtual_machine: + raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP") + + def save(self, *args, **kwargs): + + # Set interface assignment + if self.cleaned_data['interface']: + self.instance.assigned_object = self.cleaned_data['interface'] + + ipaddress = super().save(*args, **kwargs) + + # Set as primary for device/VM + if self.cleaned_data['is_primary']: + parent = self.cleaned_data['device'] or self.cleaned_data['virtual_machine'] + if self.instance.address.version == 4: + parent.primary_ip4 = ipaddress + elif self.instance.address.version == 6: + parent.primary_ip6 = ipaddress + parent.save() + + return ipaddress + + +class VLANGroupCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + required=False, + label='Scope type (app & model)' + ) + + class Meta: + model = VLANGroup + fields = ('name', 'slug', 'scope_type', 'scope_id', 'description') + labels = { + 'scope_id': 'Scope ID', + } + + +class VLANCSVForm(CustomFieldModelCSVForm): + site = CSVModelChoiceField( + queryset=Site.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned site' + ) + group = CSVModelChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned VLAN group' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + to_field_name='name', + required=False, + help_text='Assigned tenant' + ) + status = CSVChoiceField( + choices=VLANStatusChoices, + help_text='Operational status' + ) + role = CSVModelChoiceField( + queryset=Role.objects.all(), + required=False, + to_field_name='name', + help_text='Functional role' + ) + + class Meta: + model = VLAN + fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description') + help_texts = { + 'vid': 'Numeric VLAN ID (1-4095)', + 'name': 'VLAN name', + } + + +class ServiceCSVForm(CustomFieldModelCSVForm): + device = CSVModelChoiceField( + queryset=Device.objects.all(), + required=False, + to_field_name='name', + help_text='Required if not assigned to a VM' + ) + virtual_machine = CSVModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + to_field_name='name', + help_text='Required if not assigned to a device' + ) + protocol = CSVChoiceField( + choices=ServiceProtocolChoices, + help_text='IP protocol' + ) + + class Meta: + model = Service + fields = ('device', 'virtual_machine', 'name', 'protocol', 'ports', 'description') diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py new file mode 100644 index 000000000..8bc0f10fb --- /dev/null +++ b/netbox/ipam/forms/filtersets.py @@ -0,0 +1,486 @@ +from django import forms +from django.utils.translation import gettext as _ + +from dcim.models import Location, Rack, Region, Site, SiteGroup +from extras.forms import CustomFieldModelFilterForm +from ipam.choices import * +from ipam.constants import * +from ipam.models import * +from tenancy.forms import TenancyFilterForm +from utilities.forms import ( + add_blank_choice, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect, + StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, +) + +__all__ = ( + 'AggregateFilterForm', + 'IPAddressFilterForm', + 'IPRangeFilterForm', + 'PrefixFilterForm', + 'RIRFilterForm', + 'RoleFilterForm', + 'RouteTargetFilterForm', + 'ServiceFilterForm', + 'VLANFilterForm', + 'VLANGroupFilterForm', + 'VRFFilterForm', +) + +PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([ + (i, i) for i in range(PREFIX_LENGTH_MIN, PREFIX_LENGTH_MAX + 1) +]) + +IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([ + (i, i) for i in range(IPADDRESS_MASK_LENGTH_MIN, IPADDRESS_MASK_LENGTH_MAX + 1) +]) + + +class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = VRF + field_groups = [ + ['q', 'tag'], + ['import_target_id', 'export_target_id'], + ['tenant_group_id', 'tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + import_target_id = DynamicModelMultipleChoiceField( + queryset=RouteTarget.objects.all(), + required=False, + label=_('Import targets'), + fetch_trigger='open' + ) + export_target_id = DynamicModelMultipleChoiceField( + queryset=RouteTarget.objects.all(), + required=False, + label=_('Export targets'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class RouteTargetFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = RouteTarget + field_groups = [ + ['q', 'tag'], + ['importing_vrf_id', 'exporting_vrf_id'], + ['tenant_group_id', 'tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + importing_vrf_id = DynamicModelMultipleChoiceField( + queryset=VRF.objects.all(), + required=False, + label=_('Imported by VRF'), + fetch_trigger='open' + ) + exporting_vrf_id = DynamicModelMultipleChoiceField( + queryset=VRF.objects.all(), + required=False, + label=_('Exported by VRF'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class RIRFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = RIR + field_groups = [ + ['q'], + ['is_private'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + is_private = forms.NullBooleanField( + required=False, + label=_('Private'), + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class AggregateFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = Aggregate + field_groups = [ + ['q', 'tag'], + ['family', 'rir_id'], + ['tenant_group_id', 'tenant_id'] + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + family = forms.ChoiceField( + required=False, + choices=add_blank_choice(IPAddressFamilyChoices), + label=_('Address family'), + widget=StaticSelect() + ) + rir_id = DynamicModelMultipleChoiceField( + queryset=RIR.objects.all(), + required=False, + label=_('RIR'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class RoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = Role + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + +class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = Prefix + field_groups = [ + ['q', 'tag'], + ['within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized'], + ['vrf_id', 'present_in_vrf_id'], + ['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') + ) + mask_length__lte = forms.IntegerField( + widget=forms.HiddenInput() + ) + within_include = forms.CharField( + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': 'Prefix', + } + ), + label=_('Search within') + ) + family = forms.ChoiceField( + required=False, + choices=add_blank_choice(IPAddressFamilyChoices), + label=_('Address family'), + widget=StaticSelect() + ) + mask_length = forms.MultipleChoiceField( + required=False, + choices=PREFIX_MASK_LENGTH_CHOICES, + label=_('Mask length'), + widget=StaticSelectMultiple() + ) + vrf_id = DynamicModelMultipleChoiceField( + queryset=VRF.objects.all(), + required=False, + label=_('Assigned VRF'), + null_option='Global', + fetch_trigger='open' + ) + present_in_vrf_id = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label=_('Present in VRF'), + fetch_trigger='open' + ) + status = forms.MultipleChoiceField( + choices=PrefixStatusChoices, + required=False, + widget=StaticSelectMultiple() + ) + 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, + null_option='None', + query_params={ + 'region_id': '$region_id' + }, + label=_('Site'), + fetch_trigger='open' + ) + role_id = DynamicModelMultipleChoiceField( + queryset=Role.objects.all(), + required=False, + null_option='None', + label=_('Role'), + fetch_trigger='open' + ) + is_pool = forms.NullBooleanField( + required=False, + label=_('Is a pool'), + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + mark_utilized = forms.NullBooleanField( + required=False, + label=_('Marked as 100% utilized'), + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + tag = TagFilterField(model) + + +class IPRangeFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = IPRange + field_groups = [ + ['q', 'tag'], + ['family', 'vrf_id', 'status', 'role_id'], + ['tenant_group_id', 'tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + family = forms.ChoiceField( + required=False, + choices=add_blank_choice(IPAddressFamilyChoices), + label=_('Address family'), + widget=StaticSelect() + ) + vrf_id = DynamicModelMultipleChoiceField( + queryset=VRF.objects.all(), + required=False, + label=_('Assigned VRF'), + null_option='Global', + fetch_trigger='open' + ) + status = forms.MultipleChoiceField( + choices=PrefixStatusChoices, + required=False, + widget=StaticSelectMultiple() + ) + role_id = DynamicModelMultipleChoiceField( + queryset=Role.objects.all(), + required=False, + null_option='None', + label=_('Role'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = IPAddress + field_order = [ + 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'role', + 'assigned_to_interface', 'tenant_group_id', 'tenant_id', + ] + field_groups = [ + ['q', 'tag'], + ['parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface'], + ['vrf_id', 'present_in_vrf_id'], + ['tenant_group_id', 'tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + parent = forms.CharField( + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': 'Prefix', + } + ), + label='Parent Prefix' + ) + family = forms.ChoiceField( + required=False, + choices=add_blank_choice(IPAddressFamilyChoices), + label=_('Address family'), + widget=StaticSelect() + ) + mask_length = forms.ChoiceField( + required=False, + choices=IPADDRESS_MASK_LENGTH_CHOICES, + label=_('Mask length'), + widget=StaticSelect() + ) + vrf_id = DynamicModelMultipleChoiceField( + queryset=VRF.objects.all(), + required=False, + label=_('Assigned VRF'), + null_option='Global', + fetch_trigger='open' + ) + present_in_vrf_id = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label=_('Present in VRF'), + fetch_trigger='open' + ) + status = forms.MultipleChoiceField( + choices=IPAddressStatusChoices, + required=False, + widget=StaticSelectMultiple() + ) + role = forms.MultipleChoiceField( + choices=IPAddressRoleChoices, + required=False, + widget=StaticSelectMultiple() + ) + assigned_to_interface = forms.NullBooleanField( + required=False, + label=_('Assigned to an interface'), + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + tag = TagFilterField(model) + + +class VLANGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + field_groups = [ + ['q'], + ['region', 'sitegroup', 'site', 'location', 'rack'] + ] + model = VLANGroup + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + region = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + sitegroup = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) + site = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + label=_('Site'), + fetch_trigger='open' + ) + location = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + label=_('Location'), + fetch_trigger='open' + ) + rack = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + label=_('Rack'), + fetch_trigger='open' + ) + + +class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = VLAN + field_groups = [ + ['q', 'tag'], + ['region_id', 'site_group_id', 'site_id'], + ['group_id', 'status', 'role_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, + null_option='None', + query_params={ + 'region': '$region' + }, + label=_('Site'), + fetch_trigger='open' + ) + group_id = DynamicModelMultipleChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + null_option='None', + query_params={ + 'region': '$region' + }, + label=_('VLAN group'), + fetch_trigger='open' + ) + status = forms.MultipleChoiceField( + choices=VLANStatusChoices, + required=False, + widget=StaticSelectMultiple() + ) + role_id = DynamicModelMultipleChoiceField( + queryset=Role.objects.all(), + required=False, + null_option='None', + label=_('Role'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class ServiceFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = Service + field_groups = ( + ('q', 'tag'), + ('protocol', 'port'), + ) + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + protocol = forms.ChoiceField( + choices=add_blank_choice(ServiceProtocolChoices), + required=False, + widget=StaticSelectMultiple() + ) + port = forms.IntegerField( + required=False, + ) + tag = TagFilterField(model) diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py new file mode 100644 index 000000000..d28f7b3ae --- /dev/null +++ b/netbox/ipam/forms/models.py @@ -0,0 +1,691 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType + +from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup +from extras.forms import CustomFieldModelForm +from extras.models import Tag +from ipam.constants import * +from ipam.models import * +from tenancy.forms import TenancyForm +from utilities.forms import ( + BootstrapMixin, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple, +) +from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface + +__all__ = ( + 'AggregateForm', + 'IPAddressAssignForm', + 'IPAddressBulkAddForm', + 'IPAddressForm', + 'IPRangeForm', + 'PrefixForm', + 'RIRForm', + 'RoleForm', + 'RouteTargetForm', + 'ServiceForm', + 'VLANForm', + 'VLANGroupForm', + 'VRFForm', +) + + +class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + import_targets = DynamicModelMultipleChoiceField( + queryset=RouteTarget.objects.all(), + required=False + ) + export_targets = DynamicModelMultipleChoiceField( + queryset=RouteTarget.objects.all(), + required=False + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = VRF + fields = [ + 'name', 'rd', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tenant_group', 'tenant', + 'tags', + ] + fieldsets = ( + ('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')), + ('Route Targets', ('import_targets', 'export_targets')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + labels = { + 'rd': "RD", + } + help_texts = { + 'rd': "Route distinguisher in any format", + } + + +class RouteTargetForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = RouteTarget + fields = [ + 'name', 'description', 'tenant_group', 'tenant', 'tags', + ] + fieldsets = ( + ('Route Target', ('name', 'description', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + + +class RIRForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + + class Meta: + model = RIR + fields = [ + 'name', 'slug', 'is_private', 'description', + ] + + +class AggregateForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + rir = DynamicModelChoiceField( + queryset=RIR.objects.all(), + label='RIR' + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = Aggregate + fields = [ + 'prefix', 'rir', 'date_added', 'description', 'tenant_group', 'tenant', 'tags', + ] + fieldsets = ( + ('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + help_texts = { + 'prefix': "IPv4 or IPv6 network", + 'rir': "Regional Internet Registry responsible for this prefix", + } + widgets = { + 'date_added': DatePicker(), + } + + +class RoleForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + + class Meta: + model = Role + fields = [ + 'name', 'slug', 'weight', 'description', + ] + + +class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + vrf = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) + 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, + null_option='None', + query_params={ + 'region_id': '$region', + 'group_id': '$site_group', + } + ) + vlan_group = DynamicModelChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + label='VLAN group', + null_option='None', + query_params={ + 'site_id': '$site' + }, + initial_params={ + 'vlans': '$vlan' + } + ) + vlan = DynamicModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + label='VLAN', + query_params={ + 'site_id': '$site', + 'group_id': '$vlan_group', + } + ) + role = DynamicModelChoiceField( + queryset=Role.objects.all(), + required=False + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = Prefix + fields = [ + 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', + 'tenant_group', 'tenant', 'tags', + ] + fieldsets = ( + ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')), + ('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + widgets = { + 'status': StaticSelect(), + } + + +class IPRangeForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + vrf = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) + role = DynamicModelChoiceField( + queryset=Role.objects.all(), + required=False + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = IPRange + fields = [ + 'vrf', 'start_address', 'end_address', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags', + ] + fieldsets = ( + ('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'description', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + widgets = { + 'status': StaticSelect(), + } + + +class IPAddressForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + device = DynamicModelChoiceField( + queryset=Device.objects.all(), + required=False, + initial_params={ + 'interfaces': '$interface' + } + ) + interface = DynamicModelChoiceField( + queryset=Interface.objects.all(), + required=False, + query_params={ + 'device_id': '$device' + } + ) + virtual_machine = DynamicModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + initial_params={ + 'interfaces': '$vminterface' + } + ) + vminterface = DynamicModelChoiceField( + queryset=VMInterface.objects.all(), + required=False, + label='Interface', + query_params={ + 'virtual_machine_id': '$virtual_machine' + } + ) + vrf = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) + nat_region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + label='Region', + initial_params={ + 'sites': '$nat_site' + } + ) + nat_site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label='Site group', + initial_params={ + 'sites': '$nat_site' + } + ) + nat_site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + label='Site', + query_params={ + 'region_id': '$nat_region', + 'group_id': '$nat_site_group', + } + ) + nat_rack = DynamicModelChoiceField( + queryset=Rack.objects.all(), + required=False, + label='Rack', + null_option='None', + query_params={ + 'site_id': '$site' + } + ) + nat_device = DynamicModelChoiceField( + queryset=Device.objects.all(), + required=False, + label='Device', + query_params={ + 'site_id': '$site', + 'rack_id': '$nat_rack', + } + ) + nat_cluster = DynamicModelChoiceField( + queryset=Cluster.objects.all(), + required=False, + label='Cluster' + ) + nat_virtual_machine = DynamicModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + label='Virtual Machine', + query_params={ + 'cluster_id': '$nat_cluster', + } + ) + nat_vrf = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) + nat_inside = DynamicModelChoiceField( + queryset=IPAddress.objects.all(), + required=False, + label='IP Address', + query_params={ + 'device_id': '$nat_device', + 'virtual_machine_id': '$nat_virtual_machine', + 'vrf_id': '$nat_vrf', + } + ) + primary_for_parent = forms.BooleanField( + required=False, + label='Make this the primary IP for the device/VM' + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = IPAddress + fields = [ + 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack', + 'nat_device', 'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', + 'tags', + ] + widgets = { + 'status': StaticSelect(), + 'role': StaticSelect(), + } + + def __init__(self, *args, **kwargs): + + # Initialize helper selectors + instance = kwargs.get('instance') + initial = kwargs.get('initial', {}).copy() + if instance: + if type(instance.assigned_object) is Interface: + initial['interface'] = instance.assigned_object + elif type(instance.assigned_object) is VMInterface: + initial['vminterface'] = instance.assigned_object + if instance.nat_inside: + nat_inside_parent = instance.nat_inside.assigned_object + if type(nat_inside_parent) is Interface: + initial['nat_site'] = nat_inside_parent.device.site.pk + if nat_inside_parent.device.rack: + initial['nat_rack'] = nat_inside_parent.device.rack.pk + initial['nat_device'] = nat_inside_parent.device.pk + elif type(nat_inside_parent) is VMInterface: + initial['nat_cluster'] = nat_inside_parent.virtual_machine.cluster.pk + initial['nat_virtual_machine'] = nat_inside_parent.virtual_machine.pk + kwargs['initial'] = initial + + super().__init__(*args, **kwargs) + + # Initialize primary_for_parent if IP address is already assigned + if self.instance.pk and self.instance.assigned_object: + parent = self.instance.assigned_object.parent_object + if ( + self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or + self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk + ): + self.initial['primary_for_parent'] = True + + def clean(self): + super().clean() + + # Cannot select both a device interface and a VM interface + if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'): + raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface") + self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface') + + # Primary IP assignment is only available if an interface has been assigned. + interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface') + if self.cleaned_data.get('primary_for_parent') and not interface: + self.add_error( + 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs." + ) + + def save(self, *args, **kwargs): + ipaddress = super().save(*args, **kwargs) + + # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine. + interface = self.instance.assigned_object + if interface: + parent = interface.parent_object + if self.cleaned_data['primary_for_parent']: + if ipaddress.address.version == 4: + parent.primary_ip4 = ipaddress + else: + parent.primary_ip6 = ipaddress + parent.save() + elif ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress: + parent.primary_ip4 = None + parent.save() + elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress: + parent.primary_ip6 = None + parent.save() + + return ipaddress + + +class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + vrf = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = IPAddress + fields = [ + 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags', + ] + widgets = { + 'status': StaticSelect(), + 'role': StaticSelect(), + } + + +class IPAddressAssignForm(BootstrapMixin, forms.Form): + vrf_id = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) + q = forms.CharField( + required=False, + label='Search', + ) + + +class VLANGroupForm(BootstrapMixin, CustomFieldModelForm): + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + required=False, + widget=StaticSelect + ) + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + initial_params={ + 'sites': '$site' + } + ) + sitegroup = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + initial_params={ + 'sites': '$site' + }, + label='Site group' + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + initial_params={ + 'locations': '$location' + }, + query_params={ + 'region_id': '$region', + 'group_id': '$sitegroup', + } + ) + location = DynamicModelChoiceField( + queryset=Location.objects.all(), + required=False, + initial_params={ + 'racks': '$rack' + }, + query_params={ + 'site_id': '$site', + } + ) + rack = DynamicModelChoiceField( + queryset=Rack.objects.all(), + required=False, + query_params={ + 'site_id': '$site', + 'location_id': '$location', + } + ) + clustergroup = DynamicModelChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + initial_params={ + 'clusters': '$cluster' + }, + label='Cluster group' + ) + cluster = DynamicModelChoiceField( + queryset=Cluster.objects.all(), + required=False, + query_params={ + 'group_id': '$clustergroup', + } + ) + slug = SlugField() + + class Meta: + model = VLANGroup + fields = [ + 'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', + 'clustergroup', 'cluster', + ] + fieldsets = ( + ('VLAN Group', ('name', 'slug', 'description')), + ('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')), + ) + widgets = { + 'scope_type': StaticSelect, + } + + def __init__(self, *args, **kwargs): + instance = kwargs.get('instance') + initial = kwargs.get('initial', {}) + + if instance is not None and instance.scope: + initial[instance.scope_type.model] = instance.scope + + kwargs['initial'] = initial + + super().__init__(*args, **kwargs) + + def clean(self): + super().clean() + + # Assign scope based on scope_type + if self.cleaned_data.get('scope_type'): + scope_field = self.cleaned_data['scope_type'].model + self.instance.scope = self.cleaned_data.get(scope_field) + else: + self.instance.scope_id = None + + +class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + # VLANGroup assignment fields + scope_type = forms.ChoiceField( + choices=( + ('', ''), + ('dcim.region', 'Region'), + ('dcim.sitegroup', 'Site group'), + ('dcim.site', 'Site'), + ('dcim.location', 'Location'), + ('dcim.rack', 'Rack'), + ('virtualization.clustergroup', 'Cluster group'), + ('virtualization.cluster', 'Cluster'), + ), + required=False, + widget=StaticSelect, + label='Group scope' + ) + group = DynamicModelChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + query_params={ + 'scope_type': '$scope_type', + }, + label='VLAN Group' + ) + + # Site assignment fields + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + initial_params={ + 'sites': '$site' + }, + label='Region' + ) + sitegroup = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + initial_params={ + 'sites': '$site' + }, + label='Site group' + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + null_option='None', + query_params={ + 'region_id': '$region', + 'group_id': '$sitegroup', + } + ) + + # Other fields + role = DynamicModelChoiceField( + queryset=Role.objects.all(), + required=False + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = VLAN + fields = [ + 'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags', + ] + help_texts = { + 'site': "Leave blank if this VLAN spans multiple sites", + 'group': "VLAN group (optional)", + 'vid': "Configured VLAN ID", + 'name': "Configured VLAN name", + 'status': "Operational status of this VLAN", + 'role': "The primary function of this VLAN", + } + widgets = { + 'status': StaticSelect(), + } + + +class ServiceForm(BootstrapMixin, CustomFieldModelForm): + ports = NumericArrayField( + base_field=forms.IntegerField( + min_value=SERVICE_PORT_MIN, + max_value=SERVICE_PORT_MAX + ), + help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen." + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = Service + fields = [ + 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'tags', + ] + help_texts = { + 'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be " + "reachable via all IPs assigned to the device.", + } + widgets = { + 'protocol': StaticSelect(), + 'ipaddresses': StaticSelectMultiple(), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit IP address choices to those assigned to interfaces of the parent device/VM + if self.instance.device: + self.fields['ipaddresses'].queryset = IPAddress.objects.filter( + interface__in=self.instance.device.vc_interfaces().values_list('id', flat=True) + ) + elif self.instance.virtual_machine: + self.fields['ipaddresses'].queryset = IPAddress.objects.filter( + vminterface__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True) + ) + else: + self.fields['ipaddresses'].choices = [] diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py deleted file mode 100644 index 63dcdd468..000000000 --- a/netbox/tenancy/forms.py +++ /dev/null @@ -1,196 +0,0 @@ -from django import forms -from django.utils.translation import gettext as _ - -from extras.forms import ( - AddRemoveTagsForm, CustomFieldModelForm, CustomFieldModelBulkEditForm, CustomFieldModelFilterForm, CustomFieldModelCSVForm, -) -from extras.models import Tag -from utilities.forms import ( - BootstrapMixin, CommentField, CSVModelChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - SlugField, TagFilterField, -) -from .models import Tenant, TenantGroup - - -# -# Tenant groups -# - -class TenantGroupForm(BootstrapMixin, CustomFieldModelForm): - parent = DynamicModelChoiceField( - queryset=TenantGroup.objects.all(), - required=False - ) - slug = SlugField() - - class Meta: - model = TenantGroup - fields = [ - 'parent', 'name', 'slug', 'description', - ] - - -class TenantGroupCSVForm(CustomFieldModelCSVForm): - parent = CSVModelChoiceField( - queryset=TenantGroup.objects.all(), - required=False, - to_field_name='name', - help_text='Parent group' - ) - slug = SlugField() - - class Meta: - model = TenantGroup - fields = ('name', 'slug', 'parent', 'description') - - -class TenantGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=TenantGroup.objects.all(), - widget=forms.MultipleHiddenInput - ) - parent = DynamicModelChoiceField( - queryset=TenantGroup.objects.all(), - required=False - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['parent', 'description'] - - -class TenantGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = TenantGroup - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - parent_id = DynamicModelMultipleChoiceField( - queryset=TenantGroup.objects.all(), - required=False, - label=_('Parent group'), - fetch_trigger='open' - ) - - -# -# Tenants -# - -class TenantForm(BootstrapMixin, CustomFieldModelForm): - slug = SlugField() - group = DynamicModelChoiceField( - queryset=TenantGroup.objects.all(), - required=False - ) - comments = CommentField() - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = Tenant - fields = ( - 'name', 'slug', 'group', 'description', 'comments', 'tags', - ) - fieldsets = ( - ('Tenant', ('name', 'slug', 'group', 'description', 'tags')), - ) - - -class TenantCSVForm(CustomFieldModelCSVForm): - slug = SlugField() - group = CSVModelChoiceField( - queryset=TenantGroup.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned group' - ) - - class Meta: - model = Tenant - fields = ('name', 'slug', 'group', 'description', 'comments') - - -class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Tenant.objects.all(), - widget=forms.MultipleHiddenInput() - ) - group = DynamicModelChoiceField( - queryset=TenantGroup.objects.all(), - required=False - ) - - class Meta: - nullable_fields = [ - 'group', - ] - - -class TenantFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = Tenant - field_groups = ( - ('q', 'tag'), - ('group_id',), - ) - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - group_id = DynamicModelMultipleChoiceField( - queryset=TenantGroup.objects.all(), - required=False, - null_option='None', - label=_('Group'), - fetch_trigger='open' - ) - tag = TagFilterField(model) - - -# -# Form extensions -# - -class TenancyForm(forms.Form): - tenant_group = DynamicModelChoiceField( - queryset=TenantGroup.objects.all(), - required=False, - null_option='None', - initial_params={ - 'tenants': '$tenant' - } - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False, - query_params={ - 'group_id': '$tenant_group' - } - ) - - -class TenancyFilterForm(forms.Form): - tenant_group_id = DynamicModelMultipleChoiceField( - queryset=TenantGroup.objects.all(), - required=False, - null_option='None', - label=_('Tenant group'), - fetch_trigger='open' - ) - tenant_id = DynamicModelMultipleChoiceField( - queryset=Tenant.objects.all(), - required=False, - null_option='None', - query_params={ - 'group_id': '$tenant_group_id' - }, - label=_('Tenant'), - fetch_trigger='open' - ) diff --git a/netbox/tenancy/forms/__init__.py b/netbox/tenancy/forms/__init__.py new file mode 100644 index 000000000..61f0bc961 --- /dev/null +++ b/netbox/tenancy/forms/__init__.py @@ -0,0 +1,5 @@ +from .forms import * +from .models import * +from .filtersets import * +from .bulk_edit import * +from .bulk_import import * diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py new file mode 100644 index 000000000..b2fc7dafd --- /dev/null +++ b/netbox/tenancy/forms/bulk_edit.py @@ -0,0 +1,44 @@ +from django import forms + +from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm +from tenancy.models import Tenant, TenantGroup +from utilities.forms import BootstrapMixin, DynamicModelChoiceField + +__all__ = ( + 'TenantBulkEditForm', + 'TenantGroupBulkEditForm', +) + + +class TenantGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=TenantGroup.objects.all(), + widget=forms.MultipleHiddenInput + ) + parent = DynamicModelChoiceField( + queryset=TenantGroup.objects.all(), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['parent', 'description'] + + +class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Tenant.objects.all(), + widget=forms.MultipleHiddenInput() + ) + group = DynamicModelChoiceField( + queryset=TenantGroup.objects.all(), + required=False + ) + + class Meta: + nullable_fields = [ + 'group', + ] diff --git a/netbox/tenancy/forms/bulk_import.py b/netbox/tenancy/forms/bulk_import.py new file mode 100644 index 000000000..335d71ef6 --- /dev/null +++ b/netbox/tenancy/forms/bulk_import.py @@ -0,0 +1,36 @@ +from extras.forms import CustomFieldModelCSVForm +from tenancy.models import Tenant, TenantGroup +from utilities.forms import CSVModelChoiceField, SlugField + +__all__ = ( + 'TenantCSVForm', + 'TenantGroupCSVForm', +) + + +class TenantGroupCSVForm(CustomFieldModelCSVForm): + parent = CSVModelChoiceField( + queryset=TenantGroup.objects.all(), + required=False, + to_field_name='name', + help_text='Parent group' + ) + slug = SlugField() + + class Meta: + model = TenantGroup + fields = ('name', 'slug', 'parent', 'description') + + +class TenantCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + group = CSVModelChoiceField( + queryset=TenantGroup.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned group' + ) + + class Meta: + model = Tenant + fields = ('name', 'slug', 'group', 'description', 'comments') diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py new file mode 100644 index 000000000..6e2eb7fd1 --- /dev/null +++ b/netbox/tenancy/forms/filtersets.py @@ -0,0 +1,42 @@ +from django import forms +from django.utils.translation import gettext as _ + +from extras.forms import CustomFieldModelFilterForm +from tenancy.models import Tenant, TenantGroup +from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, TagFilterField + + +class TenantGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = TenantGroup + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + parent_id = DynamicModelMultipleChoiceField( + queryset=TenantGroup.objects.all(), + required=False, + label=_('Parent group'), + fetch_trigger='open' + ) + + +class TenantFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = Tenant + field_groups = ( + ('q', 'tag'), + ('group_id',), + ) + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + group_id = DynamicModelMultipleChoiceField( + queryset=TenantGroup.objects.all(), + required=False, + null_option='None', + label=_('Group'), + fetch_trigger='open' + ) + tag = TagFilterField(model) diff --git a/netbox/tenancy/forms/forms.py b/netbox/tenancy/forms/forms.py new file mode 100644 index 000000000..cad63c1a6 --- /dev/null +++ b/netbox/tenancy/forms/forms.py @@ -0,0 +1,48 @@ +from django import forms +from django.utils.translation import gettext as _ + +from tenancy.models import Tenant, TenantGroup +from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField + +__all__ = ( + 'TenancyForm', + 'TenancyFilterForm', +) + + +class TenancyForm(forms.Form): + tenant_group = DynamicModelChoiceField( + queryset=TenantGroup.objects.all(), + required=False, + null_option='None', + initial_params={ + 'tenants': '$tenant' + } + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + query_params={ + 'group_id': '$tenant_group' + } + ) + + +class TenancyFilterForm(forms.Form): + tenant_group_id = DynamicModelMultipleChoiceField( + queryset=TenantGroup.objects.all(), + required=False, + null_option='None', + label=_('Tenant group'), + fetch_trigger='open' + ) + tenant_id = DynamicModelMultipleChoiceField( + queryset=Tenant.objects.all(), + required=False, + null_option='None', + query_params={ + 'group_id': '$tenant_group_id' + }, + label=_('Tenant'), + fetch_trigger='open' + ) diff --git a/netbox/tenancy/forms/models.py b/netbox/tenancy/forms/models.py new file mode 100644 index 000000000..de3a9e515 --- /dev/null +++ b/netbox/tenancy/forms/models.py @@ -0,0 +1,47 @@ +from extras.forms import CustomFieldModelForm +from extras.models import Tag +from tenancy.models import Tenant, TenantGroup +from utilities.forms import ( + BootstrapMixin, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, +) + +__all__ = ( + 'TenantForm', + 'TenantGroupForm', +) + + +class TenantGroupForm(BootstrapMixin, CustomFieldModelForm): + parent = DynamicModelChoiceField( + queryset=TenantGroup.objects.all(), + required=False + ) + slug = SlugField() + + class Meta: + model = TenantGroup + fields = [ + 'parent', 'name', 'slug', 'description', + ] + + +class TenantForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + group = DynamicModelChoiceField( + queryset=TenantGroup.objects.all(), + required=False + ) + comments = CommentField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = Tenant + fields = ( + 'name', 'slug', 'group', 'description', 'comments', 'tags', + ) + fieldsets = ( + ('Tenant', ('name', 'slug', 'group', 'description', 'tags')), + ) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py deleted file mode 100644 index bf5dec00c..000000000 --- a/netbox/virtualization/forms.py +++ /dev/null @@ -1,965 +0,0 @@ -from django import forms -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError -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.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup -from extras.forms import ( - AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, - CustomFieldModelFilterForm, CustomFieldsMixin, LocalConfigContextFilterForm, -) -from extras.models import Tag -from ipam.models import IPAddress, VLAN, VLANGroup -from tenancy.forms import TenancyFilterForm, TenancyForm -from tenancy.models import Tenant -from utilities.forms import ( - add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, ConfirmationForm, - CSVChoiceField, CSVModelChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, - form_from_model, JSONField, SlugField, SmallTextarea, StaticSelect, StaticSelectMultiple, TagFilterField, - BOOLEAN_WITH_BLANK_CHOICES, -) -from .choices import * -from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface - - -# -# Cluster types -# - -class ClusterTypeForm(BootstrapMixin, CustomFieldModelForm): - slug = SlugField() - - class Meta: - model = ClusterType - fields = [ - 'name', 'slug', 'description', - ] - - -class ClusterTypeCSVForm(CustomFieldModelCSVForm): - slug = SlugField() - - class Meta: - model = ClusterType - fields = ('name', 'slug', 'description') - - -class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=ClusterType.objects.all(), - widget=forms.MultipleHiddenInput - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['description'] - - -class ClusterTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = ClusterType - field_groups = [ - ['q'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - - -# -# Cluster groups -# - -class ClusterGroupForm(BootstrapMixin, CustomFieldModelForm): - slug = SlugField() - - class Meta: - model = ClusterGroup - fields = [ - 'name', 'slug', 'description', - ] - - -class ClusterGroupCSVForm(CustomFieldModelCSVForm): - slug = SlugField() - - class Meta: - model = ClusterGroup - fields = ('name', 'slug', 'description') - - -class ClusterGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=ClusterGroup.objects.all(), - widget=forms.MultipleHiddenInput - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['description'] - - -class ClusterGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = ClusterGroup - field_groups = [ - ['q'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - - -# -# Clusters -# - -class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - type = DynamicModelChoiceField( - queryset=ClusterType.objects.all() - ) - group = DynamicModelChoiceField( - queryset=ClusterGroup.objects.all(), - required=False - ) - 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', - } - ) - comments = CommentField() - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = Cluster - fields = ( - 'name', 'type', 'group', 'tenant', 'region', 'site_group', 'site', 'comments', 'tags', - ) - fieldsets = ( - ('Cluster', ('name', 'type', 'group', 'region', 'site_group', 'site', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) - - -class ClusterCSVForm(CustomFieldModelCSVForm): - type = CSVModelChoiceField( - queryset=ClusterType.objects.all(), - to_field_name='name', - help_text='Type of cluster' - ) - group = CSVModelChoiceField( - queryset=ClusterGroup.objects.all(), - to_field_name='name', - required=False, - help_text='Assigned cluster group' - ) - site = CSVModelChoiceField( - queryset=Site.objects.all(), - to_field_name='name', - required=False, - help_text='Assigned site' - ) - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - to_field_name='name', - required=False, - help_text='Assigned tenant' - ) - - class Meta: - model = Cluster - fields = ('name', 'type', 'group', 'site', 'comments') - - -class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Cluster.objects.all(), - widget=forms.MultipleHiddenInput() - ) - type = DynamicModelChoiceField( - queryset=ClusterType.objects.all(), - required=False - ) - group = DynamicModelChoiceField( - queryset=ClusterGroup.objects.all(), - required=False - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - region = DynamicModelChoiceField( - queryset=Region.objects.all(), - required=False, - ) - site_group = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region', - 'group_id': '$site_group', - } - ) - comments = CommentField( - widget=SmallTextarea, - label='Comments' - ) - - class Meta: - nullable_fields = [ - 'group', 'site', 'comments', 'tenant', - ] - - -class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = Cluster - field_order = [ - 'q', 'type_id', 'region_id', 'site_id', 'group_id', 'tenant_group_id', 'tenant_id', - ] - field_groups = [ - ['q', 'tag'], - ['group_id', 'type_id'], - ['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') - ) - type_id = DynamicModelMultipleChoiceField( - queryset=ClusterType.objects.all(), - required=False, - label=_('Type'), - fetch_trigger='open' - ) - 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, - null_option='None', - query_params={ - 'region_id': '$region_id', - 'site_group_id': '$site_group_id', - }, - label=_('Site'), - fetch_trigger='open' - ) - group_id = DynamicModelMultipleChoiceField( - queryset=ClusterGroup.objects.all(), - required=False, - null_option='None', - label=_('Group'), - fetch_trigger='open' - ) - tag = TagFilterField(model) - - -class ClusterAddDevicesForm(BootstrapMixin, forms.Form): - region = DynamicModelChoiceField( - queryset=Region.objects.all(), - required=False, - null_option='None' - ) - site_group = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - null_option='None' - ) - 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' - } - ) - devices = DynamicModelMultipleChoiceField( - queryset=Device.objects.all(), - query_params={ - 'site_id': '$site', - 'rack_id': '$rack', - 'cluster_id': 'null', - } - ) - - class Meta: - fields = [ - 'region', 'site', 'rack', 'devices', - ] - - def __init__(self, cluster, *args, **kwargs): - - self.cluster = cluster - - super().__init__(*args, **kwargs) - - self.fields['devices'].choices = [] - - def clean(self): - super().clean() - - # If the Cluster is assigned to a Site, all Devices must be assigned to that Site. - if self.cluster.site is not None: - for device in self.cleaned_data.get('devices', []): - if device.site != self.cluster.site: - raise ValidationError({ - 'devices': "{} belongs to a different site ({}) than the cluster ({})".format( - device, device.site, self.cluster.site - ) - }) - - -class ClusterRemoveDevicesForm(ConfirmationForm): - pk = forms.ModelMultipleChoiceField( - queryset=Device.objects.all(), - widget=forms.MultipleHiddenInput() - ) - - -# -# Virtual Machines -# - -class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - cluster_group = DynamicModelChoiceField( - queryset=ClusterGroup.objects.all(), - required=False, - null_option='None', - initial_params={ - 'clusters': '$cluster' - } - ) - cluster = DynamicModelChoiceField( - queryset=Cluster.objects.all(), - query_params={ - 'group_id': '$cluster_group' - } - ) - role = DynamicModelChoiceField( - queryset=DeviceRole.objects.all(), - required=False, - query_params={ - "vm_role": "True" - } - ) - platform = DynamicModelChoiceField( - queryset=Platform.objects.all(), - required=False - ) - local_context_data = JSONField( - required=False, - label='' - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = VirtualMachine - fields = [ - 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', - 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', - ] - fieldsets = ( - ('Virtual Machine', ('name', 'role', 'status', 'tags')), - ('Cluster', ('cluster_group', 'cluster')), - ('Tenancy', ('tenant_group', 'tenant')), - ('Management', ('platform', 'primary_ip4', 'primary_ip6')), - ('Resources', ('vcpus', 'memory', 'disk')), - ('Config Context', ('local_context_data',)), - ) - help_texts = { - 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered " - "config context", - } - widgets = { - "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 VM - interface_ids = self.instance.interfaces.values_list('pk', flat=True) - - # Collect interface IPs - interface_ips = IPAddress.objects.filter( - address__family=family, - assigned_object_type=ContentType.objects.get_for_model(VMInterface), - assigned_object_id__in=interface_ids - ) - 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(VMInterface), - nat_inside__assigned_object_id__in=interface_ids - ) - 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 - - 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 - - -class VirtualMachineCSVForm(CustomFieldModelCSVForm): - status = CSVChoiceField( - choices=VirtualMachineStatusChoices, - required=False, - help_text='Operational status of device' - ) - cluster = CSVModelChoiceField( - queryset=Cluster.objects.all(), - to_field_name='name', - help_text='Assigned cluster' - ) - role = CSVModelChoiceField( - queryset=DeviceRole.objects.filter( - vm_role=True - ), - required=False, - to_field_name='name', - help_text='Functional role' - ) - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned tenant' - ) - platform = CSVModelChoiceField( - queryset=Platform.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned platform' - ) - - class Meta: - model = VirtualMachine - fields = ( - 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', - ) - - -class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=VirtualMachine.objects.all(), - widget=forms.MultipleHiddenInput() - ) - status = forms.ChoiceField( - choices=add_blank_choice(VirtualMachineStatusChoices), - required=False, - initial='', - widget=StaticSelect(), - ) - cluster = DynamicModelChoiceField( - queryset=Cluster.objects.all(), - required=False - ) - role = DynamicModelChoiceField( - queryset=DeviceRole.objects.filter( - vm_role=True - ), - required=False, - query_params={ - "vm_role": "True" - } - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - platform = DynamicModelChoiceField( - queryset=Platform.objects.all(), - required=False - ) - vcpus = forms.IntegerField( - required=False, - label='vCPUs' - ) - memory = forms.IntegerField( - required=False, - label='Memory (MB)' - ) - disk = forms.IntegerField( - required=False, - label='Disk (GB)' - ) - comments = CommentField( - widget=SmallTextarea, - label='Comments' - ) - - class Meta: - nullable_fields = [ - 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', - ] - - -class VirtualMachineFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm): - model = VirtualMachine - field_groups = [ - ['q', 'tag'], - ['cluster_group_id', 'cluster_type_id', 'cluster_id'], - ['region_id', 'site_group_id', 'site_id'], - ['status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data'], - ['tenant_group_id', 'tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - cluster_group_id = DynamicModelMultipleChoiceField( - queryset=ClusterGroup.objects.all(), - required=False, - null_option='None', - label=_('Cluster group'), - fetch_trigger='open' - ) - cluster_type_id = DynamicModelMultipleChoiceField( - queryset=ClusterType.objects.all(), - required=False, - null_option='None', - label=_('Cluster type'), - fetch_trigger='open' - ) - cluster_id = DynamicModelMultipleChoiceField( - queryset=Cluster.objects.all(), - required=False, - label=_('Cluster'), - fetch_trigger='open' - ) - 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, - null_option='None', - query_params={ - 'region_id': '$region_id', - 'group_id': '$site_group_id', - }, - label=_('Site'), - fetch_trigger='open' - ) - role_id = DynamicModelMultipleChoiceField( - queryset=DeviceRole.objects.all(), - required=False, - null_option='None', - query_params={ - 'vm_role': "True" - }, - label=_('Role'), - fetch_trigger='open' - ) - status = forms.MultipleChoiceField( - choices=VirtualMachineStatusChoices, - required=False, - widget=StaticSelectMultiple() - ) - platform_id = DynamicModelMultipleChoiceField( - queryset=Platform.objects.all(), - required=False, - null_option='None', - label=_('Platform'), - fetch_trigger='open' - ) - 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 - ) - ) - tag = TagFilterField(model) - - -# -# VM interfaces -# - -class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): - parent = DynamicModelChoiceField( - queryset=VMInterface.objects.all(), - required=False, - label='Parent interface' - ) - 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 = VMInterface - fields = [ - 'virtual_machine', 'name', 'enabled', 'parent', 'mac_address', 'mtu', 'description', 'mode', 'tags', - 'untagged_vlan', 'tagged_vlans', - ] - widgets = { - 'virtual_machine': forms.HiddenInput(), - 'mode': StaticSelect() - } - labels = { - 'mode': '802.1Q Mode', - } - help_texts = { - 'mode': INTERFACE_MODE_HELP_TEXT, - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') - - # Restrict parent interface assignment by VM - self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) - - # Limit VLAN choices by virtual machine - self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) - self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) - - -class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonForm): - model = VMInterface - virtual_machine = DynamicModelChoiceField( - queryset=VirtualMachine.objects.all() - ) - name_pattern = ExpandableNameField( - label='Name' - ) - enabled = forms.BooleanField( - required=False, - initial=True - ) - parent = DynamicModelChoiceField( - queryset=VMInterface.objects.all(), - required=False, - query_params={ - 'virtual_machine_id': '$virtual_machine', - } - ) - mac_address = forms.CharField( - required=False, - label='MAC Address' - ) - description = forms.CharField( - max_length=200, - required=False - ) - 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 - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - field_order = ( - 'virtual_machine', 'name_pattern', 'enabled', 'parent', 'mtu', 'mac_address', 'description', 'mode', - 'untagged_vlan', 'tagged_vlans', 'tags' - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') - - # Limit VLAN choices by virtual machine - self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) - self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) - - -class VMInterfaceCSVForm(CustomFieldModelCSVForm): - virtual_machine = CSVModelChoiceField( - queryset=VirtualMachine.objects.all(), - to_field_name='name' - ) - mode = CSVChoiceField( - choices=InterfaceModeChoices, - required=False, - help_text='IEEE 802.1Q operational mode (for L2 interfaces)' - ) - - class Meta: - model = VMInterface - fields = ( - 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode', - ) - - 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 VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=VMInterface.objects.all(), - widget=forms.MultipleHiddenInput() - ) - virtual_machine = forms.ModelChoiceField( - queryset=VirtualMachine.objects.all(), - required=False, - disabled=True, - widget=forms.HiddenInput() - ) - parent = DynamicModelChoiceField( - queryset=VMInterface.objects.all(), - required=False - ) - enabled = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - mtu = forms.IntegerField( - required=False, - min_value=INTERFACE_MTU_MIN, - max_value=INTERFACE_MTU_MAX, - label='MTU' - ) - description = forms.CharField( - max_length=100, - required=False - ) - 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 - ) - - class Meta: - nullable_fields = [ - 'parent', 'mtu', 'description', - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if 'virtual_machine' in self.initial: - vm_id = self.initial.get('virtual_machine') - - # Restrict parent interface assignment by VM - self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) - - # Limit VLAN choices by virtual machine - self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) - self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) - - else: - # See 5643 - if 'pk' in self.initial: - site = None - interfaces = VMInterface.objects.filter(pk__in=self.initial['pk']).prefetch_related( - 'virtual_machine__cluster__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.virtual_machine.cluster.site - elif interface.virtual_machine.cluster.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) - - -class VMInterfaceBulkRenameForm(BulkRenameForm): - pk = forms.ModelMultipleChoiceField( - queryset=VMInterface.objects.all(), - widget=forms.MultipleHiddenInput() - ) - - -class VMInterfaceFilterForm(BootstrapMixin, forms.Form): - model = VMInterface - field_groups = [ - ['q', 'tag'], - ['cluster_id', 'virtual_machine_id'], - ['enabled', 'mac_address'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - cluster_id = DynamicModelMultipleChoiceField( - queryset=Cluster.objects.all(), - required=False, - label=_('Cluster'), - fetch_trigger='open' - ) - virtual_machine_id = DynamicModelMultipleChoiceField( - queryset=VirtualMachine.objects.all(), - required=False, - query_params={ - 'cluster_id': '$cluster_id' - }, - label=_('Virtual machine'), - fetch_trigger='open' - ) - enabled = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - mac_address = forms.CharField( - required=False, - label='MAC address' - ) - tag = TagFilterField(model) - - -# -# Bulk VirtualMachine component creation -# - -class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form): - pk = forms.ModelMultipleChoiceField( - queryset=VirtualMachine.objects.all(), - widget=forms.MultipleHiddenInput() - ) - name_pattern = ExpandableNameField( - label='Name' - ) - - def clean_tags(self): - # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we - # must first convert the list of tags to a string. - return ','.join(self.cleaned_data.get('tags')) - - -class VMInterfaceBulkCreateForm( - form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']), - VirtualMachineBulkAddComponentForm -): - pass diff --git a/netbox/virtualization/forms/__init__.py b/netbox/virtualization/forms/__init__.py new file mode 100644 index 000000000..00f28b852 --- /dev/null +++ b/netbox/virtualization/forms/__init__.py @@ -0,0 +1,6 @@ +from .models import * +from .filtersets import * +from .object_create import * +from .bulk_create import * +from .bulk_edit import * +from .bulk_import import * diff --git a/netbox/virtualization/forms/bulk_create.py b/netbox/virtualization/forms/bulk_create.py new file mode 100644 index 000000000..6cf7c0d7c --- /dev/null +++ b/netbox/virtualization/forms/bulk_create.py @@ -0,0 +1,30 @@ +from django import forms + +from utilities.forms import BootstrapMixin, ExpandableNameField, form_from_model +from virtualization.models import VMInterface, VirtualMachine + +__all__ = ( + 'VMInterfaceBulkCreateForm', +) + + +class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form): + pk = forms.ModelMultipleChoiceField( + queryset=VirtualMachine.objects.all(), + widget=forms.MultipleHiddenInput() + ) + name_pattern = ExpandableNameField( + label='Name' + ) + + def clean_tags(self): + # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we + # must first convert the list of tags to a string. + return ','.join(self.cleaned_data.get('tags')) + + +class VMInterfaceBulkCreateForm( + form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']), + VirtualMachineBulkAddComponentForm +): + pass diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py new file mode 100644 index 000000000..c140fbc73 --- /dev/null +++ b/netbox/virtualization/forms/bulk_edit.py @@ -0,0 +1,239 @@ +from django import forms + +from dcim.choices import InterfaceModeChoices +from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN +from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup +from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm +from ipam.models import VLAN +from tenancy.models import Tenant +from utilities.forms import ( + add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, DynamicModelChoiceField, + DynamicModelMultipleChoiceField, SmallTextarea, StaticSelect +) +from virtualization.choices import * +from virtualization.models import * + +__all__ = ( + 'ClusterBulkEditForm', + 'ClusterGroupBulkEditForm', + 'ClusterTypeBulkEditForm', + 'VirtualMachineBulkEditForm', + 'VMInterfaceBulkEditForm', + 'VMInterfaceBulkRenameForm', +) + + +class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ClusterType.objects.all(), + widget=forms.MultipleHiddenInput + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['description'] + + +class ClusterGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + widget=forms.MultipleHiddenInput + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['description'] + + +class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Cluster.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = DynamicModelChoiceField( + queryset=ClusterType.objects.all(), + required=False + ) + group = DynamicModelChoiceField( + queryset=ClusterGroup.objects.all(), + required=False + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + ) + site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region', + 'group_id': '$site_group', + } + ) + comments = CommentField( + widget=SmallTextarea, + label='Comments' + ) + + class Meta: + nullable_fields = [ + 'group', 'site', 'comments', 'tenant', + ] + + +class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=VirtualMachine.objects.all(), + widget=forms.MultipleHiddenInput() + ) + status = forms.ChoiceField( + choices=add_blank_choice(VirtualMachineStatusChoices), + required=False, + initial='', + widget=StaticSelect(), + ) + cluster = DynamicModelChoiceField( + queryset=Cluster.objects.all(), + required=False + ) + role = DynamicModelChoiceField( + queryset=DeviceRole.objects.filter( + vm_role=True + ), + required=False, + query_params={ + "vm_role": "True" + } + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + platform = DynamicModelChoiceField( + queryset=Platform.objects.all(), + required=False + ) + vcpus = forms.IntegerField( + required=False, + label='vCPUs' + ) + memory = forms.IntegerField( + required=False, + label='Memory (MB)' + ) + disk = forms.IntegerField( + required=False, + label='Disk (GB)' + ) + comments = CommentField( + widget=SmallTextarea, + label='Comments' + ) + + class Meta: + nullable_fields = [ + 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', + ] + + +class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=VMInterface.objects.all(), + widget=forms.MultipleHiddenInput() + ) + virtual_machine = forms.ModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + disabled=True, + widget=forms.HiddenInput() + ) + parent = DynamicModelChoiceField( + queryset=VMInterface.objects.all(), + required=False + ) + enabled = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + mtu = forms.IntegerField( + required=False, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, + label='MTU' + ) + description = forms.CharField( + max_length=100, + required=False + ) + 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 + ) + + class Meta: + nullable_fields = [ + 'parent', 'mtu', 'description', + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if 'virtual_machine' in self.initial: + vm_id = self.initial.get('virtual_machine') + + # Restrict parent interface assignment by VM + self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) + + # Limit VLAN choices by virtual machine + self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) + self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) + + else: + # See 5643 + if 'pk' in self.initial: + site = None + interfaces = VMInterface.objects.filter(pk__in=self.initial['pk']).prefetch_related( + 'virtual_machine__cluster__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.virtual_machine.cluster.site + elif interface.virtual_machine.cluster.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) + + +class VMInterfaceBulkRenameForm(BulkRenameForm): + pk = forms.ModelMultipleChoiceField( + queryset=VMInterface.objects.all(), + widget=forms.MultipleHiddenInput() + ) diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py new file mode 100644 index 000000000..1f0496b7c --- /dev/null +++ b/netbox/virtualization/forms/bulk_import.py @@ -0,0 +1,125 @@ +from dcim.choices import InterfaceModeChoices +from dcim.models import DeviceRole, Platform, Site +from extras.forms import CustomFieldModelCSVForm +from tenancy.models import Tenant +from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField +from virtualization.choices import * +from virtualization.models import * + +__all__ = ( + 'ClusterCSVForm', + 'ClusterGroupCSVForm', + 'ClusterTypeCSVForm', + 'VirtualMachineCSVForm', + 'VMInterfaceCSVForm', +) + + +class ClusterTypeCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + + class Meta: + model = ClusterType + fields = ('name', 'slug', 'description') + + +class ClusterGroupCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + + class Meta: + model = ClusterGroup + fields = ('name', 'slug', 'description') + + +class ClusterCSVForm(CustomFieldModelCSVForm): + type = CSVModelChoiceField( + queryset=ClusterType.objects.all(), + to_field_name='name', + help_text='Type of cluster' + ) + group = CSVModelChoiceField( + queryset=ClusterGroup.objects.all(), + to_field_name='name', + required=False, + help_text='Assigned cluster group' + ) + site = CSVModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name', + required=False, + help_text='Assigned site' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + to_field_name='name', + required=False, + help_text='Assigned tenant' + ) + + class Meta: + model = Cluster + fields = ('name', 'type', 'group', 'site', 'comments') + + +class VirtualMachineCSVForm(CustomFieldModelCSVForm): + status = CSVChoiceField( + choices=VirtualMachineStatusChoices, + required=False, + help_text='Operational status of device' + ) + cluster = CSVModelChoiceField( + queryset=Cluster.objects.all(), + to_field_name='name', + help_text='Assigned cluster' + ) + role = CSVModelChoiceField( + queryset=DeviceRole.objects.filter( + vm_role=True + ), + required=False, + to_field_name='name', + help_text='Functional role' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) + platform = CSVModelChoiceField( + queryset=Platform.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned platform' + ) + + class Meta: + model = VirtualMachine + fields = ( + 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', + ) + + +class VMInterfaceCSVForm(CustomFieldModelCSVForm): + virtual_machine = CSVModelChoiceField( + queryset=VirtualMachine.objects.all(), + to_field_name='name' + ) + mode = CSVChoiceField( + choices=InterfaceModeChoices, + required=False, + help_text='IEEE 802.1Q operational mode (for L2 interfaces)' + ) + + class Meta: + model = VMInterface + fields = ( + 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode', + ) + + 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'] diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py new file mode 100644 index 000000000..0bb5c2bd7 --- /dev/null +++ b/netbox/virtualization/forms/filtersets.py @@ -0,0 +1,237 @@ +from django import forms +from django.utils.translation import gettext as _ + +from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup +from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm +from tenancy.forms import TenancyFilterForm +from utilities.forms import ( + BootstrapMixin, DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, TagFilterField, + BOOLEAN_WITH_BLANK_CHOICES, +) +from virtualization.choices import * +from virtualization.models import * + +__all__ = ( + 'ClusterFilterForm', + 'ClusterGroupFilterForm', + 'ClusterTypeFilterForm', + 'VirtualMachineFilterForm', + 'VMInterfaceFilterForm', +) + + +class ClusterTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = ClusterType + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + +class ClusterGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = ClusterGroup + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + +class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = Cluster + field_order = [ + 'q', 'type_id', 'region_id', 'site_id', 'group_id', 'tenant_group_id', 'tenant_id', + ] + field_groups = [ + ['q', 'tag'], + ['group_id', 'type_id'], + ['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') + ) + type_id = DynamicModelMultipleChoiceField( + queryset=ClusterType.objects.all(), + required=False, + label=_('Type'), + fetch_trigger='open' + ) + 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, + null_option='None', + query_params={ + 'region_id': '$region_id', + 'site_group_id': '$site_group_id', + }, + label=_('Site'), + fetch_trigger='open' + ) + group_id = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + null_option='None', + label=_('Group'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class VirtualMachineFilterForm( + BootstrapMixin, + LocalConfigContextFilterForm, + TenancyFilterForm, + CustomFieldModelFilterForm +): + model = VirtualMachine + field_groups = [ + ['q', 'tag'], + ['cluster_group_id', 'cluster_type_id', 'cluster_id'], + ['region_id', 'site_group_id', 'site_id'], + ['status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data'], + ['tenant_group_id', 'tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + cluster_group_id = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + null_option='None', + label=_('Cluster group'), + fetch_trigger='open' + ) + cluster_type_id = DynamicModelMultipleChoiceField( + queryset=ClusterType.objects.all(), + required=False, + null_option='None', + label=_('Cluster type'), + fetch_trigger='open' + ) + cluster_id = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_('Cluster'), + fetch_trigger='open' + ) + 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, + null_option='None', + query_params={ + 'region_id': '$region_id', + 'group_id': '$site_group_id', + }, + label=_('Site'), + fetch_trigger='open' + ) + role_id = DynamicModelMultipleChoiceField( + queryset=DeviceRole.objects.all(), + required=False, + null_option='None', + query_params={ + 'vm_role': "True" + }, + label=_('Role'), + fetch_trigger='open' + ) + status = forms.MultipleChoiceField( + choices=VirtualMachineStatusChoices, + required=False, + widget=StaticSelectMultiple() + ) + platform_id = DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), + required=False, + null_option='None', + label=_('Platform'), + fetch_trigger='open' + ) + 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 + ) + ) + tag = TagFilterField(model) + + +class VMInterfaceFilterForm(BootstrapMixin, forms.Form): + model = VMInterface + field_groups = [ + ['q', 'tag'], + ['cluster_id', 'virtual_machine_id'], + ['enabled', 'mac_address'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + cluster_id = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_('Cluster'), + fetch_trigger='open' + ) + virtual_machine_id = DynamicModelMultipleChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + query_params={ + 'cluster_id': '$cluster_id' + }, + label=_('Virtual machine'), + fetch_trigger='open' + ) + enabled = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + mac_address = forms.CharField( + required=False, + label='MAC address' + ) + tag = TagFilterField(model) diff --git a/netbox/virtualization/forms/models.py b/netbox/virtualization/forms/models.py new file mode 100644 index 000000000..d66bc9f1f --- /dev/null +++ b/netbox/virtualization/forms/models.py @@ -0,0 +1,324 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError + +from dcim.forms.common import InterfaceCommonForm +from dcim.forms.models import INTERFACE_MODE_HELP_TEXT +from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup +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 ( + BootstrapMixin, CommentField, ConfirmationForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + JSONField, SlugField, StaticSelect, +) +from virtualization.models import * + +__all__ = ( + 'ClusterAddDevicesForm', + 'ClusterForm', + 'ClusterGroupForm', + 'ClusterRemoveDevicesForm', + 'ClusterTypeForm', + 'VirtualMachineForm', + 'VMInterfaceForm', +) + + +class ClusterTypeForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + + class Meta: + model = ClusterType + fields = [ + 'name', 'slug', 'description', + ] + + +class ClusterGroupForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + + class Meta: + model = ClusterGroup + fields = [ + 'name', 'slug', 'description', + ] + + +class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + type = DynamicModelChoiceField( + queryset=ClusterType.objects.all() + ) + group = DynamicModelChoiceField( + queryset=ClusterGroup.objects.all(), + required=False + ) + 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', + } + ) + comments = CommentField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = Cluster + fields = ( + 'name', 'type', 'group', 'tenant', 'region', 'site_group', 'site', 'comments', 'tags', + ) + fieldsets = ( + ('Cluster', ('name', 'type', 'group', 'region', 'site_group', 'site', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + + +class ClusterAddDevicesForm(BootstrapMixin, forms.Form): + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + null_option='None' + ) + site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + null_option='None' + ) + 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' + } + ) + devices = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + query_params={ + 'site_id': '$site', + 'rack_id': '$rack', + 'cluster_id': 'null', + } + ) + + class Meta: + fields = [ + 'region', 'site', 'rack', 'devices', + ] + + def __init__(self, cluster, *args, **kwargs): + + self.cluster = cluster + + super().__init__(*args, **kwargs) + + self.fields['devices'].choices = [] + + def clean(self): + super().clean() + + # If the Cluster is assigned to a Site, all Devices must be assigned to that Site. + if self.cluster.site is not None: + for device in self.cleaned_data.get('devices', []): + if device.site != self.cluster.site: + raise ValidationError({ + 'devices': "{} belongs to a different site ({}) than the cluster ({})".format( + device, device.site, self.cluster.site + ) + }) + + +class ClusterRemoveDevicesForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField( + queryset=Device.objects.all(), + widget=forms.MultipleHiddenInput() + ) + + +class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + cluster_group = DynamicModelChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + null_option='None', + initial_params={ + 'clusters': '$cluster' + } + ) + cluster = DynamicModelChoiceField( + queryset=Cluster.objects.all(), + query_params={ + 'group_id': '$cluster_group' + } + ) + role = DynamicModelChoiceField( + queryset=DeviceRole.objects.all(), + required=False, + query_params={ + "vm_role": "True" + } + ) + platform = DynamicModelChoiceField( + queryset=Platform.objects.all(), + required=False + ) + local_context_data = JSONField( + required=False, + label='' + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = VirtualMachine + fields = [ + 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', + 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', + ] + fieldsets = ( + ('Virtual Machine', ('name', 'role', 'status', 'tags')), + ('Cluster', ('cluster_group', 'cluster')), + ('Tenancy', ('tenant_group', 'tenant')), + ('Management', ('platform', 'primary_ip4', 'primary_ip6')), + ('Resources', ('vcpus', 'memory', 'disk')), + ('Config Context', ('local_context_data',)), + ) + help_texts = { + 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered " + "config context", + } + widgets = { + "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 VM + interface_ids = self.instance.interfaces.values_list('pk', flat=True) + + # Collect interface IPs + interface_ips = IPAddress.objects.filter( + address__family=family, + assigned_object_type=ContentType.objects.get_for_model(VMInterface), + assigned_object_id__in=interface_ids + ) + 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(VMInterface), + nat_inside__assigned_object_id__in=interface_ids + ) + 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 + + 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 + + +class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): + parent = DynamicModelChoiceField( + queryset=VMInterface.objects.all(), + required=False, + label='Parent interface' + ) + 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 = VMInterface + fields = [ + 'virtual_machine', 'name', 'enabled', 'parent', 'mac_address', 'mtu', 'description', 'mode', 'tags', + 'untagged_vlan', 'tagged_vlans', + ] + widgets = { + 'virtual_machine': forms.HiddenInput(), + 'mode': StaticSelect() + } + labels = { + 'mode': '802.1Q Mode', + } + help_texts = { + 'mode': INTERFACE_MODE_HELP_TEXT, + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') + + # Restrict parent interface assignment by VM + self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) + + # Limit VLAN choices by virtual machine + self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) + self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) diff --git a/netbox/virtualization/forms/object_create.py b/netbox/virtualization/forms/object_create.py new file mode 100644 index 000000000..b58fb51f8 --- /dev/null +++ b/netbox/virtualization/forms/object_create.py @@ -0,0 +1,74 @@ +from django import forms + +from dcim.choices import InterfaceModeChoices +from dcim.forms.common import InterfaceCommonForm +from extras.forms import CustomFieldsMixin +from extras.models import Tag +from ipam.models import VLAN +from utilities.forms import ( + add_blank_choice, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, + StaticSelect, +) +from virtualization.models import VMInterface, VirtualMachine + +__all__ = ( + 'VMInterfaceCreateForm', +) + + +class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonForm): + model = VMInterface + virtual_machine = DynamicModelChoiceField( + queryset=VirtualMachine.objects.all() + ) + name_pattern = ExpandableNameField( + label='Name' + ) + enabled = forms.BooleanField( + required=False, + initial=True + ) + parent = DynamicModelChoiceField( + queryset=VMInterface.objects.all(), + required=False, + query_params={ + 'virtual_machine_id': '$virtual_machine', + } + ) + mac_address = forms.CharField( + required=False, + label='MAC Address' + ) + description = forms.CharField( + max_length=200, + required=False + ) + 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 + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + field_order = ( + 'virtual_machine', 'name_pattern', 'enabled', 'parent', 'mtu', 'mac_address', 'description', 'mode', + 'untagged_vlan', 'tagged_vlans', 'tags' + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') + + # Limit VLAN choices by virtual machine + self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) + self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)