import json import logging 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.views.decorators.csrf import csrf_exempt 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, dev_container:DevContainer): self._devcontainer = dev_container self.vmid = dev_container.lxc.vmid self.lxc_status = dev_container.lxc.synced_status kwargs = dev_container.statuscache or {} 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.lease_status = kwargs.get('lease_status', 'waiting') 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).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), }) @csrf_exempt def create_container(request): try: user_profile = request.user.profile except AttributeError as e: logging.error(f"{e}") user_profile = None 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, })