From 342f1314c7eaf438958ad736b9d0e62183b87086 Mon Sep 17 00:00:00 2001 From: ilkeral Date: Mon, 21 Jul 2025 13:49:36 +0300 Subject: [PATCH] yeni --- .idea/.gitignore | 8 + .idea/deployment.xml | 14 + .idea/inspectionProfiles/Project_Default.xml | 24 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/webServers.xml | 14 + .idea/yonetim.iml | 30 + build/Dockerfile | 65 + build/init.sh | 56 + build/requirements.txt | 48 + db.sqlite3 | Bin 0 -> 1081344 bytes docker-compose.yml | 31 + manage.py | 22 + requirements.txt | Bin 0 -> 352 bytes ssh_manager/2backup.py | 67 + ssh_manager/__init__.py | 1 + ssh_manager/admin.py | 71 + ssh_manager/apps.py | 24 + ssh_manager/backup.py | 626 +++++ ssh_manager/middleware.py | 11 + ssh_manager/migrations/0001_initial.py | 54 + .../migrations/0002_default_ssh_credential.py | 20 + ...ve_project_path_sshcredential_base_path.py | 23 + ...d_at_alter_project_folder_name_and_more.py | 33 + ...er_project_options_project_url_and_more.py | 39 + ...sage_alter_project_folder_name_and_more.py | 39 + ...ackup_alter_project_disk_usage_and_more.py | 38 + ...il_project_image_project_phone_and_more.py | 33 + ...ect_email_remove_project_image_and_more.py | 40 + .../0010_sshcredential_disk_usage.py | 18 + .../0011_customer_project_customer.py | 46 + ...shcredential_connection_status_and_more.py | 38 + ssh_manager/migrations/__init__.py | 1 + ssh_manager/models.py | 198 ++ ssh_manager/settings.py | 13 + ssh_manager/signals.py | 13 + ssh_manager/ssh_client.py | 607 ++++ ssh_manager/ssh_manager.py | 19 + ssh_manager/templatetags/__init__.py | 0 ssh_manager/templatetags/ssh_manager_tags.py | 9 + ssh_manager/urls.py | 79 + ssh_manager/utils.py | 146 + ssh_manager/views.py | 2494 +++++++++++++++++ templates/ssh_manager/base.html | 506 ++++ templates/ssh_manager/dashboard.html | 445 +++ templates/ssh_manager/host_yonetimi.html | 359 +++ templates/ssh_manager/islem_gecmisi.html | 140 + templates/ssh_manager/musteriler.html | 467 +++ templates/ssh_manager/project_list.html | 1081 +++++++ templates/ssh_manager/projeler.html | 554 ++++ templates/ssh_manager/yedeklemeler.html | 426 +++ yonetim/__init__.py | 0 yonetim/asgi.py | 16 + yonetim/settings.py | 131 + yonetim/urls.py | 23 + yonetim/wsgi.py | 16 + 57 files changed, 9297 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/deployment.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/webServers.xml create mode 100644 .idea/yonetim.iml create mode 100644 build/Dockerfile create mode 100644 build/init.sh create mode 100644 build/requirements.txt create mode 100644 db.sqlite3 create mode 100644 docker-compose.yml create mode 100644 manage.py create mode 100644 requirements.txt create mode 100644 ssh_manager/2backup.py create mode 100644 ssh_manager/__init__.py create mode 100644 ssh_manager/admin.py create mode 100644 ssh_manager/apps.py create mode 100644 ssh_manager/backup.py create mode 100644 ssh_manager/middleware.py create mode 100644 ssh_manager/migrations/0001_initial.py create mode 100644 ssh_manager/migrations/0002_default_ssh_credential.py create mode 100644 ssh_manager/migrations/0003_remove_project_path_sshcredential_base_path.py create mode 100644 ssh_manager/migrations/0004_project_updated_at_alter_project_folder_name_and_more.py create mode 100644 ssh_manager/migrations/0005_alter_project_options_project_url_and_more.py create mode 100644 ssh_manager/migrations/0006_project_disk_usage_alter_project_folder_name_and_more.py create mode 100644 ssh_manager/migrations/0007_project_last_backup_alter_project_disk_usage_and_more.py create mode 100644 ssh_manager/migrations/0008_project_email_project_image_project_phone_and_more.py create mode 100644 ssh_manager/migrations/0009_remove_project_email_remove_project_image_and_more.py create mode 100644 ssh_manager/migrations/0010_sshcredential_disk_usage.py create mode 100644 ssh_manager/migrations/0011_customer_project_customer.py create mode 100644 ssh_manager/migrations/0012_sshcredential_connection_status_and_more.py create mode 100644 ssh_manager/migrations/__init__.py create mode 100644 ssh_manager/models.py create mode 100644 ssh_manager/settings.py create mode 100644 ssh_manager/signals.py create mode 100644 ssh_manager/ssh_client.py create mode 100644 ssh_manager/ssh_manager.py create mode 100644 ssh_manager/templatetags/__init__.py create mode 100644 ssh_manager/templatetags/ssh_manager_tags.py create mode 100644 ssh_manager/urls.py create mode 100644 ssh_manager/utils.py create mode 100644 ssh_manager/views.py create mode 100644 templates/ssh_manager/base.html create mode 100644 templates/ssh_manager/dashboard.html create mode 100644 templates/ssh_manager/host_yonetimi.html create mode 100644 templates/ssh_manager/islem_gecmisi.html create mode 100644 templates/ssh_manager/musteriler.html create mode 100644 templates/ssh_manager/project_list.html create mode 100644 templates/ssh_manager/projeler.html create mode 100644 templates/ssh_manager/yedeklemeler.html create mode 100644 yonetim/__init__.py create mode 100644 yonetim/asgi.py create mode 100644 yonetim/settings.py create mode 100644 yonetim/urls.py create mode 100644 yonetim/wsgi.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 0000000..133083e --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..117ec52 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..776a1a3 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..48b881b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/webServers.xml b/.idea/webServers.xml new file mode 100644 index 0000000..04305f9 --- /dev/null +++ b/.idea/webServers.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/.idea/yonetim.iml b/.idea/yonetim.iml new file mode 100644 index 0000000..b2cb4aa --- /dev/null +++ b/.idea/yonetim.iml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..6f61546 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,65 @@ +FROM python:3.11.12-alpine + +# Install zip and other necessary packages +RUN apk add --no-cache \ + zip \ + unzip \ + tar \ + gzip \ + bzip2 \ + xz \ + p7zip \ + sudo \ + shadow \ + openssh-client \ + rsync \ + curl \ + wget \ + bash \ + sshpass \ + git \ + su-exec \ + && which zip && zip --help + +# Create app user with specific UID/GID +RUN addgroup -g 1000 appgroup && \ + adduser -u 1000 -G appgroup -s /bin/sh -D appuser + +# set work directory +WORKDIR /app + +# Change ownership of the work directory +RUN chown -R appuser:appgroup /app + +# set env variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# install dependencies +COPY requirements.txt . +RUN pip install -r requirements.txt + +# copy project +COPY . . + +# Copy init script +COPY init.sh /usr/local/bin/init.sh +RUN chmod +x /usr/local/bin/init.sh + +# Ensure proper permissions for SQLite database and directories +RUN chown -R appuser:appgroup /app +RUN chmod -R 755 /app + +# Specifically set permissions for SQLite database and its directory +RUN if [ -f /app/db.sqlite3 ]; then \ + chown appuser:appgroup /app/db.sqlite3 && \ + chmod 664 /app/db.sqlite3; \ + fi + +# Create and set permissions for media and static directories +RUN mkdir -p /app/media /app/static /app/logs /tmp/backups && \ + chown -R appuser:appgroup /app/media /app/static /app/logs /tmp/backups && \ + chmod -R 755 /app/media /app/static /app/logs /tmp/backups + +# Use init script as entrypoint (runs as root, then switches to appuser) +ENTRYPOINT ["/usr/local/bin/init.sh"] diff --git a/build/init.sh b/build/init.sh new file mode 100644 index 0000000..065c0f0 --- /dev/null +++ b/build/init.sh @@ -0,0 +1,56 @@ +#!/bin/bash +set -e + +echo "=== Container başlatılıyor ===" + +# Veritabanı dosyası yolu +DB_FILE="/app/db.sqlite3" +DB_DIR="/app" + +echo "Mevcut durumu kontrol ediyorum..." +ls -la /app/ || true + +# Veritabanı dosyası kontrolü ve oluşturma +if [ ! -f "$DB_FILE" ]; then + echo "SQLite veritabanı bulunamadı, oluşturuluyor..." + touch "$DB_FILE" +fi + +# Tüm /app dizinini 1000:1000 kullanıcısına ata +echo "Dizin sahipliği ayarlanıyor..." +chown -R 1000:1000 /app + +# Veritabanı dosyası için özel izinler +echo "Veritabanı izinleri ayarlanıyor..." +chmod 666 "$DB_FILE" # Daha geniş izin +chmod 777 "$DB_DIR" # Dizin için tam izin + +# Gerekli dizinleri oluştur +mkdir -p /app/media /app/static /app/logs +mkdir -p /tmp/backups +chown -R 1000:1000 /app/media /app/static /app/logs /tmp/backups +chmod -R 777 /app/media /app/static /app/logs /tmp/backups + +# /tmp dizinine de tam izin ver +chmod 777 /tmp +chown 1000:1000 /tmp + +echo "Final izin kontrolü:" +ls -la /app/db.sqlite3 || true +ls -ld /app/ || true + +echo "=== İzinler ayarlandı, appuser olarak geçiliyor ===" + +# Django migrate çalıştır (root olarak) +echo "Django migrate çalıştırılıyor..." +cd /app +python manage.py migrate --noinput || echo "Migrate hatası, devam ediliyor..." + +# Veritabanı izinlerini tekrar ayarla +chown 1000:1000 "$DB_FILE" +chmod 666 "$DB_FILE" + +echo "=== Uygulama başlatılıyor ===" + +# appuser olarak uygulamayı başlat +exec su-exec 1000:1000 "$@" diff --git a/build/requirements.txt b/build/requirements.txt new file mode 100644 index 0000000..793d17a --- /dev/null +++ b/build/requirements.txt @@ -0,0 +1,48 @@ +anyio==1.4.0 +asgiref==3.7.2 +async-generator==1.10 +captcha==0.5.0 +certifi==2022.6.15 +charset-normalizer==2.1.0 +Django==4.2.8 +django-admin-interface==0.24.2 +django-appconf==1.0.5 +django-ckeditor==6.4.1 +django-colorfield==0.8.0 +django-imagekit==4.1.0 +django-js-asset==2.0.0 +django-ranged-response==0.2.0 +django-recaptcha==4.0.0 +django-recaptcha3==0.4.0 +django-simple-captcha==0.6.0 +django-user-agents==0.4.0 +gunicorn==20.1.0 +h11==0.12.0 +httpcore==0.13.3 +httpx==0.20.0 +idna==3.3 +pilkit==2.0 +Pillow==9.5.0 +pydash==7.0.6 +recaptcha==1.0rc1 +requests==2.31.0 +rfc3986==1.5.0 +sitemaps==0.1.0 +six==1.16.0 +sniffio==1.2.0 +sqlparse==0.4.2 +typing_extensions==4.8.0 +tzdata==2022.1 +ua-parser==0.10.0 +urllib3==2.1.0 +user-agents==2.2.0 +verify==1.1.1 +django-resized==1.0.2 +django-tinymce==4.1.0 +whitenoise==6.9.0 +python-slugify==8.0.1 +python-dotenv +grappelli +paramiko>=2.12.0 +boto3>=1.26.0 +botocore>=1.29.0 \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..443d0f38cf3b2d46a6f48cc3313352978a67d328 GIT binary patch literal 1081344 zcmeEv31D1DdH?QvdOCOa*-0G5aU8E@JGNr)`W^e;?kYHmEX%TNNwy{V%&~deeUet% zL)t@!0|Yq<0m6OXq1;fQgriWdmL};1EznZh(gJ@dl&e5X3;g+0+W&9nz1@9#NOGLe z5;}77&AvA?-+c3%nQvykIo{aGp=81IPiAvzqu^Iv8(eO;>sG(th}+h9O^&o@2U;84%06G@savSUH&~? zRIp^CYoEW1iU)bRx>tHOwscNK-77{V2bS-gnM)`0`D8Yek5zt?OhiI)Egn@&Dp_6N zE9_i2#jgvr3P*?Vbx}wbs^oz1dpERn-T~o<*Am|11i2?MMjOk$@si}OW44~i zr3jFAMr`d|;iK-0J1$Aws?5fuctTaQ38_+Z>JZwndi|*jTd9(3By-7mLzSvxgUs&q zp=kD26^$cEJew()nL@0vJZF+BrA&sxx*3|FMNk7!xV0ozm7%Nkxc!MYq)Yct(9rUcuth(&)Y@b+mK_0`B{+tbvxcFbAO4vu8CJ zE$?3|@GKp1X{AELULs`o-q_aCxow;K**mHsD_6euRY1BdbmokFeleRvja)Et@o6Ks zM^+-;Qbe3 zSq-D^ZCitquM2cp)xhHNq*rF3Uda?0K>aFo%*ux5md@?l-7B*-fKtq9&RSLj?y|tD zu7i>K`rNIyGyK`y9=utuOQDe9_Fr z&6-EPDt|Sedq^AHhnU?f=iDuw`}eyq8cyue$*G)CAd@j0m&yySif>n)|7D>DTq^yJ zqE&Z1)%v2=7g1A5XYT?P&?$p#>hywM-=ez2KNeppCdBQ&FZrJDJLvtT_v7A0Z^-j) z&qJQmo{b%!>$tz;M8`GlUuZA3``SL(Hq`p9){Cv%TE5isd_-n{3;~7!Lx3T`5cthU z;LP=cYsXY?K0h5x8yN#87D zo7Xf-MlUDhWLA~RWHyyRqGS+Pmy8niOQ9Yq(j&`3Sqf=-=v*fNo2|)=RwH(s~rFN7jO2MGc1{=GBdI5UOyQBld|z%qY|(w`LAhzpV9yv>r(b%Ce$I z!{;|42kDv|sI?Vn&VXs7x>ZdWdDJ?*w`!~$P*gvuuAv?U=!E5v7V5tWy!&c+E3t$* zX%tfh!Y@{7rVZI>;iw$ZuLPT2HEiTqGLt0D+Zj*mQ6oJnvZjWWXhgk2aBW3A(^{vI zpc>YUq^O!K9oZ$(}MVyREX1{fCk;15R>M;-a+)?5aw)rd=sU#|NHV5sTNW{!sE}M&` zQFPE;4Iz>==tQlz1Iz|X%yiV-`C>9>mTVAY1tlLt$t#UemO|mMt{iL!i(@4grN&E3 zykZQEWs0eWRZR-15jApa8+eq;S_(D925O*jMwDn+Q~O%Mp;XqA>f~Ul^mWQw4u@pr zNDG*hDzBv2OezKmfT2M_ zaCJhaL^_!P3zZm`(J|+iX*m-^Ec)filra^HPiK>`3p8YoufV_y8Oc%%4naY1}ge64s$yu%5xR+K3sjj}l};Yq8|=wwN6Bq!tCe(H{Jqg?{?sADg z6h9+=S^T8vFa#I^3;~7!Lx3T0*$7>M%8o+EP{+;=SNqr6KidA1_DuU=`}J-A28Qg9A;1t|2rvW~0t^9$ z07Kwcj=-)xg3G;IZeK`Z1*|ki?K3id^1?s-$R{3q2xl9T-MbNNuhx#^$|mj-a0awq zIfC9`1;wom864(t2DM&Qg6`Obpu6_Bla18QaCoi++P)Kkb_Cj}gw|lWEFT{I7?tkY zfgro(He0$%AX~Olur4c@Em{Rw|J_ zPA`jOKi{woVY=m3TO>y)Tc%Pt91J4iY?&P4Y@zHoew_1h@7UW)gmMHTQrVCDael`w z-_%mVsj>`NmW{|?d9C7{j$6B>rGi?;k3?~RSMgAB@`gmJU|8`ZksMG~JOY~$X@9iE zMzrEVVw8ZbyzROM!FL6kt(;hsn@S$6=NoXK1-Lbrgs+4uiEW3w+6q^aw-T--srAN> z<0ZhinTl8mM1`{+_u`ObpKLz$_!~ue~-7*!nH0v&l ziWh!*FkuhQ2(<0hY(bsjY%wb#aW()UZ6Tc@Z4t{MyKpF=ZP$JxpfeZ|uN;P~|L^g9 z#wGp;tN)L|2Ji;)eyshU5eLMOxLw>Jx_v+LeZ}|ZzK{6c=)2#S@!jql@F~7+z83F4 zd%xiQsQ2yOmw2DGgNVRcl zhxN{8XIYuLHZ-}nyB%hy?UZ|!naO`ule^npV^!K_y|kIx=Ai4T98P=D)@z#Fvb%wO z=(=m0+?uauv-?1N+LAmWR#OVHvrCmSC;DWTV*R-sY~DMmA6ZoW>B*dSf#r77d@T z$-UKGHgMVcw}gQTvAS?#;4OG;bW&K&s;Fi zjGrFOXlD{b#-cHNGFcc>`VUxF4Tb}9)UT*La=1s8gK8*@*U8kZnZvD?naS*}_U~Xk zo36Q=FiNgsRibiK*2((6Tl|R&|Fv8~7FiI607HNwzz|>vFa#I^3;~7!Lx3T`5MT%} z1fD7gwDViqZ0i77|95S8stC^HWC$<>7y=9dh5$o=A;1t|2rvW~0t^9$07Kw%5Fqpa z4*sCa_f_wLXS_pc-_ZKQmd`f7TX;^>Bwn*Wh5$ogO$07(?+}LXJ=t3b&4p*;^Ye1H zxS;EiuoSvrEElA3bS9gcSelv$nn#yT&kW~}FD83Wj!p&+7sdu6{pTl(v6B;h!-FHI z4vmea&W>hc>iERm$=sxxIdWoh=-7FA{%n6@WH^3QId}SKVj-6pJbo%u7*(eR!>7Yq zIIW&PulI%*voZa6pE|QJrXMMWCr_SIW+rl{`x3+2Sa$q)-`LXNqPBc2a`0F&-+$`p zSbWgvm*mjIxzT}>=STVupFeSQBCE+~rp_vXs6M4GFZRgA#6ULO6P})*9qXS6Ob;7j z`NDEsnaPeVE)Ng%ot__5B66%Fn3#0yiic#;0Sf4W5w|MHHaYD^Vht4FX z56@2-W73>HJb58Ld%~QS&(9oP=s$aCa;z{F&ME!V@zKLZ=7@HF>BRW#@u`#1{D?j= zeoQXvOGBx#DLw)^m8-Qy+G*f`OKM~-rljPgPFN==aDlQE=JG=Jf>NBFo)1SeTKuVo&`@S(x#vQznAV0e>FDywp`!uW zn4U-$vg5|VQ2&IKn3#L}Ds4 zKdZ&37Sf5-R3yBt&d$%ov+?Xx523z;J(J1f2bc7N*+ZFuz>>CbekwLK*Lz_`J)1ZY z=${Q7PKD2$mHH=6_nsNF>QD(G1sh1C!Dv|4!>B`oRfi^*ip4@fl9O6y$XQIiAE%Xlgt)JfSgQ|g7KnJGgvr?Tm}^s=I5E@YPGjiuT6QmHSS9XP(Y z+Y0I&^pQiwqw#^U#ni~@`B+#Rh>Q(rr*r0fK%SI}N6*Zu z7mh8&PAw<<<*bxZF7%qm6Q_E^bJN9SXnANposTRZ%#Ds}bE!G=aNtOPpqL8u^-YeP zD#UxwO{LEn(%5t^buKb9d;Iv+=uuO!qJ&rVdmuIvoV#EslUgEC z)U>QVmzfBsi%&UiX3Q-Vd*BCEpH)#)-xD4Q%>vHOa&5XJ+omG3ACQ&jHfcKV?5$^@WXBESw7hQ{{~f+ zyO8&;q!I}8Da07HNwzz|>vFa#I^ z3;~7!Lx3T`5NLn^+5f*s+~gAfL;RWeWAO*#x5clEe=q*E_&M=U#Ye6i2E^Usc5w?BvOk6ZLx3T` z5MT%}1Q-Gg0fqoWfFZyTU6MgTLFq<9n>J7?QtG4BODON5w1d)i zO4|tKS}ARzw3$+&iEnc^5yBI~@qAkwTmS!cuZ&DSh5$o=A;1t|2rvW~0t^9$07HNw zzz|>vJaGib`ak>r-xJ4?abO5A1Q-Gg0fqoWfFZyTUO>s&fa zHiiI0fFZyTUJTJKpaf968i~*56g@T^GO0e|)4qSeJiK7fm&p z=;~g%<%*WhS%{QhTO=xB46;Q-swTyyc&wO7&euwCr7hs8k-?Lv)|ay})Agm^=kF>- zMGks4wscNK-77{V2bS-gnM)`0`D8Yek5zt?OhoQ|=9ZSuK*0UL{(>=)G7~dKW-1#q z66s_nmdZ}mK3{L=YrOa1P`|&c)|(&-F#X9)!JIO4{*mM3{*hBdL;e$^gTuX}=lsX| z&-r^#jUOKz0i)smk#Xc8J27L%3y`MEUoe*nG(zQd&Ya72`4@~_eA>wEQ6#Cm%oTx* zcmcVJO{R>gE`L64q*6%QU(Do_QyDW+4(abZ(%*Ls!c~R8`G8;Q1`}}3OqsE?na>+j zW=RTLbmE^yb~1%nVR_D^VwSTNq_2OhjE6IV4N15E5D7O*aT^>N93Si* z8aijc>_3E5N;R<_JZB2!igw9-?JO6PX=E*76io7~j7;~+(ax66Y_EG|zEW$bD3nxc zOKr`OCnqMfNhMwu$ZB+s2-mLGNH39dZS}8>Mi8hZM+)YDYaj%+?dR z6rrp8qSQfE-+fn7)mN#=G)TVo*<;INtG-%qyFJ#o-nCR8t&>9w=X^_bR!esjj@LrSl|)8(K}qt;$zv zB+;;@MYT|<46!{%y1GI0pvkSqpjKWh9Fda#qWbgVNNY=HH0r)DTq$46w<5mPFzVj6 zH7NPIK&w=PwZ{i#@t)kuTvsNSz`lZlos)IC^=^!NukPHw-MupF1au{GbxUU)eeSuM z!e6x>8d1$O60&0;TpM)rYW1NuV7YFWc-B^o+6c~y-2Lk~a-R{QMEI!t;*JWObycs@ zqAG<*Iw2{ZP!(-Ls%Vh9s2f(-9(7?aDH#XibquN+WL+KYzIfKt(%IYVzVD_=g`x#k zTWS@3QS0Mv&{7++xUAMnIsFQ{w5n2TFvFvFa#I^ z3;~7!L*O?M0Wb0-HXV009T!je{?PkV?_Lkz5omvQ+mG8m*}9?SD=j;lzukO-f4ci0 z+}m7}eBtsSSkZN?Y0ITG?RWBBSkEFvZ_X+imcZM z-esFt>}><}0oLGV+3f105R*)u3}i80Yw1j??iG{TB%Sf*a#_0IT^B2+o6(RQ)}jqU z?_6E<_4(DyZpT?g_>1?1S~_LfeSf!Q3#&ujDIk8M9ClP&uId9^mg$NVFj;kXSg5Mk z|Kr&->O*=DYk8*=(x32o5tVst}gO6EOpE>Wp3RCmE`$ zW?+$=BAX4$dmNk=$eR3DV%jjQziz<&xKmZ#_iuKVKy$d6HlvHTsV&6!zB`=0v{l#z z>rB5?)~lCLDFmu=L(A=2OPFgL5GS*#1lsD_fe7|&AIksseJEGSEuCZC?v*H2n6_y= zk3BhLgO9m1m&}c6Ylk?snS$?L1Y7tyws)_M~dsMq<`nu4u!MLmN8x zSP_L~hX-Uvy&81pv0mSG%faT({%xz}y0Ww+mw&e2dt;zw&8-0~)%vF#^RhaIIU}E6 z%;uo@OFP44B~rDpkuvfHvV$#|aVm6d*a9}md@NtY`Vd*Gs_zQO+iHa+YfhZ4O$1;= zlUO4pf|zL|nW_k>XrU@0i68}IahfT3+Vc4yDHiUW>5ngF3cd&$z%?BUqi5N zbzgJm!1guE*IJmg3Yh-hwz6?=3th`j)7nf6kor`9i4Dq_2Dz+4ULyy7s}QZN0o)Ym~$f@85$~+p|Ea-tY!2{IA68yp!uozuFA!+j9Up!a$1utMuTJ9TRNBb zx-W`UZ>*OJB?r4p!4_QtF)IVY%HB@Wm?waec^ zR!r}6S1Lp$>6%Om4XQ>Z@MYDHTA0_~z+tOYXCyn-bp~V3JXh3B%|_Jfx~e!=S-V$` zY{M2y%mwatc5O~UVpwR6rHlzP6-%S+Q?al|yzvqy8rY#_td>Y{HD;+EdVv&mZTy7*OKmy;n`FYTG=UfFY9OJ@wb!cIDCN;zzOYYt7s z&4@WsoA#D;IQ$(O|I{0t^9$07HNwzz|>vFa#I^3;~7! zLx3UhR6>Bw|DQ^w#bjj&Fa#I^3;~7!Lx3T`5MT%}1Q-Gg0fxZkA;9MUmxmjR#SmZ! zFa#I^3;~7!Lx3T`5MT%}1Q-Ggfu|AzWd8rDjuDsm3-P<+KZ?IEzE>RR_)*8}I$qxK z?2g+zPKm!Keo!0}hrJ*6{*L!u-nV$)(DBQTpLTqwba_x{rRGw;88zwiB)_iNt2^Zu3h&%J-_{j~Sv-dA~F;(gG2zxN*R zvN!Kdd#AjH_cre-?{V*-x6gaP8}`cHz205kZQjk^E4^NCi`V7(x#!28?|Z)K`HJUD zp3iyy#Pey-$2=eOyvOr4&l^3j^1RsdT+c<%-JXIc?V0q%JZC*8Jx4u#o~L;vFa#I^3;~7!L%@kZGw#dga7R!0F<@}cAyvcGNvYabmh&>pd8y^R#ByG2IWMxD7h28>Ea&-_^PuHC&vKq?InS}22Q25= zmUF-5+-EuWTFynwS+SgF3B23gb`Pb`r1Tk-KAqCLDZN1HGNntDE>gNcX_3+brFlwo zl+IH+M`@PQ45evGQ7A6`LFw(3 z-bU$pO3zVxmeMnno~HB^rQ?*2Q94TLNlH&pdYsY`N{1;OqVyQ0M=2eo^a!N`lpdzE zpVC8=_ECC}(q2k$rSuj`Z>IDBrB9>uCQ5rKjZzw+G)$>ZsYYptQk7DLQkhbT(*2YM zDGgA%kJ7!A-biUTrF$sdP3aAk?xJ)jr8_9?qI5f@enMNe5!!k^rCaIy>nPnq>9v$@ zrt}(0J1M=I(oK|JMd_84UP0+bN;gm{658S;wAD+ghra0`w5^@eHcDG5ZK1T8&=!Hx zCi;e_lxyPMZ7p>E@A0_A{}6vB{#g8h_#IdQzAFBm_(kz^;-6vO{s-d6#orYl7T<#z z{F}u$h_4o3CcY4J`1{0r#0z3koWm@BQZ&Td#WUilI4m9!`^1~Ys2IY0{zh?^=ohz$ zS7S!+5nDvg_eHC`R%b3-F-uD^bANfA*`vm6oAMm}`_fFqid~d|e z{^hKp%#5d?Wg!z4sPxDE>y}lbT!@thg>AS+` z^|fM-|DWEUd;i`0Bk%Vx%l|*#zxRI0`vvc3G0*=)?V3%jTJI~o zFUG9@+1_V)FL(>yjCa~Q;l15^+Izx#)O*N#vp3>Zy!*U2c>UgMy;pgC-c~Q?`47)e zJ^$kQ9_IaD_57{pFFl|2{E_DmF!TSA=Y5`cdfx1Lz2}vf`#;ZfpXZsLB~Q+i!tDPp z&w0Fa}e`?-6MH!^z8Ip@3{sR0FSc=VZRvy3;~7!Lx3T`5MT%}1Q-JUT?hz# z8|V5IrJtnq6O?|O(vMO4dz5~Z(vML3yOe&I(hpJkK}tVB>BE%1pVHr<^nH}Rm(ur8 z`rDMgo6>ht`c6vULFwBmeH*23rSvV7zM0bBqV!FaK1AsoDSZQ_uc!2Nl)jeI*HHRu zN?%3kD=B>ir7x%SWt6^@(w9*BVoF~`=?f`+0j1BU^g&9WN9l7ZeGa7$Q2K02@2B)W zO7ErgBBd*oKC6jubKgVAGYNSHAx|gdZbB{)vP{SlA&Z185K<(hKuDgD93k_B%n_0$ zBtuA=kQ5=agv=0VM2xoIY!7)LIw#rLdXCihY9H?(+IhVkRC#!ghU7l6QUEM5fUOqB}5@aCPX4+KOsRv0)*@%WG^8% z64Fh`9zu2#aswf|2-!)<4nn#J*-nU`kZpuqPsmn6t|Md%A=eVJnUHG;=_KT8LN*a{ z6(Ls=as?q93E4o1NQjRRFCiX6ItXbeq>YePLRtuECPW~li4dL;j_2Fj$o>EA;!YQ4 z{(p!0{wKtT#OH}Aaa=rrm+X%rzz|>vFa#I^3;~7!Lx3T`5MT%}1Q-JUixB8+Z*uL} z>i8B{yqGU!(`If{8$32UJn$3Ws=2xqueUf}+aKn-t_AORJKp1G<&$DpHaEGpZn3}j zlegV1LX&IP4*S-A@(H!PmE^9yZLW>?xV`SS-iV^fK{XPTB{it1VSm7HeS-`?6iLwnk{nP}zZC6}v>qiC zjObEWQT9qwk0kMJ)gct6M-2tls2WlAJ8wh^cUvi_btxoMW<&m3FsdqHEwUbeB{YE# zDr{VFTM2|}6R+uh36T2K3MQRgRs~o~XNeP7&eZ4ev`Ock{ zJVa}gSQR6}KPsu~ViXGBxueWKDm@bBVHhBear|%VrC=Rzbzx zc1Md---LT)wMUVIN<>qosCGT#?xpHLJz?Z#XNx)i@#SJBZp`wnb!#5{kG;{DPGqe07tWPI7w+~P>4f&N)D86_A}H7(juRXdYWWb;}_GIcr#-QcRZ(R;LLP*d>r z*oK7#~S8;S)f$U#Yq zhNA0b1pVJ#os|^o)e-%GXMMrb6x2va3pG@D&OE5myRI&$pwug*HB*R6=u{dtHD?NP z#N1RV45fieTD8m-31|hQx)u#L>_HvLgrgDjDn~L6w9=Z%M1yFzk9X~H5_<2Q&7~qe6+K; zW3|rW?ymT>k(t6bAA9_F;}ef&Dv`&}+qw=KdDA~<6sEiGc*ZlTyA-`gm3w4#DVnD1 zA@nT#sm)mpAqa-GszrB;!QD8Yyvj>zk#AcgO0siaV+uhvLG4a%w%ksG!>M;<~E z`L599a5RomXiE#tPkyNpD_Y8LhNUkZ~h9RoHstVfz? z$v|< z#0&Pv5MT%}1Q-Gg0fqoWfFZyTUAbMT+jEqtv!>GfTZ{p zmF$pHu}vIn=HXCB#XB63BYp|{Uu3e$0^1oRJ=$LJ)&sKUm&u}jkE8@O?0$WYHjPIJ^9XhhNa5Csw@PAT>w~6}{eRfah%J#V_1+3Z!={0-tcSzRPHzdKVJ#n9 zbLd`3C87%zZzZ6F{W3OAAcPWBLaHvyO-^rF@{`?~;T}Z`Vt;Hj9O9kcQk1|F(#gIb zJwo<#a$EUcdqcbukaUu^8pV#!ATU$Z2$}!y5Q8r9|B3%D{t&nSeM9^Q@k`?8aTma& z;-|!qiXXtue{UDxB)(RBIqv;?j(8C_{>_L9@lNq9?)w`O4~w^oJ-F>JC@zRk7rVqR z-%;Gb_bdBHDyh-e>e{=h*WJ5+teBZ9=2Aw^f6kbD^x{1>YFJ1;suBz<*s>9>VL&kz($)3V5l6rLtDz>kvIDLR^?^;a?6)#;aMJD`b zZ*6tSTfjWrn=6>6H;0|!&V8;*6o+tar^VT-_uRGA*uqnZObWTFVL8>D$>udJs7E8RR?E5K71r#y)e_H!T3!1|3Q?$%>0~P9A5NlmryhNH z7TSQks7Z@eg83|ker=L zWhac;uTe^I8?)E>_=1IOkfPh?X#_ce{g?O!#?V2i(w zQ%h)@wYkAy!16-BQ;BYab$%DpAf?cK%$P7{{X@ygHv1$jAKy=zD+=>4K`6V}ym_of?w$kY2;Q>=l+YJ1!CY_8oRD z-8C>)9&kd}OmD|K3l?LdgMwrD?! z-9I*+oip(x54SoOvn!v4yQktW5-Zz0*i}d$2rBjqn`U>D_(%%V6;@3}yO8qI5r{K}rLZ($5rd^fLt<{Y(KzKU2W% zp}xB*-9_n6O6dm*IQoGCj((tkqaP*U=;sJH`Z)rQeu#j(n%w`_>e|YQ@Am$O_l@2o zuC3zDzQ=qfw*&uPyZga`HrM7IADnONb?2ASlcY~1=lp{i^kAt}UpAE@2TC(j{;6Un z8PDc22M&;vuR*EZ?>~jcJmrrY@o6(r_77Ozfw^ThkjZAufRR}aE>5TXdm@s5_F#9r zgQt<7!W?4qzyUQF4l1=AZQlU(Y`{+m)$*Ij7P9Jr1LR~b8hb52>Km{<8}P#^mqrYc zX_N58psF!X5&&$eetl?&W-ue(o zW-uC6!793X66J6}3Bej9 zqA9Cjgc;D3Ih!ny!UhDZfK&>FQb!>0SAz8oD5S2E#!Nn7;5recp{$X{Og=_K1!$lK zd4R!il>}fBN?tG%RzcS_RJ%gifoMT14cKGaz6yJCw@CmnWeRyx5SV_i0uu=svjUQNXk^ znIaV=Qm<*PP_a@d$c{k3p;0Bo%^9_&XeW|S6{nJu%LfiXLj~(Vw$rc!1!$O6l8DyV z!@P>gO(s8SLK$E-Sf7PT08|Y4Hz>t$v{vwZa*0$4jJ)eA9K28+h|elQ%5{}FKc9lJ zE>HDts9vp?ylKS^HIOe<$FX286$0pY6J|V{usWgIx=Vt@%0VEqx{AEvNNw&57f|2^ zs)U3io@yOq>Km{;SK*22KrPdv5uhN@a*|QXwS0?4%nAS;i2rJ=>%_$(Kq@(5b?-V# zrjD`Y4+zld$@(gMaUDpV$WHBp6`2MyJJb#h0Oqx$8=PIOWju%JxG{w>Qmv?Ej{xp9 zYE)HqYb0Wi!ssZ=8i`?`WkojgJ^nJ*`~1~4WuM1eD%Zb={?xjv}UWFjr!nqOlX)mnUx;$%3Eg@+YbY{niu3~PMXS(%d;B?bK3G_?jVTW9xR67O=&~+xlMfz( z%mc^3-M*pjL@qmH`eCSg>>*+Abr`1daux&4cai~BLo zF?0K;i^dGJPz^X(wZO6i?4^o=Y>Yf<*--ts;Q`CpPnQXxiLu3Ysb+?y2p+~SZo2aj08ftk6w?sry!?1j~WjiS?yM__Vd(JTt{~#9oHt((E;2^~prc(zYP$X@Rn)5{%&Pacg_a~sm(@^5W=0dz!)fa#- z{=-y6#R^K4zwlr$DeINCGUH3gNF$YYshPs23|RGXlU7vYU&cLnE3q@ajdY)D*%=)+ zL0k=5J94hr1mW3W6pjl+Yix|xu+1{H$!po#nlQjHGtbu)6JQDu=iz?Ie-B!F1YApd1XNJcL?p(i4oVl8e$H@xRMD)nIaE=fq*1 zLZAj%{f}9vVE$fC6LacX20S}Z*?w1B(qwSjxFR`**}oFwmo*h6Huk!?xaEauOC!7+ z8of#;tA@O6ixqA-x^#IUB|}bK-K)3lPy@5mI8!m;P}}w;nPzOQ zS2m@TtPABAwWgX)}?5${iejDgNJ)bQ6cRe^l=KpT-7oe_hioft(=Y6r)gD=;&b-c7=bNj2?uWEaF z+x4xlY;A6NMvJTYS;7y5=L(*tdz)JLm+@{c@BW!PP1~?R zp?9e7_^^?J-NPy!9MPbgZY3CFurE*2a7@D9Ra5V!>NI9BVJ#{fEq4g8nLRfmLD4_2W8ovKvqd>bQ}) zyc^vi0A$1DG!oX!vB@s)jLIfw z6HldG%4JC`L_*|3UR~8fa%nqVwIpFx#ui&AgLHA4*y8djYsYgViAW?#%H7dnZ&XIj zZ)mh!(>RHKMTdd6B$m@|X?cYs@m#uQukwZ4JFK040f11p_^xKz#mqg7x=`LVTFyGE z3jJ0FPF&IP5-}Uw`xI;PTMCK0$5)zfLVcjy^)~gko~XVhAIpJNI~f}ujVPxNb%33- zvMhzdBx2ps2w9B|*9c-qk)q+q-6~WL-GL4e9`7b}hYT=HlWXBa(fUp3b%|SHwXi%P zci6kdQR@A_nhoPC8zj_qx?>zUd?K61ow>z&i5kUM3)a{)UY~6mNz z(e@H|lsg515g@9C)KLA7^7`~?SXEc<4nh$Ug}fK(SFF>e9phF+d~1eo(y#0(FK;(@ zrjMQod$uLD6uEKDhZ;jb+(IAGSDFJI_D*!9{scCoJES0Y2ummqt$s6l9ST-hEfiky z?t>r%hMOSB6WXd?76m8oWSoB_+iF+Ys$L^cDFO~mt_XWO>|N}T=W;f(m&HJrfoT;! z8WUDm+sIxcM>&Qh7jA@Fq+8q}#^c=O?vwzQLX003-MP!XK2;hD*XuvC8=0s25UD=i zE%4=J0VnDi&f+L~!!7VN>6e0sl<10k57Y$7wgv$%XM?;$2z2}S-i;oX8f}oTlfep& z?(3f2XqR;BJVfX#@0mweeyO|X%QB$LQ1EGpFop(|=#ATEDJZ_evbYNZ5DdB@z~kLo zUrrmB>Z3CVt4c_!-dbOmb1R&zE5)65SG)rWKkj|^&XmdLn6O+XJNDhzCrbm#(dhCH zyT$E7vX`?d-;pwQ*y195MbVv`^6QeeLdkMuMd-q)xK!aUbIZOXd9nl-j$rAz;gVej>a3{P;ozf&OW zp$;pNh`rt4J}}T=Xp9w1E2u2pd0hv-NQWk{W0R}(Af_pYVceL&rdT*u4$v{;L^&4Y zF&{#Yju)$-HuCA#r^xk`7QQnM6>z#<8(#=dnJ;y2#FI? z_JcDmvUW`enQ~N%mL>t#3bBNT>O~$L7eq}oA^fYZm}SyaT8!nyh7~o{nGbS-;aD> z_WhObvp5;>Dc^T|Ul*Hj`v0VORO}OA{yA`V@caXCuH*SZ2EE}p-Lb&;R*{0f{O^Zc{me4pp< zf%8qCe$Ac@%-Iz{)Fc*!1*-KFT?p5&o9CGAkQztc@NJozLN#q)VMFXs6ioagfVJe-R>KL_V-p3lN5@O%bNn&;DSCV4&uC&u%$aL)4l z44jiZpM-Oi=cnQH@%$8=r}6wG9F6C(($gH|`2?KZJRgVS=lKaZn|a=Va|O@e1*e1O zV{n>y{!W)m_$ANZ0q3VYkE6K4k9cwxSNIN34&w@6=gDbY;mbTZjw}2XPtM~CpXJGc zT;VaEoX8bE#giks!bf;=CRcbrPY&e@@8Zd+T;a_;IhHHDjwk1Gg_rZ>V6N~2o}A1T zp3Re^xxzg>Ih!ji@#Ju>Fwc|Ixxx%jj^_$-o}AAWZs*AXUEvf@PUs3FJUOB(4DjTP zu5c^QAA}R-`Cd2*&)*7XFVEisXD83!3}-9PAAoZ;&p!>0$n!VBY32DIIBuSg;v3li zkLM$B{*C7`FmC#1p4Z`gi{~{sU*-7_oWJ3D70%~*UV-zcJTKFVDSQ}DALoP*!TAs; zd=SoiIpG6v-p&aR!+D4k-Vf*1obWqvUcw3QgY!I2crToLIpIBUp3Vus4X4Nn?}n4% zgm=N2;)Hj?xr-Ct0p}biydBOcC%g^LF-~|ZoI{-O7B~ku;mvTco%grk?B|3x!P&zJ z55d{a32%gREhoGI&Xt_-dN>|VcpV&p6JG0b@&CyQuYvP3PIxt(f8m5z!TByHyb{hg zIN=p={+<(F4(E%U@G>}m&IvDt^GBTU5;(ul2``58QBFV;Ggi$!noPZ|k z{x42I6LtSzPCyfN|0^e;iMqeX32373f8>NA-|emzPWVsE6ko;~{tM0oC;Sx7ZJh8EIOCk~?{J1W;osmK=7b-^xrGz{6;6Z` z{soTA2|t2!BPaY2&JIra0i5eN;h*7b;)L(R@o~cU;6SABTJ7&Umh)}P`6tWymgRht zI?XMX(`>!`y5;;I%lVq+=dqr?VmbdHUUXf@3x6+u+I0&rd>KxJ7yb^8%nN^OMYzIp zHd@XG%MmTdXE|Q#2w$+A&s)ytEaxvQ=g+|t`THy!5**e8P(UaW5X- zV#0+R{jS4L_Qw!l2rvW~0t^9$07HNwzz|>vFa#I^41xbq1jLuS3cELH?zZ0EZfxkn z6~HnsMpx8uE}JcE*Nn+KkDMK!KX_(YI(y;<`P8}e)z$(!J_Uds3fRnzOI5LeuHyo7 zT!Cqw*~WQL>xOF$_p?NBPhzn+$xaj76&_ET683HDFfO{@>H>q6&725#7ZHBb zw$|Q&yN=tmeo7m?ai@A{c=pD!HpQTq97* zdOd?f^hOo`Na_5+h`06WFbC+aj(4|*yy^BIG+!ko@%V%P(Zc>!CEohMRmcf8*}-g|JU-`}-%;4c53 zu4JMM2geE~((#WRANP-(8XEGS7#$q$9X;ni)_>05dusgn;0V|b_m7P4^LI_-O681^ zHoN?|ZX-TzOy35g?hojJeiMWGvs($ z7jCJ+)uttx;e~rZ3NhU47@zI(lUrK~xaQvJ4-D}(5i`KKkw5n5o4Dr**R+X;+xuI~HHftF59b3Z3tSG!B$oQam+-B_&>8wI(n>RjRf^BVTN&cdZx`xc6zAUYBU8oKmDh zJcdj9ad&h@3|WR0RxE6ZoWtdfu}0ajLKKXpItEl1R=l&5lgYRvAC|JW{EZ?md&JjS zpqFt)Q9hfgh+%{nrzRA&&D=lQ zf4G0Nf26N}th(<*)JjYnhHn2M5_*&l=?6y!#|L|dhR)e9`ww-a^7nSCs{6h>>Po;a zIwDX_9qB?DG!I%0=l%`-Erh{y2ObB5ovRn3qryDy=|y!%Q#&n~%%*S(xTZI%4Fmw! zRQ&(F#e@2?UQdKOO&z@+olRN2UL$qq=v(Q8$GXw3ej0)9jT|g%+{ZyE8kvTun=)C6 zr_F*9n>Cj!qNz${Tu>PkqWiQfH{z{Yu}h<+ORUpm{eN@oZ7zKA?*Z|Qc)jmiz7PA( zdVk^lj5p@_x#x|ZSjR6rUe~d!{X6aNYCqP_w>{Q2-PYXtRzzWc3;~7!Lx3T`5MT%} z1ZojDymE7!>&igeUaWh_a(FyvB*5%P^)~rIdoTI)m$kaWIwMBc;byaG9{3g8F2pfSbwnNJI*&=T~k57@h7Z3PuQv3{%D$ zf{*2={f_ww5#I(#4@kIy2A`jn$PF|RTz8|YH?8y#<~Gt6^TzO&V&l`-xP=0%Ns_Mi zuS5}huN7O;2M+f8`*3w3Eb2xkzTB7(?mWgvuyHPxdqMW-da5%@PO;ZpU$ zT|-!Q!#&^h>)%)u#8>U|D_Xm2<7W3tCt^nN$V;y`xm|-EZ7*M86^{MS@xw_s5{xhn~brSxO;9KPHGzp5yMS*osBPBR>5e#iiw8Ns-U51Q5QHA|R_p+#S7EYh63zPzL1cb4wY7u#$d$ zW#5y_T*0@a@qH~^;-Tw-mA#dm>UBA#!VvY2Z&KnWs%ThWxe?*_+Q`)0Fy(*oC3}A$-Ju+&e{X_XAj3L_55)o3uo#-5RK; z8a-$CWGGZH!uM5W!blEby+{kJ+)znaYLqbUaznzj8K}DIbwA{~%5~D^(|uoT`Rmr{ z)^D}Fuzi2~7dz(Nf61l!4g80j#+tq@Jij^G{0#*Ak?=2^pZkDtkT>}MYoP8N3I}k%FlkBT%k8*oETl`3$8)JusiUvwvJ#Mdl^d`i-4nS4 z%PA&4s1S)pqw-~4f>keXiCxPxT&ndAX_u=F;$FhVU@O<`^4T}nZ0aR(9`EHfSKx4SH8{%HsedZ*vBCs`k63p8I9Io z?1O7S1}s6!_hdLO_o)++G79UGveLB&6@=b>WQlk=*B?10#79sRRSqeQu0M)~<$4^; zS0L33S>m;_rQ27&A&M07Dmj#$kjdvaIFqxx|}=3%7WoqSg0jjn;TvAWGn^Rg1JLM#N$?b7p#8xjXSm8b|ToQIe0K(F*s9#Cd z%oDi2&M7MH8O1F^T7z$?M7?r?)$y`UvVE7t^}bV^tJ0oBX6<uCu?2`~OxpY_WSe+^j_P11@j6+{=~9c~^T>4L2cUR7);$s$9=h zx?D*ul~1D>cVFi#-z8O&=ZM^Kt!eg&)(YyOLATJVY685*V~V$4}%m3Pt{ zfULK>ue;AV_uO;OJzw`+v_ewbN!3J?rL-6CfA5E{giW;|0xR~_QgGmA7DLB%In9oG z5jU}fPrKZGl)hRqfiUQL7Y^k9hd&C{DLRmW(ELrvRNBz?u7<8vwY*Kpk^~A!_doWr zE3#D^&Et~Y+EglGrR!+dmVUu{l8dal8yub_kpj^DKmTZ;>>uNM_%iM66bdyc`&z@c zZMB`9W|?ty1Ye9bzC%m91aT6^{YSnBQ_Q-malM(l#h*%f2LRois&H> zMYm=(n@PF^gzdlhP=#%foBn^NcAK02e~o$l|L%YAeOJP^bx;G`2j}i4moXl3NbOvw z2(XFU4Ya1wsO9QhhqZ4eml%VE;Er|w$qzu?2QB_cNiE&wPoWei4s=9WG;5hIe~D!$ zp}}|li|pOjl5Z7>7kxn;s*p1$J(mU+ql<^Jcpjv(j$G-hCl6qa|uzd5ddU@+J9;zA2FJ0C6 z$^Ap;`FDQ#5H)Y*YrVdMVkGa~ulA>El!0|TK$gmCB48hhmEcZ6JVb?!VcCX7{{JUR zKUv!TmF?HA{LYnMymI@>`OAOj^0iCfx^(x_BNzXVi@$jB)vbTI^>1xGcHuw0@I&W+ z|NLJ$|J1o(KX-8M)Y-pr);RNDo%!iAtuv24{LaIz(|>sSZ=U|-L%;dZ-G|

X%Nv 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

En6+{t_>_ISd`EU*zP?Z!=eRT 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

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

+ + +
+ + + +
+ + +
+

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

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

+ Dashboard +

+

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

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

{{ projects.count }}

+

Toplam Proje

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

{{ active_sites_count }}

+

Aktif Site

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

{{ customers.count }}

+

Toplam Müşteri

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

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

+

Çevrimiçi Host

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

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

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

Host tanımlanmamış

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

Henüz yedekleme yapılmamış

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

Host Yönetimi

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

İşlem Geçmişi

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

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

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

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

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

Henüz müşteri kaydı bulunmuyor

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

Dashboard

+ + +
+
+ + + +
+
+

Host Hesapları

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

Projeler

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

+ Projeler +

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

+ Yedeklemeler +

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

{{ total_backups|default:0 }}

+

Toplam Yedekleme

+
+
+
+
+
+
+
+ +
+

{{ successful_backups|default:0 }}

+

Başarılı

+
+
+
+
+
+
+
+ +
+

{{ failed_backups|default:0 }}

+

Başarısız

+
+
+
+
+
+
+
+ +
+

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

+

Son Yedekleme

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

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

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