commit 342f1314c7eaf438958ad736b9d0e62183b87086 Author: ilkeral Date: Mon Jul 21 13:49:36 2025 +0300 yeni diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 0000000..133083e --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..117ec52 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..776a1a3 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..48b881b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/webServers.xml b/.idea/webServers.xml new file mode 100644 index 0000000..04305f9 --- /dev/null +++ b/.idea/webServers.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/.idea/yonetim.iml b/.idea/yonetim.iml new file mode 100644 index 0000000..b2cb4aa --- /dev/null +++ b/.idea/yonetim.iml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..6f61546 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,65 @@ +FROM python:3.11.12-alpine + +# Install zip and other necessary packages +RUN apk add --no-cache \ + zip \ + unzip \ + tar \ + gzip \ + bzip2 \ + xz \ + p7zip \ + sudo \ + shadow \ + openssh-client \ + rsync \ + curl \ + wget \ + bash \ + sshpass \ + git \ + su-exec \ + && which zip && zip --help + +# Create app user with specific UID/GID +RUN addgroup -g 1000 appgroup && \ + adduser -u 1000 -G appgroup -s /bin/sh -D appuser + +# set work directory +WORKDIR /app + +# Change ownership of the work directory +RUN chown -R appuser:appgroup /app + +# set env variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# install dependencies +COPY requirements.txt . +RUN pip install -r requirements.txt + +# copy project +COPY . . + +# Copy init script +COPY init.sh /usr/local/bin/init.sh +RUN chmod +x /usr/local/bin/init.sh + +# Ensure proper permissions for SQLite database and directories +RUN chown -R appuser:appgroup /app +RUN chmod -R 755 /app + +# Specifically set permissions for SQLite database and its directory +RUN if [ -f /app/db.sqlite3 ]; then \ + chown appuser:appgroup /app/db.sqlite3 && \ + chmod 664 /app/db.sqlite3; \ + fi + +# Create and set permissions for media and static directories +RUN mkdir -p /app/media /app/static /app/logs /tmp/backups && \ + chown -R appuser:appgroup /app/media /app/static /app/logs /tmp/backups && \ + chmod -R 755 /app/media /app/static /app/logs /tmp/backups + +# Use init script as entrypoint (runs as root, then switches to appuser) +ENTRYPOINT ["/usr/local/bin/init.sh"] diff --git a/build/init.sh b/build/init.sh new file mode 100644 index 0000000..065c0f0 --- /dev/null +++ b/build/init.sh @@ -0,0 +1,56 @@ +#!/bin/bash +set -e + +echo "=== Container başlatılıyor ===" + +# Veritabanı dosyası yolu +DB_FILE="/app/db.sqlite3" +DB_DIR="/app" + +echo "Mevcut durumu kontrol ediyorum..." +ls -la /app/ || true + +# Veritabanı dosyası kontrolü ve oluşturma +if [ ! -f "$DB_FILE" ]; then + echo "SQLite veritabanı bulunamadı, oluşturuluyor..." + touch "$DB_FILE" +fi + +# Tüm /app dizinini 1000:1000 kullanıcısına ata +echo "Dizin sahipliği ayarlanıyor..." +chown -R 1000:1000 /app + +# Veritabanı dosyası için özel izinler +echo "Veritabanı izinleri ayarlanıyor..." +chmod 666 "$DB_FILE" # Daha geniş izin +chmod 777 "$DB_DIR" # Dizin için tam izin + +# Gerekli dizinleri oluştur +mkdir -p /app/media /app/static /app/logs +mkdir -p /tmp/backups +chown -R 1000:1000 /app/media /app/static /app/logs /tmp/backups +chmod -R 777 /app/media /app/static /app/logs /tmp/backups + +# /tmp dizinine de tam izin ver +chmod 777 /tmp +chown 1000:1000 /tmp + +echo "Final izin kontrolü:" +ls -la /app/db.sqlite3 || true +ls -ld /app/ || true + +echo "=== İzinler ayarlandı, appuser olarak geçiliyor ===" + +# Django migrate çalıştır (root olarak) +echo "Django migrate çalıştırılıyor..." +cd /app +python manage.py migrate --noinput || echo "Migrate hatası, devam ediliyor..." + +# Veritabanı izinlerini tekrar ayarla +chown 1000:1000 "$DB_FILE" +chmod 666 "$DB_FILE" + +echo "=== Uygulama başlatılıyor ===" + +# appuser olarak uygulamayı başlat +exec su-exec 1000:1000 "$@" diff --git a/build/requirements.txt b/build/requirements.txt new file mode 100644 index 0000000..793d17a --- /dev/null +++ b/build/requirements.txt @@ -0,0 +1,48 @@ +anyio==1.4.0 +asgiref==3.7.2 +async-generator==1.10 +captcha==0.5.0 +certifi==2022.6.15 +charset-normalizer==2.1.0 +Django==4.2.8 +django-admin-interface==0.24.2 +django-appconf==1.0.5 +django-ckeditor==6.4.1 +django-colorfield==0.8.0 +django-imagekit==4.1.0 +django-js-asset==2.0.0 +django-ranged-response==0.2.0 +django-recaptcha==4.0.0 +django-recaptcha3==0.4.0 +django-simple-captcha==0.6.0 +django-user-agents==0.4.0 +gunicorn==20.1.0 +h11==0.12.0 +httpcore==0.13.3 +httpx==0.20.0 +idna==3.3 +pilkit==2.0 +Pillow==9.5.0 +pydash==7.0.6 +recaptcha==1.0rc1 +requests==2.31.0 +rfc3986==1.5.0 +sitemaps==0.1.0 +six==1.16.0 +sniffio==1.2.0 +sqlparse==0.4.2 +typing_extensions==4.8.0 +tzdata==2022.1 +ua-parser==0.10.0 +urllib3==2.1.0 +user-agents==2.2.0 +verify==1.1.1 +django-resized==1.0.2 +django-tinymce==4.1.0 +whitenoise==6.9.0 +python-slugify==8.0.1 +python-dotenv +grappelli +paramiko>=2.12.0 +boto3>=1.26.0 +botocore>=1.29.0 \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000..443d0f3 Binary files /dev/null and b/db.sqlite3 differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a1eb9c8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +services: + yonetim: + build: ./build/ + container_name: yonetim + restart: unless-stopped + #command: python manage.py runserver 0.0.0.0:8000 + command: gunicorn --bind 0.0.0.0:8000 yonetim.wsgi:application + volumes: + - ./:/app:rw + - /tmp:/tmp:rw + #ports: + # - 8025:8000 + networks: + - proxy + environment: + - DEBUG=1 + privileged: true + cap_add: + - SYS_ADMIN + - DAC_OVERRIDE + labels: + - "traefik.enable=false" + - "traefik.http.routers.tasima.rule=Host(`habitatnakliyat.com.tr`)" + - "traefik.http.routers.tasima.entrypoints=websecure" + - "traefik.http.routers.tasima.tls=true" + - "traefik.http.routers.tasima.tls.certresolver=letencrypt" + - "traefik.http.services.tasima.loadbalancer.server.port=8000" + +networks: + proxy: + external: true diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..a528f72 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yonetim.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..04e80cc Binary files /dev/null and b/requirements.txt differ diff --git a/ssh_manager/2backup.py b/ssh_manager/2backup.py new file mode 100644 index 0000000..85370d4 --- /dev/null +++ b/ssh_manager/2backup.py @@ -0,0 +1,67 @@ +import os +import zipfile +from datetime import datetime +from config.settings import BASE_DIR +import boto3 +import botocore + +# Configuration for Vultr S3 storage +script_klasoru = BASE_DIR +haric_dosya_uzantilari = ['.zip'] +excluded_folders = ['venv', 'yedek', '.idea'] +hostname = "ams1.vultrobjects.com" +secret_key = "Ec1pq3OQAObFLOQrfAVqJKhDAk4BkT7OqgYszlef" +access_key = "KQAOMJ8CQ8HP4CY23YPK" + +def zip_klasor(ziplenecek_klasor, hedef_zip_adi, haric_klasorler=[], haric_dosya_uzantilari=[]): + """Zip the target folder excluding specified folders and file extensions.""" + with zipfile.ZipFile(hedef_zip_adi, 'w', zipfile.ZIP_DEFLATED) as zipf: + for klasor_yolu, _, dosya_listesi in os.walk(ziplenecek_klasor): + if not any(k in klasor_yolu for k in haric_klasorler): + for dosya in dosya_listesi: + dosya_yolu = os.path.join(klasor_yolu, dosya) + if not any(dosya_yolu.endswith(ext) for ext in haric_dosya_uzantilari): + zipf.write(dosya_yolu, os.path.relpath(dosya_yolu, ziplenecek_klasor)) + +def job(folder_name): + """Create a zip backup and upload it to Vultr S3.""" + session = boto3.session.Session() + client = session.client('s3', region_name='ams1', + endpoint_url=f'https://{hostname}', + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + config=botocore.client.Config(signature_version='s3v4')) + + output_zip = os.path.join(os.getcwd(), f"{folder_name}.zip") + zip_klasor(script_klasoru, output_zip, excluded_folders, haric_dosya_uzantilari) + + try: + # Ensure the bucket exists + try: + client.head_bucket(Bucket=folder_name) + print(f"Bucket already exists: {folder_name}") + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] == '404': + print(f"Bucket not found, creating: {folder_name}") + client.create_bucket(Bucket=folder_name) + + # Upload the file to S3 using boto3 (avoids XAmzContentSHA256Mismatch) + try: + client.upload_file( + output_zip, + folder_name, + os.path.basename(output_zip), + ExtraArgs={ + 'ACL': 'public-read', + 'ContentType': 'application/zip' + } + ) + print(f"File successfully uploaded: {output_zip}") + except Exception as e: + print(f"Upload error: {e}") + + finally: + # Clean up local zip file + if os.path.exists(output_zip): + os.remove(output_zip) + print(f"Local zip file deleted: {output_zip}") diff --git a/ssh_manager/__init__.py b/ssh_manager/__init__.py new file mode 100644 index 0000000..c4d817e --- /dev/null +++ b/ssh_manager/__init__.py @@ -0,0 +1 @@ +default_app_config = 'ssh_manager.apps.SshManagerConfig' \ No newline at end of file diff --git a/ssh_manager/admin.py b/ssh_manager/admin.py new file mode 100644 index 0000000..913c1ff --- /dev/null +++ b/ssh_manager/admin.py @@ -0,0 +1,71 @@ +from django.contrib import admin +from .models import SSHCredential, Project, SSHLog + +@admin.register(SSHCredential) +class SSHCredentialAdmin(admin.ModelAdmin): + list_display = ('hostname', 'username', 'port', 'is_online', 'last_check') + list_filter = ('is_online', 'created_at') + search_fields = ('hostname', 'username') + readonly_fields = ('is_online', 'last_check', 'created_at') + fieldsets = ( + ('Bağlantı Bilgileri', { + 'fields': ('hostname', 'username', 'password', 'port') + }), + ('Durum Bilgisi', { + 'fields': ('is_online', 'last_check', 'created_at'), + 'classes': ('collapse',) + }), + ) + +@admin.register(Project) +class ProjectAdmin(admin.ModelAdmin): + list_display = ('name', 'folder_name', 'url', 'ssh_credential', 'created_at') + list_filter = ('ssh_credential', 'created_at') + search_fields = ('name', 'folder_name', 'url') + readonly_fields = ('created_at', 'updated_at') + fieldsets = ( + ('Temel Bilgiler', { + 'fields': ('name', 'folder_name', 'ssh_credential') + }), + ('Domain Bilgisi', { + 'fields': ('url',), + 'description': 'Nginx konfigürasyonu için domain adı (Örnek: example.com)' + }), + ('Zaman Bilgileri', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + + def get_full_path(self, obj): + return obj.get_full_path() + get_full_path.short_description = 'Tam Yol' + +@admin.register(SSHLog) +class SSHLogAdmin(admin.ModelAdmin): + list_display = ('ssh_credential', 'log_type', 'command', 'status', 'created_at') + list_filter = ('log_type', 'status', 'created_at') + search_fields = ('command', 'output') + readonly_fields = ('created_at',) + fieldsets = ( + ('Log Detayları', { + 'fields': ('ssh_credential', 'log_type', 'command', 'status') + }), + ('Çıktı', { + 'fields': ('output',), + 'classes': ('wide',) + }), + ('Zaman Bilgisi', { + 'fields': ('created_at',), + 'classes': ('collapse',) + }), + ) + + def get_queryset(self, request): + return super().get_queryset(request).select_related('ssh_credential') + + def has_add_permission(self, request): + return False # Log kayıtları manuel olarak eklenemez + + def has_change_permission(self, request, obj=None): + return False # Log kayıtları değiştirilemez \ No newline at end of file diff --git a/ssh_manager/apps.py b/ssh_manager/apps.py new file mode 100644 index 0000000..fd1e405 --- /dev/null +++ b/ssh_manager/apps.py @@ -0,0 +1,24 @@ +from django.apps import AppConfig +from django.conf import settings + +def check_server_connection(): + from .ssh_client import SSHManager # utils yerine ssh_client'dan import et + from .models import SSHCredential + + # Tüm SSH bağlantılarını kontrol et + for credential in SSHCredential.objects.all(): + ssh_manager = SSHManager(credential) + is_online = ssh_manager.check_connection() + + # Bağlantı durumunu güncelle + credential.is_online = is_online + credential.save(update_fields=['is_online', 'last_check']) + + ssh_manager.close() + +class SshManagerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'ssh_manager' + + def ready(self): + import ssh_manager.signals # signals'ı import et \ No newline at end of file diff --git a/ssh_manager/backup.py b/ssh_manager/backup.py new file mode 100644 index 0000000..33bc1df --- /dev/null +++ b/ssh_manager/backup.py @@ -0,0 +1,626 @@ +import os +import zipfile +import boto3 +from boto3.s3.transfer import TransferConfig +from django.utils.text import slugify +from datetime import datetime +import requests +import stat + +haric_dosya_uzantilari = ['.zip', ] +excluded_folders = ['venv', 'yedek', '.idea', '.sock'] +hostname = "ams1.vultrobjects.com" +secret_key = "Ec1pq3OQAObFLOQrfAVqJKhDAk4BkT7OqgYszlef" +access_key = "KQAOMJ8CQ8HP4CY23YPK" +x = 1 + + +def upload_file_via_presigned_url(url, file_path): + if not os.path.exists(file_path): + print(f"Dosya bulunamadi: {file_path}") + return False + + with open(file_path, 'rb') as file_data: + try: + response = requests.put(url, data=file_data) + if response.status_code == 200: + print("Dosya yuklendi!") + return True + else: + print(f"Yukleme olmadi. Status code: {response.status_code}") + print(f"Response: {response.content}") + return False + except Exception as e: + print(f"Yukleme hatasi: {e}") + return False + + +def get_filtered_folder_names(directory, excluded_folders): + folder_names = [] + + for item in os.listdir(directory): + item_path = os.path.join(directory, item) + if os.path.isdir(item_path) and item not in excluded_folders: + folder_names.append(item) + + return folder_names + + +def zip_klasor(ziplenecek_klasor, hedef_zip_adi, haric_klasorler=[], haric_dosya_uzantilari=[]): + # Parametrelerin geçerliliğini kontrol et + if not ziplenecek_klasor or not hedef_zip_adi: + raise ValueError("Ziplenecek klasör ve hedef zip adı boş olamaz") + + if not os.path.exists(ziplenecek_klasor): + raise FileNotFoundError(f"Ziplenecek klasör bulunamadı: {ziplenecek_klasor}") + + # Hedef zip dosyasının bulunacağı dizini oluştur ve izinleri ayarla + hedef_dizin = os.path.dirname(hedef_zip_adi) + + # Eğer hedef dizin boşsa, mevcut dizini kullan + if not hedef_dizin: + hedef_dizin = "." + hedef_zip_adi = os.path.join(hedef_dizin, hedef_zip_adi) + + if not os.path.exists(hedef_dizin): + os.makedirs(hedef_dizin, mode=0o755, exist_ok=True) + + # Zip dosyası oluşturmadan önce izinleri kontrol et + if os.path.exists(hedef_zip_adi): + try: + os.chmod(hedef_zip_adi, 0o666) + except Exception as e: + print(f"Mevcut zip dosyasinin izinleri guncellenemedi: {e}") + + with zipfile.ZipFile(hedef_zip_adi, 'w', zipfile.ZIP_DEFLATED) as zipf: + for klasor_yolu, _, dosya_listesi in os.walk(ziplenecek_klasor): + if not any(k in klasor_yolu for k in haric_klasorler): + for dosya in dosya_listesi: + dosya_adi, dosya_uzantisi = os.path.splitext(dosya) + dosya_yolu = os.path.join(klasor_yolu, dosya) + + # Dosyanın var olup olmadığını kontrol et + if not os.path.exists(dosya_yolu): + print(f"Dosya bulunamadi: {dosya_yolu}") + continue + + # Socket dosyalarını atla + try: + file_stat = os.stat(dosya_yolu) + if stat.S_ISSOCK(file_stat.st_mode): + print(f"Socket dosyasi atlandi: {dosya_yolu}") + continue + except (OSError, PermissionError) as e: + print(f"Dosya stat alinamadi: {dosya_yolu} -> Hata: {e}") + continue + + if dosya_uzantisi.lower() not in haric_dosya_uzantilari: + try: + # Dosya okuma izinlerini kontrol et + if os.access(dosya_yolu, os.R_OK): + zipf.write(dosya_yolu, os.path.relpath(dosya_yolu, ziplenecek_klasor)) + print(f"Dosya eklendi: {dosya_yolu}") + else: + print(f"Dosya okuma izni yok: {dosya_yolu}") + except (PermissionError, OSError) as e: + print(f"Dosya eklenemedi: {dosya_yolu} -> Hata: {e}") + except Exception as e: + print(f"Beklenmeyen hata: {dosya_yolu} -> Hata: {e}") + + # Oluşturulan zip dosyasının izinlerini ayarla + try: + os.chmod(hedef_zip_adi, 0o644) + print(f"Zip dosyasi olusturuldu: {hedef_zip_adi}") + except Exception as e: + print(f"Zip dosyasi izinleri ayarlanamadi: {e}") + + + +def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], excluded_extensions=[]): + """SSH üzerinden uzak sunucuda zip dosyası oluşturur""" + + # Uzak sunucuda geçici zip dosyası yolu + remote_zip_path = f"/tmp/{zip_name}" + + # Önce kaynak dizinin varlığını kontrol et + check_dir_command = f"test -d '{source_dir}' && echo 'exists' || echo 'not_exists'" + try: + stdout, stderr, status = ssh_manager.execute_command(check_dir_command) + if not status or stdout.strip() != "exists": + raise Exception(f"Kaynak dizin bulunamadı: {source_dir}") + except Exception as e: + raise Exception(f"Dizin kontrolü hatası: {str(e)}") + + # Zip komutunun varlığını kontrol et ve gerekirse kur + zip_check_command = "which zip || command -v zip" + try: + stdout, stderr, status = ssh_manager.execute_command(zip_check_command) + if not status: + print("Zip komutu bulunamadı, kurulum deneniyor...") + if not install_zip_on_remote(ssh_manager): + raise Exception("Zip komutu uzak sunucuda bulunamadı ve kurulum başarısız oldu.") + except Exception as e: + raise Exception(f"Zip komutu kontrolü hatası: {str(e)}") + + # Hariç tutulacak klasörler için exclude parametresi + exclude_args = "" + for folder in excluded_folders: + exclude_args += f" --exclude='{folder}/*' --exclude='{folder}'" + + for ext in excluded_extensions: + exclude_args += f" --exclude='*{ext}'" + + # Eski zip dosyasını temizle + cleanup_command = f"rm -f '{remote_zip_path}'" + ssh_manager.execute_command(cleanup_command) + + # Zip komutunu oluştur (daha basit ve güvenilir) + zip_command = f"cd '{source_dir}' && zip -r '{remote_zip_path}' . {exclude_args}" + + print(f"Çalıştırılan komut: {zip_command}") + + try: + stdout, stderr, status = ssh_manager.execute_command(zip_command) + + print(f"Zip komutu sonucu - Status: {status}, Stdout: {stdout}, Stderr: {stderr}") + + # Zip komutu bazen uyarılarla birlikte başarılı olabilir + # Bu yüzden sadece status kontrolü yerine dosya varlığını da kontrol edelim + + # Zip dosyasının varlığını kontrol et + check_command = f"test -f '{remote_zip_path}' && echo 'exists' || echo 'not_exists'" + stdout_check, stderr_check, status_check = ssh_manager.execute_command(check_command) + + if not status_check or stdout_check.strip() != "exists": + error_details = f"Status: {status}, Stdout: {stdout}, Stderr: {stderr}" + raise Exception(f"Zip dosyası oluşturulamadı. Detaylar: {error_details}") + + # Dosya boyutunu al + size_command = f"stat -c%s '{remote_zip_path}' 2>/dev/null || stat -f%z '{remote_zip_path}' 2>/dev/null || wc -c < '{remote_zip_path}'" + stdout_size, stderr_size, status_size = ssh_manager.execute_command(size_command) + + file_size = 0 + if status_size and stdout_size.strip().isdigit(): + file_size = int(stdout_size.strip()) + else: + # Boyut alınamazsa alternatif yöntem + ls_command = f"ls -la '{remote_zip_path}'" + stdout_ls, stderr_ls, status_ls = ssh_manager.execute_command(ls_command) + if status_ls: + print(f"Zip dosyası bilgileri: {stdout_ls}") + + print(f"Zip dosyası başarıyla oluşturuldu: {remote_zip_path}, Boyut: {file_size}") + return remote_zip_path, file_size + + except Exception as e: + # Hata durumunda oluşmuş olabilecek zip dosyasını temizle + cleanup_command = f"rm -f '{remote_zip_path}'" + ssh_manager.execute_command(cleanup_command) + raise e + + +def download_ssh_file(ssh_manager, remote_path, local_path): + """SSH üzerinden dosya indirir""" + try: + print(f"Dosya indiriliyor: {remote_path} -> {local_path}") + + # Local dizinin varlığını kontrol et ve oluştur + local_dir = os.path.dirname(local_path) + if not os.path.exists(local_dir): + os.makedirs(local_dir, mode=0o755, exist_ok=True) + + # SFTP kullanarak dosyayı indir + with ssh_manager.client.open_sftp() as sftp: + # Uzak dosyanın varlığını kontrol et + try: + file_stat = sftp.stat(remote_path) + print(f"Uzak dosya boyutu: {file_stat.st_size} byte") + except FileNotFoundError: + raise Exception(f"Uzak dosya bulunamadı: {remote_path}") + + sftp.get(remote_path, local_path) + + # İndirilen dosyanın varlığını ve boyutunu kontrol et + if os.path.exists(local_path): + local_size = os.path.getsize(local_path) + print(f"Dosya başarıyla indirildi. Local boyut: {local_size} byte") + return True + else: + raise Exception("Dosya indirildikten sonra bulunamadı") + + except Exception as e: + print(f"Dosya indirme hatası: {e}") + # Başarısız indirme durumunda local dosyayı temizle + if os.path.exists(local_path): + try: + os.remove(local_path) + except: + pass + return False + + +def cleanup_ssh_file(ssh_manager, remote_path): + """SSH sunucusunda geçici dosyayı temizler""" + try: + cleanup_command = f"rm -f '{remote_path}'" + ssh_manager.execute_command(cleanup_command) + except Exception as e: + print(f"Temizleme hatası: {e}") + + +from ssh_manager.models import SSHLog, Project, SSHCredential + +def job(folder, calisma_dizini, project_id=None): + import ssl + logs = [] + + # Parametrelerin geçerliliğini kontrol et + if not folder or folder.strip() == "": + return {'success': False, 'message': 'Klasör adı boş olamaz', 'logs': logs} + + if not calisma_dizini or calisma_dizini.strip() == "": + return {'success': False, 'message': 'Çalışma dizini boş olamaz', 'logs': logs} + + if not project_id: + return {'success': False, 'message': 'Proje ID gerekli', 'logs': logs} + + # NOT: calisma_dizini SSH sunucusundaki bir yol olduğu için burada local kontrol yapılmaz + # Dizin kontrolü views.py'da SSH üzerinden yapılmalı + + try: + project = Project.objects.get(id=project_id) + ssh_manager = project.ssh_credential.get_manager() + except Exception as e: + return {'success': False, 'message': f'SSH bağlantısı kurulamadı: {str(e)}', 'logs': logs} + + # --- Vultr/S3 config --- + config = { + 'access_key': "KQAOMJ8CQ8HP4CY23YPK", + 'secret_key': "Ec1pq3OQAObFLOQrfAVqJKhDAk4BkT7OqgYszlef", + 'host_base': "ams1.vultrobjects.com", + 'bucket_location': "US", + 'use_https': True, + 'check_ssl_certificate': False, # SSL doğrulamasını kapat + 'multipart_chunk_size_mb': 50, # Chunk boyutunu artır + } + endpoint_url = f"https://{config['host_base']}" + region_name = config['bucket_location'] + # --- + session = boto3.session.Session() + client = session.client('s3', + region_name=region_name, + endpoint_url=endpoint_url, + aws_access_key_id=config['access_key'], + aws_secret_access_key=config['secret_key'], + use_ssl=config['use_https'], + verify=False, # SSL doğrulamasını tamamen kapat + config=boto3.session.Config( + signature_version='s3v4', + retries={'max_attempts': 3}, + s3={ + 'addressing_style': 'path', + 'payload_signing_enabled': False, + 'chunked_encoding': False + } + ) + ) + def log_and_db(msg, status=True): + logs.append(msg) + if project_id: + try: + project = Project.objects.get(id=project_id) + SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='backup', + command=f'Backup: {folder}', + output=msg, + status=status + ) + except Exception: + pass + log_and_db("S3 oturumu başlatıldı.") + local_dt = datetime.now() + current_date = slugify(str(local_dt)) + + # Zip dosyası için tam yol oluştur + zip_dosya_adi = folder + "_" + current_date + ".zip" + output_zip = os.path.join("/tmp", zip_dosya_adi) # /tmp dizininde oluştur + + log_and_db(f"SSH üzerinden zip dosyası oluşturuluyor...") + + try: + # SSH üzerinden uzak sunucuda zip oluştur + zip_dosya_adi = folder + "_" + current_date + ".zip" + + log_and_db(f"Kaynak dizin: {calisma_dizini}") + log_and_db(f"Zip dosyası adı: {zip_dosya_adi}") + + try: + remote_zip_path, file_size = create_ssh_zip( + ssh_manager, + calisma_dizini, + zip_dosya_adi, + excluded_folders, + haric_dosya_uzantilari + ) + + log_and_db(f"Uzak sunucuda zip oluşturuldu: {remote_zip_path} ({file_size} byte)") + + # Zip dosyasını local'e indir + local_zip_path = os.path.join("/tmp", zip_dosya_adi) + + log_and_db(f"Zip dosyası indiriliyor: {local_zip_path}") + + if not download_ssh_file(ssh_manager, remote_zip_path, local_zip_path): + raise Exception("Zip dosyası indirilemedi") + + log_and_db(f"Zip dosyası başarıyla indirildi") + + # Uzak sunucudaki geçici zip dosyasını temizle + cleanup_ssh_file(ssh_manager, remote_zip_path) + + output_zip = local_zip_path + + except Exception as zip_error: + log_and_db(f"Zip oluşturma başarısız: {str(zip_error)}") + log_and_db(f"Tar ile yedekleme deneniyor...") + + # Zip başarısız olursa tar kullan + tar_dosya_adi = folder + "_" + current_date + ".tar.gz" + + remote_tar_path, file_size = create_tar_backup( + ssh_manager, + calisma_dizini, + tar_dosya_adi, + excluded_folders, + haric_dosya_uzantilari + ) + + log_and_db(f"Uzak sunucuda tar.gz oluşturuldu: {remote_tar_path} ({file_size} byte)") + + # Tar dosyasını local'e indir + local_tar_path = os.path.join("/tmp", tar_dosya_adi) + + log_and_db(f"Tar dosyası indiriliyor: {local_tar_path}") + + if not download_ssh_file(ssh_manager, remote_tar_path, local_tar_path): + raise Exception("Tar dosyası indirilemedi") + + log_and_db(f"Tar dosyası başarıyla indirildi") + + # Uzak sunucudaki geçici tar dosyasını temizle + cleanup_ssh_file(ssh_manager, remote_tar_path) + + output_zip = local_tar_path + + except Exception as e: + error_msg = f"SSH zip oluşturma hatası: {str(e)}" + log_and_db(f"{error_msg}", status=False) + + # SSH bağlantısını kapat + try: + ssh_manager.close() + except: + pass + + return {'success': False, 'message': error_msg, 'logs': logs} + log_and_db(f"Zip işlemi tamamlandı: {output_zip}") + + # --- Zip dosyası oluştu mu ve boş mu kontrolü --- + if not os.path.exists(output_zip): + log_and_db(f"Zip dosyası oluşmadı: {output_zip}", status=False) + return {'success': False, 'message': 'Zip dosyası oluşmadı', 'logs': logs} + else: + size = os.path.getsize(output_zip) + log_and_db(f"Zip dosyası boyutu: {size} byte") + if size == 0: + log_and_db(f"Zip dosyası BOŞ!", status=False) + return {'success': False, 'message': 'Zip dosyası boş', 'logs': logs} + + bucket_name = folder + s3_key = output_zip # Bucket içinde alt klasör olmadan doğrudan zip dosyası + try: + # Bucket kontrol/oluşturma + buckets = client.list_buckets() + bucket_exists = any(obj['Name'] == bucket_name for obj in buckets['Buckets']) + if not bucket_exists: + client.create_bucket(Bucket=bucket_name) + log_and_db(f"Bucket oluşturuldu: {bucket_name}") + else: + log_and_db(f"Bucket mevcut: {bucket_name}") + # S3'e yükle (Vultr Object Storage için özel yöntem) + log_and_db(f"Dosya S3'e yükleniyor: {s3_key}") + + # Dosya boyutunu kontrol et + file_size = os.path.getsize(output_zip) + log_and_db(f"Yüklenecek dosya boyutu: {file_size} bytes") + + try: + # Küçük dosyalar için basit put_object kullan + if file_size < 50 * 1024 * 1024: # 50MB'dan küçükse + with open(output_zip, 'rb') as file_data: + client.put_object( + Bucket=bucket_name, + Key=s3_key, + Body=file_data.read(), + ACL='private', + ContentType='application/zip', + Metadata={ + 'uploaded_by': 'ssh_manager', + 'upload_date': current_date + } + ) + else: + # Büyük dosyalar için multipart upload + transfer_config = TransferConfig( + multipart_threshold=1024 * 1024 * 50, # 50MB + max_concurrency=1, # Tek thread kullan + multipart_chunksize=1024 * 1024 * 50, # 50MB chunk + use_threads=False + ) + + client.upload_file( + output_zip, + bucket_name, + s3_key, + ExtraArgs={ + 'ACL': 'private', + 'ContentType': 'application/zip', + 'Metadata': { + 'uploaded_by': 'ssh_manager', + 'upload_date': current_date + } + }, + Config=transfer_config + ) + except Exception as upload_error: + # Son çare: presigned URL ile yükleme + log_and_db(f"Standart yükleme başarısız, presigned URL deneniyor: {upload_error}") + + try: + presigned_url = client.generate_presigned_url( + 'put_object', + Params={'Bucket': bucket_name, 'Key': s3_key}, + ExpiresIn=3600 + ) + + import requests + with open(output_zip, 'rb') as file_data: + headers = {'Content-Type': 'application/zip'} + response = requests.put(presigned_url, data=file_data, headers=headers) + + if response.status_code not in [200, 201]: + raise Exception(f"Presigned URL yükleme hatası: {response.status_code} - {response.text}") + + except Exception as presigned_error: + raise Exception(f"Tüm yükleme yöntemleri başarısız: {presigned_error}") + log_and_db(f"S3'e başarıyla yüklendi: {bucket_name}/{s3_key}") + except Exception as e: + log_and_db(f"S3 yükleme hatası: {e}", status=False) + return {'success': False, 'message': str(e), 'logs': logs} + finally: + if os.path.exists(output_zip): + os.remove(output_zip) + log_and_db(f"Geçici zip dosyası silindi: {output_zip}") + return {'success': True, 'message': 'Yedekleme tamamlandı', 'logs': logs} + +def install_zip_on_remote(ssh_manager): + """Uzak sunucuya zip kurulumu yapar""" + + # Önce zip komutunun varlığını kontrol et + check_zip = "which zip || command -v zip" + stdout, stderr, status = ssh_manager.execute_command(check_zip) + + if status and stdout.strip(): + print(f"Zip komutu zaten kurulu: {stdout.strip()}") + return True + + print("Zip komutu bulunamadı, kurulum yapılıyor...") + + # İşletim sistemi kontrolü + os_check = "cat /etc/os-release 2>/dev/null || uname -a" + stdout, stderr, status = ssh_manager.execute_command(os_check) + + install_commands = [] + + if "ubuntu" in stdout.lower() or "debian" in stdout.lower(): + install_commands = [ + "sudo apt-get update -y", + "sudo apt-get install -y zip unzip" + ] + elif "centos" in stdout.lower() or "rhel" in stdout.lower() or "red hat" in stdout.lower(): + install_commands = [ + "sudo yum install -y zip unzip" + ] + elif "alpine" in stdout.lower(): + install_commands = [ + "sudo apk update", + "sudo apk add zip unzip" + ] + else: + # Diğer sistemler için genel deneme + install_commands = [ + "sudo apt-get update -y && sudo apt-get install -y zip unzip", + "sudo yum install -y zip unzip", + "sudo apk add zip unzip" + ] + + # Kurulum komutlarını dene + for cmd in install_commands: + print(f"Denenen komut: {cmd}") + stdout, stderr, status = ssh_manager.execute_command(cmd) + + if status: + # Kurulum sonrası zip kontrolü + stdout_check, stderr_check, status_check = ssh_manager.execute_command("which zip") + if status_check and stdout_check.strip(): + print(f"Zip başarıyla kuruldu: {stdout_check.strip()}") + return True + else: + print(f"Kurulum hatası: {stderr}") + + print("Zip kurulumu başarısız") + return False + + +def create_tar_backup(ssh_manager, source_dir, tar_name, excluded_folders=[], excluded_extensions=[]): + """SSH üzerinden tar kullanarak yedek oluşturur (zip alternatifi)""" + + # Uzak sunucuda geçici tar dosyası yolu + remote_tar_path = f"/tmp/{tar_name}" + + # Kaynak dizinin varlığını kontrol et + check_dir_command = f"test -d '{source_dir}' && echo 'exists' || echo 'not_exists'" + stdout, stderr, status = ssh_manager.execute_command(check_dir_command) + + if not status or stdout.strip() != "exists": + raise Exception(f"Kaynak dizin bulunamadı: {source_dir}") + + # Hariç tutulacak klasörler için exclude parametresi + exclude_args = "" + for folder in excluded_folders: + exclude_args += f" --exclude='{folder}'" + + for ext in excluded_extensions: + exclude_args += f" --exclude='*{ext}'" + + # Eski tar dosyasını temizle + cleanup_command = f"rm -f '{remote_tar_path}'" + ssh_manager.execute_command(cleanup_command) + + # Tar komutunu oluştur (gzip ile sıkıştır) + tar_command = f"cd '{source_dir}' && tar -czf '{remote_tar_path}' {exclude_args} . 2>/dev/null" + + print(f"Çalıştırılan tar komutu: {tar_command}") + + try: + stdout, stderr, status = ssh_manager.execute_command(tar_command) + + print(f"Tar komutu sonucu - Status: {status}, Stdout: {stdout}, Stderr: {stderr}") + + # Tar dosyasının varlığını kontrol et + check_command = f"test -f '{remote_tar_path}' && echo 'exists' || echo 'not_exists'" + stdout_check, stderr_check, status_check = ssh_manager.execute_command(check_command) + + if not status_check or stdout_check.strip() != "exists": + error_details = f"Status: {status}, Stdout: {stdout}, Stderr: {stderr}" + raise Exception(f"Tar dosyası oluşturulamadı. Detaylar: {error_details}") + + # Dosya boyutunu al + size_command = f"stat -c%s '{remote_tar_path}' 2>/dev/null || stat -f%z '{remote_tar_path}' 2>/dev/null || wc -c < '{remote_tar_path}'" + stdout_size, stderr_size, status_size = ssh_manager.execute_command(size_command) + + file_size = 0 + if status_size and stdout_size.strip().isdigit(): + file_size = int(stdout_size.strip()) + + print(f"Tar dosyası başarıyla oluşturuldu: {remote_tar_path}, Boyut: {file_size}") + return remote_tar_path, file_size + + except Exception as e: + # Hata durumunda oluşmuş olabilecek tar dosyasını temizle + cleanup_command = f"rm -f '{remote_tar_path}'" + ssh_manager.execute_command(cleanup_command) + raise e + + diff --git a/ssh_manager/middleware.py b/ssh_manager/middleware.py new file mode 100644 index 0000000..1729b9c --- /dev/null +++ b/ssh_manager/middleware.py @@ -0,0 +1,11 @@ +from django.utils.deprecation import MiddlewareMixin + +class SSHConnectionMiddleware(MiddlewareMixin): + _connection_checked = False # Sınıf değişkeni olarak tanımla + + def process_request(self, request): + # Sadece bir kez çalıştır + if not SSHConnectionMiddleware._connection_checked: + from .apps import check_server_connection + check_server_connection() + SSHConnectionMiddleware._connection_checked = True \ No newline at end of file diff --git a/ssh_manager/migrations/0001_initial.py b/ssh_manager/migrations/0001_initial.py new file mode 100644 index 0000000..2502c32 --- /dev/null +++ b/ssh_manager/migrations/0001_initial.py @@ -0,0 +1,54 @@ +# Generated by Django 5.1.5 on 2025-01-23 05:38 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='SSHCredential', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('hostname', models.CharField(max_length=255)), + ('username', models.CharField(max_length=100)), + ('password', models.CharField(max_length=100)), + ('port', models.IntegerField(default=22)), + ('is_online', models.BooleanField(default=False)), + ('last_check', models.DateTimeField(auto_now=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('folder_name', models.CharField(max_length=255)), + ('path', models.CharField(max_length=500)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('ssh_credential', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ssh_manager.sshcredential')), + ], + ), + migrations.CreateModel( + name='SSHLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('log_type', models.CharField(choices=[('connection', 'Bağlantı Kontrolü'), ('command', 'Komut Çalıştırma'), ('folder', 'Klasör İşlemi')], max_length=20)), + ('command', models.TextField()), + ('output', models.TextField()), + ('status', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('ssh_credential', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ssh_manager.sshcredential')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/ssh_manager/migrations/0002_default_ssh_credential.py b/ssh_manager/migrations/0002_default_ssh_credential.py new file mode 100644 index 0000000..356efc5 --- /dev/null +++ b/ssh_manager/migrations/0002_default_ssh_credential.py @@ -0,0 +1,20 @@ +from django.db import migrations + +def create_default_ssh_credential(apps, schema_editor): + SSHCredential = apps.get_model('ssh_manager', 'SSHCredential') + if not SSHCredential.objects.exists(): + SSHCredential.objects.create( + hostname='localhost', # Varsayılan sunucu bilgileri + username='root', + password='password', + port=22 + ) + +class Migration(migrations.Migration): + dependencies = [ + ('ssh_manager', '0001_initial'), + ] + + operations = [ + migrations.RunPython(create_default_ssh_credential), + ] \ No newline at end of file diff --git a/ssh_manager/migrations/0003_remove_project_path_sshcredential_base_path.py b/ssh_manager/migrations/0003_remove_project_path_sshcredential_base_path.py new file mode 100644 index 0000000..8ec7307 --- /dev/null +++ b/ssh_manager/migrations/0003_remove_project_path_sshcredential_base_path.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.5 on 2025-01-23 06:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ssh_manager', '0002_default_ssh_credential'), + ] + + operations = [ + migrations.RemoveField( + model_name='project', + name='path', + ), + migrations.AddField( + model_name='sshcredential', + name='base_path', + field=models.CharField(default=1, help_text='Projelerin oluşturulacağı ana dizin', max_length=500), + preserve_default=False, + ), + ] diff --git a/ssh_manager/migrations/0004_project_updated_at_alter_project_folder_name_and_more.py b/ssh_manager/migrations/0004_project_updated_at_alter_project_folder_name_and_more.py new file mode 100644 index 0000000..fc849a0 --- /dev/null +++ b/ssh_manager/migrations/0004_project_updated_at_alter_project_folder_name_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.5 on 2025-01-23 15:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ssh_manager', '0003_remove_project_path_sshcredential_base_path'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='project', + name='folder_name', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='project', + name='name', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='sshlog', + name='log_type', + field=models.CharField(choices=[('connection', 'Bağlantı Kontrolü'), ('command', 'Komut Çalıştırma'), ('folder', 'Klasör İşlemi'), ('file', 'Dosya İşlemi')], max_length=20), + ), + ] diff --git a/ssh_manager/migrations/0005_alter_project_options_project_url_and_more.py b/ssh_manager/migrations/0005_alter_project_options_project_url_and_more.py new file mode 100644 index 0000000..0db5fd1 --- /dev/null +++ b/ssh_manager/migrations/0005_alter_project_options_project_url_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.1.5 on 2025-01-25 05:25 + +import django.db.models.deletion +import ssh_manager.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ssh_manager', '0004_project_updated_at_alter_project_folder_name_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='project', + options={'ordering': ['-created_at'], 'verbose_name': 'Proje', 'verbose_name_plural': 'Projeler'}, + ), + migrations.AddField( + model_name='project', + name='url', + field=models.CharField(blank=True, help_text='Örnek: example.com veya subdomain.example.com', max_length=255, null=True, unique=True, validators=[ssh_manager.models.validate_domain], verbose_name='Domain Adı'), + ), + migrations.AlterField( + model_name='project', + name='folder_name', + field=models.CharField(max_length=100, verbose_name='Klasör Adı'), + ), + migrations.AlterField( + model_name='project', + name='name', + field=models.CharField(max_length=100, verbose_name='Proje Adı'), + ), + migrations.AlterField( + model_name='project', + name='ssh_credential', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ssh_manager.sshcredential', verbose_name='SSH Bağlantısı'), + ), + ] diff --git a/ssh_manager/migrations/0006_project_disk_usage_alter_project_folder_name_and_more.py b/ssh_manager/migrations/0006_project_disk_usage_alter_project_folder_name_and_more.py new file mode 100644 index 0000000..30bffba --- /dev/null +++ b/ssh_manager/migrations/0006_project_disk_usage_alter_project_folder_name_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.1.5 on 2025-01-29 04:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ssh_manager', '0005_alter_project_options_project_url_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='disk_usage', + field=models.CharField(blank=True, help_text='Projenin disk kullanımı', max_length=20, null=True), + ), + migrations.AlterField( + model_name='project', + name='folder_name', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='project', + name='name', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='project', + name='ssh_credential', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ssh_manager.sshcredential'), + ), + migrations.AlterField( + model_name='project', + name='url', + field=models.CharField(blank=True, max_length=200, null=True), + ), + ] diff --git a/ssh_manager/migrations/0007_project_last_backup_alter_project_disk_usage_and_more.py b/ssh_manager/migrations/0007_project_last_backup_alter_project_disk_usage_and_more.py new file mode 100644 index 0000000..e067e3a --- /dev/null +++ b/ssh_manager/migrations/0007_project_last_backup_alter_project_disk_usage_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.1.5 on 2025-01-30 10:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ssh_manager', '0006_project_disk_usage_alter_project_folder_name_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='last_backup', + field=models.DateTimeField(blank=True, null=True, verbose_name='Son Yedekleme'), + ), + migrations.AlterField( + model_name='project', + name='disk_usage', + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AlterField( + model_name='project', + name='folder_name', + field=models.CharField(max_length=100, verbose_name='Klasör Adı'), + ), + migrations.AlterField( + model_name='project', + name='name', + field=models.CharField(max_length=100, verbose_name='Proje Adı'), + ), + migrations.AlterField( + model_name='project', + name='url', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/ssh_manager/migrations/0008_project_email_project_image_project_phone_and_more.py b/ssh_manager/migrations/0008_project_email_project_image_project_phone_and_more.py new file mode 100644 index 0000000..8de1145 --- /dev/null +++ b/ssh_manager/migrations/0008_project_email_project_image_project_phone_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.5 on 2025-03-01 18:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ssh_manager', '0007_project_last_backup_alter_project_disk_usage_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='email', + field=models.EmailField(blank=True, max_length=254, null=True), + ), + migrations.AddField( + model_name='project', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='project_images'), + ), + migrations.AddField( + model_name='project', + name='phone', + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AlterField( + model_name='sshlog', + name='log_type', + field=models.CharField(choices=[('connection', 'Bağlantı Kontrolü'), ('command', 'Komut Çalıştırma'), ('folder', 'Klasör İşlemi'), ('file', 'Dosya İşlemi'), ('backup', 'Yedekleme')], max_length=20), + ), + ] diff --git a/ssh_manager/migrations/0009_remove_project_email_remove_project_image_and_more.py b/ssh_manager/migrations/0009_remove_project_email_remove_project_image_and_more.py new file mode 100644 index 0000000..cf438b8 --- /dev/null +++ b/ssh_manager/migrations/0009_remove_project_email_remove_project_image_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 5.2.4 on 2025-07-20 00:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ssh_manager', '0008_project_email_project_image_project_phone_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='project', + name='email', + ), + migrations.RemoveField( + model_name='project', + name='image', + ), + migrations.RemoveField( + model_name='project', + name='phone', + ), + migrations.AddField( + model_name='project', + name='is_site_active', + field=models.BooleanField(default=False, verbose_name='Site Aktif'), + ), + migrations.AddField( + model_name='project', + name='last_site_check', + field=models.DateTimeField(blank=True, null=True, verbose_name='Son Site Kontrolü'), + ), + migrations.AddField( + model_name='project', + name='meta_key', + field=models.CharField(blank=True, help_text='Site aktiflik kontrolü için benzersiz anahtar', max_length=32, null=True, verbose_name='Meta Key'), + ), + ] diff --git a/ssh_manager/migrations/0010_sshcredential_disk_usage.py b/ssh_manager/migrations/0010_sshcredential_disk_usage.py new file mode 100644 index 0000000..595b784 --- /dev/null +++ b/ssh_manager/migrations/0010_sshcredential_disk_usage.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-07-20 02:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ssh_manager', '0009_remove_project_email_remove_project_image_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='sshcredential', + name='disk_usage', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Disk Kullanımı'), + ), + ] diff --git a/ssh_manager/migrations/0011_customer_project_customer.py b/ssh_manager/migrations/0011_customer_project_customer.py new file mode 100644 index 0000000..e219700 --- /dev/null +++ b/ssh_manager/migrations/0011_customer_project_customer.py @@ -0,0 +1,46 @@ +# Generated by Django 5.2.4 on 2025-07-20 11:19 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ssh_manager', '0010_sshcredential_disk_usage'), + ] + + operations = [ + migrations.CreateModel( + name='Customer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('customer_type', models.CharField(choices=[('individual', 'Bireysel'), ('corporate', 'Kurumsal')], max_length=20, verbose_name='Müşteri Tipi')), + ('name', models.CharField(max_length=200, verbose_name='Ad/Firma Adı')), + ('email', models.EmailField(max_length=254, verbose_name='E-posta')), + ('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Telefon')), + ('address', models.TextField(blank=True, null=True, verbose_name='Adres')), + ('surname', models.CharField(blank=True, max_length=100, null=True, verbose_name='Soyad')), + ('birth_date', models.DateField(blank=True, null=True, verbose_name='Doğum Tarihi')), + ('tc_number', models.CharField(blank=True, max_length=11, null=True, verbose_name='TC Kimlik No')), + ('company_name', models.CharField(blank=True, max_length=200, null=True, verbose_name='Şirket Adı')), + ('tax_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='Vergi No')), + ('tax_office', models.CharField(blank=True, max_length=100, null=True, verbose_name='Vergi Dairesi')), + ('authorized_person', models.CharField(blank=True, max_length=200, null=True, verbose_name='Yetkili Kişi')), + ('notes', models.TextField(blank=True, null=True, verbose_name='Notlar')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturma Tarihi')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')), + ('is_active', models.BooleanField(default=True, verbose_name='Aktif')), + ], + options={ + 'verbose_name': 'Müşteri', + 'verbose_name_plural': 'Müşteriler', + 'ordering': ['-created_at'], + }, + ), + migrations.AddField( + model_name='project', + name='customer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='ssh_manager.customer', verbose_name='Müşteri'), + ), + ] diff --git a/ssh_manager/migrations/0012_sshcredential_connection_status_and_more.py b/ssh_manager/migrations/0012_sshcredential_connection_status_and_more.py new file mode 100644 index 0000000..70b187c --- /dev/null +++ b/ssh_manager/migrations/0012_sshcredential_connection_status_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.4 on 2025-07-20 13:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ssh_manager', '0011_customer_project_customer'), + ] + + operations = [ + migrations.AddField( + model_name='sshcredential', + name='connection_status', + field=models.CharField(choices=[('connected', 'Bağlı'), ('failed', 'Başarısız'), ('unknown', 'Bilinmiyor')], default='unknown', max_length=20), + ), + migrations.AddField( + model_name='sshcredential', + name='is_default', + field=models.BooleanField(default=False, verbose_name='Varsayılan Host'), + ), + migrations.AddField( + model_name='sshcredential', + name='last_checked', + field=models.DateTimeField(blank=True, null=True, verbose_name='Son Kontrol'), + ), + migrations.AddField( + model_name='sshcredential', + name='name', + field=models.CharField(default='Varsayılan Host', max_length=100, verbose_name='Host Adı'), + ), + migrations.AlterField( + model_name='sshcredential', + name='disk_usage', + field=models.FloatField(blank=True, null=True, verbose_name='Disk Kullanımı (%)'), + ), + ] diff --git a/ssh_manager/migrations/__init__.py b/ssh_manager/migrations/__init__.py new file mode 100644 index 0000000..c92f47a --- /dev/null +++ b/ssh_manager/migrations/__init__.py @@ -0,0 +1 @@ +# Bu dosya boş kalacak \ No newline at end of file diff --git a/ssh_manager/models.py b/ssh_manager/models.py new file mode 100644 index 0000000..4de8401 --- /dev/null +++ b/ssh_manager/models.py @@ -0,0 +1,198 @@ +from django.db import models +import logging +from .ssh_client import SSHManager +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError + +logger = logging.getLogger(__name__) + +def validate_domain(value): + """Domain formatını kontrol et""" + if ' ' in value: # Boşluk kontrolü + raise ValidationError('Domain adında boşluk olamaz') + if not any(char.isalpha() for char in value): # En az bir harf kontrolü + raise ValidationError('Domain adında en az bir harf olmalıdır') + if not all(c.isalnum() or c in '-.' for c in value): # Geçerli karakter kontrolü + raise ValidationError('Domain adında sadece harf, rakam, tire (-) ve nokta (.) olabilir') + +class Customer(models.Model): + CUSTOMER_TYPES = ( + ('individual', 'Bireysel'), + ('corporate', 'Kurumsal'), + ) + + customer_type = models.CharField(max_length=20, choices=CUSTOMER_TYPES, verbose_name='Müşteri Tipi') + + # Ortak alanlar + name = models.CharField(max_length=200, verbose_name='Ad/Firma Adı') + email = models.EmailField(verbose_name='E-posta') + phone = models.CharField(max_length=20, blank=True, null=True, verbose_name='Telefon') + address = models.TextField(blank=True, null=True, verbose_name='Adres') + + # Bireysel müşteri alanları + surname = models.CharField(max_length=100, blank=True, null=True, verbose_name='Soyad') + birth_date = models.DateField(blank=True, null=True, verbose_name='Doğum Tarihi') + tc_number = models.CharField(max_length=11, blank=True, null=True, verbose_name='TC Kimlik No') + + # Kurumsal müşteri alanları + company_name = models.CharField(max_length=200, blank=True, null=True, verbose_name='Şirket Adı') + tax_number = models.CharField(max_length=20, blank=True, null=True, verbose_name='Vergi No') + tax_office = models.CharField(max_length=100, blank=True, null=True, verbose_name='Vergi Dairesi') + authorized_person = models.CharField(max_length=200, blank=True, null=True, verbose_name='Yetkili Kişi') + + # Genel bilgiler + notes = models.TextField(blank=True, null=True, verbose_name='Notlar') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='Oluşturma Tarihi') + updated_at = models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi') + is_active = models.BooleanField(default=True, verbose_name='Aktif') + + def __str__(self): + if self.customer_type == 'corporate': + return f"{self.company_name or self.name} (Kurumsal)" + else: + return f"{self.name} {self.surname or ''} (Bireysel)".strip() + + def get_display_name(self): + """Görüntüleme için uygun isim döndür""" + if self.customer_type == 'corporate': + return self.company_name or self.name + else: + return f"{self.name} {self.surname or ''}".strip() + + def clean(self): + """Model validasyonu""" + if self.customer_type == 'individual': + if not self.surname: + raise ValidationError('Bireysel müşteriler için soyad zorunludur.') + elif self.customer_type == 'corporate': + if not self.company_name: + raise ValidationError('Kurumsal müşteriler için şirket adı zorunludur.') + + class Meta: + verbose_name = "Müşteri" + verbose_name_plural = "Müşteriler" + ordering = ['-created_at'] + +class SSHCredential(models.Model): + name = models.CharField(max_length=100, verbose_name='Host Adı', default='Varsayılan Host') + hostname = models.CharField(max_length=255) + username = models.CharField(max_length=100) + password = models.CharField(max_length=100) + port = models.IntegerField(default=22) + base_path = models.CharField(max_length=500, help_text="Projelerin oluşturulacağı ana dizin") # Yeni alan + is_default = models.BooleanField(default=False, verbose_name='Varsayılan Host') + connection_status = models.CharField(max_length=20, default='unknown', choices=[ + ('connected', 'Bağlı'), + ('failed', 'Başarısız'), + ('unknown', 'Bilinmiyor') + ]) + is_online = models.BooleanField(default=False) # Bağlantı durumu + last_check = models.DateTimeField(auto_now=True) # Son kontrol zamanı + last_checked = models.DateTimeField(null=True, blank=True, verbose_name='Son Kontrol') + disk_usage = models.FloatField(null=True, blank=True, verbose_name='Disk Kullanımı (%)') + created_at = models.DateTimeField(auto_now_add=True) + + def get_manager(self): + """SSHManager instance'ı döndür""" + return SSHManager(self) + + def __str__(self): + return f"{self.username}@{self.hostname}" + +class Project(models.Model): + name = models.CharField(max_length=100, verbose_name='Proje Adı') + folder_name = models.CharField(max_length=100, verbose_name='Klasör Adı') + ssh_credential = models.ForeignKey(SSHCredential, on_delete=models.CASCADE) + customer = models.ForeignKey(Customer, on_delete=models.CASCADE, verbose_name='Müşteri', null=True, blank=True) + url = models.CharField(max_length=255, null=True, blank=True) + disk_usage = models.CharField(max_length=20, null=True, blank=True) + last_backup = models.DateTimeField(null=True, blank=True, verbose_name='Son Yedekleme') + meta_key = models.CharField(max_length=32, null=True, blank=True, verbose_name='Meta Key', help_text='Site aktiflik kontrolü için benzersiz anahtar') + is_site_active = models.BooleanField(default=False, verbose_name='Site Aktif') + last_site_check = models.DateTimeField(null=True, blank=True, verbose_name='Son Site Kontrolü') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def generate_meta_key(self): + """Benzersiz meta key oluştur""" + import uuid + self.meta_key = uuid.uuid4().hex[:32] + return self.meta_key + + def get_meta_tag(self): + """HTML meta tag'ı döndür""" + if not self.meta_key: + self.generate_meta_key() + self.save() + return f'' + + def clean(self): + # URL formatını kontrol et + if self.url: + # URL'den http/https ve www. kısımlarını temizle + cleaned_url = self.url.lower() + for prefix in ['http://', 'https://', 'www.']: + if cleaned_url.startswith(prefix): + cleaned_url = cleaned_url[len(prefix):] + # Sondaki / işaretini kaldır + cleaned_url = cleaned_url.rstrip('/') + self.url = cleaned_url + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + + def get_full_path(self): + """Projenin tam yolunu döndür""" + return f"{self.ssh_credential.base_path}/{self.folder_name}" + + @property + def has_requirements(self): + """Projenin req.txt dosyası var mı kontrol et""" + try: + ssh_manager = self.ssh_credential.get_manager() + check_cmd = f'test -f "{self.get_full_path()}/req.txt" && echo "exists"' + stdout, stderr, status = ssh_manager.execute_command(check_cmd) + + # Debug için log ekle + logger.info(f"has_requirements check for project {self.id}") + logger.info(f"Command: {check_cmd}") + logger.info(f"Status: {status}, Stdout: {stdout}, Stderr: {stderr}") + + return status and stdout.strip() == "exists" + except Exception as e: + logger.exception(f"Error checking requirements for project {self.id}") + return False + finally: + if 'ssh_manager' in locals(): + ssh_manager.close() + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Proje" + verbose_name_plural = "Projeler" + ordering = ['-created_at'] + +class SSHLog(models.Model): + LOG_TYPES = ( + ('connection', 'Bağlantı Kontrolü'), + ('command', 'Komut Çalıştırma'), + ('folder', 'Klasör İşlemi'), + ('file', 'Dosya İşlemi'), + ('backup', 'Yedekleme'), # Yeni tip ekleyelim + ) + + ssh_credential = models.ForeignKey(SSHCredential, on_delete=models.CASCADE) + log_type = models.CharField(max_length=20, choices=LOG_TYPES) + command = models.TextField() + output = models.TextField() + status = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.ssh_credential.hostname} - {self.log_type} - {self.created_at}" + + class Meta: + ordering = ['-created_at'] \ No newline at end of file diff --git a/ssh_manager/settings.py b/ssh_manager/settings.py new file mode 100644 index 0000000..a214574 --- /dev/null +++ b/ssh_manager/settings.py @@ -0,0 +1,13 @@ +# Google Drive API Settings +GOOGLE_DRIVE_CREDENTIALS = { + "type": "service_account", + "project_id": "your-project-id", + "private_key_id": "your-private-key-id", + "private_key": "-----BEGIN PRIVATE KEY-----\nYour Private Key\n-----END PRIVATE KEY-----\n", + "client_email": "your-service-account@your-project.iam.gserviceaccount.com", + "client_id": "your-client-id", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-service-account" +} \ No newline at end of file diff --git a/ssh_manager/signals.py b/ssh_manager/signals.py new file mode 100644 index 0000000..5a32a90 --- /dev/null +++ b/ssh_manager/signals.py @@ -0,0 +1,13 @@ +from django.db.models.signals import post_migrate +from django.dispatch import receiver +from django.apps import apps +from .ssh_client import SSHManager # utils yerine ssh_client'dan import et + +@receiver(post_migrate) +def check_connection_on_startup(sender, **kwargs): + """ + Uygulama başlatıldığında sunucu bağlantısını kontrol et + """ + if sender.name == 'ssh_manager': + from .apps import check_server_connection + check_server_connection() \ No newline at end of file diff --git a/ssh_manager/ssh_client.py b/ssh_manager/ssh_client.py new file mode 100644 index 0000000..c6586b2 --- /dev/null +++ b/ssh_manager/ssh_client.py @@ -0,0 +1,607 @@ +import paramiko +import logging +import os +import tempfile +from django.utils import timezone +from django.template.loader import render_to_string + +logger = logging.getLogger(__name__) + +class SSHManager: + def __init__(self, ssh_credential): + self.ssh_credential = ssh_credential + self.client = None + self.connect() + logger.info(f'SSHManager başlatıldı: {ssh_credential.hostname}') + + def connect(self): + """SSH bağlantısı kur""" + try: + self.client = paramiko.SSHClient() + self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self.client.connect( + hostname=self.ssh_credential.hostname, + username=self.ssh_credential.username, + password=self.ssh_credential.password, + port=self.ssh_credential.port or 22, + look_for_keys=False, + allow_agent=False + ) + return True + except Exception as e: + logger.error(f'SSH bağlantı hatası: {str(e)}') + return False + + def close(self): + """SSH bağlantısını kapat""" + if self.client: + self.client.close() + self.client = None + + def check_connection(self): + """SSH bağlantısını kontrol et""" + try: + if not self.client: + return self.connect() + self.client.exec_command('echo "Connection test"') + return True + except: + return self.connect() + + def execute_command(self, command): + """ + SSH üzerinden komut çalıştır ve sonuçları döndür + """ + try: + if not self.client: + self.connect() + + stdin, stdout, stderr = self.client.exec_command(command) + exit_status = stdout.channel.recv_exit_status() + + # Binary veriyi oku + stdout_data = stdout.read() + stderr_data = stderr.read() + + # Farklı encoding'leri dene + encodings = ['utf-8', 'latin1', 'cp1252', 'iso-8859-9'] + + # stdout için encoding dene + stdout_str = None + for enc in encodings: + try: + stdout_str = stdout_data.decode(enc) + break + except UnicodeDecodeError: + continue + + # stderr için encoding dene + stderr_str = None + for enc in encodings: + try: + stderr_str = stderr_data.decode(enc) + break + except UnicodeDecodeError: + continue + + # Eğer hiçbir encoding çalışmazsa, latin1 kullan (her byte'ı decode edebilir) + if stdout_str is None: + stdout_str = stdout_data.decode('latin1') + if stderr_str is None: + stderr_str = stderr_data.decode('latin1') + + return stdout_str, stderr_str, exit_status == 0 + + except Exception as e: + logger.exception(f"Komut çalıştırma hatası: {command}") + return "", str(e), False + + def download_req_file(self, project): + """req.txt dosyasını oku ve geçici dosya olarak kaydet""" + try: + # Dosya içeriğini oku + cmd = f'cat "{project.get_full_path()}/req.txt"' + stdout, stderr, status = self.execute_command(cmd) + + if not status: + logger.error(f"req.txt okunamadı: {stderr}") + return None + + # Geçici dosya oluştur + temp = tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.txt') + temp.write(stdout) + temp.close() + + logger.info(f"req.txt temp file created: {temp.name}") + return temp.name + + except Exception as e: + logger.exception(f"Error in download_req_file: {str(e)}") + return None + + def delete_project(self, project): + """Proje klasörünü sil""" + try: + cmd = f'rm -rf "{project.get_full_path()}"' + stdout, stderr, status = self.execute_command(cmd) + + if status: + return True, "Proje başarıyla silindi" + else: + return False, f"Silme hatası: {stderr}" + + except Exception as e: + return False, str(e) + + def upload_zip(self, project, zip_file): + """ZIP dosyasını yükle ve aç""" + try: + # Geçici dizin oluştur + temp_dir = '/tmp/project_upload' + mkdir_out, mkdir_err, mkdir_status = self.execute_command(f'rm -rf {temp_dir} && mkdir -p {temp_dir}') + if not mkdir_status: + return False, f'Geçici dizin oluşturulamadı: {mkdir_err}' + + # SFTP bağlantısı + sftp = self.client.open_sftp() + + try: + # ZIP dosyasını yükle + remote_zip = f"{temp_dir}/{zip_file.name}" + sftp.putfo(zip_file, remote_zip) + + # ZIP dosyasını aç + unzip_cmd = f''' + cd {temp_dir} && \ + unzip -o "{zip_file.name}" && \ + rm "{zip_file.name}" && \ + mv * "{project.get_full_path()}/" 2>/dev/null || true && \ + cd / && \ + rm -rf {temp_dir} + ''' + + stdout, stderr, status = self.execute_command(unzip_cmd) + + if not status: + return False, f'ZIP açma hatası: {stderr}' + + return True, "Dosya başarıyla yüklendi" + + finally: + sftp.close() + + except Exception as e: + return False, str(e) + + def upload_txt(self, project, txt_file): + """TXT dosyasını yükle""" + try: + # SFTP bağlantısı + sftp = self.client.open_sftp() + + try: + # Dosya adını belirle + remote_path = f"{project.get_full_path()}/req.txt" + + # Dosyayı yükle + sftp.putfo(txt_file, remote_path) + + # İzinleri ayarla + self.execute_command(f'chmod 644 "{remote_path}"') + + return True, "Dosya başarıyla yüklendi" + + finally: + sftp.close() + + except Exception as e: + return False, str(e) + + def create_config_files(self, project): + """Tüm konfigürasyon dosyalarını oluşturur""" + try: + logger.info(f'"{project.folder_name}" projesi için konfigürasyon dosyaları oluşturuluyor') + + # Değişkenleri hazırla + context = { + 'project_name': project.folder_name, + 'project_path': project.get_full_path(), + 'domain_name': project.url + } + logger.info('Konfigürasyon şablonları için context hazırlandı') + + # Konfigürasyon içeriklerini hazırla + configs = { + 'nginx.conf': render_to_string('ssh_manager/nginx.conf.template', context), + 'supervisor.conf': render_to_string('ssh_manager/supervisor.conf.template', context), + 'wsgi_conf': render_to_string('ssh_manager/wsgi.conf.template', context) + } + logger.info('Konfigürasyon şablonları render edildi') + + # WSGI dosyasını proje dizininde oluştur + logger.info('WSGI dosyası oluşturuluyor') + wsgi_cmd = f'cat > "{project.get_full_path()}/wsgi_conf" << "EOF"\n{configs["wsgi_conf"]}\nEOF' + stdout, stderr, status = self.execute_command(wsgi_cmd) + + if not status: + logger.error(f'WSGI dosyası oluşturma hatası: {stderr}') + raise Exception(f'WSGI dosyası oluşturulamadı: {stderr}') + logger.info('WSGI dosyası başarıyla oluşturuldu') + + # WSGI için izinleri ayarla + logger.info('WSGI dosyası için çalıştırma izinleri ayarlanıyor') + chmod_cmd = f'chmod +x "{project.get_full_path()}/wsgi_conf"' + stdout, stderr, status = self.execute_command(chmod_cmd) + if not status: + logger.error(f'WSGI izin ayarlama hatası: {stderr}') + raise Exception(f'WSGI için izinler ayarlanamadı: {stderr}') + logger.info('WSGI dosyası için izinler başarıyla ayarlandı') + + # Nginx konfigürasyonunu direkt hedef konumunda oluştur + if project.url: + logger.info('Nginx konfigürasyonu ayarlanıyor') + nginx_target = f'/etc/nginx/sites-available/{project.url}' + nginx_enabled = f'/etc/nginx/sites-enabled/{project.url}' + + # Eski konfigürasyonları temizle + logger.info('Eski Nginx konfigürasyonları temizleniyor') + self.execute_command(f'sudo rm -f {nginx_target} {nginx_enabled}') + + # Yeni konfigürasyonu oluştur + logger.info(f'Nginx konfigürasyonu "{nginx_target}" konumunda oluşturuluyor') + nginx_cmd = f'sudo bash -c \'cat > "{nginx_target}" << "EOF"\n{configs["nginx.conf"]}\nEOF\'' + stdout, stderr, status = self.execute_command(nginx_cmd) + + if not status: + logger.error(f'Nginx konfigürasyon oluşturma hatası: {stderr}') + raise Exception(f'Nginx konfigürasyonu oluşturulamadı: {stderr}') + logger.info('Nginx konfigürasyonu başarıyla oluşturuldu') + + # İzinleri ayarla + logger.info('Nginx konfigürasyonu için izinler ayarlanıyor') + self.execute_command(f'sudo chown root:root "{nginx_target}"') + self.execute_command(f'sudo chmod 644 "{nginx_target}"') + logger.info('Nginx konfigürasyonu için izinler başarıyla ayarlandı') + + # Symbolic link oluştur + logger.info('Nginx symbolic link oluşturuluyor') + link_cmd = f'sudo ln -sf "{nginx_target}" "{nginx_enabled}"' + stdout, stderr, status = self.execute_command(link_cmd) + + if not status: + logger.error(f'Nginx symbolic link oluşturma hatası: {stderr}') + raise Exception(f'Nginx symbolic link oluşturulamadı: {stderr}') + logger.info('Nginx symbolic link başarıyla oluşturuldu') + + # Supervisor konfigürasyonunu direkt hedef konumunda oluştur + logger.info('Supervisor konfigürasyonu ayarlanıyor') + supervisor_target = f'/etc/supervisor/conf.d/{project.folder_name}.conf' + + # Eski konfigürasyonu temizle + logger.info('Eski Supervisor konfigürasyonu temizleniyor') + self.execute_command(f'sudo rm -f {supervisor_target}') + + # Yeni konfigürasyonu oluştur + logger.info(f'Supervisor konfigürasyonu "{supervisor_target}" konumunda oluşturuluyor') + supervisor_cmd = f'sudo bash -c \'cat > "{supervisor_target}" << "EOF"\n{configs["supervisor.conf"]}\nEOF\'' + stdout, stderr, status = self.execute_command(supervisor_cmd) + + if not status: + logger.error(f'Supervisor konfigürasyon oluşturma hatası: {stderr}') + raise Exception(f'Supervisor konfigürasyonu oluşturulamadı: {stderr}') + logger.info('Supervisor konfigürasyonu başarıyla oluşturuldu') + + # İzinleri ayarla + logger.info('Supervisor konfigürasyonu için izinler ayarlanıyor') + self.execute_command(f'sudo chown root:root "{supervisor_target}"') + self.execute_command(f'sudo chmod 644 "{supervisor_target}"') + logger.info('Supervisor konfigürasyonu için izinler başarıyla ayarlandı') + + # Servisleri yeniden yükle + logger.info('Servisler yeniden başlatılıyor') + self.execute_command('sudo systemctl reload nginx') + self.execute_command('sudo supervisorctl reread') + self.execute_command('sudo supervisorctl update') + logger.info('Servisler başarıyla yeniden başlatıldı') + + logger.info('Tüm konfigürasyon işlemleri başarıyla tamamlandı') + return True, 'Konfigürasyon dosyaları başarıyla oluşturuldu ve konumlandırıldı' + + except Exception as e: + logger.error('Konfigürasyon dosyaları oluşturma hatası') + logger.exception(e) + return False, str(e) + + def get_disk_usage(self): + """Sunucunun disk kullanım bilgilerini al""" + try: + # Ana disk bölümünün kullanım bilgilerini al (genellikle /) + cmd = "df -h / | tail -n 1 | awk '{print $2,$3,$4,$5}'" + stdout, stderr, status = self.execute_command(cmd) + + if status: + # Çıktıyı parçala: toplam, kullanılan, boş, yüzde + parts = stdout.strip().split() + if len(parts) == 4: + # Yüzde işaretini kaldır ve sayıya çevir + usage_percent = int(parts[3].replace('%', '')) + return { + 'total': parts[0], + 'used': parts[1], + 'available': parts[2], + 'usage_percent': usage_percent + } + return None + except Exception as e: + logger.exception("Disk kullanım bilgisi alınamadı") + return None + + def upload_project_zip(self, project, zip_file): + """Proje dosyalarını yükle (zip veya txt)""" + try: + logger.info(f'"{project.folder_name}" projesi için dosya yükleme başlatıldı') + + # Başlangıç disk kullanımını al + initial_disk = self.get_disk_usage() + if initial_disk: + logger.info(f'Başlangıç disk kullanımı - Toplam: {initial_disk["total"]}, Kullanılan: {initial_disk["used"]}, Boş: {initial_disk["available"]}') + + # Dosya uzantısını kontrol et + file_extension = os.path.splitext(zip_file.name)[1].lower() + + # Sadece zip ve txt dosyalarına izin ver + if file_extension not in ['.zip', '.txt']: + logger.warning(f'Geçersiz dosya uzantısı: {file_extension}') + return False, "Sadece .zip ve .txt dosyaları yüklenebilir." + + # Dosyayı yükle + logger.info('SFTP bağlantısı açılıyor') + sftp = self.client.open_sftp() + + try: + if file_extension == '.txt': + # TXT dosyası ise direkt req.txt olarak kaydet + remote_path = f"{project.get_full_path()}/req.txt" + logger.info('TXT dosyası req.txt olarak yükleniyor') + + # İlerleme için callback fonksiyonu + total_size = zip_file.size + uploaded_size = 0 + start_time = timezone.now() + last_update = start_time + + def progress_callback(sent_bytes, remaining_bytes): + nonlocal uploaded_size, start_time, last_update + uploaded_size = sent_bytes + current_time = timezone.now() + elapsed_time = (current_time - start_time).total_seconds() + + # Her 0.5 saniyede bir güncelle + if (current_time - last_update).total_seconds() >= 0.5: + if elapsed_time > 0: + speed = uploaded_size / elapsed_time # bytes/second + percent = (uploaded_size / total_size) * 100 + remaining_size = total_size - uploaded_size + eta = remaining_size / speed if speed > 0 else 0 + logger.info(f'Upload Progress: {percent:.1f}% - Speed: {speed/1024:.1f} KB/s - ETA: {eta:.1f}s') + last_update = current_time + + sftp.putfo(zip_file, remote_path, callback=progress_callback) + logger.info('TXT dosyası başarıyla yüklendi') + + # İzinleri ayarla + self.execute_command(f'chmod 644 "{remote_path}"') + + # Venv kontrolü yap + logger.info('Virtual environment kontrol ediliyor') + venv_exists = self.check_venv_exists(project) + + if not venv_exists: + # Venv oluştur + logger.info('Virtual environment oluşturuluyor') + venv_cmd = f'cd "{project.get_full_path()}" && python3 -m venv venv' + stdout, stderr, status = self.execute_command(venv_cmd) + if not status: + error_msg = f'Venv oluşturma hatası: {stderr}' + logger.error(error_msg) + return False, stderr + logger.info('Virtual environment başarıyla oluşturuldu') + else: + logger.info('Mevcut virtual environment kullanılacak') + + # Requirements'ları kur + logger.info('Requirements kuruluyor') + install_cmd = f''' + cd "{project.get_full_path()}" && \ + source venv/bin/activate && \ + pip install --upgrade pip 2>&1 && \ + pip install -r req.txt 2>&1 + ''' + stdout, stderr, status = self.execute_command(install_cmd) + + if not status: + # Pip çıktısını logla + error_msg = 'Requirements kurulum hatası' + logger.error(error_msg) + logger.error(f'Pip çıktısı:\n{stdout}') + if stderr: + logger.error(f'Pip hata çıktısı:\n{stderr}') + return False, stdout if stdout else stderr + + # Başarılı pip çıktısını da logla + logger.info('Requirements başarıyla kuruldu') + logger.info(f'Pip kurulum çıktısı:\n{stdout}') + return True, "Requirements dosyası yüklendi ve kuruldu" + + else: # ZIP dosyası + # Geçici dizin oluştur + temp_dir = f'/tmp/project_upload_{project.id}' + mkdir_cmd = f'rm -rf {temp_dir} && mkdir -p {temp_dir}' + stdout, stderr, status = self.execute_command(mkdir_cmd) + if not status: + return False, f'Geçici dizin oluşturulamadı: {stderr}' + + # ZIP dosyasını geçici dizine yükle + remote_zip = f"{temp_dir}/upload.zip" + logger.info(f'Zip dosyası "{remote_zip}" konumuna yükleniyor') + + # İlerleme için callback fonksiyonu + total_size = zip_file.size + uploaded_size = 0 + start_time = timezone.now() + last_update = start_time + + def progress_callback(sent_bytes, remaining_bytes): + nonlocal uploaded_size, start_time, last_update + uploaded_size = sent_bytes + current_time = timezone.now() + elapsed_time = (current_time - start_time).total_seconds() + + # Her 0.5 saniyede bir güncelle + if (current_time - last_update).total_seconds() >= 0.5: + if elapsed_time > 0: + speed = uploaded_size / elapsed_time # bytes/second + percent = (uploaded_size / total_size) * 100 + remaining_size = total_size - uploaded_size + eta = remaining_size / speed if speed > 0 else 0 + logger.info(f'Upload Progress: {percent:.1f}% - Speed: {speed/1024:.1f} KB/s - ETA: {eta:.1f}s') + last_update = current_time + + sftp.putfo(zip_file, remote_zip, callback=progress_callback) + logger.info('Zip dosyası başarıyla yüklendi') + + # Zip dosyasını çıkart + logger.info('Zip dosyası çıkartılıyor') + unzip_cmd = f''' + cd "{temp_dir}" && \ + unzip -o upload.zip && \ + rm upload.zip && \ + cp -rf * "{project.get_full_path()}/" && \ + cd / && \ + rm -rf "{temp_dir}" + ''' + stdout, stderr, status = self.execute_command(unzip_cmd) + + if not status: + error_msg = f'Zip çıkartma hatası: {stderr}' + logger.error(error_msg) + return False, stderr + + logger.info('Zip dosyası başarıyla çıkartıldı') + + # req.txt var mı kontrol et + logger.info('req.txt dosyası kontrol ediliyor') + check_req = f'test -f "{project.get_full_path()}/req.txt" && echo "exists"' + stdout, stderr, status = self.execute_command(check_req) + + if status and stdout.strip() == "exists": + logger.info('req.txt bulundu, venv kurulumu başlatılıyor') + + # Venv kontrolü yap + logger.info('Virtual environment kontrol ediliyor') + venv_exists = self.check_venv_exists(project) + + if not venv_exists: + # Venv oluştur + logger.info('Virtual environment oluşturuluyor') + venv_cmd = f'cd "{project.get_full_path()}" && python3 -m venv venv' + stdout, stderr, status = self.execute_command(venv_cmd) + if not status: + error_msg = f'Venv oluşturma hatası: {stderr}' + logger.error(error_msg) + return False, stderr + logger.info('Virtual environment başarıyla oluşturuldu') + else: + logger.info('Mevcut virtual environment kullanılacak') + + # Requirements'ları kur + logger.info('Requirements kuruluyor') + install_cmd = f''' + cd "{project.get_full_path()}" && \ + source venv/bin/activate && \ + pip install --upgrade pip 2>&1 && \ + pip install -r req.txt 2>&1 + ''' + stdout, stderr, status = self.execute_command(install_cmd) + + if not status: + # Pip çıktısını logla + error_msg = 'Requirements kurulum hatası' + logger.error(error_msg) + logger.error(f'Pip çıktısı:\n{stdout}') + if stderr: + logger.error(f'Pip hata çıktısı:\n{stderr}') + return False, stdout if stdout else stderr + + # Başarılı pip çıktısını da logla + logger.info('Requirements başarıyla kuruldu') + logger.info(f'Pip kurulum çıktısı:\n{stdout}') + return True, "Proje dosyaları yüklendi ve requirements kuruldu" + + return True, "Proje dosyaları başarıyla yüklendi" + + finally: + sftp.close() + logger.info('SFTP bağlantısı kapatıldı') + + # Son disk kullanımını al ve değişimi logla + final_disk = self.get_disk_usage() + if final_disk: + logger.info(f'Son disk kullanımı - Toplam: {final_disk["total"]}, Kullanılan: {final_disk["used"]}, Boş: {final_disk["available"]}') + + except Exception as e: + logger.exception("Dosya yükleme hatası") + return False, str(e) + + def check_venv_exists(self, project): + """Virtual environment'ın var olup olmadığını kontrol et""" + cmd = f'test -d "{project.get_full_path()}/venv" && echo "exists" || echo "not exists"' + stdout, stderr, status = self.execute_command(cmd) + return stdout.strip() == "exists" + + def setup_venv_and_install_requirements(self, project): + """Virtual environment oluştur ve requirements'ları kur""" + try: + logger.info(f'"{project.folder_name}" projesi için venv oluşturuluyor') + + # Venv klasörünün varlığını kontrol et + check_cmd = f'test -d "{project.get_full_path()}/venv" && echo "exists" || echo "not exists"' + stdout, stderr, status = self.execute_command(check_cmd) + + if stdout.strip() == "exists": + logger.info('Mevcut venv kullanılacak') + else: + # Venv oluştur + logger.info('Yeni venv oluşturuluyor') + venv_cmd = f'cd "{project.get_full_path()}" && python3 -m venv venv' + stdout, stderr, status = self.execute_command(venv_cmd) + + if not status: + logger.error(f'Venv oluşturma hatası: {stderr}') + return False, f'Venv oluşturulamadı: {stderr}' + + logger.info('Venv başarıyla oluşturuldu') + + # pip'i güncelle ve requirements'ları kur + logger.info('Requirements kuruluyor') + install_cmd = f''' + cd "{project.get_full_path()}" && \ + source venv/bin/activate && \ + pip install --upgrade pip && \ + pip install -r req.txt + ''' + stdout, stderr, status = self.execute_command(install_cmd) + + if not status: + logger.error(f'Requirements kurulum hatası: {stderr}') + return False, f'Requirements kurulamadı: {stderr}' + + logger.info('Requirements başarıyla kuruldu') + return True, "Venv oluşturuldu ve requirements kuruldu" + + except Exception as e: + logger.exception(f"Venv ve requirements kurulum hatası: {str(e)}") + return False, str(e) + + # SSHManager'ın tüm metodları buraya taşınacak... + # utils.py'daki SSHManager sınıfının tüm içeriğini buraya kopyalayın \ No newline at end of file diff --git a/ssh_manager/ssh_manager.py b/ssh_manager/ssh_manager.py new file mode 100644 index 0000000..69e4455 --- /dev/null +++ b/ssh_manager/ssh_manager.py @@ -0,0 +1,19 @@ +def read_requirements_file(self, binary=False): + """ + Requirements dosyasını oku + binary=True ise binary modda okur + """ + try: + command = f'cat "{self.project_path}/req.txt"' + stdin, stdout, stderr = self.client.exec_command(command) + + if binary: + # Binary modda oku + return stdout.read() + else: + # Text modda oku + return stdout.read().decode('utf-8', errors='replace') + + except Exception as e: + logger.exception("req.txt okunamadı") + return None \ No newline at end of file diff --git a/ssh_manager/templatetags/__init__.py b/ssh_manager/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ssh_manager/templatetags/ssh_manager_tags.py b/ssh_manager/templatetags/ssh_manager_tags.py new file mode 100644 index 0000000..3da359f --- /dev/null +++ b/ssh_manager/templatetags/ssh_manager_tags.py @@ -0,0 +1,9 @@ +from django import template +from ssh_manager.models import Project + +register = template.Library() + +@register.simple_tag +def get_projects(): + """Tüm projeleri döndür""" + return Project.objects.all().select_related('ssh_credential', 'customer') diff --git a/ssh_manager/urls.py b/ssh_manager/urls.py new file mode 100644 index 0000000..01a75e5 --- /dev/null +++ b/ssh_manager/urls.py @@ -0,0 +1,79 @@ +from django.urls import path +from . import views +from django.apps import apps +from django.core.management import call_command + +# İlk yüklemede SSH kontrolü yap +if apps.apps_ready: + try: + call_command('check_ssh') + except: + pass + +# app_name = 'ssh_manager' # namespace'i kaldır + +urlpatterns = [ + path('', views.dashboard, name='project_list'), # Ana sayfa dashboard olsun + path('dashboard/', views.dashboard, name='dashboard'), + path('projeler/', views.project_list, name='projeler'), + path('host-yonetimi/', views.host_yonetimi, name='host_yonetimi'), + path('yedeklemeler/', views.yedeklemeler, name='yedeklemeler'), + path('islem-gecmisi/', views.islem_gecmisi, name='islem_gecmisi'), + path('ayarlar/', views.ayarlar, name='ayarlar'), + path('musteriler/', views.musteriler, name='musteriler'), + path('musteri/create/', views.create_customer, name='create_customer'), + path('musteri//edit/', views.edit_customer, name='edit_customer'), + path('musteri//delete/', views.delete_customer, name='delete_customer'), + path('get-customer-details//', views.get_customer_details, name='get_customer_details'), + path('update-customer//', views.update_customer, name='update_customer'), + path('get_host//', views.get_host, name='get_host'), + path('update_host//', views.update_host, name='update_host'), + path('delete_host//', views.delete_host, name='delete_host'), + path('update-hosts-status/', views.update_hosts_status, name='update_hosts_status'), + path('project/create/', views.create_project, name='create_project'), + path('project//upload/', views.upload_project_zip, name='upload_project_zip'), + path('delete_project//', views.delete_project, name='delete_project'), + path('project//setup-venv/', views.setup_venv, name='setup_venv'), + path('project//check-requirements/', views.check_requirements, name='check_requirements'), + path('project//update-requirements/', views.update_requirements, name='update_requirements'), + path('project//delete-requirement-line/', views.delete_requirement_line, name='delete_requirement_line'), + path('project//download-req/', views.download_req_file, name='download_req_file'), + path('logs//', views.view_logs, name='credential_logs'), + path('logs/clear/', views.clear_logs, name='clear_logs'), + path('logs/all/', views.get_all_logs, name='get_all_logs'), + path('logs/test/', views.get_all_logs, name='get_all_logs_test'), # Test için + path('logs/', views.view_logs, name='system_logs'), + path('project//check-venv/', views.check_venv, name='check_venv'), + path('project//install-requirements/', views.install_requirements, name='install_requirements'), + path('project//check-folder/', views.check_folder_empty, name='check_folder_empty'), + path('project//list-files/', views.list_project_files, name='list_project_files'), + path('project/upload/', views.upload_project_files, name='upload_project_files'), + path('get-latest-logs/', views.get_latest_logs, name='get_latest_logs'), + path('project//restart-supervisor/', views.restart_supervisor, name='restart_supervisor'), + path('project//refresh/', views.refresh_project, name='refresh_project'), + # path('backup/', views.backup_projects, name='backup_projects'), + path('backup-project//', views.backup_project, name='backup_project'), + path('project//backup-logs/', views.project_backup_logs, name='project_backup_logs'), + path('project//clear-logs/', views.clear_project_logs, name='clear_project_logs'), + path('project//check-site/', views.check_site_status_view, name='check_site_status'), + path('project//meta-key/', views.get_project_meta_key, name='get_project_meta_key'), + path('check-all-sites/', views.check_all_sites_view, name='check_all_sites'), + path('get-project-details//', views.get_project_details, name='get_project_details'), + path('update-project//', views.update_project, name='update_project'), + + # Host yönetimi URL'leri + path('test-host-connection//', views.test_host_connection, name='test_host_connection'), + path('refresh-all-hosts/', views.refresh_all_hosts, name='refresh_all_hosts'), + path('create-host/', views.create_host, name='create_host'), + path('update-host//', views.update_host, name='update_host'), + path('get-host-details//', views.get_host_details, name='get_host_details'), + path('delete-host//', views.delete_host, name='delete_host'), + path('test-host-connection-form/', views.test_host_connection_form, name='test_host_connection_form'), + + # Yedekleme endpoints + path('start-backup/', views.start_backup, name='start_backup'), + path('backup-all-projects/', views.backup_all_projects, name='backup_all_projects'), + path('retry-backup/', views.retry_backup, name='retry_backup'), + + # path('upload-to-drive//', views.upload_to_drive, name='upload_to_drive'). +] diff --git a/ssh_manager/utils.py b/ssh_manager/utils.py new file mode 100644 index 0000000..649dec0 --- /dev/null +++ b/ssh_manager/utils.py @@ -0,0 +1,146 @@ +from .models import SSHLog, Project +from django.utils import timezone +import logging +import os +import tempfile +import requests +from bs4 import BeautifulSoup + +logger = logging.getLogger(__name__) + +# Yardımcı fonksiyonlar buraya gelebilir + +def check_site_status(project): + """ + Projenin web sitesinin aktif olup olmadığını kontrol eder + Aynı zamanda proje klasörünün disk kullanımını günceller + """ + from .ssh_client import SSHManager + + result_messages = [] + + # 1. Disk kullanımını güncelle + try: + if project.ssh_credential: + ssh_manager = SSHManager(project.ssh_credential) + + if ssh_manager.check_connection(): + # Proje klasörünün tam yolu + base_path = project.ssh_credential.base_path.rstrip('/') + folder_name = project.folder_name.strip('/') + full_path = f"{base_path}/{folder_name}" + + # Debug bilgisi ekle + result_messages.append(f"Kontrol edilen path: {full_path}") + + # Önce base path'in var olup olmadığını kontrol et + base_check_command = f"test -d '{base_path}' && echo 'BASE_EXISTS' || echo 'BASE_NOT_EXISTS'" + stdout_base, stderr_base, success_base = ssh_manager.execute_command(base_check_command) + + if success_base and stdout_base.strip() == 'BASE_EXISTS': + # Base path var, şimdi proje klasörünü kontrol et + + # Önce base path içindeki klasörleri listele + list_command = f"ls -la '{base_path}' | grep '^d'" + stdout_list, stderr_list, success_list = ssh_manager.execute_command(list_command) + + if success_list: + result_messages.append(f"Base path içindeki klasörler: {stdout_list.strip()[:200]}") + + # Proje klasörünü kontrol et + check_command = f"test -d '{full_path}' && echo 'EXISTS' || echo 'NOT_EXISTS'" + stdout_check, stderr_check, success_check = ssh_manager.execute_command(check_command) + + if success_check and stdout_check.strip() == 'EXISTS': + # Disk kullanımını al + command = f"du -sh '{full_path}' 2>/dev/null | cut -f1" + stdout, stderr, success = ssh_manager.execute_command(command) + + if success and stdout.strip(): + old_usage = project.disk_usage or "Bilinmiyor" + project.disk_usage = stdout.strip() + result_messages.append(f"Disk kullanımı güncellendi: {old_usage} → {project.disk_usage}") + else: + result_messages.append("Disk kullanımı komutu başarısız") + else: + result_messages.append(f"Proje klasörü bulunamadı: {full_path}") + else: + result_messages.append(f"Base path bulunamadı: {base_path}") + + ssh_manager.close() + else: + result_messages.append("SSH bağlantısı kurulamadı") + else: + result_messages.append("SSH bilgisi eksik") + except Exception as e: + result_messages.append(f"Disk kontrolü hatası: {str(e)}") + + # 2. Site durumunu kontrol et + if not project.url: + project.last_site_check = timezone.now() + project.save() + return False, "; ".join(result_messages + ["URL eksik"]) + + try: + # URL'yi düzenle + url = project.url + if not url.startswith(('http://', 'https://')): + url = f'http://{url}' + + # Site kontrolü + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + + response = requests.get(url, headers=headers, timeout=10) + + if response.status_code == 200: + # Site erişilebilir + project.is_site_active = True + project.last_site_check = timezone.now() + project.save() + result_messages.append(f"Site aktif (HTTP {response.status_code})") + return True, "; ".join(result_messages) + else: + # Site erişilemez + project.is_site_active = False + project.last_site_check = timezone.now() + project.save() + result_messages.append(f"Site erişilemez (HTTP {response.status_code})") + return False, "; ".join(result_messages) + + except requests.exceptions.Timeout: + project.is_site_active = False + project.last_site_check = timezone.now() + project.save() + result_messages.append("Site zaman aşımı") + return False, "; ".join(result_messages) + + except requests.exceptions.ConnectionError: + project.is_site_active = False + project.last_site_check = timezone.now() + project.save() + result_messages.append("Site bağlantı hatası") + return False, "; ".join(result_messages) + + except Exception as e: + project.is_site_active = False + project.last_site_check = timezone.now() + project.save() + result_messages.append(f"Site hatası: {str(e)}") + return False, "; ".join(result_messages) + +def check_all_sites(): + """Tüm projelerin site durumunu kontrol et""" + projects = Project.objects.filter(url__isnull=False).exclude(url='') + results = [] + + for project in projects: + status, message = check_site_status(project) + results.append({ + 'project': project, + 'status': status, + 'message': message + }) + + return results \ No newline at end of file diff --git a/ssh_manager/views.py b/ssh_manager/views.py new file mode 100644 index 0000000..a9426e7 --- /dev/null +++ b/ssh_manager/views.py @@ -0,0 +1,2494 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib import messages +from django.views.decorators.http import require_http_methods +from django.db import models +from django.db.models import Q + +from .backup import job +from .models import SSHCredential, Project, SSHLog, Customer +from .ssh_client import SSHManager +from django.http import JsonResponse, HttpResponse +from django.http import HttpResponseNotAllowed + +import logging +from django.core.files.storage import FileSystemStorage +import os +import json +from django.views.decorators.csrf import csrf_exempt # Ekleyin +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ValidationError +from google.oauth2 import service_account +from googleapiclient.discovery import build +from googleapiclient.http import MediaFileUpload +import zipfile +import shutil +from datetime import datetime +from django.conf import settings +import tempfile # Ekleyin +from io import BytesIO +from django.utils import timezone +from django.views.decorators.http import require_GET + +from django.views.decorators.csrf import ensure_csrf_cookie + +# Logger oluştur +logger = logging.getLogger(__name__) + +@require_GET +def get_host(request, host_id): + from .models import SSHCredential + try: + host = SSHCredential.objects.get(id=host_id) + return JsonResponse({ + 'success': True, + 'host': { + 'id': host.id, + 'hostname': host.hostname, + 'username': host.username, + 'password': '', # Güvenlik için boş bırak + 'port': host.port, + 'base_path': host.base_path, + 'created_at': host.created_at.strftime('%Y-%m-%d %H:%M:%S') if host.created_at else '', + 'is_online': host.is_online, + 'last_check': host.last_check.strftime('%Y-%m-%d %H:%M:%S') if host.last_check else '' + } + }) + except SSHCredential.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Host bulunamadı'}) + +@ensure_csrf_cookie +def dashboard(request): + """Dashboard sayfası - özet ve istatistikler""" + projects = Project.objects.all().select_related('customer', 'ssh_credential') + ssh_credentials = SSHCredential.objects.all() + customers = Customer.objects.filter(is_active=True) + recent_logs = SSHLog.objects.all().order_by('-created_at')[:10] + + # İstatistikler + active_sites_count = projects.filter(is_site_active=True).count() + online_hosts_count = ssh_credentials.filter(connection_status='connected').count() + recent_backups = projects.filter(last_backup__isnull=False).order_by('-last_backup')[:6] + + context = { + 'projects': projects, + 'ssh_credentials': ssh_credentials, + 'customers': customers, + 'recent_logs': recent_logs, + 'active_sites_count': active_sites_count, + 'online_hosts_count': online_hosts_count, + 'recent_backups': recent_backups, + } + return render(request, 'ssh_manager/dashboard.html', context) + +# Bağlantı kontrolü yapıldı mı kontrolü için global değişken +_connection_checked = False + +@ensure_csrf_cookie +def project_list(request): + """Projeler sayfası - detaylı proje listesi""" + projects = Project.objects.all().select_related('customer', 'ssh_credential') + ssh_credentials = SSHCredential.objects.all() + customers = Customer.objects.filter(is_active=True).order_by('name') + ssh_logs = SSHLog.objects.all() # Log kayıtlarını al + + context = { + 'projects': projects, + 'ssh_credentials': ssh_credentials, + 'customers': customers, + 'ssh_logs': ssh_logs, # Log kayıtlarını context'e ekle + } + return render(request, 'ssh_manager/projeler.html', context) + +def projeler(request): + """Projeler sayfası için wrapper - project_list'i çağırır""" + return project_list(request) + + + +def update_all_hosts_status(): + """Tüm hostların bağlantı durumunu ve disk kullanım bilgisini güncelle""" + print("update_all_hosts_status fonksiyonu çağrıldı") + ssh_credentials = SSHCredential.objects.all() + print(f"Toplam {ssh_credentials.count()} host bulundu") + + for credential in ssh_credentials: + print(f"Host kontrol ediliyor: {credential.hostname}") + ssh_manager = SSHManager(credential) + try: + # Bağlantı durumunu kontrol et + is_online = ssh_manager.check_connection() + print(f"{credential.hostname} bağlantı durumu: {is_online}") + credential.is_online = is_online + + # Disk kullanım bilgisini al (sadece online ise) + if is_online: + disk_usage_info = ssh_manager.get_disk_usage() + print(f"{credential.hostname} disk bilgisi: {disk_usage_info}") + if disk_usage_info: + # Dictionary'yi string'e çevir (görüntüleme için) + credential.disk_usage = f"{disk_usage_info['used']} / {disk_usage_info['total']} ({disk_usage_info['usage_percent']}%)" + print(f"{credential.hostname} disk kullanımı kaydedildi: {credential.disk_usage}") + else: + credential.disk_usage = None + print(f"{credential.hostname} disk bilgisi alınamadı") + else: + credential.disk_usage = None + print(f"{credential.hostname} offline, disk bilgisi null") + + credential.save() + print(f"{credential.hostname} veritabanına kaydedildi") + + except Exception as e: + # Hata durumunda offline olarak işaretle + credential.is_online = False + credential.disk_usage = None + credential.save() + print(f"Host {credential.hostname} güncelleme hatası: {e}") + finally: + ssh_manager.close() + + print("update_all_hosts_status tamamlandı") + +@require_http_methods(["GET", "POST"]) +def create_project(request): + if request.method == 'POST': + name = request.POST.get('name') + folder_name = request.POST.get('folder_name') + ssh_credential_id = request.POST.get('ssh_credential') + url = request.POST.get('url', '').strip() + ssh_manager = None + + try: + ssh_credential = SSHCredential.objects.get(id=ssh_credential_id) + + # SSH bağlantısı kur ve kontrol et + ssh_manager = SSHManager(ssh_credential) + connection_status = ssh_manager.check_connection() + + if not connection_status: + return JsonResponse({ + 'success': False, + 'message': 'SSH bağlantısı kurulamadı! Lütfen bağlantı bilgilerini kontrol edin.' + }) + + # Proje klasör adının benzersiz olup olmadığını kontrol et + if Project.objects.filter(folder_name=folder_name, ssh_credential=ssh_credential).exists(): + return JsonResponse({ + 'success': False, + 'message': f'"{folder_name}" klasör adı bu sunucuda zaten kullanılıyor!' + }) + + # Klasörün var olup olmadığını kontrol et + check_cmd = f'test -d "{ssh_credential.base_path}/{folder_name}" && echo "exists" || echo "not exists"' + stdout, stderr, status = ssh_manager.execute_command(check_cmd) + + if stdout.strip() == "exists": + return JsonResponse({ + 'success': False, + 'message': f'"{folder_name}" klasörü sunucuda zaten mevcut!' + }) + + # Proje oluştur + customer_id = request.POST.get('customer') + customer = None + if customer_id: + try: + customer = Customer.objects.get(id=customer_id) + except Customer.DoesNotExist: + pass + + project = Project( + name=name, + folder_name=folder_name, + ssh_credential=ssh_credential, + customer=customer, + url=url if url else None + ) + + try: + project.full_clean() + except ValidationError as e: + return JsonResponse({ + 'success': False, + 'message': f'Validasyon hatası: {e}' + }) + + # Önce projeyi kaydet + project.save() + + # Klasör oluştur ve izinleri ayarla + commands = [ + f'mkdir -p "{project.get_full_path()}"', + f'chown -R www-data:www-data "{project.get_full_path()}"', + f'chmod -R 755 "{project.get_full_path()}"', + ] + + for cmd in commands: + stdout, stderr, status = ssh_manager.execute_command(cmd) + if not status: + # Hata durumunda projeyi sil + ssh_manager.execute_command(f'rm -rf "{project.get_full_path()}"') + project.delete() + return JsonResponse({ + 'success': False, + 'message': f'Klasör işlemleri sırasında hata: {stderr}' + }) + + + + # Log oluştur + SSHLog.objects.create( + ssh_credential=ssh_credential, + log_type='project', + command=f'Proje oluşturuldu: {name}', + output=f'Klasör: {project.get_full_path()}', + status=True + ) + + return JsonResponse({ + 'success': True, + 'message': f'Proje başarıyla oluşturuldu: {project.get_full_path()}' + }) + + except SSHCredential.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': 'Geçersiz SSH bağlantısı!' + }) + + except Exception as e: + logger.exception("Proje oluşturma hatası") + return JsonResponse({ + 'success': False, + 'message': f'Beklenmeyen bir hata oluştu: {str(e)}' + }) + + finally: + if ssh_manager: + ssh_manager.close() + + # GET isteği için SSH credentials listesini JSON olarak dön + ssh_credentials = SSHCredential.objects.all() + return JsonResponse({ + 'ssh_credentials': list(ssh_credentials.values('id', 'hostname', 'username', 'base_path')) + }) + +def view_logs(request, ssh_credential_id=None): + if ssh_credential_id: + # Belirli bir SSH bağlantısının loglarını göster + credential = SSHCredential.objects.get(id=ssh_credential_id) + logs = SSHLog.objects.filter(ssh_credential=credential) + context = {'credential': credential, 'logs': logs} + else: + # Tüm logları göster + logs = SSHLog.objects.all() + context = {'logs': logs} + + return render(request, 'ssh_manager/view_logs.html', context) + +@require_http_methods(["POST"]) +def check_connection(request, project_id): + try: + project = Project.objects.get(id=project_id) + ssh_manager = SSHManager(project.ssh_credential) + + if ssh_manager.check_connection(): + messages.success(request, 'Bağlantı başarılı') + else: + messages.error(request, 'Bağlantı başarısız') + + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return render(request, 'ssh_manager/messages.html') + return redirect('project_list') + + except Exception as e: + messages.error(request, str(e)) + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return render(request, 'ssh_manager/messages.html') + return redirect('project_list') + +def check_folder_permissions(request): + # Klasör işlemleriyle ilgili logları getir + folder_logs = SSHLog.objects.filter( + log_type='folder' + ).order_by('created_at') + + # Bağlantı loglarını getir + connection_logs = SSHLog.objects.filter( + log_type='connection' + ).order_by('created_at') + + context = { + 'folder_logs': folder_logs, + 'connection_logs': connection_logs + } + + return render(request, 'ssh_manager/check_permissions.html', context) + +@require_http_methods(["POST"]) +def upload_project_zip(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + + if 'zip_file' not in request.FILES: + return JsonResponse({ + 'success': False, + 'message': 'Dosya seçilmedi' + }) + + zip_file = request.FILES['zip_file'] + + # Dosya uzantısı kontrolü + allowed_extensions = ['.zip', '.txt'] + file_extension = os.path.splitext(zip_file.name)[1].lower() + + if file_extension not in allowed_extensions: + return JsonResponse({ + 'success': False, + 'message': 'Geçersiz dosya formatı. Sadece .zip ve .txt dosyaları yüklenebilir.' + }) + + # SSH bağlantısı + ssh_manager = project.ssh_credential.get_manager() + + try: + # Bağlantı kontrolü + if not ssh_manager.check_connection(): + return JsonResponse({ + 'success': False, + 'message': 'SSH bağlantısı kurulamadı' + }) + + # Dosyayı geçici olarak kaydet + fs = FileSystemStorage(location='temp_uploads') + filename = fs.save(zip_file.name, zip_file) + file_path = fs.path(filename) + + try: + # SFTP ile dosyayı yükle + sftp = ssh_manager.client.open_sftp() + remote_path = f"{project.get_full_path()}/{zip_file.name}" + sftp.put(file_path, remote_path) + sftp.close() + + # Zip dosyasını aç + if file_extension == '.zip': + unzip_cmd = f'cd {project.get_full_path()} && unzip -o "{zip_file.name}" && rm "{zip_file.name}"' + stdout, stderr, status = ssh_manager.execute_command(unzip_cmd) + + if not status: + return JsonResponse({ + 'success': False, + 'message': f'Zip dosyası açılamadı: {stderr}' + }) + + # Log oluştur + SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='upload', + command='Dosya Yükleme', + output=f'Dosya başarıyla yüklendi: {zip_file.name}', + status=True + ) + + return JsonResponse({ + 'success': True, + 'message': 'Dosya başarıyla yüklendi' + }) + + finally: + # Geçici dosyayı sil + if os.path.exists(file_path): + os.remove(file_path) + + finally: + ssh_manager.close() + + except Exception as e: + logger.exception("Dosya yükleme hatası") + return JsonResponse({ + 'success': False, + 'message': f'Beklenmeyen bir hata oluştu: {str(e)}' + }) + +@require_http_methods(["POST"]) +@csrf_exempt +def delete_project(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + try: + # Önce proje klasörünün varlığını kontrol et + check_cmd = f'test -d "{project.get_full_path()}" && echo "exists" || echo "not exists"' + stdout, stderr, status = ssh_manager.execute_command(check_cmd) + + if stdout.strip() != "exists": + project.delete() + return JsonResponse({'success': True, 'message': 'Proje veritabanından silindi (klasör zaten silinmiş)'}) + + # Klasör varsa silme işlemini başlat + delete_cmd = f'rm -rf "{project.get_full_path()}"' + stdout, stderr, status = ssh_manager.execute_command(delete_cmd) + + if status: + project.delete() + return JsonResponse({'success': True, 'message': 'Proje başarıyla silindi'}) + else: + return JsonResponse({'success': False, 'message': f'Proje silinirken hata oluştu: {stderr}'}) + + except Exception as e: + return JsonResponse({'success': False, 'message': f'Silme işlemi sırasında hata: {str(e)}'}) + finally: + ssh_manager.close() + + except Project.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Proje bulunamadı'}) + +@require_http_methods(["POST"]) +def setup_venv(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + logger.info(f"Setting up virtual environment for project: {project.name}") + + # Virtual environment kurulumu + cmd = f''' + cd {project.get_full_path()} && \ + python3 -m venv venv && \ + sudo chown -R www-data:www-data venv && \ + sudo chmod -R 755 venv + ''' + + stdout, stderr, status = ssh_manager.execute_command(cmd) + + if status: + SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='command', + command="Virtual Environment Kurulumu", + output=f"Başarılı: {stdout}", + status=True + ) + return JsonResponse({ + 'status': 'success', + 'message': 'Virtual environment başarıyla oluşturuldu' + }) + else: + error_msg = f'Virtual environment oluşturulamadı: {stderr}' + logger.error(error_msg) + SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='command', + command="Virtual Environment Kurulumu", + output=f"Hata: {stderr}", + status=False + ) + return JsonResponse({ + 'status': 'error', + 'message': error_msg + }, status=500) + + except Exception as e: + logger.exception("Error in setup_venv") + return JsonResponse({ + 'status': 'error', + 'message': str(e) + }, status=500) + finally: + if 'ssh_manager' in locals(): + ssh_manager.close() + +@require_http_methods(["GET"]) +def download_requirements(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + # req.txt dosyasını kontrol et ve içeriğini al + check_cmd = f'test -f "{project.get_full_path()}/req.txt" && cat "{project.get_full_path()}/req.txt"' + stdout, stderr, status = ssh_manager.execute_command(check_cmd) + + if status: + response = HttpResponse(stdout, content_type='text/plain') + response['Content-Disposition'] = f'attachment; filename="{project.folder_name}_req.txt"' + return response + else: + return HttpResponse('req.txt dosyası bulunamadı.', status=404) + except Exception as e: + return HttpResponse(f'Hata: {str(e)}', status=500) + finally: + if 'ssh_manager' in locals(): + ssh_manager.close() + +@require_http_methods(["GET"]) +def check_requirements(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + # Tam yolu logla + full_path = project.get_full_path() + logger.info(f"Proje bilgileri:") + logger.info(f"Proje adı: {project.name}") + logger.info(f"Klasör adı: {project.folder_name}") + logger.info(f"Tam yol: {full_path}") + + # Önce dosyanın varlığını kontrol et + check_cmd = f'test -f "{full_path}/req.txt" && echo "exists"' + check_stdout, check_stderr, check_status = ssh_manager.execute_command(check_cmd) + logger.info(f"Dosya kontrol komutu: {check_cmd}") + logger.info(f"Kontrol çıktısı: {check_stdout}") + + if check_status and check_stdout.strip() == "exists": + # Dosya varsa içeriğini oku + cat_cmd = f'cat "{full_path}/req.txt"' + stdout, stderr, status = ssh_manager.execute_command(cat_cmd) + logger.info(f"Okuma komutu: {cat_cmd}") + logger.info(f"Okuma çıktısı: {stdout}") + logger.info(f"Okuma hatası: {stderr}") + + if status and stdout.strip(): + return JsonResponse({ + 'success': True, + 'content': stdout + }) + else: + error_msg = 'req.txt dosyası boş' if status else f'req.txt dosyası okunamadı: {stderr}' + logger.error(error_msg) + return JsonResponse({ + 'success': False, + 'message': error_msg + }) + else: + error_msg = 'req.txt dosyası bulunamadı' + logger.error(error_msg) + return JsonResponse({ + 'success': False, + 'message': error_msg + }) + + except Exception as e: + logger.exception("req.txt kontrol hatası") + return JsonResponse({ + 'success': False, + 'message': f'Hata: {str(e)}' + }) + finally: + if 'ssh_manager' in locals(): + ssh_manager.close() + +@require_http_methods(["GET"]) +def download_req_file(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + try: + # Dosya içeriğini doğrudan oku + command = f'cat "{project.get_full_path()}/req.txt"' + stdout, stderr, status = ssh_manager.execute_command(command) + + if not status: + return JsonResponse({ + 'success': False, + 'message': 'req.txt dosyası bulunamadı' + }) + + # Dosya içeriği varsa, indirme yanıtı oluştur + response = HttpResponse(stdout, content_type='text/plain') + response['Content-Disposition'] = f'attachment; filename="{project.folder_name}_req.txt"' + + # Log oluştur + SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='download', + command='Requirements İndirme', + output=f'req.txt dosyası indirildi: {project.folder_name}', + status=True + ) + + return response + + finally: + ssh_manager.close() + + except Exception as e: + logger.exception("Requirements indirme hatası") + return JsonResponse({ + 'success': False, + 'message': f'Dosya indirme hatası: {str(e)}' + }) + +def create_system_log(message, status=True, log_type='system', ssh_credential=None, command=None, output=None): + """Sistem logu oluştur ve JSON olarak dön""" + log = SSHLog.objects.create( + ssh_credential=ssh_credential, + log_type=log_type, + command=command or 'Sistem', + output=output or message, + status=status + ) + return { + 'id': log.id, + 'timestamp': log.created_at.strftime('%H:%M:%S'), + 'message': message, + 'command': command, + 'output': output, + 'status': status + } + +@require_http_methods(["POST"]) +def clear_logs(request): + try: + # Tüm logları sil + SSHLog.objects.all().delete() + + # Log oluştur + SSHLog.objects.create( + ssh_credential=SSHCredential.objects.first(), + log_type='command', + command='Clear Logs', + output='Tüm log kayıtları temizlendi', + status=True + ) + + return JsonResponse({ + 'success': True, + 'message': 'Loglar başarıyla temizlendi' + }) + except Exception as e: + logger.exception("Log temizleme hatası") + return JsonResponse({ + 'success': False, + 'message': f'Loglar temizlenirken hata oluştu: {str(e)}' + }, status=500) + +@require_http_methods(["GET"]) +def check_venv(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + # venv klasörünün varlığını kontrol et + check_cmd = f'test -d "{project.get_full_path()}/venv" && echo "exists" || echo "not exists"' + stdout, stderr, status = ssh_manager.execute_command(check_cmd) + + exists = stdout.strip() == "exists" + + log_data = create_system_log( + message=f'Virtual environment kontrolü: {"Mevcut" if exists else "Mevcut değil"}', + status=True, + ssh_credential=project.ssh_credential, + command='Venv Kontrolü', + output=f'Proje: {project.name}' + ) + + return JsonResponse({ + 'success': True, + 'exists': exists, + 'log': log_data + }) + + except Exception as e: + logger.exception("Venv kontrol hatası") + log_data = create_system_log( + message=f'Virtual environment kontrol hatası: {str(e)}', + status=False, + ssh_credential=project.ssh_credential, + command='Venv Kontrolü' + ) + return JsonResponse({ + 'success': False, + 'exists': False, + 'log': log_data + }) + finally: + if 'ssh_manager' in locals(): + ssh_manager.close() + +@require_http_methods(["POST"]) +def install_requirements(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + # Venv'i aktive et ve pip install çalıştır + cmd = f''' + cd {project.get_full_path()} && \ + source venv/bin/activate && \ + if [ -f "req.txt" ]; then + pip install -r req.txt + else + echo "req.txt dosyası bulunamadı" + exit 1 + fi + ''' + + stdout, stderr, status = ssh_manager.execute_command(cmd) + + if status: + log_data = create_system_log( + message='Paketler başarıyla kuruldu', + status=True, + ssh_credential=project.ssh_credential, + command='Pip Install', + output=stdout + ) + else: + log_data = create_system_log( + message='Paket kurulumu başarısız oldu', + status=False, + ssh_credential=project.ssh_credential, + command='Pip Install', + output=stderr + ) + + return JsonResponse({ + 'success': status, + 'log': log_data + }) + + except Exception as e: + logger.exception("Paket kurulum hatası") + log_data = create_system_log( + message=f'Paket kurulum hatası: {str(e)}', + status=False, + ssh_credential=project.ssh_credential, + command='Pip Install' + ) + return JsonResponse({ + 'success': False, + 'log': log_data + }) + finally: + if 'ssh_manager' in locals(): + ssh_manager.close() + +@require_http_methods(["GET"]) +def check_folder_empty(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + # Klasördeki dosya ve klasörleri listele + cmd = f'ls -A "{project.get_full_path()}"' + stdout, stderr, status = ssh_manager.execute_command(cmd) + + is_empty = not stdout.strip() # Boş string ise klasör boştur + + if not is_empty: + files = stdout.strip().split('\n') + log_data = create_system_log( + message=f'Klasör içeriği kontrol edildi: {len(files)} öğe mevcut', + status=True, + ssh_credential=project.ssh_credential, + command='Klasör Kontrolü', + output=f'Mevcut dosyalar: {", ".join(files)}' + ) + else: + log_data = create_system_log( + message='Klasör boş', + status=True, + ssh_credential=project.ssh_credential, + command='Klasör Kontrolü' + ) + + return JsonResponse({ + 'success': True, + 'is_empty': is_empty, + 'files': stdout.strip().split('\n') if not is_empty else [], + 'log': log_data + }) + + except Exception as e: + logger.exception("Klasör kontrol hatası") + log_data = create_system_log( + message=f'Klasör kontrol hatası: {str(e)}', + status=False, + ssh_credential=project.ssh_credential, + command='Klasör Kontrolü' + ) + return JsonResponse({ + 'success': False, + 'log': log_data + }) + finally: + if 'ssh_manager' in locals(): + ssh_manager.close() + +@login_required +def list_project_files(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = SSHManager(project.ssh_credential) + + if not ssh_manager.check_connection(): + return JsonResponse({ + 'success': False, + 'message': 'SSH bağlantısı kurulamadı!' + }) + + # Proje klasöründeki dosyaları listele + cmd = f'ls -la "{project.get_full_path()}"' + stdout, stderr, status = ssh_manager.execute_command(cmd) + + if not status: + return JsonResponse({ + 'success': False, + 'message': f'Dosya listesi alınamadı: {stderr}' + }) + + # ls -la çıktısını parse et + files = [] + for line in stdout.splitlines()[1:]: # İlk satırı atla (toplam) + if line.strip(): + parts = line.split() + if len(parts) >= 9: + permissions = parts[0] + size = parts[4] + date = ' '.join(parts[5:8]) + name = ' '.join(parts[8:]) + + files.append({ + 'name': name, + 'permissions': permissions, + 'size': size, + 'date': date + }) + + return JsonResponse({ + 'success': True, + 'files': files + }) + + except Project.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': 'Proje bulunamadı!' + }) + except Exception as e: + logger.exception("Dosya listeleme hatası") + return JsonResponse({ + 'success': False, + 'message': f'Beklenmeyen bir hata oluştu: {str(e)}' + }) + finally: + if 'ssh_manager' in locals(): + ssh_manager.close() + +@login_required +def upload_project_files(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = SSHManager(project.ssh_credential) + + try: + success, ssh_message = ssh_manager.upload_project_zip(project, request.FILES['files']) + + if success: + # Başarılı durumda log oluştur + SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='upload', + command='Dosya Yükleme', + output=ssh_message, # SSH'den dönen detaylı başarı mesajı + status=True + ) + # Kullanıcıya genel başarı mesajı + return JsonResponse({ + 'success': True, + 'message': 'Dosyalar başarıyla yüklendi' + }) + else: + # Hata durumunda detaylı log oluştur + logger.error(ssh_message) # SSH hatasını direkt logla + SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='upload', + command='Dosya Yükleme', + output=ssh_message, # SSH'den dönen detaylı hata mesajı + status=False + ) + # Kullanıcıya genel hata mesajı + return JsonResponse({ + 'success': False, + 'message': 'Dosya yükleme işlemi başarısız oldu' + }) + + finally: + ssh_manager.close() + + except Project.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': 'Proje bulunamadı' + }) + + except Exception as e: + # Beklenmeyen hataları detaylı şekilde logla + logger.exception('Beklenmeyen bir hata oluştu') + SSHLog.objects.create( + ssh_credential=project.ssh_credential if 'project' in locals() else None, + log_type='upload', + command='Dosya Yükleme', + output=str(e), # Ham hata mesajı + status=False + ) + # Kullanıcıya genel hata mesajı + return JsonResponse({ + 'success': False, + 'message': 'Beklenmeyen bir hata oluştu' + }) + +@require_http_methods(["POST"]) +def get_latest_logs(request): + logs = SSHLog.objects.all().order_by('-created_at')[:50] # Son 50 log + log_data = [{ + 'created_at': log.created_at.strftime('%d.%m.%Y %H:%M:%S'), + 'log_type_display': log.get_log_type_display(), + 'command': log.command, + 'output': log.output, + 'status': log.status + } for log in logs] + + return JsonResponse({'logs': log_data}) + +@require_http_methods(["POST"]) +def restart_supervisor(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + try: + # Supervisor'ı yeniden başlat + cmd = f'sudo supervisorctl restart {project.folder_name}' + stdout, stderr, status = ssh_manager.execute_command(cmd) + + if status: + log_data = create_system_log( + message='Supervisor başarıyla yeniden başlatıldı', + status=True, + ssh_credential=project.ssh_credential, + command='Supervisor Restart', + output=stdout + ) + return JsonResponse({ + 'success': True, + 'message': 'Uygulama başarıyla yeniden başlatıldı', + 'log': log_data + }) + else: + log_data = create_system_log( + message='Supervisor yeniden başlatılamadı', + status=False, + ssh_credential=project.ssh_credential, + command='Supervisor Restart', + output=stderr + ) + return JsonResponse({ + 'success': False, + 'message': 'Uygulama yeniden başlatılamadı', + 'log': log_data + }) + + except Exception as e: + logger.exception("Supervisor restart hatası") + log_data = create_system_log( + message=f'Supervisor restart hatası: {str(e)}', + status=False, + ssh_credential=project.ssh_credential, + command='Supervisor Restart' + ) + return JsonResponse({ + 'success': False, + 'message': str(e), + 'log': log_data + }) + finally: + ssh_manager.close() + + except Project.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': 'Proje bulunamadı' + }) + +@require_http_methods(["POST"]) +def update_requirements(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + # JSON verilerini al + data = json.loads(request.body) + new_content = data.get('content', '') + + # Yeni içeriği req.txt dosyasına yaz + write_cmd = f'echo "{new_content}" > "{project.get_full_path()}/req.txt"' + stdout, stderr, status = ssh_manager.execute_command(write_cmd) + + if status: + log_data = create_system_log( + message='Requirements içeriği güncellendi', + status=True, + ssh_credential=project.ssh_credential, + command='Requirements Güncelleme', + output=stdout + ) + return JsonResponse({ + 'success': True, + 'message': 'Requirements içeriği başarıyla güncellendi', + 'log': log_data + }) + else: + log_data = create_system_log( + message='Requirements güncellenirken hata oluştu', + status=False, + ssh_credential=project.ssh_credential, + command='Requirements Güncelleme', + output=stderr + ) + return JsonResponse({ + 'success': False, + 'message': f'Requirements güncellenirken hata oluştu: {stderr}', + 'log': log_data + }) + + except Exception as e: + logger.exception("Requirements güncelleme hatası") + return JsonResponse({ + 'success': False, + 'message': f'Hata: {str(e)}' + }) + finally: + if 'ssh_manager' in locals(): + ssh_manager.close() + +@require_http_methods(["POST"]) +def delete_requirement_line(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + # JSON verilerini al + data = json.loads(request.body) + line_index = int(data.get('line_index', -1)) # int'e çevir + + # Mevcut içeriği oku + read_cmd = f'cat "{project.get_full_path()}/req.txt"' + stdout, stderr, status = ssh_manager.execute_command(read_cmd) + + if not status: + return JsonResponse({ + 'success': False, + 'message': 'Requirements dosyası okunamadı' + }) + + # Satırları ayır ve belirtilen satırı sil + lines = [line for line in stdout.strip().split('\n') if line.strip()] # Boş satırları filtrele + + if 0 <= line_index < len(lines): + deleted_line = lines.pop(line_index) + new_content = '\n'.join(lines) + + # Yeni içeriği dosyaya yaz + write_cmd = f"echo '{new_content}' > '{project.get_full_path()}/req.txt'" + stdout, stderr, status = ssh_manager.execute_command(write_cmd) + + if status: + log_data = create_system_log( + message=f'Requirements satırı silindi: {deleted_line}', + status=True, + ssh_credential=project.ssh_credential, + command='Requirements Satır Silme', + output=stdout + ) + return JsonResponse({ + 'success': True, + 'message': 'Satır başarıyla silindi', + 'content': new_content, + 'log': log_data + }) + + return JsonResponse({ + 'success': False, + 'message': 'Satır silinemedi' + }) + + except Exception as e: + logger.exception("Requirements satır silme hatası") + return JsonResponse({ + 'success': False, + 'message': f'Hata: {str(e)}' + }) + finally: + if 'ssh_manager' in locals(): + ssh_manager.close() + +@require_http_methods(["POST"]) +def refresh_project(request, project_id): + ssh_manager = None + try: + project = get_object_or_404(Project, id=project_id) + ssh_manager = project.ssh_credential.get_manager() + + # Proje klasörünü kontrol et + project_path = project.get_full_path() + + # Disk kullanımını hesapla (du komutu ile) + du_cmd = f"du -sh {project_path}" + stdout, stderr, status = ssh_manager.execute_command(du_cmd) + + if status: + # du çıktısını parse et (örn: "156M /path/to/folder") + disk_usage = stdout.split()[0] + project.disk_usage = disk_usage + project.save() + + # Requirements dosyasını kontrol et (req.txt) + stdout, stderr, status = ssh_manager.execute_command(f"ls {project_path}/req.txt") + has_requirements = status + + # Requirements içeriğini al + if has_requirements: + stdout, stderr, status = ssh_manager.execute_command(f"cat {project_path}/req.txt") + req_content = stdout if status else "" + else: + req_content = "" + + # Log oluştur + log_data = create_system_log( + message='Proje bilgileri güncellendi', + status=True, + ssh_credential=project.ssh_credential, + command='Proje Güncelleme', + output=f"Requirements durumu: {'Mevcut' if has_requirements else 'Mevcut değil'}" + ) + + return JsonResponse({ + 'success': True, + 'project_info': { + 'base_path': project.ssh_credential.base_path, + 'folder_name': project.folder_name, + 'disk_usage': project.disk_usage or '0B' + }, + 'has_requirements': has_requirements, + 'requirements_content': req_content, + 'message': 'Proje başarıyla güncellendi', + 'log': log_data + }) + + except Project.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': 'Proje bulunamadı' + }, status=404) + except Exception as e: + logger.exception("Proje güncelleme hatası") + log_data = create_system_log( + message=f'Proje güncellenirken hata oluştu: {str(e)}', + status=False, + ssh_credential=project.ssh_credential if 'project' in locals() else None, + command='Proje Güncelleme' + ) + return JsonResponse({ + 'success': False, + 'message': str(e), + 'log': log_data + }, status=500) + finally: + if ssh_manager: + ssh_manager.close() + +# @require_http_methods(["POST"]) +# def backup_projects(request): +# logger.info("====== BACKUP İŞLEMİ BAŞLIYOR ======") +# +# try: +# ssh_credential = SSHCredential.objects.first() +# if not ssh_credential: +# raise ValueError("SSH bağlantısı bulunamadı") +# +# # Başlangıç logu +# SSHLog.objects.create( +# ssh_credential=ssh_credential, +# log_type='backup', +# command='Backup İşlemi', +# output='Yedekleme işlemi başlatıldı', +# status=True +# ) +# +# # Google Drive bağlantı logu +# SSHLog.objects.create( +# ssh_credential=ssh_credential, +# log_type='backup', +# command='Google Drive Bağlantısı', +# output='Google Drive API bağlantısı kuruluyor...', +# status=True +# ) +# +# credentials = service_account.Credentials.from_service_account_info( +# settings.GOOGLE_DRIVE_CREDENTIALS, +# scopes=['https://www.googleapis.com/auth/drive.file'] +# ) +# drive_service = build('drive', 'v3', credentials=credentials) +# +# SSHLog.objects.create( +# ssh_credential=ssh_credential, +# log_type='backup', +# command='Google Drive Bağlantısı', +# output='Google Drive API bağlantısı başarıyla kuruldu', +# status=True +# ) +# +# ssh_manager = ssh_credential.get_manager() +# +# try: +# for project in Project.objects.all(): +# # Proje yedekleme başlangıç logu +# SSHLog.objects.create( +# ssh_credential=ssh_credential, +# log_type='backup', +# command=f'Proje: {project.name}', +# output=f'{project.name} projesi yedekleme işlemi başladı', +# status=True +# ) +# +# backup_name = f'{project.folder_name}_{datetime.now().strftime("%Y%m%d_%H%M%S")}.zip' +# project_path = project.get_full_path() +# +# # Zip işlemi +# zip_cmd = f''' +# cd {project_path}/.. && \ +# zip -9 -r "{project.folder_name}.zip" "{project.folder_name}" && \ +# mv "{project.folder_name}.zip" "/tmp/{backup_name}" +# ''' +# logger.info(f"Zip komutu çalıştırılıyor: {zip_cmd}") +# +# stdout, stderr, status = ssh_manager.execute_command(zip_cmd) +# +# if not status: +# error_msg = f"{project.name} için zip oluşturma hatası:\nStdout: {stdout}\nStderr: {stderr}" +# SSHLog.objects.create( +# ssh_credential=ssh_credential, +# log_type='backup', +# command=f'Zip Hatası: {project.name}', +# output=error_msg, +# status=False +# ) +# continue +# +# # Zip dosyası boyutunu ve başarı durumunu logla +# SSHLog.objects.create( +# ssh_credential=ssh_credential, +# log_type='backup', +# command=f'Zip: {project.name}', +# output=f'{project.name} projesi için zip dosyası oluşturuldu\n{stdout}', +# status=True +# ) +# +# # Dosya tipini kontrol et +# check_cmd = f'file "/tmp/{backup_name}"' +# stdout, stderr, status = ssh_manager.execute_command(check_cmd) +# logger.info(f"Dosya tipi kontrolü: {stdout}") +# +# # Dosya boyutunu kontrol et +# size_cmd = f'ls -lh "/tmp/{backup_name}"' +# size_out, size_err, size_status = ssh_manager.execute_command(size_cmd) +# logger.info(f"Dosya boyutu: {size_out}") +# +# if not status or 'Zip archive data' not in stdout: +# error_msg = f"Oluşturulan dosya bir zip arşivi değil: {stdout}" +# logger.error(error_msg) +# SSHLog.objects.create( +# ssh_credential=ssh_credential, +# log_type='backup', +# command=f'Kontrol: {project.name}', +# output=error_msg, +# status=False +# ) +# continue +# +# # Google Drive yükleme işlemi... +# # ... (mevcut Drive yükleme kodu devam eder) +# +# # Tamamlanma logu +# SSHLog.objects.create( +# ssh_credential=ssh_credential, +# log_type='backup', +# command='Backup İşlemi', +# output='Tüm projelerin yedekleme işlemi başarıyla tamamlandı', +# status=True +# ) +# +# return JsonResponse({ +# 'success': True, +# 'message': 'Yedekleme işlemi tamamlandı!' +# }) +# +# finally: +# ssh_manager.close() +# +# except Exception as e: +# error_msg = f"Yedekleme sırasında hata oluştu: {str(e)}" +# if 'ssh_credential' in locals(): +# SSHLog.objects.create( +# ssh_credential=ssh_credential, +# log_type='backup', +# command='Backup Hatası', +# output=error_msg, +# status=False +# ) +# return JsonResponse({ +# 'success': False, +# 'message': error_msg +# }) + +@require_http_methods(["POST"]) +def backup_project(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + folder_name = project.folder_name + calisma_dizini = f"{project.ssh_credential.base_path}/{folder_name}" + + if not folder_name: + return JsonResponse({ + 'success': False, + 'message': 'Klasör adı bulunamadı' + }) + + try: + # Önce klasörün varlığını kontrol et + ssh_manager = project.ssh_credential.get_manager() + check_cmd = f'test -d "{calisma_dizini}" && echo "exists"' + stdout, stderr, status = ssh_manager.execute_command(check_cmd) + + if not status or stdout.strip() != "exists": + return JsonResponse({ + 'success': False, + 'message': 'Yedeklenecek klasör bulunamadı' + }) + + # Backup işlemini başlat + result = job(folder_name, calisma_dizini, project_id) + + if not result.get('success'): + raise Exception(result.get('message', 'Backup işlemi başarısız oldu')) + + # Backup tarihini güncelle + project.last_backup = timezone.now() + project.save() + + # Log oluştur + SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='backup', + command=f'Backup: {folder_name}', + output=f'Backup başarıyla tamamlandı. Dizin: {calisma_dizini}', + status=True + ) + + return JsonResponse({ + 'success': True, + 'message': 'Yedekleme işlemi başarıyla tamamlandı' + }) + + except Exception as e: + error_msg = str(e) + if 'NoSuchBucket' in error_msg: + error_msg = 'Backup bucket\'ı bulunamadı. Sistem yöneticinize başvurun.' + + # Hata logu oluştur + SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='backup', + command=f'Backup Error: {folder_name}', + output=f'Hata: {error_msg}', + status=False + ) + + logger.exception(f"Backup error for project {project_id}") + return JsonResponse({ + 'success': False, + 'message': f'Yedekleme işlemi başarısız: {error_msg}' + }) + + except Project.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': 'Proje bulunamadı' + }) + finally: + if 'ssh_manager' in locals(): + ssh_manager.close() + +def get_project_details(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + data = { + 'success': True, + 'project': { + 'name': project.name, + 'folder_name': project.folder_name, + 'url': project.url, + 'ssh_credential_id': project.ssh_credential.id if project.ssh_credential else '', + 'last_backup': project.last_backup.strftime('%d.%m.%Y %H:%M') if project.last_backup else None, + 'disk_usage': project.disk_usage or '0B', + } + } + return JsonResponse(data) + except Exception as e: + return JsonResponse({'success': False, 'message': 'Proje detayları alınırken bir hata oluştu.'}) + +@require_http_methods(["POST"]) +def delete_host(request, host_id): + try: + host = SSHCredential.objects.get(id=host_id) + host.delete() + return JsonResponse({'success': True, 'message': 'Host başarıyla silindi'}) + except SSHCredential.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Host bulunamadı'}) + except Exception as e: + return JsonResponse({'success': False, 'message': 'Host silinirken bir hata oluştu'}) + +@require_http_methods(["POST"]) +def update_host(request, host_id): + try: + host = SSHCredential.objects.get(id=host_id) + host.hostname = request.POST.get('hostname') + host.username = request.POST.get('username') + password = request.POST.get('password') + if password: + host.password = password + host.port = request.POST.get('port') + host.base_path = request.POST.get('base_path') + host.full_clean() + host.save() + return JsonResponse({'success': True, 'message': 'Host başarıyla güncellendi'}) + except SSHCredential.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Host bulunamadı'}) + except ValidationError as e: + return JsonResponse({'success': False, 'message': str(e)}) + except Exception as e: + return JsonResponse({'success': False, 'message': 'Host güncellenirken bir hata oluştu'}) + +@require_http_methods(["GET"]) +def project_backup_logs(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + # Hem backup hem de site kontrol loglarını getir + logs = SSHLog.objects.filter( + ssh_credential=project.ssh_credential, + ).filter( + models.Q(log_type='backup', command__icontains=project.folder_name) | + models.Q(log_type='command', command__icontains=project.name) + ).order_by('-created_at') # En yeni önce + + log_data = [ + { + 'created_at': log.created_at.strftime('%d.%m.%Y %H:%M:%S'), + 'command': log.command, + 'output': log.output, + 'status': log.status, + 'log_type': log.log_type + } + for log in logs + ] + return JsonResponse({'success': True, 'logs': log_data}) + except Exception as e: + return JsonResponse({'success': False, 'message': str(e)}) + +@require_http_methods(["POST"]) +def update_project(request, project_id): + try: + project = Project.objects.get(id=project_id) + + project.name = request.POST.get('name') + project.folder_name = request.POST.get('folder_name') + project.url = request.POST.get('url', '') + + # Model validation + project.full_clean() + project.save() + + return JsonResponse({ + 'success': True, + 'message': 'Proje başarıyla güncellendi' + }) + + except Project.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': 'Proje bulunamadı' + }, status=404) + + except ValidationError as e: + return JsonResponse({ + 'success': False, + 'message': str(e) + }, status=400) + + except Exception as e: + return JsonResponse({ + 'success': False, + 'message': 'Proje güncellenirken bir hata oluştu' + }, status=500) + +@require_http_methods(["POST"]) +def clear_project_logs(request, project_id): + """Belirtilen projenin tüm loglarını sil""" + try: + project = get_object_or_404(Project, id=project_id) + + # Bu projeye ait tüm logları sil - hem backup hem de site kontrol logları + deleted_count = SSHLog.objects.filter( + ssh_credential=project.ssh_credential + ).filter( + # Backup logları veya site kontrol logları + Q(log_type='backup', command__icontains=project.folder_name) | + Q(log_type='command', command__icontains=project.name) + ).delete()[0] + + return JsonResponse({ + 'success': True, + 'message': f'{deleted_count} log kaydı silindi', + 'deleted_count': deleted_count + }) + + except Exception as e: + return JsonResponse({ + 'success': False, + 'message': f'Log silme hatası: {str(e)}' + }, status=500) + +@require_http_methods(["POST"]) +def check_site_status_view(request, project_id): + """Tek projenin site durumunu kontrol et""" + try: + project = get_object_or_404(Project, id=project_id) + + from .utils import check_site_status + from .models import SSHLog + + # Site kontrol işlemini başlat ve log kaydı yap + log_entry = SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='command', + command=f"Site kontrol: {project.url} (Proje: {project.name})", + output="Site kontrol işlemi başlatıldı...", + status=False # Başlangıçta False, sonra güncellenecek + ) + + try: + status, message = check_site_status(project) + + # Host bilgilerini güncelle (bağlantı durumu ve disk kullanımı) + if project.ssh_credential: + ssh_manager = SSHManager(project.ssh_credential) + try: + # Bağlantı durumunu kontrol et + is_online = ssh_manager.check_connection() + project.ssh_credential.is_online = is_online + + # Disk kullanım bilgisini al (sadece online ise) + if is_online: + disk_usage_info = ssh_manager.get_disk_usage() + if disk_usage_info: + # Dictionary'yi string'e çevir (görüntüleme için) + project.ssh_credential.disk_usage = f"{disk_usage_info['used']} / {disk_usage_info['total']} ({disk_usage_info['usage_percent']}%)" + else: + project.ssh_credential.disk_usage = None + else: + project.ssh_credential.disk_usage = None + + project.ssh_credential.save() + + except Exception as e: + # Hata durumunda offline olarak işaretle + project.ssh_credential.is_online = False + project.ssh_credential.disk_usage = None + project.ssh_credential.save() + print(f"Host bilgi güncelleme hatası: {e}") + finally: + ssh_manager.close() + + # Log kaydını güncelle + log_entry.output = message + log_entry.status = status + log_entry.save() + + return JsonResponse({ + 'success': True, + 'status': status, + 'message': message, + 'is_active': project.is_site_active, + 'last_check': project.last_site_check.strftime('%d.%m.%Y %H:%M') if project.last_site_check else None + }) + + except Exception as e: + # Hata durumunda log kaydını güncelle + log_entry.output = f"Site kontrol hatası: {str(e)}" + log_entry.status = False + log_entry.save() + raise e + + except Exception as e: + return JsonResponse({ + 'success': False, + 'message': f'Site kontrol hatası: {str(e)}' + }, status=500) + + +@require_http_methods(["GET"]) +def get_project_meta_key(request, project_id): + """Projenin meta key'ini döndür""" + try: + project = get_object_or_404(Project, id=project_id) + + if not project.meta_key: + project.generate_meta_key() + project.save() + + return JsonResponse({ + 'success': True, + 'meta_key': project.meta_key, + 'meta_tag': project.get_meta_tag(), + 'instructions': 'Bu meta tag\'ı sitenizin bölümüne ekleyin' + }) + + except Exception as e: + return JsonResponse({ + 'success': False, + 'message': f'Meta key hatası: {str(e)}' + }, status=500) + + +@require_http_methods(["POST"]) +def check_all_sites_view(request): + """Tüm projelerin site durumunu kontrol et""" + try: + from .utils import check_all_sites + results = check_all_sites() + + return JsonResponse({ + 'success': True, + 'results': [ + { + 'project_id': result['project'].id, + 'project_name': result['project'].name, + 'status': result['status'], + 'message': result['message'] + } + for result in results + ] + }) + + except Exception as e: + return JsonResponse({ + 'success': False, + 'message': f'Toplu kontrol hatası: {str(e)}' + }, status=500) + +@csrf_exempt +def update_hosts_status(request): + """Tüm hostların durumunu güncelle""" + print(f"update_hosts_status çağrıldı, method: {request.method}") + + if request.method == 'POST': + try: + print("update_all_hosts_status fonksiyonu çağrılıyor...") + update_all_hosts_status() + print("update_all_hosts_status başarıyla tamamlandı") + return JsonResponse({ + 'success': True, + 'message': 'Tüm host bilgileri güncellendi' + }) + except Exception as e: + print(f"update_hosts_status hatası: {e}") + return JsonResponse({ + 'success': False, + 'message': f'Host güncelleme hatası: {str(e)}' + }) + + print("Geçersiz istek (POST değil)") + return JsonResponse({'success': False, 'message': 'Geçersiz istek'}) + +@csrf_exempt +def get_all_logs(request): + """Tüm sistem loglarını getir (İşlem Geçmişi için)""" + try: + # Tüm backup ve site kontrol loglarını al + backup_logs = SSHLog.objects.filter(log_type='backup').select_related('ssh_credential') + command_logs = SSHLog.objects.filter(log_type='command').select_related('ssh_credential') + + all_logs = [] + + # Backup loglarını ekle + for log in backup_logs: + all_logs.append({ + 'id': log.id, + 'created_at': log.created_at.isoformat(), + 'log_type': log.log_type, + 'command': log.command, + 'output': log.output, + 'status': log.status, + 'hostname': log.ssh_credential.hostname if log.ssh_credential else 'Bilinmiyor' + }) + + # Komut loglarını ekle + for log in command_logs: + all_logs.append({ + 'id': log.id, + 'created_at': log.created_at.isoformat(), + 'log_type': log.log_type, + 'command': log.command, + 'output': log.output, + 'status': log.status, + 'hostname': log.ssh_credential.hostname if log.ssh_credential else 'Bilinmiyor' + }) + + # Tarihe göre sırala (en yeni önce) + all_logs.sort(key=lambda x: x['created_at'], reverse=True) + + return JsonResponse({ + 'success': True, + 'logs': all_logs, + 'total_count': len(all_logs) + }) + + except Exception as e: + print(f"get_all_logs hatası: {e}") + return JsonResponse({ + 'success': False, + 'message': f'Log yükleme hatası: {str(e)}', + 'logs': [] + }) + +def islem_gecmisi(request): + """İşlem Geçmişi sayfası""" + # Tüm logları al + backup_logs = SSHLog.objects.filter(log_type='backup').select_related('ssh_credential') + command_logs = SSHLog.objects.filter(log_type='command').select_related('ssh_credential') + + # Logları birleştir ve tarihe göre sırala + all_logs = list(backup_logs) + list(command_logs) + all_logs.sort(key=lambda x: x.created_at, reverse=True) + + context = { + 'logs': all_logs, + 'page_title': 'İşlem Geçmişi', + 'active_menu': 'islem_gecmisi' + } + return render(request, 'ssh_manager/islem_gecmisi.html', context) + +def host_yonetimi(request): + """Host Yönetimi sayfası""" + ssh_credentials = SSHCredential.objects.all() + context = { + 'ssh_credentials': ssh_credentials, + 'page_title': 'Host Yönetimi', + 'active_menu': 'host_yonetimi' + } + return render(request, 'ssh_manager/host_yonetimi.html', context) + +def projeler(request): + """Projeler sayfası""" + projects = Project.objects.all() + context = { + 'projects': projects, + 'page_title': 'Projeler', + 'active_menu': 'projeler' + } + return render(request, 'ssh_manager/projeler.html', context) + +def yedeklemeler(request): + """Yedeklemeler sayfası""" + # Yedekleme loglarını al + backup_logs = SSHLog.objects.filter(log_type='backup').select_related('ssh_credential').order_by('-created_at') + + # İstatistikler + total_backups = backup_logs.count() + successful_backups = backup_logs.filter(status='success').count() + failed_backups = backup_logs.filter(status='error').count() + + context = { + 'backup_logs': backup_logs, + 'total_backups': total_backups, + 'successful_backups': successful_backups, + 'failed_backups': failed_backups, + 'page_title': 'Yedeklemeler', + 'active_menu': 'yedeklemeler' + } + return render(request, 'ssh_manager/yedeklemeler.html', context) + +def ayarlar(request): + """Ayarlar sayfası""" + context = { + 'page_title': 'Ayarlar', + 'active_menu': 'ayarlar' + } + return render(request, 'ssh_manager/ayarlar.html', context) + +# Müşteri Yönetimi Views + +def musteriler(request): + """Müşteriler sayfası""" + customers = Customer.objects.all().order_by('-created_at') + + # Müşteri tipine göre filtrele + customer_type = request.GET.get('type') + if customer_type in ['individual', 'corporate']: + customers = customers.filter(customer_type=customer_type) + + context = { + 'customers': customers, + 'page_title': 'Müşteriler', + 'active_menu': 'musteriler', + 'filter_type': customer_type + } + return render(request, 'ssh_manager/musteriler.html', context) + +def create_customer(request): + """Yeni müşteri oluştur""" + if request.method == 'POST': + try: + customer_type = request.POST.get('customer_type') + name = request.POST.get('name') + email = request.POST.get('email') + + # Zorunlu alan kontrolü + if not all([customer_type, name, email]): + return JsonResponse({ + 'success': False, + 'message': 'Gerekli alanlar eksik!' + }) + + # E-posta benzersizlik kontrolü + if Customer.objects.filter(email=email).exists(): + return JsonResponse({ + 'success': False, + 'message': 'Bu e-posta adresi zaten kullanılıyor!' + }) + + # Müşteri oluştur + customer = Customer( + customer_type=customer_type, + name=name, + email=email, + phone=request.POST.get('phone', ''), + address=request.POST.get('address', ''), + notes=request.POST.get('notes', '') + ) + + # Müşteri tipine göre alanları doldur + if customer_type == 'individual': + customer.surname = request.POST.get('surname', '') + customer.tc_number = request.POST.get('tc_number', '') + birth_date = request.POST.get('birth_date') + if birth_date: + customer.birth_date = birth_date + elif customer_type == 'corporate': + customer.company_name = request.POST.get('company_name', '') + customer.authorized_person = request.POST.get('authorized_person', '') + customer.tax_number = request.POST.get('tax_number', '') + customer.tax_office = request.POST.get('tax_office', '') + + # Validasyon + customer.full_clean() + customer.save() + + return JsonResponse({ + 'success': True, + 'message': 'Müşteri başarıyla oluşturuldu!' + }) + + except ValidationError as e: + return JsonResponse({ + 'success': False, + 'message': str(e) + }) + except Exception as e: + logger.exception("Müşteri oluşturma hatası") + return JsonResponse({ + 'success': False, + 'message': f'Müşteri oluşturulamadı: {str(e)}' + }) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek!'}) + +def get_customer_details(request, customer_id): + """Müşteri detaylarını getir""" + try: + customer = get_object_or_404(Customer, id=customer_id) + + customer_data = { + 'id': customer.id, + 'customer_type': customer.customer_type, + 'name': customer.name, + 'email': customer.email, + 'phone': customer.phone, + 'address': customer.address, + 'notes': customer.notes, + 'surname': customer.surname, + 'tc_number': customer.tc_number, + 'birth_date': customer.birth_date.strftime('%Y-%m-%d') if customer.birth_date else '', + 'company_name': customer.company_name, + 'authorized_person': customer.authorized_person, + 'tax_number': customer.tax_number, + 'tax_office': customer.tax_office, + } + + return JsonResponse({ + 'success': True, + 'customer': customer_data + }) + + except Exception as e: + logger.exception("Müşteri detay alma hatası") + return JsonResponse({ + 'success': False, + 'message': f'Müşteri bilgisi alınamadı: {str(e)}' + }) + +def update_customer(request, customer_id): + """Müşteri güncelle""" + if request.method == 'POST': + try: + customer = get_object_or_404(Customer, id=customer_id) + + customer_type = request.POST.get('customer_type') + name = request.POST.get('name') + email = request.POST.get('email') + + # Zorunlu alan kontrolü + if not all([customer_type, name, email]): + return JsonResponse({ + 'success': False, + 'message': 'Gerekli alanlar eksik!' + }) + + # E-posta benzersizlik kontrolü (kendisi hariç) + if Customer.objects.filter(email=email).exclude(id=customer_id).exists(): + return JsonResponse({ + 'success': False, + 'message': 'Bu e-posta adresi zaten kullanılıyor!' + }) + + # Müşteriyi güncelle + customer.customer_type = customer_type + customer.name = name + customer.email = email + customer.phone = request.POST.get('phone', '') + customer.address = request.POST.get('address', '') + customer.notes = request.POST.get('notes', '') + + # Müşteri tipine göre alanları güncelle + if customer_type == 'individual': + customer.surname = request.POST.get('surname', '') + customer.tc_number = request.POST.get('tc_number', '') + birth_date = request.POST.get('birth_date') + customer.birth_date = birth_date if birth_date else None + # Kurumsal alanları temizle + customer.company_name = '' + customer.authorized_person = '' + customer.tax_number = '' + customer.tax_office = '' + elif customer_type == 'corporate': + customer.company_name = request.POST.get('company_name', '') + customer.authorized_person = request.POST.get('authorized_person', '') + customer.tax_number = request.POST.get('tax_number', '') + customer.tax_office = request.POST.get('tax_office', '') + # Bireysel alanları temizle + customer.surname = '' + customer.tc_number = '' + customer.birth_date = None + + # Validasyon + customer.full_clean() + customer.save() + + return JsonResponse({ + 'success': True, + 'message': 'Müşteri başarıyla güncellendi!' + }) + + except ValidationError as e: + return JsonResponse({ + 'success': False, + 'message': str(e) + }) + except Exception as e: + logger.exception("Müşteri güncelleme hatası") + return JsonResponse({ + 'success': False, + 'message': f'Müşteri güncellenemedi: {str(e)}' + }) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek!'}) + +def edit_customer(request, customer_id): + """Müşteri düzenleme sayfası (gerekirse)""" + customer = get_object_or_404(Customer, id=customer_id) + # Bu view'ı şu an kullanmıyoruz, AJAX ile hallediyoruz + return redirect('musteriler') + +def delete_customer(request, customer_id): + """Müşteri sil""" + if request.method == 'POST': + try: + customer = get_object_or_404(Customer, id=customer_id) + + # Müşteriye ait proje kontrolü + project_count = customer.project_set.count() + if project_count > 0: + return JsonResponse({ + 'success': False, + 'message': f'Bu müşteriye ait {project_count} proje bulunuyor. Önce projeleri silin veya başka müşteriye atayın.' + }) + + customer_name = str(customer) + customer.delete() + + return JsonResponse({ + 'success': True, + 'message': f'{customer_name} başarıyla silindi!' + }) + + except Exception as e: + logger.exception("Müşteri silme hatası") + return JsonResponse({ + 'success': False, + 'message': f'Müşteri silinemedi: {str(e)}' + }) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek!'}) + +@csrf_exempt +def test_host_connection(request, host_id): + """Host bağlantı testi""" + if request.method == 'POST': + try: + host = SSHCredential.objects.get(id=host_id) + client = SSHManager(host) + + # Bağlantı testi ve disk kullanımı güncelleme + if client.check_connection(): + # Disk kullanımı al + disk_info = client.get_disk_usage() + if disk_info and isinstance(disk_info, dict): + host.disk_usage = disk_info.get('usage_percent') + + host.connection_status = 'connected' + host.last_checked = timezone.now() + host.save() + + client.close() + + disk_msg = f" Disk kullanımı: {host.disk_usage}%" if host.disk_usage else "" + return JsonResponse({ + 'success': True, + 'message': f'{host.name} - Bağlantı başarılı!{disk_msg}' + }) + else: + host.connection_status = 'failed' + host.last_checked = timezone.now() + host.save() + + return JsonResponse({ + 'success': False, + 'message': f'{host.name} - Bağlantı başarısız!' + }) + + except SSHCredential.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Host bulunamadı'}) + except Exception as e: + return JsonResponse({'success': False, 'message': f'Hata: {str(e)}'}) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek'}) + +@csrf_exempt +def refresh_all_hosts(request): + """Tüm hostları yenile""" + if request.method == 'POST': + try: + hosts = SSHCredential.objects.all() + success_count = 0 + total_count = hosts.count() + + for host in hosts: + try: + client = SSHManager(host) + if client.check_connection(): + # Disk kullanımı al + disk_info = client.get_disk_usage() + if disk_info and isinstance(disk_info, dict): + host.disk_usage = disk_info.get('usage_percent') + + host.connection_status = 'connected' + success_count += 1 + else: + host.connection_status = 'failed' + + host.last_checked = timezone.now() + host.save() + client.close() + + except Exception as e: + host.connection_status = 'failed' + host.last_checked = timezone.now() + host.save() + + return JsonResponse({ + 'success': True, + 'message': f'{total_count} host kontrol edildi. {success_count} host başarılı.' + }) + + except Exception as e: + return JsonResponse({'success': False, 'message': f'Hata: {str(e)}'}) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek'}) + +@csrf_exempt +def create_host(request): + """Yeni host oluştur""" + if request.method == 'POST': + try: + name = request.POST.get('name') + hostname = request.POST.get('hostname') + port = request.POST.get('port', 22) + username = request.POST.get('username') + password = request.POST.get('password') + base_path = request.POST.get('base_path', '/var/www') + is_default = request.POST.get('is_default') == 'on' + + if not all([name, hostname, username]): + return JsonResponse({'success': False, 'message': 'Zorunlu alanları doldurun'}) + + # Eğer bu varsayılan olarak ayarlanıyorsa, diğerlerini güncelle + if is_default: + SSHCredential.objects.filter(is_default=True).update(is_default=False) + + host = SSHCredential.objects.create( + name=name, + hostname=hostname, + port=port, + username=username, + password=password, + base_path=base_path, + is_default=is_default + ) + + return JsonResponse({ + 'success': True, + 'message': f'Host "{name}" başarıyla oluşturuldu' + }) + + except Exception as e: + return JsonResponse({'success': False, 'message': f'Hata: {str(e)}'}) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek'}) + +@csrf_exempt +def update_host(request, host_id): + """Host güncelle""" + if request.method == 'POST': + try: + host = SSHCredential.objects.get(id=host_id) + + host.name = request.POST.get('name', host.name) + host.hostname = request.POST.get('hostname', host.hostname) + host.port = request.POST.get('port', host.port) + host.username = request.POST.get('username', host.username) + + password = request.POST.get('password') + if password: # Yalnızca yeni şifre girilmişse güncelle + host.password = password + + host.base_path = request.POST.get('base_path', host.base_path) + + is_default = request.POST.get('is_default') == 'on' + if is_default and not host.is_default: + # Diğer varsayılanları kaldır + SSHCredential.objects.filter(is_default=True).update(is_default=False) + + host.is_default = is_default + host.save() + + return JsonResponse({ + 'success': True, + 'message': f'Host "{host.name}" başarıyla güncellendi' + }) + + except SSHCredential.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Host bulunamadı'}) + except Exception as e: + return JsonResponse({'success': False, 'message': f'Hata: {str(e)}'}) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek'}) + +def get_host_details(request, host_id): + """Host detaylarını getir""" + try: + host = SSHCredential.objects.get(id=host_id) + return JsonResponse({ + 'success': True, + 'host': { + 'id': host.id, + 'name': host.name, + 'hostname': host.hostname, + 'port': host.port, + 'username': host.username, + 'base_path': host.base_path, + 'is_default': host.is_default + } + }) + except SSHCredential.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Host bulunamadı'}) + +@csrf_exempt +def delete_host(request, host_id): + """Host sil""" + if request.method == 'DELETE': + try: + host = SSHCredential.objects.get(id=host_id) + + # Varsayılan host siliniyorsa uyarı ver + if host.is_default: + return JsonResponse({ + 'success': False, + 'message': 'Varsayılan host silinemez. Önce başka bir host\'u varsayılan yapın.' + }) + + host_name = host.name + host.delete() + + return JsonResponse({ + 'success': True, + 'message': f'Host "{host_name}" başarıyla silindi' + }) + + except SSHCredential.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Host bulunamadı'}) + except Exception as e: + return JsonResponse({'success': False, 'message': f'Hata: {str(e)}'}) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek'}) + +@csrf_exempt +def test_host_connection_form(request): + """Form verisiyle bağlantı testi""" + if request.method == 'POST': + try: + hostname = request.POST.get('hostname') + port = request.POST.get('port', 22) + username = request.POST.get('username') + password = request.POST.get('password') + + if not all([hostname, username]): + return JsonResponse({'success': False, 'message': 'Hostname ve kullanıcı adı gerekli'}) + + # Geçici SSH credential oluştur + temp_host = SSHCredential( + hostname=hostname, + port=port, + username=username, + password=password + ) + + client = SSHManager(temp_host) + if client.check_connection(): + client.close() + return JsonResponse({ + 'success': True, + 'message': 'Bağlantı testi başarılı!' + }) + else: + return JsonResponse({ + 'success': False, + 'message': 'Bağlantı testi başarısız!' + }) + + except Exception as e: + return JsonResponse({'success': False, 'message': f'Hata: {str(e)}'}) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek'}) + +@csrf_exempt +def start_backup(request): + """Tek proje yedekleme başlat""" + if request.method == 'POST': + try: + project_id = request.POST.get('project_id') + backup_type = request.POST.get('backup_type', 'full') + compress = request.POST.get('compress') == 'on' + note = request.POST.get('note', '') + + if not project_id: + return JsonResponse({'success': False, 'message': 'Proje ID gerekli'}) + + project = Project.objects.get(id=project_id) + + # Yedekleme işlemini başlat (arka planda) + from threading import Thread + + def backup_project(): + try: + ssh_manager = SSHManager(project.ssh_credential) + + # Log kaydı oluştur + log = SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='backup', + command=f'Backup {project.name} ({backup_type})', + status='running' + ) + + if ssh_manager.check_connection(): + # Yedekleme komutlarını çalıştır + backup_commands = [] + + if backup_type in ['full', 'files']: + # Dosya yedekleme + if compress: + backup_commands.append(f'cd {project.ssh_credential.base_path} && tar -czf {project.folder_name}_backup_{log.created_at.strftime("%Y%m%d_%H%M%S")}.tar.gz {project.folder_name}/') + else: + backup_commands.append(f'cd {project.ssh_credential.base_path} && cp -r {project.folder_name} {project.folder_name}_backup_{log.created_at.strftime("%Y%m%d_%H%M%S")}') + + if backup_type in ['full', 'database']: + # Veritabanı yedekleme (örnek MySQL) + backup_commands.append(f'mysqldump -u username -p password database_name > {project.folder_name}_db_backup_{log.created_at.strftime("%Y%m%d_%H%M%S")}.sql') + + outputs = [] + for cmd in backup_commands: + output = ssh_manager.execute_command(cmd) + outputs.append(f"Command: {cmd}\nOutput: {output}") + + log.output = '\n\n'.join(outputs) + log.status = 'success' + + # Proje son yedekleme tarihini güncelle + project.last_backup = timezone.now() + project.save() + + else: + log.output = 'SSH bağlantı hatası' + log.status = 'error' + + log.save() + ssh_manager.close() + + except Exception as e: + log.output = f'Yedekleme hatası: {str(e)}' + log.status = 'error' + log.save() + + # Arka planda çalıştır + backup_thread = Thread(target=backup_project) + backup_thread.start() + + return JsonResponse({ + 'success': True, + 'message': f'{project.name} projesi için yedekleme başlatıldı' + }) + + except Project.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Proje bulunamadı'}) + except Exception as e: + return JsonResponse({'success': False, 'message': f'Hata: {str(e)}'}) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek'}) + +@csrf_exempt +def backup_all_projects(request): + """Tüm projeleri yedekle""" + if request.method == 'POST': + try: + projects = Project.objects.filter(ssh_credential__isnull=False) + + if not projects.exists(): + return JsonResponse({'success': False, 'message': 'Yedeklenecek proje bulunamadı'}) + + # Her proje için yedekleme başlat + from threading import Thread + + def backup_all(): + for project in projects: + try: + ssh_manager = SSHManager(project.ssh_credential) + + log = SSHLog.objects.create( + ssh_credential=project.ssh_credential, + log_type='backup', + command=f'Auto backup {project.name}', + status='running' + ) + + if ssh_manager.check_connection(): + # Basit dosya yedekleme + cmd = f'cd {project.ssh_credential.base_path} && tar -czf {project.folder_name}_auto_backup_{log.created_at.strftime("%Y%m%d_%H%M%S")}.tar.gz {project.folder_name}/' + output = ssh_manager.execute_command(cmd) + + log.output = f"Command: {cmd}\nOutput: {output}" + log.status = 'success' + + project.last_backup = timezone.now() + project.save() + else: + log.output = 'SSH bağlantı hatası' + log.status = 'error' + + log.save() + ssh_manager.close() + + except Exception as e: + log.output = f'Yedekleme hatası: {str(e)}' + log.status = 'error' + log.save() + + backup_thread = Thread(target=backup_all) + backup_thread.start() + + return JsonResponse({ + 'success': True, + 'message': f'{projects.count()} proje için toplu yedekleme başlatıldı' + }) + + except Exception as e: + return JsonResponse({'success': False, 'message': f'Hata: {str(e)}'}) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek'}) + +@csrf_exempt +def retry_backup(request): + """Yedeklemeyi tekrar dene""" + if request.method == 'POST': + try: + import json + data = json.loads(request.body) + project_id = data.get('project_id') + + if not project_id: + return JsonResponse({'success': False, 'message': 'Proje ID gerekli'}) + + project = Project.objects.get(id=project_id) + + # start_backup fonksiyonunu çağır + from django.test import RequestFactory + factory = RequestFactory() + retry_request = factory.post('/start-backup/', { + 'project_id': project_id, + 'backup_type': 'full', + 'compress': 'on' + }) + retry_request.META['HTTP_X_CSRFTOKEN'] = request.META.get('HTTP_X_CSRFTOKEN') + + return start_backup(retry_request) + + except Project.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Proje bulunamadı'}) + except Exception as e: + return JsonResponse({'success': False, 'message': f'Hata: {str(e)}'}) + + return JsonResponse({'success': False, 'message': 'Geçersiz istek'}) diff --git a/templates/ssh_manager/base.html b/templates/ssh_manager/base.html new file mode 100644 index 0000000..c986c94 --- /dev/null +++ b/templates/ssh_manager/base.html @@ -0,0 +1,506 @@ + + + + + {% block title %}Hosting Yönetim Paneli{% endblock %} + + + + + + + + + +
+ + + +
+ + +
+

