Files
Holger Sielaff 90c0ff61ed initial
2025-08-27 09:55:55 +02:00

353 lines
13 KiB
Python

# 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)'
}