Files
Django-Proxmox-Mikrotik/frontend/views.py

638 lines
22 KiB
Python
Raw Normal View History

2025-08-27 09:55:55 +02:00
import json
import threading
from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.forms import model_to_dict
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
from django_proxmox_mikrotik.settings import ProxmoxConfig
from lib.decorators import readonly
from lib.proxmox import Proxmox
from lib.task_decorator import (
create_container_with_task,
delete_container_with_task,
resize_container_disk_with_task,
start_container_with_task,
stop_container_with_task,
update_container_config_sync,
)
from lib.utils import paginator
from manager.models import DevContainer
from mikrotik.models import DNSStatic
from proxmox.models import Lxc, LxcTemplate
from tasklogger.models import TaskFactory
from .forms import CloneContainerForm, DNSSearchForm, DNSStaticForm, DevContainerForm, FAQForm
from .models import FAQ, UserProfile
from .permissions import user_can_access_container
# Oben in den Imports hinzufügen:
def login_view(request):
TaskFactory.reset_current_task(request=request)
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
if next_url := request.GET.get('next'):
return redirect(next_url)
else:
return redirect('frontend:dashboard')
else:
messages.error(request, 'Ungültige Anmeldedaten')
return render(request, 'frontend/login.html')
def logout_view(request):
TaskFactory.reset_current_task(request=request)
logout(request)
if next_url := request.GET.get('next'):
return redirect('/frontend/login?next=' + next_url)
return redirect('frontend:login')
@login_required
def dashboard(request):
TaskFactory.reset_current_task(request=request)
try:
user_profile = request.user.profile
except UserProfile.DoesNotExist:
user_profile = UserProfile.objects.create(
user=request.user,
ldap_uid=request.user.username # Annahme: LDAP-UID = Username
)
searchdomain = Q()
if s := request.GET.get('search', ''):
searchdomain &= DevContainer.term_filter(s)
if user_profile.is_internal():
from mikrotik.models import IPAddress
ip_addresses = IPAddress.objects.filter(comment__icontains=f' {user_profile.ldap_uid} ')
networks = [ip.network for ip in ip_addresses]
leasefilter = Q()
for nw in networks:
leasefilter |= Q(lease__address__startswith=nw)
searchdomain &= leasefilter
if user_profile.is_external():
searchdomain &= Q(lease__address__startswith='172.2')
if lxc_status := request.GET.get('lxc_status', ''):
searchdomain &= Q(lxc__status=lxc_status)
if network_status := request.GET.get('network_status', ''):
searchdomain &= Q(lease__status=network_status)
# Paginierung hinzufügen
containers = DevContainer.objects.filter(searchdomain)
page_obj = paginator(containers, request=request)
default_template = LxcTemplate.objects.filter(is_default_template=True).first()
return render(request, 'frontend/dashboard.html', {
'containers': containers,
'page_obj': page_obj,
'user_profile': user_profile,
'page_vmids': ','.join(map(str, page_obj.object_list.values_list('lxc__vmid', flat=True))),
'page_ids': ','.join(map(str, page_obj.object_list.values_list('internal_id', flat=True))),
'proxmox_host': ProxmoxConfig.HOST,
'default_template': default_template,
})
class ContainerStatus:
low_limit = 80
def __init__(self, **kwargs):
self.cpus = kwargs.get('cpus', 0)
self.cpu = kwargs.get('cpu', 0)
self.maxmem = kwargs.get('maxmem', 256)
self.mem = kwargs.get('mem', 256)
self.maxdisk = kwargs.get('maxdisk', 6)
self.disk = kwargs.get('disk', 6)
self.maxswap = kwargs.get('maxswap', 0)
self.swap = kwargs.get('swap', 0)
self.status = kwargs.get('status')
self.vmid = kwargs.get('vmid')
self.lease_status = kwargs.get('lease_status', 'waiting')
self.lxc_status = kwargs.get('lxc_status', 'stopped')
def __hash__(self):
return int(self.vmid)
def __getattr__(self, item):
if item.endswith('percent'):
act = int(getattr(self, item[:-8]) or 0)
soll = int(getattr(self, 'max' + item[:-8]) or 1)
return round(act / soll * 100, 2)
raise AttributeError(f'{item} not found')
@property
def cpu_percent(self):
return round((self.cpu or 0) * 100, 2)
@property
def is_low(self):
if not self.cpu_percent and not self.mem_percent and not self.disk_percent:
return False
return self.cpu_percent > self.low_limit or self.mem_percent > self.low_limit or self.disk_percent > self.low_limit
@property
def to_json(self):
return {
'cpus': self.cpus,
'cpu': self.cpu,
'maxmem': self.maxmem,
'mem': self.mem,
'maxdisk': self.maxdisk,
'disk': self.disk,
'maxswap': self.maxswap,
'swap': self.swap,
'is_low': self.is_low,
'cpu_percent': self.cpu_percent,
'mem_percent': self.mem_percent,
'disk_percent': self.disk_percent,
'swap_percent': self.swap_percent,
'vmid': self.vmid,
'lease_status': self.lease_status,
'lxc_status': self.lxc_status,
}
@readonly
def container_details(request):
ids = filter(None, request.GET.get('ids', '').split(','))
ret = []
if ids:
with Proxmox() as pm:
for id in ids:
container = DevContainer.objects.get(internal_id=id)
ret.append(ContainerStatus(**(container.statuscache or {})).to_json)
return HttpResponse(json.dumps(ret), content_type='application/json')
@login_required
def start_lxc(request, id=None):
if request.GET.get('id'):
id = request.GET.get('id')
container = get_object_or_404(DevContainer, internal_id=id)
try:
# Create task and execute synchronously
task = TaskFactory(request=request)
success = start_container_with_task(str(task.uuid), container.lxc.vmid, request=request)
if success:
messages.success(request, f'Container {container.name} wurde gestartet!')
else:
messages.error(request, f'Fehler beim Starten von Container {container.name}')
return redirect('frontend:dashboard')
except Exception as e:
messages.error(request, f'Fehler beim Starten des Containers: {str(e)}')
return redirect('frontend:dashboard')
@login_required
def stop_lxc(request, id=None):
if request.GET.get('id'):
id = request.GET.get('id')
container = get_object_or_404(DevContainer, internal_id=id)
try:
# Create task and execute synchronously
task = TaskFactory(request=request)
success = stop_container_with_task(str(task.uuid), container.lxc.vmid, request=request)
if success:
messages.success(request, f'Container {container.name} wurde gestoppt!')
else:
messages.error(request, f'Fehler beim Stoppen von Container {container.name}')
return redirect('frontend:dashboard')
except Exception as e:
messages.error(request, f'Fehler beim Stoppen des Containers: {str(e)}')
return redirect('frontend:dashboard')
@login_required
def container_detail(request, container_id):
container = get_object_or_404(DevContainer, internal_id=container_id)
user_profile = request.user.profile
if False and not user_can_access_container(user_profile, container):
return HttpResponseForbidden("Sie haben keine Berechtigung, diesen Container zu sehen.")
return render(request, 'frontend/container_details.html', {
'container': container
})
@login_required
def edit_container(request, container_id):
container = get_object_or_404(DevContainer, internal_id=container_id)
user_profile = request.user.profile
task = TaskFactory(request=request)
# Berechtigungsprüfung
if not user_can_access_container(user_profile, container):
return HttpResponseForbidden("Sie haben keine Berechtigung, diesen Container zu bearbeiten.")
if request.method == 'POST':
form = DevContainerForm(request.POST, instance=container, user_profile=user_profile)
if form.is_valid():
# Check if resource changes require updates
old_disksize = container.lxc.disksize
old_cores = container.lxc.cores
old_memory = container.lxc.memory
new_disksize = form.cleaned_data.get('disksize')
new_cores = form.cleaned_data.get('cores')
new_memory = form.cleaned_data.get('memory')
try:
# Save form first (updates local database)
form.save()
# Handle disk resize (synchronously with TaskLogger)
if new_disksize and new_disksize != old_disksize:
success = resize_container_disk_with_task(str(task.uuid), container.lxc.vmid, new_disksize, request=request)
if not success:
messages.error(request, 'Fehler beim Vergrößern der Festplatte. Siehe Task-Log für Details.')
return redirect('frontend:dashboard')
# Handle memory/cores changes (synchronously)
config_updates = {}
if new_cores and new_cores != old_cores:
config_updates['cores'] = new_cores
if new_memory and new_memory != old_memory:
config_updates['memory'] = new_memory
if config_updates:
success = update_container_config_sync(container.lxc.vmid, task=task, **config_updates)
if not success:
messages.error(request, 'Fehler beim Aktualisieren der Container-Konfiguration')
return redirect('frontend:dashboard')
messages.success(request, 'Container wurde erfolgreich aktualisiert')
return redirect('frontend:dashboard')
except Exception as e:
messages.error(request, f'Fehler beim Aktualisieren des Containers: {str(e)}')
return redirect('frontend:dashboard')
finally:
TaskFactory.reset_current_task(request=request)
else:
TaskFactory.reset_current_task(request=request)
task = TaskFactory(request=request)
form = DevContainerForm(instance=container, user_profile=user_profile)
return render(request, 'frontend/edit_container.html', {
'form': form,
'container': container,
'task_uuid': str(task.uuid),
})
@login_required
def delete_container(request, container_id):
container = get_object_or_404(DevContainer, internal_id=container_id)
user_profile = request.user.profile
# Berechtigungsprüfung
if not user_can_access_container(user_profile, container):
return HttpResponseForbidden("Sie haben keine Berechtigung, diesen Container zu löschen.")
if request.method == 'GET':
TaskFactory.reset_current_task(request=request)
task = TaskFactory(request=request)
if request.method == 'POST':
try:
# Execute container deletion in background thread
def _delete_container_async():
delete_container_with_task(str(task.uuid), container, request=request)
thread = threading.Thread(target=_delete_container_async)
thread.daemon = True
thread.start()
# Return JSON response for async handling with task_uuid
return HttpResponse(json.dumps({
'status': 'task_started',
'task_uuid': str(task.uuid),
'message': 'Container deletion initiated'
}), content_type='application/json')
except Exception as e:
return HttpResponse(json.dumps({
'status': 'error',
'message': f'Fehler beim Löschen des Containers: {str(e)}'
}), content_type='application/json', status=500)
finally:
TaskFactory.reset_current_task(request=request)
return render(request, 'frontend/delete_container.html', {
'container': container,
'task_id': str(task.uuid),
})
def create_container(request):
user_profile = request.user.profile
if request.method == 'POST':
# Get task_uuid from form
task_uuid = request.POST.get('task_uuid')
form = CloneContainerForm(request.POST, user_profile=user_profile)
if form.is_valid():
container = form.save()
container.is_active = True
container.save()
# Start container creation asynchronously with TaskLogger
try:
# Get the task created when form was loaded
# Create new task if not found
task = TaskFactory(request=request)
task_uuid = str(task.uuid)
# Execute container creation in background thread
def _create_container_async():
create_container_with_task(task_uuid, container, request=request)
thread = threading.Thread(target=_create_container_async)
thread.daemon = True
thread.start()
# Return JSON response for async handling with task_uuid
return HttpResponse(json.dumps({
'status': 'task_started',
'task_uuid': task_uuid,
'message': 'Container creation initiated'
}), content_type='application/json')
except Exception as e:
return HttpResponse(json.dumps({
'status': 'error',
'message': f'Fehler beim Erstellen des Containers: {str(e)}'
}), content_type='application/json', status=500)
else:
if cloneid := request.GET.get('clone_lxc'):
lxc = Lxc.objects.filter(pk=cloneid)
else:
lxc = None
if template_id := request.GET.get('clone_template'):
template = LxcTemplate.objects.filter(pk=template_id)
else:
template = None
form = CloneContainerForm(
user_profile=user_profile,
vm=lxc,
template=template,
hostname=request.GET.get('clone_hostname'),
)
# Create new task when form is loaded and put UUID in form
task = TaskFactory(request=request)
task_uuid = str(task.uuid)
return render(request, 'frontend/create_container.html', {
'form': form,
'task_uuid': task_uuid
})
# DNS Management Views
@login_required
def dns_list(request):
"""List all DNS entries with search functionality"""
search_form = DNSSearchForm(request.GET or None)
dev_container_dns = list(DevContainer.objects.all().values_list('dns_id', flat=True))
dns_entries = DNSStatic.objects.all().order_by('name', 'regexp', 'address')
# Apply search filters
if search_form.is_valid():
search_query = search_form.cleaned_data.get('search')
entry_type = search_form.cleaned_data.get('entry_type')
if search_query:
dns_entries = dns_entries.filter(
Q(name__icontains=search_query) |
Q(regexp__icontains=search_query) |
Q(address__icontains=search_query) |
Q(comment__icontains=search_query)
)
if entry_type == 'name':
dns_entries = dns_entries.exclude(name__isnull=True).exclude(name='')
elif entry_type == 'regexp':
dns_entries = dns_entries.exclude(regexp__isnull=True).exclude(regexp='')
# Pagination
page_obj = paginator(dns_entries, request=request)
return render(request, 'frontend/dns_list.html', {
'dns_entries': dns_entries,
'page_obj': page_obj,
'search_form': search_form,
'dev_container_dns': dev_container_dns,
})
@login_required
def dns_create(request):
"""Create a new DNS entry"""
if request.method == 'POST':
form = DNSStaticForm(request.POST)
if form.is_valid():
# Remove container field before saving (it's just for UI)
dns_entry = form.save(commit=False)
dns_entry.save()
messages.success(request, f'DNS entry "{dns_entry}" created successfully')
return redirect('frontend:dns_list')
else:
form = DNSStaticForm()
return render(request, 'frontend/dns_form.html', {
'form': form,
'title': 'Create DNS Entry',
'submit_text': 'Create DNS Entry'
})
@login_required
def dns_edit(request, dns_id):
"""Edit an existing DNS entry"""
dns_entry = get_object_or_404(DNSStatic, id=dns_id)
if request.method == 'POST':
form = DNSStaticForm(request.POST, instance=dns_entry)
if form.is_valid():
dns_entry = form.save()
messages.success(request, f'DNS entry "{dns_entry}" updated successfully')
return redirect('frontend:dns_list')
else:
form = DNSStaticForm(instance=dns_entry)
return render(request, 'frontend/dns_form.html', {
'form': form,
'dns_entry': dns_entry,
'title': 'Edit DNS Entry',
'submit_text': 'Update DNS Entry'
})
@login_required
def dns_delete(request, dns_id):
"""Delete a DNS entry"""
dns_entry = get_object_or_404(DNSStatic, id=dns_id)
if request.method == 'POST':
dns_name = str(dns_entry)
dns_entry.delete()
messages.success(request, f'DNS entry "{dns_name}" deleted successfully')
return redirect('frontend:dns_list')
return render(request, 'frontend/dns_delete.html', {
'dns_entry': dns_entry,
})
@login_required
def dns_container_api(request):
"""API endpoint for container selection in DNS forms"""
search = request.GET.get('search', '')
containers = DevContainer.objects.all()
if search:
containers = containers.filter(
Q(lxc__hostname__icontains=search) |
Q(lxc__name__icontains=search) |
Q(lease__address__icontains=search)
)
results = []
for container in containers:
try:
results.append({
'id': container.pk,
'text': f"{container.name} ({container.address})",
'address': container.address,
})
except:
continue
return HttpResponse(json.dumps({
'results': results
}), content_type='application/json')
@login_required
def faq_list(request):
"""Simple FAQ list view with accordion display"""
term = request.GET.get('search', '')
faqs = FAQ.term_search(term).order_by('order', 'title')
return render(request, 'frontend/faq_list.html', {
'faqs': faqs,
})
def faq_raw(request, id=None):
response_type = 'text/markdown' if not request.GET.get('type', '') == 'json' else 'applicaton/json'
indent = int(request.GET.get('indent', 0)) or None
if id:
faq = get_object_or_404(FAQ, pk=id)
if response_type == 'text/markdown':
content = f"# {faq.title}\n\n{faq.content}"
else:
content = json.dumps(model_to_dict(faq), indent=indent)
else:
term = request.GET.get('search', '')
faqs = FAQ.term_search(term).order_by('order', 'title')
result = []
if response_type == 'text/markdown':
for faq in faqs:
result.append(f"# {faq.title}\n{faq.content}\n\n")
content = '\n-----------------------------------------------\n\n'.join(result)
else:
for faq in faqs:
result.append(model_to_dict(faq))
content = json.dumps(result, default=str, indent=indent)
return HttpResponse(content, content_type=response_type)
@login_required
def faq_create(request):
"""Create new FAQ entry"""
if request.method == 'POST':
form = FAQForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, 'FAQ entry created successfully!')
return redirect('frontend:faq_list')
else:
form = FAQForm()
return render(request, 'frontend/faq_form.html', {
'form': form,
'title': 'Create FAQ Entry',
'submit_text': 'Create FAQ'
})
@login_required
def faq_edit(request, faq_id):
"""Edit existing FAQ entry"""
faq = get_object_or_404(FAQ, pk=faq_id)
if request.method == 'POST':
form = FAQForm(request.POST, instance=faq)
if form.is_valid():
form.save()
messages.success(request, 'FAQ entry updated successfully!')
return redirect('frontend:faq_list')
else:
form = FAQForm(instance=faq)
return render(request, 'frontend/faq_form.html', {
'form': form,
'faq': faq,
'title': 'Edit FAQ Entry',
'submit_text': 'Update FAQ'
})
@login_required
def faq_delete(request, faq_id):
"""Delete FAQ entry"""
faq = get_object_or_404(FAQ, pk=faq_id)
if request.method == 'POST':
faq.delete()
messages.success(request, 'FAQ entry deleted successfully!')
return redirect('frontend:faq_list')
return render(request, 'frontend/faq_delete.html', {
'faq': faq,
})