{% block page_title %}{{ page_title|default:"Dashboard" }}{% endblock %}

+ + {% block content %} + {% endblock %} +
+ + + + + diff --git a/templates/ssh_manager/dashboard.html b/templates/ssh_manager/dashboard.html new file mode 100644 index 0000000..9a5bda1 --- /dev/null +++ b/templates/ssh_manager/dashboard.html @@ -0,0 +1,445 @@ +{% extends 'ssh_manager/base.html' %} + +{% block title %}Dashboard - Hosting Yönetim Paneli{% endblock %} + +{% block content %} + +
+
+
+
+

+ Dashboard +

+

Hosting yönetim sistemi genel görünümü

+
+
+ Son güncelleme: {{ "now"|date:"d.m.Y H:i" }} +
+
+
+
+ + +
+ +
+
+
+
+
+ +
+
+

{{ projects.count }}

+

Toplam Proje

+
+
+ +
+
+
+ + +
+
+
+
+
+ +
+
+

{{ active_sites_count }}

+

Aktif Site

+
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ +
+
+

{{ customers.count }}

+

Toplam Müşteri

+
+
+ +
+
+
+ + +
+
+
+
+
+ +
+
+

{{ online_hosts_count }}/{{ ssh_credentials.count }}

+

Çevrimiçi Host

