initial
This commit is contained in:
352
frontend/forms.py
Normal file
352
frontend/forms.py
Normal file
@@ -0,0 +1,352 @@
|
||||
# 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)'
|
||||
}
|
||||
Reference in New Issue
Block a user