PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /opt/imunify360/venv/lib/python3.11/site-packages/clcommon/cpapi/plugins
Viewing File: /opt/imunify360/venv/lib/python3.11/site-packages/clcommon/cpapi/plugins/plesk.py
# -*- coding: utf-8 -*- import os import re import time import xml.etree.ElementTree as ETree from collections import defaultdict from functools import wraps from traceback import format_exc from typing import List, Any, Tuple, Dict, AnyStr, Optional, Union # NOQA from urllib.parse import urlparse from clcommon import ClPwd, mysql_lib from clcommon.features import Feature from clcommon.cpapi.cpapiexceptions import ( NotSupported, NoPanelUser, NoPackage, NoDomain, DuplicateData ) from clcommon.clfunc import uid_max from clcommon.cpapi.GeneralPanel import GeneralPanelPluginV1, PHPDescription, DomainDescription from clcommon.cpapi.cpapicustombin import get_domains_via_custom_binary, _docroot_under_user_via_custom_bin from clcommon.utils import run_command, find_module_param_in_config, ExternalProgramFailed PSA_SHADOW_PATH = "/etc/psa/.psa.shadow" SUPPORTED_CPINFO = {'cplogin', 'package', 'mail', 'reseller', 'dns', 'locale'} UID_MAX = uid_max() __cpname__ = 'Plesk' # WARN: Probably will be deprecated for our "official" plugins. # See pluginlib.detect_panel_fast() def detect(): return os.path.isfile('/usr/local/psa/version') def db_access(_pass_path=PSA_SHADOW_PATH): access = {} access['login'] = 'admin' with open(_pass_path, 'r', encoding='utf-8') as f: access['pass'] = f.read().strip() return access def query_sql(query, data=None, _access=None, _dbname='psa', as_dict=False): """ Return the result of a Plesk database query :param query: SQL query string with possible parameters :param data: arguments for the SQL parameter insertion :param _access: database authentication data :param _dbname: the name of the database :param as_dict: controls the format of the output data :type query: str :type _access: dict :type as_dict: bool :return: Tuple of rows according to the query in the format specified by as_dict :rtype: tuple(tuple) or tuple(dict) """ # Example of returned data: # >>> query_sql('SELECT login from sys_users') # ((u'cltest',), (u'cltest3',), (u'user2',), (u'user1tst',)) # >>> query_sql('SELECT login from sys_users', as_dict=True) # ({'login': u'cltest'}, # {'login': u'cltest3'}, # {'login': u'user2'}, # {'login': u'user1tst'}) access = _access or db_access() dbhost = access.get('host', 'localhost') dblogin = access['login'] dbpass = access['pass'] connector = mysql_lib.MySQLConnector(host=dbhost, user=dblogin, passwd=dbpass, db=_dbname, use_unicode=True, charset='utf8', as_dict=as_dict) with connector.connect() as db: return db.execute_query(query, args=data) def cpusers(_access=None, _dbname='psa'): cpusers_lst = [fetched_one[0] for fetched_one in cpinfo(keyls=('cplogin', ))] return cpusers_lst def resellers(): sql = "SELECT clients.login FROM clients WHERE clients.type='reseller'" return [cplogin for (cplogin, ) in query_sql(sql)] def admins(): sql = "SELECT clients.login FROM clients WHERE clients.type='admin'" return set([cplogin for (cplogin, ) in query_sql(sql)]) def is_reseller(username): sql = "SELECT clients.type FROM clients WHERE clients.login=%s" try: return query_sql(sql, (username,))[0][0] == 'reseller' except IndexError: return False def _sys_users_info(sys_login, keyls): # type: (Any[str, None], Tuple[str]) -> List[Tuple] # Templates.name can be None and it is ok mapping = { 'cplogin': 'sys_users.login AS cplogin', 'mail': 'clients.email AS email', 'reseller': 'reseller.login AS reseller', 'dns': 'domains.name AS dns', 'locale': 'clients.locale AS local', 'package': 'Templates.name AS package' } select_query = ', '.join([mapping[key] for key in keyls]) sql = rf"""SELECT {select_query} FROM sys_users JOIN hosting ON hosting.sys_user_id=sys_users.id JOIN domains ON hosting.dom_id=domains.id AND domains.webspace_id=0 JOIN clients ON clients.id=domains.cl_id JOIN clients reseller ON reseller.id=domains.vendor_id LEFT JOIN Subscriptions ON Subscriptions.object_type = "domain" AND domains.id = Subscriptions.object_id LEFT JOIN PlansSubscriptions ON PlansSubscriptions.subscription_id = Subscriptions.id LEFT JOIN Templates AS Templates ON Templates.id = PlansSubscriptions.plan_id AND "domain" = Templates.type """ # make query like "where x in (%s, %s, %s, ...)" if isinstance(sys_login, (list, tuple)): placeholders = ','.join(['%s'] * len(sys_login)) sql += rf" WHERE sys_users.login IN ({placeholders})" users = query_sql(sql, data=sys_login) return users def _resellers_info(sys_login, keyls): # type: (Any[str, None], Tuple[str]) -> List[Tuple] # items with 'NULL' are not available for this panel mapping = { 'cplogin': 'clients.login AS cplogin', 'mail': 'clients.email AS email', 'reseller': 'NULL as reseller', 'dns': 'NULL as dns', 'locale': 'clients.locale AS local', 'package': 'NULL as package' } select_query = ', '.join([mapping[key] for key in keyls]) sql = f"SELECT {select_query} FROM clients WHERE clients.type IN (\"reseller\", \"admin\")" # make query like "where x in (%s, %s, %s, ...)" if isinstance(sys_login, (list, tuple)): placeholders = ','.join(['%s'] * len(sys_login)) sql += rf" AND clients.login IN ({placeholders})" users = query_sql(sql, data=sys_login) return users def cpinfo(cpuser=None, keyls=('cplogin', 'package', 'mail', 'reseller', 'dns', 'locale'), search_sys_users=True): """ Get info about user[s] or about reseller[s]. :param str|None cpuser: get info about specified login, None for all :param list|tuple keyls: keys to return :param bool search_sys_users: work with sys users or with resellers :rtype: tuple[tuple] """ if isinstance(cpuser, str): cpuser = [cpuser] # just for developers for key in keyls: if key not in SUPPORTED_CPINFO: raise NotSupported(f'Key {key} is not supported for this control panel. ' f'Available keys: {SUPPORTED_CPINFO}') if search_sys_users: return _sys_users_info(cpuser, keyls) return _resellers_info(cpuser, keyls) def get_admin_email(*args, **kwargs): try: return query_sql(r"SELECT val FROM misc WHERE param='admin_email';")[0][0] except IndexError: return None def docroot_basic(domain): # type: (str) -> Any[None, Tuple[str, str]] sql = r""" SELECT hosting.www_root, sys_users.login FROM hosting JOIN domains ON hosting.dom_id=domains.id JOIN sys_users ON hosting.sys_user_id=sys_users.id WHERE domains.name=%s """ try: return query_sql(sql, data=(domain,))[0] except IndexError as e: raise NoDomain(f'Cannot obtain document root for {domain}') from e def docroot(domain): # type: (str) -> Any[None, Tuple[str, str]] res = None domain = domain.strip() uid = os.getuid() euid = os.geteuid() if euid == 0 and uid == 0: res = docroot_basic(domain) else: res = _docroot_under_user_via_custom_bin(domain) # If there was successful result, res object will have # (doc_root, domain_user) format. If there wasn't found any correct # doc_roots, res will be None. if res is not None: return res raise NoDomain(f"Can't obtain document root for domain '{domain}'") def reseller_users(resellername): """ Return reseller users :param resellername: reseller name; return empty list if None :return list[str]: user names list """ if resellername is None: return [] sql = """ SELECT sys_users.login FROM clients as reseller JOIN domains ON domains.vendor_id=reseller.id JOIN hosting ON hosting.dom_id=domains.id JOIN sys_users ON hosting.sys_user_id=sys_users.id WHERE domains.webspace_id=0 AND reseller.login=%s; """ return [sys_login for (sys_login,) in query_sql(sql, data=(resellername,))] def memoize(f): cache = {'userdomains_map': {}} @wraps(f) def wrapper(cpuser, *args, **kwargs): if cpuser not in cache['userdomains_map']: cache['userdomains_map'] = f(cpuser, *args, **kwargs) return cache['userdomains_map'][cpuser] return wrapper @memoize def userdomains_basic(cpuser, _access=None, _dbname='psa'): """ Return domains of given user :param str cpuser: Username :param str _dbname: Database name where is located data :return: List of domains pairs such as (domain_name, None) to be suitable for domain_lib, starting from a main domain. :rtype: list of tuples :raises NoPanelUser: User is not found in Plesk database. """ # WARN: ORDER BY columns must be present in SELECT columns for newer Mysql # webspace_id == 0 is main domain sql = r""" SELECT DISTINCT su.login, d.name, h.www_root, d.webspace_id FROM domains as d, hosting as h, sys_users as su WHERE h.sys_user_id = su.id AND h.dom_id = d.id ORDER BY d.webspace_id ASC; """ # data: # ( # (u'customer1', u'customer1.org', 10L), # (u'customer1', u'mk.customer1.org.customer1.org', 10L) # ) data = query_sql(sql, as_dict=True, _access=_access) # _user_to_domains_map: # { 'user1': [('user1.org', '/var/www/vhosts/user1.com/httpdocs'), # ('mk.user1.org', '/var/www/vhosts/user1.com/mk.user1.org')] } _user_to_domains_map = defaultdict(list) for data_dict in data: _user_to_domains_map[data_dict['login']].append( (data_dict['name'], data_dict['www_root'])) if cpuser not in _user_to_domains_map: raise NoPanelUser( f'User {cpuser} not found in the database') return _user_to_domains_map def userdomains(cpuser, _access=None, _dbname='psa', as_root=False): """ Return domains of given user :param str cpuser: Username :param str _dbname: Database name where is located data :return: List of domains pairs such as (domain_name, None) to be suitable for domain_lib, starting from a main domain. :rtype: list of tuples :raises NoPanelUser: User is not found in Plesk database. """ euid = os.geteuid() if euid == 0 or _access or as_root: return userdomains_basic(cpuser, _access, _dbname) # this case works the same as above but through the rights escalation binary wrapper # call path: here -> binary -> python(diradmin euid) -> userdomains(as_root=True) -> print json result to stdout rc, res = get_domains_via_custom_binary() if rc == 0: return res elif rc == 11: raise NoPanelUser(f'User {cpuser} not found in the database') else: raise ExternalProgramFailed(f'Failed to get userdomains: {res}') def domain_owner(domain, _access=None, _dbname='psa'): """ Return domain owner :param str domain: Domain/sub-domain/add-domain name :param str _dbname: Database name where is located data :return: user name or None if domain not found :rtype: str """ sql = r""" SELECT DISTINCT `su`.`login` FROM `sys_users` `su`, `hosting` `h`, `domains` `d`, `domains` `sd` WHERE `h`.`sys_user_id`=`su`.`id` AND `h`.`dom_id`=`d`.`id` AND (`d`.`name`=%s OR `d`.`id`=`sd`.`webspace_id` AND `sd`.`name`=%s)""" users_list = [u[0] for u in query_sql(sql, (domain, domain))] # FIXME: how this possible? if len(users_list) > 1: raise DuplicateData( f"domain {domain} belongs to few users: [{','.join(users_list)}]" ) if len(users_list) == 0: return None return users_list[0] def dblogin_cplogin_pairs(cplogin_lst=None, with_system_users=False): raise NotSupported('Getting binding credentials in the database to the user name in the system is not currently ' 'supported.') def homedirs(_sysusers=None, _cpusers=None): """ Detects and returns list of folders contained the home dirs of users of the Plesk :param str|None _sysusers: for testing :param str|None _cpusers: for testing :return: list of folders, which are parent of home dirs of users of the panel """ homedirs = [] if _cpusers is None: try: results = cpusers() except NoPackage: results = None else: results = _cpusers users = [] if results is not None: users = [line[0] for line in results] # Plesk assumes MIN_UID as 10000 clpwd = ClPwd(10000) users_dict = clpwd.get_user_dict() # for testing only if isinstance(_sysusers, (list, tuple)): class pw: def __init__(self, name, dir): self.pw_name = name self.pw_dir = dir users_dict = {} for (name, dir) in _sysusers: users_dict[name] = pw(name, dir) for user_name, user_data in users_dict.items(): if len(users) and user_name not in users: continue homedir = os.path.dirname(user_data.pw_dir) if homedir not in homedirs: homedirs.append(homedir) return homedirs def get_user_login_url(domain): return f'https://{domain}:8443' def get_reseller_id_pairs(): """ Plesk has no user associated with reseller, but we need some id for out internal purposes. Let's take it from database. """ sql = """SELECT clients.login, clients.id + %s FROM clients WHERE clients.type='reseller'""" return dict(query_sql(sql, data=[UID_MAX])) def reseller_domains(resellername): # type: (str) -> Dict[str, str] if not resellername: return {} sql = r"""SELECT sys_users.login AS cplogin, domains.name AS dns FROM sys_users JOIN hosting ON hosting.sys_user_id=sys_users.id JOIN domains ON hosting.dom_id=domains.id AND domains.webspace_id=0 JOIN clients reseller ON reseller.id=domains.vendor_id WHERE reseller.login=%s """ users = query_sql(sql, data=[resellername]) return dict(users) def _extract_xml_value(xml_string, key): """ Plesk stores some information in simple xml formatted strings. """ try: elem = ETree.fromstring(xml_string).find(key) except ETree.ParseError: return None else: return elem.text if elem is not None else None def get_domains_php_info(): """ Plesk stores the information about the handler in xml format. Return the php version info for each domain. Example output: {'cltest.com': {'handler_type': 'fpm', 'php_version_id': 'plesk-php71-fpm', 'username': 'cltest'},` 'cltest2.com': {'handler_type': 'fastcgi', 'php_version_id': 'x-httpd-lsphp-custom', 'username': 'kek_2'}, 'cltest3.com': {'handler_type': 'fastcgi', 'php_version_id': 'plesk-php56-fastcgi', 'username': 'cltest3'}, 'omg.kek': {'handler_type': 'fastcgi', 'php_version_id': 'plesk-php71-fastcgi', 'username': 'cltest'}} :rtype: dict[str, dict] """ sql = r""" SELECT sys_users.login, d.name, h.php_handler_id, handlers.value FROM domains AS d JOIN hosting AS h ON h.dom_id=d.id JOIN sys_users ON h.sys_user_id=sys_users.id JOIN (SELECT ServiceNodeEnvironment.* FROM ServiceNodeEnvironment WHERE (serviceNodeId = '1' AND section = 'phphandlers')) AS handlers ON handlers.name=h.php_handler_id WHERE h.php='true' """ # Php hanlder info xml example: # # <?xml version="1.0" encoding="UTF-8"?> # <handler> # <id>plesk-php71-fpm</id> # <type>fpm</type> # <typeName>FPM application</typeName> # <version>7.1</version> # <fullVersion>7.1.22</fullVersion> # <displayname>7.1.22</displayname> # <path>/opt/plesk/php/7.1/sbin/php-fpm</path> # <clipath>/opt/plesk/php/7.1/bin/php</clipath> # <phpini>/opt/plesk/php/7.1/etc/php.ini</phpini> # <custom>true</custom> # <registered>true</registered> # <service>plesk-php71-fpm</service> # <poold>/opt/plesk/php/7.1/etc/php-fpm.d</poold> # <outdated /> # </handler> domains_php_info = query_sql(sql) # yep, vendor php_handler_id has only "fpm/cgi/fastcgi" w/o version, so additional bicycle needed vendor_version_ids = ['cgi', 'fastcgi', 'fpm', 'x-httpd-lsphp-custom'] php_versions = {} for username, domain, php_handler_id, handler_xml in domains_php_info: display_version = php_handler_id if php_handler_id not in vendor_version_ids \ else f'vendor-php{_extract_xml_value(handler_xml, "version")}'.replace('.', '') def _cast(handler_name: str, version_id: str) -> str: if handler_name == 'fpm': return 'php-fpm' elif 'x-httpd-lsphp' in version_id: return 'lsapi' return handler_name handler = _extract_xml_value(handler_xml, key='type') or 'unknown' handler = _cast(handler, php_handler_id) # transform different php variations into some normal form display_version = display_version\ .replace('-dedicated', '')\ .replace('-fpm', '')\ .replace('-fastcgi', '')\ .replace('x-httpd-lsphp-', 'alt-php') php_versions[domain] = DomainDescription( username=username, php_version_id=display_version, # not a typo handler_type=handler, display_version=display_version ) return php_versions def get_main_username_by_uid(uid: int) -> str: """ Get "main" panel username by uid. :param uid: uid :return Username or 'N/A' if user not found """ if uid == 0: return 'root' try: _clpwd = ClPwd() pwd_list = _clpwd.get_pw_by_uid(uid) if os.geteuid() == 0: for user_pwd in pwd_list: username = user_pwd.pw_name try: userdomains(username) return username except NoPanelUser: pass else: # Under user cycle implemented in suid binary, see scripts/plesk_suid_caller.py username = pwd_list[0].pw_name userdomains(username) return username except (NoPanelUser, ClPwd.NoSuchUserException): pass return 'N/A' class PanelPlugin(GeneralPanelPluginV1): def __init__(self): super().__init__() self.HTTPD_MPM_CONFIG = '/etc/httpd/conf.modules.d/01-cgi.conf' # Defaults of MaxRequestWorkers for all possible mpm modules self.MPM_MODULES = { "prefork": 256, "worker": 400, "event": 400 } # Vars for httpd modules caching self.httpd_modules_ts = 0 self.httpd_modules = "" def getCPName(self): """ Return panel name :return: """ return __cpname__ def get_cp_description(self): """ Retrieve panel name and it's version :return: dict: { 'name': 'panel_name', 'version': 'panel_version', 'additional_info': 'add_info'} or None if can't get info """ try: with open("/usr/local/psa/version", "r", encoding="utf-8") as f: out = f.read() return {'name': __cpname__, 'version': out.split()[0], 'additional_info': None} except Exception: return None def db_access(self): """ Getting root access to mysql database. For example {'login': 'root', 'db': 'mysql', 'host': 'localhost', 'pass': '9pJUv38sAqqW'} :return: root access to mysql database :rtype: dict :raises: NoDBAccessData """ return db_access() def cpusers(self): """ Generates a list of cpusers registered in the control panel :return: list of cpusers registered in the control panel :rtype: tuple """ return cpusers() def resellers(self): """ Generates a list of resellers in the control panel :return: list of cpusers registered in the control panel :rtype: tuple """ return resellers() def is_reseller(self, username): """ Check if given user is reseller; :type username: str :rtype: bool """ return is_reseller(username) def dblogin_cplogin_pairs(self, cplogin_lst=None, with_system_users=False): """ Get mapping between system and DB users @param cplogin_lst :list: list with usernames for generate mapping @param with_system_users :bool: add system users to result list or no. default: False """ return dblogin_cplogin_pairs(cplogin_lst, with_system_users) def cpinfo(self, cpuser=None, keyls=('cplogin', 'package', 'mail', 'reseller', 'dns', 'locale'), search_sys_users=True): """ Retrieves info about panel user(s) :param str|unicode|list|tuple|None cpuser: user login :param keyls: list of data which is necessary to obtain the user, the valuescan be: cplogin - name/login user control panel mail - Email users reseller - name reseller/owner users locale - localization of the user account package - User name of the package dns - domain of the user :param bool search_sys_users: search for cpuser in sys_users or in control panel users (e.g. for Plesk) :return: returns a tuple of tuples of data in the same sequence as specified keys in keylst :rtype: tuple """ return cpinfo(cpuser, keyls, search_sys_users=search_sys_users) def get_admin_email(self): """ Retrieve admin email address :return: Host admin's email """ return get_admin_email() def docroot(self, domain): """ Return document root for domain :param str|unicode domain: :return str: document root for domain """ return docroot(domain) @staticmethod def useraliases(cpuser, domain): """ Return aliases from user domain :param str|unicode cpuser: user login :param str|unicode domain: :return list of aliases """ sql = """ SELECT a.name, d.name FROM domains AS d INNER JOIN domain_aliases AS a ON a.dom_id = d.id INNER JOIN hosting AS h ON h.dom_id = d.id INNER JOIN sys_users AS su ON h.sys_user_id = su.id WHERE su.login = %s AND d.name = %s """ return [item[0] for item in query_sql(sql, (cpuser, domain))] def userdomains(self, cpuser): """ Return domain and document root pairs for control panel user first domain is main domain :param str|unicode cpuser: user login :return list of tuples (domain_name, documen_root) """ return userdomains(cpuser) def homedirs(self): """ Detects and returns list of folders contained the home dirs of users of the cPanel :return: list of folders, which are parent of home dirs of users of the panel """ return homedirs() def reseller_users(self, resellername=None): """ Return reseller users :param resellername: reseller name; autodetect name if None :return list[str]: user names list """ return reseller_users(resellername) def reseller_domains(self, resellername=None): """ Get dict[user, domain] :param resellername: reseller's name :rtype: dict[str, str|None] :raises DomainException: if cannot obtain domains """ return reseller_domains(resellername) def get_user_login_url(self, domain): """ Get login url for current panel; :type domain: str :rtype: str """ return get_user_login_url(domain) def admins(self): """ List all admins names in given control panel :return: list of strings """ return admins() def get_reseller_id_pairs(self): """ Plesk has no user associated with reseller, but we need some id for out internal purposes. Let's take it from database. """ return get_reseller_id_pairs() def domain_owner(self, domain): """ Return domain's owner :param domain: Domain/sub-domain/add-domain name :rtype: str :return: user name or None if domain not found """ return domain_owner(domain) def get_domains_php_info(self): """ Return php version information for each domain :return: domain to php info mapping :rtype: dict[str, dict] """ return get_domains_php_info() def get_installed_php_versions(self): """ Get the list of PHP version installed in panel in the form of 'versionXY', for example alt-php56 or plesk-php80 "Versions by OS vendor" in Plesk DB have names: - module - synced They are FILTERED from the list :return: list """ sql = """ SELECT ServiceNodeEnvironment.name, ServiceNodeEnvironment.value FROM ServiceNodeEnvironment WHERE (serviceNodeId = '1' AND section = 'phphandlers') """ # handler list example: # ['alt-php-internal-cgi', 'alt-php44-cgi', 'alt-php44-fastcgi', # 'alt-php51-cgi', 'alt-php51-fastcgi', 'fpm', 'cgi', # 'fastcgi', 'x-httpd-lsphp-custom'] query_result = query_sql(sql) ver_name_pattern = re.compile(r'^(alt-|plesk-)php+\d+', re.IGNORECASE) named_php_handlers = [item[0] for item in query_result if ver_name_pattern.match(item[0])] vendor_handler_names = ['cgi', 'fastcgi', 'fpm', 'x-httpd-lsphp-custom'] named_php_handlers.extend([self._cast_to_vendor_name(name, xmlconfig) for name, xmlconfig in query_result if name in vendor_handler_names]) versions_set = set('-'.join(item.split('-')[:2]) for item in named_php_handlers) php_description = [] for php_name in versions_set: if php_name.startswith("alt-") or php_name.startswith("x-httpd-lsphp-"): php_root_dir = f'/opt/{php_name.replace("-", "/")}/' php_description.append(PHPDescription( identifier=php_name, version=f'{php_name[-2]}.{php_name[-1]}', dir=os.path.join(php_root_dir), modules_dir=os.path.join(php_root_dir, 'usr/lib64/php/modules/'), bin=os.path.join(php_root_dir, 'usr/bin/php'), ini=os.path.join(php_root_dir, 'link/conf/default.ini'), )) elif php_name.startswith("plesk-"): php_root_dir = f'/opt/plesk/php/{php_name[-2]}.{php_name[-1]}/' php_description.append(PHPDescription( identifier=php_name, version=f'{php_name[-2]}.{php_name[-1]}', modules_dir=os.path.join(php_root_dir, 'lib64/php/modules/'), dir=os.path.join(php_root_dir), bin=os.path.join(php_root_dir, 'bin/php'), ini=os.path.join(php_root_dir, 'etc/php.ini'), )) elif php_name.startswith("vendor-"): php_root_dir = '/' php_description.append(PHPDescription( identifier=php_name, version=f'{php_name[-2]}.{php_name[-1]}', modules_dir=os.path.join(php_root_dir, 'usr/lib64/php/modules/'), dir=os.path.join(php_root_dir), bin=os.path.join(php_root_dir, 'bin/php'), ini=os.path.join(php_root_dir, 'etc/php.ini'), )) else: # unknown php, skip continue return php_description def _cast_to_vendor_name(self, name, value): return f'vendor-php{_extract_xml_value(value, "version")}-{name}'.replace('.', '') def get_unsupported_cl_features(self) -> tuple[Feature, ...]: return ( Feature.RUBY_SELECTOR, Feature.PYTHON_SELECTOR, Feature.NODEJS_SELECTOR, ) def _get_active_apache_mpm_module(self) -> Optional[AnyStr]: """ Determines active MPM module for Apache Web Server :return: apache_active_module_name apache_active_module_name: 'prefork', 'event', 'worker' """ try: # Caching httpd output and refresh it only one time in hour if time.time() - self.httpd_modules_ts > 3600: self.httpd_modules = run_command(["httpd", "-M"]) self.httpd_modules_ts = time.time() except (OSError, IOError, ExternalProgramFailed): self.httpd_modules = "" self.httpd_modules_ts = time.time() for mpm_module in self.MPM_MODULES: if f"mpm_{mpm_module}_module" in self.httpd_modules: return mpm_module return None def _get_max_request_workers_for_module(self, apache_module_name: str) \ -> Tuple[int, str]: """ Determine MaxRequestWorkers directive value for specified apache module Reads config file /etc/httpd/conf.modules.d/01-cgi.conf :param apache_module_name: Current apache's module name: 'prefork', 'event', 'worker' :return: tuple (max_req_num, message) max_req_num - Maximum request apache workers number or 0 if error message - OK/Error message """ try: return find_module_param_in_config(self.HTTPD_MPM_CONFIG, apache_module_name, 'MaxRequestWorkers', self.MPM_MODULES[apache_module_name]) except (OSError, IOError, IndexError, ValueError): return 0, format_exc() def get_apache_max_request_workers(self) -> Tuple[int, str]: """ Get current maximum request apache workers from httpd's config :return: tuple (max_req_num, message) max_req_num - Maximum request apache workers number or 0 if error message - OK/Error message """ apache_active_module = self._get_active_apache_mpm_module() if apache_active_module is None: return 0, "httpd service doesn't work or mpm modules are absent" return self._get_max_request_workers_for_module(apache_active_module) @staticmethod def get_main_username_by_uid(uid: int) -> str: """ Get "main" panel username by uid. :param uid: uid :return Username """ return get_main_username_by_uid(uid) @staticmethod def get_user_emails_list(username: str, domain: str): sql = f""" SELECT clients.email FROM clients WHERE clients.id = ( SELECT domains.cl_id FROM domains WHERE domains.name = '{domain}') """ query_result = query_sql(sql) return ','.join(item[0] for item in query_result) @staticmethod def panel_login_link(username): link = run_command(['/usr/sbin/plesk', 'login']) if not link: return '' # https://10.51.32.129/login?secret=RZ3NqTqneO0ZQgkIb-QKxyMZkvOgdAS0SGaNnAgN-nKyAYgc -> https://10.51.32.129/ parsed = urlparse(link) return f'{parsed.scheme}://{parsed.netloc}/' @staticmethod def panel_awp_link(username): link = PanelPlugin.panel_login_link(username).rstrip("/") if len(link) == 0: return '' return f'{link}/modules/plesk-lvemanager/index.php/awp/index#/' def get_customer_login(self, username): """ In some rare situations we need customer login instead of system user name. E.g. when communicating with WHMCS. This method resolves customer login by his system user name. """ sql = r"""SELECT clients.login FROM sys_users JOIN hosting ON hosting.sys_user_id=sys_users.id JOIN domains ON hosting.dom_id=domains.id AND domains.webspace_id=0 JOIN clients ON clients.id=domains.cl_id WHERE sys_users.login = %s""" customers = query_sql(sql, data=[username]) try: return customers[0][0] except IndexError as e: raise NoPanelUser(f'Unknown user {username}') from e def get_domain_login(self, username, domain): """ In some rare situations we need subscription login instead of client login. E.g. when communicating with WHMCS. This method resolves sys_users login by domain. One client can create several subscriptions Each subscription creates a new login in the sys_users table The user can create several domains for one subscription upgrade_url requires subscription login from sys_users. """ sql = r"""SELECT sys_users.login FROM sys_users JOIN hosting ON hosting.sys_user_id=sys_users.id JOIN domains ON hosting.dom_id=domains.id AND domains.webspace_id=0 AND domains.name = %s""" logins = query_sql(sql, data=[domain]) try: return logins[0][0] except IndexError as e: raise NoPanelUser(f'Unknown user for domain {domain}') from e def get_server_ip(self): sql = r""" SELECT ip_address FROM IP_Addresses WHERE main = 'true' """ ip_addresses = query_sql(sql) try: return ip_addresses[0][0] except IndexError as e: raise NotSupported( 'Unable to detect main ip for this server. ' 'Contact CloudLinux support and report the issue.' ) from e def suspended_users_list(self): """ Returns list of suspended system users suspended means domain status == 2 """ sql = r""" SELECT su.login FROM sys_users su JOIN hosting h ON su.id = h.sys_user_id JOIN domains d ON h.dom_id = d.id WHERE d.status = 2 """ suspended = query_sql(sql) return [item[0] for item in suspended]