+
+
+
+ + + Yönet + +
+
+
+
+
+ + +
+ +
+
+
+
+ Son İşlemler +
+
+
+ {% if recent_logs %} +
+ + + + + + + + + + + {% for log in recent_logs|slice:":10" %} + + + + + + + {% endfor %} + +
TarihİşlemProjeDurum
+ {{ log.created_at|date:"d.m H:i" }} + + + {{ log.get_log_type_display }} + + {% if log.ssh_credential %} + {{ log.ssh_credential.name }} + {% else %} + - + {% endif %} + + + {{ log.get_status_display }} + +
+
+ + {% else %} +
+ +

Henüz işlem geçmişi yok

+
+ {% endif %} +
+
+
+ + +
+
+
+
+ Sistem Durumu +
+
+
+ +
+
Host Durumları
+ {% for host in ssh_credentials|slice:":5" %} +
+ {{ host.name }} + + {% if host.connection_status == 'connected' %}Bağlı{% elif host.connection_status == 'failed' %}Hata{% else %}Bilinmiyor{% endif %} + +
+ {% empty %} +

Host tanımlanmamış

+ {% endfor %} +
+ + +
+
Disk Kullanımı
+ {% for host in ssh_credentials %} + {% if host.disk_usage %} +
+
+ {{ host.name }} + {{ host.disk_usage }}% +
+
+
+
+
+ {% endif %} + {% endfor %} +
+ + +
+
Hızlı İşlemler
+
+ + + Yedekleme Başlat + +
+
+
+
+
+
+ + +
+
+
+
+
+ Son Yedeklemeler +
+
+
+ {% if recent_backups %} +
+ {% for project in recent_backups|slice:":6" %} +
+
+
+
+
{{ project.name }}
+ + {% if project.last_backup %} + {{ project.last_backup|date:"d.m.Y H:i" }} + {% else %} + Yedek alınmamış + {% endif %} + +
+
+ {% if project.last_backup %} + Tamam + {% else %} + Bekliyor + {% endif %} +
+
+
+
+ {% endfor %} +
+ + {% else %} +
+ +

