yeni
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -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
|
||||||
14
.idea/deployment.xml
generated
Normal file
14
.idea/deployment.xml
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PublishConfigData" serverName="YONETIM" remoteFilesAllowedToDisappearOnAutoupload="false">
|
||||||
|
<serverData>
|
||||||
|
<paths name="YONETIM">
|
||||||
|
<serverdata>
|
||||||
|
<mappings>
|
||||||
|
<mapping local="$PROJECT_DIR$" web="/" />
|
||||||
|
</mappings>
|
||||||
|
</serverdata>
|
||||||
|
</paths>
|
||||||
|
</serverData>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
24
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
24
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myValues">
|
||||||
|
<value>
|
||||||
|
<list size="1">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="Jost" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="myCustomValuesEnabled" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyInterpreterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredIdentifiers">
|
||||||
|
<list>
|
||||||
|
<option value="pages.views.about.*" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ReassignedToPlainText" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.11 (yonetim)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (yonetim)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/yonetim.iml" filepath="$PROJECT_DIR$/.idea/yonetim.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
14
.idea/webServers.xml
generated
Normal file
14
.idea/webServers.xml
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="WebServers">
|
||||||
|
<option name="servers">
|
||||||
|
<webServer id="229148d6-cc65-4e83-9311-3d59c0d43b95" name="YONETIM">
|
||||||
|
<fileTransfer rootFolder="/srv/yonetim" accessType="SFTP" host="51.38.103.237" port="22" sshConfigId="076ef7a5-08c2-4382-acef-942ea7decf61" sshConfig="root@51.38.103.237:22 password">
|
||||||
|
<advancedOptions>
|
||||||
|
<advancedOptions dataProtectionLevel="Private" passiveMode="true" shareSSLContext="true" />
|
||||||
|
</advancedOptions>
|
||||||
|
</fileTransfer>
|
||||||
|
</webServer>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
30
.idea/yonetim.iml
generated
Normal file
30
.idea/yonetim.iml
generated
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="FacetManager">
|
||||||
|
<facet type="django" name="Django">
|
||||||
|
<configuration>
|
||||||
|
<option name="rootFolder" value="$MODULE_DIR$" />
|
||||||
|
<option name="settingsModule" value="yonetim/settings.py" />
|
||||||
|
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
|
||||||
|
<option name="environment" value="<map/>" />
|
||||||
|
<option name="doNotUseTestRunner" value="false" />
|
||||||
|
<option name="trackFilePattern" value="migrations" />
|
||||||
|
</configuration>
|
||||||
|
</facet>
|
||||||
|
</component>
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="TemplatesService">
|
||||||
|
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||||
|
<option name="TEMPLATE_FOLDERS">
|
||||||
|
<list>
|
||||||
|
<option value="$MODULE_DIR$/../yonetim\templates" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
65
build/Dockerfile
Normal file
65
build/Dockerfile
Normal file
@ -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"]
|
||||||
56
build/init.sh
Normal file
56
build/init.sh
Normal file
@ -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 "$@"
|
||||||
48
build/requirements.txt
Normal file
48
build/requirements.txt
Normal file
@ -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
|
||||||
BIN
db.sqlite3
Normal file
BIN
db.sqlite3
Normal file
Binary file not shown.
31
docker-compose.yml
Normal file
31
docker-compose.yml
Normal file
@ -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
|
||||||
22
manage.py
Normal file
22
manage.py
Normal file
@ -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()
|
||||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
67
ssh_manager/2backup.py
Normal file
67
ssh_manager/2backup.py
Normal file
@ -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}")
|
||||||
1
ssh_manager/__init__.py
Normal file
1
ssh_manager/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'ssh_manager.apps.SshManagerConfig'
|
||||||
71
ssh_manager/admin.py
Normal file
71
ssh_manager/admin.py
Normal file
@ -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
|
||||||
24
ssh_manager/apps.py
Normal file
24
ssh_manager/apps.py
Normal file
@ -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
|
||||||
626
ssh_manager/backup.py
Normal file
626
ssh_manager/backup.py
Normal file
@ -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("<span style='color:#8bc34a'>S3 oturumu başlatıldı.</span>")
|
||||||
|
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"<span style='color:#bdbdbd'>SSH üzerinden zip dosyası oluşturuluyor...</span>")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# SSH üzerinden uzak sunucuda zip oluştur
|
||||||
|
zip_dosya_adi = folder + "_" + current_date + ".zip"
|
||||||
|
|
||||||
|
log_and_db(f"<span style='color:#bdbdbd'>Kaynak dizin: {calisma_dizini}</span>")
|
||||||
|
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası adı: {zip_dosya_adi}</span>")
|
||||||
|
|
||||||
|
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"<span style='color:#8bc34a'>Uzak sunucuda zip oluşturuldu: {remote_zip_path} ({file_size} byte)</span>")
|
||||||
|
|
||||||
|
# Zip dosyasını local'e indir
|
||||||
|
local_zip_path = os.path.join("/tmp", zip_dosya_adi)
|
||||||
|
|
||||||
|
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası indiriliyor: {local_zip_path}</span>")
|
||||||
|
|
||||||
|
if not download_ssh_file(ssh_manager, remote_zip_path, local_zip_path):
|
||||||
|
raise Exception("Zip dosyası indirilemedi")
|
||||||
|
|
||||||
|
log_and_db(f"<span style='color:#8bc34a'>Zip dosyası başarıyla indirildi</span>")
|
||||||
|
|
||||||
|
# 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"<span style='color:#ff9800'>Zip oluşturma başarısız: {str(zip_error)}</span>")
|
||||||
|
log_and_db(f"<span style='color:#bdbdbd'>Tar ile yedekleme deneniyor...</span>")
|
||||||
|
|
||||||
|
# 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"<span style='color:#8bc34a'>Uzak sunucuda tar.gz oluşturuldu: {remote_tar_path} ({file_size} byte)</span>")
|
||||||
|
|
||||||
|
# Tar dosyasını local'e indir
|
||||||
|
local_tar_path = os.path.join("/tmp", tar_dosya_adi)
|
||||||
|
|
||||||
|
log_and_db(f"<span style='color:#bdbdbd'>Tar dosyası indiriliyor: {local_tar_path}</span>")
|
||||||
|
|
||||||
|
if not download_ssh_file(ssh_manager, remote_tar_path, local_tar_path):
|
||||||
|
raise Exception("Tar dosyası indirilemedi")
|
||||||
|
|
||||||
|
log_and_db(f"<span style='color:#8bc34a'>Tar dosyası başarıyla indirildi</span>")
|
||||||
|
|
||||||
|
# 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"<span style='color:#ff5252'>{error_msg}</span>", 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"<span style='color:#bdbdbd'>Zip işlemi tamamlandı: <b>{output_zip}</b></span>")
|
||||||
|
|
||||||
|
# --- Zip dosyası oluştu mu ve boş mu kontrolü ---
|
||||||
|
if not os.path.exists(output_zip):
|
||||||
|
log_and_db(f"<span style='color:#ff5252'>Zip dosyası oluşmadı: <b>{output_zip}</b></span>", status=False)
|
||||||
|
return {'success': False, 'message': 'Zip dosyası oluşmadı', 'logs': logs}
|
||||||
|
else:
|
||||||
|
size = os.path.getsize(output_zip)
|
||||||
|
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası boyutu: <b>{size} byte</b></span>")
|
||||||
|
if size == 0:
|
||||||
|
log_and_db(f"<span style='color:#ff5252'>Zip dosyası BOŞ!</span>", 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"<span style='color:#ffd600'>Bucket oluşturuldu: <b>{bucket_name}</b></span>")
|
||||||
|
else:
|
||||||
|
log_and_db(f"<span style='color:#ffd600'>Bucket mevcut: <b>{bucket_name}</b></span>")
|
||||||
|
# S3'e yükle (Vultr Object Storage için özel yöntem)
|
||||||
|
log_and_db(f"<span style='color:#bdbdbd'>Dosya S3'e yükleniyor: <b>{s3_key}</b></span>")
|
||||||
|
|
||||||
|
# Dosya boyutunu kontrol et
|
||||||
|
file_size = os.path.getsize(output_zip)
|
||||||
|
log_and_db(f"<span style='color:#bdbdbd'>Yüklenecek dosya boyutu: <b>{file_size} bytes</b></span>")
|
||||||
|
|
||||||
|
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"<span style='color:#ff9800'>Standart yükleme başarısız, presigned URL deneniyor: {upload_error}</span>")
|
||||||
|
|
||||||
|
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"<span style='color:#8bc34a'>S3'e başarıyla yüklendi: <b>{bucket_name}/{s3_key}</b></span>")
|
||||||
|
except Exception as e:
|
||||||
|
log_and_db(f"<span style='color:#ff5252'>S3 yükleme hatası: {e}</span>", 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"<span style='color:#bdbdbd'>Geçici zip dosyası silindi: <b>{output_zip}</b></span>")
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
11
ssh_manager/middleware.py
Normal file
11
ssh_manager/middleware.py
Normal file
@ -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
|
||||||
54
ssh_manager/migrations/0001_initial.py
Normal file
54
ssh_manager/migrations/0001_initial.py
Normal file
@ -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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
20
ssh_manager/migrations/0002_default_ssh_credential.py
Normal file
20
ssh_manager/migrations/0002_default_ssh_credential.py
Normal file
@ -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),
|
||||||
|
]
|
||||||
@ -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,
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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ı'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
ssh_manager/migrations/0010_sshcredential_disk_usage.py
Normal file
18
ssh_manager/migrations/0010_sshcredential_disk_usage.py
Normal file
@ -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ı'),
|
||||||
|
),
|
||||||
|
]
|
||||||
46
ssh_manager/migrations/0011_customer_project_customer.py
Normal file
46
ssh_manager/migrations/0011_customer_project_customer.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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ı (%)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
1
ssh_manager/migrations/__init__.py
Normal file
1
ssh_manager/migrations/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Bu dosya boş kalacak
|
||||||
198
ssh_manager/models.py
Normal file
198
ssh_manager/models.py
Normal file
@ -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'<meta name="site-verification" content="{self.meta_key}">'
|
||||||
|
|
||||||
|
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']
|
||||||
13
ssh_manager/settings.py
Normal file
13
ssh_manager/settings.py
Normal file
@ -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"
|
||||||
|
}
|
||||||
13
ssh_manager/signals.py
Normal file
13
ssh_manager/signals.py
Normal file
@ -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()
|
||||||
607
ssh_manager/ssh_client.py
Normal file
607
ssh_manager/ssh_client.py
Normal file
@ -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
|
||||||
19
ssh_manager/ssh_manager.py
Normal file
19
ssh_manager/ssh_manager.py
Normal file
@ -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
|
||||||
0
ssh_manager/templatetags/__init__.py
Normal file
0
ssh_manager/templatetags/__init__.py
Normal file
9
ssh_manager/templatetags/ssh_manager_tags.py
Normal file
9
ssh_manager/templatetags/ssh_manager_tags.py
Normal file
@ -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')
|
||||||
79
ssh_manager/urls.py
Normal file
79
ssh_manager/urls.py
Normal file
@ -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/<int:customer_id>/edit/', views.edit_customer, name='edit_customer'),
|
||||||
|
path('musteri/<int:customer_id>/delete/', views.delete_customer, name='delete_customer'),
|
||||||
|
path('get-customer-details/<int:customer_id>/', views.get_customer_details, name='get_customer_details'),
|
||||||
|
path('update-customer/<int:customer_id>/', views.update_customer, name='update_customer'),
|
||||||
|
path('get_host/<int:host_id>/', views.get_host, name='get_host'),
|
||||||
|
path('update_host/<int:host_id>/', views.update_host, name='update_host'),
|
||||||
|
path('delete_host/<int:host_id>/', 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/<int:project_id>/upload/', views.upload_project_zip, name='upload_project_zip'),
|
||||||
|
path('delete_project/<int:project_id>/', views.delete_project, name='delete_project'),
|
||||||
|
path('project/<int:project_id>/setup-venv/', views.setup_venv, name='setup_venv'),
|
||||||
|
path('project/<int:project_id>/check-requirements/', views.check_requirements, name='check_requirements'),
|
||||||
|
path('project/<int:project_id>/update-requirements/', views.update_requirements, name='update_requirements'),
|
||||||
|
path('project/<int:project_id>/delete-requirement-line/', views.delete_requirement_line, name='delete_requirement_line'),
|
||||||
|
path('project/<int:project_id>/download-req/', views.download_req_file, name='download_req_file'),
|
||||||
|
path('logs/<int:ssh_credential_id>/', 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/<int:project_id>/check-venv/', views.check_venv, name='check_venv'),
|
||||||
|
path('project/<int:project_id>/install-requirements/', views.install_requirements, name='install_requirements'),
|
||||||
|
path('project/<int:project_id>/check-folder/', views.check_folder_empty, name='check_folder_empty'),
|
||||||
|
path('project/<int:project_id>/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/<int:project_id>/restart-supervisor/', views.restart_supervisor, name='restart_supervisor'),
|
||||||
|
path('project/<int:project_id>/refresh/', views.refresh_project, name='refresh_project'),
|
||||||
|
# path('backup/', views.backup_projects, name='backup_projects'),
|
||||||
|
path('backup-project/<int:project_id>/', views.backup_project, name='backup_project'),
|
||||||
|
path('project/<int:project_id>/backup-logs/', views.project_backup_logs, name='project_backup_logs'),
|
||||||
|
path('project/<int:project_id>/clear-logs/', views.clear_project_logs, name='clear_project_logs'),
|
||||||
|
path('project/<int:project_id>/check-site/', views.check_site_status_view, name='check_site_status'),
|
||||||
|
path('project/<int:project_id>/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/<int:project_id>/', views.get_project_details, name='get_project_details'),
|
||||||
|
path('update-project/<int:project_id>/', views.update_project, name='update_project'),
|
||||||
|
|
||||||
|
# Host yönetimi URL'leri
|
||||||
|
path('test-host-connection/<int:host_id>/', 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/<int:host_id>/', views.update_host, name='update_host'),
|
||||||
|
path('get-host-details/<int:host_id>/', views.get_host_details, name='get_host_details'),
|
||||||
|
path('delete-host/<int:host_id>/', 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/<int:project_id>/', views.upload_to_drive, name='upload_to_drive').
|
||||||
|
]
|
||||||
146
ssh_manager/utils.py
Normal file
146
ssh_manager/utils.py
Normal file
@ -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
|
||||||
2494
ssh_manager/views.py
Normal file
2494
ssh_manager/views.py
Normal file
File diff suppressed because it is too large
Load Diff
506
templates/ssh_manager/base.html
Normal file
506
templates/ssh_manager/base.html
Normal file
@ -0,0 +1,506 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="tr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{% block title %}Hosting Yönetim Paneli{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #181a1b;
|
||||||
|
color: #f0f0f0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Styles */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 280px;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #1a1d23 0%, #23272b 100%);
|
||||||
|
border-right: 1px solid #333;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 2px 0 10px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 2rem 1.5rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4fc3f7;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-subtitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #c0c0c0;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: block;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
color: #e8e8e8;
|
||||||
|
text-decoration: none;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: rgba(79, 195, 247, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
border-left-color: #4fc3f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: rgba(79, 195, 247, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
border-left-color: #4fc3f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item i {
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown menu styles */
|
||||||
|
.nav-dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-content {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: #1a1d23;
|
||||||
|
border-left: 3px solid #4fc3f7;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown.active .nav-dropdown-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-item {
|
||||||
|
display: block;
|
||||||
|
padding: 0.5rem 1.5rem 0.5rem 3rem;
|
||||||
|
color: #e0e0e0;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-item:hover {
|
||||||
|
background: rgba(79, 195, 247, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-item.active {
|
||||||
|
background: rgba(79, 195, 247, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-toggle::after {
|
||||||
|
content: '\F282';
|
||||||
|
font-family: 'bootstrap-icons';
|
||||||
|
float: right;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown.active .nav-dropdown-toggle::after {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
margin-left: 280px;
|
||||||
|
padding: 2rem;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark th, .table-dark td { color: #f0f0f0; }
|
||||||
|
.table-dark { background: #23272b; }
|
||||||
|
.modal-content { background: #23272b; color: #f0f0f0; }
|
||||||
|
.form-control, .form-select { background: #181a1b; color: #f0f0f0; border: 1px solid #444; }
|
||||||
|
.form-label { color: #f0f0f0; }
|
||||||
|
.actions { min-width: 140px; }
|
||||||
|
.btn { padding: 0.25rem 0.5rem; }
|
||||||
|
.btn i { font-size: 1.1rem; }
|
||||||
|
.action-icon {
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: 0 0.3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
.action-icon:hover { color: #4fc3f7; }
|
||||||
|
.action-icon.edit:hover { color: #17a2b8; }
|
||||||
|
.action-icon.delete:hover { color: #dc3545; }
|
||||||
|
.action-icon.backup:hover { color: #ffc107; }
|
||||||
|
.action-icon.logs:hover { color: #6c757d; }
|
||||||
|
.action-icon.site:hover { color: #28a745; }
|
||||||
|
.action-icon.meta:hover { color: #007bff; }
|
||||||
|
.table-striped > tbody > tr:nth-of-type(odd) { background-color: #23272b; }
|
||||||
|
.table-striped > tbody > tr:nth-of-type(even) { background-color: #181a1b; }
|
||||||
|
.badge.bg-success, .badge.bg-danger { font-size: 0.9em; }
|
||||||
|
.form-control.editing:focus { background:rgb(108, 106, 106) !important; color:rgb(176, 181, 185) !important; border-color: #bdbdbd !important; }
|
||||||
|
#projectSearch::placeholder { color: #bbb; }
|
||||||
|
#mainToast .toast-body { color: #ffffff; font-weight: bold; }
|
||||||
|
|
||||||
|
.log-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-row:hover {
|
||||||
|
background-color: rgba(79, 195, 247, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-status-success {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-status-error {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-type-backup {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-type-command {
|
||||||
|
color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main content text improvements */
|
||||||
|
.main-content h1, .main-content h2, .main-content h3, .main-content h4, .main-content h5, .main-content h6 {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content p, .main-content span, .main-content div {
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content .text-muted {
|
||||||
|
color: #b0b0b0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form controls and inputs */
|
||||||
|
.form-control::placeholder {
|
||||||
|
color: #bbb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
background-color: #2a2d31 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-color: #4fc3f7 !important;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(79, 195, 247, 0.25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button text improvements */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #4fc3f7;
|
||||||
|
border-color: #4fc3f7;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #6c757d;
|
||||||
|
border-color: #6c757d;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table improvements */
|
||||||
|
.table th {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
color: #f0f0f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal improvements */
|
||||||
|
.modal-header {
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast improvements */
|
||||||
|
#mainToastBody {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Customer Form Improvements */
|
||||||
|
.modal-lg {
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body h6 {
|
||||||
|
color: #4fc3f7;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid rgba(79, 195, 247, 0.3);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:checked {
|
||||||
|
background-color: #4fc3f7;
|
||||||
|
border-color: #4fc3f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.g-3 {
|
||||||
|
--bs-gutter-x: 1.5rem;
|
||||||
|
--bs-gutter-y: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #4fc3f7;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(79, 195, 247, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #b0b0b0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<i class="bi bi-server"></i>
|
||||||
|
HostPanel
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-subtitle">Hosting Yönetim Sistemi</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a href="{% url 'dashboard' %}" class="nav-item {% if request.resolver_match.url_name == 'project_list' or request.resolver_match.url_name == 'dashboard' %}active{% endif %}">
|
||||||
|
<i class="bi bi-speedometer2"></i>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'musteriler' %}" class="nav-item {% if request.resolver_match.url_name == 'musteriler' %}active{% endif %}">
|
||||||
|
<i class="bi bi-people"></i>
|
||||||
|
Müşteriler
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'host_yonetimi' %}" class="nav-item {% if request.resolver_match.url_name == 'host_yonetimi' %}active{% endif %}">
|
||||||
|
<i class="bi bi-hdd-network"></i>
|
||||||
|
Host Yönetimi
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'projeler' %}" class="nav-item {% if request.resolver_match.url_name == 'projeler' %}active{% endif %}">
|
||||||
|
<i class="bi bi-folder-fill"></i>
|
||||||
|
Projeler
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'yedeklemeler' %}" class="nav-item {% if request.resolver_match.url_name == 'yedeklemeler' %}active{% endif %}">
|
||||||
|
<i class="bi bi-cloud-arrow-up"></i>
|
||||||
|
Yedeklemeler
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'islem_gecmisi' %}" class="nav-item {% if request.resolver_match.url_name == 'islem_gecmisi' %}active{% endif %}">
|
||||||
|
<i class="bi bi-clock-history"></i>
|
||||||
|
İşlem Geçmişi
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'ayarlar' %}" class="nav-item {% if request.resolver_match.url_name == 'ayarlar' %}active{% endif %}">
|
||||||
|
<i class="bi bi-gear"></i>
|
||||||
|
Ayarlar
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Container -->
|
||||||
|
<div class="position-fixed top-0 end-0 p-3" style="z-index: 1100">
|
||||||
|
<div id="mainToast" class="toast align-items-center text-bg-dark border-0" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="4000">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body" id="mainToastBody">
|
||||||
|
Mesaj
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Kapat"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="progressToast" class="toast align-items-center text-bg-info border-0" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="false">
|
||||||
|
<div class="toast-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span id="progressMessage">İşlem devam ediyor...</span>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Kapat"></button>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 6px;">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated" id="progressBar" role="progressbar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted" id="progressDetail">Başlatılıyor...</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="main-content">
|
||||||
|
<h1 class="mb-4" style="color: #ffffff;">{% block page_title %}{{ page_title|default:"Dashboard" }}{% endblock %}</h1>
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function getCookie(name) {
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const toast = document.getElementById('mainToast');
|
||||||
|
const toastBody = document.getElementById('mainToastBody');
|
||||||
|
|
||||||
|
toastBody.textContent = message;
|
||||||
|
|
||||||
|
// Remove existing classes
|
||||||
|
toast.classList.remove('text-bg-dark', 'text-bg-success', 'text-bg-danger', 'text-bg-warning', 'text-bg-info');
|
||||||
|
|
||||||
|
// Add appropriate class based on type
|
||||||
|
switch(type) {
|
||||||
|
case 'success':
|
||||||
|
toast.classList.add('text-bg-success');
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
toast.classList.add('text-bg-danger');
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
toast.classList.add('text-bg-warning');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
toast.classList.add('text-bg-info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const bsToast = new bootstrap.Toast(toast);
|
||||||
|
bsToast.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropdown toggle fonksiyonu
|
||||||
|
function toggleDropdown(element) {
|
||||||
|
const dropdown = element.closest('.nav-dropdown');
|
||||||
|
const allDropdowns = document.querySelectorAll('.nav-dropdown');
|
||||||
|
|
||||||
|
// Diğer dropdown'ları kapat
|
||||||
|
allDropdowns.forEach(dd => {
|
||||||
|
if (dd !== dropdown) {
|
||||||
|
dd.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bu dropdown'ı aç/kapat
|
||||||
|
dropdown.classList.toggle('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yeni müşteri modal'ını aç
|
||||||
|
function openNewCustomerModal(type) {
|
||||||
|
// Eğer müşteri sayfasındaysak modal'ı doğrudan aç
|
||||||
|
if (window.location.pathname.includes('/musteriler/')) {
|
||||||
|
if (typeof resetCustomerForm === 'function') {
|
||||||
|
resetCustomerForm();
|
||||||
|
if (type === 'corporate') {
|
||||||
|
document.getElementById('typeCorporate').checked = true;
|
||||||
|
document.getElementById('corporateFields').style.display = 'block';
|
||||||
|
document.getElementById('individualFields').style.display = 'none';
|
||||||
|
document.getElementById('surname').required = false;
|
||||||
|
document.getElementById('company_name').required = true;
|
||||||
|
} else {
|
||||||
|
document.getElementById('typeIndividual').checked = true;
|
||||||
|
document.getElementById('individualFields').style.display = 'block';
|
||||||
|
document.getElementById('corporateFields').style.display = 'none';
|
||||||
|
document.getElementById('surname').required = true;
|
||||||
|
document.getElementById('company_name').required = false;
|
||||||
|
}
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('customerModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Müşteri sayfasına yönlendir
|
||||||
|
window.location.href = '/musteriler/?new=' + type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sayfa dışına tıklanırsa dropdown'ları kapat
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
if (!event.target.closest('.nav-dropdown')) {
|
||||||
|
document.querySelectorAll('.nav-dropdown').forEach(dd => {
|
||||||
|
dd.classList.remove('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
445
templates/ssh_manager/dashboard.html
Normal file
445
templates/ssh_manager/dashboard.html
Normal file
@ -0,0 +1,445 @@
|
|||||||
|
{% extends 'ssh_manager/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard - Hosting Yönetim Paneli{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Dashboard Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-1">
|
||||||
|
<i class="bi bi-speedometer2 me-2"></i>Dashboard
|
||||||
|
</h2>
|
||||||
|
<p class="text-muted mb-0">Hosting yönetim sistemi genel görünümü</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<small class="text-muted">Son güncelleme: {{ "now"|date:"d.m.Y H:i" }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dashboard Cards -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<!-- Toplam Projeler -->
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="card dashboard-card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="icon-box bg-primary bg-opacity-10">
|
||||||
|
<i class="bi bi-folder-fill text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ms-3">
|
||||||
|
<h3 class="mb-0">{{ projects.count }}</h3>
|
||||||
|
<p class="text-muted mb-0">Toplam Proje</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{% url 'projeler' %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-arrow-right"></i> Projeleri Görüntüle
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktif Siteler -->
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="card dashboard-card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="icon-box bg-success bg-opacity-10">
|
||||||
|
<i class="bi bi-globe text-success"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ms-3">
|
||||||
|
<h3 class="mb-0">{{ active_sites_count }}</h3>
|
||||||
|
<p class="text-muted mb-0">Aktif Site</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<button class="btn btn-sm btn-outline-success" onclick="checkAllSites()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> Tümünü Kontrol Et
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toplam Müşteriler -->
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="card dashboard-card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="icon-box bg-info bg-opacity-10">
|
||||||
|
<i class="bi bi-people text-info"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ms-3">
|
||||||
|
<h3 class="mb-0">{{ customers.count }}</h3>
|
||||||
|
<p class="text-muted mb-0">Toplam Müşteri</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{% url 'musteriler' %}" class="btn btn-sm btn-outline-info">
|
||||||
|
<i class="bi bi-arrow-right"></i> Müşterileri Görüntüle
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Host Durumu -->
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="card dashboard-card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="icon-box bg-warning bg-opacity-10">
|
||||||
|
<i class="bi bi-server text-warning"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ms-3">
|
||||||
|
<h3 class="mb-0">{{ online_hosts_count }}/{{ ssh_credentials.count }}</h3>
|
||||||
|
<p class="text-muted mb-0">Çevrimiçi Host</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 d-flex gap-2">
|
||||||
|
<button class="btn btn-sm btn-outline-success" onclick="refreshAllHosts()" title="Tüm host durumlarını kontrol et">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> Kontrol Et
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'host_yonetimi' %}" class="btn btn-sm btn-outline-warning">
|
||||||
|
<i class="bi bi-gear"></i> Yönet
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts Row -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<!-- Son İşlemler -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="bi bi-clock-history me-2"></i>Son İşlemler
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if recent_logs %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tarih</th>
|
||||||
|
<th>İşlem</th>
|
||||||
|
<th>Proje</th>
|
||||||
|
<th>Durum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for log in recent_logs|slice:":10" %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<small>{{ log.created_at|date:"d.m H:i" }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<i class="bi {% if log.log_type == 'backup' %}bi-cloud-arrow-up text-warning{% elif log.log_type == 'command' %}bi-terminal text-info{% else %}bi-gear text-secondary{% endif %}"></i>
|
||||||
|
{{ log.get_log_type_display }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if log.ssh_credential %}
|
||||||
|
<small>{{ log.ssh_credential.name }}</small>
|
||||||
|
{% else %}
|
||||||
|
<small class="text-muted">-</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if log.status == 'success' %}bg-success{% else %}bg-danger{% endif %} fs-6">
|
||||||
|
{{ log.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="{% url 'islem_gecmisi' %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-list-ul"></i> Tüm İşlem Geçmişi
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-clock-history" style="font-size: 2rem; color: #6c757d;"></i>
|
||||||
|
<p class="text-muted mt-2">Henüz işlem geçmişi yok</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sistem Durumu -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="bi bi-activity me-2"></i>Sistem Durumu
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Host Durumları -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-3">Host Durumları</h6>
|
||||||
|
{% for host in ssh_credentials|slice:":5" %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="small">{{ host.name }}</span>
|
||||||
|
<span class="badge {% if host.connection_status == 'connected' %}bg-success{% elif host.connection_status == 'failed' %}bg-danger{% else %}bg-secondary{% endif %}">
|
||||||
|
{% if host.connection_status == 'connected' %}Bağlı{% elif host.connection_status == 'failed' %}Hata{% else %}Bilinmiyor{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p class="text-muted small">Host tanımlanmamış</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Disk Kullanımı -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-3">Disk Kullanımı</h6>
|
||||||
|
{% for host in ssh_credentials %}
|
||||||
|
{% if host.disk_usage %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<small>{{ host.name }}</small>
|
||||||
|
<small>{{ host.disk_usage }}%</small>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 6px;">
|
||||||
|
<div class="progress-bar {% if host.disk_usage > 80 %}bg-danger{% elif host.disk_usage > 60 %}bg-warning{% else %}bg-success{% endif %}"
|
||||||
|
style="width: {{ host.disk_usage }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hızlı İşlemler -->
|
||||||
|
<div>
|
||||||
|
<h6 class="text-muted mb-3">Hızlı İşlemler</h6>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="refreshAllData()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> Tüm Verileri Yenile
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'yedeklemeler' %}" class="btn btn-sm btn-outline-warning">
|
||||||
|
<i class="bi bi-cloud-arrow-up"></i> Yedekleme Başlat
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Son Yedeklemeler -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="bi bi-cloud-arrow-up me-2"></i>Son Yedeklemeler
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if recent_backups %}
|
||||||
|
<div class="row">
|
||||||
|
{% for project in recent_backups|slice:":6" %}
|
||||||
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
|
<div class="backup-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">{{ project.name }}</h6>
|
||||||
|
<small class="text-muted">
|
||||||
|
{% if project.last_backup %}
|
||||||
|
{{ project.last_backup|date:"d.m.Y H:i" }}
|
||||||
|
{% else %}
|
||||||
|
Yedek alınmamış
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% if project.last_backup %}
|
||||||
|
<span class="badge bg-success">Tamam</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning">Bekliyor</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="{% url 'yedeklemeler' %}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-cloud-arrow-up"></i> Tüm Yedeklemeler
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-cloud-arrow-up" style="font-size: 2rem; color: #6c757d;"></i>
|
||||||
|
<p class="text-muted mt-2">Henüz yedekleme yapılmamış</p>
|
||||||
|
<button class="btn btn-primary" onclick="startBackup()">
|
||||||
|
<i class="bi bi-cloud-arrow-up"></i> İlk Yedeklemeyi Başlat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dashboard-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.5);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box i {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-item {
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #23272b;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: #1a1d23;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Tüm host durumlarını kontrol et
|
||||||
|
function refreshAllHosts() {
|
||||||
|
showToast('Host durumları kontrol ediliyor...', 'info');
|
||||||
|
|
||||||
|
fetch('/refresh-all-hosts/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast('Host durumları başarıyla güncellendi', 'success');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showToast('Host kontrol hatası: ' + (data.message || 'Bilinmeyen hata'), 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Host kontrol hatası:', error);
|
||||||
|
showToast('Host kontrol hatası', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tüm siteleri kontrol et
|
||||||
|
function checkAllSites() {
|
||||||
|
showToast('Tüm siteler kontrol ediliyor...', 'info');
|
||||||
|
|
||||||
|
fetch('/check-all-sites/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
setTimeout(() => location.reload(), 2000);
|
||||||
|
} else {
|
||||||
|
showToast(data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('Site kontrol hatası', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tüm verileri yenile
|
||||||
|
function refreshAllData() {
|
||||||
|
showToast('Veriler yenileniyor...', 'info');
|
||||||
|
|
||||||
|
fetch('/refresh-all-hosts/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast('Veriler başarıyla yenilendi', 'success');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showToast('Veri yenileme hatası', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('Veri yenileme hatası', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yedekleme başlat
|
||||||
|
function startBackup() {
|
||||||
|
showToast('Yedekleme işlemi başlatılıyor...', 'info');
|
||||||
|
// Yedekleme sayfasına yönlendir
|
||||||
|
window.location.href = '{% url "yedeklemeler" %}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sayfa yüklendiğinde otomatik host kontrol (opsiyonel)
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Son host kontrolünden 10 dakika geçtiyse otomatik kontrol et
|
||||||
|
const lastCheck = localStorage.getItem('lastHostCheck');
|
||||||
|
const now = new Date().getTime();
|
||||||
|
|
||||||
|
if (!lastCheck || (now - parseInt(lastCheck)) > 10 * 60 * 1000) { // 10 dakika
|
||||||
|
// Sessizce host durumlarını kontrol et (sadece ilk açılışta)
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Otomatik host kontrolü başlatılıyor...');
|
||||||
|
refreshAllHosts();
|
||||||
|
localStorage.setItem('lastHostCheck', now.toString());
|
||||||
|
}, 2000); // 2 saniye sonra başlat
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
359
templates/ssh_manager/host_yonetimi.html
Normal file
359
templates/ssh_manager/host_yonetimi.html
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
{% extends 'ssh_manager/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Host Yönetimi - Hosting Yönetim Paneli{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-1">Host Yönetimi</h3>
|
||||||
|
<small class="text-muted">SSH bağlantı bilgileri ve sunucu durumları</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-success" onclick="refreshAllHosts()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> Tümünü Yenile
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#hostModal" onclick="resetHostForm()">
|
||||||
|
<i class="bi bi-plus-circle"></i> Yeni Host
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Host Tablosu -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Host Adı</th>
|
||||||
|
<th>IP/Domain</th>
|
||||||
|
<th>Port</th>
|
||||||
|
<th>Kullanıcı</th>
|
||||||
|
<th>Durum</th>
|
||||||
|
<th>Disk Kullanımı</th>
|
||||||
|
<th>Son Kontrol</th>
|
||||||
|
<th class="actions">İşlemler</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for host in ssh_credentials %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ host.name }}</strong>
|
||||||
|
{% if host.is_default %}
|
||||||
|
<span class="badge bg-info ms-1">Varsayılan</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ host.hostname }}</td>
|
||||||
|
<td>{{ host.port }}</td>
|
||||||
|
<td>{{ host.username }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if host.connection_status == 'connected' %}bg-success{% elif host.connection_status == 'failed' %}bg-danger{% else %}bg-secondary{% endif %}">
|
||||||
|
{% if host.connection_status == 'connected' %}
|
||||||
|
<i class="bi bi-check-circle"></i> Bağlı
|
||||||
|
{% elif host.connection_status == 'failed' %}
|
||||||
|
<i class="bi bi-x-circle"></i> Hata
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-question-circle"></i> Bilinmiyor
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if host.disk_usage %}
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="progress me-2" style="width: 80px; height: 8px;">
|
||||||
|
<div class="progress-bar {% if host.disk_usage > 80 %}bg-danger{% elif host.disk_usage > 60 %}bg-warning{% else %}bg-success{% endif %}"
|
||||||
|
style="width: {{ host.disk_usage }}%"></div>
|
||||||
|
</div>
|
||||||
|
<small>{{ host.disk_usage }}%</small>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if host.last_checked %}
|
||||||
|
<small class="text-muted">{{ host.last_checked|date:"d.m.Y H:i" }}</small>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Hiçbir zaman</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="actions">
|
||||||
|
<i class="action-icon bi bi-arrow-clockwise" title="Bağlantı Testi" onclick="testConnection({{ host.id }})"></i>
|
||||||
|
<i class="action-icon edit bi bi-pencil" title="Düzenle" onclick="editHost({{ host.id }})"></i>
|
||||||
|
<i class="action-icon delete bi bi-trash" title="Sil" onclick="deleteHost({{ host.id }})"></i>
|
||||||
|
<i class="action-icon logs bi bi-list-ul" title="Loglar" onclick="viewHostLogs({{ host.id }})"></i>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center text-muted py-4">
|
||||||
|
<i class="bi bi-server" style="font-size: 2rem;"></i>
|
||||||
|
<div class="mt-2">Henüz host tanımlanmamış</div>
|
||||||
|
<button class="btn btn-sm btn-outline-primary mt-2" data-bs-toggle="modal" data-bs-target="#hostModal" onclick="resetHostForm()">
|
||||||
|
İlk host'u ekle
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Host Ekleme/Düzenleme Modal -->
|
||||||
|
<div class="modal fade" id="hostModal" tabindex="-1" aria-labelledby="hostModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="hostForm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="hostModalLabel">Yeni Host Ekle</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="hostId" name="hostId">
|
||||||
|
|
||||||
|
<!-- Bağlantı Bilgileri -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-3">
|
||||||
|
<i class="bi bi-server me-1"></i>Bağlantı Bilgileri
|
||||||
|
</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="hostName" class="form-label">Host Adı *</label>
|
||||||
|
<input type="text" class="form-control" id="hostName" name="name" required placeholder="Sunucu adı">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="hostname" class="form-label">IP/Domain *</label>
|
||||||
|
<input type="text" class="form-control" id="hostname" name="hostname" required placeholder="192.168.1.100 veya example.com">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="port" class="form-label">Port</label>
|
||||||
|
<input type="number" class="form-control" id="port" name="port" value="22" min="1" max="65535">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="username" class="form-label">Kullanıcı Adı *</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required placeholder="root">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kimlik Doğrulama -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-3">
|
||||||
|
<i class="bi bi-key me-1"></i>Kimlik Doğrulama
|
||||||
|
</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="password" class="form-label">Şifre</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" placeholder="SSH şifresi">
|
||||||
|
<small class="text-muted">Güvenlik için şifre şifrelenmiş olarak saklanır</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="basePath" class="form-label">Temel Dizin</label>
|
||||||
|
<input type="text" class="form-control" id="basePath" name="base_path" placeholder="/var/www" value="/var/www">
|
||||||
|
<small class="text-muted">Projelerin bulunduğu ana dizin</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ayarlar -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-muted mb-3">
|
||||||
|
<i class="bi bi-gear me-1"></i>Ayarlar
|
||||||
|
</h6>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="isDefault" name="is_default">
|
||||||
|
<label class="form-check-label" for="isDefault">
|
||||||
|
Varsayılan host olarak ayarla
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted d-block">Yeni projeler için otomatik olarak bu host kullanılacak</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Kapat</button>
|
||||||
|
<button type="button" class="btn btn-info me-2" onclick="testHostConnection()">
|
||||||
|
<i class="bi bi-wifi"></i> Bağlantı Testi
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Kaydet</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Host bağlantı testi
|
||||||
|
function testConnection(hostId) {
|
||||||
|
showToast('Bağlantı test ediliyor...', 'info');
|
||||||
|
|
||||||
|
fetch(`/test-host-connection/${hostId}/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showToast(data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('Bağlantı testi sırasında hata oluştu', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tüm hostları yenile
|
||||||
|
function refreshAllHosts() {
|
||||||
|
showToast('Tüm hostlar kontrol ediliyor...', 'info');
|
||||||
|
|
||||||
|
fetch('/refresh-all-hosts/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
setTimeout(() => location.reload(), 2000);
|
||||||
|
} else {
|
||||||
|
showToast(data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('Host yenileme sırasında hata oluştu', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host formu sıfırla
|
||||||
|
function resetHostForm() {
|
||||||
|
document.getElementById('hostForm').reset();
|
||||||
|
document.getElementById('hostId').value = '';
|
||||||
|
document.getElementById('hostModalLabel').textContent = 'Yeni Host Ekle';
|
||||||
|
document.getElementById('port').value = '22';
|
||||||
|
document.getElementById('basePath').value = '/var/www';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host düzenle
|
||||||
|
function editHost(hostId) {
|
||||||
|
fetch(`/get-host-details/${hostId}/`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const host = data.host;
|
||||||
|
|
||||||
|
document.getElementById('hostId').value = host.id;
|
||||||
|
document.getElementById('hostName').value = host.name;
|
||||||
|
document.getElementById('hostname').value = host.hostname;
|
||||||
|
document.getElementById('port').value = host.port;
|
||||||
|
document.getElementById('username').value = host.username;
|
||||||
|
document.getElementById('basePath').value = host.base_path || '/var/www';
|
||||||
|
document.getElementById('isDefault').checked = host.is_default;
|
||||||
|
|
||||||
|
document.getElementById('hostModalLabel').textContent = 'Host Düzenle';
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('hostModal'));
|
||||||
|
modal.show();
|
||||||
|
} else {
|
||||||
|
showToast('Host bilgileri alınamadı', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('Host bilgileri alınırken hata oluştu', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host sil
|
||||||
|
function deleteHost(hostId) {
|
||||||
|
if (confirm('Bu host\'u silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.')) {
|
||||||
|
fetch(`/delete-host/${hostId}/`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showToast(data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('Host silme sırasında hata oluştu', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host logları görüntüle
|
||||||
|
function viewHostLogs(hostId) {
|
||||||
|
// Host spesifik logları göstermek için logs sayfasına yönlendir
|
||||||
|
window.location.href = `/islem-gecmisi/?host=${hostId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal içinde bağlantı testi
|
||||||
|
function testHostConnection() {
|
||||||
|
const formData = new FormData(document.getElementById('hostForm'));
|
||||||
|
|
||||||
|
showToast('Bağlantı test ediliyor...', 'info');
|
||||||
|
|
||||||
|
fetch('/test-host-connection-form/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
} else {
|
||||||
|
showToast(data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('Bağlantı testi sırasında hata oluştu', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host form submit
|
||||||
|
document.getElementById('hostForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const hostId = document.getElementById('hostId').value;
|
||||||
|
const url = hostId ? `/update-host/${hostId}/` : '/create-host/';
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('hostModal')).hide();
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showToast(data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('Host kaydederken hata oluştu', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
140
templates/ssh_manager/islem_gecmisi.html
Normal file
140
templates/ssh_manager/islem_gecmisi.html
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
{% extends 'ssh_manager/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
.log-table tbody td {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
.log-table .log-type-backup {
|
||||||
|
color: #ffc107;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.log-table .log-type-command {
|
||||||
|
color: #17a2b8;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h3>İşlem Geçmişi</h3>
|
||||||
|
<small class="text-muted">Tüm sistem işlemleri ve logları</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<input type="text" id="logSearch" class="form-control" style="max-width: 250px;" placeholder="Proje adına göre ara...">
|
||||||
|
<button class="btn btn-danger btn-sm" onclick="clearAllLogs()" title="Tüm Logları Temizle">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-bordered table-striped log-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 40px;">#</th>
|
||||||
|
<th style="width: 130px;">Tarih</th>
|
||||||
|
<th style="width: 120px;">Tip</th>
|
||||||
|
<th style="width: 150px;">Proje</th>
|
||||||
|
<th>İşlem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for log in logs %}
|
||||||
|
<tr class="log-row" data-project="{{ log.command|lower }}">
|
||||||
|
<td>{{ forloop.counter }}</td>
|
||||||
|
<td>{{ log.created_at|date:"d.m.Y H:i" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if log.log_type == 'backup' %}
|
||||||
|
<span class="log-type-backup">💾 Yedekleme</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="log-type-command">⚙️ Komut</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% 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 %}
|
||||||
|
</td>
|
||||||
|
<td title="{{ log.output }}">{{ log.command|default:log.output|truncatechars:100 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center text-muted">
|
||||||
|
<i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||||
|
<p class="mt-2">Henüz işlem geçmişi bulunmuyor</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Log arama işlevselliği
|
||||||
|
document.getElementById('logSearch').addEventListener('keyup', function() {
|
||||||
|
const searchTerm = this.value.toLowerCase().trim();
|
||||||
|
const logRows = document.querySelectorAll('.log-row');
|
||||||
|
|
||||||
|
logRows.forEach(row => {
|
||||||
|
const projectName = row.getAttribute('data-project') || '';
|
||||||
|
const command = row.cells[4].textContent.toLowerCase(); // İşlem kolonu artık 4. indekste
|
||||||
|
|
||||||
|
if (searchTerm === '' ||
|
||||||
|
projectName.includes(searchTerm) ||
|
||||||
|
command.includes(searchTerm)) {
|
||||||
|
row.style.display = '';
|
||||||
|
} else {
|
||||||
|
row.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tüm logları temizle
|
||||||
|
function clearAllLogs() {
|
||||||
|
if (!confirm('Tüm işlem geçmişini silmek istediğinizden emin misiniz?\nBu işlem geri alınamaz.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/logs/clear/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast(`✅ ${data.deleted_count} log kaydı silindi`, 'success');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showToast(`❌ ${data.message || 'Log silme işlemi başarısız!'}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showToast('❌ Log silme sırasında bir hata oluştu!', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
467
templates/ssh_manager/musteriler.html
Normal file
467
templates/ssh_manager/musteriler.html
Normal file
@ -0,0 +1,467 @@
|
|||||||
|
{% extends 'ssh_manager/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
.customer-card {
|
||||||
|
background: #23272b;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.customer-card:hover {
|
||||||
|
border-color: #4fc3f7;
|
||||||
|
box-shadow: 0 2px 8px rgba(79, 195, 247, 0.1);
|
||||||
|
}
|
||||||
|
.customer-type-badge {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.customer-projects {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h3>Müşteri Yönetimi
|
||||||
|
{% if filter_type == 'individual' %}
|
||||||
|
<small class="text-muted">- Bireysel Müşteriler</small>
|
||||||
|
{% elif filter_type == 'corporate' %}
|
||||||
|
<small class="text-muted">- Kurumsal Müşteriler</small>
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
|
<small class="text-muted">Bireysel ve kurumsal müşteri profilleri</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<input type="text" id="customerSearch" class="form-control" style="max-width: 250px;" placeholder="Müşteri ara...">
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#customerModal" onclick="resetCustomerForm()">
|
||||||
|
<i class="bi bi-plus-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{% for customer in customers %}
|
||||||
|
<div class="col-md-6 col-lg-4 customer-item" data-customer="{{ customer.get_display_name|lower }}">
|
||||||
|
<div class="customer-card">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">{{ customer.get_display_name }}</h5>
|
||||||
|
<span class="customer-type-badge badge {% if customer.customer_type == 'corporate' %}bg-info{% else %}bg-success{% endif %}">
|
||||||
|
{% if customer.customer_type == 'corporate' %}Kurumsal{% else %}Bireysel{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-sm btn-outline-info" onclick="editCustomer({{ customer.id }})" title="Düzenle">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteCustomer({{ customer.id }})" title="Sil">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="customer-info">
|
||||||
|
{% if customer.email %}
|
||||||
|
<div class="mb-1">
|
||||||
|
<i class="bi bi-envelope me-1"></i>
|
||||||
|
<small>{{ customer.email }}</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if customer.phone %}
|
||||||
|
<div class="mb-1">
|
||||||
|
<i class="bi bi-telephone me-1"></i>
|
||||||
|
<small>{{ customer.phone }}</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if customer.customer_type == 'corporate' and customer.tax_number %}
|
||||||
|
<div class="mb-1">
|
||||||
|
<i class="bi bi-building me-1"></i>
|
||||||
|
<small>Vergi No: {{ customer.tax_number }}</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="customer-projects mt-3">
|
||||||
|
<i class="bi bi-folder me-1"></i>
|
||||||
|
<small>{{ customer.project_set.count }} proje</small>
|
||||||
|
{% if customer.notes %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<i class="bi bi-chat-text me-1"></i>
|
||||||
|
<small class="text-muted">{{ customer.notes|truncatechars:50 }}</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="bi bi-people" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||||
|
<p class="mt-2">Henüz müşteri kaydı bulunmuyor</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Müşteri Modal -->
|
||||||
|
<div class="modal fade" id="customerModal" tabindex="-1" aria-labelledby="customerModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="customerForm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="customerModalLabel">Yeni Müşteri Ekle</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="customerId" name="customerId">
|
||||||
|
|
||||||
|
<!-- Müşteri Tipi Seçimi -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-bold">Müşteri Tipi</label>
|
||||||
|
<div class="d-flex gap-4 mt-2">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="customer_type" id="typeIndividual" value="individual" checked>
|
||||||
|
<label class="form-check-label" for="typeIndividual">
|
||||||
|
<i class="bi bi-person me-1"></i>Bireysel
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="customer_type" id="typeCorporate" value="corporate">
|
||||||
|
<label class="form-check-label" for="typeCorporate">
|
||||||
|
<i class="bi bi-building me-1"></i>Kurumsal
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bireysel Müşteri Alanları -->
|
||||||
|
<div id="individualFields">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="name" class="form-label">Ad *</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="surname" class="form-label">Soyad *</label>
|
||||||
|
<input type="text" class="form-control" id="surname" name="surname">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="tc_number" class="form-label">TC Kimlik No</label>
|
||||||
|
<input type="text" class="form-control" id="tc_number" name="tc_number" maxlength="11" placeholder="11 haneli TC kimlik numarası">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="birth_date" class="form-label">Doğum Tarihi</label>
|
||||||
|
<input type="date" class="form-control" id="birth_date" name="birth_date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kurumsal Müşteri Alanları -->
|
||||||
|
<div id="corporateFields" style="display: none;">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="company_name" class="form-label">Şirket Adı *</label>
|
||||||
|
<input type="text" class="form-control" id="company_name" name="company_name">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="authorized_person" class="form-label">Yetkili Kişi</label>
|
||||||
|
<input type="text" class="form-control" id="authorized_person" name="authorized_person">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="tax_number" class="form-label">Vergi No</label>
|
||||||
|
<input type="text" class="form-control" id="tax_number" name="tax_number" placeholder="10 haneli vergi numarası">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="tax_office" class="form-label">Vergi Dairesi</label>
|
||||||
|
<input type="text" class="form-control" id="tax_office" name="tax_office">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- İletişim Bilgileri -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<h6 class="text-muted mb-3">
|
||||||
|
<i class="bi bi-telephone me-1"></i>İletişim Bilgileri
|
||||||
|
</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="email" class="form-label">E-posta *</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required placeholder="ornek@email.com">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="phone" class="form-label">Telefon</label>
|
||||||
|
<input type="tel" class="form-control" id="phone" name="phone" placeholder="0555 123 45 67">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="address" class="form-label">Adres</label>
|
||||||
|
<textarea class="form-control" id="address" name="address" rows="2" placeholder="Tam adres bilgisi..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ek Bilgiler -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<h6 class="text-muted mb-3">
|
||||||
|
<i class="bi bi-chat-text me-1"></i>Ek Bilgiler
|
||||||
|
</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="notes" class="form-label">Notlar</label>
|
||||||
|
<textarea class="form-control" id="notes" name="notes" rows="3" placeholder="Müşteri hakkında notlar, özel istekler vb."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Kapat</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Kaydet</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Müşteri tipi değiştiğinde alanları göster/gizle
|
||||||
|
document.querySelectorAll('input[name="customer_type"]').forEach(radio => {
|
||||||
|
radio.addEventListener('change', function() {
|
||||||
|
const individualFields = document.getElementById('individualFields');
|
||||||
|
const corporateFields = document.getElementById('corporateFields');
|
||||||
|
|
||||||
|
if (this.value === 'individual') {
|
||||||
|
individualFields.style.display = 'block';
|
||||||
|
corporateFields.style.display = 'none';
|
||||||
|
document.getElementById('name').required = true;
|
||||||
|
document.getElementById('surname').required = true;
|
||||||
|
document.getElementById('company_name').required = false;
|
||||||
|
} else {
|
||||||
|
individualFields.style.display = 'none';
|
||||||
|
corporateFields.style.display = 'block';
|
||||||
|
document.getElementById('name').required = false;
|
||||||
|
document.getElementById('surname').required = false;
|
||||||
|
document.getElementById('company_name').required = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Müşteri arama
|
||||||
|
document.getElementById('customerSearch').addEventListener('keyup', function() {
|
||||||
|
const searchTerm = this.value.toLowerCase().trim();
|
||||||
|
const customerItems = document.querySelectorAll('.customer-item');
|
||||||
|
|
||||||
|
customerItems.forEach(item => {
|
||||||
|
const customerName = item.getAttribute('data-customer') || '';
|
||||||
|
if (searchTerm === '' || customerName.includes(searchTerm)) {
|
||||||
|
item.style.display = '';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form reset
|
||||||
|
function resetCustomerForm() {
|
||||||
|
document.getElementById('customerForm').reset();
|
||||||
|
document.getElementById('customerId').value = '';
|
||||||
|
document.getElementById('customerModalLabel').textContent = 'Yeni Müşteri Ekle';
|
||||||
|
document.getElementById('typeIndividual').checked = true;
|
||||||
|
document.getElementById('individualFields').style.display = 'block';
|
||||||
|
document.getElementById('corporateFields').style.display = 'none';
|
||||||
|
document.getElementById('name').required = true;
|
||||||
|
document.getElementById('surname').required = true;
|
||||||
|
document.getElementById('company_name').required = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Müşteri düzenle
|
||||||
|
function editCustomer(id) {
|
||||||
|
fetch(`/get-customer-details/${id}/`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const customer = data.customer;
|
||||||
|
|
||||||
|
document.getElementById('customerId').value = id;
|
||||||
|
document.getElementById('customerModalLabel').textContent = 'Müşteri Düzenle';
|
||||||
|
|
||||||
|
// Müşteri tipini seç
|
||||||
|
if (customer.customer_type === 'corporate') {
|
||||||
|
document.getElementById('typeCorporate').checked = true;
|
||||||
|
document.getElementById('corporateFields').style.display = 'block';
|
||||||
|
document.getElementById('individualFields').style.display = 'none';
|
||||||
|
document.getElementById('name').required = false;
|
||||||
|
document.getElementById('surname').required = false;
|
||||||
|
document.getElementById('company_name').required = true;
|
||||||
|
} else {
|
||||||
|
document.getElementById('typeIndividual').checked = true;
|
||||||
|
document.getElementById('individualFields').style.display = 'block';
|
||||||
|
document.getElementById('corporateFields').style.display = 'none';
|
||||||
|
document.getElementById('name').required = true;
|
||||||
|
document.getElementById('surname').required = true;
|
||||||
|
document.getElementById('company_name').required = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form alanlarını doldur
|
||||||
|
document.getElementById('name').value = customer.name || '';
|
||||||
|
document.getElementById('surname').value = customer.surname || '';
|
||||||
|
document.getElementById('email').value = customer.email || '';
|
||||||
|
document.getElementById('phone').value = customer.phone || '';
|
||||||
|
document.getElementById('address').value = customer.address || '';
|
||||||
|
document.getElementById('notes').value = customer.notes || '';
|
||||||
|
document.getElementById('tc_number').value = customer.tc_number || '';
|
||||||
|
document.getElementById('birth_date').value = customer.birth_date || '';
|
||||||
|
document.getElementById('company_name').value = customer.company_name || '';
|
||||||
|
document.getElementById('authorized_person').value = customer.authorized_person || '';
|
||||||
|
document.getElementById('tax_number').value = customer.tax_number || '';
|
||||||
|
document.getElementById('tax_office').value = customer.tax_office || '';
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('customerModal'));
|
||||||
|
modal.show();
|
||||||
|
} else {
|
||||||
|
showToast('❌ Müşteri bilgisi alınamadı!', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showToast('❌ Müşteri bilgisi alınırken hata oluştu!', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Müşteri sil
|
||||||
|
function deleteCustomer(id) {
|
||||||
|
if (confirm('Müşteriyi silmek istediğinizden emin misiniz?\nBu işlem müşteriye ait tüm projeleri de etkileyebilir.')) {
|
||||||
|
fetch(`/musteri/${id}/delete/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast('✅ Müşteri başarıyla silindi', 'success');
|
||||||
|
setTimeout(() => location.reload(), 1500);
|
||||||
|
} else {
|
||||||
|
showToast(`❌ ${data.message}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showToast('❌ Müşteri silme sırasında hata oluştu!', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form submit
|
||||||
|
document.getElementById('customerForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const id = document.getElementById('customerId').value;
|
||||||
|
const url = id ? `/update-customer/${id}/` : '/musteri/create/';
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast('✅ Müşteri başarıyla kaydedildi', 'success');
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('customerModal'));
|
||||||
|
if (modal) modal.hide();
|
||||||
|
setTimeout(() => location.reload(), 1500);
|
||||||
|
} else {
|
||||||
|
showToast(`❌ ${data.message}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showToast('❌ Müşteri kaydedilirken hata oluştu!', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cookie helper
|
||||||
|
function getCookie(name) {
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toast helper
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const toastBody = document.getElementById('mainToastBody');
|
||||||
|
const toastEl = document.getElementById('mainToast');
|
||||||
|
|
||||||
|
toastBody.textContent = message;
|
||||||
|
|
||||||
|
// Toast rengini ayarla
|
||||||
|
toastEl.className = 'toast align-items-center border-0';
|
||||||
|
if (type === 'success') {
|
||||||
|
toastEl.classList.add('text-bg-success');
|
||||||
|
} else if (type === 'error') {
|
||||||
|
toastEl.classList.add('text-bg-danger');
|
||||||
|
} else {
|
||||||
|
toastEl.classList.add('text-bg-info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = new bootstrap.Toast(toastEl);
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sayfa yüklendiğinde URL parametrelerini kontrol et
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const newCustomerType = urlParams.get('new');
|
||||||
|
|
||||||
|
if (newCustomerType && (newCustomerType === 'individual' || newCustomerType === 'corporate')) {
|
||||||
|
resetCustomerForm();
|
||||||
|
|
||||||
|
if (newCustomerType === 'corporate') {
|
||||||
|
document.getElementById('typeCorporate').checked = true;
|
||||||
|
document.getElementById('corporateFields').style.display = 'block';
|
||||||
|
document.getElementById('individualFields').style.display = 'none';
|
||||||
|
document.getElementById('name').required = false;
|
||||||
|
document.getElementById('surname').required = false;
|
||||||
|
document.getElementById('company_name').required = true;
|
||||||
|
} else {
|
||||||
|
document.getElementById('typeIndividual').checked = true;
|
||||||
|
document.getElementById('individualFields').style.display = 'block';
|
||||||
|
document.getElementById('corporateFields').style.display = 'none';
|
||||||
|
document.getElementById('name').required = true;
|
||||||
|
document.getElementById('surname').required = true;
|
||||||
|
document.getElementById('company_name').required = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('customerModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// URL'yi temizle
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
1081
templates/ssh_manager/project_list.html
Normal file
1081
templates/ssh_manager/project_list.html
Normal file
File diff suppressed because it is too large
Load Diff
554
templates/ssh_manager/projeler.html
Normal file
554
templates/ssh_manager/projeler.html
Normal file
@ -0,0 +1,554 @@
|
|||||||
|
{% extends 'ssh_manager/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Projeler - Hosting Yönetim Paneli{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Projeler Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-1">
|
||||||
|
<i class="bi bi-folder-fill me-2"></i>Projeler
|
||||||
|
</h3>
|
||||||
|
<small class="text-muted">Tüm hosting projeleri ve yönetim işlemleri</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<input type="text" id="projectSearch" class="form-control" style="max-width: 250px;" placeholder="Proje ara...">
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProjectModal">
|
||||||
|
<i class="bi bi-plus-circle"></i> Yeni Proje
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Projeler Tablosu -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Proje Bilgileri</th>
|
||||||
|
<th>Klasör & Disk</th>
|
||||||
|
<th>Site Durumu</th>
|
||||||
|
<th>Son Yedekleme</th>
|
||||||
|
<th class="actions">İşlemler</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for project in projects %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ forloop.counter }}</td>
|
||||||
|
<td style="line-height: 1.3;">
|
||||||
|
<div class="fw-bold">{{ project.name }}</div>
|
||||||
|
{% if project.customer %}
|
||||||
|
<small class="text-info">
|
||||||
|
<i class="bi bi-person me-1"></i>{{ project.customer.get_display_name }}
|
||||||
|
</small><br>
|
||||||
|
{% endif %}
|
||||||
|
{% if project.ssh_credential %}
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-server me-1"></i>{{ project.ssh_credential.hostname }}
|
||||||
|
</small>
|
||||||
|
{% else %}
|
||||||
|
<small class="text-danger">SSH credential yok</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="line-height: 1.3;">
|
||||||
|
<div class="fw-bold">{{ project.folder_name }}</div>
|
||||||
|
{% if project.disk_usage %}
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-hdd me-1"></i>{{ project.disk_usage }}
|
||||||
|
</small>
|
||||||
|
{% else %}
|
||||||
|
<small class="text-muted">-</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if project.url %}
|
||||||
|
<div class="d-flex align-items-center mb-1">
|
||||||
|
{% if project.is_site_active %}
|
||||||
|
<span class="badge bg-success me-2" title="Site aktif">
|
||||||
|
<i class="bi bi-check-circle"></i> Aktif
|
||||||
|
</span>
|
||||||
|
{% elif project.last_site_check %}
|
||||||
|
<span class="badge bg-danger me-2" title="Site pasif">
|
||||||
|
<i class="bi bi-x-circle"></i> Pasif
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary me-2" title="Kontrol edilmemiş">
|
||||||
|
<i class="bi bi-question-circle"></i> Bilinmiyor
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<a href="{% if not project.url|slice:':4' == 'http' %}http://{% endif %}{{ project.url }}" target="_blank" class="text-decoration-none small" style="color: #4fc3f7;">
|
||||||
|
<i class="bi bi-globe me-1"></i>{{ project.url }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">URL tanımlanmamış</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if project.last_backup %}
|
||||||
|
<small>{{ project.last_backup|date:"d.m.Y H:i" }}</small>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-warning">Yedek alınmamış</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="actions">
|
||||||
|
<i class="action-icon edit bi bi-pencil" onclick="editProject({{ project.id }})" title="Düzenle"></i>
|
||||||
|
<i class="action-icon delete bi bi-trash" onclick="deleteProject({{ project.id }})" title="Sil"></i>
|
||||||
|
<i class="action-icon backup bi bi-cloud-arrow-up" onclick="backupProject({{ project.id }})" title="Yedekle"></i>
|
||||||
|
<i class="action-icon logs bi bi-journal-text" onclick="showLogsByProject({{ project.id }})" title="Loglar"></i>
|
||||||
|
{% if project.url %}
|
||||||
|
<i class="action-icon site bi bi-globe-americas" onclick="checkSiteStatus({{ project.id }})" title="Site Kontrol"></i>
|
||||||
|
<i class="action-icon meta bi bi-key" onclick="showMetaKey({{ project.id }})" title="Meta Key"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center text-muted py-4">
|
||||||
|
<i class="bi bi-folder" style="font-size: 2rem;"></i>
|
||||||
|
<div class="mt-2">Henüz proje eklenmemiş</div>
|
||||||
|
<button class="btn btn-sm btn-outline-primary mt-2" data-bs-toggle="modal" data-bs-target="#addProjectModal">
|
||||||
|
İlk projeyi ekle
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Proje Ekleme/Düzenleme Modal -->
|
||||||
|
<div class="modal fade" id="addProjectModal" tabindex="-1" aria-labelledby="addProjectModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="projectForm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addProjectModalLabel">Yeni Proje Ekle</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Kapat"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="projectId" name="projectId">
|
||||||
|
|
||||||
|
<!-- Proje Bilgileri -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-3">
|
||||||
|
<i class="bi bi-folder me-1"></i>Proje Bilgileri
|
||||||
|
</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="name" class="form-label">Proje Adı *</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required placeholder="Proje adı">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="folder_name" class="form-label">Klasör Adı *</label>
|
||||||
|
<input type="text" class="form-control" id="folder_name" name="folder_name" required placeholder="Sunucudaki klasör adı">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="customer" class="form-label">Müşteri</label>
|
||||||
|
<select class="form-select" id="customer" name="customer">
|
||||||
|
<option value="">Müşteri Seçiniz (Opsiyonel)</option>
|
||||||
|
{% for customer in customers %}
|
||||||
|
<option value="{{ customer.id }}">{{ customer.get_display_name }} ({{ customer.get_customer_type_display }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="ssh_credential" class="form-label">Host *</label>
|
||||||
|
<select class="form-select" id="ssh_credential" name="ssh_credential" required>
|
||||||
|
<option value="">Host Seçiniz</option>
|
||||||
|
{% for host in ssh_credentials %}
|
||||||
|
<option value="{{ host.id }}">{{ host.name|default:host.hostname }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Site Bilgileri -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-muted mb-3">
|
||||||
|
<i class="bi bi-globe me-1"></i>Site Bilgileri
|
||||||
|
</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="url" class="form-label">Site URL'i</label>
|
||||||
|
<input type="url" class="form-control" id="url" name="url" placeholder="https://example.com">
|
||||||
|
<small class="text-muted">Site aktiflik kontrolü için gerekli</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Kapat</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Kaydet</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log Modal -->
|
||||||
|
<div class="modal fade" id="logsModal" tabindex="-1" aria-labelledby="logsModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="logsModalLabel">
|
||||||
|
<i class="bi bi-journal-text me-2"></i>Proje Logları
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Kapat"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="logsContent">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Yükleniyor...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">Loglar yükleniyor...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger" id="clearLogsBtn" onclick="clearProjectLogs()" style="display: none;">
|
||||||
|
<i class="bi bi-trash"></i> Logları Temizle
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Kapat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Meta Key Modal -->
|
||||||
|
<div class="modal fade" id="metaKeyModal" tabindex="-1" aria-labelledby="metaKeyModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="metaKeyModalLabel">
|
||||||
|
<i class="bi bi-key me-2"></i>Site Doğrulama Meta Key
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Kapat"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="metaKeyContent">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Yükleniyor...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">Meta key bilgileri yükleniyor...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Kapat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Tüm JavaScript fonksiyonları buradan project_list.html'den kopyalanacak
|
||||||
|
// Proje düzenleme fonksiyonu
|
||||||
|
window.editProject = function(id) {
|
||||||
|
fetch(`/get-project-details/${id}/`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('projectId').value = id;
|
||||||
|
document.getElementById('name').value = data.project.name;
|
||||||
|
document.getElementById('folder_name').value = data.project.folder_name;
|
||||||
|
document.getElementById('url').value = data.project.url || '';
|
||||||
|
document.getElementById('ssh_credential').value = data.project.ssh_credential_id || '';
|
||||||
|
document.getElementById('customer').value = data.project.customer_id || '';
|
||||||
|
|
||||||
|
document.getElementById('addProjectModalLabel').innerText = 'Proje Düzenle';
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('addProjectModal'));
|
||||||
|
modal.show();
|
||||||
|
} else {
|
||||||
|
showToast('❌ Proje bilgisi alınamadı!', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('❌ Proje bilgisi alınırken hata oluştu!', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proje Sil
|
||||||
|
window.deleteProject = function(id) {
|
||||||
|
if (confirm('Projeyi silmek istediğinize emin misiniz?')) {
|
||||||
|
fetch(`/delete_project/${id}/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRFToken': getCookie('csrftoken') }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast('✅ Proje başarıyla silindi', 'success');
|
||||||
|
setTimeout(() => location.reload(), 1200);
|
||||||
|
} else {
|
||||||
|
showToast(`❌ ${data.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proje Yedekle
|
||||||
|
window.backupProject = function(id) {
|
||||||
|
if (confirm('Projeyi yedeklemek istiyor musunuz?')) {
|
||||||
|
showToast('🔄 Yedekleme başlatılıyor...', 'info');
|
||||||
|
|
||||||
|
fetch(`/backup-project/${id}/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRFToken': getCookie('csrftoken') }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast('✅ Yedekleme başarılı', 'success');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.showLogsByProject(id);
|
||||||
|
}, 800);
|
||||||
|
} else {
|
||||||
|
showToast(`❌ ${data.message}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('❌ Yedekleme hatası!', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log görüntüleme
|
||||||
|
window.showLogsByProject = function(projectId) {
|
||||||
|
window.currentProjectId = projectId;
|
||||||
|
|
||||||
|
fetch(`/project/${projectId}/backup-logs/`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
let html = '';
|
||||||
|
let hasLogs = false;
|
||||||
|
|
||||||
|
if (data.success && data.logs.length > 0) {
|
||||||
|
hasLogs = true;
|
||||||
|
html = '<div class="log-list">';
|
||||||
|
data.logs.forEach(function(log) {
|
||||||
|
let statusBadge = log.status ?
|
||||||
|
'<span class="badge bg-success me-2"><i class="bi bi-check-circle"></i> Başarılı</span>' :
|
||||||
|
'<span class="badge bg-danger me-2"><i class="bi bi-x-circle"></i> Hata</span>';
|
||||||
|
|
||||||
|
let typeIcon = '';
|
||||||
|
if (log.log_type === 'backup') {
|
||||||
|
typeIcon = '<i class="bi bi-cloud-arrow-up text-warning me-2"></i>';
|
||||||
|
} else if (log.log_type === 'command') {
|
||||||
|
typeIcon = '<i class="bi bi-terminal text-info me-2"></i>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="log-entry mb-3 p-3" style="background: rgba(255,255,255,0.05); border-radius: 8px; border-left: 3px solid ${log.status ? '#28a745' : '#dc3545'};">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div>${statusBadge}${typeIcon}<strong>${log.command}</strong></div>
|
||||||
|
<small class="text-muted">${log.created_at}</small>
|
||||||
|
</div>
|
||||||
|
<div class="log-output" style="font-family: monospace; font-size: 0.9em; color: #bdbdbd;">
|
||||||
|
${log.output.replace(/\n/g, '<br>')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
} else {
|
||||||
|
html = `
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-journal-text" style="font-size: 2rem; color: #6c757d;"></i>
|
||||||
|
<p class="text-muted mt-2">Bu projeye ait log bulunamadı</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('logsContent').innerHTML = html;
|
||||||
|
|
||||||
|
const clearBtn = document.getElementById('clearLogsBtn');
|
||||||
|
clearBtn.style.display = hasLogs ? 'inline-block' : 'none';
|
||||||
|
|
||||||
|
const logsModal = new bootstrap.Modal(document.getElementById('logsModal'));
|
||||||
|
logsModal.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Site durumu kontrol
|
||||||
|
window.checkSiteStatus = function(projectId) {
|
||||||
|
showToast('🔄 Site kontrol ediliyor...', 'info');
|
||||||
|
|
||||||
|
fetch(`/project/${projectId}/check-site/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const statusText = data.status ? '✅ Site Aktif' : '❌ Site Pasif';
|
||||||
|
showToast(statusText, data.status ? 'success' : 'error');
|
||||||
|
setTimeout(() => location.reload(), 1500);
|
||||||
|
} else {
|
||||||
|
showToast(`❌ ${data.message}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('❌ Kontrol hatası!', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meta key göster
|
||||||
|
window.showMetaKey = function(projectId) {
|
||||||
|
fetch(`/project/${projectId}/meta-key/`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const content = `
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6><i class="bi bi-info-circle"></i> Kullanım Talimatları:</h6>
|
||||||
|
<p>Bu meta tag'ı sitenizin <code><head></code> bölümüne ekleyin:</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><strong>Meta Key:</strong></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" value="${data.meta_key}" readonly>
|
||||||
|
<button class="btn btn-outline-secondary" onclick="copyToClipboard('${data.meta_key}')">
|
||||||
|
<i class="bi bi-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><strong>HTML Meta Tag:</strong></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<textarea class="form-control" rows="2" readonly>${data.meta_tag}</textarea>
|
||||||
|
<button class="btn btn-outline-secondary" onclick="copyToClipboard('${data.meta_tag}')">
|
||||||
|
<i class="bi bi-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<small><i class="bi bi-exclamation-triangle"></i> Meta tag'ı ekledikten sonra "Site Kontrol" butonuyla doğrulama yapabilirsiniz.</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('metaKeyContent').innerHTML = content;
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('metaKeyModal'));
|
||||||
|
modal.show();
|
||||||
|
} else {
|
||||||
|
showToast(`❌ ${data.message}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('❌ Meta key alınırken hata oluştu!', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clipboard'a kopyala
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
showToast('📋 Panoya kopyalandı!', 'success');
|
||||||
|
}).catch(err => {
|
||||||
|
showToast('❌ Kopyalama hatası!', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log temizleme
|
||||||
|
function clearProjectLogs() {
|
||||||
|
if (!window.currentProjectId) {
|
||||||
|
showToast('Proje ID bulunamadı!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Bu projenin tüm loglarını silmek istediğinizden emin misiniz?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/project/${window.currentProjectId}/clear-logs/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('logsModal'));
|
||||||
|
if (modal) modal.hide();
|
||||||
|
|
||||||
|
showToast(`✅ ${data.deleted_count} log kaydı silindi`, 'success');
|
||||||
|
window.currentProjectId = null;
|
||||||
|
} else {
|
||||||
|
showToast(`❌ ${data.message || 'Log silme işlemi başarısız!'}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('❌ Log silme sırasında bir hata oluştu!', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proje Form Submit
|
||||||
|
document.getElementById('projectForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = document.getElementById('projectId').value;
|
||||||
|
const url = id ? `/update-project/${id}/` : '/project/create/';
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRFToken': getCookie('csrftoken') },
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast('✅ Proje başarıyla kaydedildi', 'success');
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('addProjectModal'));
|
||||||
|
if (modal) modal.hide();
|
||||||
|
setTimeout(() => location.reload(), 1200);
|
||||||
|
} else {
|
||||||
|
showToast(`❌ ${data.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Proje Arama
|
||||||
|
document.getElementById('projectSearch').addEventListener('keyup', function() {
|
||||||
|
const searchTerm = this.value.toLowerCase().trim();
|
||||||
|
const projectRows = document.querySelectorAll('tbody tr');
|
||||||
|
|
||||||
|
if (searchTerm.length < 2) {
|
||||||
|
projectRows.forEach(row => {
|
||||||
|
if (row.cells.length > 1) {
|
||||||
|
row.style.display = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projectRows.forEach(row => {
|
||||||
|
if (row.cells.length > 1) {
|
||||||
|
const projectInfo = row.cells[1].textContent.toLowerCase();
|
||||||
|
const folderInfo = row.cells[2].textContent.toLowerCase();
|
||||||
|
const siteInfo = row.cells[3].textContent.toLowerCase();
|
||||||
|
|
||||||
|
if (projectInfo.includes(searchTerm) || folderInfo.includes(searchTerm) || siteInfo.includes(searchTerm)) {
|
||||||
|
row.style.display = '';
|
||||||
|
} else {
|
||||||
|
row.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal reset
|
||||||
|
document.getElementById('addProjectModal').addEventListener('hidden.bs.modal', function () {
|
||||||
|
document.getElementById('projectForm').reset();
|
||||||
|
document.getElementById('projectId').value = '';
|
||||||
|
document.getElementById('addProjectModalLabel').textContent = 'Yeni Proje Ekle';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
426
templates/ssh_manager/yedeklemeler.html
Normal file
426
templates/ssh_manager/yedeklemeler.html
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
{% extends 'ssh_manager/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Yedeklemeler - Hosting Yönetim Paneli{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Yedeklemeler Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-1">
|
||||||
|
<i class="bi bi-cloud-arrow-up me-2"></i>Yedeklemeler
|
||||||
|
</h3>
|
||||||
|
<small class="text-muted">Proje yedekleme işlemleri ve S3 yönetimi</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-info" onclick="refreshBackupStatus()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> Durumu Yenile
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#backupModal">
|
||||||
|
<i class="bi bi-cloud-arrow-up"></i> Yeni Yedekleme
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success" onclick="startAllBackups()">
|
||||||
|
<i class="bi bi-cloud-arrow-up-fill"></i> Tümünü Yedekle
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Yedekleme İstatistikleri -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="icon-box bg-info bg-opacity-10 mx-auto mb-3">
|
||||||
|
<i class="bi bi-cloud-arrow-up text-info"></i>
|
||||||
|
</div>
|
||||||
|
<h4 class="mb-1">{{ total_backups|default:0 }}</h4>
|
||||||
|
<p class="text-muted mb-0">Toplam Yedekleme</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="icon-box bg-success bg-opacity-10 mx-auto mb-3">
|
||||||
|
<i class="bi bi-check-circle text-success"></i>
|
||||||
|
</div>
|
||||||
|
<h4 class="mb-1">{{ successful_backups|default:0 }}</h4>
|
||||||
|
<p class="text-muted mb-0">Başarılı</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="icon-box bg-danger bg-opacity-10 mx-auto mb-3">
|
||||||
|
<i class="bi bi-x-circle text-danger"></i>
|
||||||
|
</div>
|
||||||
|
<h4 class="mb-1">{{ failed_backups|default:0 }}</h4>
|
||||||
|
<p class="text-muted mb-0">Başarısız</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="icon-box bg-warning bg-opacity-10 mx-auto mb-3">
|
||||||
|
<i class="bi bi-clock text-warning"></i>
|
||||||
|
</div>
|
||||||
|
<h4 class="mb-1">
|
||||||
|
{% if backup_logs %}
|
||||||
|
{{ backup_logs.0.created_at|date:"d.m H:i" }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</h4>
|
||||||
|
<p class="text-muted mb-0">Son Yedekleme</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Yedekleme Geçmişi -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="bi bi-clock-history me-2"></i>Yedekleme Geçmişi
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if backup_logs %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tarih</th>
|
||||||
|
<th>Proje</th>
|
||||||
|
<th>Host</th>
|
||||||
|
<th>Durum</th>
|
||||||
|
<th>Detay</th>
|
||||||
|
<th class="actions">İşlemler</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for log in backup_logs %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span>{{ log.created_at|date:"d.m.Y" }}</span><br>
|
||||||
|
<small class="text-muted">{{ log.created_at|date:"H:i:s" }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if log.ssh_credential %}
|
||||||
|
<strong>{{ log.ssh_credential.name }}</strong>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Genel</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if log.ssh_credential %}
|
||||||
|
<small>{{ log.ssh_credential.hostname }}</small>
|
||||||
|
{% else %}
|
||||||
|
<small class="text-muted">-</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if log.status == 'success' %}bg-success{% elif log.status == 'error' %}bg-danger{% else %}bg-warning{% endif %} fs-6">
|
||||||
|
{% if log.status == 'success' %}
|
||||||
|
<i class="bi bi-check-circle me-1"></i>Başarılı
|
||||||
|
{% elif log.status == 'error' %}
|
||||||
|
<i class="bi bi-x-circle me-1"></i>Hata
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-clock me-1"></i>Bekliyor
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if log.output %}
|
||||||
|
<button class="btn btn-sm btn-outline-info" onclick="showLogDetail('{{ log.output|escapejs }}', '{{ log.created_at|date:"d.m.Y H:i" }}')">
|
||||||
|
<i class="bi bi-eye"></i> Detay
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
{% if log.ssh_credential %}
|
||||||
|
<button class="btn btn-sm btn-outline-warning" onclick="retryBackup({{ log.ssh_credential.id }})" title="Tekrar Dene">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteLog({{ log.id }})" title="Kaydı Sil">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-cloud-arrow-up" style="font-size: 3rem; color: #6c757d;"></i>
|
||||||
|
<h5 class="mt-3 text-muted">Henüz yedekleme yapılmamış</h5>
|
||||||
|
<p class="text-muted">İlk yedeklemenizi başlatmak için yukarıdaki butonu kullanın</p>
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#backupModal">
|
||||||
|
<i class="bi bi-cloud-arrow-up"></i> İlk Yedeklemeyi Başlat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Yedekleme Modal -->
|
||||||
|
<div class="modal fade" id="backupModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content bg-dark border-secondary">
|
||||||
|
<div class="modal-header border-secondary">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="bi bi-cloud-arrow-up me-2"></i>Yeni Yedekleme
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="backupForm">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="backupProject" class="form-label">Proje Seçin</label>
|
||||||
|
<select class="form-select bg-dark text-white border-secondary" id="backupProject" name="project_id" required>
|
||||||
|
<option value="">Proje seçin...</option>
|
||||||
|
{% load ssh_manager_tags %}
|
||||||
|
{% get_projects as projects %}
|
||||||
|
{% for project in projects %}
|
||||||
|
<option value="{{ project.id }}">{{ project.name }} ({{ project.ssh_credential.hostname }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="backupType" class="form-label">Yedekleme Türü</label>
|
||||||
|
<select class="form-select bg-dark text-white border-secondary" id="backupType" name="backup_type">
|
||||||
|
<option value="full">Tam Yedekleme</option>
|
||||||
|
<option value="files">Sadece Dosyalar</option>
|
||||||
|
<option value="database">Sadece Veritabanı</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="compressBackup" name="compress" checked>
|
||||||
|
<label class="form-check-label" for="compressBackup">
|
||||||
|
Sıkıştırılmış yedekleme (.tar.gz)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="backupNote" class="form-label">Not (Opsiyonel)</label>
|
||||||
|
<textarea class="form-control bg-dark text-white border-secondary" id="backupNote" name="note" rows="3" placeholder="Yedekleme hakkında not..."></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-secondary">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">İptal</button>
|
||||||
|
<button type="button" class="btn btn-warning" onclick="startBackup()">
|
||||||
|
<i class="bi bi-cloud-arrow-up"></i> Yedeklemeyi Başlat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log Detay Modal -->
|
||||||
|
<div class="modal fade" id="logDetailModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content bg-dark border-secondary">
|
||||||
|
<div class="modal-header border-secondary">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="bi bi-file-text me-2"></i>Yedekleme Detayı
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="logDetailContent" style="font-family: monospace; background: #1a1d23; padding: 1rem; border-radius: 0.375rem; max-height: 400px; overflow-y: auto;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-secondary">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Kapat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon-box {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box i {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #23272b;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: #1a1d23;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark {
|
||||||
|
--bs-table-bg: #23272b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Yedekleme durumunu yenile
|
||||||
|
function refreshBackupStatus() {
|
||||||
|
showToast('Yedekleme durumu kontrol ediliyor...', 'info');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tüm projeleri yedekle
|
||||||
|
function startAllBackups() {
|
||||||
|
if (!confirm('Tüm projelerin yedeklenmesi uzun sürebilir. Devam etmek istediğinizden emin misiniz?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('Toplu yedekleme başlatılıyor...', 'info');
|
||||||
|
|
||||||
|
fetch('/backup-all-projects/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
setTimeout(() => location.reload(), 2000);
|
||||||
|
} else {
|
||||||
|
showToast(data.message || 'Toplu yedekleme başlatılamadı', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Toplu yedekleme hatası:', error);
|
||||||
|
showToast('Toplu yedekleme hatası', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tek proje yedekleme başlat
|
||||||
|
function startBackup() {
|
||||||
|
const form = document.getElementById('backupForm');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
if (!formData.get('project_id')) {
|
||||||
|
showToast('Lütfen bir proje seçin', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('Yedekleme başlatılıyor...', 'info');
|
||||||
|
|
||||||
|
fetch('/start-backup/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('backupModal')).hide();
|
||||||
|
setTimeout(() => location.reload(), 2000);
|
||||||
|
} else {
|
||||||
|
showToast(data.message || 'Yedekleme başlatılamadı', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Yedekleme hatası:', error);
|
||||||
|
showToast('Yedekleme hatası', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yedeklemeyi tekrar dene
|
||||||
|
function retryBackup(projectId) {
|
||||||
|
if (!confirm('Bu projenin yedeklenmesini tekrar denemek istediğinizden emin misiniz?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('Yedekleme tekrar deneniyor...', 'info');
|
||||||
|
|
||||||
|
fetch('/retry-backup/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
project_id: projectId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
setTimeout(() => location.reload(), 2000);
|
||||||
|
} else {
|
||||||
|
showToast(data.message || 'Yedekleme tekrar denenemedi', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Yedekleme tekrar deneme hatası:', error);
|
||||||
|
showToast('Yedekleme tekrar deneme hatası', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log detayını göster
|
||||||
|
function showLogDetail(logOutput, logDate) {
|
||||||
|
document.getElementById('logDetailContent').innerHTML = logOutput.replace(/\n/g, '<br>');
|
||||||
|
document.querySelector('#logDetailModal .modal-title').innerHTML =
|
||||||
|
'<i class="bi bi-file-text me-2"></i>Yedekleme Detayı - ' + logDate;
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('logDetailModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log kaydını sil
|
||||||
|
function deleteLog(logId) {
|
||||||
|
if (!confirm('Bu log kaydını silmek istediğinizden emin misiniz?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/delete-log/${logId}/`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast('Log kaydı silindi', 'success');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showToast(data.message || 'Log kaydı silinemedi', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Log silme hatası:', error);
|
||||||
|
showToast('Log silme hatası', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
0
yonetim/__init__.py
Normal file
0
yonetim/__init__.py
Normal file
16
yonetim/asgi.py
Normal file
16
yonetim/asgi.py
Normal file
@ -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()
|
||||||
131
yonetim/settings.py
Normal file
131
yonetim/settings.py
Normal file
@ -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'
|
||||||
23
yonetim/urls.py
Normal file
23
yonetim/urls.py
Normal file
@ -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')),
|
||||||
|
]
|
||||||
16
yonetim/wsgi.py
Normal file
16
yonetim/wsgi.py
Normal file
@ -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()
|
||||||
Reference in New Issue
Block a user