353 lines
13 KiB
Python
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)'
|
||
|
|
}
|