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, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm, ) from extras.models import Tag from ipam.models import IPAddress, VLAN from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea, StaticSelect2, StaticSelect2Multiple, 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 = ClusterType.csv_headers class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ClusterType.objects.all(), widget=forms.MultipleHiddenInput ) description = forms.CharField( max_length=200, required=False ) class Meta: nullable_fields = ['description'] # # 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 = ClusterGroup.csv_headers class ClusterGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), widget=forms.MultipleHiddenInput ) description = forms.CharField( max_length=200, required=False ) class Meta: nullable_fields = ['description'] # # 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 = Cluster.csv_headers class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): 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, CustomFieldFilterForm): model = Cluster field_order = [ 'q', 'type_id', 'region_id', 'site_id', 'group_id', 'tenant_group_id', 'tenant_id', ] q = forms.CharField( required=False, label=_('Search') ) type_id = DynamicModelMultipleChoiceField( queryset=ClusterType.objects.all(), required=False, label=_('Type') ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, null_option='None', query_params={ 'region_id': '$region_id' }, label=_('Site') ) group_id = DynamicModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), required=False, null_option='None', label=_('Group') ) 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": StaticSelect2(), 'primary_ip4': StaticSelect2(), 'primary_ip6': StaticSelect2(), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance.pk: # Compile list of choices for primary IPv4 and IPv6 addresses for family in [4, 6]: ip_choices = [(None, '---------')] # Gather PKs of all interfaces belonging to this 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 = VirtualMachine.csv_headers class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=VirtualMachine.objects.all(), widget=forms.MultipleHiddenInput() ) status = forms.ChoiceField( choices=add_blank_choice(VirtualMachineStatusChoices), required=False, initial='', widget=StaticSelect2(), ) 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, TenancyFilterForm, CustomFieldFilterForm): model = VirtualMachine field_order = [ 'q', 'cluster_group_id', 'cluster_type_id', 'cluster_id', 'status', 'role_id', 'region_id', 'site_group_id', 'site_id', 'tenant_group_id', 'tenant_id', 'platform_id', 'mac_address', ] q = forms.CharField( required=False, label=_('Search') ) cluster_group_id = DynamicModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), required=False, null_option='None', label=_('Cluster group') ) cluster_type_id = DynamicModelMultipleChoiceField( queryset=ClusterType.objects.all(), required=False, null_option='None', label=_('Cluster type') ) cluster_id = DynamicModelMultipleChoiceField( queryset=Cluster.objects.all(), required=False, label=_('Cluster') ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, null_option='None', query_params={ 'region_id': '$region_id', 'group_id': '$site_group_id', }, label=_('Site') ) role_id = DynamicModelMultipleChoiceField( queryset=DeviceRole.objects.all(), required=False, null_option='None', query_params={ 'vm_role': "True" }, label=_('Role') ) status = forms.MultipleChoiceField( choices=VirtualMachineStatusChoices, required=False, widget=StaticSelect2Multiple() ) platform_id = DynamicModelMultipleChoiceField( queryset=Platform.objects.all(), required=False, null_option='None', label=_('Platform') ) mac_address = forms.CharField( required=False, label='MAC address' ) has_primary_ip = forms.NullBooleanField( required=False, label='Has a primary IP', widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) tag = TagFilterField(model) # # VM interfaces # class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): parent = DynamicModelChoiceField( queryset=VMInterface.objects.all(), required=False, label='Parent interface' ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, label='Untagged VLAN' ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, label='Tagged VLANs' ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False ) class Meta: model = VMInterface fields = [ 'virtual_machine', 'name', 'enabled', 'parent', 'mac_address', 'mtu', 'description', 'mode', 'tags', 'untagged_vlan', 'tagged_vlans', ] widgets = { 'virtual_machine': forms.HiddenInput(), 'mode': StaticSelect2() } 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, InterfaceCommonForm): 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, display_field='display_name', query_params={ 'virtual_machine_id': '$virtual_machine', } ) mtu = forms.IntegerField( required=False, min_value=INTERFACE_MTU_MIN, max_value=INTERFACE_MTU_MAX, label='MTU' ) mac_address = forms.CharField( required=False, label='MAC Address' ) description = forms.CharField( max_length=200, required=False ) mode = forms.ChoiceField( choices=add_blank_choice(InterfaceModeChoices), required=False, widget=StaticSelect2(), ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False ) 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(CSVModelForm): 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 = VMInterface.csv_headers 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, BulkEditForm): 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, display_field='display_name' ) 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=StaticSelect2() ) 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 cluster_id = DynamicModelMultipleChoiceField( queryset=Cluster.objects.all(), required=False, label=_('Cluster') ) virtual_machine_id = DynamicModelMultipleChoiceField( queryset=VirtualMachine.objects.all(), required=False, query_params={ 'cluster_id': '$cluster_id' }, label=_('Virtual machine') ) enabled = forms.NullBooleanField( required=False, widget=StaticSelect2( 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