Files
hostpanel/ssh_manager/ssh_client.py
ilkeral f4ee7a2d0b yeni
2025-08-08 07:24:25 +03:00

646 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"""
import traceback
try:
logger.info(f"SSH bağlantısı başlatılıyor: {self.ssh_credential.hostname}:{self.ssh_credential.port or 22}")
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# Bağlantı parametrelerini logla
logger.info(f"Bağlantı parametreleri:")
logger.info(f" Host: {self.ssh_credential.hostname}")
logger.info(f" Port: {self.ssh_credential.port or 22}")
logger.info(f" Username: {self.ssh_credential.username}")
# SSH timeout değeri ekle
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,
timeout=30 # Timeout değeri ekle
)
logger.info(f"SSH bağlantısı başarıyla kuruldu: {self.ssh_credential.hostname}")
return True
except Exception as e:
error_details = traceback.format_exc()
logger.error(f'SSH bağlantı hatası: {str(e)}')
logger.error(f'SSH bağlantı hata detayları:\n{error_details}')
raise Exception(f"SSH bağlantısı kurulamadı: {str(e)}")
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
"""
import traceback
try:
logger.info(f"SSH komutu çalıştırılıyor: {command}")
if not self.client:
logger.info("SSH istemcisi yok, yeniden bağlanılıyor...")
self.connect()
if not self.client:
raise Exception("SSH istemcisi oluşturulamadı")
logger.info("Komut çalıştırılıyor...")
stdin, stdout, stderr = self.client.exec_command(command, timeout=120) # Timeout ekle
logger.info("Komut çalıştırıldı, çıkış kodu bekleniyor...")
# Timeout ile çıkış kodu bekle
import select
channel = stdout.channel
status_ready = select.select([channel], [], [], 120) # 120 saniye timeout
if not status_ready[0]:
logger.error("Komut zaman aşımına uğradı!")
raise Exception("Komut zaman aşımına uğradı")
exit_status = stdout.channel.recv_exit_status()
logger.info(f"Komut çıkış kodu: {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ı
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ıı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