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