From db522f96be87ee2d4a77bc1bbab5e12e2e7146b7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 28 Sep 2021 10:25:07 -0400 Subject: [PATCH] Refactor virtualization forms --- netbox/virtualization/forms.py | 971 ------------------- netbox/virtualization/forms/__init__.py | 6 + netbox/virtualization/forms/bulk_create.py | 30 + netbox/virtualization/forms/bulk_edit.py | 239 +++++ netbox/virtualization/forms/bulk_import.py | 125 +++ netbox/virtualization/forms/filtersets.py | 237 +++++ netbox/virtualization/forms/models.py | 324 +++++++ netbox/virtualization/forms/object_create.py | 74 ++ 8 files changed, 1035 insertions(+), 971 deletions(-) delete mode 100644 netbox/virtualization/forms.py create mode 100644 netbox/virtualization/forms/__init__.py create mode 100644 netbox/virtualization/forms/bulk_create.py create mode 100644 netbox/virtualization/forms/bulk_edit.py create mode 100644 netbox/virtualization/forms/bulk_import.py create mode 100644 netbox/virtualization/forms/filtersets.py create mode 100644 netbox/virtualization/forms/models.py create mode 100644 netbox/virtualization/forms/object_create.py diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py deleted file mode 100644 index 74bf32e54..000000000 --- a/netbox/virtualization/forms.py +++ /dev/null @@ -1,971 +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.models import INTERFACE_MODE_HELP_TEXT -from dcim.forms.common import InterfaceCommonForm -from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup -from extras.forms import ( - AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, - CustomFieldModelFilterForm, CustomFieldsMixin, LocalConfigContextFilterForm, -) -from extras.models import Tag -from ipam.models import IPAddress, VLAN, VLANGroup -from tenancy.forms import TenancyFilterForm, TenancyForm -from tenancy.models import Tenant -from utilities.forms import ( - add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, ConfirmationForm, - CSVChoiceField, CSVModelChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, - form_from_model, JSONField, SlugField, SmallTextarea, StaticSelect, StaticSelectMultiple, TagFilterField, - BOOLEAN_WITH_BLANK_CHOICES, -) -from .choices import * -from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface - - -# -# Cluster types -# - -class ClusterTypeForm(BootstrapMixin, CustomFieldModelForm): - slug = SlugField() - - class Meta: - model = ClusterType - fields = [ - 'name', 'slug', 'description', - ] - - -class ClusterTypeCSVForm(CustomFieldModelCSVForm): - slug = SlugField() - - class Meta: - model = ClusterType - fields = ('name', 'slug', 'description') - - -class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=ClusterType.objects.all(), - widget=forms.MultipleHiddenInput - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['description'] - - -class ClusterTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = ClusterType - field_groups = [ - ['q'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - - -# -# Cluster groups -# - -class ClusterGroupForm(BootstrapMixin, CustomFieldModelForm): - slug = SlugField() - - class Meta: - model = ClusterGroup - fields = [ - 'name', 'slug', 'description', - ] - - -class ClusterGroupCSVForm(CustomFieldModelCSVForm): - slug = SlugField() - - class Meta: - model = ClusterGroup - fields = ('name', 'slug', 'description') - - -class ClusterGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=ClusterGroup.objects.all(), - widget=forms.MultipleHiddenInput - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['description'] - - -class ClusterGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = ClusterGroup - field_groups = [ - ['q'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - - -# -# Clusters -# - -class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - type = DynamicModelChoiceField( - queryset=ClusterType.objects.all() - ) - group = DynamicModelChoiceField( - queryset=ClusterGroup.objects.all(), - required=False - ) - region = DynamicModelChoiceField( - queryset=Region.objects.all(), - required=False, - initial_params={ - 'sites': '$site' - } - ) - site_group = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - initial_params={ - 'sites': '$site' - } - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region', - 'group_id': '$site_group', - } - ) - comments = CommentField() - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = Cluster - fields = ( - 'name', 'type', 'group', 'tenant', 'region', 'site_group', 'site', 'comments', 'tags', - ) - fieldsets = ( - ('Cluster', ('name', 'type', 'group', 'region', 'site_group', 'site', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) - - -class ClusterCSVForm(CustomFieldModelCSVForm): - type = CSVModelChoiceField( - queryset=ClusterType.objects.all(), - to_field_name='name', - help_text='Type of cluster' - ) - group = CSVModelChoiceField( - queryset=ClusterGroup.objects.all(), - to_field_name='name', - required=False, - help_text='Assigned cluster group' - ) - site = CSVModelChoiceField( - queryset=Site.objects.all(), - to_field_name='name', - required=False, - help_text='Assigned site' - ) - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - to_field_name='name', - required=False, - help_text='Assigned tenant' - ) - - class Meta: - model = Cluster - fields = ('name', 'type', 'group', 'site', 'comments') - - -class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Cluster.objects.all(), - widget=forms.MultipleHiddenInput() - ) - type = DynamicModelChoiceField( - queryset=ClusterType.objects.all(), - required=False - ) - group = DynamicModelChoiceField( - queryset=ClusterGroup.objects.all(), - required=False - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - region = DynamicModelChoiceField( - queryset=Region.objects.all(), - required=False, - ) - site_group = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region', - 'group_id': '$site_group', - } - ) - comments = CommentField( - widget=SmallTextarea, - label='Comments' - ) - - class Meta: - nullable_fields = [ - 'group', 'site', 'comments', 'tenant', - ] - - -class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): - model = Cluster - field_order = [ - 'q', 'type_id', 'region_id', 'site_id', 'group_id', 'tenant_group_id', 'tenant_id', - ] - field_groups = [ - ['q', 'tag'], - ['group_id', 'type_id'], - ['region_id', 'site_group_id', 'site_id'], - ['tenant_group_id', 'tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - type_id = DynamicModelMultipleChoiceField( - queryset=ClusterType.objects.all(), - required=False, - label=_('Type'), - fetch_trigger='open' - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_group_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site group'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - null_option='None', - query_params={ - 'region_id': '$region_id', - 'site_group_id': '$site_group_id', - }, - label=_('Site'), - fetch_trigger='open' - ) - group_id = DynamicModelMultipleChoiceField( - queryset=ClusterGroup.objects.all(), - required=False, - null_option='None', - label=_('Group'), - fetch_trigger='open' - ) - tag = TagFilterField(model) - - -class ClusterAddDevicesForm(BootstrapMixin, forms.Form): - region = DynamicModelChoiceField( - queryset=Region.objects.all(), - required=False, - null_option='None' - ) - site_group = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - null_option='None' - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region', - 'group_id': '$site_group', - } - ) - rack = DynamicModelChoiceField( - queryset=Rack.objects.all(), - required=False, - null_option='None', - query_params={ - 'site_id': '$site' - } - ) - devices = DynamicModelMultipleChoiceField( - queryset=Device.objects.all(), - query_params={ - 'site_id': '$site', - 'rack_id': '$rack', - 'cluster_id': 'null', - } - ) - - class Meta: - fields = [ - 'region', 'site', 'rack', 'devices', - ] - - def __init__(self, cluster, *args, **kwargs): - - self.cluster = cluster - - super().__init__(*args, **kwargs) - - self.fields['devices'].choices = [] - - def clean(self): - super().clean() - - # If the Cluster is assigned to a Site, all Devices must be assigned to that Site. - if self.cluster.site is not None: - for device in self.cleaned_data.get('devices', []): - if device.site != self.cluster.site: - raise ValidationError({ - 'devices': "{} belongs to a different site ({}) than the cluster ({})".format( - device, device.site, self.cluster.site - ) - }) - - -class ClusterRemoveDevicesForm(ConfirmationForm): - pk = forms.ModelMultipleChoiceField( - queryset=Device.objects.all(), - widget=forms.MultipleHiddenInput() - ) - - -# -# Virtual Machines -# - -class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - cluster_group = DynamicModelChoiceField( - queryset=ClusterGroup.objects.all(), - required=False, - null_option='None', - initial_params={ - 'clusters': '$cluster' - } - ) - cluster = DynamicModelChoiceField( - queryset=Cluster.objects.all(), - query_params={ - 'group_id': '$cluster_group' - } - ) - role = DynamicModelChoiceField( - queryset=DeviceRole.objects.all(), - required=False, - query_params={ - "vm_role": "True" - } - ) - platform = DynamicModelChoiceField( - queryset=Platform.objects.all(), - required=False - ) - local_context_data = JSONField( - required=False, - label='' - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = VirtualMachine - fields = [ - 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', - 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', - ] - fieldsets = ( - ('Virtual Machine', ('name', 'role', 'status', 'tags')), - ('Cluster', ('cluster_group', 'cluster')), - ('Tenancy', ('tenant_group', 'tenant')), - ('Management', ('platform', 'primary_ip4', 'primary_ip6')), - ('Resources', ('vcpus', 'memory', 'disk')), - ('Config Context', ('local_context_data',)), - ) - help_texts = { - 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered " - "config context", - } - widgets = { - "status": StaticSelect(), - 'primary_ip4': StaticSelect(), - 'primary_ip6': StaticSelect(), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - if self.instance.pk: - - # Compile list of choices for primary IPv4 and IPv6 addresses - for family in [4, 6]: - ip_choices = [(None, '---------')] - - # Gather PKs of all interfaces belonging to this VM - interface_ids = self.instance.interfaces.values_list('pk', flat=True) - - # Collect interface IPs - interface_ips = IPAddress.objects.filter( - address__family=family, - assigned_object_type=ContentType.objects.get_for_model(VMInterface), - assigned_object_id__in=interface_ids - ) - if interface_ips: - ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips] - ip_choices.append(('Interface IPs', ip_list)) - # Collect NAT IPs - nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter( - address__family=family, - nat_inside__assigned_object_type=ContentType.objects.get_for_model(VMInterface), - nat_inside__assigned_object_id__in=interface_ids - ) - if nat_ips: - ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips] - ip_choices.append(('NAT IPs', ip_list)) - self.fields['primary_ip{}'.format(family)].choices = ip_choices - - else: - - # An object that doesn't exist yet can't have any IPs assigned to it - self.fields['primary_ip4'].choices = [] - self.fields['primary_ip4'].widget.attrs['readonly'] = True - self.fields['primary_ip6'].choices = [] - self.fields['primary_ip6'].widget.attrs['readonly'] = True - - -class VirtualMachineCSVForm(CustomFieldModelCSVForm): - status = CSVChoiceField( - choices=VirtualMachineStatusChoices, - required=False, - help_text='Operational status of device' - ) - cluster = CSVModelChoiceField( - queryset=Cluster.objects.all(), - to_field_name='name', - help_text='Assigned cluster' - ) - role = CSVModelChoiceField( - queryset=DeviceRole.objects.filter( - vm_role=True - ), - required=False, - to_field_name='name', - help_text='Functional role' - ) - tenant = CSVModelChoiceField( - queryset=Tenant.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned tenant' - ) - platform = CSVModelChoiceField( - queryset=Platform.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned platform' - ) - - class Meta: - model = VirtualMachine - fields = ( - 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', - ) - - -class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=VirtualMachine.objects.all(), - widget=forms.MultipleHiddenInput() - ) - status = forms.ChoiceField( - choices=add_blank_choice(VirtualMachineStatusChoices), - required=False, - initial='', - widget=StaticSelect(), - ) - cluster = DynamicModelChoiceField( - queryset=Cluster.objects.all(), - required=False - ) - role = DynamicModelChoiceField( - queryset=DeviceRole.objects.filter( - vm_role=True - ), - required=False, - query_params={ - "vm_role": "True" - } - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - platform = DynamicModelChoiceField( - queryset=Platform.objects.all(), - required=False - ) - vcpus = forms.IntegerField( - required=False, - label='vCPUs' - ) - memory = forms.IntegerField( - required=False, - label='Memory (MB)' - ) - disk = forms.IntegerField( - required=False, - label='Disk (GB)' - ) - comments = CommentField( - widget=SmallTextarea, - label='Comments' - ) - - class Meta: - nullable_fields = [ - 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', - ] - - -class VirtualMachineFilterForm( - BootstrapMixin, - LocalConfigContextFilterForm, - TenancyFilterForm, - CustomFieldModelFilterForm -): - model = VirtualMachine - field_groups = [ - ['q', 'tag'], - ['cluster_group_id', 'cluster_type_id', 'cluster_id'], - ['region_id', 'site_group_id', 'site_id'], - ['status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data'], - ['tenant_group_id', 'tenant_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - cluster_group_id = DynamicModelMultipleChoiceField( - queryset=ClusterGroup.objects.all(), - required=False, - null_option='None', - label=_('Cluster group'), - fetch_trigger='open' - ) - cluster_type_id = DynamicModelMultipleChoiceField( - queryset=ClusterType.objects.all(), - required=False, - null_option='None', - label=_('Cluster type'), - fetch_trigger='open' - ) - cluster_id = DynamicModelMultipleChoiceField( - queryset=Cluster.objects.all(), - required=False, - label=_('Cluster'), - fetch_trigger='open' - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Region'), - fetch_trigger='open' - ) - site_group_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site group'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - null_option='None', - query_params={ - 'region_id': '$region_id', - 'group_id': '$site_group_id', - }, - label=_('Site'), - fetch_trigger='open' - ) - role_id = DynamicModelMultipleChoiceField( - queryset=DeviceRole.objects.all(), - required=False, - null_option='None', - query_params={ - 'vm_role': "True" - }, - label=_('Role'), - fetch_trigger='open' - ) - status = forms.MultipleChoiceField( - choices=VirtualMachineStatusChoices, - required=False, - widget=StaticSelectMultiple() - ) - platform_id = DynamicModelMultipleChoiceField( - queryset=Platform.objects.all(), - required=False, - null_option='None', - label=_('Platform'), - fetch_trigger='open' - ) - mac_address = forms.CharField( - required=False, - label='MAC address' - ) - has_primary_ip = forms.NullBooleanField( - required=False, - label='Has a primary IP', - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - tag = TagFilterField(model) - - -# -# VM interfaces -# - -class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): - parent = DynamicModelChoiceField( - queryset=VMInterface.objects.all(), - required=False, - label='Parent interface' - ) - vlan_group = DynamicModelChoiceField( - queryset=VLANGroup.objects.all(), - required=False, - label='VLAN group' - ) - untagged_vlan = DynamicModelChoiceField( - queryset=VLAN.objects.all(), - required=False, - label='Untagged VLAN', - query_params={ - 'group_id': '$vlan_group', - } - ) - tagged_vlans = DynamicModelMultipleChoiceField( - queryset=VLAN.objects.all(), - required=False, - label='Tagged VLANs', - query_params={ - 'group_id': '$vlan_group', - } - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = VMInterface - fields = [ - 'virtual_machine', 'name', 'enabled', 'parent', 'mac_address', 'mtu', 'description', 'mode', 'tags', - 'untagged_vlan', 'tagged_vlans', - ] - widgets = { - 'virtual_machine': forms.HiddenInput(), - 'mode': StaticSelect() - } - labels = { - 'mode': '802.1Q Mode', - } - help_texts = { - 'mode': INTERFACE_MODE_HELP_TEXT, - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') - - # Restrict parent interface assignment by VM - self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) - - # Limit VLAN choices by virtual machine - self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) - self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) - - -class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonForm): - model = VMInterface - virtual_machine = DynamicModelChoiceField( - queryset=VirtualMachine.objects.all() - ) - name_pattern = ExpandableNameField( - label='Name' - ) - enabled = forms.BooleanField( - required=False, - initial=True - ) - parent = DynamicModelChoiceField( - queryset=VMInterface.objects.all(), - required=False, - query_params={ - 'virtual_machine_id': '$virtual_machine', - } - ) - mac_address = forms.CharField( - required=False, - label='MAC Address' - ) - description = forms.CharField( - max_length=200, - required=False - ) - mode = forms.ChoiceField( - choices=add_blank_choice(InterfaceModeChoices), - required=False, - widget=StaticSelect(), - ) - untagged_vlan = DynamicModelChoiceField( - queryset=VLAN.objects.all(), - required=False - ) - tagged_vlans = DynamicModelMultipleChoiceField( - queryset=VLAN.objects.all(), - required=False - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - field_order = ( - 'virtual_machine', 'name_pattern', 'enabled', 'parent', 'mtu', 'mac_address', 'description', 'mode', - 'untagged_vlan', 'tagged_vlans', 'tags' - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') - - # Limit VLAN choices by virtual machine - self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) - self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) - - -class VMInterfaceCSVForm(CustomFieldModelCSVForm): - virtual_machine = CSVModelChoiceField( - queryset=VirtualMachine.objects.all(), - to_field_name='name' - ) - mode = CSVChoiceField( - choices=InterfaceModeChoices, - required=False, - help_text='IEEE 802.1Q operational mode (for L2 interfaces)' - ) - - class Meta: - model = VMInterface - fields = ( - 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode', - ) - - def clean_enabled(self): - # Make sure enabled is True when it's not included in the uploaded data - if 'enabled' not in self.data: - return True - else: - return self.cleaned_data['enabled'] - - -class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=VMInterface.objects.all(), - widget=forms.MultipleHiddenInput() - ) - virtual_machine = forms.ModelChoiceField( - queryset=VirtualMachine.objects.all(), - required=False, - disabled=True, - widget=forms.HiddenInput() - ) - parent = DynamicModelChoiceField( - queryset=VMInterface.objects.all(), - required=False - ) - enabled = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - mtu = forms.IntegerField( - required=False, - min_value=INTERFACE_MTU_MIN, - max_value=INTERFACE_MTU_MAX, - label='MTU' - ) - description = forms.CharField( - max_length=100, - required=False - ) - mode = forms.ChoiceField( - choices=add_blank_choice(InterfaceModeChoices), - required=False, - widget=StaticSelect() - ) - untagged_vlan = DynamicModelChoiceField( - queryset=VLAN.objects.all(), - required=False - ) - tagged_vlans = DynamicModelMultipleChoiceField( - queryset=VLAN.objects.all(), - required=False - ) - - class Meta: - nullable_fields = [ - 'parent', 'mtu', 'description', - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if 'virtual_machine' in self.initial: - vm_id = self.initial.get('virtual_machine') - - # Restrict parent interface assignment by VM - self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) - - # Limit VLAN choices by virtual machine - self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) - self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) - - else: - # See 5643 - if 'pk' in self.initial: - site = None - interfaces = VMInterface.objects.filter(pk__in=self.initial['pk']).prefetch_related( - 'virtual_machine__cluster__site' - ) - - # Check interface sites. First interface should set site, further interfaces will either continue the - # loop or reset back to no site and break the loop. - for interface in interfaces: - if site is None: - site = interface.virtual_machine.cluster.site - elif interface.virtual_machine.cluster.site is not site: - site = None - break - - if site is not None: - self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk) - self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk) - - -class VMInterfaceBulkRenameForm(BulkRenameForm): - pk = forms.ModelMultipleChoiceField( - queryset=VMInterface.objects.all(), - widget=forms.MultipleHiddenInput() - ) - - -class VMInterfaceFilterForm(BootstrapMixin, forms.Form): - model = VMInterface - field_groups = [ - ['q', 'tag'], - ['cluster_id', 'virtual_machine_id'], - ['enabled', 'mac_address'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - cluster_id = DynamicModelMultipleChoiceField( - queryset=Cluster.objects.all(), - required=False, - label=_('Cluster'), - fetch_trigger='open' - ) - virtual_machine_id = DynamicModelMultipleChoiceField( - queryset=VirtualMachine.objects.all(), - required=False, - query_params={ - 'cluster_id': '$cluster_id' - }, - label=_('Virtual machine'), - fetch_trigger='open' - ) - enabled = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - mac_address = forms.CharField( - required=False, - label='MAC address' - ) - tag = TagFilterField(model) - - -# -# Bulk VirtualMachine component creation -# - -class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form): - pk = forms.ModelMultipleChoiceField( - queryset=VirtualMachine.objects.all(), - widget=forms.MultipleHiddenInput() - ) - name_pattern = ExpandableNameField( - label='Name' - ) - - def clean_tags(self): - # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we - # must first convert the list of tags to a string. - return ','.join(self.cleaned_data.get('tags')) - - -class VMInterfaceBulkCreateForm( - form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']), - VirtualMachineBulkAddComponentForm -): - pass diff --git a/netbox/virtualization/forms/__init__.py b/netbox/virtualization/forms/__init__.py new file mode 100644 index 000000000..00f28b852 --- /dev/null +++ b/netbox/virtualization/forms/__init__.py @@ -0,0 +1,6 @@ +from .models import * +from .filtersets import * +from .object_create import * +from .bulk_create import * +from .bulk_edit import * +from .bulk_import import * diff --git a/netbox/virtualization/forms/bulk_create.py b/netbox/virtualization/forms/bulk_create.py new file mode 100644 index 000000000..6cf7c0d7c --- /dev/null +++ b/netbox/virtualization/forms/bulk_create.py @@ -0,0 +1,30 @@ +from django import forms + +from utilities.forms import BootstrapMixin, ExpandableNameField, form_from_model +from virtualization.models import VMInterface, VirtualMachine + +__all__ = ( + 'VMInterfaceBulkCreateForm', +) + + +class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form): + pk = forms.ModelMultipleChoiceField( + queryset=VirtualMachine.objects.all(), + widget=forms.MultipleHiddenInput() + ) + name_pattern = ExpandableNameField( + label='Name' + ) + + def clean_tags(self): + # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we + # must first convert the list of tags to a string. + return ','.join(self.cleaned_data.get('tags')) + + +class VMInterfaceBulkCreateForm( + form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']), + VirtualMachineBulkAddComponentForm +): + pass diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py new file mode 100644 index 000000000..c140fbc73 --- /dev/null +++ b/netbox/virtualization/forms/bulk_edit.py @@ -0,0 +1,239 @@ +from django import forms + +from dcim.choices import InterfaceModeChoices +from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN +from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup +from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm +from ipam.models import VLAN +from tenancy.models import Tenant +from utilities.forms import ( + add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, DynamicModelChoiceField, + DynamicModelMultipleChoiceField, SmallTextarea, StaticSelect +) +from virtualization.choices import * +from virtualization.models import * + +__all__ = ( + 'ClusterBulkEditForm', + 'ClusterGroupBulkEditForm', + 'ClusterTypeBulkEditForm', + 'VirtualMachineBulkEditForm', + 'VMInterfaceBulkEditForm', + 'VMInterfaceBulkRenameForm', +) + + +class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ClusterType.objects.all(), + widget=forms.MultipleHiddenInput + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['description'] + + +class ClusterGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + widget=forms.MultipleHiddenInput + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['description'] + + +class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Cluster.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = DynamicModelChoiceField( + queryset=ClusterType.objects.all(), + required=False + ) + group = DynamicModelChoiceField( + queryset=ClusterGroup.objects.all(), + required=False + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + ) + site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region', + 'group_id': '$site_group', + } + ) + comments = CommentField( + widget=SmallTextarea, + label='Comments' + ) + + class Meta: + nullable_fields = [ + 'group', 'site', 'comments', 'tenant', + ] + + +class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=VirtualMachine.objects.all(), + widget=forms.MultipleHiddenInput() + ) + status = forms.ChoiceField( + choices=add_blank_choice(VirtualMachineStatusChoices), + required=False, + initial='', + widget=StaticSelect(), + ) + cluster = DynamicModelChoiceField( + queryset=Cluster.objects.all(), + required=False + ) + role = DynamicModelChoiceField( + queryset=DeviceRole.objects.filter( + vm_role=True + ), + required=False, + query_params={ + "vm_role": "True" + } + ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + platform = DynamicModelChoiceField( + queryset=Platform.objects.all(), + required=False + ) + vcpus = forms.IntegerField( + required=False, + label='vCPUs' + ) + memory = forms.IntegerField( + required=False, + label='Memory (MB)' + ) + disk = forms.IntegerField( + required=False, + label='Disk (GB)' + ) + comments = CommentField( + widget=SmallTextarea, + label='Comments' + ) + + class Meta: + nullable_fields = [ + 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', + ] + + +class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=VMInterface.objects.all(), + widget=forms.MultipleHiddenInput() + ) + virtual_machine = forms.ModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + disabled=True, + widget=forms.HiddenInput() + ) + parent = DynamicModelChoiceField( + queryset=VMInterface.objects.all(), + required=False + ) + enabled = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + mtu = forms.IntegerField( + required=False, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, + label='MTU' + ) + description = forms.CharField( + max_length=100, + required=False + ) + mode = forms.ChoiceField( + choices=add_blank_choice(InterfaceModeChoices), + required=False, + widget=StaticSelect() + ) + untagged_vlan = DynamicModelChoiceField( + queryset=VLAN.objects.all(), + required=False + ) + tagged_vlans = DynamicModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False + ) + + class Meta: + nullable_fields = [ + 'parent', 'mtu', 'description', + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if 'virtual_machine' in self.initial: + vm_id = self.initial.get('virtual_machine') + + # Restrict parent interface assignment by VM + self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) + + # Limit VLAN choices by virtual machine + self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) + self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) + + else: + # See 5643 + if 'pk' in self.initial: + site = None + interfaces = VMInterface.objects.filter(pk__in=self.initial['pk']).prefetch_related( + 'virtual_machine__cluster__site' + ) + + # Check interface sites. First interface should set site, further interfaces will either continue the + # loop or reset back to no site and break the loop. + for interface in interfaces: + if site is None: + site = interface.virtual_machine.cluster.site + elif interface.virtual_machine.cluster.site is not site: + site = None + break + + if site is not None: + self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk) + self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk) + + +class VMInterfaceBulkRenameForm(BulkRenameForm): + pk = forms.ModelMultipleChoiceField( + queryset=VMInterface.objects.all(), + widget=forms.MultipleHiddenInput() + ) diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py new file mode 100644 index 000000000..1f0496b7c --- /dev/null +++ b/netbox/virtualization/forms/bulk_import.py @@ -0,0 +1,125 @@ +from dcim.choices import InterfaceModeChoices +from dcim.models import DeviceRole, Platform, Site +from extras.forms import CustomFieldModelCSVForm +from tenancy.models import Tenant +from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField +from virtualization.choices import * +from virtualization.models import * + +__all__ = ( + 'ClusterCSVForm', + 'ClusterGroupCSVForm', + 'ClusterTypeCSVForm', + 'VirtualMachineCSVForm', + 'VMInterfaceCSVForm', +) + + +class ClusterTypeCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + + class Meta: + model = ClusterType + fields = ('name', 'slug', 'description') + + +class ClusterGroupCSVForm(CustomFieldModelCSVForm): + slug = SlugField() + + class Meta: + model = ClusterGroup + fields = ('name', 'slug', 'description') + + +class ClusterCSVForm(CustomFieldModelCSVForm): + type = CSVModelChoiceField( + queryset=ClusterType.objects.all(), + to_field_name='name', + help_text='Type of cluster' + ) + group = CSVModelChoiceField( + queryset=ClusterGroup.objects.all(), + to_field_name='name', + required=False, + help_text='Assigned cluster group' + ) + site = CSVModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name', + required=False, + help_text='Assigned site' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + to_field_name='name', + required=False, + help_text='Assigned tenant' + ) + + class Meta: + model = Cluster + fields = ('name', 'type', 'group', 'site', 'comments') + + +class VirtualMachineCSVForm(CustomFieldModelCSVForm): + status = CSVChoiceField( + choices=VirtualMachineStatusChoices, + required=False, + help_text='Operational status of device' + ) + cluster = CSVModelChoiceField( + queryset=Cluster.objects.all(), + to_field_name='name', + help_text='Assigned cluster' + ) + role = CSVModelChoiceField( + queryset=DeviceRole.objects.filter( + vm_role=True + ), + required=False, + to_field_name='name', + help_text='Functional role' + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) + platform = CSVModelChoiceField( + queryset=Platform.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned platform' + ) + + class Meta: + model = VirtualMachine + fields = ( + 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', + ) + + +class VMInterfaceCSVForm(CustomFieldModelCSVForm): + virtual_machine = CSVModelChoiceField( + queryset=VirtualMachine.objects.all(), + to_field_name='name' + ) + mode = CSVChoiceField( + choices=InterfaceModeChoices, + required=False, + help_text='IEEE 802.1Q operational mode (for L2 interfaces)' + ) + + class Meta: + model = VMInterface + fields = ( + 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode', + ) + + def clean_enabled(self): + # Make sure enabled is True when it's not included in the uploaded data + if 'enabled' not in self.data: + return True + else: + return self.cleaned_data['enabled'] diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py new file mode 100644 index 000000000..0bb5c2bd7 --- /dev/null +++ b/netbox/virtualization/forms/filtersets.py @@ -0,0 +1,237 @@ +from django import forms +from django.utils.translation import gettext as _ + +from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup +from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm +from tenancy.forms import TenancyFilterForm +from utilities.forms import ( + BootstrapMixin, DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, TagFilterField, + BOOLEAN_WITH_BLANK_CHOICES, +) +from virtualization.choices import * +from virtualization.models import * + +__all__ = ( + 'ClusterFilterForm', + 'ClusterGroupFilterForm', + 'ClusterTypeFilterForm', + 'VirtualMachineFilterForm', + 'VMInterfaceFilterForm', +) + + +class ClusterTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = ClusterType + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + +class ClusterGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = ClusterGroup + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + +class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): + model = Cluster + field_order = [ + 'q', 'type_id', 'region_id', 'site_id', 'group_id', 'tenant_group_id', 'tenant_id', + ] + field_groups = [ + ['q', 'tag'], + ['group_id', 'type_id'], + ['region_id', 'site_group_id', 'site_id'], + ['tenant_group_id', 'tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + type_id = DynamicModelMultipleChoiceField( + queryset=ClusterType.objects.all(), + required=False, + label=_('Type'), + fetch_trigger='open' + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + null_option='None', + query_params={ + 'region_id': '$region_id', + 'site_group_id': '$site_group_id', + }, + label=_('Site'), + fetch_trigger='open' + ) + group_id = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + null_option='None', + label=_('Group'), + fetch_trigger='open' + ) + tag = TagFilterField(model) + + +class VirtualMachineFilterForm( + BootstrapMixin, + LocalConfigContextFilterForm, + TenancyFilterForm, + CustomFieldModelFilterForm +): + model = VirtualMachine + field_groups = [ + ['q', 'tag'], + ['cluster_group_id', 'cluster_type_id', 'cluster_id'], + ['region_id', 'site_group_id', 'site_id'], + ['status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data'], + ['tenant_group_id', 'tenant_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + cluster_group_id = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + null_option='None', + label=_('Cluster group'), + fetch_trigger='open' + ) + cluster_type_id = DynamicModelMultipleChoiceField( + queryset=ClusterType.objects.all(), + required=False, + null_option='None', + label=_('Cluster type'), + fetch_trigger='open' + ) + cluster_id = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_('Cluster'), + fetch_trigger='open' + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region'), + fetch_trigger='open' + ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + null_option='None', + query_params={ + 'region_id': '$region_id', + 'group_id': '$site_group_id', + }, + label=_('Site'), + fetch_trigger='open' + ) + role_id = DynamicModelMultipleChoiceField( + queryset=DeviceRole.objects.all(), + required=False, + null_option='None', + query_params={ + 'vm_role': "True" + }, + label=_('Role'), + fetch_trigger='open' + ) + status = forms.MultipleChoiceField( + choices=VirtualMachineStatusChoices, + required=False, + widget=StaticSelectMultiple() + ) + platform_id = DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), + required=False, + null_option='None', + label=_('Platform'), + fetch_trigger='open' + ) + mac_address = forms.CharField( + required=False, + label='MAC address' + ) + has_primary_ip = forms.NullBooleanField( + required=False, + label='Has a primary IP', + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + tag = TagFilterField(model) + + +class VMInterfaceFilterForm(BootstrapMixin, forms.Form): + model = VMInterface + field_groups = [ + ['q', 'tag'], + ['cluster_id', 'virtual_machine_id'], + ['enabled', 'mac_address'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + cluster_id = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_('Cluster'), + fetch_trigger='open' + ) + virtual_machine_id = DynamicModelMultipleChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + query_params={ + 'cluster_id': '$cluster_id' + }, + label=_('Virtual machine'), + fetch_trigger='open' + ) + enabled = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + mac_address = forms.CharField( + required=False, + label='MAC address' + ) + tag = TagFilterField(model) diff --git a/netbox/virtualization/forms/models.py b/netbox/virtualization/forms/models.py new file mode 100644 index 000000000..d66bc9f1f --- /dev/null +++ b/netbox/virtualization/forms/models.py @@ -0,0 +1,324 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError + +from dcim.forms.common import InterfaceCommonForm +from dcim.forms.models import INTERFACE_MODE_HELP_TEXT +from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup +from extras.forms import CustomFieldModelForm +from extras.models import Tag +from ipam.models import IPAddress, VLAN, VLANGroup +from tenancy.forms import TenancyForm +from utilities.forms import ( + BootstrapMixin, CommentField, ConfirmationForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + JSONField, SlugField, StaticSelect, +) +from virtualization.models import * + +__all__ = ( + 'ClusterAddDevicesForm', + 'ClusterForm', + 'ClusterGroupForm', + 'ClusterRemoveDevicesForm', + 'ClusterTypeForm', + 'VirtualMachineForm', + 'VMInterfaceForm', +) + + +class ClusterTypeForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + + class Meta: + model = ClusterType + fields = [ + 'name', 'slug', 'description', + ] + + +class ClusterGroupForm(BootstrapMixin, CustomFieldModelForm): + slug = SlugField() + + class Meta: + model = ClusterGroup + fields = [ + 'name', 'slug', 'description', + ] + + +class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + type = DynamicModelChoiceField( + queryset=ClusterType.objects.all() + ) + group = DynamicModelChoiceField( + queryset=ClusterGroup.objects.all(), + required=False + ) + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + initial_params={ + 'sites': '$site' + } + ) + site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + initial_params={ + 'sites': '$site' + } + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region', + 'group_id': '$site_group', + } + ) + comments = CommentField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = Cluster + fields = ( + 'name', 'type', 'group', 'tenant', 'region', 'site_group', 'site', 'comments', 'tags', + ) + fieldsets = ( + ('Cluster', ('name', 'type', 'group', 'region', 'site_group', 'site', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + + +class ClusterAddDevicesForm(BootstrapMixin, forms.Form): + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + null_option='None' + ) + site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + null_option='None' + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + query_params={ + 'region_id': '$region', + 'group_id': '$site_group', + } + ) + rack = DynamicModelChoiceField( + queryset=Rack.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site' + } + ) + devices = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + query_params={ + 'site_id': '$site', + 'rack_id': '$rack', + 'cluster_id': 'null', + } + ) + + class Meta: + fields = [ + 'region', 'site', 'rack', 'devices', + ] + + def __init__(self, cluster, *args, **kwargs): + + self.cluster = cluster + + super().__init__(*args, **kwargs) + + self.fields['devices'].choices = [] + + def clean(self): + super().clean() + + # If the Cluster is assigned to a Site, all Devices must be assigned to that Site. + if self.cluster.site is not None: + for device in self.cleaned_data.get('devices', []): + if device.site != self.cluster.site: + raise ValidationError({ + 'devices': "{} belongs to a different site ({}) than the cluster ({})".format( + device, device.site, self.cluster.site + ) + }) + + +class ClusterRemoveDevicesForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField( + queryset=Device.objects.all(), + widget=forms.MultipleHiddenInput() + ) + + +class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + cluster_group = DynamicModelChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + null_option='None', + initial_params={ + 'clusters': '$cluster' + } + ) + cluster = DynamicModelChoiceField( + queryset=Cluster.objects.all(), + query_params={ + 'group_id': '$cluster_group' + } + ) + role = DynamicModelChoiceField( + queryset=DeviceRole.objects.all(), + required=False, + query_params={ + "vm_role": "True" + } + ) + platform = DynamicModelChoiceField( + queryset=Platform.objects.all(), + required=False + ) + local_context_data = JSONField( + required=False, + label='' + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = VirtualMachine + fields = [ + 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', + 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', + ] + fieldsets = ( + ('Virtual Machine', ('name', 'role', 'status', 'tags')), + ('Cluster', ('cluster_group', 'cluster')), + ('Tenancy', ('tenant_group', 'tenant')), + ('Management', ('platform', 'primary_ip4', 'primary_ip6')), + ('Resources', ('vcpus', 'memory', 'disk')), + ('Config Context', ('local_context_data',)), + ) + help_texts = { + 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered " + "config context", + } + widgets = { + "status": StaticSelect(), + 'primary_ip4': StaticSelect(), + 'primary_ip6': StaticSelect(), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if self.instance.pk: + + # Compile list of choices for primary IPv4 and IPv6 addresses + for family in [4, 6]: + ip_choices = [(None, '---------')] + + # Gather PKs of all interfaces belonging to this VM + interface_ids = self.instance.interfaces.values_list('pk', flat=True) + + # Collect interface IPs + interface_ips = IPAddress.objects.filter( + address__family=family, + assigned_object_type=ContentType.objects.get_for_model(VMInterface), + assigned_object_id__in=interface_ids + ) + if interface_ips: + ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips] + ip_choices.append(('Interface IPs', ip_list)) + # Collect NAT IPs + nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter( + address__family=family, + nat_inside__assigned_object_type=ContentType.objects.get_for_model(VMInterface), + nat_inside__assigned_object_id__in=interface_ids + ) + if nat_ips: + ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips] + ip_choices.append(('NAT IPs', ip_list)) + self.fields['primary_ip{}'.format(family)].choices = ip_choices + + else: + + # An object that doesn't exist yet can't have any IPs assigned to it + self.fields['primary_ip4'].choices = [] + self.fields['primary_ip4'].widget.attrs['readonly'] = True + self.fields['primary_ip6'].choices = [] + self.fields['primary_ip6'].widget.attrs['readonly'] = True + + +class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): + parent = DynamicModelChoiceField( + queryset=VMInterface.objects.all(), + required=False, + label='Parent interface' + ) + vlan_group = DynamicModelChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + label='VLAN group' + ) + untagged_vlan = DynamicModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + label='Untagged VLAN', + query_params={ + 'group_id': '$vlan_group', + } + ) + tagged_vlans = DynamicModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False, + label='Tagged VLANs', + query_params={ + 'group_id': '$vlan_group', + } + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = VMInterface + fields = [ + 'virtual_machine', 'name', 'enabled', 'parent', 'mac_address', 'mtu', 'description', 'mode', 'tags', + 'untagged_vlan', 'tagged_vlans', + ] + widgets = { + 'virtual_machine': forms.HiddenInput(), + 'mode': StaticSelect() + } + labels = { + 'mode': '802.1Q Mode', + } + help_texts = { + 'mode': INTERFACE_MODE_HELP_TEXT, + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') + + # Restrict parent interface assignment by VM + self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) + + # Limit VLAN choices by virtual machine + self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) + self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) diff --git a/netbox/virtualization/forms/object_create.py b/netbox/virtualization/forms/object_create.py new file mode 100644 index 000000000..b58fb51f8 --- /dev/null +++ b/netbox/virtualization/forms/object_create.py @@ -0,0 +1,74 @@ +from django import forms + +from dcim.choices import InterfaceModeChoices +from dcim.forms.common import InterfaceCommonForm +from extras.forms import CustomFieldsMixin +from extras.models import Tag +from ipam.models import VLAN +from utilities.forms import ( + add_blank_choice, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, + StaticSelect, +) +from virtualization.models import VMInterface, VirtualMachine + +__all__ = ( + 'VMInterfaceCreateForm', +) + + +class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonForm): + model = VMInterface + virtual_machine = DynamicModelChoiceField( + queryset=VirtualMachine.objects.all() + ) + name_pattern = ExpandableNameField( + label='Name' + ) + enabled = forms.BooleanField( + required=False, + initial=True + ) + parent = DynamicModelChoiceField( + queryset=VMInterface.objects.all(), + required=False, + query_params={ + 'virtual_machine_id': '$virtual_machine', + } + ) + mac_address = forms.CharField( + required=False, + label='MAC Address' + ) + description = forms.CharField( + max_length=200, + required=False + ) + mode = forms.ChoiceField( + choices=add_blank_choice(InterfaceModeChoices), + required=False, + widget=StaticSelect(), + ) + untagged_vlan = DynamicModelChoiceField( + queryset=VLAN.objects.all(), + required=False + ) + tagged_vlans = DynamicModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + field_order = ( + 'virtual_machine', 'name_pattern', 'enabled', 'parent', 'mtu', 'mac_address', 'description', 'mode', + 'untagged_vlan', 'tagged_vlans', 'tags' + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') + + # Limit VLAN choices by virtual machine + self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) + self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)