# frontend/forms.py import re from functools import cached_property from django import forms from django.core.exceptions import ValidationError from django_proxmox_mikrotik.settings import ProxmoxConfig from manager.models import CloneContainer, DevContainer from mikrotik.models import DNSStatic, IPAddress, IPDHCPLease from proxmox.models import Lxc from .models import FAQ def fk_to_hidden_input(formobj: forms.Form, instance, fieldname: str): initial_value = str(getattr(instance, fieldname)) if instance else '' formobj.fields[fieldname] = forms.CharField( label=fieldname.title(), initial=initial_value, widget=forms.TextInput( attrs={'readonly': 'readonly', 'style': 'background-color: #eee;', 'class': 'form-control'}), required=False ) class DevContainerForm(forms.ModelForm): disksize = forms.IntegerField( required=False, min_value=1, max_value=100, help_text="Festplattengröße (GB) - kann nicht verkleinert werden" ) cores = forms.IntegerField( required=False, min_value=1, help_text="Anzahl der CPU-Kerne" ) memory = forms.CharField( required=False, help_text="Arbeitsspeicher in MB" ) class Meta: model = DevContainer fields = ['lxc', 'lease', 'dns'] @cached_property def dns_obj(self): return DNSStatic.objects.get(pk=self.cleaned_data['dns']) @cached_property def lease_obj(self): return IPDHCPLease.objects.get(pk=self.cleaned_data['lease']) @cached_property def lxc_obj(self): return Lxc.objects.get(pk=self.cleaned_data['lxc']) def _clean_dns(self): if self.dns_obj.address != self.lease_obj.address: raise ValidationError("DNS has not the same address as Lease. Please check your input.") return self.cleaned_data['dns'] def _clean_lease(self): """Brauchts eigentlich nicht""" if self.lease_obj.address != self.dns_obj.address: raise ValidationError("Lease has not the same address as DNS. Please check your input.") return self.cleaned_data['lease'] def _clean_lxc(self): if self.lxc_obj.hwaddr != self.lease_obj.mac_address: raise ValidationError("LXC has not the same MAC address as Lease. Please check your input.") return self.cleaned_data['lxc'] def clean(self): super().clean() self.cleaned_data['dns'] = self._clean_dns() self.cleaned_data['lease'] = self._clean_lease() self.cleaned_data['lxc'] = self._clean_lxc() return self.cleaned_data def clean_disksize(self): if self.cleaned_data['disksize'] and int(self.cleaned_data['disksize']) > int(ProxmoxConfig.MAX_DISK): raise ValidationError( f"Disksize > {ProxmoxConfig.MAX_DISK}GB not allowed. Please check your input.") return self.cleaned_data['disksize'] def clean_memory(self): if self.cleaned_data['memory'] and int(self.cleaned_data['memory']) > ProxmoxConfig.MAX_MEM: raise ValidationError(f"Memory > {ProxmoxConfig.MAX_MEM}MB not allowed. Please check your input.") return self.cleaned_data['memory'] def clean_cores(self): if self.cleaned_data['cores'] and int(self.cleaned_data['cores']) > ProxmoxConfig.MAX_CORES: raise ValidationError(f"Cores > {ProxmoxConfig.MAX_CORES} not allowed. Please check your input.") return self.cleaned_data['cores'] def __init__(self, *args, user_profile=None, **kwargs): super().__init__(*args, **kwargs) self.user_profile = user_profile instance = kwargs.get('instance') # Für externe Benutzer: IP-Adressen filtern if user_profile and user_profile.is_external(): ldap_uid = user_profile.ldap_uid ip_addresses = IPAddress.objects.filter(comment__icontains=f' {ldap_uid} ') networks = [ip.network for ip in ip_addresses] # Lease-Feld einschränken self.fields['lease'].queryset = self.fields['lease'].queryset.filter( address__regex=r'^(' + '|'.join([re.escape(net) for net in networks]) + r')' ) self.fields['dns'].queryset = self.fields['dns'].queryset.filter(address=instance.lease.address) if instance and hasattr(instance, 'lxc') and instance.lxc: self.fields['disksize'].initial = instance.lxc.disksize self.fields['disksize'].min_value = instance.lxc.disksize self.fields['cores'].initial = instance.lxc.cores self.fields['memory'].initial = instance.lxc.memory fk_to_hidden_input(self, instance, 'lxc') fk_to_hidden_input(self, instance, 'lease') fk_to_hidden_input(self, instance, 'dns') def save(self, commit=True): instance = super().save(commit=False) if hasattr(instance, 'lxc') and instance.lxc: lxc = instance.lxc if 'disksize' in self.cleaned_data and self.cleaned_data['disksize']: lxc.disksize = self.cleaned_data['disksize'] if 'cores' in self.cleaned_data and self.cleaned_data['cores']: lxc.cores = self.cleaned_data['cores'] if 'memory' in self.cleaned_data and self.cleaned_data['memory']: lxc.memory = self.cleaned_data['memory'] lxc.save() if commit: instance.save() return instance class CloneContainerForm(forms.ModelForm): class Meta: model = CloneContainer fields = ['hostname', 'network', 'cores', 'memory', 'disksize', 'as_regexp', 'vm', 'template'] def __init__(self, *args, user_profile=None, vm=None, template=None, hostname=None, **kwargs): super().__init__(*args, **kwargs) self.user_profile = user_profile if vm: self.fields['vm'].initial = vm[0].pk self.fields['template'].disabled = True elif template: self.fields['template'].initial = template[0].pk self.fields['vm'].disabled = True if hostname: self.fields['hostname'].initial = hostname # Für externe Benutzer: Netzwerke filtern if user_profile and user_profile.is_external(): ldap_uid = user_profile.ldap_uid ip_addresses = IPAddress.objects.filter(comment__icontains=f' {ldap_uid} ') self.fields['network'].queryset = ip_addresses def clean_hostname(self): hostname = self.cleaned_data.get('hostname') if not hostname: raise ValidationError("Der Hostname ist erforderlich.") # Prüfe, ob der Name bereits als LXC-Hostname existiert if Lxc.objects.filter(hostname=hostname).exists(): raise ValidationError(f"Ein LXC mit dem Hostnamen '{hostname}' existiert bereits.") # Prüfe, ob der Name bereits als DNS-Name existiert from mikrotik.models import DNSStatic if DNSStatic.objects.filter(name=hostname).exists(): raise ValidationError(f"Ein DNS-Eintrag mit dem Namen '{hostname}' existiert bereits.") # Prüfe auf Regex-Übereinstimmung in DNS-Einträgen dns_with_regex = DNSStatic.objects.exclude(regexp='') for dns in dns_with_regex: if dns.regexp and re.search(hostname + '$', dns.regexp): raise ValidationError( f"Der Name '{hostname}' entspricht dem regulären Ausdruck '{dns.regexp}' eines existierenden DNS-Eintrags.") # Prüfe, ob der Name bereits als CloneContainer-Name existiert existing_clone_query = CloneContainer.objects.filter(hostname=hostname, status__in=('pending', 'running')) if self.instance.pk: existing_clone_query = existing_clone_query.exclude(pk=self.instance.pk) if existing_clone_query.exists(): raise ValidationError(f"Ein CloneContainer mit dem Namen '{hostname}' existiert bereits.") return hostname def clean_disksize(self): disksize = self.cleaned_data.get('disksize') if disksize and disksize > 100: raise ValidationError("Die Festplattengröße darf maximal 100 GB betragen.") return disksize def clean(self): cleaned_data = super().clean() if not cleaned_data.get('template') and not cleaned_data.get('vm'): raise ValidationError("Es muss entweder ein Template oder eine VM ausgewählt werden.") return cleaned_data class DNSStaticForm(forms.ModelForm): # Optional: Container selection for automatic IP assignment container = forms.ModelChoiceField( queryset=DevContainer.objects.all(), required=False, empty_label="Select container (optional)", help_text="Select a container to automatically use its IP address" ) # Manual IP input address = forms.GenericIPAddressField( required=False, help_text="IP address for the DNS entry" ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Add CSS classes for field in self.fields: if isinstance(self.fields[field].widget, forms.widgets.TextInput): self.fields[field].widget.attrs.update({'class': 'form-control'}) elif isinstance(self.fields[field].widget, forms.widgets.Select): self.fields[field].widget.attrs.update({'class': 'form-select'}) # Make container field use Select2 self.fields['container'].widget.attrs.update({ 'class': 'form-select container-select', 'data-placeholder': 'Search containers...' }) def clean(self): cleaned_data = super().clean() container = cleaned_data.get('container') address = cleaned_data.get('address') name = cleaned_data.get('name') regexp = cleaned_data.get('regexp') # Either container or manual address must be provided if not container and not address: raise ValidationError("Either select a container or enter an IP address manually") # Either name or regexp must be provided (but not both) if not name and not regexp: raise ValidationError("Either DNS name or regexp must be provided") if name and regexp: raise ValidationError("Provide either DNS name or regexp, not both") # If container is selected, use its IP address if container: try: cleaned_data['address'] = container.lease.address except AttributeError: raise ValidationError("Selected container has no IP lease") # Check for existing DNS entries if name: existing = DNSStatic.objects.filter(name=name) if self.instance.pk: existing = existing.exclude(pk=self.instance.pk) if existing.exists(): raise ValidationError(f"DNS name '{name}' already exists") if regexp: existing = DNSStatic.objects.filter(regexp=regexp) if self.instance.pk: existing = existing.exclude(pk=self.instance.pk) if existing.exists(): raise ValidationError(f"DNS regexp '{regexp}' already exists") return cleaned_data class Meta: model = DNSStatic fields = ['name', 'regexp', 'address', 'comment', 'container'] widgets = { 'name': forms.TextInput(attrs={'placeholder': 'e.g. server.example.com'}), 'regexp': forms.TextInput(attrs={'placeholder': 'e.g. .*\\.test\\.com'}), 'comment': forms.TextInput(attrs={'placeholder': 'Optional description'}), } help_texts = { 'name': 'Specific DNS name (e.g. server.example.com)', 'regexp': 'DNS regexp pattern (e.g. .*\\.test\\.com)', 'comment': 'Optional description for this DNS entry' } class DNSSearchForm(forms.Form): search = forms.CharField( required=False, widget=forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Search DNS entries...', 'id': 'dns-search' }) ) entry_type = forms.ChoiceField( choices=[ ('', 'All types'), ('name', 'Names only'), ('regexp', 'Regexps only'), ], required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) class FAQForm(forms.ModelForm): class Meta: model = FAQ fields = ['title', 'content', 'order'] widgets = { 'title': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Enter FAQ question/title...' }), 'content': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 10, 'placeholder': 'Enter FAQ answer/content using Markdown syntax...\n\n# Heading\n## Subheading\n\n**Bold text**\n*Italic text*\n\n- List item 1\n- List item 2\n\n```python\ncode block\n```\n\n[Link text](http://example.com)', 'style': 'font-family: monospace;' }), 'order': forms.NumberInput(attrs={ 'class': 'form-control', 'min': 0 }) } help_texts = { 'title': 'The FAQ question or title', 'content': 'The detailed answer or content (supports Markdown formatting)', 'order': 'Display order (lower numbers appear first)' }