647 lines
22 KiB
Python
647 lines
22 KiB
Python
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,
|
|
})
|