Henüz yedekleme yapılmamış

+ +
+ {% endif %} +
+
+
+
+ + + + +{% endblock %} diff --git a/templates/ssh_manager/host_yonetimi.html b/templates/ssh_manager/host_yonetimi.html new file mode 100644 index 0000000..a30e06f --- /dev/null +++ b/templates/ssh_manager/host_yonetimi.html @@ -0,0 +1,359 @@ +{% extends 'ssh_manager/base.html' %} + +{% block title %}Host Yönetimi - Hosting Yönetim Paneli{% endblock %} + +{% block content %} +
+
+

Host Yönetimi

+ SSH bağlantı bilgileri ve sunucu durumları +
+
+ + +
+
+ + +
+ + + + + + + + + + + + + + + {% for host in ssh_credentials %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
Host AdıIP/DomainPortKullanıcıDurumDisk KullanımıSon Kontrolİşlemler
+ {{ host.name }} + {% if host.is_default %} + Varsayılan + {% endif %} + {{ host.hostname }}{{ host.port }}{{ host.username }} + + {% if host.connection_status == 'connected' %} + Bağlı + {% elif host.connection_status == 'failed' %} + Hata + {% else %} + Bilinmiyor + {% endif %} + + + {% if host.disk_usage %} +
+
+
+
+ {{ host.disk_usage }}% +
+ {% else %} + - + {% endif %} +
+ {% if host.last_checked %} + {{ host.last_checked|date:"d.m.Y H:i" }} + {% else %} + Hiçbir zaman + {% endif %} + + + + + +
+ +
Henüz host tanımlanmamış
+ +
+
+ + + + + +{% endblock %} diff --git a/templates/ssh_manager/islem_gecmisi.html b/templates/ssh_manager/islem_gecmisi.html new file mode 100644 index 0000000..dff8c50 --- /dev/null +++ b/templates/ssh_manager/islem_gecmisi.html @@ -0,0 +1,140 @@ +{% extends 'ssh_manager/base.html' %} +{% load static %} + +{% block content %} + +
+
+

İşlem Geçmişi

+ Tüm sistem işlemleri ve logları +
+
+ + +
+
+ +
+ + + + + + + + + + + + {% for log in logs %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
#TarihTipProjeİşlem
{{ forloop.counter }}{{ log.created_at|date:"d.m.Y H:i" }} + {% if log.log_type == 'backup' %} + 💾 Yedekleme + {% else %} + ⚙️ Komut + {% endif %} + + {% if 'Proje:' in log.output %} + {% with project_name=log.output|cut:'Proje: ' %} + {{ project_name|truncatechars:25 }} + {% endwith %} + {% elif 'Proje:' in log.command %} + {% if ')' in log.command %} + {% with project_part=log.command|cut:'(Proje: '|cut:')' %} + {{ project_part|truncatechars:25 }} + {% endwith %} + {% else %} + {% with project_part=log.command|cut:'Proje: ' %} + {{ project_part|truncatechars:25 }} + {% endwith %} + {% endif %} + {% elif log.log_type == 'backup' and 'Backup:' in log.command %} + {% with folder_name=log.command|cut:'Backup: ' %} + {{ folder_name|truncatechars:25 }} + {% endwith %} + {% else %} + Sistem + {% endif %} + {{ log.command|default:log.output|truncatechars:100 }}
+ +

Henüz işlem geçmişi bulunmuyor

+
+
+ + +{% endblock %} diff --git a/templates/ssh_manager/musteriler.html b/templates/ssh_manager/musteriler.html new file mode 100644 index 0000000..4437fbc --- /dev/null +++ b/templates/ssh_manager/musteriler.html @@ -0,0 +1,467 @@ +{% extends 'ssh_manager/base.html' %} +{% load static %} + +{% block content %} + + +
+
+

Müşteri Yönetimi + {% if filter_type == 'individual' %} + - Bireysel Müşteriler + {% elif filter_type == 'corporate' %} + - Kurumsal Müşteriler + {% endif %} +

+ Bireysel ve kurumsal müşteri profilleri +
+
+ + +
+
+ +
+ {% for customer in customers %} +
+
+
+
+
{{ customer.get_display_name }}
+ + {% if customer.customer_type == 'corporate' %}Kurumsal{% else %}Bireysel{% endif %} + +
+
+ + +
+
+ +
+ {% if customer.email %} +
+ + {{ customer.email }} +
+ {% endif %} + {% if customer.phone %} +
+ + {{ customer.phone }} +
+ {% endif %} + {% if customer.customer_type == 'corporate' and customer.tax_number %} +
+ + Vergi No: {{ customer.tax_number }} +
+ {% endif %} +
+ +
+ + {{ customer.project_set.count }} proje + {% if customer.notes %} +
+ + {{ customer.notes|truncatechars:50 }} +
+ {% endif %} +
+
+
+ {% empty %} +
+
+ +

Henüz müşteri kaydı bulunmuyor

+
+
+ {% endfor %} +
+ + + + + +{% endblock %} diff --git a/templates/ssh_manager/project_list.html b/templates/ssh_manager/project_list.html new file mode 100644 index 0000000..1513f36 --- /dev/null +++ b/templates/ssh_manager/project_list.html @@ -0,0 +1,1081 @@ + + + + + Hosting Yönetim Paneli + + + + + + + + + +
+ +
+ + + + + +
+

Dashboard

+ + +
+
+ + + +
+
+

Host Hesapları

+ +
+ + + + + + + + + + + + + + + {% for host in ssh_credentials %} + + + + + + + + + + + {% empty %} + + {% endfor %} + +
#HostnameKullanıcıPortBase PathDisk KullanımıSon Kontrolİşlemler
{{ forloop.counter }}{{ host.hostname }}{{ host.username }}{{ host.port }}{{ host.base_path }}{{ host.disk_usage|default:'-' }}{{ host.last_check|date:"d.m.Y H:i" }} + + +
Kayıtlı host yok.
+
+

Projeler

+ +
+ + + + + + + + + + + + + {% for project in projects %} + + + + + + + + + {% empty %} + + {% endfor %} + +
#Proje AdıKlasörURLSon Yedeklemeİşlemler
{{ forloop.counter }} + {{ project.name }}
+ {% if project.customer %} + 👤 {{ project.customer.get_display_name }}
+ {% endif %} + {% if project.ssh_credential %} + {% if project.ssh_credential.hostname %} + 🖥️ {{ project.ssh_credential.hostname }} + {% else %} + Hostname boş + {% endif %} + {% else %} + SSH credential yok + {% endif %} +
+ {{ project.folder_name }}
+ {{ project.disk_usage|default:'-' }} +
+ {% if project.url %} +
+ {% if project.is_site_active %} + 🟢 + {% elif project.last_site_check %} + 🔴 + {% else %} + + {% endif %} + + {{ project.url }} + +
+ {% else %} + - + {% endif %} +
{{ project.last_backup|date:"d.m.Y H:i" }} + + + + + {% if project.url %} + + + {% endif %} +
Kayıtlı proje yok.
+
+
+ + + + + + + + + + + + + + + + diff --git a/templates/ssh_manager/projeler.html b/templates/ssh_manager/projeler.html new file mode 100644 index 0000000..3bf8388 --- /dev/null +++ b/templates/ssh_manager/projeler.html @@ -0,0 +1,554 @@ +{% extends 'ssh_manager/base.html' %} + +{% block title %}Projeler - Hosting Yönetim Paneli{% endblock %} + +{% block content %} + +
+
+

+ Projeler +

+ Tüm hosting projeleri ve yönetim işlemleri +
+
+ + +
+
+ + +
+ + + + + + + + + + + + + {% for project in projects %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
#Proje BilgileriKlasör & DiskSite DurumuSon Yedeklemeİşlemler
{{ forloop.counter }} +
{{ project.name }}
+ {% if project.customer %} + + {{ project.customer.get_display_name }} +
+ {% endif %} + {% if project.ssh_credential %} + + {{ project.ssh_credential.hostname }} + + {% else %} + SSH credential yok + {% endif %} +
+
{{ project.folder_name }}
+ {% if project.disk_usage %} + + {{ project.disk_usage }} + + {% else %} + - + {% endif %} +
+ {% if project.url %} +
+ {% if project.is_site_active %} + + Aktif + + {% elif project.last_site_check %} + + Pasif + + {% else %} + + Bilinmiyor + + {% endif %} +
+ + {{ project.url }} + + {% else %} + URL tanımlanmamış + {% endif %} +
+ {% if project.last_backup %} + {{ project.last_backup|date:"d.m.Y H:i" }} + {% else %} + Yedek alınmamış + {% endif %} + + + + + + {% if project.url %} + + + {% endif %} +
+ +
Henüz proje eklenmemiş
+ +
+
+ + + + + + + + + + + +{% endblock %} diff --git a/templates/ssh_manager/yedeklemeler.html b/templates/ssh_manager/yedeklemeler.html new file mode 100644 index 0000000..5618d7e --- /dev/null +++ b/templates/ssh_manager/yedeklemeler.html @@ -0,0 +1,426 @@ +{% extends 'ssh_manager/base.html' %} + +{% block title %}Yedeklemeler - Hosting Yönetim Paneli{% endblock %} + +{% block content %} + +
+
+

+ Yedeklemeler +

+ Proje yedekleme işlemleri ve S3 yönetimi +
+
+ + + +
+
+ + +
+
+
+
+
+ +
+

{{ total_backups|default:0 }}

+

Toplam Yedekleme

+
+
+
+
+
+
+
+ +
+

{{ successful_backups|default:0 }}

+

Başarılı

+
+
+
+
+
+
+
+ +
+

{{ failed_backups|default:0 }}

+

Başarısız

+
+
+
+
+
+
+
+ +
+

+ {% if backup_logs %} + {{ backup_logs.0.created_at|date:"d.m H:i" }} + {% else %} + - + {% endif %} +

+

Son Yedekleme

+
+
+
+
+ + +
+
+
+ Yedekleme Geçmişi +
+
+
+ {% if backup_logs %} +
+ + + + + + + + + + + + + {% for log in backup_logs %} + + + + + + + + + {% endfor %} + +
TarihProjeHostDurumDetayİşlemler
+ {{ log.created_at|date:"d.m.Y" }}
+ {{ log.created_at|date:"H:i:s" }} +
+ {% if log.ssh_credential %} + {{ log.ssh_credential.name }} + {% else %} + Genel + {% endif %} + + {% if log.ssh_credential %} + {{ log.ssh_credential.hostname }} + {% else %} + - + {% endif %} + + + {% if log.status == 'success' %} + Başarılı + {% elif log.status == 'error' %} + Hata + {% else %} + Bekliyor + {% endif %} + + + {% if log.output %} + + {% else %} + - + {% endif %} + +
+ {% if log.ssh_credential %} + + {% endif %} + +
+
+
+ {% else %} +
+ +
Henüz yedekleme yapılmamış
+

İlk yedeklemenizi başlatmak için yukarıdaki butonu kullanın

+ +
+ {% endif %} +
+
+ + + + + + + + + + +{% endblock %} diff --git a/yonetim/__init__.py b/yonetim/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yonetim/asgi.py b/yonetim/asgi.py new file mode 100644 index 0000000..53823ca --- /dev/null +++ b/yonetim/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for yonetim project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yonetim.settings') + +application = get_asgi_application() diff --git a/yonetim/settings.py b/yonetim/settings.py new file mode 100644 index 0000000..8a72378 --- /dev/null +++ b/yonetim/settings.py @@ -0,0 +1,131 @@ +""" +Django settings for yonetim project. + +Generated by 'django-admin startproject' using Django 5.2.4. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-q3)mbwf)1sn6y+9x)&y_d35e)$mm6$&&^2n8i9dwv)kjz%7)6t' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['yonetim.alcom.dev'] + +CSRF_TRUSTED_ORIGINS = ['https://yonetim.alcom.dev'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'ssh_manager', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'yonetim.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'] + , + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'yonetim.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + +# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +# SECURE_SSL_REDIRECT = True +# SESSION_COOKIE_SECURE = True +# CSRF_COOKIE_SECURE = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/yonetim/urls.py b/yonetim/urls.py new file mode 100644 index 0000000..fc0cd19 --- /dev/null +++ b/yonetim/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for yonetim project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('ssh_manager.urls')), +] diff --git a/yonetim/wsgi.py b/yonetim/wsgi.py new file mode 100644 index 0000000..afb9280 --- /dev/null +++ b/yonetim/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for yonetim project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yonetim.settings') + +application = get_wsgi_application()